/*
 * Dinejot marketing site styles (single file, no JavaScript, no build step).
 * Warm Kitchen brand: cream background, ink text, paprika primary, butter accent.
 *
 * SERVED THROUGH PROPSHAFT, from app/assets/stylesheets. The layout links it
 * with stylesheet_link_tag "site", so the URL carries a content digest and can
 * be cached immutably for a year. There is no manual "?v=N" to remember to bump:
 * editing this file changes the digest, which changes the URL.
 *
 * Fonts are self hosted below, from app/assets/fonts (named in
 * config/initializers/assets.rb, so each file's logical path is its bare
 * filename and Propshaft rewrites the url() references here to fingerprinted
 * ones at compile time). Fraunces and Inter are both licensed under the SIL Open
 * Font License 1.1 (OFL); the full license for each ships alongside the fonts.
 *   Fraunces: Copyright 2018 The Fraunces Project Authors.
 *   Inter:    Copyright 2020 The Inter Project Authors.
 *
 * WOFF2, subset to Latin and Latin Extended plus the handful of symbols this
 * site renders (the breadcrumb chevron, the play triangle, the admin arrows).
 * The four TrueType originals were 831 KB together and the four woff2 files are
 * 123 KB, which is the single largest byte saving available on any page here.
 * The catalog is pure ASCII and the published locales are Latin script, so
 * nothing on the site falls outside the subset. Regenerate with pyftsubset if a
 * non Latin locale is ever published; see docs.
 *
 * font-display: swap on all four. Text paints immediately in the fallback and
 * reflows to the webfont when it arrives, so a slow font never leaves the page
 * blank. The fallback stacks in --body-font and --display-font are metrically
 * close enough that the swap does not shove the layout around.
 */

/* THE DISPLAY CUT, AND IT IS NOT AN APPROXIMATION. The Warm Cinema laws ask for
   Fraunces at weight 200 with the optical size axis at 144, which is what gives
   the face its fine hairlines and tight joints at 110px. Rendering that role in
   the 400 cut below was measurably a different typeface: 231 vs 152 units of
   stem on a lowercase l (1.51x thicker) and 9582 vs 7312 units across "What to
   cook today?" (1.31x wider), which moves where a 12ch hero question breaks.

   A STATIC INSTANCE, NOT THE VARIABLE FONT. Google's full-axis Fraunces variable
   is ~226 KB at this site's coverage and would put four axes of machinery on the
   render path for two selectors. This file is `fontTools.varLib.instancer` on
   that variable pinned to wght 200 / opsz 144 / SOFT 0 / WONK 1, then subset to
   the same 580 codepoints and the same features (liga, kern) as the two cuts
   below, flavoured woff2: 23,976 bytes, SMALLER than either of them, because
   opsz 144 outlines are finer and 200 is lighter than 400.

   Used by .hero__title and .billboard__title only, which are the two roles the
   laws call display. Section headings stay on the 400 cut: the drawing asks 300
   for those and 400 is the nearer of the two weights this site self hosts.
   Regenerate the same way if the codepoint coverage ever widens. */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url("/assets/Fraunces_200ExtraLight-3bc7cb5d.woff2") format("woff2");
}
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/Fraunces_400Regular-cba13579.woff2") format("woff2");
}
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/Fraunces_600SemiBold-b31fc77f.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/Inter_400Regular-4facb4c4.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/Inter_600SemiBold-1416da2f.woff2") format("woff2");
}

/* THE WARM CINEMA TOKENS. The canonical design is
   docs/superpowers/design/2026-08-09-warm-cinema-v2.html, and where that file
   and any older note here disagree on a visual matter the file wins.

   Three hue families and nothing else: warm neutral around 34deg, paprika
   around 12deg, one sage for a dietary mark. Text is never pure black and the
   ground is never pure white, which is what keeps a cream page from glaring.

   THE ESPRESSO HALF IS A SECOND GROUND, NOT A SECOND COMPONENT SET. --night*
   and --lit* are the cinema band the shelf lives in; every component below
   reads the SEMANTIC variables (--bg, --fg, --hair, --focus) set on `body` and
   overridden once on `.cinema`, so one class turns a whole band dark and no
   card, chip or row needs a dark variant of its own.

   THE PRE-CINEMA TOKENS STAY. --paper, --ink-soft, --line, --butter, --radius
   and --wrap are read by the recipe pages, the hubs, search, the chef pages and
   the admin chrome, none of which Task R reskins. Deleting them here would
   restyle two dozen pages nobody asked about. --cream, --cream-2, --ink and
   --paprika are the four the v2 palette lands on directly, and the shift is
   small enough (a warmer cream, a greyer second cream, a slightly cooler ink)
   that the pages that inherit it read as the same site. */
:root {
  --cream: #f6f1e7;
  --cream-2: #efe9db;
  --cream-3: #e6ddca;
  --paper: #fffaf0;
  --ink: #221f1a;
  --ink-2: #4b4438;
  --ink-3: #7b7263;
  --ink-4: #9e9484;
  --ink-soft: #6b5647;
  --paprika: #d9482b;
  --paprika-dark: #c13a1c;
  --paprika-deep: #b23a21;
  --butter: #f0c948;
  --line: #e6d8c1;
  --rule: #dcd2be;
  --rule-soft: #e9e1d1;
  --night: #231a13;
  --night-2: #2e231a;
  --night-3: #3b2c21;
  --night-rule: #4a382a;
  --lit: #f3ebdd;
  --lit-2: #cbbca6;
  --lit-3: #9b8b76;
  --sage: #6e7a5e;

  /* Component gaps 8 to 24, section rhythm 96 to 128. A 16:1 ratio between the
     two is what stops a page reading as one undifferentiated scroll. */
  --s1: 8px;
  --s2: 12px;
  --s3: 16px;
  --s4: 24px;
  --s5: 32px;
  --s6: 48px;
  --sec: 128px;
  --sec-2: 96px;

  /* Exactly three radii site wide. 4px is content and chrome; the pill is for
     chips, icon circles and dots only; 18px is the pre-cinema card radius the
     unreskinned pages still use. */
  --r: 4px;
  --r-chip: 999px;
  --radius: 18px;

  /* SHELF CARDS ARE WHOLE PAGE COLUMNS, which is the only reason a row of them
     lines up with the headings above it. Twelve columns inside --cw; a dish
     card spans three of them and the wide variant four, and the track bleeds
     off the right edge of the viewport by starting and ending at --edge. */
  --wrap: 1080px;
  --maxw: 1296px;
  --gutter: clamp(20px, 5vw, 72px);
  --gap: 24px;
  --cw: min(var(--maxw), 100vw - var(--gutter) * 2);
  --edge: calc((100vw - var(--cw)) / 2);
  --col: calc((var(--cw) - 11 * var(--gap)) / 12);
  --card3: calc(var(--col) * 3 + var(--gap) * 2);
  --card4: calc(var(--col) * 4 + var(--gap) * 3);
  --card5: calc(var(--col) * 5 + var(--gap) * 4);
  --card6: calc(var(--col) * 6 + var(--gap) * 5);
  --dish: var(--card3);
  --wide: var(--card4);

  /* Named curves only. Nothing on this site runs on bare `ease`. */
  --e-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
  --e-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
  --e-in-out: cubic-bezier(0.66, 0, 0.1, 1);
  --t-quick: 0.15s;
  --t-reg: 0.22s;
  --t-image: 0.7s;

  /* Nothing interactive is under this, either in the shape that is drawn or in
     the invisible expander sitting behind it. */
  --tap: 44px;

  --body-font: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  --display-font: "Fraunces", Georgia, "Times New Roman", serif;
}

/* One fewer card per row before the cards themselves get narrow. */
@media (max-width: 1180px) {
  :root {
    --dish: var(--card4);
    --wide: var(--card5);
  }
}

/* A PHONE GETS ITS OWN COLUMN MATH, not the desktop grid squeezed. Six columns
   inside a 342px measure at 390px: a dish card spans three of them (165px), so
   two are on screen with the third peeking past the gutter, and the wide
   variant spans four (224px). Twelve columns at this width would give a 70px
   card, which is a thumbnail rather than a plate. */
@media (max-width: 700px) {
  :root {
    --gutter: 24px;
    --gap: 12px;
    --sec: 64px;
    --sec-2: 64px;
    --col: calc((var(--cw) - 5 * var(--gap)) / 6);
    --dish: calc(var(--col) * 3 + var(--gap) * 2);
    --wide: calc(var(--col) * 4 + var(--gap) * 3);
  }
}

/* THE SEMANTIC SURFACE. Read these, never the palette above, in anything that
   can appear on both grounds. */
body {
  --bg: var(--cream);
  --fg: var(--ink);
  --fg2: var(--ink-2);
  --fg3: var(--ink-3);
  --fg4: var(--ink-4);
  --hair: var(--rule);
  --hair-soft: var(--rule-soft);
  --well: var(--cream-3);
  --focus: var(--paprika);
}
.cinema {
  --bg: var(--night);
  --fg: var(--lit);
  --fg2: var(--lit-2);
  --fg3: var(--lit-3);
  --fg4: var(--lit-3);
  --hair: var(--night-rule);
  --hair-soft: #3a2b20;
  --well: var(--night-2);
  --focus: var(--lit);
  background: var(--night);
  color: var(--lit);
}

/* GRAIN AS A TILED RASTER, never a turbulence filter: an SVG filter is a
   per-paint GPU cost on a full page band and no premium site ships one. One
   128px greyscale PNG, cached for a year by Propshaft's digest, multiplied over
   the cream and overlaid on the espresso so the same tile reads on both. */
.grained {
  position: relative;
  isolation: isolate;
}
.grained::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  background-image: url("/assets/grain-51414511.png");
  background-size: 128px 128px;
  background-repeat: repeat;
  opacity: 0.05;
  mix-blend-mode: multiply;
}
.cinema.grained::after {
  opacity: 0.055;
  mix-blend-mode: overlay;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Defensive only. Every row below is built to fit; this keeps a future
     overlong word from dragging the whole document wide on a phone. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body-font);
  font-size: 18px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  font-family: var(--display-font);
  font-weight: 600;
  line-height: 1.12;
  margin: 0 0 0.4em;
  letter-spacing: -0.01em;
}

p {
  margin: 0 0 1em;
}

a {
  color: var(--paprika-dark);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  color: var(--paprika);
}

sup a {
  text-decoration: none;
  font-size: 0.7em;
  padding-left: 2px;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 24px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: #fff;
  padding: 10px 16px;
  border-radius: 0 0 12px 0;
  z-index: 20;
}
.skip-link:focus {
  left: 0;
}

/* ONE FOCUS TREATMENT, TWO COLOURS: paprika on the cream page, cream inside the
   espresso band. --focus is set on `body` and overridden once on `.cinema`, so
   a ring never has to be redeclared per component and can never be drawn in a
   colour the surface behind it swallows. Nothing anywhere sets `outline: none`
   without drawing something in its place. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Flash messages, rendered at the top of <main> by the pages layout. Same two
   tones the admin layout uses (butter for a notice, paprika for an alert) so a
   message reads the same wherever a signed-in person meets one. The colour is
   the quiet half of the signal: the loud half is the role and the visually
   hidden word in front of the text, which is what a screen reader gets.
   padding-block/padding-inline rather than the shorthand, because these sit
   inside a `.wrap` whose horizontal gutter a shorthand would flatten. */
.flash {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--ink);
  font-weight: 600;
  line-height: 1.5;
  margin: 20px 0 0;
  padding-block: 12px;
  padding-inline: 16px;
}
.flash--notice {
  background: color-mix(in srgb, var(--butter) 24%, var(--paper));
  border-color: var(--butter);
}
.flash--alert {
  background: color-mix(in srgb, var(--paprika) 8%, var(--paper));
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
/* THE WAY BACK, when a notice is about something that can be undone. A form
   rather than a link, because undoing writes; inline in the sentence, because a
   button under it would make a one line notice a two line box. The same words go
   on to the toast strip as .toast__action when the bridge takes the message. */
.flash__action-form {
  display: inline;
  margin: 0 0 0 8px;
}
.flash__action {
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* Header */
.site-header {
  border-bottom: 1px solid var(--line);
  /* Nearly opaque: at 88% the hero headline ghosted through the sticky bar as
     the page scrolled under it. */
  background: color-mix(in srgb, var(--cream) 96%, transparent);
  position: sticky;
  top: 0;
  z-index: 10;
  backdrop-filter: saturate(1.2) blur(6px);
}
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 64px;
  gap: 12px;
}
.site-header__inner > * {
  min-width: 0;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--ink);
  text-decoration: none;
}
.wordmark__dot {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  display: inline-block;
  background: url("/assets/brand-icon-661b4245.svg") center / cover no-repeat;
}
.site-nav {
  display: flex;
  align-items: center;
  gap: 22px;
  font-size: 0.98rem;
  font-weight: 600;
  flex-shrink: 0;
}
.site-nav a {
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}
.site-nav a:hover {
  color: var(--paprika);
}

/* THE HAMBURGER, and the one number this whole feature turns on.

   The section links collapse below 600px because five of them plus a user menu
   are wider than a phone. For a long time that was the end of the sentence, and
   what collapsed was simply gone: Features and Support were caught by the
   footer, and Meal planning, Recipes and Search were caught by nothing at all.
   So the SAME query that hides the links draws the button that opens the drawer
   holding them. One number, one place: the button and the links it stands in
   for cannot disagree about when either exists.

   The button is `display: none` by default rather than shown and then hidden,
   so a stylesheet that failed to load leaves a header with real links in it
   rather than a header with a button no script is running behind. */
.site-nav__menu {
  display: none;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}
.site-nav__menu:hover {
  color: var(--paprika);
}
.site-nav__menu:focus-visible {
  outline: 3px solid var(--paprika);
  outline-offset: 2px;
}

@media (max-width: 599px) {
  .site-nav__link {
    display: none;
  }
  .site-nav__menu {
    display: inline-flex;
  }
  /* Three things now share a phone's header row rather than two, so the row
     tightens: 22px of gap between a wordmark, an account trigger and a 44px
     button overflowed 390px once a name of any length was in the trigger. */
  .site-nav {
    gap: 10px;
  }
  .user-menu__trigger {
    max-width: 38vw;
  }
  .wordmark {
    font-size: 1.15rem;
  }
}

/* Signed-in user menu. A NATIVE <details> disclosure: this site ships no
   JavaScript, so the browser's own open/close drives the dropdown (click the
   trigger to toggle, click away or press Escape to close). Like Sign in it never
   collapses, so it stays reachable at every width. Built on the Warm Kitchen
   tokens; the site has one intentional light palette (the header is always
   cream, in OS light and dark alike), so the menu matches it in both. */
.user-menu {
  position: relative;
  flex-shrink: 0;
}
.user-menu__trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  max-width: 46vw;
  /* Strip the browser's default disclosure triangle; the caret span replaces it. */
  list-style: none;
}
.user-menu__trigger::-webkit-details-marker {
  display: none;
}
.user-menu__trigger:hover {
  border-color: var(--paprika);
}
.user-menu__trigger:focus-visible {
  outline: 3px solid var(--paprika);
  outline-offset: 2px;
}
.user-menu__avatar {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--paprika);
  color: #fff;
}
.user-menu__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.user-menu__initial {
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1;
}
.user-menu__name {
  font-weight: 600;
  font-size: 0.95rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-menu__caret {
  flex-shrink: 0;
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}
.user-menu[open] .user-menu__caret {
  transform: rotate(225deg);
}
.user-menu__panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 20;
  min-width: 184px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 12px 28px rgba(43, 29, 22, 0.16);
}
/* button_to wraps the sign-out button in a form; keep it flush in the panel. */
.user-menu__form {
  margin: 0;
}
.user-menu__item {
  display: block;
  width: 100%;
  padding: 10px 12px;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.user-menu__item:hover,
.user-menu__item:focus-visible {
  background: var(--cream-2);
  color: var(--paprika-dark);
}
.user-menu__signout {
  color: var(--paprika-dark);
}
/* Admin section inside the user menu (admins only). */
.user-menu__divider {
  height: 1px;
  margin: 4px 6px;
  background: var(--line);
}
.user-menu__label {
  padding: 4px 12px 2px;
  color: var(--ink-soft);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
@media (prefers-reduced-motion: reduce) {
  .user-menu__caret {
    transition: none;
  }
}

/* ---- The nav drawer -------------------------------------------------------
   The phone's copy of the header, as a right hand sheet.

   A NATIVE <dialog>, so everything below is paint. The focus trap, the Escape
   key, the inert background and the top layer are the browser's, bought by
   showModal(); the sign-in dialog and the planner's swap sheet are drawn on the
   same footing.

   THE UA STYLESHEET HAS TO BE ARGUED WITH, once. A <dialog> ships centred, with
   its own margins, a border, and max-width/max-height that stop short of the
   viewport. A sheet is the opposite of all four, so inset, margin, border and
   both maxima are set here rather than inherited.

   NOT BREAKPOINTED, and that is deliberate. The BUTTON is what belongs to
   narrow screens; the sheet itself is just the menu, and a phone rotated into
   landscape mid-drawer would otherwise be a grey backdrop with nothing under
   it. Left alone, the same drawer keeps working at any width it is opened at.

   Warm Cinema on cream: one ground, hairline rows, and Fraunces on the wordmark
   alone. The site has one intentional light palette (the header is cream in OS
   light and dark alike), so this matches it in both. */
.nav-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(320px, 86vw);
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--line);
  background: var(--cream);
  color: var(--ink);
  overflow: hidden;
}
.nav-drawer[open] {
  display: flex;
  flex-direction: column;
}
.nav-drawer::backdrop {
  background: rgba(34, 31, 26, 0.5);
}

/* The slide, in the stylesheet rather than in the controller, so the reduced
   motion query below is asked where it lives instead of depending on a script
   remembering to ask it. 250ms, the same length the account menu's hover grace
   uses, on the curve the rest of the site enters with. */
@keyframes nav-drawer-in {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}
@keyframes nav-drawer-veil {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.nav-drawer[open] {
  animation: nav-drawer-in 250ms cubic-bezier(0.2, 0, 0, 1);
}
.nav-drawer[open]::backdrop {
  animation: nav-drawer-veil 250ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .nav-drawer[open],
  .nav-drawer[open]::backdrop {
    animation: none;
  }
}

.nav-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex: none;
  padding: 8px 8px 8px 20px;
  border-bottom: 1px solid var(--line);
}
.nav-drawer__title {
  margin: 0;
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.15rem;
  color: var(--ink);
}
.nav-drawer__close {
  flex: none;
  width: var(--tap);
  height: var(--tap);
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}
.nav-drawer__close:hover {
  color: var(--ink);
}

/* The rows scroll and the head does not: an admin signs in to eleven of these
   and a short phone in landscape is 390px tall. */
.nav-drawer__nav {
  flex: 1 1 auto;
  overflow-y: auto;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.nav-drawer__row {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap);
  padding: 12px 20px;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  font-size: 1rem;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.nav-drawer__row:first-child {
  border-top: 0;
}
.nav-drawer__row:hover,
.nav-drawer__row:focus-visible {
  background: var(--cream-2);
  color: var(--paprika-dark);
}
.nav-drawer__row--signin,
.nav-drawer__row--signout {
  color: var(--paprika-dark);
}
.nav-drawer__label {
  margin: 0;
  padding: 18px 20px 6px;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
/* A label already drew the rule that separates the group, so the row under it
   does not draw a second one two pixels away. */
.nav-drawer__label + .nav-drawer__row {
  border-top: 0;
}
/* button_to wraps sign out in a form; keep it flush with the rows around it. */
.nav-drawer__form {
  margin: 0;
}

/* ==========================================================================
   THE WARM CINEMA HOMEPAGE
   Cream opens the page. The shelf lives in a warm dark cinema band. Cooking
   returns to daylight. Canonical drawing:
   docs/superpowers/design/2026-08-09-warm-cinema-v2.html.
   ========================================================================== */

/* --- Type roles -----------------------------------------------------------
   FRAUNCES CARRIES DISPLAY AND EDITORIAL BODY; INTER IS INTERFACE CHROME ONLY.

   THE TWO DISPLAY ROLES ARE THE DRAWING'S OWN FACE, exactly: weight 200 at
   optical size 144, self hosted as a static instance (declared with the other
   faces at the top of this file). The section roles below them ask for 300 in the
   drawing and render in the 400 cut, which is the nearer of the two weights
   this site ships; a wght 300 / opsz 96 instance is the follow up if the
   screenshot pass wants that step too.

   Zero or positive tracking at every size, never negative, and line-height
   under 1.0 on the display so the two lines of the question lock together. */
.hero__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(44px, 7.6vw, 110px);
  line-height: 0.9;
  letter-spacing: 0.005em;
  text-wrap: balance;
  max-width: 12ch;
  margin: 0;
  color: var(--fg);
}
.h2 {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(28px, 3.6vw, 52px);
  line-height: 1.08;
  letter-spacing: 0.005em;
  text-wrap: balance;
  max-width: 20ch;
  margin: 0;
}
.lede {
  font-family: var(--display-font);
  font-size: clamp(18px, 1.6vw, 21px);
  line-height: 1.5;
  max-width: 42ch;
  text-wrap: pretty;
  color: var(--fg2);
  margin: 0;
}
.body-tight {
  font-family: var(--display-font);
  font-size: 19px;
  line-height: 1.55;
  max-width: 44ch;
  text-wrap: pretty;
  color: var(--fg2);
  margin: 0;
}
/* Inter, uppercase, 11px: labels, kickers and metadata. */
.label {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0;
  max-width: 40ch;
}
.meta {
  font-family: var(--body-font);
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--fg3);
  line-height: 1.4;
  max-width: 44ch;
  margin: 0;
}

/* The page's own measure, which is WIDER than .wrap: shelf cards are page
   columns, and twelve of them need 1296px to land on 306px cards at 1440. */
.container {
  width: var(--cw);
  margin-inline: auto;
}

/* --- Hero -----------------------------------------------------------------
   THE PORTRAIT IS A CONTAINED OBJECT, NOT A CROP OFF THE VIEWPORT. It used to
   run off the right edge: the column carried the gutter inside it and the page
   itself cut the photograph. On both widths that reads as a picture stuck to
   the edge with a piece missing rather than as a deliberate bleed, so the
   column now SPENDS --edge as padding instead of filling it. The track keeps
   exactly the width it had, so the text column still lands near 830px at 1440,
   inside the 860px measure ceiling; what changed is that the photograph stops
   at the container's right edge with the page's own gutter beside it.

   BEING A CONTAINED OBJECT IS WHAT EARNS IT A RADIUS. A crop wants no corner,
   because the page is the frame; a thing sitting inside the measure takes the
   site's 4px content radius like every other framed photograph here.

   The portrait is still capped under 58vh so the question stays the one
   oversized thing in the first screen. */
.hero {
  background: var(--cream);
  padding-block: var(--s6) var(--sec-2);
}
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) calc(var(--card4) + var(--edge));
  gap: calc(var(--gap) * 2);
  align-items: end;
  padding-left: var(--edge);
}
/* A catalog with no photographed dish in it renders no portrait, and a reserved
   empty column would leave the question sitting in half a page for no reason. */
.hero-grid:not(:has(.hero-media)) {
  grid-template-columns: minmax(0, 1fr);
}
/* Inside the h1, above the question. It names the product in the words people
   search for while the question keeps the display size, so the one h1 on the
   page reads as a kicker and a headline rather than as one very long title. */
.hero__kicker {
  display: block;
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 20px;
  line-height: 1.4;
}
.hero__answer {
  margin: 32px 0 0;
}
.hero__meta {
  margin-top: 20px;
}

/* The portrait. 4:5, the content radius, no border. It is a <figure>, whose UA
   margin would otherwise push it out of its grid column.

   THE INSET IS ON THE FIGURE, NOT ON THE FRAME, so the photograph and the line
   of type under it end on the same vertical: one declaration holds the right
   edge of both, and the caption cannot drift off it later. */
.hero-media {
  position: relative;
  margin: 0;
  padding-right: var(--edge);
}
.hero-media__frame {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--cream-3);
  max-height: min(58vh, 620px);
}
.hero-media__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.hero-media__caption {
  margin-top: 14px;
}

/* --- The search field -----------------------------------------------------
   ONE OBJECT, NOT THREE CONTROLS IN A ROW. The page asks one question and
   offers one action, so which kitchen, what you are looking for and the paprika
   are segments of a single field divided by the same hairline that structures
   the rest of the page. It spans six of the twelve page columns.

   The class names are the ones the markup shipped with; what changed is the
   drawing. .answer-search .answer-search__submit stays two classes deep
   because .btn-cta is declared later in this file and would otherwise win
   every property they share. */
.answer-search {
  margin: var(--s6) 0 0;
  max-width: var(--card6);
}
.answer-search__row {
  display: flex;
  align-items: stretch;
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--cream-2);
}
.answer-search__row:focus-within {
  border-color: var(--ink);
}
/* The country lens: a segment of the field, not a control beside it. It opens
   the chip rail below and shows whichever kitchen the shelf is currently
   reading, which is the same choice the chips write (see
   region_row_controller.js and the dinejot.country store). */
.answer-search__lens {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 17px 0 19px;
  border: 0;
  border-right: 1px solid var(--rule);
  background: none;
  flex: 0 0 auto;
  text-align: left;
  cursor: pointer;
  min-height: var(--tap);
  color: inherit;
  font: inherit;
}
.answer-search__lens-key {
  display: block;
  font-family: var(--body-font);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: 4px;
}
.answer-search__lens-value {
  display: block;
  font-family: var(--body-font);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.004em;
  color: var(--ink-2);
  line-height: 1;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.answer-search__caret {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  color: var(--ink-4);
}
.answer-search__input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 20px;
  line-height: 1.2;
  font-family: var(--body-font);
  font-size: 16.5px;
  letter-spacing: 0.004em;
  color: var(--ink);
  background: none;
  border: 0;
}
.answer-search__input::placeholder {
  color: var(--ink-4);
}
/* Drawn INSIDE the field, so the ring never straddles the hairline that divides
   the segments. */
.answer-search__input:focus-visible {
  outline: 2px solid var(--paprika);
  outline-offset: -5px;
}
/* The page's one filled element, and the only paprika in this viewport.
   TWO CLASSES DEEP, AND THAT IS THE WHOLE POINT. `.btn-cta` is declared eight
   hundred lines BELOW this rule, so at one class each the cascade goes to the
   later rule and every declaration here that names the same property loses. It
   shipped that way for one commit: a 320px rectangle wrapped onto its own line
   under the field, at every width. Specificity is what fixes it, rather than
   moving this block down beside a button rule it has nothing else to do with.
   The shadow and the hover are restated because .btn-cta's own are drawn for a
   button standing alone on a cream page, not for a segment of a field. */
.answer-search .answer-search__submit {
  flex: 0 0 auto;
  width: auto;
  max-width: none;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 26px;
  border: 0;
  border-radius: 0 calc(var(--r) - 1px) calc(var(--r) - 1px) 0;
  background: var(--paprika);
  color: var(--cream);
  font-family: var(--body-font);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  box-shadow: none;
  transition: background-color var(--t-quick) var(--e-out-cubic);
}
.answer-search .answer-search__submit:hover {
  background: var(--paprika-deep);
}
@media (prefers-reduced-motion: reduce) {
  .answer-search .answer-search__submit {
    transition: none;
  }
}

/* --- The country chips ----------------------------------------------------
   THE LENS, SHOWN OPEN. Pressing one changes the band's lead below without
   leaving the page; with no JavaScript the same press is a link to that
   country's page, which is what the markup says on its own.

   A SCROLLING RAIL, TWO CHIPS TALL (owner, 2026-08-10), and the overflow is
   the rail's own, never the document's: twenty three countries laid out flat
   would drag the whole page sideways. Column flow stacks the rail two high at
   every width, so twice the countries sit in the first screenful before
   anybody scrolls; grid-auto-columns: max-content keeps each pill its own
   width instead of stretching to its column partner's. The mask on the right
   edge is the peek that says the rail continues. */
.country-chips {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(2, auto);
  grid-auto-columns: max-content;
  justify-items: start;
  gap: 10px;
  margin-top: 14px;
  padding-block: 6px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 48px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 48px), transparent 100%);
}
.country-chips::-webkit-scrollbar {
  display: none;
}
/* The rail's third row, outside the rail: the all-communities chip stays put
   while the countries glide, so the way out never has to be scrolled to. */
.country-chips__all {
  margin: 10px 0 0;
}
.country-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  scroll-snap-align: start;
  padding: 9px 16px;
  border: 1px solid var(--hair);
  border-radius: var(--r-chip);
  background: none;
  color: var(--fg2);
  font-family: var(--body-font);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.012em;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--t-quick) var(--e-out-cubic),
    background-color var(--t-quick) var(--e-out-cubic),
    border-color var(--t-quick) var(--e-out-cubic);
}
/* A CHIP IS DRAWN AT 31px AND TAPPED AT 44. The shape stays the size the design
   asks for and an invisible expander carries the target, rather than the pill
   being inflated to suit a finger. */
.country-chip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--tap);
  transform: translateY(-50%);
}
.country-chip:hover {
  border-color: var(--fg3);
  color: var(--fg);
}
.country-chip__flag {
  font-size: 1rem;
  line-height: 1;
}
/* The way out of the rail rather than another country in it. */
.country-chip--all {
  color: var(--fg);
}
/* The country whose lead is on the shelf right now, set by region_row_controller
   after a press and again on the next visit. FILLED, not outlined: a chip that
   only changed its border would read as the hover state one is already wearing.
   Restated with :hover at equal specificity so the pointer cannot paint ink over
   ink. Only the visitor's own choice is ever lit; a country read off their
   address is a guess, and the controller marks nothing for it. */
.country-chip--selected,
.country-chip--selected:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
}

/* --- The cinema band ------------------------------------------------------
   The shelf's room. Espresso, grained, and it is the only dark surface on the
   site: everything inside it reads the semantic surface variables, so no card,
   chip or row carries a dark variant of its own. */
.shelf-band {
  background: var(--night);
}
.shelf-band__inner {
  padding-block: var(--sec) 0;
}
.band-head {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  align-items: end;
  padding-bottom: var(--sec-2);
}
.band-head__side {
  justify-self: end;
  max-width: 40ch;
}
.band-head__title {
  margin-top: 20px;
  color: var(--lit);
}
/* The daypart kicker. The dot is the one paprika mark in the band's first
   screen, and it is six pixels wide. */
.daypart {
  display: flex;
  align-items: center;
  gap: 10px;
}
.daypart__dot {
  width: 6px;
  height: 6px;
  border-radius: var(--r-chip);
  background: var(--paprika);
  flex: 0 0 auto;
}
/* The swappable band: the kicker, the first shelf, the billboard AND every row
   under them. The server renders the global one inside the page's fragment
   cache and region_row_controller replaces this element's contents with the
   visitor's own country and daypart. It wraps the whole band rather than its
   lead because country first has to mean every row: while it held the lead
   alone, a visitor in Mumbai read their own kitchen's kicker and then somebody
   else's country row directly underneath it.

   A HANDLE, not a box: it must not add a line to the layout, or the band would
   shift when the swap lands. The rows inside carry their own handle for the same
   reason, so a spec can tell the lead's rows from the band's without either
   wrapper becoming something the design has to reckon with. */
.band-swap,
.band-rows {
  display: contents;
}
/* The line that closes the band. Only a bottom padding: the last row above it
   already carries the section rhythm as its own bottom margin, and adding a
   second would put a quarter of a screen of espresso between them. Quieter than
   the band's body text, because it is a note about the mechanism rather than
   part of the offer. */
.band-foot {
  padding-bottom: var(--sec-2);
}
.band-foot .body-tight {
  color: var(--lit-3);
}

/* --- Return to daylight ---------------------------------------------------- */
.return {
  background: var(--cream);
  padding-block: var(--sec-2);
}
.return__split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
  align-items: start;
  margin-top: var(--sec-2);
}
.return__title {
  margin-top: 20px;
}
.return__body {
  font-family: var(--display-font);
  font-size: 19px;
  line-height: 1.55;
  max-width: 44ch;
  color: var(--ink-2);
  margin: 0;
}
.return__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  margin-top: var(--s5);
  padding: 13px 22px;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  color: var(--ink);
  font-family: var(--body-font);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1;
  text-decoration: none;
  transition: border-color var(--t-quick) var(--e-out-cubic), color var(--t-quick) var(--e-out-cubic);
}
.return__link:hover {
  border-color: var(--ink-3);
  color: var(--ink);
}

/* --- The steam curl -------------------------------------------------------
   ONE DRAWN MARK, PLACED EXACTLY TWICE on the homepage: rising off the hero
   dish, and again as the mark that returns the page from the cinema to
   daylight. The line thins to nothing as it rises, and that is an ALPHA MASK
   rather than a stroke gradient, so the colour is set by whichever element
   carries it and the same partial serves a cream curl and an ink one. */
.steam {
  display: block;
  overflow: visible;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.7;
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 16%, transparent 96%);
  mask-image: linear-gradient(to top, #000 0%, #000 16%, transparent 96%);
}
/* Inside the frame and cream, so it reads as steam coming off the dish rather
   than as ink drawn beside it. */
.steam--hero {
  position: absolute;
  left: 15%;
  top: 3%;
  width: 66px;
  height: 188px;
  z-index: 2;
  color: var(--lit);
  opacity: 0.62;
}
.steam--mark {
  width: 58px;
  height: 152px;
  margin-inline: auto;
  color: var(--ink-4);
  stroke-width: 1.5;
}

/* Phone frame: a bezel drawn in CSS around a plain image, so the product art
   costs one file and no JavaScript. The image carries its own width/height, so
   the box is reserved before it loads. */
.phone {
  width: clamp(210px, 58vw, 300px);
  padding: 8px;
  background: var(--night);
  border: 1px solid var(--night-3);
  border-radius: 34px;
}
.phone__screen {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 26px;
  background: var(--cream);
}

/* Narrow: one column everywhere, and the hero portrait is held by the page's
   own gutter on BOTH sides. It used to be pulled out to the right edge from
   here with a negative margin; a phone is where the cut read worst, because
   there is no page left over beside the photograph to explain it as a bleed. */
@media (max-width: 900px) {
  .hero {
    padding-inline: var(--gutter);
  }
  .hero-grid {
    /* minmax(0, 1fr), never bare 1fr: 1fr's min is min-content, and the
       nowrap chips rail's min-content is all twenty three countries laid out
       flat, which drags the text column past the viewport's right edge. The
       desktop track above carries the same guard for the same reason. */
    grid-template-columns: minmax(0, 1fr);
    padding-left: 0;
  }
  .hero-media {
    /* .hero already holds the gutter on both sides here, and --edge resolves to
       that same gutter once --cw is the viewport minus it, so the desktop
       column's own inset would draw the right margin twice. */
    padding-right: 0;
  }
  .hero-media__frame {
    /* Clamped, not freed: unclamped, 4:5 at full phone width is most of the
       first screen, and the question stops being the one oversized thing.
       object-fit: cover turns the clamp into a wider crop, not a squash. */
    max-height: min(46vh, 400px);
  }
  .band-head,
  .return__split {
    grid-template-columns: 1fr;
  }
  .band-head {
    padding-bottom: var(--s6);
  }
  .band-head__side {
    justify-self: start;
  }
  .return__split {
    gap: var(--s6);
  }
}
@media (max-width: 560px) {
  .answer-search__lens-key {
    display: none;
  }
  .answer-search__input {
    padding: 16px 14px;
    font-size: 15px;
  }
  .answer-search .answer-search__submit {
    padding: 0 16px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .country-chip,
  .return__link {
    transition: none;
  }
}

/* Store badges: inert placeholders until the apps ship, so they are drawn as
   dashed outlines in soft ink. Nothing here looks like the solid button that
   actually does something. */
.store-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 26px;
}
.store-badges--center {
  justify-content: center;
}
.store-badge {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  padding: 9px 18px;
  min-width: 168px;
  background: transparent;
  color: var(--ink-soft);
  border-radius: 12px;
  border: 1px dashed color-mix(in srgb, var(--ink-soft) 45%, transparent);
  line-height: 1.2;
  cursor: default;
  user-select: none;
}
/* The live badge. Solid and tappable so the difference from the dashed
   "coming soon" placeholder beside it is obvious at a glance. It stays ink
   rather than paprika: "Continue with Google" is the one primary action in the
   hero, and two solid paprika targets would compete. */
a.store-badge--live {
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  cursor: pointer;
  text-decoration: none;
  user-select: auto;
  transition: background 0.15s ease, transform 0.15s ease;
}
a.store-badge--live:hover {
  background: color-mix(in srgb, var(--ink) 86%, #fff);
}
a.store-badge--live:active {
  transform: translateY(1px);
}
/* The global focus ring rounds to 4px; match the badge so the ring hugs it. */
a.store-badge--live:focus-visible {
  border-radius: 12px;
}
@media (prefers-reduced-motion: reduce) {
  a.store-badge--live {
    transition: none;
  }
  a.store-badge--live:active {
    transform: none;
  }
}

.store-badge__kicker {
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.9;
}
.store-badge__name {
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.15rem;
}

/* --- Cream sections below the band ----------------------------------------
   ONE GROUND, NOT ALTERNATING BANDS. The page used to run cream, cream-2,
   cream, cream-2 with a hairline opening each, which is how a page reads as a
   stack of pitches. The band is the contrast now: everything below it returns
   to one daylight surface, and the rhythm comes from the 96 to 128px section
   padding instead of from a change of paint. */
.section-title {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(28px, 3.6vw, 44px);
  line-height: 1.08;
  letter-spacing: 0.005em;
  text-wrap: balance;
  margin: 0 0 var(--s6);
}

.features {
  background: var(--cream);
  padding-block: var(--sec-2);
}

/* THE APP STORY. The product shot, the one line claim and the store badges are
   the same three things they always were, and they sit here rather than at the
   top of the page because the top belongs to the question. The six feature
   cards below them are untouched markup. */
.app-story__intro {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s6);
  align-items: center;
  margin-bottom: var(--sec-2);
}
/* The art follows the copy at every width. On a phone a full height phone frame
   ahead of the words pushed the badges past the fold of this band, so the claim
   and the one live action come first and the product sits just under them; on
   desktop the same order puts the frame in the right hand column. */
.app-story__art {
  order: 1;
  display: flex;
  justify-content: center;
}
.app-story__lede {
  font-family: var(--display-font);
  font-size: clamp(18px, 1.6vw, 21px);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 42ch;
}
.app-story__note {
  color: var(--ink-3);
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  margin-top: 16px;
}

/* THE SIX CARDS ARE NO LONGER CARDS. A grid of bordered, shadowed, rounded
   boxes on a cream page is the look the design brief threw out; a hairline over
   each claim does the same grouping work with none of the chrome, and it is the
   same hairline that divides the shelf headings and the footer columns. */
.feature-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s6) var(--gap);
}
.feature {
  border-top: 1px solid var(--rule-soft);
  padding-top: var(--s4);
}
.feature__title {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 23px;
  line-height: 1.25;
  letter-spacing: 0.006em;
  color: var(--ink);
  margin: 0 0 var(--s2);
  text-wrap: balance;
}
.feature p {
  color: var(--ink-2);
  font-family: var(--display-font);
  font-size: 17px;
  line-height: 1.55;
  text-wrap: pretty;
  margin-bottom: 0;
}

/* The hub shortlist: the browse links a crawler builds a site out of. Kept and
   made quiet, because six rows of real dishes sit above it now. */
.hub-shortlist {
  background: var(--cream);
  padding-block: 0 var(--sec-2);
}
.hub-shortlist--compact {
  padding-block: 0 var(--sec-2);
}
.hub-shortlist__title {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
}
.hub-shortlist__all {
  margin: var(--s4) 0 0;
  font-family: var(--body-font);
  font-size: 13px;
  font-weight: 600;
}

/* CTA. The closing band: a heading, a line, and the store badges. */
.cta {
  background: var(--cream);
  padding-block: var(--sec-2) var(--sec);
  border-top: 1px solid var(--rule);
}
.cta p {
  color: var(--ink-2);
  font-family: var(--display-font);
  max-width: 46ch;
}

/* Legal + support pages */
.legal {
  padding: clamp(40px, 7vw, 80px) 0 clamp(56px, 8vw, 96px);
}
.legal__inner {
  max-width: 760px;
}
.legal__title {
  font-size: clamp(2.2rem, 5vw, 3.2rem);
}
.legal__meta {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin-top: -0.4em;
}
.legal h2 {
  font-size: 1.5rem;
  margin-top: 1.8em;
  color: var(--paprika-dark);
}
.legal ul {
  padding-left: 1.2em;
}
.legal li {
  margin-bottom: 0.6em;
}
.legal__links {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
  margin-top: 2.4em;
  padding-top: 1.4em;
  border-top: 1px solid var(--line);
  font-weight: 600;
}

.faq__item {
  border-top: 1px solid var(--line);
  padding: 18px 0;
}
.faq__item:last-of-type {
  border-bottom: 1px solid var(--line);
}
.faq__q {
  font-size: 1.2rem;
  margin-bottom: 0.4em;
}
.faq__item p {
  color: var(--ink);
  margin-bottom: 0;
}

/* Footer */
.site-footer {
  background: var(--ink);
  color: #f4ece0;
  padding: 44px 0;
}
.site-footer__inner {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.site-footer__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.3rem;
  margin: 0;
  color: #fff;
}
/* The app icon, at signature size. It used to headline the hero; a product
   screenshot does that now. */
.footer-mark {
  display: inline-flex;
  flex-shrink: 0;
}
.footer-mark .brand-mark {
  display: block;
  width: 34px;
  height: 34px;
}
.site-footer__nav {
  display: flex;
  gap: 8px 22px;
  flex-wrap: wrap;
  font-weight: 600;
  margin: -6px 0;
}
/* Comfortable thumb targets: 44px tall, gaps kept wide enough that the
   boxes never touch. */
.site-footer__nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--butter);
  text-decoration: none;
}
.site-footer__nav a:hover {
  color: #fff;
}
.site-footer__note,
.site-footer__copy {
  margin: 0;
  color: rgba(244, 236, 224, 0.72);
  font-size: 0.9rem;
}

/* Responsive */
@media (min-width: 720px) {
  .app-story__intro {
    grid-template-columns: 0.95fr 1.05fr;
    gap: 40px;
  }
  .feature-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  /* Two lines of title on every card, wrapped or not, so the paragraphs below
     start on one level instead of stepping down card by card. */
  .feature__title {
    min-height: 2.24em;
  }
  /* Six cards in two columns leaves the lead and the tail card wide, which
     keeps every row full instead of stranding one card on its own. */
  .feature:first-child,
  .feature:last-child {
    grid-column: 1 / -1;
  }
  /* Those two span the full width and share a row with nobody, so they have no
     neighbour to line up with and keep their natural title height. */
  .feature:first-child .feature__title,
  .feature:last-child .feature__title {
    min-height: 0;
  }
}

@media (min-width: 980px) {
  .feature-grid {
    grid-template-columns: repeat(3, 1fr);
  }
  /* Six cards land as two clean rows of three, so nothing needs to span. */
  .feature:first-child,
  .feature:last-child {
    grid-column: auto;
  }
  /* Back in a shared row, so back to the reserved two line title. */
  .feature:first-child .feature__title,
  .feature:last-child .feature__title {
    min-height: 2.24em;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* Sign-in header link: quiet by default, a small pill on wider screens. */
.site-nav__signin {
  color: var(--paprika-dark);
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
@media (min-width: 720px) {
  .site-nav__signin {
    padding: 6px 14px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--paper);
  }
  .site-nav__signin:hover {
    border-color: var(--paprika);
    color: var(--paprika);
  }
}

/* Account + sign-in page */
.account {
  padding: clamp(40px, 7vw, 80px) 0 clamp(56px, 8vw, 96px);
}
.account__inner {
  max-width: 620px;
}
.account__title {
  font-size: clamp(2rem, 5vw, 3rem);
  margin-bottom: 0.6em;
}
.account__lede {
  color: var(--ink-soft);
  font-size: 1.05rem;
}
.account__fineprint {
  color: var(--ink-soft);
  font-size: 0.9rem;
  margin: 0;
}
.account__back {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin-top: 24px;
}

.account-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 28px;
  margin-bottom: 20px;
  box-shadow: 0 1px 0 rgba(43, 29, 22, 0.03);
}
.account-card--signin {
  text-align: center;
  padding: 34px 28px;
}
.account-card--quiet {
  background: var(--cream);
}
.account-card--empty {
  background: var(--cream-2);
}
.account-card__kicker {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 0.35em;
}
.account-card__name {
  font-size: 1.7rem;
  margin: 0 0 0.3em;
  color: var(--paprika-dark);
}
.account-card__plan {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin: 0;
}
.account-card__status {
  color: var(--ink-soft);
  font-size: 0.95rem;
}
.account-card__note {
  color: var(--ink-soft);
  font-size: 0.9rem;
  margin: 14px 0 0;
}
/* A second subject inside one card, under a hairline. The member list moved in
   under the household's name and plan when /account lost three cards; the rule
   is what keeps "who is in this household" from reading as a footnote to the
   plan status above it. */
.account-card__section {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

.account-tag {
  display: inline-block;
  font-size: 0.74rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--ink-soft);
  background: var(--cream);
}
.account-tag--plan {
  color: var(--paprika-dark);
  border-color: var(--paprika);
  background: color-mix(in srgb, var(--paprika) 8%, var(--paper));
}
.account-tag--claimed {
  color: #3f6b2f;
  border-color: #cfe0bf;
  background: #eef5e6;
}

.member-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.member {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--line);
}
.member:first-child {
  border-top: none;
}
.member__name {
  font-weight: 600;
}

/* THE HOUSEHOLD INVITE, inside the household card on /account.

   THE CODE IS THE APP'S CODE, deliberately: the Household tab draws it in the
   display cut at 30px with 4px of tracking, and a family reading the same six
   characters off a laptop and off a phone should be reading the same object.
   Grouped "ABC DEF" by AccountHelper#account_invite_code, which is the app's
   formatCodeForDisplay.

   The link is spelled out under it, wrapping anywhere, because a join URL has
   no spaces in it and a long unbroken string is what pushes a card wider than
   a phone. */
.invite__code {
  margin: 2px 0 0;
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.9rem;
  line-height: 1.1;
  letter-spacing: 0.18em;
  color: var(--paprika-dark);
}
.invite__caption {
  margin: 10px 0 0;
  color: var(--ink-soft);
  font-size: 0.95rem;
}
.invite__locked-title {
  margin: 2px 0 0;
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--ink);
}
.invite__link {
  margin: 12px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--cream);
  color: var(--ink-2);
  font-size: 0.85rem;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.invite__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 14px;
}
.invite__copy {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 8px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
}
.invite__copy:hover {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
.invite__copy:focus-visible {
  outline: 3px solid var(--paprika);
  outline-offset: 2px;
}
/* The confirmation. Empty until a copy happens, so it holds no height and
   announces something rather than merely appearing. */
.invite__status {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.9rem;
}
.invite__status:empty {
  display: none;
}

.account-actions {
  margin-top: 8px;
}

/* Buttons */
.btn-signin,
.btn-signout,
.btn-cta {
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  border-radius: 12px;
  border: 1px solid transparent;
  padding: 13px 22px;
}
.btn-signin {
  display: inline-block;
  width: 100%;
  max-width: 320px;
  margin: 8px 0 22px;
  font-size: 1.05rem;
  background: var(--paprika);
  border-color: var(--paprika);
  color: #fff;
}
.btn-signin:hover {
  background: var(--paprika-dark);
}
.btn-signout {
  background: var(--paper);
  border-color: var(--line);
  color: var(--ink);
}
.btn-signout:hover {
  border-color: var(--paprika);
  color: var(--paprika);
}
/* The site's one solid, tappable call to action, wherever a page needs one: the
   search submit in the landing page's Answer Bar and on the search page itself.
   It began as the landing hero's "Get started" link, which is why it is written
   to work as an <a> as well as a <button> and carries the centring and the
   underline reset a button would have given free. */
.btn-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 320px;
  min-height: 52px;
  font-size: 1.05rem;
  text-align: center;
  text-decoration: none;
  background: var(--paprika);
  border-color: var(--paprika);
  color: #fff;
  box-shadow: 0 10px 24px rgba(193, 58, 28, 0.22);
  transition: background 0.15s ease, transform 0.15s ease;
}
.btn-cta:hover {
  background: var(--paprika-dark);
  color: #fff;
}
.btn-cta:active {
  transform: translateY(1px);
}
@media (prefers-reduced-motion: reduce) {
  .btn-cta {
    transition: none;
  }
  .btn-cta:active {
    transform: none;
  }
}
/* button_to wraps its button in a form; keep that form inline with no gap. */
.account form,
.account-card--signin form,
.plan-price form {
  margin: 0;
}

/* Pricing page. Deliberately built out of the account card rather than a new
   component set: the state cards under the grid ARE account cards, and the plan
   card is the same paper/line/radius with a price block inside it.

   The gutter rule: .pricing is the section, .pricing__inner is the `wrap`
   companion, so only .pricing may use the padding shorthand. See the guard in
   spec/copy_style_spec.rb. */
.pricing {
  padding: clamp(40px, 7vw, 80px) 0 clamp(56px, 8vw, 96px);
}
.pricing__inner {
  max-width: 900px;
}
.pricing__title {
  font-size: clamp(2rem, 5vw, 3rem);
  margin-bottom: 0.4em;
}
.pricing__lede {
  color: var(--ink-soft);
  font-size: 1.05rem;
  max-width: 46ch;
}
/* The Polar-is-unreachable notice. Carries its meaning in words and in role
   ="status", never in the border colour alone. */
.pricing__notice {
  background: var(--cream-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px 20px;
  margin: 0 0 24px;
  color: var(--ink);
}

/* auto-fit with a min track, so two cards sit side by side on a laptop and
   stack on a phone without a media query to keep in step. */
.pricing__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
  margin-bottom: 28px;
}
.plan-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 28px;
  box-shadow: 0 1px 0 rgba(43, 29, 22, 0.03);
}
.plan-card__name {
  font-size: 1.7rem;
  margin: 0 0 0.2em;
  color: var(--paprika-dark);
}
.plan-card__who {
  color: var(--ink-soft);
  margin: 0 0 16px;
}
.plan-card__list {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
}
.plan-card__list li {
  position: relative;
  padding-left: 26px;
  margin-bottom: 8px;
  line-height: 1.45;
}
/* A drawn tick rather than a bullet glyph: the webfont subset carries Latin and
   a handful of symbols, and a checkmark is not among them. */
.plan-card__list li::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 0.42em;
  width: 10px;
  height: 6px;
  border-left: 2px solid var(--paprika);
  border-bottom: 2px solid var(--paprika);
  transform: rotate(-45deg);
}
.plan-card__note {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin: 0;
}

.plan-price {
  border-top: 1px solid var(--line);
  padding-top: 16px;
  margin-top: 16px;
}
.plan-price__label {
  font-size: 0.78rem;
  font-family: var(--body-font);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 0.3em;
}
.plan-price__amount {
  font-family: var(--display-font);
  font-size: 2rem;
  line-height: 1.1;
  color: var(--ink);
  margin: 0;
}
/* No left margin: the template already carries a real space, which a screen
   reader needs and a margin cannot supply. */
.plan-price__period {
  font-family: var(--body-font);
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--ink-soft);
}
.plan-price__trial {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin: 6px 0 0;
}
/* The buy button is full width inside the card, and its bottom margin is the
   card's own padding, not another 22px of air. */
.plan-price .btn-signin {
  max-width: none;
  margin: 14px 0 0;
}

/* ==========================================================================
   Admin dish catalog: the reusable dish card + grid, and the per-dish edit
   form. Built on the Warm Kitchen tokens (:root above). The card carries no
   admin-only styling so the future public catalog can reuse it verbatim.
   ========================================================================== */

/* One-line note under the index header: edits are live DB changes a reseed can
   overwrite. */
.admin-note {
  margin: 0 0 20px;
  padding: 10px 14px;
  font-size: 0.9rem;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--butter) 16%, var(--paper));
  border: 1px solid var(--line);
  border-left: 4px solid var(--butter);
  border-radius: 10px;
}

/* Responsive card grid: a clean 3-4 columns on desktop, collapsing on phones. */
.dish-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
  margin: 8px 0 24px;
}

/* The reusable dish card. As an <a> it lifts on hover; as a <div> it is static. */
.dish-card {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow: hidden;
  color: var(--ink);
  text-decoration: none;
  transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
a.dish-card:hover {
  transform: translateY(-2px);
  border-color: var(--paprika);
  box-shadow: 0 12px 28px rgba(43, 29, 22, 0.12);
}
a.dish-card:focus-visible {
  outline: 3px solid var(--paprika);
  outline-offset: 2px;
}

/* The media slot. A REAL photo gets a 3:2 frame (none of the 843 dishes has one
   yet, but the frame is ready and declares its own box, so the day they arrive
   nothing on the page shifts). With no photo the band is deliberately much
   shorter: a 3:2 hole holding one emoji is what turned a page of 48 cards into a
   field of empty yellow rectangles with a little text underneath. */
.dish-card__photo {
  aspect-ratio: 3 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Butter gradient fallback behind the cuisine emoji, mirroring the mobile app. */
  background: linear-gradient(135deg, var(--butter), color-mix(in srgb, var(--butter) 35%, var(--paper)));
}
.dish-card__photo--placeholder {
  aspect-ratio: auto;
  height: 84px;
  /* A soft, off centre wash rather than a flat diagonal: at four cards across,
     nineteen cuisines repeat their tile constantly, so the band has to read as
     texture behind the name instead of as the subject of the card. */
  background:
    radial-gradient(120% 160% at 22% -40%, color-mix(in srgb, var(--butter) 92%, #fff) 0%,
      color-mix(in srgb, var(--butter) 42%, var(--paper)) 78%);
  border-bottom: 1px solid color-mix(in srgb, var(--butter) 55%, var(--line));
}
.dish-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.dish-card__emoji {
  font-size: 2.1rem;
  line-height: 1;
  /* The tile is decoration, not the subject; it sits back behind the name. */
  opacity: 0.92;
  filter: drop-shadow(0 2px 3px rgba(43, 29, 22, 0.14));
}

.dish-card__body {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 13px 16px 16px;
}
.dish-card__name {
  font-family: var(--display-font);
  font-size: 1.15rem;
  line-height: 1.22;
  margin: 0;
  /* Cancels the -0.01em the shared heading rule applies; at card size the tighter
     tracking made two word names look cramped. */
  letter-spacing: 0;
}
/* The cuisine reads as an eyebrow ABOVE the name now, the same shape the recipe
   page uses over its h1, so a card and the page it opens are recognisably the
   same design rather than two different ideas about the same dish. */
.dish-card__cuisine {
  order: -1;
  margin: 0;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--paprika);
}

/* How long, how much work, how hot: one muted line, middot separated. */
.dish-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 6px;
  margin: 1px 0 2px;
  font-size: 0.86rem;
  color: var(--ink-soft);
}
.dish-card__meta-item + .dish-card__meta-item::before {
  content: "\00B7";
  padding-right: 6px;
  color: color-mix(in srgb, var(--ink-soft) 55%, transparent);
}

/* Diet and health tags share ONE quiet weight. They used to be three different
   fills competing at the same size, which made a well tagged dish look like a
   warning label. Paprika is now reserved for the one row that is genuinely a
   caution, the allergen line below. */
.dish-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.dish-tag {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  white-space: nowrap;
}
.dish-tag--diet,
.dish-tag--health {
  background: color-mix(in srgb, var(--cream-2) 55%, var(--paper));
  color: var(--ink-soft);
}
.dish-tag--allergen {
  background: color-mix(in srgb, var(--paprika) 10%, var(--paper));
  border-color: color-mix(in srgb, var(--paprika) 30%, var(--line));
  color: var(--paprika-dark);
}

/* "Contains dairy, gluten". The label is not decoration: without it the pills
   beside it read equally well as "free from". */
.dish-card__contains {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  margin: 1px 0 0;
}
.dish-card__contains-label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--paprika-dark);
}

.dish-card__season {
  margin: 3px 0 0;
  font-size: 0.78rem;
  color: var(--ink-soft);
  font-weight: 600;
}

/* Disabled pager buttons (first/last page). */
.pager .btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: default;
  pointer-events: none;
}

/* --- Per-dish edit form --------------------------------------------------- */

.dish-form,
.voice-form {
  max-width: 760px;
}
.form-group {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--paper);
  padding: 18px 20px 22px;
  margin: 0 0 22px;
}
.form-group > legend {
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.15rem;
  padding: 0 8px;
  color: var(--ink);
}

.field {
  display: block;
  margin: 0 0 16px;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.field:last-child { margin-bottom: 0; }
.field__label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink-soft);
}
/* A quiet line under an input: what the field is FOR, or why it is locked. */
.field__hint {
  display: block;
  margin-top: 6px;
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-soft);
}
.field input[type="text"],
.field input[type="url"],
.field input[type="number"],
.field select,
.field textarea {
  display: block;
  width: 100%;
  margin-top: 6px;
  font: inherit;
  font-weight: 400;
  font-size: 0.95rem;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: #fff;
  color: var(--ink);
}
.field textarea {
  resize: vertical;
  line-height: 1.5;
}
.field--check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 400;
  color: var(--ink);
}
.field--check input { margin: 0; }

/* A voice's sample player, on the registry form and in the registry table.
   Bounded rather than full width: in a table cell the browser's own control
   would otherwise stretch the column past every other one, and the point of
   the column is comparing voices, not one wide player. */
.field input[type="file"] {
  display: block;
  margin-top: 6px;
  font: inherit;
  font-size: 0.9rem;
  color: var(--ink);
}
.voice-sample {
  display: block;
  width: 100%;
  max-width: 280px;
  height: 36px;
  margin-top: 6px;
}

/* Checkbox vocab groups: an inline, wrapping set of "check + label" pairs. */
.vocab-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
}
.vocab-check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  color: var(--ink);
  font-size: 0.9rem;
}
.vocab-check label { margin: 0; cursor: pointer; }

/* The SAFETY panel: visually distinct, red-accented, flagged safety-critical. */
.safety-panel {
  border: 2px solid var(--paprika);
  background: color-mix(in srgb, var(--paprika) 6%, var(--paper));
}
.safety-panel > legend {
  color: var(--paprika-dark);
}
.safety-panel__note {
  margin: 0 0 16px;
  font-size: 0.85rem;
  color: var(--paprika-dark);
}

.form-errors {
  border: 1px solid var(--paprika);
  background: color-mix(in srgb, var(--paprika) 10%, var(--paper));
  border-radius: 12px;
  padding: 14px 18px;
  margin: 0 0 22px;
  color: var(--paprika-dark);
}
.form-errors__title { margin: 0 0 8px; font-weight: 700; }
.form-errors ul { margin: 0; padding-left: 20px; }

.form-actions {
  display: flex;
  gap: 12px;
  align-items: center;
}
.admin-back {
  color: var(--paprika);
  font-weight: 600;
  text-decoration: none;
}
.admin-back:hover { text-decoration: underline; }

/* --- Public recipe page (/recipes/:slug) --- */
.recipe {
  max-width: 680px;
  padding-top: 40px;
  padding-bottom: 72px;
}
/* The photo frame, for the day photos exist. It declares its own aspect ratio
   and the img declares width and height, so an image arriving late cannot shift
   the text under it. */
.recipe__hero {
  border-radius: 22px;
  overflow: hidden;
  aspect-ratio: 16 / 10;
  display: grid;
  place-items: center;
  margin-bottom: 28px;
  box-shadow: 0 18px 40px rgba(43, 29, 22, 0.12);
}
.recipe__photo { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The head. With no photo (all 843 dishes today) the cuisine tile is a medallion
   beside the title rather than a 400px band above it, so the h1 is the first
   thing painted and the first thing read. */
.recipe__head {
  margin-bottom: 18px;
}
.recipe__head--placeholder {
  display: flex;
  align-items: center;
  gap: 18px;
}
.recipe__mark {
  flex: none;
  width: 76px;
  height: 76px;
  display: grid;
  place-items: center;
  font-size: 34px;
  border-radius: 22px;
  background: radial-gradient(120% 150% at 26% -30%, #f7dd87 0%, #eec13b 78%);
  box-shadow: 0 8px 20px rgba(43, 29, 22, 0.14);
}
.recipe__headline {
  min-width: 0;
}
.recipe__eyebrow {
  color: var(--paprika);
  font-weight: 800;
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 0 0 6px;
}
.recipe__title {
  font-family: var(--display-font);
  font-size: clamp(2rem, 6vw, 3rem);
  line-height: 1.05;
  color: var(--ink);
  margin: 0;
  letter-spacing: -0.02em;
}
/* WHO COOKED THIS, one compact row between the title and the share bar. Sized to
   read as an attribution rather than as a second call to action: no card, no
   border, no button. The whole row is the link, so there is one target rather
   than a small one beside a big one. */
.recipe__byline {
  margin: 14px 0 0;
}
.recipe__byline-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--ink);
  border-radius: 999px;
  /* Negative inline margin so the padded hit area does not visibly indent the
     row away from the title it sits under. */
  margin-inline-start: -6px;
  padding: 4px 10px 4px 6px;
}
.recipe__byline-link:hover .recipe__byline-name {
  color: var(--paprika-dark);
}
.recipe__byline-link:focus-visible {
  outline: 3px solid var(--paprika);
  outline-offset: 2px;
}
/* One box for both branches, so swapping an initial for a photo moves nothing
   around it. Same round mark, same 40px, same paprika ground as the member
   avatars in the site header. */
.recipe__byline-avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;
  object-fit: cover;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--paprika);
  color: #fff;
}
.recipe__byline-avatar--initial {
  font-family: var(--display-font);
  font-weight: 600;
  font-size: 1.1rem;
  line-height: 1;
}
.recipe__byline-name {
  font-weight: 600;
  font-size: 0.98rem;
}
.recipe__byline-handle {
  color: var(--ink-soft);
  font-size: 0.9rem;
}

/* Up to eleven of these on a well tagged dish, so they are sized to read as one
   calm band under the lede rather than as eleven buttons. */
.recipe__chips { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 4px; }
.recipe__chip {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.recipe__chip--good { background: #e7eee2; color: #4f6b48; border-color: #d6e2ce; }

/* Section rhythm. The page is nine sections of prose, tables and lists in one
   680px column, and with nothing but a margin between them it read as one long
   scroll. A hairline above each heading turns it into rooms, the same device
   the landing page uses between its bands. The first section sits under the
   chips, so it opens without a rule. */
.recipe__section {
  margin-top: 40px;
  padding-top: 30px;
  border-top: 1px solid var(--line);
}
.recipe__section:first-of-type {
  padding-top: 0;
  border-top: 0;
}
.recipe__h2 {
  font-family: var(--display-font);
  font-size: 1.55rem;
  color: var(--ink);
  margin: 0 0 14px;
}
.recipe__videos { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; }
.recipe__video {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--paper);
  border: 1.5px solid var(--line);
  border-radius: 14px;
  padding: 14px 16px;
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
}
.recipe__video:hover { border-color: var(--paprika); color: var(--paprika-dark); }
.recipe__video-play {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  background: var(--paprika);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 11px;
}
.recipe__list { margin: 0; padding-left: 1.2em; color: var(--ink); line-height: 1.7; }
.recipe__list li { margin-bottom: 6px; }
.recipe__cta {
  margin-top: 48px;
  background: var(--cream-2);
  border: 1.5px solid var(--line);
  border-radius: 22px;
  padding: 34px 28px;
  text-align: center;
}
.recipe__cta-title { font-family: var(--display-font); font-size: 1.6rem; color: var(--ink); margin: 0 0 10px; }
.recipe__cta-lede { color: var(--ink-soft); margin: 0 auto 22px; max-width: 460px; line-height: 1.5; }
.recipe__disclaimer { margin-top: 40px; color: var(--ink-soft); font-size: 0.85rem; text-align: center; }

/* --- The computed recipe sections -----------------------------------------
   "Serve it with", the allergen matrix, the nutrition bands, the timing prose
   and the three related rails. All of it is body copy first: these sections
   exist to be read and to be crawled, so nothing here hides text behind an
   interaction and every colour is one already defined at the top of this file. */
.recipe__lede {
  font-size: 1.12rem;
  line-height: 1.65;
  color: var(--ink);
  margin: 0 0 18px;
}
.recipe__prose {
  color: var(--ink-soft);
  line-height: 1.7;
  margin: 0 0 16px;
}
.recipe__footnote {
  color: var(--ink-soft);
  font-size: 0.85rem;
  line-height: 1.6;
  margin: 12px 0 0;
}

/* Screen reader only. Used for the facade's "play the video" label, which the
   triangle glyph next to it cannot carry on its own. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* THE COMPOSED PLATE. This is the one section on the site nobody else can
   render, and it used to look like three loose bordered boxes with a link in
   each. It is now framed as ONE object: a warm panel with a paprika edge, the
   role slots laid inside it as a grid so three slots do not become three wide
   near empty rows, and a divider between them so the whole thing reads as a
   plate being composed rather than as a list that happens to be nearby. */
.recipe__plate {
  background: color-mix(in srgb, var(--butter) 9%, var(--paper));
  border: 1px solid color-mix(in srgb, var(--butter) 62%, var(--line));
  /* Overrides the shared section hairline: the panel is its own separation, and
     the paprika edge is what marks this as the section that matters. */
  border-top: 3px solid var(--paprika);
  border-radius: 20px;
  padding: 26px 24px 24px;
}
.recipe__plate .recipe__prose {
  max-width: 56ch;
}
.plate {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: grid;
  gap: 0;
}
.plate__slot {
  padding: 16px 0;
  border-top: 1px solid color-mix(in srgb, var(--butter) 45%, var(--line));
}
.plate__slot:first-child {
  padding-top: 0;
  border-top: 0;
}
.plate__slot:last-child {
  padding-bottom: 0;
}
.plate__role {
  margin: 0 0 10px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paprika);
}
/* THE SLOT'S TILES ARE THE SHELF'S COMPACT CARD (.plate-card--compact), which
   is why this block declares a track and a scale and nothing else. The bordered
   box that used to live here (.plate__dish, with its own mark, name and meta)
   was a fourth way to draw one dish on a site that has one, and it showed a
   glyph where every other card on the page shows a photograph.

   THEY ARE THUMBNAILS HERE, NOT CARDS, and that is the difference between this
   section and the rail. The rail is a list of things to open next, so its card
   is read; these are the OTHER DISHES ON TONIGHT'S PLATE, named beside the one
   the page is about, and at the rail's 112px crop and 17px name three of them
   were a second gallery arguing with the recipe above them. So the crop drops
   to 72px, the name to 15px and the line under it to 11.5px, and the panel's
   own rhythm (the role kicker, the slot padding, the divider between slots) is
   untouched: what shrank is the tile, not the section.

   The grid item is the li, like the related rail's, because the card is a grid
   and needs the whole cell rather than its content's height. */
.plate__dishes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* auto-FILL, never auto-fit. auto-fit collapses the tracks it did not use and
     lets the survivors stretch across them, so a slot holding one suggestion
     drew that one tile at the full 822px of the panel: the single biggest
     reason this section read as oversized. auto-fill keeps the empty tracks, so
     one dish is one tile the same width as four. */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px 14px;
}
.plate__dishes > li {
  display: grid;
}
/* THE MODIFIER IS NAMED IN EVERY SELECTOR BELOW, and it is not decoration:
   .plate-card--compact's own scale is declared beside .plate-card, LATER in
   this file, so `.plate__dishes .plate-card__name` would tie its two classes
   and lose the tie to source order. Naming the modifier as well as the list
   wins the sizes outright and says which card is being resized. */
.plate__dishes .plate-card--compact {
  grid-template-columns: 72px minmax(0, 1fr);
  column-gap: 11px;
}
.plate__dishes .plate-card--compact .plate-card__body { gap: 4px; }
.plate__dishes .plate-card--compact .plate-card__name { font-size: 15px; }
.plate__dishes .plate-card--compact .plate-card__meta { font-size: 11.5px; }
.plate__dishes .plate-card--compact .plate-card__emoji { font-size: 1.15rem; }

/* THE ALLERGEN MATRIX. Ten rows on every page, present and absent, so a reader
   learns where to look and finds the same shape on the next dish. That content
   rule is deliberate and stays; what changed is that it now READS as a designed
   answer rather than as a dumped table.
     * It sits on paper inside a rounded frame, like every other panel here.
     * The common case (absent) is QUIET: a hairline outline, not a green pill.
       Ten loud green pills gave the non signal all the emphasis on the page.
     * The rare case (present) is LOUD: a paprika flag, a tinted row and a solid
       paprika edge down the left, so the eye lands on it without reading.
     * It scrolls inside its own box rather than dragging the page sideways on a
       phone when the swap column is there. */
.allergens__scroll {
  overflow-x: auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
}
.allergens {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}
.allergens__caption {
  text-align: left;
  color: var(--ink-soft);
  font-size: 0.85rem;
  padding: 14px 16px 10px;
}
.allergens th,
.allergens td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.allergens tbody tr:last-child th,
.allergens tbody tr:last-child td {
  border-bottom: 0;
}
.allergens thead th {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--cream-2) 45%, var(--paper));
}
.allergens tbody th { font-weight: 600; color: var(--ink); }
/* With no swaps the table is two columns. The status is pushed to the right edge
   so the ten answers form one clean column to run an eye down, which is the same
   shape the nutrition panel below it uses for the same reason. */
.allergens--two-col thead th:last-child,
.allergens--two-col tbody td {
  text-align: right;
}
.allergens__row--present {
  background: color-mix(in srgb, var(--paprika) 6%, transparent);
}
.allergens__row--present > th:first-child {
  box-shadow: inset 3px 0 0 var(--paprika);
}
.allergens__flag {
  display: inline-block;
  border-radius: 999px;
  padding: 3px 11px;
  font-size: 0.8rem;
  font-weight: 700;
}
.allergens__flag--yes {
  background: var(--paprika);
  color: #fff;
}
.allergens__flag--no {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-weight: 600;
}
.allergens__none { color: var(--ink-soft); font-size: 0.85rem; }
.allergens td a { color: var(--paprika-dark); font-weight: 600; }

/* Nutrition, as labelled bands rather than invented numbers. A panel, so it
   reads as the same family of object as the plate and the matrix above it
   rather than as four loose lines of text with rules under them. */
.nutrition {
  margin: 0;
  display: grid;
  gap: 0;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 4px 16px;
}
.nutrition__row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  align-items: baseline;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.nutrition__row:last-child { border-bottom: 0; }
.nutrition__row dt { color: var(--ink-soft); margin: 0; font-size: 0.95rem; }
.nutrition__row dd { margin: 0; color: var(--ink); font-weight: 600; text-align: right; }
.nutrition__audited { color: #4f6b48; }

/* The click to load video facade. Sized by aspect ratio so the poster and the
   iframe that replaces it occupy exactly the same box: pressing play must not
   move the rest of the page. */
.video-facade {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: 16px;
  overflow: hidden;
  background: var(--ink);
  text-decoration: none;
}
/* Pressing play HIDES the facade and inserts the player after it, so a dormant
   facade is still in the DOM for Turbo's before-cache sweep to restore (see
   app/javascript/application.js). This rule is what makes `hidden` bite: the
   display: block above is an author style and would otherwise beat the browser's
   own [hidden] { display: none }, leaving the poster stacked over the player. */
.video-facade[hidden] { display: none; }
.video-facade__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* The hqdefault still is 4:3 with letterbox bars; this crops them off. */
  transform: scale(1.35);
}
.video-facade__play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--paprika);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 20px;
  box-shadow: 0 8px 22px rgba(43, 29, 22, 0.4);
}
.video-facade:hover .video-facade__play { background: var(--paprika-dark); }
.video-facade__title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 26px 16px 12px;
  color: #fff;
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.4;
  background: linear-gradient(rgba(43, 29, 22, 0), rgba(43, 29, 22, 0.85));
}
.video-facade__frame {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: 16px;
  display: block;
}

/* The three related rails. */
.rail { margin-bottom: 26px; }
.rail__title {
  font-family: var(--display-font);
  font-size: 1.15rem;
  color: var(--ink);
  margin: 0 0 4px;
}
.rail__lede { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0 10px; line-height: 1.6; }
.rail__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* ONE COLUMN IN THE 320px RAIL, two on the narrow page where the rail is
     simply the next thing down. A compact card is a 112px crop beside a name,
     and catalog names carry a gloss in parentheses ("Besara (Odia Vegetables in
     Mustard Poppy Paste)"), so below about 260px the name column starts
     shredding those into a stack. */
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  /* THE ROW GAP IS THE HAIRLINE'S PADDING, which is why it is zero here. */
  column-gap: 18px;
  row-gap: 0;
}
/* The card is the <a>, one level inside the grid item, so it does not inherit
   the row's stretch on its own. A single item grid lets it.

   RULED, NOT FLOATING. Four suggestions with nothing between them read as a
   list that fell out of the section above; a hairline over each one makes the
   rail a ruled strip, which is the idiom the chef page's credits and the hub's
   count line already use. It is on EVERY item, first included: in one column
   that opening rule closes the rail's lede, and in two it is the line the row
   sits on, so the same declaration is right on both shapes without a rule that
   has to guess which item begins a row.

   --rule-soft rather than the page's --line: twelve section-weight rules down a
   320px column would stripe it. */
.rail__list > li {
  display: grid;
  padding-block: 13px;
  border-top: 1px solid var(--rule-soft);
}

/* The compact card the two lists above wear is .plate-card--compact, declared
   with the card it modifies rather than here. It used to be declared in this
   section, and that is exactly why it did not work: see the note over it. */

/* --- Hub pages ------------------------------------------------------------
   The head term landing pages (/dinner-recipes, /italian-recipes), the
   namespaced intersections (/dinner-recipes/italian), the two question pages
   and the browse directory at /recipes: ninety four pages, all of them here.

   WARM CINEMA IN DAYLIGHT, drawn from the vocabulary of
   docs/superpowers/design/2026-08-09-warm-cinema-v2.html, which is canonical
   and has no hub page in it. So this is the chef page's arrangement applied to
   a facet of the catalog: a kicker, the display cut at 200 over the head term,
   the hand written paragraph as the lede, the count on a hairline ruled strip,
   then the grid. No band, because the band is where the site shows what
   everybody is cooking and a hub is a catalog somebody is reading down.

   ALMOST NOTHING NEW IS DECLARED HERE, which is the point: the type roles, the
   container, the hairlines, the section rhythm and the focus ring are all the
   site's. What follows is the masthead's own size, the ruled index the
   directory is built from, and the ONE card the hub grids and the search
   results share.
   ------------------------------------------------------------------------- */

/* Breadcrumbs. Shared with the recipe page, which is where they matter most:
   they are the link that stopped every recipe from being an orphan. The
   site's smallest interface voice, above a masthead that is about to be loud. */
.breadcrumb {
  margin: 0 0 var(--s5);
}
.breadcrumb__list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 6px;
  margin: 0;
  padding: 0;
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--fg3);
}
.breadcrumb__item + .breadcrumb__item::before {
  content: "\203A";
  padding-right: 6px;
  color: var(--fg4);
}
.breadcrumb__link {
  color: var(--fg3);
  text-decoration: none;
  transition: color var(--t-quick) var(--e-out-cubic);
}
.breadcrumb__link:hover {
  color: var(--paprika);
}
.breadcrumb__current {
  color: var(--fg2);
}

/* ---- The masthead -------------------------------------------------------- */

/* Tighter at the foot than the homepage's hero: what follows is this page's own
   collection rather than a change of ground, so the two read as one column of a
   magazine rather than as two bands. */
.hub-hero {
  padding-block: var(--s6) var(--s5);
}
/* THE MEASURE IS AN INNER ELEMENT, not a narrower container: .container centres
   itself, so capping ITS width would centre the masthead and leave the heading
   starting a hundred pixels right of the grid below it. */
.hub-hero__inner {
  max-width: 62ch;
}
.hub-hero__kicker {
  margin-bottom: var(--s4);
}
/* The display cut at 200, line-height under 1.0, tracking at zero or above,
   never bold. 16ch rather than the country page's 8ch: a head term is three or
   four words ("Vegetarian Dinner Recipes") where a country's name is one. */
.hub-hero__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(38px, 5.4vw, 76px);
  line-height: 0.94;
  letter-spacing: 0.005em;
  text-wrap: balance;
  overflow-wrap: break-word;
  max-width: 16ch;
  margin: 0;
  color: var(--fg);
}
.hub-hero__lede {
  margin-top: var(--s4);
  max-width: 58ch;
}

/* THE FACTS, ON A HAIRLINE RULE: how many dishes are on this page, and on the
   directory how many pages there are. The site's smallest interface voice
   between two rules rather than a paprika pill, because a pill is a control and
   neither of these is pressable. */
.hub-facts {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: var(--s5) 0 0;
  padding: 0;
  border-block: 1px solid var(--hair);
}
.hub-facts__fact {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  padding: 14px var(--s4);
}
.hub-facts__fact:first-child {
  padding-left: 0;
}
/* The rule between two facts, drawn once by the second of them, so a strip of
   one fact carries no stray edge. */
.hub-facts__fact + .hub-facts__fact {
  border-left: 1px solid var(--hair-soft);
}

/* ---- The sections of a hub page ------------------------------------------ */

.hub-collection {
  padding-block: var(--s6) 0;
}
.hub-answers {
  padding-block: var(--s6) 0;
}
.hub-answer {
  margin: 0 0 var(--sec-2);
}
.hub-answer__title,
.hub-related__title {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(22px, 2.2vw, 28px);
  line-height: 1.2;
  letter-spacing: 0.006em;
  color: var(--fg);
  margin: 0 0 var(--s5);
}
.hub-related {
  padding-block: var(--sec-2) 0;
}
.hub-more {
  margin: var(--s6) 0 0;
  font-family: var(--body-font);
  font-size: 13px;
  font-weight: 600;
}
.hub-directory {
  padding-block: var(--s6) 0;
}

/* ---- The ruled index ------------------------------------------------------
   The browse directory, the related hubs, the question pages' facets, the
   community page's browse block and the homepage's shortlist are all this one
   shape. NINETY FOUR LINKS WEARING BORDERED BOXES read as ninety four buttons;
   ruled rows under a small heading read as the contents page of a book, which
   is what a directory is. Radius belongs to chips, and none of these is one. */
.hub-links {
  margin: 0 0 var(--s6);
}
/* The group's name in the label voice, on the rule that opens the group. */
.hub-links__title {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  margin: 0 0 var(--s1);
  padding-top: var(--s4);
  border-top: 1px solid var(--hair);
}
/* Column gap only. The rows are separated by their own rules, so a row gap here
   would break each rule away from the row above it. */
.hub-links__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  column-gap: var(--s6);
}
.hub-links__list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  /* The 44px hit area, met by the row itself rather than by padding a label. */
  min-height: var(--tap);
  padding: 11px 0;
  border-bottom: 1px solid var(--hair-soft);
  color: var(--fg);
  text-decoration: none;
  font-family: var(--body-font);
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  transition: color var(--t-quick) var(--e-out-cubic),
              border-color var(--t-quick) var(--e-out-cubic);
}
.hub-links__list a:hover {
  color: var(--paprika);
  border-bottom-color: var(--fg3);
}
.hub-links__name {
  min-width: 0;
}
/* Tabular figures, so a column of counts lines up on its own decimal. */
.hub-links__count {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--fg4);
}

/* ---- The catalog card ----------------------------------------------------
   ONE CARD, TWO SURFACES: every hub grid and every search result is
   shared/_catalog_card, by owner directive, so the two cannot drift. It is the
   RICH card, deliberately: unlike the shelf's plate card, which is glanced at
   sideways while a row scrolls, this one is scanned in a grid of forty eight
   where the whole decision is made before anything is opened. So it keeps every
   fact the pre-cinema hub card carried and adds the country.

   NO CHROME, per the design laws: no border, no background, no shadow, no lift.
   The card is one anchor, and the only motion is the photograph moving inside
   its fixed crop (declared with the plate card's, under the reduced motion
   query near the foot of this file). Focus is not restyled: the site's global
   :focus-visible ring draws it. */
.catalog-grid {
  --catalog-min: 244px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--catalog-min), 1fr));
  gap: var(--s6) var(--gap);
  margin: 0;
}
.catalog-card {
  position: relative;
  display: flex;
  flex-direction: column;
  color: var(--fg);
  text-decoration: none;
}
/* THE CROP IS FIXED AND THE PHOTOGRAPH MOVES INSIDE IT, so a row of cards is
   one height whether the photographs have arrived or not. */
.catalog-card__photo {
  position: relative;
  overflow: hidden;
  border-radius: var(--r);
  aspect-ratio: 4 / 3;
  background: var(--well);
  display: flex;
  align-items: center;
  justify-content: center;
}
.catalog-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* NO PHOTOGRAPH, SAME BOX: the crop keeps its shape and holds the cuisine's
   glyph on the surface's own well tone, so an unphotographed dish is a quiet
   card in a grid rather than a hole in it. */
.catalog-card__photo--placeholder {
  color: var(--fg3);
}
.catalog-card__emoji {
  font-size: 1.9rem;
  line-height: 1;
}
.catalog-card__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 14px;
  min-width: 0;
}

/* THE CUISINE, IN RED: the pre-cinema hub card's eyebrow, kept to the letter by
   owner directive. Every declaration below is the old `.dish-card__cuisine`
   verbatim, and the two the old rule did not write (family and line-height)
   were INHERITED from body then and are inherited from body now, which is the
   same Inter at the same 1.6. `order: -1` is the one dropped declaration, and it
   is not a text property: the old rule needed it because the markup put the
   eyebrow after the name and flexbox reordered it, while here it is simply
   written first.

   IT IS A PLAIN BLOCK OF TEXT, exactly as it was. It briefly named the
   community, and briefly became a flex row so a flag could sit beside the name,
   which centred the letters against a 13px emoji instead of setting them on
   their own baseline: the same font, a pixel off the line it used to sit on.
   Both are ruled off. One paragraph, one string, the old line.

   The search cards wear the same line, which is the convergence: the hub
   treatment is the source of truth for it on both surfaces. */
.catalog-card__origin {
  margin: 0;
  font-family: var(--body-font);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--paprika);
}

.catalog-card__name {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 20px;
  line-height: 1.22;
  letter-spacing: 0.004em;
  color: var(--fg);
  margin: 0;
  max-width: 24ch;
}

/* The kitchen, how long, how much work, how hot: one muted line, middot
   separated. As pills these were four of the dozen rounded shapes competing on
   every card; as a sentence they are read in one pass. */
.catalog-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 6px;
  margin: 0;
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.012em;
  color: var(--fg3);
}
.catalog-card__fact + .catalog-card__fact::before {
  content: "\00B7";
  padding-right: 6px;
  color: var(--fg4);
}

/* Diet and health tags share ONE quiet weight and carry no fill. They used to
   be three competing tones, which made a well tagged dish look like a warning
   label; paprika is reserved for the one line that is genuinely a caution. */
.catalog-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.catalog-tag {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 9px;
  border: 1px solid var(--hair);
  border-radius: var(--r-chip);
  color: var(--fg3);
  white-space: nowrap;
}
.catalog-tag--allergen {
  border-color: color-mix(in srgb, var(--paprika) 34%, var(--hair));
  color: var(--paprika-dark);
}
/* "Contains dairy, gluten". The label is not decoration: without it the tags
   beside it read exactly as well as "free from", which is a safety problem
   rather than a styling one. */
.catalog-card__contains {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px 6px;
  margin: 0;
}
.catalog-card__contains-label {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paprika-dark);
}
.catalog-card__season {
  margin: 0;
  font-family: var(--body-font);
  font-size: 12px;
  letter-spacing: 0.01em;
  color: var(--fg4);
}

/* ---- Pagination ----------------------------------------------------------
   Every page number is a link, so no page of a large hub sits more than one hop
   from the first, for a reader or for a crawler.

   TWO PAGERS, AND THE DIFFERENCE IS DELIBERATE. `.pager-block` is what
   shared/_pagination renders: the public collections (hubs, chef profiles) in
   the cinema's own voice, folded back into the component from the block that
   was scoped to `.chef-collection` while the chef page was the only reskinned
   surface with a pager. The bare `.pager` rules below it are for the ADMIN
   screens, which write that markup by hand and are not part of this reskin. */
.pager-block {
  margin: var(--sec-2) 0 0;
  border-top: 1px solid var(--hair);
  padding-top: var(--s5);
}
.pager-block .pager {
  margin: 0;
}
.pager-block .btn,
.pager-block .pager__page {
  border-radius: var(--r);
  border-color: var(--hair);
  background: transparent;
  color: var(--fg2);
  font-family: var(--body-font);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.pager-block a.btn:hover,
.pager-block .pager__page:hover {
  border-color: var(--fg3);
  color: var(--fg);
}
/* The page you are standing on, stated rather than lit: these pages spend their
   one filled shape elsewhere. */
.pager-block .pager__page--current {
  background: transparent;
  border-color: var(--fg);
  color: var(--fg);
}
.pager-block .pager__info {
  margin: var(--s3) 0 0;
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
}

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 26px 0 6px;
}
.pager .btn {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 6px 16px;
  border: 1.5px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 600;
}
.pager a.btn:hover {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
.pager__pages {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.pager__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  min-height: 38px;
  padding: 0 8px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 600;
}
.pager__page:hover {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
.pager__page--current {
  background: var(--paprika);
  border-color: var(--paprika);
  color: #fff;
}
.pager__info {
  color: var(--ink-soft);
  font-size: 0.85rem;
  margin: 0 0 8px;
}

/* ---- 390 ------------------------------------------------------------------
   THE CARD TURNS ON ITS SIDE RATHER THAN SHRINKING. Two rich cards across a
   phone leaves about 165px for a name, four facts, the diet tags and an
   allergen line, which is how a card that says everything ends up saying none
   of it legibly. One column with a square crop beside the text gives the words
   210px, keeps every fact the desktop card carries (the allergen line above
   all, which is the one nobody may lose to a breakpoint), and scrolls a page of
   forty eight in a few flicks.

   The index drops to one column for the same reason: two 150px columns of
   ruled rows would wrap "Vegetarian Dinner Recipes" onto three lines. */
@media (max-width: 700px) {
  .hub-collection,
  .hub-answers,
  .hub-directory {
    padding-block: var(--s5) 0;
  }
  .catalog-grid {
    grid-template-columns: 1fr;
    gap: var(--s5) 0;
  }
  .catalog-card {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--s3);
  }
  .catalog-card__photo {
    flex: 0 0 116px;
    aspect-ratio: 1 / 1;
  }
  .catalog-card__body {
    flex: 1 1 auto;
    padding-top: 0;
    gap: 6px;
  }
  .catalog-card__name {
    font-size: 17px;
    max-width: none;
  }
  .catalog-card__emoji {
    font-size: 1.5rem;
  }
  .hub-links__list {
    grid-template-columns: 1fr;
  }
  .hub-answer {
    margin-bottom: var(--sec-2);
  }
}

/* --- Chef profile pages ----------------------------------------------------
   /chefs/<handle>: one cook's title page, then everything they have published.
   Warm Cinema in daylight, extended to a surface the canonical mockup does not
   draw: the community masthead's scale over a person's name, the keepers
   strip's avatar at hero size, a hairline ruled credits strip, and the band's
   plate cards laid out in a grid rather than run in a track. See the comment at
   the head of app/views/chefs/show.html.erb for why a profile is a grid.

   ALMOST NOTHING NEW IS DECLARED HERE, which is the point: the type roles, the
   card, the crop, the column maths and the hairlines are all the site's, and
   what follows is the masthead's own size, the avatar's, the credits strip and
   the grid the cards sit in.
   ------------------------------------------------------------------------- */

/* Tighter at the foot than the other heroes: what follows here is this page's
   own collection rather than a change of ground, so the two read as one column
   of a magazine rather than as two bands. */
.chef-hero {
  padding-block: var(--s6) var(--s6);
}
.chef-hero__inner {
  max-width: 62ch;
}
.chef-hero__kicker {
  margin-bottom: var(--s4);
}

/* ONE BOX FOR BOTH BRANCHES, so the day a photograph arrives nothing under it
   moves. The keepers strip's mark at hero size: espresso on cream, which is the
   one place the band's ground appears in daylight, and the only filled shape in
   this viewport. */
.chef-hero__avatar {
  display: grid;
  place-items: center;
  width: 112px;
  height: 112px;
  border-radius: var(--r-chip);
  overflow: hidden;
  object-fit: cover;
  background: var(--night);
  color: var(--lit);
}
.chef-hero__avatar--initial {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: 44px;
  line-height: 1;
  letter-spacing: 0.01em;
}

/* THE MASTHEAD, at the community page's scale and in its cut: weight 200,
   line-height under 1.0, positive tracking, never bold. 11ch rather than the
   country page's 8ch, because a person's name is two or three words where a
   country's is one, and break-word only so a single unbroken name cannot drag
   the column wider than the page. */
.chef-hero__name {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(56px, 8.4vw, 124px);
  line-height: 0.9;
  letter-spacing: 0.005em;
  text-wrap: balance;
  overflow-wrap: break-word;
  max-width: 11ch;
  margin: var(--s4) 0 0;
  color: var(--fg);
}
/* Not a label: a handle is a literal string and uppercasing it would print an
   address nobody could type back. */
.chef-hero__handle {
  font-family: var(--body-font);
  font-size: 14px;
  letter-spacing: 0.01em;
  color: var(--fg3);
  margin: var(--s3) 0 0;
}
.chef-hero__bio {
  margin-top: var(--s5);
}

/* THE OWNER'S DOOR BACK TO THE FORM, in the credits strip's voice rather than
   the site's button one. A paprika block under somebody's own name would be the
   loudest thing in the viewport and would read as an invitation to every visitor
   rather than a control only its owner can see. This is a rule under a small
   word: quiet at rest, ink and a solid rule under the pointer. */
.chef-hero__own {
  margin: var(--s3) 0 0;
}
.chef-hero__edit {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  text-decoration: none;
  border-bottom: 1px solid var(--hair);
  padding-bottom: 3px;
}
.chef-hero__edit:hover,
.chef-hero__edit:focus-visible {
  color: var(--fg);
  border-bottom-color: currentColor;
}

/* THE CREDITS STRIP. Where they cook, how much is here, how long they have been
   at it: the site's smallest interface voice between two hairlines, rather than
   three pills. A pill is a control, and not one of these is pressable. */
.chef-facts {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: var(--s6) 0 0;
  padding: 0;
  border-block: 1px solid var(--hair);
}
.chef-facts__fact {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  padding: 14px var(--s4);
}
.chef-facts__fact:first-child {
  padding-left: 0;
}
/* The rule between two facts, drawn once by the second of them, so a strip of
   one fact carries no stray edge. */
.chef-facts__fact + .chef-facts__fact {
  border-left: 1px solid var(--hair-soft);
}

.chef-collection {
  padding-block: var(--sec-2) var(--sec);
}
.chef-collection__title {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(22px, 2.2vw, 28px);
  line-height: 1.2;
  letter-spacing: 0.006em;
  color: var(--fg);
  margin: 0 0 var(--s5);
}
.chef-collection__empty {
  margin: 0;
}

/* ---- The tab rail --------------------------------------------------------
   A profile is one page with two collections on it, and this is how somebody
   moves between them. HAIRLINE UNDERLINE TABS AND NEVER BOXES: a box is a
   button, and these are two places rather than two actions.

   IT STANDS WHERE THE COLLECTION'S HEADING DID, so it carries that heading's
   bottom margin and its own hairline runs the width of the column. The active
   tab's 2px rule sits ON that hairline, which is what the negative margin under
   each tab is for: the rule and the hair are the same line, so nothing shifts
   by a pixel between the two tabs.

   BUILT FOR TWO AND SAFE FOR MORE. Nothing here counts the tabs, so a third one
   wraps rather than squeezing. */
.tab-rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin: 0 0 var(--s5);
  border-bottom: 1px solid var(--hair);
}
.tab-rail__tab {
  margin-bottom: -1px;
  padding-bottom: 10px;
  border-bottom: 2px solid transparent;
  color: var(--fg3);
  font-family: var(--body-font);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--t-quick) var(--e-out-cubic);
}
.tab-rail__tab:hover {
  color: var(--fg);
}
/* THE ONE YOU ARE STANDING ON, stated in ink and underlined. It is still a link
   (a dead word where a link was a moment ago is worse than a redundant one) and
   it carries aria-current, which is what actually says so to somebody who
   cannot see the rule. */
.tab-rail__tab--active {
  border-bottom-color: var(--fg);
  color: var(--fg);
}

/* ---- Cooksnaps, in a grid ------------------------------------------------
   THE CATALOG GRID'S RHYTHM WITH SQUARE TILES. Same four columns and the same
   gaps as .plate-grid, because a page of photographs and a page of cards are
   the same kind of page; the crop is square because a cooksnap is a photograph
   somebody took on a phone in a kitchen, and a square is the crop that survives
   all of them.

   TWO PAGES RENDER IT: every photograph of one dish
   (/recipes/<slug>/cooksnaps) and every photograph by one cook
   (/chefs/<handle>/cooksnaps). */
.snap-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: var(--gap);
  row-gap: var(--s6);
  margin: 0;
  padding: 0;
  list-style: none;
}
.snap-card {
  margin: 0;
}
.snap-card__link {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  color: inherit;
  text-decoration: none;
}
/* The frame reserves the square before the photograph arrives, so a grid of
   twenty four does not reflow as they land. */
.snap-card__frame {
  display: block;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  border-radius: var(--r);
  background: var(--cream-3);
}
.snap-card__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--e-out-cubic);
}
/* The plate card's own hover, in the one place a photograph is the whole tile.
   Nothing moves for somebody who asked for no motion. */
.snap-card__link:hover .snap-card__img {
  transform: scale(1.03);
}
@media (prefers-reduced-motion: reduce) {
  .snap-card__img {
    transition: none;
  }
  .snap-card__link:hover .snap-card__img {
    transform: none;
  }
}
.snap-card__body {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.snap-card__byline {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.snap-card__avatar {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: var(--r-chip);
  object-fit: cover;
  background: var(--cream-3);
}
.snap-card__avatar--initial {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  color: var(--cream);
  font-family: var(--display-font);
  font-size: 12px;
  line-height: 1;
}
.snap-card__name {
  overflow: hidden;
  font-family: var(--body-font);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--fg);
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* THE WHOLE SENTENCE, up to three lines of it: this is the one page on the site
   where a 280 character caption is meant to be read, and the rail's one line
   over the photograph is what links here. */
.snap-card__caption {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  font-family: var(--body-font);
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--fg2);
}
.snap-card__date {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg4);
}
.snap-collection {
  padding-block: var(--sec-2) var(--sec);
}
.snap-collection__empty {
  margin: 0;
}

/* ---- The gallery's masthead ----------------------------------------------
   The chef profile's masthead over a dish instead of a person: the kicker says
   what kind of page this is, the dish's name is the h1 AND the way back to it,
   and the count line says the same sentence the shelf on that page says. */
.snap-hero__inner {
  max-width: 34ch;
}
.snap-hero__kicker {
  margin: 0 0 var(--s3);
}
.snap-hero__title {
  margin: 0;
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(38px, 6vw, 68px);
  line-height: 0.98;
  letter-spacing: 0.01em;
  color: var(--fg);
}
/* THE TITLE IS THE LINK, so it wears no underline until a pointer is on it:
   underlining the largest thing on the page would draw a rule across it. */
.snap-hero__back {
  color: inherit;
  text-decoration: none;
}
.snap-hero__back:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
}
.snap-hero__count {
  margin: var(--s4) 0 0;
  font-family: var(--body-font);
  font-size: 15px;
  color: var(--fg2);
}
/* The number in the display cut, exactly as the count line under the recipe
   page sets it: social proof as typography. */
.snap-hero__number {
  font-family: var(--display-font);
  font-size: 28px;
  font-weight: 500;
  line-height: 1;
  color: var(--fg);
}

@media (max-width: 1180px) {
  .snap-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Two across on the phone's own measure, which is what the plate grid does at
   the same width. */
@media (max-width: 700px) {
  .snap-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: var(--s5);
  }
}

/* THE SHELF'S CARD, IN A GRID. Four page columns at 1440 (three of the twelve
   each, so a card here is exactly a card there), and the portrait crop, which
   is what makes a page of one cook's work read as a contact sheet rather than
   as a row that forgot to scroll. The cards themselves are untouched: no
   border, no shadow, no hover chrome, and the whole card is the link.

   The row gap is larger than the column gap on purpose: two lines of card text
   sit under every photograph, and an even gap would read as text crowding the
   next row's crop. */
.plate-grid {
  --ar: 4 / 5;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: var(--gap);
  row-gap: var(--s6);
}

/* One card narrower at the width the rest of the site drops a column at. */
@media (max-width: 1180px) {
  .plate-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* 390: two cards across on the phone's own six column measure, which is the
   same 165px card the shelf rows show there, and the credits strip becomes
   stacked rows because three facts of eleven uppercase pixels do not survive
   being squeezed onto one line. */
@media (max-width: 700px) {
  .chef-hero__avatar {
    width: 80px;
    height: 80px;
  }
  .chef-hero__avatar--initial {
    font-size: 32px;
  }
  .chef-facts {
    flex-direction: column;
    border-bottom: 0;
  }
  .chef-facts__fact {
    padding: 12px 0;
  }
  .chef-facts__fact + .chef-facts__fact {
    border-left: 0;
    border-top: 1px solid var(--hair-soft);
  }
  .plate-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The hub shortlist in the footer of every page. Darker surface than the body
   lists, because the footer band is dark. */
.site-footer__hubs {
  width: 100%;
  margin: 4px 0 18px;
}
.site-footer__hubs-lead {
  display: inline-block;
  margin-bottom: 10px;
  color: rgba(244, 236, 224, 0.95);
  font-weight: 600;
}
.site-footer__hub-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
}
.site-footer__hub-list a {
  color: rgba(244, 236, 224, 0.72);
  font-size: 0.9rem;
  text-decoration: none;
}
.site-footer__hub-list a:hover {
  color: #fff;
  text-decoration: underline;
}

/* The "more like this" block on a recipe page sits between the steps and the
   store CTA, so it needs its own breathing room. */
.recipe__related {
  margin-top: 44px;
}

/* ---- Claiming your own chef page (/chef_profile) ---------------------------
   The one signed-in form on the site. It reuses the admin form furniture above
   (.form-group, .field, .form-errors, .form-actions) rather than growing a
   second set: the shapes are identical and a divergence would only be visible
   as drift. What is new here is the photo control, which is the site's first
   direct upload and therefore the first thing that has to show progress. */

.profile-form {
  max-width: 620px;
}
/* The two facts on this form rather than fields: the claimed handle, which
   cannot change, and the name, which is the Google account's name and changes
   there. Same treatment, because they are the same kind of thing. */
.profile-form__handle,
.profile-form__name {
  display: block;
  margin-top: 6px;
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink);
}
/* "Where you cook": ONE control, whichever world the page is in. The grouped
   select is the floor and location_picker_controller.js swaps a searchable
   combobox in over it, so this file draws both and only ever shows one.

   THE HIDDEN SELECT NEEDS ITS OWN RULE, and this is not a tidiness line: the
   `.field select` rule above sets display: block, which outranks the browser's
   own [hidden] rule, so the attribute alone would leave two controls on the
   screen. That is the exact defect the swap exists to remove.

   Focus is deliberately NOT restyled anywhere below: the site's global
   :focus-visible rule already draws a 3px paprika outline, and a local override
   would only be a chance to draw a thinner one. */
.field select[hidden] {
  display: none;
}
.location-combo {
  position: relative;
}
.location-combo[hidden] {
  display: none;
}
/* The input inherits the field's own text-input drawing (.field
   input[type="text"] above), so the combobox is the same shape, the same width
   and the same rhythm as the select it replaced and as the boxes above it. */
.location-combo__list {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  right: 0;
  left: 0;
  max-height: 280px;
  overflow-y: auto;
  padding: 4px 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper);
  box-shadow: 0 12px 28px rgba(43, 29, 22, 0.14);
}
.location-combo__list[hidden] {
  display: none;
}
/* The country, drawn for the eye only: the group carries the same words as its
   accessible name, so a screen reader hears them from the group instead. */
.location-combo__list [data-heading] {
  padding: 10px 12px 4px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* 44px, because this list is chosen from with a thumb as often as with a
   keyboard. */
.location-combo__list [role="option"] {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 6px 12px;
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--ink-2);
  cursor: pointer;
}
.location-combo__list [role="option"]:hover,
.location-combo__list [role="option"][data-active="true"] {
  background: var(--cream-2);
  color: var(--ink);
}
/* What is already chosen, so a chef opening the list can see their own answer
   in it rather than hunting for it. */
.location-combo__list [role="option"][data-chosen="true"] {
  font-weight: 700;
  color: var(--ink);
}
.location-combo__empty {
  margin: 6px 0 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--ink-soft);
}
.location-combo__empty[hidden] {
  display: none;
}
/* ONE THING TO PRESS, and the row it sits in has to look like it. .btn-signin
   is the sign-in page's block button and carries that page's own margins
   (8px over, 22px under); in this flex row those margins centred the button
   against the words beside it and left the two visibly out of line. Reset here
   rather than there, because the sign-in page still wants them. */
.profile-form .form-actions .btn-signin {
  margin: 0;
}
.profile-form__cancel {
  color: var(--ink-soft);
  font-size: 0.95rem;
}
/* Its own form (HTML has no nested ones), so it needs the gap the shared
   .form-actions row would have given it. */
.profile-form__remove {
  margin-top: 18px;
}

.avatar-field {
  display: flex;
  align-items: flex-start;
  gap: 18px;
  flex-wrap: wrap;
}
.avatar-field__preview {
  margin: 0;
  flex: 0 0 auto;
}
/* Square, cropped by the browser: the stored file is already 512px and square
   enough, and object-fit keeps a portrait from stretching before it is. */
.avatar-field__preview img,
.avatar-field__initial {
  display: block;
  width: 88px;
  height: 88px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--line);
  background: var(--cream);
}
.avatar-field__initial {
  display: grid;
  place-items: center;
  font-family: var(--display-font);
  font-size: 2rem;
  font-weight: 600;
  color: var(--paprika-dark);
}
.avatar-field__controls {
  flex: 1 1 240px;
  min-width: 200px;
}

/* A neutral button for an action that is not the submit. Deliberately plain
   next to .btn-signin, which is the one thing on the form to press. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 6px 16px;
  border: 1.5px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.btn--secondary:hover {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}

/* THE POINT OF A DIRECT UPLOAD BEING VISIBLE. The file is going to the bucket,
   not to us, so nothing else on the page moves while it does. Hidden until an
   upload starts and hidden again the moment it ends (the Stimulus controller
   owns the `hidden` attribute; this only draws it). */
.upload-progress {
  height: 6px;
  margin: 12px 0 0;
  border-radius: 999px;
  background: var(--cream-2);
  overflow: hidden;
}
.upload-progress[hidden] {
  display: none;
}
.upload-progress__bar {
  width: 0;
  height: 100%;
  background: var(--paprika);
  transition: width 120ms linear;
}

/* Where an upload failure is told. Inline, in words, next to the control that
   failed: never a browser dialog. */
.field-error {
  margin: 10px 0 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--paprika-dark);
}
.field-error[hidden] {
  display: none;
}

/* ---- The search page ------------------------------------------------------
   Warm Cinema on cream. The form is a plain GET, so this is styling only: the
   page's whole state is the query string and none of it lives in CSS.

   ALMOST NOTHING NEW IS DRAWN HERE, and that is the point. The box is the
   homepage's search object (.answer-search, declared once, up in the hero
   section) and a result is the shelf's plate card (.plate-card, declared once,
   in the shelf section). What this block adds is the three things that are this
   page's own: the GRID those cards land in instead of a track, the DISCLOSURE
   that holds the filters, and the CHIPS that undo them.

   ONE FILLED ELEMENT IN THE VIEWPORT, which on this page is the paprika submit
   inside the field. The panel's own Apply is ink rather than a second paprika
   rectangle, and the checkboxes take ink as their accent for the same reason.

   THE SUPERSEDED .search-form RULES ARE GONE rather than left behind: the old
   label-above-a-rounded-input form is not on this page any more, and nothing
   else ever used those classes. */
.search-page {
  background: var(--cream);
  padding-block: var(--s6) var(--sec-2);
}
.search-page__head {
  max-width: 46ch;
  margin-bottom: var(--s5);
}
/* Fraunces at 200, under 1.0 line height, tracking at zero or above: the one
   display cut this site self hosts, at the one size on this page that gets to
   be loud. */
.search-page__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(2.5rem, 6vw, 4rem);
  line-height: 0.98;
  letter-spacing: 0.004em;
  color: var(--ink);
  margin: 16px 0 0;
}

/* Where this search is standing, and the two ways out of it. A sentence with
   its escape hatches beside it, never a chip: the country is not one filter
   among five, it is the pool everything else narrows. */
.search-scope {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 14px;
  margin: var(--s4) 0 0;
  font-family: var(--body-font);
  font-size: 14px;
  color: var(--ink-2);
}
.search-scope__flag {
  font-size: 17px;
  line-height: 1;
}
.search-scope__link {
  font-size: 12.5px;
  color: var(--ink-3);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition: color var(--t-quick) var(--e-out-cubic), border-color var(--t-quick) var(--e-out-cubic);
}
.search-scope__link:hover {
  color: var(--ink);
  border-bottom-color: var(--ink-3);
}

.search-page__form {
  margin-bottom: var(--s5);
  max-width: var(--card6);
}
/* The field is the hero's object verbatim. Only its position changes: there it
   opens a page, here it sits under a heading that has already spoken. */
.search-page__form .answer-search {
  margin: 0;
  max-width: none;
}

/* --- The filter disclosure ------------------------------------------------
   A NATIVE <details>, WHICH IS THE WHOLE FEATURE. It opens on a click and on
   Enter, announces itself as expanded or collapsed, and is findable by browser
   text search when shut, before a single byte of JavaScript has run. A div and
   a controller would have had to be taught all four. */
.filters {
  margin-top: var(--s2);
}
.filters__summary {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  width: fit-content;
  min-height: var(--tap);
  font-family: var(--body-font);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  cursor: pointer;
  /* Both lines are needed to drop the UA disclosure triangle: the pseudo
     element is WebKit's, `list-style` is everybody else's. */
  list-style: none;
}
.filters__summary::-webkit-details-marker {
  display: none;
}
.filters__summary:focus-visible {
  outline: 2px solid var(--paprika);
  outline-offset: 3px;
}
/* How many filters are on, when the panel is shut and cannot say so itself.
   Ink rather than paprika: it is a count, not an alarm. */
.filters__badge {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--r-chip);
  background: var(--ink);
  color: var(--cream);
  font-size: 10.5px;
  font-weight: 700;
}
.filters__caret {
  width: 11px;
  height: 11px;
  color: var(--ink-4);
  transition: transform var(--t-quick) var(--e-out-cubic);
}
.filters[open] .filters__caret {
  transform: rotate(180deg);
}
/* AUTO FITTING COLUMNS, WHICH IS WHAT MAKES 390px DELIBERATE RATHER THAN
   SURVIVED: at 190px minimum the panel is three columns on a desktop, two on a
   tablet and ONE FULL WIDTH STACK on a phone, with no breakpoint of its own. */
.filters__panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--s4);
  margin-top: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--cream-2);
}
/* fieldset's UA border, padding and margin all have to go, or two of these five
   groups would be drawn in a box the other three are not. */
.filters__group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
/* The two tag lists are ten and seven boxes wide; in a 190px column they would
   be a stack of ten lines beside two lonely selects. */
.filters__group--wide {
  grid-column: 1 / -1;
}
.filters__legend {
  display: block;
  padding: 0;
  margin-bottom: 10px;
  font-family: var(--body-font);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.filters__select {
  width: 100%;
  min-height: var(--tap);
  padding: 10px 12px;
  font-family: var(--body-font);
  font-size: 14.5px;
  color: var(--ink);
  background: var(--cream);
  border: 1px solid var(--rule);
  border-radius: var(--r);
}
.filters__select:focus-visible {
  outline: 2px solid var(--paprika);
  outline-offset: 2px;
}
.filters__boxes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
}
.filters__box {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  font-family: var(--body-font);
  font-size: 14px;
  color: var(--ink-2);
}
.filters__box input {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  accent-color: var(--ink);
}
.filters__box label {
  cursor: pointer;
}
.filters__actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}
/* .btn-cta is a 320px wide paprika block built for a marketing page, and both
   of its defining properties are wrong here: this button sits in a row beside a
   quiet link, and the paprika in this viewport is already spent on the submit
   inside the field. Two classes deep because .btn-cta declares the same
   properties and specificity is what settles it. */
.filters .filters__apply {
  width: auto;
  max-width: none;
  min-height: var(--tap);
  padding: 0 22px;
  font-size: 14px;
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
  box-shadow: none;
}
.filters .filters__apply:hover {
  background: var(--ink-2);
}
.filters__clear {
  font-family: var(--body-font);
  font-size: 12.5px;
  color: var(--ink-3);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
}
.filters__clear:hover {
  color: var(--ink);
  border-bottom-color: var(--ink-3);
}

/* --- The active filter chips ----------------------------------------------
   PILLS, WHICH IS THE ONE SHAPE THE SHEET ALLOWS TO BE ROUND. Each is a link to
   the same search without one filter, so the whole affordance works with
   scripting off and a middle click opens it in a tab like any other link. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: var(--s5);
}
.filter-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: var(--r-chip);
  background: var(--cream-2);
  font-family: var(--body-font);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink-2);
  text-decoration: none;
  transition: color var(--t-quick) var(--e-out-cubic), border-color var(--t-quick) var(--e-out-cubic);
}
/* Drawn at 34px, tapped at 44, through an invisible expander rather than by
   inflating a pill until a row of them reads as buttons. */
.filter-chip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--tap);
  transform: translateY(-50%);
}
.filter-chip:hover {
  border-color: var(--ink-3);
  color: var(--ink);
}
.filter-chip__remove {
  font-size: 14px;
  line-height: 1;
  color: var(--ink-4);
}
/* The way to drop all of them: the same pill, unfilled, so it reads as the end
   of the row rather than as another filter that is on. */
.filter-chip--clear {
  background: none;
  border-style: dashed;
  color: var(--ink-3);
}

/* --- The results ---------------------------------------------------------- */
.search-page__count {
  margin: 0 0 var(--s4);
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--ink-3);
}
/* The prompts and the empty states, in the serif the sheet gives editorial body
   copy. A measure, because these are sentences rather than labels. */
.search-page__note {
  margin: 0;
  max-width: 52ch;
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--ink-2);
}
/* Turning the page: two links with the position between them, centred under the
   grid. It reads as the end of the results rather than as a control panel, which
   is why there is no row of numbered buttons and no box around it. */
.search-pagination {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--s4);
  margin-top: var(--s6);
}
.search-pagination__link {
  font-family: var(--body-font);
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid var(--ink-4);
  padding-bottom: 2px;
}
.search-pagination__link:hover {
  color: var(--ink);
  border-color: var(--ink-2);
}
/* The one line that is not a link, so it sits quieter than the two that are. */
.search-pagination__position {
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--ink-3);
}
.search-page__fallback {
  margin-top: var(--sec-2);
}

@media (prefers-reduced-motion: reduce) {
  .filters__caret,
  .filter-chip,
  .search-scope__link {
    transition: none;
  }
}

/* ==========================================================================
   The 404 page (app/views/errors/not_found.html.erb)
   ==========================================================================

   The smallest page on the site, and deliberately the search page's head with
   nothing under it: same cream section, same kicker, same Fraunces line, same
   measure. A visitor who lands here has already seen that masthead on the page
   they came from, so a 404 built out of anything else would read as a different
   website having caught them.

   FLOOR PADDING, NOT A FULL SCREEN. Nothing follows this section, so the footer
   would otherwise ride up under a short paragraph. */
.not-found {
  background: var(--cream);
  padding-block: var(--s6) var(--sec-2);
}
.not-found__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(2.5rem, 6vw, 4rem);
  line-height: 0.98;
  letter-spacing: 0.004em;
  color: var(--ink);
  margin: 16px 0 0;
}
.not-found__body {
  margin: var(--s4) 0 0;
  max-width: 46ch;
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--ink-2);
}
/* The two ways back, side by side rather than stacked: they are alternatives,
   not a step and its afterthought. */
.not-found__ways {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 24px;
  margin: var(--s5) 0 0;
}
/* The CTA is written for a hero, where it fills its column. Here it sits next to
   a text link and has to be the width of its own label. */
.not-found__ways .btn-cta {
  width: auto;
}
.not-found__link {
  font-size: 0.95rem;
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition: color var(--t-quick) var(--e-out-cubic), border-color var(--t-quick) var(--e-out-cubic);
}
.not-found__link:hover {
  color: var(--ink);
  border-bottom-color: var(--ink-3);
}
@media (prefers-reduced-motion: reduce) {
  .not-found__link {
    transition: none;
  }
}
/* The header entry point. Hidden on small screens, where the header already
   collapses its section links and the page has its own search field. */
.nav__search {
  display: none;
}
@media (min-width: 900px) {
  .nav__search {
    display: inline-flex;
  }
}

/* button_to wraps its button in a form, which is a block element and would
   otherwise push the quiet "start again" onto a line of its own, away from the
   sentence that explains it. */
.account-card__form {
  display: inline;
}

/* ---- Print and share, under a recipe's title -----------------------------
   A quiet row: this is a utility bar, not a call to action, so it sits at chip
   weight rather than competing with the h1 above it or the lede below. */
.recipe__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 18px 0 4px;
}

.recipe__action {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 7px 14px;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-soft);
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}

.recipe__action:hover,
.recipe__action:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

/* Print once led the row with accent weight; the owner ruled it read as a
   darker outlier beside the quiet marks (2026-08-11), so it now wears exactly
   the base button's clothes and earns its colour on hover like everyone. */

/* The four share targets are the MARK ALONE, so they are round and equal, sized
   by the icon rather than by a word. 38px is the floor a touch target wants;
   the 18px icon sits centred in it. */
.recipe__action--icon {
  width: 38px;
  height: 38px;
  padding: 0;
  justify-content: center;
}

/* The mark inherits the button's colour, which is what makes one hover rule per
   network enough: set the colour, and the logo follows. */
.recipe__action-icon {
  display: block;
  flex: none;
}

/* Each network's own colour, on hover and on keyboard focus only. At rest the row
   stays quiet and monochrome, so four brand colours are not competing with the
   recipe title directly above them. */
.recipe__action--whatsapp:hover,
.recipe__action--whatsapp:focus-visible { border-color: #25d366; color: #25d366; }
.recipe__action--facebook:hover,
.recipe__action--facebook:focus-visible { border-color: #0866ff; color: #0866ff; }
.recipe__action--telegram:hover,
.recipe__action--telegram:focus-visible { border-color: #26a5e4; color: #26a5e4; }
.recipe__action--x:hover,
.recipe__action--x:focus-visible { border-color: #000; color: #000; }
.recipe__action--pinterest:hover,
.recipe__action--pinterest:focus-visible { border-color: #e60023; color: #e60023; }

/* The copy confirmation. Empty until something happens, so it must not reserve a
   line and push the lede down on every page that never uses it. */
.recipe__copied {
  margin: 0;
  font-size: 0.82rem;
  color: var(--ink-soft);
}
.recipe__copied:empty { display: none; }
/* --- Shelf -----------------------------------------------------------------
   The Plate Cards, the rows they scroll in, and the billboard, shared by the
   homepage and every /communities/:country page. Drawn to the Warm Cinema v2
   sheet: cards span whole page columns, the row bleeds off the right edge of
   the viewport with a mandatory snap inset at the gutter, and NO CARD CARRIES
   BORDER, SHADOW, BACKGROUND OR HOVER CHROME. The only motion on a card is the
   photograph moving inside a fixed crop, so a row never reflows under a
   pointer.

   THE ROW SCROLLS WITHOUT JAVASCRIPT. Everything below is a track that
   overflows with CSS scroll snap, so touch, trackpad and keyboard all work from
   the moment the server sends the page; shelf_controller only adds the two
   buttons a mouse wants, and they ship hidden until it has measured an
   overflow.

   GEOMETRY IS A ROW LEVEL DECISION, passed in as `variant:` and landing here as
   --ar plus one modifier class. No two consecutive rows on a page share a card
   shape, which is what stops seven rows reading as one wallpaper.
   ------------------------------------------------------------------------- */

.shelf {
  position: relative;
  margin: 0 0 var(--sec-2);
}

.shelf__head {
  width: var(--cw);
  margin-inline: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap);
  padding-bottom: var(--s4);
}

/* FRAUNCES AT SECTION SIZE. The row heading is editorial voice, not interface
   chrome, and it is the type role the whole band is built to carry. */
.shelf__title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(22px, 2.2vw, 28px);
  line-height: 1.2;
  letter-spacing: 0.006em;
  color: var(--fg);
  text-wrap: balance;
  max-width: 22ch;
  margin: 0;
}

.shelf__tools {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* Drawn as an 11px rule-underlined label and tapped at 44px, through an
   invisible expander rather than by inflating the shape. */
.shelf__see-all {
  position: relative;
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg3);
  white-space: nowrap;
  text-decoration: none;
  border-bottom: 1px solid var(--hair);
  padding-bottom: 3px;
  transition: color var(--t-quick) var(--e-out-cubic), border-color var(--t-quick) var(--e-out-cubic);
}
.shelf__see-all::after {
  content: "";
  position: absolute;
  left: -8px;
  right: -8px;
  top: 50%;
  height: var(--tap);
  transform: translateY(-50%);
}
.shelf__see-all:hover {
  color: var(--fg);
  border-bottom-color: var(--fg3);
}

.shelf__arrow {
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  color: var(--fg3);
  border: 1px solid var(--hair);
  border-radius: var(--r-chip);
  cursor: pointer;
  position: relative;
  transition: border-color var(--t-quick) var(--e-out-cubic), color var(--t-quick) var(--e-out-cubic);
}
.shelf__arrow::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap);
  height: var(--tap);
  transform: translate(-50%, -50%);
}
.shelf__arrow:hover {
  border-color: var(--fg3);
  color: var(--fg);
}
/* At an end the button stays in place rather than disappearing: a control that
   vanishes under the pointer moves everything beside it. */
.shelf__arrow[disabled] {
  opacity: 0.35;
  cursor: default;
}
.shelf__arrow[disabled]:hover {
  border-color: var(--hair);
  color: var(--fg3);
}
/* `hidden` has to beat the inline-flex above, or the buttons the controller
   never revealed would sit on every row. */
.shelf__arrow[hidden] {
  display: none;
}

/* THE TRACK BLEEDS OFF THE RIGHT EDGE OF THE VIEWPORT. It starts at --edge, so
   the first card lines up with the heading above it, and it ends at --edge on
   the other side so the last card is cropped by the page rather than by a box.
   `mandatory` snapping with the inset at the gutter is what makes a flick land
   on a card edge every time; the mask is the peek that says the row continues.

   grid-auto-columns rather than a width on the card, because the card width is
   a property of the ROW (three page columns, or four for the wide variant) and
   a card that carried its own width could not be reused between them. */
.shelf__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--dish);
  gap: var(--gap);
  padding-inline: var(--edge);
  padding-block: 2px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: var(--edge);
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 72px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 72px), transparent 100%);
}
.shelf__track::-webkit-scrollbar {
  display: none;
}
.shelf__track > * {
  scroll-snap-align: start;
}
/* The three geometries a row may ask for. The wide one is the only variant that
   also changes the column span, which is what keeps it from being merely the
   same card in a letterbox. */
.shelf__track--portrait {
  --ar: 4 / 5;
}
.shelf__track--landscape {
  --ar: 3 / 2;
}
.shelf__track--wide {
  --ar: 16 / 9;
  grid-auto-columns: var(--wide);
}

/* One dish. The whole card is the anchor, and it wears nothing: no border, no
   background, no shadow, no lift. */
.plate-card {
  position: relative;
  display: flex;
  flex-direction: column;
  color: var(--fg);
  text-decoration: none;
}

/* THE CROP IS FIXED AND THE PHOTOGRAPH MOVES INSIDE IT. --ar is set on the
   track by the row's variant, so every card in a row shares one shape and the
   row is one height whether the photos have arrived or not. */
.plate-card__photo {
  position: relative;
  overflow: hidden;
  border-radius: var(--r);
  aspect-ratio: var(--ar, 3 / 2);
  background: var(--well);
  display: flex;
  align-items: center;
  justify-content: center;
}
.plate-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.plate-card__emoji {
  font-size: 1.9rem;
  line-height: 1;
}
/* NO PHOTOGRAPH, SAME BOX. The crop keeps its shape and holds the cuisine's
   glyph on the surface's own well tone, so an unphotographed dish is a quiet
   card in a row rather than a hole in it. */
.plate-card__photo--placeholder,
.billboard__photo--placeholder {
  color: var(--fg3);
}

.plate-card__body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 14px;
}

/* Fraunces at card size, in the one weight the site self hosts. THE CLAMP IS
   STRUCTURAL, not cosmetic: one four line name in a row of twelve would drop
   that card's meta line below every other card's. */
.plate-card__name {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 20px;
  line-height: 1.22;
  letter-spacing: 0.004em;
  color: var(--fg);
  margin: 0;
  max-width: 20ch;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.plate-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin: 0;
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.012em;
  color: var(--fg3);
}
.plate-card__chef,
.plate-card__minutes {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}
/* A long name shortens rather than pushing the minutes onto a second line. */
.plate-card__chef {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.plate-card__avatar {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: var(--r-chip);
  object-fit: cover;
}
.plate-card__clock {
  flex: 0 0 auto;
  color: var(--fg4);
}
/* THE SECOND FACT, AND ONLY WHERE ONE WAS ASKED FOR (see the partial's
   with_effort local: the recipe page's rails, and nothing else). Middot
   separated exactly as .catalog-card__fact is, because it is the same sentence
   read the same way. The separator is drawn by the SIBLING selector rather than
   on the word itself, so a dish with no minutes on it does not open its meta
   line with a floating middot. */
.plate-card__minutes + .plate-card__effort::before {
  content: "\00B7";
  padding-right: 6px;
  color: var(--fg4);
}

/* THE COMPACT CARD: the same card with one axis turned, worn by the recipe
   page's related rails and by its composed plate.

   IT IS DECLARED HERE, AFTER THE CARD IT MODIFIES, AND THAT IS LOAD BEARING. It
   used to be declared up in the related rails section, sixteen hundred lines
   ABOVE .plate-card, where `display: grid` and `display: flex` are one class
   each, tie on specificity, and the later one wins: every compact card on the
   site was silently a column flex with `align-items: center` on it, so the crop
   never took a track width and collapsed to a shrink-to-fit box around an
   unloaded image. That is what made the rail read as leftover and the plate's
   tiles read as very large. A modifier goes after what it modifies.

   SAME PARTIAL, SAME ANATOMY, ONE AXIS TURNED. A shelf card is a wide crop with
   the name under it, and twelve of those stacked down a 320px rail would be
   roughly 3,400px of column beside a recipe: a suggestion nobody scrolls to is
   not a suggestion. Here the crop stands at the left and the name and the facts
   stand beside it, so a rail of four is one glance.

   THE CROP IS 4/3, WHICH IS THE SITE'S CARD CROP. It was a square, chosen when
   the crop was 96px and a 3/2 at that width was a 64px strip. 112px is wide
   enough to hold a real photograph at the shape .catalog-card uses on every hub
   and search grid, so a suggestion in the rail is recognisably the same object
   as the thing it is suggesting. 194px is left for the name in a 320px rail,
   which the two line clamp holds at a fixed height.

   NOTHING IS ADDED TO THE CARD. No border, no background, no shadow, no hover
   chrome. The photograph moving inside its fixed crop is the whole interaction,
   inherited from .plate-card above rather than restated, which is the point of
   reusing it instead of drawing a second dish card for one column of one page.
   The hairline between rail cards belongs to the LIST (.rail__list), not here.
   The composed plate's smaller scale belongs to .plate__dishes, for the same
   reason: a caller's proportions are the caller's business. */
.plate-card--compact {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr);
  column-gap: 14px;
  align-items: center;
}
.plate-card--compact .plate-card__photo { aspect-ratio: 4 / 3; }
.plate-card--compact .plate-card__body { padding-top: 0; gap: 5px; }
.plate-card--compact .plate-card__name {
  font-size: 17px;
  /* The 20ch measure belongs to a card 306px wide. Beside the crop the name has
     about 194px and the two line clamp is already what holds the height. */
  max-width: none;
}
/* The catalog card's own meta spacing, because this line is now the catalog
   card's own sentence: how long, then how much work, middot separated. */
.plate-card--compact .plate-card__meta { gap: 2px 6px; }
.plate-card--compact .plate-card__emoji { font-size: 1.5rem; }

/* --- The billboard --------------------------------------------------------
   The band's one oversized moment: a full bleed photograph with the copy laid
   over its dark end. Not a card. It is the only place on the page where the
   image is the ground rather than a thing sitting on it. */
.billboard {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: block;
  height: min(68vh, 620px);
  min-height: 470px;
  margin-block: var(--sec) var(--sec);
  color: var(--lit);
  text-decoration: none;
  background: var(--night-2);
}
.billboard__photo {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.billboard__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1.06);
  will-change: transform;
}
.billboard__emoji {
  font-size: 3.4rem;
  line-height: 1;
}
/* The gradient that makes type legible over a photograph, angled so the copy
   end is nearly opaque and the far end is almost untouched. */
.billboard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    96deg,
    rgba(20, 13, 9, 0.96) 0%,
    rgba(20, 13, 9, 0.88) 26%,
    rgba(20, 13, 9, 0.44) 58%,
    rgba(20, 13, 9, 0.05) 90%
  );
}
.billboard__copy {
  position: relative;
  z-index: 2;
  width: var(--cw);
  margin-inline: auto;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-block: var(--s6);
}
.billboard__kicker {
  font-family: var(--body-font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--lit-3);
  margin: 0;
}
/* The one display line inside the band, and the second of the two roles that
   carry the 200 / opsz 144 cut. */
.billboard__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(38px, 6.6vw, 95px);
  line-height: 0.94;
  letter-spacing: 0.005em;
  color: var(--lit);
  text-wrap: balance;
  max-width: 15ch;
  margin: 18px 0 0;
}
.billboard__chef {
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--lit-3);
}
.billboard__lede {
  font-family: var(--display-font);
  font-size: 19px;
  line-height: 1.55;
  max-width: 44ch;
  text-wrap: pretty;
  color: var(--lit-2);
  margin: 20px 0 0;
}
.billboard__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
  margin: var(--s5) 0 0;
}
/* A SPAN THAT LOOKS LIKE A BUTTON. The whole billboard is the anchor, so a real
   link here would be an anchor inside an anchor. This is the band's one filled
   element. */
.billboard__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 13px 22px;
  border-radius: var(--r);
  background: var(--paprika);
  color: var(--cream);
  font-family: var(--body-font);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1;
  transition: background-color var(--t-quick) var(--e-out-cubic);
}
.billboard:hover .billboard__cta {
  background: var(--paprika-deep);
}
.billboard__minutes {
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--lit-3);
}

/* Narrow: the billboard stacks into a portrait with the copy at its foot, which
   is the only shape that keeps a 95px title off a 390px screen. */
@media (max-width: 700px) {
  .billboard {
    height: auto;
    min-height: 0;
    aspect-ratio: 4 / 5;
    margin-block: var(--sec-2);
  }
  .billboard::after {
    background: linear-gradient(
      3deg,
      rgba(20, 13, 9, 0.94) 8%,
      rgba(20, 13, 9, 0.55) 42%,
      rgba(20, 13, 9, 0.04) 78%
    );
  }
  .billboard__title {
    font-size: 36px;
    line-height: 0.98;
    max-width: 14ch;
  }
  .billboard__lede {
    display: none;
  }
  .billboard__copy {
    padding-block: var(--s4);
  }
}

/* ==========================================================================
   THE COMMUNITY PAGES
   /communities/:country is the homepage's composition told about one kitchen:
   a masthead at 121px, the country's own cinema band, a cook's own words full
   bleed halfway down it, and the people who keep the shelf, back on cream, at
   the end. /communities is the grid that opens them. Canonical drawing:
   the COMMUNITY PAGE section of
   docs/superpowers/design/2026-08-09-warm-cinema-v2.html.

   ALMOST NOTHING NEW IS DECLARED HERE. The hero grid, the chip rail, the search
   field, the shelf rows and the billboard are the homepage's own components,
   used as they are; what follows is the masthead's own size, the two blocks
   this page has that no other page does, and the index grid.
   ------------------------------------------------------------------------- */

/* THE MASTHEAD. 8.4vw lands on 121px at 1440 and the clamp holds it between 56
   and 124, which is bigger than the homepage's question on purpose: that page
   asks something, this one names a place. Weight 200, line-height under 1.0 and
   positive tracking, like every display role on the site. */
.community-hero__kicker {
  display: flex;
  align-items: center;
  gap: 10px;
}
.community-hero__flag {
  font-size: 15px;
  line-height: 1;
}
.community-hero__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(56px, 8.4vw, 124px);
  line-height: 0.9;
  letter-spacing: 0.005em;
  text-wrap: balance;
  max-width: 8ch;
  margin: 22px 0 0;
  color: var(--fg);
}
.community-hero__lede {
  margin: 32px 0 0;
}
.community-hero .answer-search {
  margin-top: var(--s5);
}
.community-hero .country-chips {
  margin-top: var(--s5);
}
.community-hero__meta {
  margin-top: 26px;
  max-width: none;
}

/* The field's country segment, on the page where the kitchen is not a choice:
   the same segment, stated rather than offered, so it reads as a fact about
   where the visitor is standing rather than as a control that does nothing. */
.answer-search__lens--fixed {
  cursor: default;
}

/* --- The cook's quote -----------------------------------------------------
   THE EDITORIAL INTERRUPTION, and the one full bleed element on the site: a
   photograph edge to edge with a gradient carrying the words across it. It is
   NOT a second billboard, which is the point of it: the band's loud moment is a
   dish, and this one is a person. */
.cook-quote {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  height: min(46vh, 400px);
  min-height: 320px;
  margin: 0 0 var(--sec-2);
}
.cook-quote__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The gradient is the reason the words are legible over any photograph: it
   darkens the end they land on and lets the other end stay a photograph. */
.cook-quote::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    88deg,
    rgba(20, 13, 9, 0.93) 0%,
    rgba(20, 13, 9, 0.7) 40%,
    rgba(20, 13, 9, 0.15) 85%
  );
}
.cook-quote__copy {
  position: relative;
  z-index: 2;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.cook-quote__words {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: clamp(26px, 2.9vw, 40px);
  line-height: 1.14;
  letter-spacing: 0.005em;
  text-wrap: pretty;
  color: var(--lit);
  max-width: 24ch;
  margin: 0;
}
.cook-quote__credit {
  margin-top: 20px;
  color: var(--lit-3);
}
/* Drawn as one quiet line and tapped at 44px, like every other small target on
   the site. */
.cook-quote__credit a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--night-rule);
  transition: color var(--t-quick) var(--e-out-cubic),
    border-color var(--t-quick) var(--e-out-cubic);
}
.cook-quote__credit a:hover {
  color: var(--lit);
  border-bottom-color: var(--lit-3);
}

/* --- Who keeps this shelf -------------------------------------------------
   Back on cream, because the people are not part of the cinema. A split of a
   heading against a list of rows, each row one anchor: a photo link beside a
   name link is a small target next to a big one and two stops for anybody
   tabbing down it. */
.keepers {
  background: var(--cream);
  padding-block: var(--sec-2);
}
.keepers__split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
  align-items: start;
}
.keepers__title {
  margin-top: 20px;
}
.keepers__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.keeper {
  border-top: 1px solid var(--rule-soft);
}
.keeper:last-child {
  border-bottom: 1px solid var(--rule-soft);
}
.keeper__link {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) auto;
  gap: var(--s4);
  align-items: center;
  padding-block: var(--s4);
  min-height: var(--tap);
  color: var(--ink);
  text-decoration: none;
}
.keeper__avatar {
  width: 36px;
  height: 36px;
  border-radius: var(--r-chip);
  object-fit: cover;
  background: var(--cream-3);
  display: block;
}
/* The initial, for a cook with no photograph. Espresso on cream, which is the
   one place the band's ground appears in daylight. */
.keeper__avatar--initial {
  display: grid;
  place-items: center;
  background: var(--night);
  color: var(--lit);
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 16px;
  line-height: 1;
}
.keeper__body {
  display: block;
  min-width: 0;
}
.keeper__name {
  display: block;
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 20px;
  line-height: 1.25;
  letter-spacing: 0.006em;
  text-underline-offset: 3px;
}
.keeper__bio {
  display: block;
  margin-top: 2px;
  max-width: 46ch;
}
.keeper__where {
  white-space: nowrap;
}
/* The row wears no chrome, so the hover is on the name alone. */
.keeper__link:hover .keeper__name {
  text-decoration: underline;
}

/* --- The index of every country -------------------------------------------
   A CARD IS FOUR PHOTOGRAPHS AND A NAME. Twenty three country names in a column
   say nothing anybody can choose between; a country is its food. No border, no
   shadow, no background: the mosaic is the card. */
.communities {
  padding-block: var(--s6) var(--sec);
}
.communities__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(40px, 5.6vw, 84px);
  line-height: 0.94;
  letter-spacing: 0.005em;
  text-wrap: balance;
  max-width: 16ch;
  margin: 22px 0 0;
}
.communities__lede {
  margin: 28px 0 0;
}
.community-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  column-gap: var(--gap);
  row-gap: var(--s6);
  margin-top: var(--sec-2);
}
.community-card {
  display: block;
  color: var(--ink);
  text-decoration: none;
}
/* THE MOSAIC IS A SQUARE, ALWAYS, so the grid is one rhythm however many
   photographs a country turns out to have. The 2px gap is the cream showing
   through rather than a border drawn around anything. */
.community-card__photo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  gap: 2px;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--cream-3);
}
.community-card__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* A community with fewer than four photographed dishes fills the square with
   what it has rather than leaving a hole in it. */
.community-card__photo img:only-child {
  grid-column: span 2;
  grid-row: span 2;
}
.community-card__photo img:first-child:nth-last-child(2),
.community-card__photo img:first-child:nth-last-child(2) ~ img {
  grid-row: span 2;
}
.community-card__photo img:first-child:nth-last-child(3) {
  grid-row: span 2;
}
.community-card__body {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 14px;
}
.community-card__flag {
  font-size: 15px;
  line-height: 1;
}
.community-card__name {
  font-family: var(--display-font);
  font-weight: 400;
  font-size: 23px;
  line-height: 1.25;
  letter-spacing: 0.006em;
  text-underline-offset: 3px;
}
.community-card:hover .community-card__name {
  text-decoration: underline;
}
.community-card__count {
  margin-left: auto;
  font-family: var(--body-font);
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--ink-3);
  white-space: nowrap;
}

/* Narrow: the split becomes one column, and the quote turns from a wide band
   into a portrait with the words at its foot, which is the only shape that
   keeps a 40px blockquote on a 390px screen. */
@media (max-width: 900px) {
  .keepers__split {
    grid-template-columns: 1fr;
    gap: var(--s6);
  }
}
@media (max-width: 700px) {
  .community-hero__title {
    max-width: 10ch;
  }
  .cook-quote {
    height: auto;
    min-height: 0;
    aspect-ratio: 3 / 2;
  }
  .cook-quote::after {
    background: linear-gradient(
      4deg,
      rgba(20, 13, 9, 0.94) 10%,
      rgba(20, 13, 9, 0.5) 55%,
      rgba(20, 13, 9, 0.06) 90%
    );
  }
  .cook-quote__copy {
    justify-content: flex-end;
    padding-block: var(--s4);
  }
  .cook-quote__words {
    font-size: 21px;
    max-width: none;
  }
  .community-grid {
    /* minmax(0, 1fr), never bare 1fr: the card body's one-line row carries a
       nowrap dish count, and 1fr's min-content floor would let two of those
       rows push the page past a phone's right edge. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    row-gap: var(--s5);
  }
  .community-card__name {
    font-size: 19px;
  }
  /* On a half-width card the count leaves the name's line rather than
     squeezing it; margin-left: auto keeps it against the right edge on its
     own line, which reads as a caption instead of a casualty. */
  .community-card__body {
    flex-wrap: wrap;
  }
  .keeper__link {
    grid-template-columns: 36px minmax(0, 1fr);
    gap: 14px;
  }
  .keeper__where {
    display: none;
  }
}

/* MOTION IS OPT IN, ALL OF IT. Nothing below runs for a visitor who asked for
   less of it: the hover crop, the billboard's slow drift and the steam curl's
   breath are each drawn only inside this query, so the reduced-motion state is
   the absence of a rule rather than a rule that turns one off. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes drift {
    from {
      transform: scale(1.06) translate3d(0, 0, 0);
    }
    to {
      transform: scale(1.13) translate3d(-1.8%, -1.4%, 0);
    }
  }
  @keyframes breathe {
    0%,
    100% {
      transform: translateY(0);
      opacity: 0.62;
    }
    50% {
      transform: translateY(-7px);
      opacity: 0.9;
    }
  }

  /* The photograph moves inside the crop. The card never reflows. */
  .plate-card__img {
    transition: transform var(--t-image) var(--e-out-quart);
  }
  .plate-card:hover .plate-card__img,
  .plate-card:focus-within .plate-card__img {
    transform: scale(1.04);
  }

  /* The same one gesture on the hub and search card, because it is the same
     idea: the crop is fixed, the photograph moves inside it, and nothing on the
     card is lifted, outlined or tinted on the way past. */
  .catalog-card__img {
    transition: transform var(--t-image) var(--e-out-quart);
  }
  .catalog-card:hover .catalog-card__img,
  .catalog-card:focus-within .catalog-card__img {
    transform: scale(1.04);
  }

  /* The same rule on the index's mosaic: the photographs move inside their
     fixed crops and the card never reflows. One transform for the whole tile
     set, so a mosaic reads as one object rather than as four pictures. */
  .community-card__photo img {
    transition: transform var(--t-image) var(--e-out-quart);
  }
  .community-card:hover .community-card__photo img,
  .community-card:focus-within .community-card__photo img {
    transform: scale(1.04);
  }

  /* The one slow ambient gesture on the page. */
  .billboard__img {
    animation: drift 46s var(--e-in-out) infinite alternate;
  }
  .steam--hero {
    animation: breathe 9s var(--e-in-out) infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shelf__see-all,
  .shelf__arrow,
  .billboard__cta,
  .cook-quote__credit a {
    transition: none;
  }
  .shelf__track {
    scroll-behavior: auto;
  }
}

/* --- The recipe page's desktop grid and cook card -------------------------

   THE PAGE IS ONE COLUMN UNTIL 1100px AND TWO ABOVE IT, and nothing below this
   line changes anything under that width: the markup is written in the order the
   narrow page reads in, and the grid only picks it up and places it. That is why
   the related rails can sit in the rail on a wide screen without being rendered
   a second time.

   THE STICKINESS BUDGET FOR THIS SITE IS THREE, and all three are spent: the
   site header, the ingredient column, and the cook card. The research counted
   three sticky layers on Serious Eats and AllRecipes taking about 30% of an
   812px viewport permanently away from the recipe, plus a floating video with a
   close button. A fourth `position: sticky` anywhere in this file has to be
   argued for against spec/requests/recipe_cook_card_spec.rb, which counts them.
*/

/* The article is a plain block until the grid switches on, and wider than the
   680px measure it keeps for the narrow page. */
@media (min-width: 1100px) {
  .recipe {
    max-width: var(--maxw);
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    grid-template-areas:
      "masthead masthead"
      "body     rail"
      "tail     tail";
    column-gap: 56px;
    align-items: start;
  }
  /* THE MASTHEAD PAYS FOR THE SPACE UNDER ITSELF, because up here nobody else
     does. This grid has a column gap and no row gap, and the block that follows
     gave its own top margin away three rules below (the two cooking columns have
     to start level, or the hairline is drawn down one of them), so "Ingredients"
     and "How to cook" ended up sitting on the photograph above them.

     THE SPACE BELONGS TO THE ROW THAT ENDS, not to either column that begins:
     one declaration on an element that spans both, so the rail moves down with
     the body and the two stay level, and the body-to-tail boundary further down
     is left exactly as it was. A row gap on the grid would have moved that one
     too, on top of the margin the tail's first section already carries.

     40px is not a number picked to look right: it is .recipe__section's own top
     margin, which is precisely what this boundary renders as below 1100px, so
     the wide page keeps the narrow page's rhythm rather than inventing one. */
  .recipe__masthead {
    grid-area: masthead;
    margin-block-end: 40px;
  }
  .recipe__body { grid-area: body; }
  .recipe__rail { grid-area: rail; }
  .recipe__tail { grid-area: tail; }

  /* Ingredients against steps, at NYT's ratio. */
  .recipe__cooking {
    display: grid;
    grid-template-columns: 38fr 62fr;
    column-gap: 32px;
    align-items: start;
  }

  /* Side by side, the hairline between them is a rule drawn down only one of the
     two columns, which reads as a mistake rather than as a divider. Stacked,
     below this width, it is exactly right and stays. */
  .recipe__cooking > .recipe__section {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }

  /* STICKY, WITH A MAX HEIGHT, and the guard is not optional. Cookpad's 268px
     ingredient column works because Cookpad's lists are short; this catalog
     holds twenty line South Asian lists, and a sticky column taller than the
     viewport pins its own top and hides its own bottom forever. */
  .recipe__ingredients {
    position: sticky;
    top: 88px;
    max-height: calc(100vh - 120px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The one pinned thing in the rail. The rest of the rail scrolls under it,
     which is what stops a rail taller than the viewport from becoming
     unreachable.

     UNDER IT MEANS UNDER IT, AND THAT NEEDED SAYING. Sticky is a positioned
     value, so the card joined the painting pass for positioned boxes at
     z-index: auto. Every card in "More to cook" below it is .plate-card, which
     is `position: relative` for its own crop, so those are in that same pass at
     that same level, and within one level the painter goes in TREE ORDER. The
     cook card is written first in .recipe__rail and the rails follow it, so the
     suggestions were painted last and drew straight over the pinned card the
     moment it slid down far enough to meet them. Nothing here was a stacking
     context of its own: .recipe, .recipe__rail and .plate-card all leave
     z-index at auto, so both were competing directly in the root context and
     the DOM decided it.

     z-index: 2 is the whole fix, and the background above is what makes it an
     opaque card rather than a transparent one that wins. It is deliberately a
     small number: the site header is a sticky 10 and the toasts are 60, and a
     card in the rail must never come out over either of them. */
  .cook-card {
    position: sticky;
    top: 88px;
    z-index: 2;
  }
}

/* THE 710px MEDIA COLUMN NEEDS A WIDER PAGE THAN THE RAIL DOES. Beside a fixed
   710px frame at 1100px the standfirst is left about 280px, which is under the
   40ch measure it is set to and turns a paragraph into a gutter. So the article
   splits into two columns at 1100px and the head splits at 1240px, which is the
   width the research put the desktop grid at anyway.

   Only where a photograph exists: with none, the head keeps its medallion and
   the standfirst runs the full width rather than sitting beside a hole. */
@media (min-width: 1240px) {
  .recipe__masthead--photo {
    display: grid;
    grid-template-columns: minmax(0, 710px) minmax(0, 1fr);
    grid-template-areas:
      "crumbs crumbs"
      "head   head"
      "media  stand";
    column-gap: 56px;
    align-items: end;
  }
  .recipe__masthead--photo .breadcrumbs { grid-area: crumbs; }
  .recipe__masthead--photo .recipe__head { grid-area: head; }
  .recipe__masthead--photo .recipe__hero { grid-area: media; margin-bottom: 0; }
  .recipe__masthead--photo .recipe__standfirst { grid-area: stand; }
  /* The measure the standfirst is set to, which is the reason the media column
     is the narrower of the two. */
  .recipe__masthead--photo .recipe__lede { max-width: 40ch; }
}

/* Section rhythm across the wrappers the grid introduced.

   .recipe__section:first-of-type drops the top rule on whichever section leads
   its parent, which used to be the ingredients and is now the first section in
   each wrapper. Inside .recipe__body that is whatever follows the cooking block
   (the video where there is one, otherwise the plate), and it FOLLOWS something
   rather than opening the page, so it wants its rule back. When there is no
   cooking block at all it really is first, and then it does not. */
.recipe__body > .recipe__section:first-of-type {
  padding-top: 30px;
  border-top: 1px solid var(--line);
}
.recipe__body > .recipe__section:first-child {
  padding-top: 0;
  border-top: 0;
}
.recipe__rail > .recipe__rails {
  margin-top: 40px;
}

/* ---- The cook card ------------------------------------------------------
   NO CHROME IT HAS NOT EARNED: one hairline box, two full width controls and a
   sentence. Content radius, never a pill. */
.cook-card {
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 20px;
  background: var(--paper);
}
.cook-card__button {
  width: 100%;
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid var(--ink);
  border-radius: var(--r);
  background: var(--ink);
  color: var(--cream);
  font-family: var(--body-font);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-quick) var(--e-out-cubic),
              color var(--t-quick) var(--e-out-cubic),
              border-color var(--t-quick) var(--e-out-cubic);
}
/* THE SECOND ACTION IS THE QUIET ONE. At most one filled element per viewport,
   and on this card the filled one is Save. */
.cook-card__wake {
  margin-top: 8px;
  background: transparent;
  color: var(--ink);
  border-color: var(--line);
}
.cook-card__button:hover {
  border-color: var(--paprika);
}
.cook-card__save:hover {
  background: var(--paprika-dark);
  border-color: var(--paprika-dark);
}
/* PRESSED IS THE OUTLINE STATE, on both buttons: a save that stayed filled once
   it was done would leave two filled elements on the card. */
.cook-card__save[aria-pressed="true"] {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.cook-card__save[aria-pressed="true"] .cook-card__icon path {
  fill: currentColor;
}
.cook-card__wake[aria-pressed="true"] {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
.cook-card__icon {
  width: 14px;
  height: 14px;
  flex: none;
}

/* BOTH LABELS ARE IN THE MARKUP and this is what picks between them, so no
   button's text is written by JavaScript and none of them changes width as the
   saved state arrives after paint. */
.cook-card__button .cook-card__label--on,
.cook-card__button[aria-pressed="true"] .cook-card__label--off {
  display: none;
}
.cook-card__button[aria-pressed="true"] .cook-card__label--on {
  display: inline;
}
.cook-card__wake[hidden] {
  display: none;
}

.cook-card__promise {
  margin: 14px 0 0;
  color: var(--ink-soft);
  font-size: 0.82rem;
  line-height: 1.55;
}
.cook-card__more {
  margin: 10px 0 0;
  font-size: 0.82rem;
}
/* Drawn as one quiet line and tapped at 44px, like every other small target on
   the site. */
.cook-card__more a {
  color: var(--paprika-dark);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--paprika) 40%, transparent);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}
.cook-card__more a:hover {
  border-bottom-color: var(--paprika);
}

/* ---- The ingredient list ------------------------------------------------ */
.recipe__ingredients-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.servings {
  display: flex;
  align-items: center;
  gap: 10px;
}
.servings__label {
  color: var(--ink-soft);
  font-size: 0.85rem;
}
.servings__note {
  margin-top: 6px;
}
.stepper {
  display: flex;
  align-items: center;
  gap: 4px;
}
/* 34px drawn, 44px tapped: the expander behind each button is the real target,
   so a stepper reads as small chrome and answers a thumb. */
.stepper__button {
  position: relative;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: transparent;
  color: var(--ink);
  font-family: var(--body-font);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}
.stepper__button::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap);
  height: var(--tap);
  transform: translate(-50%, -50%);
}
.stepper__button:hover {
  border-color: var(--paprika);
  color: var(--paprika-dark);
}
.stepper__value {
  min-width: 26px;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.ingredients {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
}
.ingredient + .ingredient {
  border-top: 1px solid var(--line);
}
/* The whole row is the label, so the tick target is the line rather than a 13px
   square beside it. */
.ingredient__row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: var(--tap);
  padding: 10px 0;
  cursor: pointer;
  line-height: 1.5;
}
.ingredient__check {
  flex: none;
  width: 17px;
  height: 17px;
  margin-top: 3px;
  accent-color: var(--paprika);
  cursor: pointer;
}
/* A TICKED LINE IS DIMMED, NEVER STRUCK THROUGH: somebody standing at a counter
   still has to read what it says. */
.ingredient__check:checked ~ .ingredient__text {
  color: var(--ink-soft);
}
.ingredient__text {
  min-width: 0;
}
.ingredient__quantity {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- The steps ----------------------------------------------------------
   The numeral is the display serif at its 200 weight, which is the one place
   that cut appears at size on this page. Huge, light, and quiet enough that the
   sentence beside it is what gets read. */
.steps {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  counter-reset: none;
}
.step {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 16px;
  padding: 18px 0;
  border-top: 1px solid var(--line);
}
.step:first-child {
  border-top: 0;
}
.step__number {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: 2.5rem;
  line-height: 0.9;
  letter-spacing: 0.005em;
  color: var(--ink-soft);
  font-variant-numeric: lining-nums;
}
.step__text {
  margin: 0;
  max-width: 62ch;
  line-height: 1.65;
  color: var(--ink);
  text-wrap: pretty;
}
/* A step's own photograph, on the community recipes that carry one. It takes the
   SECOND column rather than the full row, so it lines up under the sentence it
   illustrates and the numeral column stays a numeral column. No catalog step has
   one, so this rule paints nothing on 843 of the pages that load it. */
.step__photo {
  grid-column: 2;
  display: block;
  width: 100%;
  height: auto;
  margin-top: 12px;
  border-radius: var(--radius);
}

/* ---- The cook's voice ---------------------------------------------------
   One recording per community recipe. The player is the browser's own, because
   a custom transport is a keyboard and screen reader surface to get wrong for
   no gain, and it is capped at the text measure so it does not stretch to the
   full column width on a desktop. */
.recipe__player {
  display: block;
  width: 100%;
  max-width: 42rem;
  margin-top: 12px;
}

/* ---- My kitchen ---------------------------------------------------------
   The dishes one person kept, rendered by the server. It used to be a shell the
   browser filled from localStorage; the kitchen belongs to the account now, so
   the page is somebody's and is never cached. The card is the /saved card it
   always was, renamed with the page. */
.kitchen {
  padding-top: 48px;
  padding-bottom: 96px;
  max-width: 900px;
}
.kitchen__title {
  font-family: var(--display-font);
  font-weight: 200;
  font-size: clamp(2rem, 5vw, 3rem);
  line-height: 1.05;
  color: var(--ink);
  margin: 0;
}
.kitchen__promise {
  margin: 14px 0 0;
  max-width: 46ch;
  color: var(--ink-soft);
  line-height: 1.6;
}
.kitchen__count {
  margin: 28px 0 0;
  color: var(--ink-3);
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* THE GRID IS THE CHEF PAGE'S (.plate-grid) AND THE CARD IS THE SHELF'S. This
   list used to draw its own tile down to the crop, the display cut and the
   meta line, which was the plate card wearing a second set of class names.
   Only the gap above it is this page's own. */
.kitchen__grid {
  margin-top: 16px;
}
.kitchen__empty {
  margin: 40px 0 0;
  max-width: 46ch;
  color: var(--ink-soft);
  line-height: 1.6;
}
.kitchen__browse {
  color: var(--paprika-dark);
}

/* Signed out: an invitation, in the one card on the page. */
.kitchen__signin {
  margin-top: 40px;
  max-width: 46ch;
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 28px;
  background: var(--paper);
}
.kitchen__signin-title {
  margin: 0;
  font-family: var(--display-font);
  font-weight: 500;
  font-size: 1.25rem;
  line-height: 1.2;
  color: var(--ink);
}
.kitchen__signin-lede {
  margin: 12px 0 0;
  color: var(--ink-soft);
  line-height: 1.6;
}
.kitchen__signin-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 20px;
  min-height: var(--tap);
  padding: 0 22px;
  border-radius: var(--r);
  background: var(--ink);
  color: var(--cream);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  transition: background var(--t-quick) var(--e-out-cubic);
}
.kitchen__signin-button:hover {
  background: var(--paprika-dark);
}

/* The photograph's hover moved with the tile: .plate-card declares it once, in
   the shelf section, and every grid that renders one inherits it. */

/* ---- The second save, at the head of the ingredients --------------------
   The rail is written after the recipe, so on a phone the cook card is at the
   FOOT of the page and the save is the moment this site asks for an account.
   An ask nobody scrolls to is an ask nobody answers, so a second one sits at
   the top of the list somebody has just decided to cook from.

   NOT PINNED, AND THAT IS THE RULING. The research's answer to this is a fixed
   bottom bar; the stickiness budget for this site is three and all three are
   spent, so this one scrolls with the list it belongs to.

   FULL WIDTH OF THE HEAD, on its own row under the heading and the stepper,
   which is what the flex-basis buys. Hidden from 1100px up, where the cook card
   is already beside the recipe and a second button would be the same offer
   twice on one screen. */
.ingredients-save {
  flex-basis: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap);
  margin-top: 4px;
  border: 1px solid var(--ink);
  border-radius: var(--r);
  background: var(--ink);
  color: var(--cream);
  font-family: var(--body-font);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-quick) var(--e-out-cubic),
              color var(--t-quick) var(--e-out-cubic),
              border-color var(--t-quick) var(--e-out-cubic);
}
.ingredients-save:hover {
  background: var(--paprika-dark);
  border-color: var(--paprika-dark);
}
/* PRESSED IS THE OUTLINE STATE, exactly as on the card: at most one filled
   element per viewport, and once the dish is kept the filled one has done its
   job. */
.ingredients-save[aria-pressed="true"] {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.ingredients-save[aria-pressed="true"] .cook-card__icon path {
  fill: currentColor;
}
.ingredients-save .cook-card__label--on,
.ingredients-save[aria-pressed="true"] .cook-card__label--off {
  display: none;
}
.ingredients-save[aria-pressed="true"] .cook-card__label--on {
  display: inline;
}
@media (min-width: 1100px) {
  .ingredients-save {
    display: none;
  }
}

/* ---- The toast strip ----------------------------------------------------
   The site's one notice voice. Fixed to the bottom of the viewport on a phone
   and to the bottom right on a desktop, above everything except a modal
   dialog, and EMPTY in the markup: it ships on every page, including the ones a
   CDN holds byte identical for every visitor, because there is nothing in it
   about anybody until the browser puts something there.

   pointer-events on the container is none and on a toast is auto, so an empty
   strip is not an invisible pane over the foot of the page. */
.toasts {
  position: fixed;
  z-index: 60;
  left: 16px;
  right: 16px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  pointer-events: none;
}
@media (min-width: 700px) {
  .toasts {
    left: auto;
    right: 24px;
    bottom: 24px;
    width: min(380px, calc(100vw - 48px));
  }
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 14px 14px 16px;
  border-radius: var(--r);
  border: 1px solid var(--night-rule);
  background: var(--night);
  color: var(--lit);
  box-shadow: 0 12px 32px rgba(34, 31, 26, 0.28);
}
/* THE SEVERITY IS NEVER THE COLOUR ALONE (the words carry it, and the strip is
   a live region), but an error still reads as one at a glance. */
.toast--alert {
  border-color: var(--paprika);
}
.toast__text {
  margin: 0;
  flex: 1;
  font-size: 0.9rem;
  line-height: 1.5;
}
/* THE WAY BACK, when a notice is about something that can be undone. It is a
   real form rather than a link, because no page in this layout carries
   csrf_meta_tags (most of them are held by a CDN) and Turbo would have no token
   to find: the server that asked for the notice sends one with it and
   toast_controller.js builds the form around it. See Web::DishExclusionsController.

   UNDERLINED, because it is the one thing in a toast that can be pressed and
   colour alone would not say so on a dark ground. */
.toast__action {
  display: block;
  margin-top: 6px;
}
.toast__action-button {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--lit);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.5;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.toast__action-button:hover {
  color: var(--butter);
}
.toast__close {
  flex: none;
  width: 28px;
  height: 28px;
  margin: -4px -4px -4px 0;
  border: 0;
  border-radius: var(--r);
  background: transparent;
  color: var(--lit-2);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.toast__close::before {
  content: "\00d7";
}
.toast__close:hover {
  color: var(--lit);
}

/* The slide, and the whole of it is here rather than in JavaScript: the
   controller only waits for it, and asks matchMedia whether it is happening
   before it does. */
@media (prefers-reduced-motion: no-preference) {
  .toast {
    animation: toast-in 220ms var(--e-out-cubic) both;
    transition: opacity 220ms var(--e-out-cubic), transform 220ms var(--e-out-cubic);
  }
  .toast--leaving {
    opacity: 0;
    transform: translateY(8px);
  }
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---- The sign-in dialog -------------------------------------------------
   A native <dialog>, opened with showModal(), which is what buys the focus
   trap, the Escape key, the inert background and the top layer from the browser
   rather than from a controller that has to reimplement all four. */
.auth-dialog {
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  width: min(420px, calc(100vw - 32px));
}
.auth-dialog::backdrop {
  background: rgba(34, 31, 26, 0.5);
}
.auth-dialog__panel {
  position: relative;
  padding: 32px 28px 24px;
}
.auth-dialog__title {
  margin: 0;
  font-family: var(--display-font);
  font-weight: 500;
  font-size: 1.35rem;
  line-height: 1.2;
  max-width: 22ch;
}
.auth-dialog__lede {
  margin: 12px 0 0;
  color: var(--ink-soft);
  line-height: 1.6;
  font-size: 0.95rem;
}
.auth-dialog__google {
  width: 100%;
  min-height: var(--tap);
  margin-top: 24px;
  border: 1px solid var(--ink);
  border-radius: var(--r);
  background: var(--ink);
  color: var(--cream);
  font-family: var(--body-font);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-quick) var(--e-out-cubic);
}
.auth-dialog__google:hover {
  background: var(--paprika-dark);
  border-color: var(--paprika-dark);
}
/* Empty until there is something to say, so it takes no room until it does. */
.auth-dialog__status {
  margin: 12px 0 0;
  min-height: 0;
  color: var(--ink-soft);
  font-size: 0.85rem;
  line-height: 1.5;
}
.auth-dialog__status:empty {
  margin: 0;
}
.auth-dialog__fineprint {
  margin: 16px 0 0;
  color: var(--ink-3);
  font-size: 0.78rem;
  line-height: 1.5;
}
.auth-dialog__fineprint a {
  color: var(--paprika-dark);
}
.auth-dialog__close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: var(--tap);
  height: var(--tap);
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.auth-dialog__close:hover {
  color: var(--ink);
}

/* ---- The planner's swap dialog ------------------------------------------
   The sign-in dialog's vocabulary, on the one other surface that asks a
   question over a page: a native <dialog> opened with showModal(), one
   hairline, paper rather than the page's cream so it lifts off it, and the
   same cross in the same corner.

   IT IS A BOTTOM SHEET UNTIL 700px AND A CARD AFTER IT. This is a picker with
   a search field and a scrolling list, so on a phone it wants the full width
   and the bottom of the screen, where a thumb is; on a laptop a full-width
   sheet would be a list of dish names four hundred pixels away from the card
   they are replacing. 700px is the breakpoint the toast strip already changes
   shape on, so the two overlays agree about what a narrow screen is.

   display IS ON [open] AND NOWHERE ELSE. The browser's own stylesheet hides a
   closed dialog with `display: none`, and an author `display: flex` on the
   element would beat it: the panel would sit on the page, unopened, over the
   week. */
.swap-dialog {
  padding: 0;
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  width: 100%;
  max-width: 100%;
  max-height: 86vh;
  max-height: 86svh;
  margin: auto auto 0;
  border-radius: 16px 16px 0 0;
  overflow: hidden;
}
.swap-dialog[open] {
  display: flex;
  flex-direction: column;
}
.swap-dialog::backdrop {
  background: rgba(34, 31, 26, 0.5);
}
@media (min-width: 700px) {
  .swap-dialog {
    margin: auto;
    width: min(460px, calc(100vw - 32px));
    max-height: min(600px, calc(100vh - 64px));
    border-radius: var(--r);
  }
}
.swap-dialog__close {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  width: var(--tap);
  height: var(--tap);
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.swap-dialog__close:hover {
  color: var(--ink);
}
.swap-dialog__head {
  position: relative;
  flex: none;
  padding: 16px 16px 12px;
  border-bottom: 1px solid var(--rule-soft);
}
.swap-dialog__title {
  margin: 0 var(--tap) 0 0;
  font-family: var(--display-font);
  font-weight: 500;
  font-size: 1.0625rem;
  line-height: 1.25;
}
/* The field the app's sheet has, in this site's own chrome: one hairline, the
   page's cream inside it, and the 44px every target on the planner is. */
.swap-dialog__search {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  margin-top: 12px;
  padding: 0 12px;
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--cream);
}
.swap-dialog__search:focus-within {
  border-color: var(--ink);
}
.swap-dialog__search-mark {
  flex: none;
  display: flex;
  color: var(--ink-4);
}
.swap-dialog__field {
  flex: 1;
  min-width: 0;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--body-font);
  /* 16px exactly, because anything under it makes iOS Safari zoom the page the
     moment the caret lands, and a zoomed bottom sheet cannot be scrolled back. */
  font-size: 16px;
  line-height: 1.4;
}
.swap-dialog__field:focus {
  outline: none;
}
.swap-dialog__field::placeholder {
  color: var(--ink-4);
}
.swap-dialog__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px;
}
/* WHAT IS SHOWING WHILE A REQUEST IS IN FLIGHT, and it is CSS rather than
   JavaScript because Turbo already marks a navigating frame `busy`. On a search
   the results underneath stay where they are and step back, which is what the
   app's sheet does instead of flashing the list away to skeletons. */
.swap-dialog__pending {
  display: none;
  margin: 0;
  padding: 12px 8px;
  color: var(--ink-4);
  font-size: 0.8125rem;
  line-height: 1.5;
}
.swap-dialog__body:has(turbo-frame[busy]) .swap-dialog__pending {
  display: block;
}
.swap-dialog__body:has(turbo-frame[busy]) turbo-frame {
  opacity: 0.45;
}

/* ---- Printing a recipe ---------------------------------------------------
   WHAT A PRINTOUT IS FOR. Somebody is taking this to a kitchen counter, or
   saving a PDF to cook from later. The useful page is the photo, what the dish
   is, what goes in it and what to do. Everything else on the screen page exists
   to navigate a website, reassure a reader, or sell the app, and on paper it is
   ink spent on furniture.

   AN ALLOW LIST, NOT A DENY LIST, and that is the load bearing choice here. The
   first version hid the sections it knew about by name, which prints correctly
   exactly until somebody adds a section: the new one is not in the list, so it
   joins every printout silently and nobody finds out until a user complains
   about their PDF. So everything inside .recipe is hidden and the handful of
   things worth paper are restored by name. A section added tomorrow is left OFF
   a printout until it opts in with .recipe__section--print.

   Backgrounds go too: a printer renders the warm cream ground as a grey wash
   over every line of text, and charges the ink to do it. */
@page {
  margin: 14mm 14mm 16mm;
}

@media print {
  /* Everything that is not the recipe.

     THE FOUR OVERLAYS ARE NAMED RATHER THAN LEFT TO LUCK. The toast strip is
     `position: fixed`, which a printer resolves onto the first sheet, and a
     <dialog> is only display:none while it is CLOSED: somebody who prints with
     the sign-in dialog open would otherwise get it across the middle of their
     recipe. Neither is furniture worth a page. */
  .site-header,
  .site-footer,
  .skip-link,
  .breadcrumbs,
  .masquerade-banner,
  .flash,
  .toasts,
  .auth-dialog,
  .swap-dialog,
  .nav-drawer {
    display: none !important;
  }

  html,
  body {
    background: #fff !important;
    color: #000 !important;
    font-size: 10.5pt;
    line-height: 1.45;
  }

  .wrap,
  .recipe,
  main {
    max-width: none !important;
    margin: 0 !important;
    padding: 0 !important;
  }

  /* The allow list. Hide the article's children, then restore the few things a
     cook actually needs, each with the display its screen rule uses so nothing
     collapses.

     IT REACHES ONE LEVEL DEEPER THAN IT USED TO, because the desktop grid put
     four wrappers between .recipe and its sections. Hiding only .recipe > *
     after that change would have hidden the whole recipe and printed a blank
     page; restoring only the wrappers would have printed everything inside them.
     So each wrapper hides its own children and is itself restored, and the list
     of what comes back is still a list of names.

     WHAT IS DELIBERATELY NOT IN IT: .recipe__rail. The cook card, the screen
     wake toggle and the related rails all live in there, and a printed recipe
     carrying a Save button is the tell of a print stylesheet nobody tried.
     .recipe__tail is out for the same reason: hub links and an app CTA are
     furniture on paper. Both are hidden by the first rule and never named
     again. */
  .recipe > *,
  .recipe__masthead > *,
  .recipe__standfirst > *,
  .recipe__body > *,
  .recipe__cooking > * {
    display: none !important;
  }

  .recipe__masthead,
  .recipe__standfirst,
  .recipe__body,
  .recipe__cooking {
    display: block !important;
  }
  .recipe__hero,
  .recipe__lede,
  .recipe__section--print {
    display: block !important;
  }
  .recipe__head {
    display: block !important;
  }
  .recipe__head--placeholder {
    display: flex !important;
  }
  .recipe__chips {
    display: flex !important;
  }

  /* The stepper is a screen control, and so is every Save button.

     THE SECOND SAVE HAS TO BE NAMED HERE and the first does not, which is the
     whole reason this rule grew. The cook card's Save is inside .recipe__rail,
     which the allow list never restores; the one at the head of the ingredient
     list is inside a section that DOES print, so nothing above hides it. A
     printed recipe carrying a Save button is the tell of a print stylesheet
     nobody tried, and spec/requests/recipe_cook_card_spec.rb now walks the
     printed region looking for exactly that.

     The check boxes are NOT screen controls: a printed list with a box beside
     each line is the thing somebody carried the paper to the kitchen for. */
  .servings,
  .servings__note,
  .ingredients-save {
    display: none !important;
  }

  /* The photo earns its place, at a size that leaves the method on the same
     sheet rather than pushing it to a second one. */
  .recipe__hero {
    aspect-ratio: auto;
    border-radius: 6px;
    max-height: 62mm;
    margin: 0 0 8mm;
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .recipe__photo {
    max-height: 62mm;
    width: auto;
    max-width: 100%;
    object-fit: contain;
  }

  .recipe__eyebrow {
    margin: 0 0 2mm;
    font-size: 9pt;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #555;
  }
  .recipe__title {
    margin: 0 0 3mm;
    font-size: 22pt;
    line-height: 1.15;
  }
  .recipe__lede {
    margin: 0 0 4mm;
    font-size: 10.5pt;
    color: #222;
  }

  /* Chips stay, because minutes and effort are the two facts you want before you
     start cooking. Flattened to plain text so they cost no ink as shapes. */
  .recipe__chips {
    gap: 0 4mm;
    margin: 0 0 6mm;
  }
  .recipe__chip,
  .recipe__chip--good {
    background: none !important;
    border: 0 !important;
    padding: 0 !important;
    font-size: 9.5pt;
    color: #333 !important;
  }
  .recipe__chip + .recipe__chip::before {
    content: "\00b7";
    margin-right: 4mm;
    color: #999;
  }

  /* The two sections that print. The hairline rule the screen uses to separate
     rooms is noise on paper, where whitespace already does it. */
  .recipe__section--print {
    border: 0 !important;
    background: none !important;
    margin: 0 0 6mm !important;
    padding: 0 !important;
    break-inside: auto;
  }
  .recipe__h2 {
    font-size: 12pt;
    margin: 0 0 2.5mm;
    break-after: avoid;
    page-break-after: avoid;
  }
  /* Ingredients and steps stack on paper rather than running as two columns:
     a printed two column layout that breaks across a page boundary puts the
     back half of the ingredients on sheet two, beside nothing. */
  .recipe__cooking {
    display: block !important;
  }
  .recipe__ingredients {
    position: static !important;
    max-height: none !important;
    overflow: visible !important;
  }
  .ingredients,
  .steps {
    margin: 0;
    padding-left: 0;
  }
  .ingredient__row {
    min-height: 0;
    padding: 0.6mm 0;
    gap: 2.5mm;
  }
  .ingredient,
  .step {
    margin-bottom: 1.5mm;
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .ingredient + .ingredient,
  .step {
    border: 0 !important;
  }
  .step {
    grid-template-columns: 8mm minmax(0, 1fr);
    gap: 3mm;
    padding: 0 0 1.5mm;
  }
  .step__number {
    font-size: 13pt;
    color: #666;
  }
  .step__text {
    font-size: 10.5pt;
    line-height: 1.4;
  }
  /* Not on paper. The steps section is in the print allow list because the words
     are what somebody cooks from; a page of full width photographs behind them
     is ink nobody asked for. */
  .step__photo {
    display: none !important;
  }
}

/* --- The claim banner ------------------------------------------------------
   The signed-out invitation above the header: one quiet cream-2 band, one
   sentence, one paprika pill. It renders inside the cached anonymous page
   (anonymous IS signed out), so it costs the cache nothing. The short copy
   replaces the long below 700px rather than wrapping it. */
.claim-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 9px var(--gutter);
  background: var(--cream-2);
  border-bottom: 1px solid var(--rule-soft);
}
.claim-banner__text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-2);
}
.claim-banner__short { display: none; }
.claim-banner__cta {
  flex: none;
  padding: 5px 14px;
  border-radius: 999px;
  background: var(--paprika);
  color: var(--cream);
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.claim-banner__cta:hover { background: var(--paprika-dark); }
@media (max-width: 700px) {
  .claim-banner__long { display: none; }
  .claim-banner__short { display: inline; }
}

/* The hero's quiet pair of store badges, and the compact cut they wear there:
   the standard badge is a landing-section object; under the chip rail it
   drops its minimum width and a size, so the question stays the one big
   thing on the first screen. */
.hero__stores {
  margin-top: 16px;
}
.store-badges--compact .store-badge {
  min-width: 0;
  padding: 6px 13px;
  border-radius: 10px;
}
.store-badges--compact .store-badge__kicker {
  font-size: 9px;
}
.store-badges--compact .store-badge__name {
  font-size: 0.85rem;
}

/* --- The plan band ---------------------------------------------------------
   The signed-in row above the hero: one line about tonight, one link, and no
   more height than that, because a signed-in visitor who only came to read
   recipes should lose almost nothing to it. */
.plan-band {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 10px var(--gutter);
  background: var(--cream-2);
  border-bottom: 1px solid var(--rule-soft);
}
.plan-band__text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-2);
}
.plan-band__kicker {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-right: 6px;
}
.plan-band__sides {
  color: var(--ink-3);
}
.plan-band__link {
  flex: none;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--paprika);
  text-decoration: none;
}
.plan-band__link:hover { text-decoration: underline; text-underline-offset: 4px; }

/* The band's day: each meal on its own left-aligned line (owner, 2026-08-12),
   the slot's name in small caps beside its plate, TODAY and the week link
   sharing the top line. Three quiet rows, no boxes. */
.plan-band {
  justify-content: flex-start;
}
.plan-band__day {
  width: min(100%, var(--cw));
  margin-inline: auto;
}
.plan-band__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 0 0 2px;
}
.plan-band__meal {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-2);
  padding: 1px 0;
}
.plan-band__slot {
  display: inline-block;
  min-width: 74px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-right: 6px;
}

/* ON A PHONE THE BAND IS ONE LINE: the meal the hour means (the household's
   clock, nudged to the first meal with a plate), because three stacked meals
   above the hero read as clutter at 390px. The full day stays on wider
   screens, and /plan is one press away either way. */
@media (max-width: 700px) {
  .plan-band__meal { display: none; }
  .plan-band__meal--now { display: block; }
}

/* The planner's app nudge: one small row under the week, hairline above,
   nothing that competes with the plan it sits beneath. */
.plan-nudge {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 18px;
  margin-top: 28px;
  padding-top: 16px;
  border-top: 1px solid var(--rule-soft);
}
.plan-nudge__text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-3);
  max-width: 52ch;
}
.plan-nudge__kicker {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 3px;
}
