/* ==========================================================================
   app.css — layout + every component, built ONLY from tokens.css.
   Load order: tokens.css, then this file.
   ========================================================================== */

/* ---------------------------------------------------------------- reset -- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  height: 100%;
}

body {
  margin: 0;
  min-height: 100%;
  background: var(--bg-grouped);
  color: var(--label);
  font-family: var(--font-sans);
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
  letter-spacing: var(--text-body-track);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overscroll-behavior-y: none;
}

img,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}

button,
input,
select,
textarea {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

a {
  color: var(--accent-text);
  text-decoration: none;
}

hr {
  border: 0;
  border-top: 1px solid var(--separator);
  margin: var(--space-5) 0;
}

/* Focus: keyboard only, always visible, never removed. */
:focus {
  outline: none;
}

:focus-visible {
  outline: var(--focus-ring-width) solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

::selection {
  background: var(--accent-soft);
}

/* --------------------------------------------------------------- type ---- */
.t-largeTitle,
.t-title1,
.t-title2,
.t-title3,
.t-headline,
.t-body,
.t-callout,
.t-subhead,
.t-footnote,
.t-caption1,
.t-caption2 {
  margin: 0;
}

.t-largeTitle {
  font-size: var(--text-largeTitle-size);
  line-height: var(--text-largeTitle-line);
  letter-spacing: var(--text-largeTitle-track);
  font-weight: var(--text-largeTitle-weight);
}
.t-title1 {
  font-size: var(--text-title1-size);
  line-height: var(--text-title1-line);
  letter-spacing: var(--text-title1-track);
  font-weight: var(--text-title1-weight);
}
.t-title2 {
  font-size: var(--text-title2-size);
  line-height: var(--text-title2-line);
  letter-spacing: var(--text-title2-track);
  font-weight: var(--text-title2-weight);
}
.t-title3 {
  font-size: var(--text-title3-size);
  line-height: var(--text-title3-line);
  letter-spacing: var(--text-title3-track);
  font-weight: var(--text-title3-weight);
}
.t-headline {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
}
.t-body {
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
  letter-spacing: var(--text-body-track);
  font-weight: var(--text-body-weight);
}
.t-callout {
  font-size: var(--text-callout-size);
  line-height: var(--text-callout-line);
  letter-spacing: var(--text-callout-track);
}
.t-subhead {
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
  letter-spacing: var(--text-subhead-track);
}
.t-footnote {
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  letter-spacing: var(--text-footnote-track);
}
.t-caption1 {
  font-size: var(--text-caption1-size);
  line-height: var(--text-caption1-line);
  letter-spacing: var(--text-caption1-track);
}
.t-caption2 {
  font-size: var(--text-caption2-size);
  line-height: var(--text-caption2-line);
  letter-spacing: var(--text-caption2-track);
}

.t-secondary {
  color: var(--label-secondary);
}
.t-tertiary {
  color: var(--label-tertiary);
}
.t-accent {
  color: var(--accent-text);
}
.t-destructive {
  color: var(--destructive);
}
.t-center {
  text-align: center;
}
/* Any number a human reads as a quantity gets this. */
.t-num {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: var(--numeric-tabular);
}

.u-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.u-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.u-hidden {
  display: none !important;
}
.u-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.u-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.u-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.u-spacer {
  flex: 1 1 auto;
}

/* ================================================================ SHELL === */
.app {
  --sidebar-w: 260px;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 1fr;
  background: var(--bg-grouped);
}

/* ---- Scroll container: the content area scrolls, not the body. -------- */
.app-main {
  position: relative;
  min-width: 0;
  height: 100dvh;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}

/* ---- No platform scrollbars ------------------------------------------
   Chrome on Windows paints a 15px opaque gutter down the right of every
   scroller, which is a desktop-browser artifact sitting inside an app that
   is otherwise phone-shaped: it cuts the header material, offsets the card
   grid from centre, and appears and disappears as content changes length.
   Every scroller here is one the user is already inside — nothing scrolls
   that is not the thing they are reading — so the bar is removed rather
   than styled.

   HONEST COST: on a desktop mouse this removes the only visual cue that a
   region scrolls, and the drag-the-thumb gesture with it. Wheel, trackpad,
   touch, keyboard (Page/Home/End/arrows) and programmatic scrolling are all
   unaffected. */
* {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* old Edge */
}
*::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* ---- Header: large title that collapses to a compact inline title ----- */
.app-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: transparent;
  transition: background var(--duration-base) var(--ease-standard);
}

/* The collapse trigger — see shell.js. Zero height, no paint, no layout
   effect; an IntersectionObserver watches it cross under the header. */
.app-header__sentinel {
  height: 0;
  pointer-events: none;
}

.app-header__bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--hit);
  padding: var(--space-2) var(--gutter);
}

.app-header__compact {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 56px;
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-standard),
    transform var(--duration-base) var(--ease-standard);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.app-header__large {
  padding: var(--space-1) var(--gutter) var(--space-4);
  transform-origin: left bottom;
  /* max-height (not height:auto) so the collapse can animate; 240px is far
     above any realistic title, so the expanded state never clips. */
  max-height: 240px;
  overflow: hidden;
  transition: opacity var(--duration-base) var(--ease-standard),
    transform var(--duration-base) var(--ease-standard),
    max-height var(--duration-base) var(--ease-out),
    padding var(--duration-base) var(--ease-out);
}

.app-header__leading,
.app-header__trailing {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  z-index: 1;
}
.app-header__trailing {
  margin-left: auto;
}

.app-header.is-collapsed {
  background: var(--material-thick);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: 0 0.5px 0 var(--separator);
}
.app-header.is-collapsed .app-header__compact {
  opacity: 1;
  transform: none;
}
.app-header.is-collapsed .app-header__large {
  opacity: 0;
  transform: translateY(-8px) scale(0.94);
  max-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  pointer-events: none;
}

/* Header icon button (back chevron, edit, …) */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--hit);
  min-height: var(--hit);
  /* Optical alignment: a 44px hit area around a 22px glyph leaves 11px of air,
     so the icon only lines up with the content edge if the button is pulled
     outward. THE PULL IS EDGE-ONLY. Applied to both sides of every button it
     also pulled ADJACENT buttons into each other: two 44px header buttons with
     a 4px gap and -8px on each facing side overlapped by 12px — 27% of the
     control — so the favourite and edit buttons on a recipe shared a strip of
     tappable area and the wrong one fired. Measured by core/dom.js's collision
     probe, 2026-08-01, and invisible in a screenshot because the icons
     themselves never touch. A lone button is :first-child AND :last-child and
     still gets both pulls, which is the case this rule was written for. */
  margin: 0;
  border-radius: var(--radius-full);
  color: var(--accent);
  transition: background var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}
.icon-button:first-child {
  margin-left: calc(var(--space-4) * -1 + var(--space-2));
}
.icon-button:last-child {
  margin-right: calc(var(--space-4) * -1 + var(--space-2));
}
.icon-button:hover {
  background: var(--fill-4);
}
.icon-button:active {
  background: var(--fill-3);
  transform: scale(0.94);
}
.icon-button[disabled] {
  color: var(--label-tertiary);
  pointer-events: none;
}

/* ---- Content ---------------------------------------------------------- */
.app-content {
  padding: 0 0 calc(var(--space-11) + env(safe-area-inset-bottom, 0px));
  max-width: 720px;
  margin: 0 auto;
}
.app-content--wide {
  max-width: 1100px;
}

/* ---- Tab bar (narrow) ------------------------------------------------- */
.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-tabbar);
  display: flex;
  justify-content: space-around;
  align-items: stretch;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--material-thick);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: 0 -0.5px 0 var(--separator);
}

.tabbar__item {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 50px;
  padding: var(--space-3) var(--space-2) var(--space-2);
  color: var(--label-secondary);
  font-size: var(--text-caption2-size);
  line-height: var(--text-caption2-line);
  letter-spacing: var(--text-caption2-track);
  font-weight: 500;
  transition: color var(--duration-fast) var(--ease-standard);
}
.tabbar__item .icon {
  width: 26px;
  height: 26px;
}
.tabbar__item:active {
  opacity: 0.6;
}
.tabbar__item.is-active {
  color: var(--accent-text);
}

/* ---- Sidebar (wide) --------------------------------------------------- */
.sidebar {
  display: none;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-6) var(--space-4);
  height: 100dvh;
  position: sticky;
  top: 0;
  background: var(--bg-system-secondary);
  box-shadow: inset -0.5px 0 0 var(--separator);
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3) var(--space-6);
  font-size: var(--text-title3-size);
  line-height: var(--text-title3-line);
  letter-spacing: var(--text-title3-track);
  font-weight: 700;
}
.sidebar__brand .icon {
  width: 24px;
  height: 24px;
  color: var(--accent);
}

.sidebar__item {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--hit);
  padding: 0 var(--space-4);
  border-radius: var(--radius-md);
  color: var(--label);
  font-size: var(--text-callout-size);
  letter-spacing: var(--text-callout-track);
  font-weight: 500;
  text-align: left;
  transition: background var(--duration-fast) var(--ease-standard);
}
.sidebar__item .icon {
  width: 22px;
  height: 22px;
  color: var(--label-secondary);
  flex: none;
}
.sidebar__item:hover {
  background: var(--fill-4);
}
.sidebar__item.is-active {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.sidebar__item.is-active .icon {
  color: var(--accent);
}

@media (min-width: 900px) {
  .app {
    grid-template-columns: var(--sidebar-w) 1fr;
  }
  .sidebar {
    display: flex;
  }
  .tabbar {
    display: none;
  }
  .app-content {
    padding-bottom: var(--space-10);
  }
  :root {
    --gutter: 24px;
  }
}

/* ============================================================== LISTS ==== */
/* Grouped inset list. Optional header/footer, hairline separators that
   inset past the leading icon — set --sep-inset on the row.               */
.list-group {
  margin: var(--space-6) 0;
}

.list-header {
  padding: 0 calc(var(--gutter) + var(--space-4)) var(--space-2);
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
}

.list-footer {
  padding: var(--space-2) calc(var(--gutter) + var(--space-4)) 0;
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
}

.list {
  list-style: none;
  margin: 0 var(--gutter);
  padding: 0;
  background: var(--bg-grouped-secondary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}

.list-row {
  --sep-inset: var(--space-5);
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--hit);
  padding: var(--space-3) var(--space-5);
  text-align: left;
  color: inherit;
  background: transparent;
  transition: background var(--duration-instant) linear;
}
.list-row.has-icon {
  --sep-inset: calc(var(--space-5) + 29px + var(--space-4));
}

/* The hairline. Inset from the left, flush right. Last row has none. */
.list-row:not(:last-child)::after {
  content: "";
  position: absolute;
  left: var(--sep-inset);
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--separator);
  transform: scaleY(0.5);
  transform-origin: bottom;
  pointer-events: none;
}

button.list-row,
a.list-row,
.list-row[role="button"] {
  cursor: pointer;
}
button.list-row:hover,
a.list-row:hover,
.list-row[role="button"]:hover {
  background: var(--fill-4);
}
button.list-row:active,
a.list-row:active,
.list-row[role="button"]:active,
.list-row.is-pressed {
  background: var(--fill-3);
}
.list-row[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}
.list-row:focus-visible {
  outline-offset: -2px;
}

.list-row__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 29px;
  height: 29px;
  border-radius: var(--radius-xs);
  background: var(--accent);
  color: var(--accent-on);
}
.list-row__icon .icon {
  width: 18px;
  height: 18px;
}
/* Tinted icon tile variants */
.list-row__icon.tone-neutral {
  background: var(--fill-1);
  color: var(--label-secondary);
}
.list-row__icon.tone-success {
  background: var(--success);
  color: var(--success-on);
}
.list-row__icon.tone-warning {
  background: var(--warning);
  color: var(--warning-on);
}
.list-row__icon.tone-destructive {
  background: var(--destructive);
  color: var(--destructive-on);
}

.list-row__text {
  min-width: 0;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--space-1) 0;
}
.list-row__title {
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
  letter-spacing: var(--text-body-track);
}
.list-row__subtitle {
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  letter-spacing: var(--text-footnote-track);
}
.list-row__value {
  flex: none;
  color: var(--label-secondary);
  font-size: var(--text-body-size);
  letter-spacing: var(--text-body-track);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: var(--numeric-tabular);
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.list-row__chevron {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--label-tertiary);
}
.list-row--destructive .list-row__title {
  color: var(--destructive);
}
/* A row whose whole content is a control (switch, stepper, field). */
.list-row__control {
  flex: none;
  display: flex;
  align-items: center;
}
/* Row that is a single centred action, e.g. "Delete Recipe". */
.list-row--action {
  justify-content: center;
  color: var(--accent-text);
  font-weight: 400;
}
.list-row--action.is-destructive {
  color: var(--destructive);
}

/* Plain (non-inset) list variant for full-bleed contexts. */
.list--plain {
  margin: 0;
  border-radius: 0;
  box-shadow: none;
}

/* ============================================================== CARDS ==== */
.card {
  background: var(--bg-grouped-secondary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}
.card--pad {
  padding: var(--space-5);
}

/* Recipe tile: thumbnail + text. Horizontal on narrow, grid-friendly. */
.recipe-card {
  display: flex;
  flex-direction: column;
  width: 100%;
  text-align: left;
  color: inherit;
  background: var(--bg-grouped-secondary);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition: transform var(--duration-fast) var(--ease-standard),
    box-shadow var(--duration-fast) var(--ease-standard);
}
button.recipe-card,
a.recipe-card {
  cursor: pointer;
}
button.recipe-card:hover,
a.recipe-card:hover {
  box-shadow: var(--shadow-2);
}
button.recipe-card:active,
a.recipe-card:active {
  transform: scale(0.985);
}

.recipe-card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--fill-4);
  overflow: hidden;
}
.recipe-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* ---- Generated cover for a recipe with no photo -------------------------
   Most recipes are typed or scanned and will never have an image, so the
   blank state has to be a designed one. The title is the artwork; the tint
   comes from the recipe's category (four hue families) with a hash-picked
   variation inside it, so neighbours differ but the grid stays organised.
   components.js#recipeCover only writes data-cat / data-tone / data-fig /
   data-len — every colour below is derived here, and works in both schemes. */

/* The media box is the query container, not the cover itself — an element
   can never answer a @container query about its own size. Scoped with :has()
   so a card WITH a photo is left exactly as it was. */
.recipe-card__media:has(.recipe-card__placeholder) {
  container-type: inline-size;
}

.recipe-card__media .recipe-card__placeholder {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--space-4);
  overflow: hidden;
  /* background-COLOR, not the shorthand: the figure below is a
     background-image on this same element and the shorthand would erase it. */
  background-color: var(--cover-bg);
  isolation: isolate;
}

/* Hue families. One base hue per category plus a step; the per-recipe tone
   walks the family. Savory is anchored on the app's ember accent. */
.recipe-card__placeholder {
  --cover-h0: 218;
  --cover-step: 6;
  --cover-sat: 14%;
  --cover-tone: 0;
  --cover-h: calc(var(--cover-h0) + var(--cover-tone) * var(--cover-step));
}
/* Savory and sweet started 2° and 320° apart, which at 90% lightness is two
   shades of the same pink — they read as one family. They are now separated
   on BOTH axes: savory moved off red into terracotta AND carries the highest
   chroma; sweet moved cooler into mauve at a lower chroma. Hue alone was not
   enough this pale. */
.recipe-card__placeholder[data-cat="savory"] {
  --cover-h0: 14;
  --cover-step: 3.5;
  --cover-sat: 70%;
}
/* Baking neighbours savory on the wheel on purpose (bread is warm, not green)
   and is told apart by chroma: sand, not terracotta. Held below hue 48 — past
   that it goes olive in dark mode. */
.recipe-card__placeholder[data-cat="baking"] {
  --cover-h0: 36;
  --cover-step: 3;
  --cover-sat: 42%;
}
.recipe-card__placeholder[data-cat="sweet"] {
  --cover-h0: 306;
  --cover-step: 5;
  --cover-sat: 40%;
}
.recipe-card__placeholder[data-cat="drinks"] {
  --cover-h0: 166;
  --cover-step: 8;
  --cover-sat: 42%;
}

.recipe-card__placeholder[data-tone="1"] { --cover-tone: 1; }
.recipe-card__placeholder[data-tone="2"] { --cover-tone: 2; }
.recipe-card__placeholder[data-tone="3"] { --cover-tone: 3; }
.recipe-card__placeholder[data-tone="4"] { --cover-tone: 4; }

/* Light scheme: a tinted paper ground with the title in a deep, saturated
   ink of the same hue. Deliberately well clear of white — the first pass sat
   at 95% lightness and the grid still read as a wall of blank rectangles. */
.recipe-card__placeholder {
  --cover-bg: hsl(var(--cover-h) calc(var(--cover-sat) * 0.5) 90%);
  --cover-figure: hsl(var(--cover-h) calc(var(--cover-sat) * 0.55) 83.5%);
  --cover-ink: hsl(var(--cover-h) calc(var(--cover-sat) * 0.72) 28%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .recipe-card__placeholder {
    --cover-bg: hsl(var(--cover-h) calc(var(--cover-sat) * 0.36) 14%);
    --cover-figure: hsl(var(--cover-h) calc(var(--cover-sat) * 0.32) 20%);
    --cover-ink: hsl(var(--cover-h) calc(var(--cover-sat) * 0.62) 82%);
  }
}
:root[data-theme="dark"] .recipe-card__placeholder {
  --cover-bg: hsl(var(--cover-h) calc(var(--cover-sat) * 0.36) 14%);
  --cover-figure: hsl(var(--cover-h) calc(var(--cover-sat) * 0.32) 20%);
  --cover-ink: hsl(var(--cover-h) calc(var(--cover-sat) * 0.62) 82%);
}

/* The quiet figure: one soft duotone disc, bled off a different edge per
   variant. No gradient stops that read as a gradient, no shadow — it is a
   tone, not decoration.

   IT IS PAINTED, NOT PLACED. It used to be an absolutely-positioned ::before
   pulled past the tile edge (inset-inline-end: -40%), which the tile clipped
   correctly — and which still counted as SCROLLABLE OVERFLOW on the element
   that holds the cover title. core/dom.js#clipped reads exactly that shape
   (overflow:hidden + scrollWidth > clientWidth on a box containing text) and
   is right to: it is what a HUD reading "LEVEL 1 TIME 0.0 L..." looks like.
   Here it was a false alarm on artwork — measured 323px of scroll width in a
   231px tile with the title itself fitting in its 2 lines — and a check that
   is permanently red on correct design is a check nobody reads. As a
   background image the disc paints identically and overflows nothing.

   Geometry: 1cqi is 1% of the media box's WIDTH (that box is the query
   container). The old vertical insets were percentages of its HEIGHT, and the
   grid card's media is aspect-ratio 4/3, so a vertical -52% became -39cqi. */
.recipe-card__placeholder {
  background-image: radial-gradient(
    circle closest-side,
    var(--cover-figure) 99%,
    transparent 100%
  );
  background-repeat: no-repeat;
  background-size: 118cqi 118cqi;
  background-position: right -40cqi bottom -39cqi;
}
.recipe-card__placeholder[data-fig="1"] {
  background-size: 92cqi 92cqi;
  background-position: left -32cqi top -27cqi;
}
.recipe-card__placeholder[data-fig="2"] {
  background-size: 72cqi 72cqi;
  background-position: right -18cqi top -18cqi;
}
.recipe-card__placeholder[data-fig="3"] {
  background-size: 136cqi 136cqi;
  background-position: left -50cqi bottom -43.5cqi;
}

.recipe-cover__title {
  position: relative;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  /* The clamp draws its own ellipsis, but only this DECLARES that a cut title
     is meant to be marked as cut — and it is what a reader (and core/dom.js's
     clipped-text probe) checks to tell an intentional truncation from a title
     that silently lost its last word. */
  text-overflow: ellipsis;
  color: var(--cover-ink);
  font-family: var(--font-rounded);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.02em;
  text-wrap: balance;
  overflow-wrap: break-word;
  hyphens: auto;
  font-size: clamp(15px, 16cqi, 30px);
  /* Tight leading plus the clamp's overflow:hidden guillotines the descenders
     on the last line ("Frosting" lost its g). The box clips at its padding
     edge, so this buys the tails room without loosening the leading. */
  padding-block-end: 0.16em;
}
.recipe-card__placeholder[data-len="s"] .recipe-cover__title {
  font-size: clamp(17px, 21cqi, 38px);
}
.recipe-card__placeholder[data-len="l"] .recipe-cover__title {
  font-size: clamp(14px, 13.5cqi, 25px);
  -webkit-line-clamp: 4;
  line-clamp: 4;
}
/* The longest titles get a fourth line as well as the smallest type. At a
   phone's 149px card, "Carrot Cake with Cream Cheese Frosting" needs four
   lines at 13px and the three-line clamp dropped "Frosting" — and the caption
   underneath is hidden on cover cards, so the cut title was the ONLY copy of
   the name on the card. Measured at 390x844, core/dom.js#clipped. */
.recipe-card__placeholder[data-len="xl"] .recipe-cover__title {
  font-size: clamp(13px, 11.5cqi, 21px);
  -webkit-line-clamp: 4;
  line-clamp: 4;
}

/* One title, not two. Where the cover sets the title as artwork, the caption
   underneath printed the same words again — on a three-line title that reads
   as a rendering bug, which is the impression this whole change exists to
   remove. The caption title stays in the DOM rather than being deleted: the
   cover is aria-hidden, so this text is what gives the card button its
   accessible name, and it is what the tests read.
   Row cards are excluded deliberately — at 64px the cover shows initials, not
   a title, and search results highlight their matches inside that caption. */
.recipe-card:not(.recipe-card--row):has(.recipe-card__placeholder)
  .recipe-card__title {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Initials stand in when the cover is thumbnail-sized (a list row). */
.recipe-cover__monogram {
  display: none;
  position: relative;
  color: var(--cover-ink);
  font-family: var(--font-rounded);
  font-weight: 600;
  letter-spacing: -0.01em;
  font-size: 34cqi;
  line-height: 1;
}
@container (max-width: 128px) {
  .recipe-card__placeholder {
    align-items: center;
    justify-content: center;
    padding: var(--space-2);
  }
  .recipe-cover__title {
    display: none;
  }
  .recipe-cover__monogram {
    display: block;
  }
}

.recipe-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-4) var(--space-5);
}
.recipe-card__title {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
}
.recipe-card__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  font-variant-numeric: tabular-nums;
}
.recipe-card__meta .dot {
  width: 3px;
  height: 3px;
  border-radius: var(--radius-full);
  background: var(--label-tertiary);
  flex: none;
}
.recipe-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Compact list-style recipe row (thumb on the left). */
.recipe-card--row {
  flex-direction: row;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3);
}
.recipe-card--row .recipe-card__media {
  flex: none;
  width: 64px;
  height: 64px;
  aspect-ratio: auto;
  border-radius: var(--radius-md);
}
.recipe-card--row .recipe-card__body {
  padding: 0;
}

.grid-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-4);
  padding: 0 var(--gutter);
}
@media (min-width: 900px) {
  .grid-cards {
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: var(--space-5);
  }
}

/* ================================================== SEGMENTED CONTROL ==== */
.segmented {
  position: relative;
  display: flex;
  gap: 2px;
  padding: 2px;
  background: var(--fill-3);
  border-radius: var(--radius-md);
  isolation: isolate;
}
.segmented__thumb {
  position: absolute;
  z-index: 0;
  top: 2px;
  bottom: 2px;
  left: 0;
  border-radius: calc(var(--radius-md) - 3px);
  background: var(--bg-system);
  box-shadow: var(--shadow-1), 0 0 0 0.5px rgb(0 0 0 / 0.04);
  transition: transform var(--duration-base) var(--ease-out),
    width var(--duration-base) var(--ease-out);
  will-change: transform;
}
/* Dark: an elevated grey thumb, never white. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .segmented__thumb {
    background: var(--bg-system-tertiary);
  }
}
:root[data-theme="dark"] .segmented__thumb {
  background: var(--bg-system-tertiary);
}

.segmented__option {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-height: 32px;
  padding: 0 var(--space-4);
  border-radius: calc(var(--radius-md) - 3px);
  color: var(--label);
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
  letter-spacing: var(--text-subhead-track);
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}
.segmented__option:active {
  transform: scale(0.96);
}
.segmented__option[aria-selected="true"] {
  font-weight: 600;
}
.segmented--large .segmented__option {
  min-height: var(--hit);
  font-size: var(--text-callout-size);
}

/* ============================================================ STEPPER ==== */
/* The "how many people" control. Big, tactile, tabular readout. */
.stepper {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px;
  background: var(--fill-3);
  border-radius: var(--radius-full);
  user-select: none;
}
.stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-full);
  color: var(--label);
  background: transparent;
  transition: background var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-spring),
    color var(--duration-fast) var(--ease-standard);
}
.stepper__btn .icon {
  width: 17px;
  height: 17px;
}
.stepper__btn:hover {
  background: var(--fill-4);
}
.stepper__btn:active {
  background: var(--fill-2);
  transform: scale(0.88);
}
.stepper__btn[disabled] {
  color: var(--label-quaternary);
  pointer-events: none;
}
.stepper__value {
  min-width: 44px;
  text-align: center;
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  font-weight: 600;
  letter-spacing: var(--text-headline-track);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: var(--numeric-tabular);
}
.stepper__value.is-bumped {
  animation: stepper-bump var(--duration-base) var(--ease-spring);
}
@keyframes stepper-bump {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.18);
  }
  100% {
    transform: scale(1);
  }
}

/* Hero variant: the serving-size control on a recipe page. */
.stepper--hero {
  gap: var(--space-2);
  padding: var(--space-1);
}
.stepper--hero .stepper__btn {
  width: var(--hit);
  height: var(--hit);
  background: var(--bg-system-tertiary);
  box-shadow: var(--shadow-1);
  color: var(--accent);
}
.stepper--hero .stepper__btn .icon {
  width: 20px;
  height: 20px;
}
.stepper--hero .stepper__value {
  min-width: 72px;
  font-size: var(--text-title2-size);
  line-height: var(--text-title2-line);
  font-weight: 700;
}

.stepper-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
}
.stepper-field__label {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.stepper-field__hint {
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
}

/* ============================================================ BUTTONS ==== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--hit);
  padding: 0 var(--space-6);
  border-radius: var(--radius-md);
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
  letter-spacing: var(--text-body-track);
  font-weight: 600;
  white-space: nowrap;
  transition: background var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard),
    opacity var(--duration-fast) var(--ease-standard);
}
.btn .icon {
  width: 19px;
  height: 19px;
  flex: none;
}
.btn:active {
  transform: scale(0.97);
}
.btn[disabled],
.btn[aria-disabled="true"] {
  pointer-events: none;
  opacity: 0.4;
}
.btn--block {
  width: 100%;
}
.btn--small {
  min-height: 32px;
  padding: 0 var(--space-4);
  font-size: var(--text-subhead-size);
  border-radius: var(--radius-sm);
}
.btn--large {
  min-height: 52px;
  border-radius: var(--radius-lg);
  font-size: var(--text-headline-size);
}
.btn--pill {
  border-radius: var(--radius-full);
}

.btn--filled {
  background: var(--accent);
  color: var(--accent-on);
}
.btn--filled:hover {
  background: var(--accent-pressed);
}
.btn--filled:active {
  background: var(--accent-pressed);
}

.btn--tinted {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.btn--tinted:hover,
.btn--tinted:active {
  background: var(--accent-soft-pressed);
}

.btn--gray {
  background: var(--fill-3);
  color: var(--label);
}
.btn--gray:hover,
.btn--gray:active {
  background: var(--fill-2);
}

.btn--plain {
  background: transparent;
  color: var(--accent-text);
  padding: 0 var(--space-3);
  font-weight: 400;
}
.btn--plain:hover {
  background: var(--fill-4);
}
.btn--plain:active {
  opacity: 0.6;
}

.btn--destructive {
  background: var(--destructive);
  color: var(--destructive-on);
}
.btn--destructive:hover,
.btn--destructive:active {
  background: var(--destructive-pressed);
}

.btn--destructive-tinted {
  background: var(--destructive-soft);
  color: var(--destructive);
}
.btn--destructive-plain {
  background: transparent;
  color: var(--destructive);
  font-weight: 400;
}
.btn--destructive-plain:hover {
  background: var(--destructive-soft);
}

.btn-row {
  display: flex;
  gap: var(--space-3);
}
.btn-row > .btn {
  flex: 1 1 0;
}

/* ============================================================== FIELDS === */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
}
.field__label {
  color: var(--label-secondary);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.field__control {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  background: var(--bg-grouped-secondary);
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--separator);
  transition: box-shadow var(--duration-fast) var(--ease-standard);
}
.field__control:focus-within {
  box-shadow: inset 0 0 0 1.5px var(--accent), var(--focus-ring);
}
.field__input,
.field textarea {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--hit);
  padding: var(--space-3) var(--space-4);
  background: transparent;
  border: 0;
  border-radius: inherit;
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
  letter-spacing: var(--text-body-track);
  color: var(--label);
}
.field__input:focus-visible,
.field textarea:focus-visible {
  outline: none;
}
.field__input::placeholder,
.field textarea::placeholder {
  /* A placeholder is text a user has to read to know what to type — it gets
     the readable placeholder grey, not the decorative tertiary. */
  color: var(--label-placeholder);
}
.field textarea {
  resize: vertical;
  min-height: 96px;
  padding-top: var(--space-4);
  font-family: inherit;
}
.field__hint {
  color: var(--label-secondary);
  font-size: var(--text-caption1-size);
  line-height: var(--text-caption1-line);
}
.field.is-invalid .field__control {
  box-shadow: inset 0 0 0 1.5px var(--destructive);
}
.field.is-invalid .field__hint {
  color: var(--destructive);
}
.field__affix {
  flex: none;
  padding-right: var(--space-4);
  color: var(--label-secondary);
  font-size: var(--text-subhead-size);
}

/* ============================================================== SEARCH === */
/* Search is a first-class surface: field (idle / focused+cancel / filled /
   running), scope control, highlighted results, recents, and its own
   "no results" state. All of it is built from the tokens above.            */

/* The bar owns the row: field + the Cancel button that slides in on focus. */
.search-bar {
  display: flex;
  align-items: center;
  padding: 0 var(--gutter) var(--space-4);
}

/* Search field — leading magnifier, iOS-grey fill, no border. */
.search {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  padding: 0 var(--space-3);
  background: var(--fill-3);
  border-radius: var(--radius-md);
  min-height: var(--hit);
  transition: box-shadow var(--duration-fast) var(--ease-standard);
}
.search:focus-within {
  box-shadow: var(--focus-ring);
}
.search .icon {
  flex: none;
  width: 17px;
  height: 17px;
  color: var(--label-secondary);
}
.search__input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--hit);
  background: transparent;
  border: 0;
  font-size: var(--text-callout-size);
  letter-spacing: var(--text-callout-track);
}
.search__input::placeholder {
  color: var(--label-placeholder);
}
.search__input::-webkit-search-cancel-button {
  display: none;
}
.search__clear {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  color: var(--label-inverted);
  background: var(--label-tertiary);
  transition: opacity var(--duration-fast) var(--ease-standard);
}
.search__clear .icon {
  width: 11px;
  height: 11px;
  color: inherit;
}
/* Query in flight: a small spinner takes the trailing slot. */
.search__spinner {
  flex: none;
  width: 16px;
  height: 16px;
  border-width: 2px;
}

/* Cancel slides in from zero width while the field has focus or content. */
.search__cancel {
  flex: none;
  max-width: 0;
  padding: 0;
  opacity: 0;
  overflow: hidden;
  white-space: nowrap;
  min-height: var(--hit);
  color: var(--accent-text);
  font-size: var(--text-body-size);
  letter-spacing: var(--text-body-track);
  transition: max-width var(--duration-base) var(--ease-out),
    padding var(--duration-base) var(--ease-out),
    opacity var(--duration-fast) var(--ease-standard);
}
.search-bar.is-active .search__cancel {
  max-width: 120px;
  padding-left: var(--space-4);
  opacity: 1;
}
.search__cancel:active {
  opacity: 0.55;
}

/* Scope control under the field: All / Savory / Sweet / Drinks / Baking. */
.search-scope {
  padding: 0 var(--gutter) var(--space-4);
}
@media (max-width: 430px) {
  /* five scopes have to fit a 390px phone without clipping */
  .search-scope .segmented__option {
    padding: 0 var(--space-2);
    font-size: var(--text-footnote-size);
  }
}

/* The matched substring. Never the UA's yellow <mark>. */
mark.search-highlight,
.search-highlight {
  background: var(--highlight-bg);
  color: inherit;
  font: inherit;
  font-weight: inherit;
  border-radius: 4px;
  padding: 0 2px;
  margin: 0 -1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.search-highlight.is-active {
  background: var(--highlight-bg-active);
}

/* Result row: title with highlights + one line saying WHICH field matched. */
.search-result .list-row__text {
  gap: 2px;
}
.search-result__thumb {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--fill-4);
  color: var(--label-tertiary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.search-result__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.search-result__thumb .icon {
  width: 20px;
  height: 20px;
}
/* A 44px thumb pushes the hairline further right than a 29px icon tile. */
.list-row.has-thumb {
  --sep-inset: calc(var(--space-5) + 44px + var(--space-4));
}
.search-result__match {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  letter-spacing: var(--text-footnote-track);
  color: var(--label-secondary);
  white-space: nowrap;
  overflow: hidden;
}
.search-result__field {
  flex: none;
  /* secondary, not tertiary: 13px tertiary text is unreadable on a phone
     propped against a kitchen wall. The highlight carries the emphasis. */
  color: var(--label-secondary);
  font-weight: 500;
}
.search-result__field::after {
  content: ":";
}
.search-result__snippet {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Recents / suggestions: a list header that carries a trailing action. */
.list-header--action {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.list-header__action {
  color: var(--accent-text);
  font-size: var(--text-footnote-size);
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
}
.list-header__action:active {
  opacity: 0.55;
}
/* The ↖ button that puts a recent query back in the field. */
.search-recent__insert {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin: -4px -6px -4px 0;
  border-radius: var(--radius-full);
  color: var(--label-tertiary);
}
.search-recent__insert:hover {
  background: var(--fill-4);
  color: var(--label-secondary);
}

/* Switch (used by settings rows). */
.switch {
  position: relative;
  flex: none;
  width: 51px;
  height: 31px;
  border-radius: var(--radius-full);
  background: var(--fill-1);
  transition: background var(--duration-base) var(--ease-standard);
}
.switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 27px;
  height: 27px;
  border-radius: var(--radius-full);
  background: #fff;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  transition: transform var(--duration-base) var(--ease-out);
}
.switch[aria-checked="true"] {
  background: var(--accent);
}
.switch[aria-checked="true"]::after {
  transform: translateX(20px);
}

/* =============================================================== SHEET === */
.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-standard);
}
.sheet-backdrop.is-open {
  opacity: 1;
}

.sheet {
  /* Inside a sheet every surface steps up one tier: the sheet itself is the
     grouped background and rows sit on the tertiary one. In light both land
     on the same values as the page; in dark it is what stops the sheet from
     disappearing into the backdrop. */
  --bg-grouped-secondary: var(--bg-system-tertiary);
  position: relative;
  width: 100%;
  max-width: 560px;
  max-height: min(92dvh, 860px);
  display: flex;
  flex-direction: column;
  background: var(--bg-system-secondary);
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  box-shadow: var(--shadow-sheet);
  transform: translateY(100%);
  transition: transform var(--duration-slow) var(--ease-out);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.sheet-backdrop.is-open .sheet {
  transform: translateY(0);
}

.sheet__grabber {
  flex: none;
  width: 36px;
  height: 5px;
  margin: var(--space-2) auto var(--space-1);
  border-radius: var(--radius-full);
  background: var(--label-quaternary);
}

.sheet__header {
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: var(--hit);
  padding: var(--space-2) var(--space-5);
}
.sheet__title {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
  text-align: center;
}
.sheet__close {
  position: absolute;
  right: var(--space-4);
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-full);
  background: var(--fill-3);
  color: var(--label-secondary);
}
.sheet__close .icon {
  width: 13px;
  height: 13px;
}
.sheet__close:hover {
  background: var(--fill-2);
}

.sheet__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-2) 0 var(--space-5);
  -webkit-overflow-scrolling: touch;
}
.sheet__body > .sheet__pad {
  padding: 0 var(--space-5);
}

.sheet__actions {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-6);
  box-shadow: 0 -0.5px 0 var(--separator);
  background: var(--bg-system-secondary);
}
.sheet__actions--row {
  flex-direction: row;
}
.sheet__actions--row > .btn {
  flex: 1 1 0;
}

/* Centred alert-style dialog (the alert()/confirm() replacement). */
.sheet-backdrop--dialog {
  align-items: center;
  padding: var(--space-6);
}
.dialog {
  width: 100%;
  max-width: 320px;
  background: var(--bg-system-tertiary);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  transform: scale(0.9);
  opacity: 0;
  transition: transform var(--duration-base) var(--ease-spring),
    opacity var(--duration-fast) var(--ease-standard);
}
.sheet-backdrop.is-open .dialog {
  transform: scale(1);
  opacity: 1;
}
.dialog__body {
  padding: var(--space-6) var(--space-5) var(--space-5);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.dialog__title {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
}
.dialog__message {
  color: var(--label-secondary);
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
}
.dialog__actions {
  display: flex;
  flex-direction: column;
  box-shadow: 0 -0.5px 0 var(--separator);
}
.dialog__actions.is-horizontal {
  flex-direction: row;
}
.dialog__action {
  flex: 1 1 0;
  min-height: var(--hit);
  padding: 0 var(--space-4);
  color: var(--accent-text);
  font-size: var(--text-body-size);
  letter-spacing: var(--text-body-track);
  transition: background var(--duration-instant) linear;
}
.dialog__action + .dialog__action {
  box-shadow: -0.5px 0 0 var(--separator), 0 -0.5px 0 var(--separator);
}
.dialog__actions:not(.is-horizontal) .dialog__action + .dialog__action {
  box-shadow: 0 -0.5px 0 var(--separator);
}
.dialog__action:hover {
  background: var(--fill-4);
}
.dialog__action:active {
  background: var(--fill-3);
}
.dialog__action.is-preferred {
  font-weight: 600;
}
.dialog__action.is-destructive {
  color: var(--destructive);
}
.dialog__action.is-cancel {
  color: var(--label);
}

/* =============================================================== TOAST === */
.toast-host {
  position: fixed;
  left: 50%;
  bottom: calc(var(--space-11) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: center;
  width: min(420px, calc(100vw - 2 * var(--gutter)));
  pointer-events: none;
}
@media (min-width: 900px) {
  .toast-host {
    bottom: var(--space-7);
  }
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 100%;
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-full);
  background: var(--material-thick);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: var(--shadow-3), 0 0 0 0.5px var(--separator);
  color: var(--label);
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
  letter-spacing: var(--text-subhead-track);
  font-weight: 500;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(14px) scale(0.96);
  transition: opacity var(--duration-base) var(--ease-standard),
    transform var(--duration-base) var(--ease-spring);
}
.toast.is-open {
  opacity: 1;
  transform: none;
}
.toast .icon {
  flex: none;
  width: 18px;
  height: 18px;
}
.toast--success .icon {
  color: var(--success);
}
.toast--error .icon {
  color: var(--destructive);
}
.toast--info .icon {
  color: var(--accent);
}

/* Inline banner — sits in the flow, not floating. */
.banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0 var(--gutter);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--label);
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
}
.banner .icon {
  flex: none;
  width: 19px;
  height: 19px;
  color: var(--accent);
  margin-top: 1px;
}
.banner__text {
  min-width: 0;
}
.banner__title {
  font-weight: 600;
}
.banner--warning {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
}
.banner--warning .icon {
  color: var(--warning);
}
.banner--error {
  background: var(--destructive-soft);
}
.banner--error .icon {
  color: var(--destructive);
}
.banner--success {
  background: color-mix(in srgb, var(--success) 14%, transparent);
}
.banner--success .icon {
  color: var(--success);
}

/* ====================================================== PROGRESS / SPIN == */
.progress {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: 100%;
}
.progress__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  color: var(--label-secondary);
}
.progress__value {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: var(--numeric-tabular);
  color: var(--label);
  font-weight: 600;
}
.progress__track {
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--fill-3);
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  width: 0%;
  border-radius: inherit;
  background: var(--accent);
  transition: width var(--duration-base) var(--ease-standard);
}
.progress.is-indeterminate .progress__fill {
  width: 35%;
  animation: progress-slide 1.2s var(--ease-standard) infinite;
}
@keyframes progress-slide {
  0% {
    transform: translateX(-110%);
  }
  100% {
    transform: translateX(320%);
  }
}

.spinner {
  width: 20px;
  height: 20px;
  border-radius: var(--radius-full);
  border: 2px solid var(--fill-2);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
}
.spinner--large {
  width: 32px;
  height: 32px;
  border-width: 3px;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ========================================================= EMPTY STATE == */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-10) var(--space-7);
  margin: 0 auto;
  max-width: 380px;
}
.empty__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin-bottom: var(--space-2);
  border-radius: var(--radius-xl);
  background: var(--fill-4);
  color: var(--label-tertiary);
}
.empty__icon .icon {
  width: 30px;
  height: 30px;
}
.empty__title {
  font-size: var(--text-title3-size);
  line-height: var(--text-title3-line);
  letter-spacing: var(--text-title3-track);
  font-weight: 600;
}
.empty__message {
  color: var(--label-secondary);
  font-size: var(--text-subhead-size);
  line-height: var(--text-subhead-line);
}
.empty .btn {
  margin-top: var(--space-4);
}
/* "No results" lives under a search bar — not on a blank page. */
.empty--compact {
  padding: var(--space-8) var(--space-7);
}
.empty__query {
  color: var(--label);
}

/* ========================================================= CHIPS / PILLS = */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 26px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  /* fill-4, not fill-3: a chip is 12px text, so it needs the lighter of the
     two fills under it to clear 4.5:1 with --label-secondary. */
  background: var(--fill-4);
  color: var(--label-secondary);
  font-size: var(--text-caption1-size);
  line-height: var(--text-caption1-line);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.chip .icon {
  width: 12px;
  height: 12px;
}
.chip--accent {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.chip--success {
  background: color-mix(in srgb, var(--success) 16%, transparent);
  color: var(--success);
}
.chip--warning {
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  color: var(--warning);
}
.chip--destructive {
  background: var(--destructive-soft);
  color: var(--destructive);
}
.chip__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-right: -4px;
  border-radius: var(--radius-full);
  color: inherit;
  opacity: 0.6;
}
.chip__remove:hover {
  opacity: 1;
  background: var(--fill-4);
}

/* Category pill — a bigger, tappable filter. */
.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 34px;
  /* a short label ("All") would otherwise be 41px wide — under the target in
     the other axis. The height comes from the ::before expansion below. */
  min-width: var(--hit);
  padding: 0 var(--space-4);
  border-radius: var(--radius-full);
  background: var(--bg-grouped-secondary);
  box-shadow: var(--shadow-1);
  color: var(--label);
  font-size: var(--text-subhead-size);
  letter-spacing: var(--text-subhead-track);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}
.pill:active {
  transform: scale(0.96);
}
.pill.is-active,
.pill[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-on);
  box-shadow: none;
}

/* Horizontal scroller for pills/chips — no visible scrollbar. */
.scroll-row {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-1) var(--gutter) var(--space-4);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
}
.scroll-row::-webkit-scrollbar {
  display: none;
}
.scroll-row > * {
  scroll-snap-align: start;
  flex: none;
}

/* ====================================================== DROP ZONE ======= */
/* Photo drop target for the add flow: dashed hairline, generous radius,
   pressed + drag-over states. Built for a <div role="button"> because it
   contains a file input (a <button> may not).                             */
.dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 176px;
  padding: var(--space-8) var(--space-6);
  border: 1.5px dashed var(--separator);
  border-radius: var(--radius-lg);
  background: var(--bg-grouped-secondary);
  color: var(--label-secondary);
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard);
}
.dropzone:hover {
  background: var(--fill-4);
}
.dropzone:active,
.dropzone.is-pressed {
  background: var(--fill-3);
  transform: scale(0.99);
}
.dropzone.is-dragover {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-text);
}
.dropzone.is-dragover .dropzone__icon {
  background: transparent;
  color: var(--accent);
}
.dropzone[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}
.dropzone__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: var(--radius-full);
  background: var(--fill-4);
  color: var(--accent);
  transition: background var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard);
}
.dropzone__icon .icon {
  width: 24px;
  height: 24px;
}
.dropzone__title {
  color: var(--label);
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-line);
  letter-spacing: var(--text-headline-track);
  font-weight: var(--text-headline-weight);
}
.dropzone.is-dragover .dropzone__title {
  color: var(--accent-text);
}
.dropzone__hint {
  font-size: var(--text-footnote-size);
  line-height: var(--text-footnote-line);
  color: var(--label-secondary);
  max-width: 30ch;
}
.dropzone__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* ==================================================== THUMB STRIP ======= */
/* Row of picked photos. Compose with .scroll-row for horizontal scrolling. */
.thumb-strip {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2) var(--gutter);
}
.thumb-strip__item {
  position: relative;
  flex: none;
  width: 72px;
  height: 72px;
  border-radius: var(--radius-md);
  background: var(--fill-4);
  box-shadow: var(--shadow-1);
}
.thumb-strip__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.thumb-strip__item .icon {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22px;
  height: 22px;
  color: var(--label-tertiary);
}
.thumb-strip__item.is-busy::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--scrim);
}
.thumb-strip__item .thumb-strip__spinner {
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: 1;
  border-color: rgb(255 255 255 / 0.35);
  border-top-color: #fff;
}
.thumb-strip__remove {
  position: absolute;
  top: -7px;
  right: -7px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--label);
  color: var(--label-inverted);
  /* a shadow, not a ring in the card colour: the strip sits on the grouped
     canvas as often as on a card, and a white ring haloes against grey */
  box-shadow: var(--shadow-2);
  transition: transform var(--duration-fast) var(--ease-spring);
}
/* The badge is 24px for looks; this gives it a 44px touch target. */
.thumb-strip__remove::before {
  content: "";
  position: absolute;
  inset: -10px;
}
.thumb-strip__remove .icon {
  position: static;
  width: 11px;
  height: 11px;
  color: inherit;
}
.thumb-strip__remove:active {
  transform: scale(0.88);
}
/* Optional trailing "add another" tile. */
.thumb-strip__add {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border: 1.5px dashed var(--separator);
  border-radius: var(--radius-md);
  color: var(--label-secondary);
  background: transparent;
  transition: background var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard);
}
.thumb-strip__add:hover {
  background: var(--fill-4);
  border-color: var(--accent);
  color: var(--accent-text);
}

/* =========================================================== SKELETON === */
.skeleton {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--skeleton-base);
}
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    var(--skeleton-sheen),
    transparent
  );
  animation: shimmer 1.4s var(--ease-standard) infinite;
}
@keyframes shimmer {
  100% {
    transform: translateX(100%);
  }
}
.skeleton--text {
  height: 12px;
  border-radius: var(--radius-full);
}
.skeleton--title {
  height: 18px;
  width: 60%;
  border-radius: var(--radius-full);
}
.skeleton--thumb {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
}
.skeleton--avatar {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
}

/* =========================================================== MISC ======= */
.icon {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon--filled {
  fill: currentColor;
  stroke: none;
}

.section {
  padding: var(--space-6) 0;
}
.section__title {
  padding: 0 var(--gutter) var(--space-3);
  font-size: var(--text-title3-size);
  line-height: var(--text-title3-line);
  letter-spacing: var(--text-title3-track);
  font-weight: 600;
}
.pad {
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.placeholder-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-10) var(--gutter);
  text-align: center;
  color: var(--label-secondary);
}

/* ====================================================== TOUCH TARGETS === */
/* DESIGN.md §0: every control must be tappable at >= 44px (var(--hit)).
   Several controls are deliberately SMALLER than that visually, because that
   is what the Apple idiom looks like: a 22px clear glyph, a 34px pill, a 32px
   small button. The fix is to grow the HIT AREA, not the box — an absolutely
   positioned ::before that reaches past the visible edge. A tap on it still
   targets the control, and nothing moves.

   Where a control is already >= 44px (.btn, .list-row, .icon-button,
   .tabbar__item, .dialog__action, .search__cancel, .dropzone) there is
   nothing to do. */
.search__clear,
.pill,
.btn--small,
.stepper__btn,
.sheet__close,
.search-recent__insert,
.chip__remove,
.list-header__action,
.switch {
  position: relative;
}

.search__clear::before,
.pill::before,
.btn--small::before,
.stepper__btn::before,
.sheet__close::before,
.search-recent__insert::before,
.chip__remove::before,
.list-header__action::before,
.switch::before {
  content: "";
  position: absolute;
  /* the expansion is per-control below */
}

/* 22px glyph -> 44px target */
.search__clear::before {
  inset: -11px;
}
/* 34px pill -> 44px tall; horizontal is already >= 44 and expanding sideways
   would overlap the neighbouring pill in a .scroll-row */
.pill::before {
  inset: -5px 0;
}
/* 32px small button -> 44px tall, width already comfortable */
.btn--small::before {
  inset: -6px 0;
}
/* 38px stepper button -> 44px */
.stepper__btn::before {
  inset: -3px;
}
/* 30px round buttons -> 44px */
.sheet__close::before,
.search-recent__insert::before {
  inset: -7px;
}
/* 16px chip remove: full 44px vertically, but only +10px sideways so it does
   not swallow taps meant for the next chip (the row gap is 4-8px) */
.chip__remove::before {
  inset: -14px -10px;
}
/* text-sized header action ("Clear") */
.list-header__action::before {
  inset: -13px -8px;
}
/* 31px switch -> 45px */
.switch::before {
  inset: -7px 0;
}

/* The segmented control is the one deliberate exception, and only on a fine
   pointer. 32px is Apple's own size for it with a mouse, where the target is
   the full option width and pointing is precise. On any coarse pointer — the
   phone propped against the kitchen wall — it grows to the full 44px. */
@media (pointer: coarse) {
  .segmented__option {
    min-height: var(--hit);
  }
}

/* ------------------------------------------------ reduced motion -------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .app-main {
    scroll-behavior: auto;
  }
  .skeleton::after {
    display: none;
  }
  .spinner {
    animation: spin 1.4s linear infinite !important;
  }
}
