/* Grocerating — a friendly, rounded grocery look: yellow for what you act on, lime for what is
 * done (have it, in the cart), white cards on a cool off-white ground.
 *
 * Two scenes set the rules. Planning happens sitting down, often on a laptop, so wide screens get
 * real multi-column layouts. Shopping happens in a supermarket aisle, one hand on the trolley and
 * the phone at arm's length under hard light, so every in-store control is thumb-sized and every
 * text/control pair is measured against WCAG AA rather than eyeballed.
 *
 * Colour roles are split by job. Yellow is a fill only: at 1.3:1 against white it can never carry
 * text or a hairline, and it must never touch lime (1.03:1). `--lime-ink` is lime's text role.
 *
 * Layout has three navigation modes:
 *   phone portrait            bottom bar with a raised run button
 *   tablet / phone landscape  a left rail (the bottom bar would eat a short screen)
 *   desktop (1024+)           the nav lifts into the top bar
 */

/* ---- Fonts ----------------------------------------------------------------------------------
 * Inter, self-hosted (SIL OFL, see fonts/OFL.txt): one variable file per script covering 400–700.
 * A third-party stylesheet in <head> blocked the first paint for seconds on weak in-store signal;
 * local files ride the same connection as the app. Only Latin is preloaded (index.html); the
 * others download only if a page contains those characters, e.g. an item named in Cyrillic.
 * `swap` shows the system face at once and swaps Inter in when it lands. */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-vietnamese.woff2") format("woff2"); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-cyrillic.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-cyrillic-ext.woff2") format("woff2"); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-greek.woff2") format("woff2"); unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("../fonts/inter-greek-ext.woff2") format("woff2"); unicode-range: U+1F00-1FFF; }

/* ---- Tokens --------------------------------------------------------------------------------- */

:root {
  /* brand */
  --yellow: #f9e000;           /* fill only; ink on it = 12:1 */
  --yellow-hover: #eed600;
  --yellow-press: #e2cb00;
  --yellow-soft: #fff6bf;
  --on-yellow: #202124;
  --lime: #b9dd55;             /* fill; ink on it = 10.4:1 */
  --lime-light: #d7eb9a;
  --lime-soft: #eef6d6;
  --lime-ink: #3e5c0a;         /* lime's text/icon role: 7.7:1 on white, 6.7:1 on --lime-soft */

  /* surfaces */
  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-muted: #f1f2f5;
  --surface-hover: #e9ebef;

  /* text */
  --text: #202124;
  --text-secondary: #666970;   /* 4.5:1 on every ground it lands on, worst case first: 4.6:1 on
                                  --surface-hover (a hovered chip), 4.9:1 --surface-muted, 4.9:1
                                  --lime-soft, 5.0:1 --warn-soft, 5.2:1 --bg, 5.5:1 white. The
                                  hovered ground is the binding one — the previous value passed
                                  at rest and dropped to 4.28:1 the moment a chip was hovered. */
  --text-muted: #999999;       /* decorative marks only — 2.9:1, never text */

  /* lines */
  --border: #e5e6ea;           /* separators and card edges */
  --field-border: #83878f;     /* control boundaries: 3:1 on every ground a control sits on —
                                  3.6:1 white, 3.4 --bg, 3.2 --surface-muted (the price form,
                                  a copy box), 3.0 --surface-hover, 3.2 --lime-soft (a checked
                                  shopping row). Quoting the figure on white alone would have
                                  overstated it: the tinted grounds are the binding case. */
  --focus: #202124;
  --focus-halo: rgba(249, 224, 0, 0.6);

  /* semantic */
  --danger: #b3261e;           /* 5.7:1 on --danger-soft */
  --danger-soft: #fdecea;
  --warn-text: #5e4f00;        /* 7.4:1 on --warn-soft */
  --warn-soft: #fff6bf;
  --warn-line: #d8bd1c;

  /* illustration */
  --scrim: rgba(16, 18, 20, 0.45);    /* behind dialogs */
  --on-danger: #ffffff;               /* text on a solid danger fill (error toast) */

  /* illustration */
  --art-ink: #202124;
  --art-panel: var(--lime-light);
  --art-disc: var(--lime);
  --art-paper: #ffffff;
  --art-yellow-deep: #e8cf00;         /* the bag's folded band and the bottle's cap */
  --art-leaf: #e4f2b4;                /* the bottle */
  --art-apple: #e8463c;               /* the apple — the logo's red, and its only other use */
  /* The chip behind Google's G. Fixed white in BOTH themes on purpose: the mark is Google's, not
     ours, and its four brand colours are specified against a white ground. Not --surface, which
     goes near-black in dark mode. */
  --google-chip: #ffffff;

  /* a form control's drop-down chevron, in --text-secondary */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666970' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E");

  /* shape */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-xl: 24px;
  --radius-pill: 999px;

  /* depth — offset and soft blur, never a halo */
  --shadow-sm: 0 1px 3px rgba(16, 18, 20, 0.06);
  --shadow: 0 4px 16px rgba(16, 18, 20, 0.06);
  --shadow-raised: 0 8px 24px rgba(16, 18, 20, 0.1);
  --shadow-lg: 0 18px 48px rgba(16, 18, 20, 0.18);
  --shadow-cta: 0 2px 8px rgba(122, 104, 0, 0.22);
  --shadow-nav: 0 -1px 0 var(--border), 0 -8px 24px rgba(16, 18, 20, 0.05);   /* the phone bottom bar, lifted off the page */
  --shadow-run: 0 8px 18px rgba(122, 104, 0, 0.3);   /* the raised run button in the bottom bar */
  --card-edge: 0 0 0 transparent;

  /* type */
  --text-2xs: 0.6875rem;       /* 11 — bottom-bar labels, date-tile months */
  --font: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --text-xs: 0.75rem;          /* 12 — metadata */
  --text-sm: 0.8125rem;        /* 13 — secondary lines */
  --text-base: 0.9375rem;      /* 15 — body */
  --text-md: 1rem;             /* 16 — inputs, card titles */
  --text-lg: 1.125rem;         /* 18 — section titles */
  --text-xl: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
  --text-2xl: clamp(1.5rem, 1.2rem + 1.1vw, 2rem);
  --text-numeral: 2rem;        /* 32 — counts: hero, shopping progress */

  /* metrics */
  --tap: 44px;
  --topbar-h: 56px;
  --nav-h: 64px;
  --nav-raise: 20px;           /* how far the run button stands out of the bottom bar */
  --rail-w: 88px;
  --content-max: 1440px;
  --gutter: clamp(16px, 3.2vw, 32px);
  --page-gap: clamp(16px, 1.6vw, 24px);

  /* motion */
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
  --dur: 180ms;
}

@media (prefers-color-scheme: dark) {
  :root {
    --yellow-hover: #fff04d;
    --yellow-press: #e2cb00;
    --yellow-soft: #37330f;
    --lime-light: #2f3b1a;
    --lime-soft: #252f17;
    --lime-ink: #b9dd55;       /* 10.9:1 on surface */

    --bg: #121412;
    --surface: #1b1e1a;
    --surface-muted: #252923;
    --surface-hover: #2e332c;

    --text: #eef0ea;
    --text-secondary: #a7aca2; /* 7.3:1 on surface */
    --text-muted: #6f766b;

    --border: #2c302a;
    --field-border: #7d8479;   /* 4.4:1 on surface, 3.4:1 at its worst (--surface-hover) */
    --focus: #f9e000;
    --focus-halo: rgba(249, 224, 0, 0.35);

    --danger: #f4837a;
    --danger-soft: #3a1f1c;
    --warn-text: #f0d35a;
    --warn-soft: #34300f;
    --warn-line: #8a7a1c;

    --scrim: rgba(0, 0, 0, 0.6);
    --on-danger: #1b1e1a;      /* the dark danger fill is light, so its text is dark */

    --art-ink: #121412;
    --art-panel: #2b3619;
    --art-disc: #3a4a1e;
    --art-paper: #f4f5ef;

    --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a7aca2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E");

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
    --shadow-raised: 0 6px 18px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 18px 48px rgba(0, 0, 0, 0.6);
    --shadow-cta: 0 2px 8px rgba(0, 0, 0, 0.45);
    /* Shadows vanish on a dark ground, so cards get a hairline instead. */
    --card-edge: 0 0 0 1px var(--border);
    --shadow-nav: 0 -1px 0 var(--border);   /* no glow on a dark ground, just the edge */
  }
}

/* ---- Base ----------------------------------------------------------------------------------- */

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html {
  -webkit-text-size-adjust: 100%;
  scrollbar-color: var(--field-border) transparent;
  scrollbar-width: thin;
  /* Keep a focused control clear of the sticky top bar and of the floating footer (and, on
     phones, the bottom bar) when the browser scrolls it into view — WCAG 2.4.11. */
  scroll-padding-top: calc(var(--topbar-h) + 12px);
  scroll-padding-bottom: 96px;
}
@media (max-width: 47.9988em) and (orientation: portrait), (max-width: 47.9988em) and (min-height: 32.5012em) {
  html { scroll-padding-bottom: calc(var(--nav-h) + var(--nav-raise) + env(safe-area-inset-bottom) + 96px); }
}
body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  font-feature-settings: "cv11", "ss03";   /* Inter: single-storey a off, open digits */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  caret-color: var(--text);
}
::selection { background: var(--lime-light); color: var(--text); }

/* Item names, meal names and shop names are user data and may be in any script. `plaintext`
   is the CSS form of dir="auto": each value picks its own direction from its first strong
   character, so an Arabic or Hebrew item name reads correctly inside this LTR interface.
   Applied to the text nodes themselves, never to the flex rows that hold them — resolving
   direction on a flex container would reorder its items. */
.row-title, .chip, .qty-hint, .stat-sub, .kv > span:last-child,
.table td, .page-header h1, input.input, textarea.input {
  unicode-bidi: plaintext;
}

a { color: var(--lime-ink); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
a:hover { text-decoration-thickness: 2px; }
h1, h2, h3 { margin: 0; line-height: 1.2; text-wrap: balance; }
h1 { font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.025em; }
h2 { font-size: var(--text-lg); font-weight: 600; letter-spacing: -0.01em; }
h3 { font-size: var(--text-md); font-weight: 600; }
p { margin: 0 0 0.75rem; }
code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em; background: var(--surface-muted); padding: 0.12em 0.4em; border-radius: var(--radius-sm);
}
button { font: inherit; color: inherit; }
strong { font-weight: 600; }
.muted { color: var(--text-secondary); }
.small { font-size: var(--text-sm); }
.error-text { color: var(--danger); }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.icon { display: block; flex: none; }

/* One focus treatment for everything: an ink ring (yellow in dark mode) on a surface-coloured gap,
   so it stays visible on white, on the yellow button and on lime alike. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--focus);
  border-radius: var(--radius-sm);
  position: relative;
  z-index: 1;
}
:where(.btn, .pill, .nav a, .nav-run, .step, .seg-btn, .stepper-btn, .chip-link, .brand):focus-visible { border-radius: var(--radius-pill); }
/* A page's heading takes focus on arrival so screen readers announce the new page. It is a landing
   point, not a control — the next Tab goes to the first control — so it carries no ring. */
#main h1[tabindex="-1"]:focus { box-shadow: none; outline: none; }

.skip-link {
  position: absolute; top: 0; left: 0; z-index: 60;
  transform: translateY(-150%);
  margin: 0.5rem; padding: 0.7rem 1.1rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--field-border); border-radius: var(--radius-pill);
  font: inherit; font-weight: 600; cursor: pointer;
}
.skip-link:focus { transform: none; }
#main:focus { outline: none; }

/* ---- Shell: top bar ------------------------------------------------------------------------- */

.topbar {
  position: sticky; top: 0; z-index: 30;
  min-height: var(--topbar-h);
  background: var(--surface);
  box-shadow: 0 1px 0 var(--border);
}
.topbar-inner {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--topbar-h);
  max-width: var(--content-max); margin-inline: auto;
  padding-inline: var(--gutter);
}
/* The logo (markup in index.html). Two parts: the app-icon tile and the wordmark, which is
   Nunito ExtraBold converted to outlines — the app carries no display face, and drawn letters
   cannot shift under the tile while a webfont loads. The wordmark's letters are currentColor, so
   they follow the theme's ink; everything else in the logo is fixed brand art and keeps its own
   colours in both themes (the one place in the stylesheet where a literal colour is correct). */
.brand {
  display: inline-flex; align-items: center; gap: 10px; flex: none;
  min-height: var(--tap);
  color: var(--text); text-decoration: none;
}
/* The tile keeps the app icon's own corner (13/48 of its side) rather than --radius-md: the icon
   travels to the home screen and the browser tab, and it should be the same shape everywhere.
   The shadow rides the wrapper so it follows that corner exactly. */
.brand-mark {
  display: block; width: 36px; height: 36px;
  border-radius: 10px; box-shadow: var(--shadow-cta);
}
.brand-tile { display: block; width: 100%; height: 100%; }
/* Sized against the 18px bold text it replaces: the box includes the g's descender and the leaf,
   so it is taller than the letters it shows. */
.brand-word { display: block; height: 19px; width: auto; }
.brand-leaf { fill: #8ec63f; }
.topbar-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; min-width: 0; }
/* With large text on a narrow screen the wordmark gives way to the tile alone. The link is named
   by its aria-label, so dropping the drawing costs nothing. An `em` media query tracks the
   reader's text size. (Not a container query: its layout containment would trap the fixed bottom
   bar inside the header.) */
@media (max-width: 21.25em) {
  .brand-word { display: none; }
}

/* Sync status. The label always carries the state; the dot only repeats it. */
.pill {
  display: inline-flex; align-items: center; gap: 7px; min-width: 0; max-width: 100%;
  min-height: 36px; padding: 0 14px 0 12px;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text-secondary);
  font-size: var(--text-sm); font-weight: 600; white-space: nowrap; cursor: pointer;
  transition: background-color var(--dur) ease, border-color var(--dur) ease;
}
.pill:hover { background: var(--surface-muted); }
.pill-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); flex: none; }
.pill-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.pill-ok { color: var(--lime-ink); background: var(--lime-soft); border-color: transparent; }
.pill-ok .pill-dot { background: var(--lime-ink); }
.pill-warn { color: var(--warn-text); background: var(--warn-soft); border-color: transparent; }
.pill-warn .pill-dot { background: var(--warn-text); }
.pill-bad { color: var(--danger); background: var(--danger-soft); border-color: transparent; }
.pill-bad .pill-dot { background: var(--danger); }
.pill-busy .pill-dot { background: var(--text-secondary); animation: pulse 1.1s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: 0.25; } }

/* The top-bar run button exists for desktop only; phones and tablets carry it in the nav.
   Scoped to .topbar so it outranks the later `.btn { display: inline-flex }`. */
.topbar .run-cta { display: none; }

/* ---- Shell: navigation ---------------------------------------------------------------------- */

/* Phone portrait: a bottom bar. It follows <main> in the DOM so focus and screen-reader order
   match what the eye sees; the other modes lift it elsewhere with CSS alone. */
.nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); align-items: end;
  height: calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding: 0 4px env(safe-area-inset-bottom);
  background: var(--surface);
  box-shadow: var(--shadow-nav);
}
.nav a, .nav-run {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-width: 0; height: var(--nav-h);
  color: var(--text-secondary); text-decoration: none;
  /* Grows with the reader's text size but stops at 15px: five slots in a fixed bar cannot grow
     the way content can (native tab bars cap their labels for the same reason). */
  font-size: min(var(--text-2xs), 15px); font-weight: 500; letter-spacing: 0.01em;
  -webkit-tap-highlight-color: transparent;
}
.nav-ind {
  display: grid; place-items: center;
  width: 56px; height: 30px; border-radius: var(--radius-pill);
  transition: background-color var(--dur) var(--ease-out), color var(--dur) ease;
}
.nav a:hover .nav-ind { background: var(--surface-muted); }
.nav a.active { color: var(--text); font-weight: 600; }
.nav a.active .nav-ind { background: var(--yellow); color: var(--on-yellow); }
.nav-label { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The run button: the job the app exists for, one thumb away from every screen. */
.nav-run { border: 0; background: none; padding: 0; cursor: pointer; }
.nav-run-disc {
  display: grid; place-items: center;
  width: 54px; height: 54px; margin-top: calc(var(--nav-raise) * -1);
  border-radius: 50%;
  background: var(--yellow); color: var(--on-yellow);
  box-shadow: 0 0 0 5px var(--surface), var(--shadow-run);
  transition: transform var(--dur) var(--ease-out), background-color var(--dur) ease;
}
.nav-run:hover .nav-run-disc { background: var(--yellow-hover); }
.nav-run:active .nav-run-disc { transform: scale(0.94); }
.nav-run.active { color: var(--text); font-weight: 600; }
.nav-run:focus-visible { box-shadow: none; }
.nav-run:focus-visible .nav-run-disc { box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--focus); }

/* Tablet, and phones held sideways: a rail. A short landscape screen cannot spare 64px of height
   for a bottom bar, and a tablet has width to spare. */
@media (min-width: 48em) and (max-width: 63.9988em), (max-width: 63.9988em) and (orientation: landscape) and (max-height: 32.5em) {
  body { padding-left: var(--rail-w); }
  .nav {
    top: 0; right: auto; bottom: 0; width: var(--rail-w); height: auto;
    display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 4px;
    padding: calc(var(--topbar-h) + 12px) 8px calc(12px + env(safe-area-inset-bottom)) calc(8px + env(safe-area-inset-left));
    box-shadow: 1px 0 0 var(--border);
    overflow-y: auto; scrollbar-width: none;
  }
  .nav a, .nav-run { height: 60px; }
  .nav-run { height: 76px; }
  .nav-run-disc { margin-top: 0; width: 48px; height: 48px; border-radius: var(--radius-lg); box-shadow: var(--shadow-cta); }
}
@media (max-width: 63.9988em) and (orientation: landscape) and (max-height: 32.5em) {
  /* Every pixel of height matters here: the top bar scrolls away, the rail starts at the top, and
     the step footer sits at the end of the page instead of floating over a fifth of the screen. */
  html { scroll-padding: 12px; }
  .topbar { position: relative; }
  #main .wizard-footer { position: static; box-shadow: var(--shadow), var(--card-edge); } /* #main: outranks the base rule further down */
  .nav { padding-top: 10px; gap: 0; z-index: 31; }
  .nav a { height: 52px; }
  .nav-run { height: 66px; }
  .nav-ind { height: 28px; }
}

/* Desktop: the nav takes its place in the top bar, after the brand, as the reference sketch lays it
   out — an ordinary flex item there, in the same position it has in the source. */
@media (min-width: 64em) {
  :root { --topbar-h: 64px; }
  .nav {
    position: static; z-index: auto;
    display: flex; align-items: center; gap: 4px;
    height: auto; margin-left: 20px; padding: 0;
    background: none; box-shadow: none;
  }
  .nav a {
    flex-direction: row; gap: 8px;
    height: 40px; padding: 0 16px 0 12px; border-radius: var(--radius-pill);
    font-size: var(--text-sm); font-weight: 500; letter-spacing: 0;
    transition: background-color var(--dur) ease, color var(--dur) ease;
  }
  .nav a:hover { background: var(--surface-muted); color: var(--text); }
  .nav a:hover .nav-ind, .nav a.active .nav-ind { background: none; }
  .nav-ind { width: auto; height: auto; }
  .nav-ind .icon { width: 18px; height: 18px; }
  .nav a.active { background: var(--yellow); color: var(--on-yellow); }
  .nav-run { display: none; }
  .topbar .run-cta { display: inline-flex; }
}

/* ---- Shell: content ------------------------------------------------------------------------- */

.main {
  max-width: var(--content-max); margin-inline: auto;
  padding: var(--page-gap) var(--gutter) 64px;
}
@media (max-width: 47.9988em) and (orientation: portrait), (max-width: 47.9988em) and (min-height: 32.5012em) {
  /* Clear the bottom bar, the raised run button and a sticky footer. */
  .main { padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 112px); }
}

.page { display: flex; flex-direction: column; gap: var(--page-gap); min-width: 0; }
.page-narrow { width: 100%; max-width: 780px; margin-inline: auto; }
.page-enter { animation: page-in 220ms var(--ease-out) both; }
@keyframes page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.page-header { display: flex; align-items: center; justify-content: space-between; gap: 12px 16px; flex-wrap: wrap; }
.page-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.back-link {
  display: inline-flex; align-items: center; gap: 4px; align-self: flex-start;
  min-height: 36px; padding: 0 12px 0 6px; margin: -6px 0 -8px -6px;
  border-radius: var(--radius-pill);
  color: var(--text-secondary); text-decoration: none; font-size: var(--text-sm); font-weight: 600;
}
.back-link:hover { background: var(--surface-muted); color: var(--text); }
.section-title { font-size: var(--text-lg); }
.group-title {
  font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-secondary); margin: 4px 4px 0;
}
.lead { font-size: var(--text-md); color: var(--text-secondary); max-width: 62ch; margin: 0; }
.hint { font-size: var(--text-xs); color: var(--text-secondary); }

/* ---- Cards, lists, rows --------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow), var(--card-edge);
  padding: clamp(16px, 1.8vw, 24px);
}
.card > h2 + * { margin-top: 12px; }
.card.list, .card.collection { padding: 0; overflow: hidden; }
.section { display: flex; flex-direction: column; gap: 14px; }
.section > p:last-child { margin-bottom: 0; }

.row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px;
  min-height: 60px; padding: 10px 16px;
  border-bottom: 1px solid var(--border);
}
.row:last-child { border-bottom: 0; }
/* The name keeps at least 8rem; when a row's control (Have/Need, the stepper, Staple, Price) no
   longer fits beside it — large text, a narrow phone — the control drops to its own line, right-
   aligned, instead of squeezing the name into one letter per line. */
.row-main { flex: 1 1 8rem; min-width: 0; }
.row > .seg, .row > .stepper, .row > .toggle, .row > .btn { margin-left: auto; }
.row-title { font-weight: 500; font-size: var(--text-md); overflow-wrap: anywhere; line-height: 1.35; }
.row-sub {
  display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center;
  margin-top: 2px; font-size: var(--text-sm); color: var(--text-secondary);
}
.row-link { text-decoration: none; color: inherit; }
a.row.row-link { transition: background-color var(--dur) ease; }
a.row.row-link:hover { background: var(--surface-muted); }
/* Make the whole name block a thumb-sized target rather than just its text. */
a.row-main.row-link { display: flex; flex-direction: column; justify-content: center; min-height: var(--tap); padding: 2px 0; }
a.row-main.row-link:hover .row-title { text-decoration: underline; text-underline-offset: 0.18em; }
.row.selected { background: var(--lime-soft); }
.row-amount { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.row-group { border-bottom: 1px solid var(--border); }
.row-group:last-child { border-bottom: 0; }
.row-group .row { border-bottom: 0; }
.chevron { color: var(--text-muted); display: flex; flex: none; }
.row.muted { color: var(--text-secondary); justify-content: center; }

/* A list inside a card section is not a second box — just rows. */
.section .list { margin-inline: -4px; }
.section .list .row { padding-inline: 4px; }
.section .list .row:first-child { border-top: 1px solid var(--border); }
.section .list .row:last-child { border-bottom: 1px solid var(--border); }

.kv { display: flex; gap: 16px; justify-content: space-between; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border); }
.kv:last-of-type { border-bottom: 0; }
.kv > span:first-child { color: var(--text-secondary); flex-shrink: 0; font-size: var(--text-sm); }
.kv > span:last-child { text-align: right; overflow-wrap: anywhere; font-weight: 500; }
/* .icon is display:block, so an icon inside an inline link would drop to its own line. */
.kv a, .inline-link { display: inline-flex; align-items: center; gap: 4px; }

/* Category groups flow into columns wherever there is room for a comfortable row. Multi-column
   rather than a grid: groups differ wildly in length, and columns pack them without holes while
   still reading top to bottom, the way an aisle list does. */
.groups { columns: 22rem; column-gap: var(--page-gap); }
.groups-wide { columns: 26rem; }
.group {
  display: flex; flex-direction: column; gap: 8px; min-width: 0;
  break-inside: avoid; page-break-inside: avoid;
  margin-bottom: var(--page-gap);
}
.groups > .group:last-child { margin-bottom: 0; }

/* A collection: rows in one card on a phone, a grid of tiles when there is room. */
.collection { background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow), var(--card-edge); overflow: hidden; }
@media (min-width: 40em) {
  .collection {
    display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    background: none; box-shadow: none; overflow: visible; border-radius: 0;
  }
  .collection > .row {
    border: 0; border-radius: var(--radius-lg);
    background: var(--surface); box-shadow: var(--shadow), var(--card-edge);
    padding: 14px 16px; min-height: 76px;
    transition: box-shadow var(--dur) var(--ease-out), transform var(--dur) var(--ease-out), background-color var(--dur) ease;
  }
  .collection > a.row.row-link:hover { background: var(--surface); box-shadow: var(--shadow-raised), var(--card-edge); transform: translateY(-2px); }
  .collection > .row.selected { background: var(--lime-soft); box-shadow: inset 0 0 0 2px var(--lime), var(--shadow); }
  .collection > .row.muted { grid-column: 1 / -1; background: none; box-shadow: none; }
  /* In a tile the ×N stepper gets its own line under the name, rather than squeezing it. */
  .meal-picker > .row { flex-wrap: wrap; align-content: center; row-gap: 10px; }
  .meal-picker > .row > .shop-tap { flex-basis: 100%; }
  .meal-picker > .row > .stepper { margin-left: 96px; }
}
/* A meal's ingredient line can run long; two lines is enough to recognise it. */
.collection > a.row .row-sub { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.section .add-form { padding: 0; }

/* A letter tile gives each meal a face to scan for; a photo, when the meal has one, covers it. */
.monogram {
  display: grid; place-items: center; flex: none;
  width: 44px; height: 44px; border-radius: var(--radius-md);
  font-weight: 700; font-size: var(--text-lg); color: var(--on-yellow);
  background: var(--lime-light);
}
/* A starter meal's photo covers the letter; the letter is what shows if the photo cannot load. */
.monogram { position: relative; overflow: hidden; }
.monogram-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.monogram.m-1 { background: var(--yellow-soft); }
/* The meal editor's photo: the same tile, large, beside its picker and buttons. On a phone the
   picker wraps under it and the photo takes the card's full width. The wrap point is the photo,
   the gap and the body's 220 px basis (280 + 16 + 220), so the query below must track those. */
.meal-photo { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; container: meal-photo / inline-size; }
.meal-photo > .monogram { width: min(100%, 280px); height: auto; aspect-ratio: 1; border-radius: var(--radius-lg); font-size: 6rem; }
@container meal-photo (max-width: 515.98px) { .meal-photo > .monogram { width: 100%; } }
.meal-photo-body { display: flex; flex-direction: column; gap: 10px; flex: 1 1 220px; min-width: 0; }
/* Its photos, to pick the one on show. The picked one is ringed in lime ink, the focused one gets
   the usual focus ring outside that, so the two never share an edge. */
.photo-picks { display: flex; flex-wrap: wrap; gap: 12px; padding: 5px; margin: -5px; }
.photo-pick { display: block; cursor: pointer; border-radius: var(--radius-md); }
.photo-pick .monogram { width: 56px; height: 56px; font-size: var(--text-lg); }
.photo-pick:has(input:checked) .monogram { outline: 3px solid var(--lime-ink); outline-offset: 2px; }
.photo-pick:has(input:focus-visible) .monogram { box-shadow: 0 0 0 6px var(--surface), 0 0 0 8px var(--focus); }
.credits { margin: 0; padding-left: 1.1em; display: grid; gap: 6px; font-size: var(--text-sm); color: var(--text-secondary); overflow-wrap: anywhere; }
.credits strong { color: var(--text); font-weight: 600; }
.monogram.m-2 { background: var(--lime); }
@media (prefers-color-scheme: dark) {
  .monogram { color: var(--text); }
  .monogram.m-2 { color: var(--on-yellow); }
}

/* ---- Chips ---------------------------------------------------------------------------------- */

.chip {
  display: inline-flex; align-items: center; padding: 2px 9px; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 500; line-height: 1.5;
  background: var(--surface-muted); color: var(--text-secondary);
  /* Category and meal names are unbounded user text with no maxlength anywhere in the app. A long
     unbroken one breaks inside the chip rather than pushing the page sideways. */
  max-width: 100%; overflow-wrap: anywhere;
}
.chip-staple { background: var(--warn-soft); color: var(--warn-text); }
.chip-meal, .chip-manual { background: var(--lime-soft); color: var(--lime-ink); }
a.chip-link { text-decoration: none; min-height: 34px; padding-inline: 14px; font-size: var(--text-sm); color: var(--text); background: var(--surface-muted); }
a.chip-link:hover { background: var(--surface-hover); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips-tight { gap: 5px; margin: 3px 0 1px; }

/* A meal's category. Neutral by default: the colour contract reserves lime for "chosen". */
.chip-cat { background: var(--surface-muted); color: var(--text-secondary); }
.chip-removable { padding-right: 3px; gap: 2px; }
.chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; border: 0; border-radius: var(--radius-pill);
  background: none; color: inherit; cursor: pointer;
  position: relative;
}
/* The drawing stays 20px so the chip stays chip-sized; the target is a centred 24px overlay, the
   WCAG 2.5.8 minimum without leaning on its spacing exception. Touch grows it further, below. */
.chip-x::after {
  content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 24px; height: 24px;
}
.chip-x:hover { background: var(--surface-hover); }

/* A category toggle in the roulette. Lime when it is in the draw — "this one is chosen". */
button.chip-toggle {
  gap: 6px; min-height: 32px; padding-inline: 12px; border: 1px solid var(--border);
  /* The category name is the decision being made here, so it takes full ink rather than the
     chip's secondary — this is a control, not a label on something else. */
  font-family: inherit; font-size: var(--text-sm); color: var(--text); cursor: pointer;
}
button.chip-toggle:hover { background: var(--surface-hover); }
button.chip-toggle.active { background: var(--lime-soft); color: var(--lime-ink); border-color: var(--lime-ink); }
/* How many meals are in the category: real information someone reads before choosing, so it is
   ranked below the name by *colour*, never by opacity. Fading --text-secondary to 70% composited
   to 2.65:1 — below the 2.9:1 this stylesheet forbids even for decorative marks. Opacity on text
   silently discards the measured contrast the tokens exist to guarantee. */
.chip-count { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
/* Chosen: the whole chip is lime, so the count stays in lime's family rather than turning grey
   inside it. Weight, not colour, keeps it behind the name here. */
button.chip-toggle.active .chip-count { color: inherit; font-weight: 400; }
.qty-hint { font-variant-numeric: tabular-nums; }
.price-tag { display: inline-flex; align-items: center; gap: 4px; color: var(--lime-ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- Buttons -------------------------------------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 0 20px;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: var(--surface-muted); color: var(--text);
  font-weight: 600; font-size: var(--text-base); line-height: 1.2;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--dur) ease, box-shadow var(--dur) ease, transform 90ms ease;
}
.btn:hover { background: var(--surface-hover); }
.btn:active { transform: scale(0.98); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: default; transform: none; }
.btn-primary { background: var(--yellow); color: var(--on-yellow); box-shadow: var(--shadow-cta); }
.btn-primary:hover { background: var(--yellow-hover); }
.btn-primary:active { background: var(--yellow-press); }
.btn-secondary { background: var(--surface-muted); color: var(--text); }
.btn-ghost { background: transparent; color: var(--text); }
.btn-ghost:hover { background: var(--surface-muted); }
.btn-danger { background: var(--danger-soft); color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger-soft) 80%, var(--danger)); }
.btn-danger-text { color: var(--danger); }
.btn-sm { min-height: 36px; padding: 0 14px; font-size: var(--text-sm); gap: 6px; }
.btn-lg { min-height: 52px; padding: 0 28px; font-size: var(--text-md); }
.btn-icon { min-width: 40px; min-height: 40px; padding: 0; }
.btn-icon.btn-sm { min-width: 36px; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.btn-row-center { justify-content: center; }
.form-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 8px; }
.form-actions .btn-danger { margin-left: auto; }

/* ---- Forms ---------------------------------------------------------------------------------- */

.input {
  width: 100%; min-height: var(--tap); padding: 10px 14px;
  font: inherit; font-size: var(--text-md);   /* 16px keeps iOS from zooming on focus */
  border: 1px solid var(--field-border); border-radius: var(--radius-md);
  background: var(--surface); color: var(--text);
  transition: border-color var(--dur) ease, box-shadow var(--dur) ease;
}
.input:hover { border-color: var(--text-secondary); }
.input:focus-visible, .input:focus { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-halo); }
/* Refused: the border turns Danger and thickens, so the field is identifiable without relying on
   colour alone (WCAG 1.4.1). Focus still wins — once you are in the field fixing it, the yellow
   halo is the more useful signal, and the message underneath keeps saying what is wrong. */
.input[aria-invalid="true"] { border-color: var(--danger); border-width: 2px; padding-block: 9px; }
.input[aria-invalid="true"]:hover { border-color: var(--danger); }
.input::placeholder { color: var(--text-secondary); opacity: 1; }
select.input { width: auto; padding-right: 36px; appearance: none; cursor: pointer;
  background-image: var(--chevron);
  background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; }
textarea.input { resize: vertical; line-height: 1.5; }
.input-sm { min-height: 40px; padding: 6px 10px; }
.input-qty { width: 4.75rem; flex: none; font-variant-numeric: tabular-nums; }
.input-unit { width: 5.25rem; flex: none; }
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field-label { font-size: var(--text-sm); font-weight: 600; color: var(--text); }
/* The message under a refused field. It reserves its line from the start — text that appears and
   pushes the Save button down as you reach for it is how a mis-click happens. The line is only
   rendered for fields that can actually be refused (`field(..., { validate })`), so no form pays
   height for a message it can never show; once present it holds that height whether or not it is
   currently saying anything. */
.field-msg {
  min-height: 1.25em; font-size: var(--text-xs); line-height: 1.25;
  color: var(--danger); font-weight: 500;
}
/* Optional, next to a label, when the form mixes required and optional fields. */
.field-optional { font-weight: 400; color: var(--text-secondary); }
.form { display: flex; flex-direction: column; gap: 16px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 30em) {
  .form-grid { grid-template-columns: 1fr; }
  .input-qty { width: 3.75rem; }
  .input-unit { width: 5rem; }
  .input-sm { padding-inline: 8px; }
  .ing-row, .list-row { gap: 6px; }
}

/* Search: a pill with a drawn magnifier, labelled for screen readers by the input itself. */
.search-field { position: relative; }
.search-field .icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); pointer-events: none; }
.search-field .input { padding-left: 44px; border-radius: var(--radius-pill); }

/* Checkboxes: drawn so they share the rounded language, still native underneath. */
.shop-tap input, .toggle input {
  appearance: none; -webkit-appearance: none; margin: 0; flex: none; cursor: pointer;
  width: 24px; height: 24px; border-radius: var(--radius-sm);
  border: 1.5px solid var(--field-border); background: var(--surface) center / 16px no-repeat;
  transition: background-color var(--dur) ease, border-color var(--dur) ease;
}
.shop-tap input:hover, .toggle input:hover { border-color: var(--text); }
.shop-tap input:checked, .toggle input:checked {
  background-color: var(--lime); border-color: var(--lime-ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23202124' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E");
}
.shop-tap input:focus-visible, .toggle input:focus-visible { border-radius: var(--radius-sm); }

.toggle {
  display: inline-flex; align-items: center; gap: 10px;
  min-height: var(--tap); padding: 0 12px 0 8px; margin-right: -8px;
  border-radius: var(--radius-pill);
  cursor: pointer; font-size: var(--text-sm); font-weight: 500;
}
.toggle:hover { background: var(--surface-muted); }
.toggle-block { justify-content: flex-start; margin: 0 0 0 -8px; font-size: var(--text-base); }

.add-form { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 12px; }
.add-form .input:first-child { flex: 1 1 11rem; }
.ing-list { display: flex; flex-direction: column; gap: 8px; }
.ing-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.ing-row .input:first-child { flex: 1 1 5rem; min-width: 0; }
/* An ingredient is read by its name, and on one line a phone left it six letters ("Spaghe",
   "Fresh pa"). Here the name takes the first line with its × beside it, and the amount drops
   under it. Rows sit further apart than their own two lines, so an amount never reads as
   belonging to the name below it. */
@media (max-width: 30em) {
  .ing-list { gap: 20px; }
  .ing-row {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
    grid-template-areas: "name name remove" "qty unit .";
  }
  .ing-row > .input:first-child { grid-area: name; }
  /* With a half-line each they have room for the name's padding, so all three texts share one edge. */
  .ing-row > .input-qty { grid-area: qty; width: auto; padding-inline: 14px; }
  .ing-row > .input-unit { grid-area: unit; width: auto; padding-inline: 14px; }
  .ing-row > .btn-icon { grid-area: remove; }
}

/* A meal's method: numbered steps, each a textarea that grows with what is written in it. */
.method-list { list-style: none; margin: 0; padding: 0; counter-reset: step; display: flex; flex-direction: column; gap: 8px; }
.method-step { counter-increment: step; display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 8px; align-items: start; }
.method-step::before {
  content: counter(step);
  display: grid; place-items: center; width: 28px; height: 28px; margin-top: 10px; border-radius: 50%;
  background: var(--lime-light); color: var(--text); font-weight: 700; font-size: var(--text-sm); font-variant-numeric: tabular-nums;
}
.method-step .input { min-height: calc(3em + 22px); }
/* Where it cannot grow by itself (Safari, Firefox today), the textarea keeps its resize handle. */
@supports (field-sizing: content) { .method-step .input { field-sizing: content; resize: none; } }
.method-step .btn-icon { margin-top: 2px; }
.list-row .input-qty, .list-row .input-unit { flex: none; }
/* Two inputs and a button share this row, so the name settles for less before the row wraps. */
.list-row .row-main { flex-basis: 4rem; }

/* A chip list above the field that adds to it: meal categories. */
.tag-input { display: flex; flex-direction: column; gap: 8px; }

/* The category filter carried onto the meals step, with the way back to change it. */
.filter-note {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; margin-bottom: 12px;
}
.step-intro { margin: -4px 0 14px; }
.chips-lg { gap: 10px; margin-bottom: 12px; }
.chips-lg .chip-toggle { min-height: 40px; padding-inline: 16px; font-size: var(--text-base); }

/* Starter kitchens. :where() keeps the column at zero specificity so .with-aside's desktop grid
   takes over, as on the wizard's steps. */
:where(.starter-page) { display: flex; flex-direction: column; gap: 16px; }
.starter-page .chips-lg, .starter-page .step-intro { margin-bottom: 0; }
.starter-filters { display: flex; flex-direction: column; gap: 12px; }
.starter-region { display: flex; flex-direction: column; gap: 10px; }
.starter-region-head { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px; }
.starter-region-head > .btn { margin-left: auto; }
.starter-region > .muted { margin: 0; }
/* An ingredient line can run long; two lines is enough to recognise the meal. */
.starter-picker .row-sub { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.starter-picker .row.is-mine .shop-tap { cursor: default; }
.starter-picker .row.is-mine .row-title { color: var(--text-secondary); }

/* How many meals to draw from each chosen category. */
.quota-list { margin-bottom: 12px; }
.quota-row { display: flex; align-items: center; gap: 12px; }
.quota-row .row-main { min-width: 0; }

/* ---- Roulette (random meal picker) ----------------------------------------------------------- */

.roulette { margin-bottom: 14px; }
.roulette-toggle { padding-inline: 14px; }
.roulette-toggle[aria-expanded="true"] { background: var(--surface-hover); }
.roulette-panel { display: flex; flex-direction: column; gap: 14px; margin-top: 10px; }
.roulette-cats { display: flex; flex-direction: column; gap: 8px; }
.roulette-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.roulette-actions .field-label { margin-right: -4px; }
.roulette-hint { font-size: var(--text-sm); }

/* ---- Segmented Have / Need ------------------------------------------------------------------ */

.seg {
  display: inline-flex; gap: 2px; flex-shrink: 0;
  padding: 3px; border-radius: var(--radius-pill);
  background: var(--surface-muted);
}
.seg-btn {
  min-height: 38px; min-width: 60px; padding: 0 14px;
  border: 0; border-radius: var(--radius-pill); background: transparent; color: var(--text-secondary);
  font-weight: 600; font-size: var(--text-sm); cursor: pointer;
  transition: background-color var(--dur) ease, color var(--dur) ease, box-shadow var(--dur) ease;
}
.seg-btn:hover { color: var(--text); }
.seg-btn.active.seg-have { background: var(--lime); color: var(--on-yellow); box-shadow: var(--shadow-sm); }
.seg-btn.active.seg-need { background: var(--yellow); color: var(--on-yellow); box-shadow: var(--shadow-sm); }
.stock-row.is-have .row-title { color: var(--text-secondary); }

/* ---- Stepper (×N) --------------------------------------------------------------------------- */

.stepper { display: inline-flex; align-items: center; flex: none; padding: 3px; border-radius: var(--radius-pill); background: var(--surface); box-shadow: inset 0 0 0 1px var(--field-border); }
.stepper-btn {
  display: grid; place-items: center;
  width: 34px; height: 34px; border: 0; border-radius: 50%;
  background: transparent; color: var(--text); cursor: pointer;
}
.stepper-btn:hover:not(:disabled) { background: var(--surface-muted); }
.stepper-btn:disabled { opacity: 0.35; cursor: default; }
.stepper-val { min-width: 2.4rem; text-align: center; font-weight: 700; font-size: var(--text-sm); font-variant-numeric: tabular-nums; }

/* ---- Wizard step rail ----------------------------------------------------------------------- */

/* The rail scrolls at any width rather than overflowing the page — five named steps are wider
   than a phone. The block padding keeps a focus ring from being clipped by the scroll box. */
.steps {
  display: flex; gap: 4px;
  padding: 5px; border-radius: var(--radius-pill);
  background: var(--surface); box-shadow: var(--shadow), var(--card-edge);
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none; -ms-overflow-style: none;
  scroll-snap-type: x proximity;
}
.steps::-webkit-scrollbar { display: none; }
.step {
  /* Grow to fill a rail that has room; never shrink so far that the name is squeezed out. */
  flex: 1 0 auto; min-width: max-content;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 42px; padding: 0 16px 0 8px; border-radius: var(--radius-pill);
  color: var(--text-secondary); text-decoration: none; font-size: var(--text-sm); font-weight: 600;
  scroll-snap-align: center;
  transition: background-color var(--dur) ease, color var(--dur) ease;
}
.step:hover { background: var(--surface-muted); color: var(--text); }
.step.active { color: var(--on-yellow); background: var(--yellow); }
.step.done { color: var(--text); }
.step-num {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.step.active .step-num { background: var(--on-yellow); color: var(--yellow); }
.step.done .step-num { background: var(--lime); color: var(--on-yellow); }
@media (max-width: 35em) {
  .steps { margin-inline: calc(var(--gutter) * -1); border-radius: 0; box-shadow: 0 1px 0 var(--border), 0 -1px 0 var(--border); padding-inline: var(--gutter); }
}
/* With room to spare, the rail is as wide as its steps, not a stretched bar. */
@media (min-width: 48em) {
  .run-wizard .steps { align-self: flex-start; max-width: 100%; }
  .run-wizard .step { flex: 0 0 auto; padding-inline: 8px 20px; }
}

/* :where() keeps this at element specificity so .with-aside's desktop grid can take over. */
:where(.wizard, .setup) > section { display: flex; flex-direction: column; gap: 16px; }
.wizard-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

/* The step's next action. A floating bar on phones and tablets, a sticky side card on desktop. */
.wizard-footer {
  position: sticky; bottom: 12px; z-index: 10;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px;
  margin-top: 4px; padding: 10px 10px 10px 16px;
  background: var(--surface); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), var(--card-edge);
}
.footer-count { color: var(--text-secondary); font-size: var(--text-sm); }
.footer-count strong { color: var(--text); font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums; margin-right: 2px; }
@media (max-width: 47.9988em) and (orientation: portrait), (max-width: 47.9988em) and (min-height: 32.5012em) {
  .wizard-footer { bottom: calc(var(--nav-h) + var(--nav-raise) + env(safe-area-inset-bottom) + 8px); }
}
/* A count beside two buttons does not fit a phone; those steps show the count on the page too. */
@media (max-width: 37.4988em) {
  .wizard-footer:has(> :nth-child(3)) > .footer-count { display: none; }
}
/* When the buttons outgrow one line (Back / Skip / Next on a phone, or any footer at large
   text) they wrap, and the primary action keeps its place on the right. */
.wizard-footer > :last-child { margin-left: auto; }
/* A button alone on its line that is still wider than the footer (very large text on a small
   phone) wraps its label rather than spilling out. At normal sizes nothing here ever wraps. */
.wizard-footer .btn { max-width: 100%; white-space: normal; text-align: center; }

@media (min-width: 64em) {
  .with-aside { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; gap: 0 32px; align-items: start; }
  .with-aside > .wizard-footer {
    position: sticky; top: calc(var(--topbar-h) + 24px); bottom: auto;
    flex-direction: column; align-items: stretch; gap: 10px;
    margin: 0; padding: 20px;
    box-shadow: var(--shadow), var(--card-edge);
  }
  .with-aside > .wizard-footer .btn { width: 100%; }
  .with-aside > .wizard-footer > :last-child { margin-left: 0; }
  .with-aside > .wizard-footer .footer-count { padding: 0 4px 6px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
}

/* ---- Setup wizard --------------------------------------------------------------------------
 *
 * The scene here is the opposite of the supermarket aisle: someone sitting down, two tabs open,
 * copying values between Google's console and this app. So the design optimises for carrying a
 * value across without transcription errors — every such value is a copy button — and for
 * saying which of the three near-identical Google strings is the one being asked for.
 */
.setup { width: 100%; max-width: 840px; margin-inline: auto; }
.setup .steps { flex-wrap: nowrap; }
.setup .step-label { white-space: nowrap; }
.setup h2 { margin-bottom: 2px; }
.setup .card > * + h2 { margin-top: 20px; }
.setup .card > p:last-child { margin-bottom: 0; }
.setup .card { display: flex; flex-direction: column; gap: 12px; }
.setup .card > p { margin: 0; }

/* Credentials are data, so they are set in the data face — and never wrap mid-value. */
.mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 0.9em; }

/* A value to carry to Google. Dashed, tinted and read-only so it reads as something to take
   rather than something to fill in. */
.copy-box { display: flex; gap: 8px; align-items: stretch; }
.copy-value { flex: 1; min-width: 0; background: var(--surface-muted); border-style: dashed; cursor: text; }
.copy-btn { flex: none; }
.copy-btn.is-copied { background: var(--lime); color: var(--on-yellow); }
@media (max-width: 30em) {
  .copy-box { flex-wrap: wrap; }
  .copy-value { flex: 1 1 100%; }
  .copy-btn { flex: 1; }
}

/* Something is wrong and there is exactly one thing to do about it. */
.setup-alert {
  display: flex; flex-direction: column; gap: 8px;
  padding: 16px 18px; border-radius: var(--radius-lg);
  background: var(--warn-soft); box-shadow: inset 0 0 0 1px var(--warn-line);
}
.setup-alert h2 { color: var(--warn-text); font-size: var(--text-md); }
.setup-alert p { margin: 0; }

/* The invitation on Settings, before any of this has been done. */
.setup-cta { display: flex; gap: 20px; align-items: flex-start; }
.setup-cta-icon { display: grid; place-items: center; flex: none; width: 64px; height: 64px; border-radius: var(--radius-lg); background: var(--lime-light); color: var(--lime-ink); }
.setup-cta-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.setup-cta-body p { margin: 0; max-width: 58ch; }
/* The "joining someone else's list?" route. Set apart by a rule rather than a second card: it is
   an alternative to the button above it, not a competing call to action. */
.setup-cta-alt {
  margin: 2px 0 0; padding-top: 12px; border-top: 1px solid var(--border);
  font-size: var(--text-sm); color: var(--text-secondary); max-width: 58ch;
}
.setup-cta-alt strong { color: var(--text); }
@media (max-width: 32.5em) { .setup-cta { gap: 14px; flex-direction: column; } .setup-cta-icon { width: 52px; height: 52px; border-radius: var(--radius-lg); } }

/* The one authored moment in the setup flow: the mark draws itself once, when it is finally true. */
.setup-done {
  text-align: center; display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 24px 16px 8px;
  animation: setup-rise 0.5s var(--ease-out) both;
}
.setup-done p { max-width: 46ch; color: var(--text-secondary); margin: 0; }
.setup-done-mark {
  display: grid; place-items: center;
  width: 80px; height: 80px; border-radius: 50%;
  background: var(--lime); color: var(--on-yellow);
  box-shadow: 0 0 0 10px var(--lime-soft);
  animation: setup-pop 0.45s var(--ease-out) both;
}
.setup-done-mark .icon path { stroke-dasharray: 24; stroke-dashoffset: 24; animation: draw 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.18s forwards; }
@keyframes setup-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes setup-pop { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: none; } }
@keyframes draw { to { stroke-dashoffset: 0; } }

/* Google's G keeps its own four brand colours — it is the one mark here the app does not own, and
   recolouring it is against Google's guidelines. That makes the surface behind it the problem:
   --yellow (#f9e000) is a near-match for Google's yellow (#FBBC05), so on a primary button that
   quarter of the G would disappear. A white chip gives it the neutral ground it is designed for,
   in both themes, and reads as the familiar Google button. */
.icon-google { border-radius: 50%; background: var(--google-chip); padding: 2px; box-sizing: content-box; }
.btn .icon-google { margin-left: -4px; }
/* In the sync pill the G stands in for the 8px status dot, so it wears a tighter chip than on a
   button. The chip still matters here: --warn-soft (#fff6bf) is the pill's ground in this state
   and would swallow the G's yellow segment just as the primary button's fill does. */
.pill .icon-google { padding: 1.5px; }

/* ---- Sharing the Google setup ---------------------------------------------------------------- */

/* Export lives in Settings, import at the top of the wizard — see js/share.js for why the
   passphrase is convenience, not secrecy. */
.share-setup { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.share-setup .input, .setup-import .input { max-width: 18rem; }
.setup-import { display: flex; flex-direction: column; gap: 12px; }
.setup-import h2 { margin: 0; font-size: var(--text-lg); }
.setup-import .mono { font-size: var(--text-sm); overflow-wrap: anywhere; }

/* "or set up your own project" between the import shortcut and the wizard proper. */
.or-divider {
  display: flex; align-items: center; gap: 12px;
  margin: 20px 0 4px; color: var(--text-secondary); font-size: var(--text-sm);
}
.or-divider::before, .or-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

/* ---- Banners -------------------------------------------------------------------------------- */

.banner {
  display: flex; align-items: flex-start; gap: 12px;
  margin: 0; padding: 14px 16px; border-radius: var(--radius-md);
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--border);
  font-size: var(--text-sm);
}
.banner p { margin: 0; }
.banner-accent { background: var(--yellow-soft); box-shadow: none; }
.banner-icon { display: grid; place-items: center; flex: none; width: 32px; height: 32px; margin: -4px 0; border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.banner-accent .banner-icon { background: var(--yellow); color: var(--on-yellow); }
.banner-accent a { color: var(--text); font-weight: 600; }
.banner p { flex: 1; min-width: 0; align-self: center; }
.banner a { font-weight: 600; }
.banner-dismiss { flex: none; margin: -8px -8px -8px 0; }

/* ---- Home ----------------------------------------------------------------------------------- */

/* The hero: white where the words and the button are, a flat lime field where the picture is.
   Yellow never sits on lime — 1.03:1 — so the button always stands on white. */
.hero {
  display: grid; grid-template-columns: 1fr; padding: 0; overflow: hidden;
  border-radius: var(--radius-xl);
}
.hero-art {
  display: grid; place-items: center;
  min-height: 168px; padding: 16px;
  background: var(--art-panel);
}
.hero-art svg { width: min(100%, 220px); height: auto; }
.hero-body { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; padding: 22px 20px 24px; }
.hero-title { font-size: var(--text-xl); font-weight: 700; letter-spacing: -0.02em; }
.hero-sub { margin: -6px 0 0; color: var(--text-secondary); }
.hero .btn-row { margin-top: 4px; }
@media (min-width: 40em) {
  .hero { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .hero-art { order: 2; min-height: 260px; }
  .hero-art svg { width: min(100%, 280px); }
  .hero-body { justify-content: center; padding: clamp(24px, 3.5vw, 48px); gap: 16px; }
}
@media (max-width: 63.9988em) and (orientation: landscape) and (max-height: 32.5em) {
  .hero-art { min-height: 0; }
  .hero-art svg { width: 180px; }
}

/* The Meals tab's starter invitation: the hero's shape, a little shorter, with three kitchens. */
.hero-invite { margin-top: 8px; }
.invite-kitchens { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.invite-kitchens li { display: flex; flex-direction: column; }
.invite-kitchens strong { font-weight: 600; }
.invite-kitchens span { color: var(--text-secondary); font-size: var(--text-sm); }
@media (min-width: 40em) {
  .hero-invite .hero-art { min-height: 220px; }
}

/* The bag illustration (js/ui/illustrations.js). Flat fills, one round ink line. */
.art .art-line { stroke: var(--art-ink); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.art path[class="art-line"] { fill: none; }   /* a bare line: veins, crumb scores, sparks */
.art .art-disc { fill: var(--art-disc); }
.art .art-shadow { fill: var(--art-ink); opacity: 0.1; }
.art .art-paper { fill: var(--art-paper); }
.art .art-yellow { fill: var(--yellow); }
.art .art-yellow-deep { fill: var(--art-yellow-deep); }
.art .art-lime { fill: var(--lime); }
.art .art-lime-light { fill: var(--art-leaf); }
.art .art-apple { fill: var(--art-apple); }
.art .art-ink-fill { fill: var(--art-ink); }
.art .art-mark { fill: none; stroke: var(--art-ink); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* Where the run is up to. Colour repeats the words; it never replaces them. */
.stage-track { list-style: none; margin: 2px 0 0; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; width: 100%; max-width: 30rem; }
.stage { display: flex; flex-direction: column; gap: 6px; font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); }
.stage-bar { height: 6px; border-radius: var(--radius-pill); background: var(--surface-muted); }
.stage.is-done .stage-bar { background: var(--lime); }
.stage.is-done { color: var(--text); }
.stage.is-current .stage-bar { background: var(--yellow); box-shadow: inset 0 0 0 1px var(--yellow-press); }
.stage.is-current { color: var(--text); }

/* Counts are the interface: the numbers a run is made of, where the eye lands. */
.counts { display: flex; flex-wrap: wrap; gap: 8px 28px; }
.count { display: flex; align-items: baseline; gap: 6px; }
.count-num { font-size: var(--text-numeral); font-weight: 700; letter-spacing: -0.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.count-label { color: var(--text-secondary); font-size: var(--text-sm); }

.home-grid { display: grid; gap: var(--page-gap); grid-template-columns: minmax(0, 1fr); grid-template-areas: "aside" "history"; align-items: start; }
.home-aside { grid-area: aside; display: flex; flex-direction: column; gap: var(--page-gap); min-width: 0; }
.home-history { grid-area: history; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.home-aside .library { display: none; }
@media (min-width: 64em) {
  .home-grid { grid-template-columns: minmax(0, 1fr) minmax(18rem, 24rem); grid-template-areas: "history aside"; }
  .home-aside .library { display: flex; }
}

.date-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; flex: none;
  width: 48px; height: 52px; border-radius: var(--radius-md);
  background: var(--surface-muted); line-height: 1.05;
}
.date-tile-month { font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-secondary); }
.date-tile-day { font-size: 1.125rem; font-weight: 700; font-variant-numeric: tabular-nums; }

.lib-row { gap: 14px; }
.lib-icon { display: grid; place-items: center; flex: none; width: 40px; height: 40px; border-radius: var(--radius-md); background: var(--lime-soft); color: var(--lime-ink); }
.lib-count { font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums; }

.start-steps { list-style: none; margin: 0; padding: 0; counter-reset: start; display: flex; flex-direction: column; gap: 14px; }
.start-steps li { counter-increment: start; display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: 12px; align-items: start; }
.start-steps li::before {
  content: counter(start);
  display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%;
  background: var(--lime-light); color: var(--text); font-weight: 700; font-size: var(--text-sm);
}
.start-steps li > span { padding-top: 5px; }

/* ---- Shopping ------------------------------------------------------------------------------- */

/* The tap target is the whole name block, not the checkbox: one hand, trolley moving. */
.shop-tap {
  display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0;
  min-height: 52px; margin: -10px 0 -10px -16px; padding: 10px 0 10px 16px;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.shop-row { transition: background-color 240ms ease; }
.shop-row.checked { background: var(--lime-soft); }
.shop-row.checked .row-title { text-decoration: line-through; text-decoration-thickness: 1.5px; color: var(--text-secondary); }
.shop-row .row-sub { font-variant-numeric: tabular-nums; }
/* The memorable moment: the tick lands with a small pop on the item you just checked. */
.shop-row.is-just-checked input { animation: tick-pop 320ms var(--ease-out); }
.shop-row.is-just-checked { animation: row-settle 520ms var(--ease-out); }
@keyframes tick-pop { 0% { transform: scale(0.7); } 55% { transform: scale(1.14); } 100% { transform: none; } }
@keyframes row-settle { from { background-color: var(--lime-light); } to { background-color: var(--lime-soft); } }

.shop-footer .shop-spent { display: none; }
.shop-progress { flex: 1 1 4rem; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 2px 10px; min-width: 0; }
.shop-count { font-size: var(--text-lg); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1; white-space: nowrap; }
.shop-count span { font-size: var(--text-sm); color: var(--text-secondary); font-weight: 600; letter-spacing: 0; }
.shop-count-label { grid-column: 1 / -1; font-size: var(--text-xs); color: var(--text-secondary); }
.meter { height: 8px; border-radius: var(--radius-pill); background: var(--surface-muted); overflow: hidden; min-width: 40px; }
.meter > span { display: block; height: 100%; border-radius: inherit; background: var(--lime); }
@media (max-width: 23.75em) { .shop-count-label { display: none; } }
@media (min-width: 64em) {
  .shop-footer .shop-progress { flex: none; order: -1; gap: 10px 12px; padding: 4px 4px 14px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
  .shop-footer .shop-count { font-size: var(--text-numeral); }
  .shop-footer .shop-count-label { grid-column: auto; font-size: var(--text-sm); }
  .shop-footer .meter { grid-column: 1 / -1; order: 3; }
  .shop-footer .shop-spent { display: block; order: 0; padding: 0 4px 4px; font-size: var(--text-sm); color: var(--text-secondary); }
  .shop-footer .shop-spent strong { color: var(--text); }
  .shop-footer .btn-primary { order: 1; }
  .shop-footer .btn-ghost { order: 2; }
}
.total-line { display: flex; align-items: center; gap: 8px; font-weight: 600; font-variant-numeric: tabular-nums; }
.total-line .price-tag { font-size: var(--text-md); }

/* ---- Price form ----------------------------------------------------------------------------- */

.price-form { padding: 14px 16px 16px; background: var(--surface-muted); display: flex; flex-direction: column; gap: 12px; }
.price-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: 10px; }
.details summary {
  display: inline-flex; align-items: center; gap: 6px; min-height: var(--tap);
  cursor: pointer; color: var(--text); font-weight: 600; list-style: none;
}
.details summary::-webkit-details-marker { display: none; }
.details summary::before {
  content: ""; width: 18px; height: 18px; flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M12 5.5v13M5.5 12h13'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M12 5.5v13M5.5 12h13'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform var(--dur) var(--ease-out);
}
.details[open] summary::before { transform: rotate(45deg); }
.details .price-form { border-radius: var(--radius-md); margin-top: 6px; }

/* ---- Stats, tables -------------------------------------------------------------------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 10px; }
.stat { background: var(--surface-muted); border-radius: var(--radius-md); padding: 12px 14px; }
.stat-label { font-size: var(--text-xs); color: var(--text-secondary); font-weight: 600; }
/* An unusual quantity can produce a very long per-unit price; let it wrap rather than clip. */
.stat-value { font-weight: 700; font-size: var(--text-lg); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; letter-spacing: -0.01em; margin-top: 2px; }
.stat-sub { font-size: var(--text-xs); color: var(--text-secondary); }

.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.table th, .table td { text-align: left; padding: 10px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table th { font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary); }
.table th:first-child, .table td:first-child { padding-left: 0; }
.table th:last-child, .table td:last-child { padding-right: 0; }
.table .num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.table .nowrap { white-space: nowrap; }
.table tr.muted td { color: var(--text-secondary); }
.table tbody tr:last-child td { border-bottom: 0; }
.cell-check { width: 28px; color: var(--lime-ink); }

/* Small screens: each row becomes a labelled block rather than a squeezed table. */
@media (max-width: 39.9988em) {
  .table-cards thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .table-cards, .table-cards tbody { display: block; }
  /* Name and price on the first line; the rest stacks under the name. Dense flow backfills the
     price column so the layout holds whichever cells a table has. */
  .table-cards tr {
    position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; grid-auto-flow: row dense;
    gap: 2px 12px; padding: 12px 0; border-bottom: 1px solid var(--border);
  }
  .table-cards tbody tr:last-child { border-bottom: 0; }
  .table-cards td { display: block; grid-column: 1; padding: 0; border: 0; text-align: left; font-size: var(--text-sm); color: var(--text-secondary); }
  .table-cards td.num { grid-column: 2; text-align: right; color: var(--text); font-weight: 600; }
  .table-cards td.num.muted { color: var(--text-secondary); font-weight: 400; }
  .table-cards td.cell-lead { grid-row: 1; color: var(--text); font-weight: 600; font-size: var(--text-base); }
  .table-cards tr.muted td { color: var(--text-secondary); }
  .table-cards td[data-label]:not(:empty)::before { content: attr(data-label) " "; }
  .table-cards td:empty { display: none; }
  .table-cards tr:has(.cell-check) { padding-left: 28px; }
  /* grid-column: auto, so the tick is placed against the row's edge, not inside column 1. */
  .table-cards .cell-check { position: absolute; left: 0; top: 15px; width: auto; grid-column: auto; grid-row: auto; }
  .table-cards .cell-action { grid-column: 3; grid-row: 1 / span 2; align-self: center; }
}

/* ---- Layout helpers per view ---------------------------------------------------------------- */

.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.tools { display: grid; gap: 12px; grid-template-columns: minmax(0, 1fr); align-items: center; }
@media (min-width: 56.25em) { .tools { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); } }

.detail-grid, .summary-grid { display: grid; gap: var(--page-gap); grid-template-columns: minmax(0, 1fr); align-items: start; }
.detail-col { display: flex; flex-direction: column; gap: var(--page-gap); min-width: 0; }
@media (min-width: 64em) {
  .detail-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .summary-grid { grid-template-columns: minmax(18rem, 1fr) minmax(0, 2fr); }
}

.settings-grid { column-gap: var(--page-gap); }
.settings-grid > * { break-inside: avoid; margin-bottom: var(--page-gap); }
@media (min-width: 68.75em) { .settings-grid { columns: 2; max-width: 1240px; } }

/* ---- Empty, error, loading ------------------------------------------------------------------ */

.empty { text-align: center; padding: 36px 16px; display: flex; flex-direction: column; align-items: center; gap: 8px; color: var(--text-secondary); }
.empty-icon { display: grid; place-items: center; width: 84px; height: 84px; margin-bottom: 8px; border-radius: 50%; background: var(--lime-light); color: var(--lime-ink); }
.empty-icon .icon { width: 38px; height: 38px; }
.empty-art svg { width: 180px; height: auto; margin-bottom: 4px; }
.empty-title { color: var(--text); font-size: var(--text-lg); font-weight: 600; letter-spacing: -0.01em; }
.empty p { max-width: 32rem; margin: 0; }
.empty .btn, .empty .btn-row { margin-top: 10px; }
.empty-error .empty-icon { background: var(--warn-soft); color: var(--warn-text); }
.error-details { max-width: 32rem; margin: 0 auto; color: var(--text-secondary); font-size: var(--text-sm); }
.error-details summary { cursor: pointer; min-height: var(--tap); display: flex; align-items: center; justify-content: center; }
.error-details pre { white-space: pre-wrap; background: var(--surface-muted); padding: 12px; border-radius: var(--radius-md); margin: 0; }

.skeleton-list { display: flex; flex-direction: column; gap: 10px; }

/* The boot frame in index.html: the home page's shape in skeleton, shown until the first render
   replaces it. It is static HTML, so it paints with the stylesheet, seconds before the scripts. */
.boot .boot-title { display: block; width: min(60%, 14rem); height: 30px; }   /* .boot: outranks the generic skeleton sizes below */
.boot-hero { display: grid; grid-template-columns: 1fr; padding: 0; overflow: hidden; border-radius: var(--radius-xl); }
.boot-hero-body { display: flex; flex-direction: column; gap: 14px; padding: 24px 20px; }
.boot .boot-hero-body .skeleton-line:first-child { width: 55%; height: 18px; }
.boot .boot-hero-body .skeleton-line:nth-child(2) { width: 80%; }
.boot .boot-hero-body .boot-button { width: 11rem; height: 44px; }
.boot-hero-art { order: -1; min-height: 168px; background: var(--art-panel); }
.boot-list { display: flex; flex-direction: column; gap: 18px; }
@media (min-width: 40em) {
  .boot-hero { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .boot-hero-body { justify-content: center; padding: clamp(24px, 3.5vw, 48px); }
  .boot-hero-art { order: 2; min-height: 260px; }
}
.skeleton-row { display: flex; align-items: center; gap: 12px; }
.skeleton-block, .skeleton-line {
  background: linear-gradient(90deg, var(--surface-muted) 0%, var(--surface-hover) 50%, var(--surface-muted) 100%) 0 0 / 200% 100%;
  animation: shimmer 1.3s linear infinite;
}
.skeleton-block { width: 40px; height: 40px; border-radius: var(--radius-md); flex: none; }
.skeleton-lines { display: flex; flex-direction: column; gap: 8px; flex: 1; }
.skeleton-line { height: 10px; border-radius: var(--radius-pill); }
.skeleton-line:last-child { width: 60%; }
@keyframes shimmer { to { background-position: -200% 0; } }

/* ---- Modal, toasts -------------------------------------------------------------------------- */

dialog.modal {
  border: 0; border-radius: var(--radius-xl); padding: 0;
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow-lg), var(--card-edge);
  width: min(calc(100% - 32px), 30rem); max-width: none; max-height: min(85vh, 40rem);
}
dialog.modal[open] { animation: modal-in 200ms var(--ease-out); }
dialog.modal::backdrop { background: var(--scrim); animation: fade-in 200ms ease; }
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes fade-in { from { opacity: 0; } }
.modal-body { padding: 24px; display: flex; flex-direction: column; gap: 14px; }
.modal-body p { margin: 0; color: var(--text-secondary); }
.modal-title { font-size: var(--text-lg); font-weight: 700; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
@media (max-width: 26.25em) {
  .modal-actions { flex-direction: column-reverse; }
  .modal-actions .btn { width: 100%; }
}

.toasts {
  position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom)); transform: translateX(-50%);
  z-index: 50; display: flex; flex-direction: column; align-items: center; gap: 8px; pointer-events: none;
  width: max-content; max-width: calc(100vw - 32px);
}
@media (max-width: 47.9988em) and (orientation: portrait), (max-width: 47.9988em) and (min-height: 32.5012em) {
  .toasts { bottom: calc(var(--nav-h) + var(--nav-raise) + env(safe-area-inset-bottom) + 16px); }
}
.toast {
  background: var(--text); color: var(--bg);
  padding: 12px 20px; border-radius: var(--radius-pill); box-shadow: var(--shadow-lg);
  font-size: var(--text-sm); font-weight: 600; text-align: center;
  opacity: 0; transform: translateY(8px); transition: opacity 200ms ease, transform 200ms var(--ease-out);
}
.toast.show { opacity: 1; transform: none; }
.toast-error { background: var(--danger); color: var(--on-danger); }

/* ---- Pointer and motion preferences --------------------------------------------------------- */

/* Touch pointers get the full 44px comfort target; mouse pointers keep a denser rhythm. */
@media (pointer: coarse) {
  .btn-sm { min-height: var(--tap); padding-inline: 16px; }
  .btn-icon, .btn-icon.btn-sm { min-width: var(--tap); min-height: var(--tap); }
  .stepper-btn { width: var(--tap); height: var(--tap); }
  .seg-btn { min-height: var(--tap); }
  .input-sm { min-height: var(--tap); }
  .pill, .back-link { min-height: var(--tap); }
  .step, a.chip-link { min-height: var(--tap); }
  /* A toggle chip is a control, so it gets the full target. Chips sit 8–10px apart, so 44px stays
     inside each chip's own box and steals no neighbour's taps. `.chips-lg` is named so this
     outranks its desktop 40px. */
  button.chip-toggle, .chips-lg .chip-toggle { min-height: var(--tap); }
  /* The × keeps its 28px drawing so the chip stays chip-sized, and grows its *target* through an
     overlay centred on it. The overlay stops short of 44px on both axes on purpose: chips sit 8px
     apart in a wrapping row, so a 44px box would reach past the gap and swallow taps meant for the
     neighbouring chip — or for one in the row above. 36 × 40 is the largest box that stays inside
     this chip's own half of the gap, and it clears the 24px WCAG 2.5.8 floor with room to spare. */
  .chip-x { width: 28px; height: 28px; }
  .chip-x::after { width: 36px; height: 40px; }
}

/* Reduced motion is a designed alternative, not a kill switch. Every state change stays legible —
   colour, the strike-through, the lime row fill — and movement becomes a short fade in place:
   pages, dialogs and the setup mark still *arrive*, they just don't travel or scale. Nothing
   loops: the shimmer and the syncing dot hold still, and the words say what is happening. */
@media (prefers-reduced-motion: reduce) {
  .page-enter, .setup-done, .setup-done-mark, dialog.modal[open] { animation: fade-in 160ms ease both; }
  .setup-done-mark .icon path { stroke-dashoffset: 0; animation: none; }
  .shop-row.is-just-checked input { animation: none; }                 /* the fill still turns lime */
  .shop-row.is-just-checked { animation: row-settle 520ms ease; }       /* a colour fade, no movement */
  .btn:active, .nav-run:active .nav-run-disc { transform: none; }
  .collection > a.row.row-link:hover { transform: none; }
  .toast { transform: none; }
  .skeleton-block, .skeleton-line { animation: none; }
  .pill-busy .pill-dot { animation: none; }
  .details summary::before { transition: none; }
  html { scroll-behavior: auto; }
}

/* ---- Forced colours (Windows High Contrast) -------------------------------------------------
 * The OS replaces every author background and border with its own palette, which is exactly the
 * pairing this design leans on: yellow = still to do, lime = done. Both collapse to the same
 * system Canvas, so anything that says its state *only* by fill goes mute. The rule here is the
 * same one the stage track already follows — colour repeats the words, it never replaces them —
 * applied to the controls that had no words to fall back on.
 *
 * `forced-color-adjust: none` appears once, on the brand art: the logo tile and the illustration
 * are pictures, not UI, and flattening them to two system colours turns the cart into a blob.
 * Everywhere else the system palette is kept and given *shape* to carry the meaning instead. */
@media (forced-colors: active) {

  /* The aisle scene, and the one that matters most. The checked fill is gone, so the tick itself
     has to be the signal: the drawn check mark is a background-image (kept in forced colours,
     unlike background-color), and the box it sits in goes to Highlight with a matching border. */
  .shop-tap input, .toggle input { border-color: CanvasText; }
  .shop-tap input:checked, .toggle input:checked {
    background-color: Highlight;
    border-color: Highlight;
    forced-color-adjust: none;   /* keeps the ink check legible on the Highlight fill */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E");
  }
  /* The row's lime wash is gone too. The strike-through survives on its own (it is a text
     decoration, not a fill) and is what a checked row now reads by; GrayText dims it to match. */
  .shop-row.checked .row-title { color: GrayText; }

  /* Progress: the track keeps an outline so an empty meter is still visibly a meter, and the fill
     becomes Highlight. Without the border a 0%-full meter would vanish entirely. */
  .meter { border: 1px solid CanvasText; }
  .meter > span { background: Highlight; forced-color-adjust: none; }

  /* Selected / active / done states that were a fill only. A 2px inset ring is the substitute:
     it survives the palette swap and does not move anything. */
  .row.selected,
  .collection > .row.selected { box-shadow: inset 0 0 0 2px Highlight; }
  button.chip-toggle.active,
  .seg-btn.active.seg-have, .seg-btn.active.seg-need,
  .step.active, .step.done,
  .nav a.active .nav-ind, .nav-run-disc,
  .copy-btn.is-copied { box-shadow: inset 0 0 0 2px Highlight; }
  /* The meal's picked photo is already a ring; it only needs the system's colour for "selected". */
  .photo-pick:has(input:checked) .monogram { outline-color: Highlight; }
  /* Have/Need and the step rail carry their own words, so the ring is enough to say which is on;
     the numbered discs, which do not, keep a boundary of their own. */
  .step-num { box-shadow: inset 0 0 0 1px CanvasText; }

  /* The run stage track: `is-done` and `is-current` differed by bar colour alone. Highlight fills
     the reached stages and an outline holds the empty ones, so the sequence still reads. */
  .stage-bar { border: 1px solid CanvasText; }
  .stage.is-done .stage-bar, .stage.is-current .stage-bar { background: Highlight; forced-color-adjust: none; }

  /* Sync pills drop their border to transparent when they are carrying a state colour, which in
     forced colours leaves them with no edge at all. Give it back — the dot alone is too small to
     locate, and the pill is a button. */
  .pill-ok, .pill-warn, .pill-bad { border-color: CanvasText; }

  /* Buttons all resolve to the same ButtonFace here, so "one primary action per region" — the
     rule the whole layout leans on — would be lost. Highlight/HighlightText is the system's own
     emphasis pair, and is the correct way to say "this is the action" without a brand colour.
     The others keep a plain border so they still read as buttons, ranked below it. */
  .btn { border: 1px solid ButtonText; }
  .btn-primary {
    background: Highlight; color: HighlightText; border-color: Highlight;
    forced-color-adjust: none;
  }
  .btn-danger { border-color: LinkText; color: LinkText; }

  /* Cards and the phone bottom bar are shadow-defined; shadows do not render here. */
  .card, .collection, .seg, .steps, .stepper, dialog.modal { border: 1px solid CanvasText; }
  .nav { border-top: 1px solid CanvasText; }

  /* Brand art is a picture, not a control: keep its own colours. */
  .brand-mark, .brand-tile, .art { forced-color-adjust: none; }

  /* The focus ring is drawn with box-shadow, which is dropped here; the system outline replaces it
     and must not be suppressed. */
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible { outline: 2px solid CanvasText; outline-offset: 2px; }
  /* Text fields signal focus with an Ink border and a yellow halo, and this mode erases both: the
     border is repainted in the system colour and the halo is a shadow. `.input:focus` sets
     `outline: none` at higher specificity than the rule above, so it has to be restated here. */
  .input:focus, .input:focus-visible { outline: 2px solid CanvasText; outline-offset: 1px; }
  /* The photo choices are visually hidden radios whose focus ring is a shadow on the thumbnail.
     The thumbnail's own outline already says "picked", so focus rings the label around it. */
  .photo-pick:has(input:focus-visible) { outline: 2px solid CanvasText; outline-offset: 7px; }
}
