/* Snerf — one mobile-first stylesheet.
 *
 * Ordering is handled by cascade layers rather than by source position, so a
 * later rule can no longer quietly win over an earlier one just by being
 * further down the file. That was the previous failure mode here: the sheet had
 * grown a second layout pass appended to the bottom which re-declared .shell,
 * .content, .stat-grid and .bottom-nav, and two `@media (max-width: 760px)`
 * blocks where the second undid the first.
 *
 * Two rules keep it that way:
 *   - no !important outside [hidden], because it inverts layer order
 *   - no colour literal outside the tokens layer
 *
 * Phone is the base case. Every media query adds capability rather than
 * clawing back a desktop assumption. System fonts only — no webfont is
 * fetched, so first paint costs one request.
 */

@layer tokens, base, layout, components, states, utilities;

/* ═══════════════════════════════════════════════════════════ tokens ══ */
@layer tokens {
  :root {
    color-scheme: dark;

    /* House palette, shared with darksgames.app so Snerf reads as part of the
     * same studio. Sourced verbatim from the site's stylesheet. */
    --bg: #05050c;
    --panel: rgba(255, 255, 255, 0.03);
    --line: rgba(255, 255, 255, 0.08);
    --line-strong: rgba(255, 255, 255, 0.16);
    --text: #eceded;
    --dim: #9497b8;
    --mint: #7df9c6;
    --cyan: #6bd5fa;
    --violet: #c26bfa;

    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

    /* Snerf's own accent, from its entry in the site catalogue — the green one. */
    --accent: #57c97a;
    --accent-deep: #123321;
    --accent-lift: #7fe09b;
    --accent-ink: #04140c;
    --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);
    --accent-line: color-mix(in srgb, var(--accent) 42%, transparent);
    --accent-glow: color-mix(in srgb, var(--accent) 30%, transparent);
    --accent-grad: linear-gradient(115deg, var(--accent), var(--mint) 85%);

    /* Surfaces. Anything that floats above the page is opaque, so the blurred
     * aurora behind it cannot bleed through and wash out the text. */
    --surface-1: var(--panel);
    --surface-2: rgba(255, 255, 255, 0.055);
    --surface-3: rgba(255, 255, 255, 0.085);
    --surface-float: #0d0d18;
    /* Opaque stand-in for the translucent card when transparency is reduced. */
    --surface-solid: #0a0a14;
    --scrim: rgba(3, 3, 8, 0.66);

    /* Semantic status. Foregrounds all clear 9:1 on --bg; each state also
     * carries a glyph and a border style, because colour alone is not a
     * signal for everyone. */
    --good-fg: #9dedb9;
    --good-bg: rgba(95, 217, 138, 0.13);
    --good-line: rgba(95, 217, 138, 0.38);
    --know-fg: #9fe4fd;
    --know-bg: rgba(107, 213, 250, 0.12);
    --know-line: rgba(107, 213, 250, 0.36);
    --consider-fg: #ffd980;
    --consider-bg: rgba(244, 192, 79, 0.13);
    --consider-line: rgba(244, 192, 79, 0.36);
    --concern-fg: #ffa79b;
    --concern-bg: rgba(255, 122, 107, 0.14);
    --concern-line: rgba(255, 122, 107, 0.38);
    --unknown-fg: #b9bbd4;
    --unknown-bg: rgba(148, 151, 184, 0.12);
    --unknown-line: rgba(148, 151, 184, 0.34);

    /* Spacing, 4px base. */
    --sp-1: 4px;
    --sp-2: 6px;
    --sp-3: 8px;
    --sp-4: 10px;
    --sp-5: 12px;
    --sp-6: 16px;
    --sp-7: 20px;
    --sp-8: 24px;
    --sp-9: 32px;

    /* Fluid gutters. These replace an entire block of phone-only padding
     * overrides — the value scales instead of snapping at a breakpoint. */
    --gutter: clamp(8px, 2.6vw, 14px);
    --pad-card: clamp(11px, 3.4vw, 16px);
    --pad-tile: clamp(8px, 2.4vw, 12px);
    --gap-panel: clamp(7px, 2vw, 10px);
    --gap-tile: clamp(5px, 1.6vw, 8px);

    --r-1: 8px;
    --r-2: 12px;
    --r-3: 15px;
    --r-4: 18px;
    --r-5: 22px;
    --r-6: 26px;
    --r-full: 999px;

    --fs-3xs: 0.625rem;
    --fs-2xs: clamp(0.6rem, 1.9vw, 0.7rem);
    --fs-xs: clamp(0.7rem, 2.2vw, 0.78rem);
    --fs-sm: clamp(0.78rem, 2.6vw, 0.86rem);
    --fs-md: clamp(0.88rem, 3vw, 0.95rem);
    --fs-lg: clamp(1rem, 3.6vw, 1.12rem);
    --fs-xl: clamp(1.15rem, 4.4vw, 1.4rem);
    --fs-2xl: clamp(1.35rem, 5.2vw, 1.75rem);
    --fs-3xl: clamp(1.5rem, 6.4vw, 2rem);

    --lh-tight: 1.1;
    --lh-body: 1.55;
    --track-mono: 0.08em;
    --track-label: 0.14em;
    --track-eyebrow: 0.28em;

    --z-bg: -1;
    --z-nav: 20;
    --z-modal: 60;
    --z-camera: 65;
    --z-noise: 90;

    /* Motion. Press feedback is near-instant; entrances are slower than exits,
     * because arriving should feel unhurried and leaving should feel decisive. */
    --dur-press: 90ms;
    --dur-1: 140ms;
    --dur-2: 200ms;
    --dur-3: 280ms;
    --dur-4: 420ms;
    --dur-ring: 900ms;
    --stagger: 40ms;
    --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-in: cubic-bezier(0.4, 0, 1, 1);
    --ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);

    --shadow-1: 0 8px 24px rgba(0, 0, 0, 0.45);
    --shadow-float: 0 28px 80px rgba(0, 0, 0, 0.6);
    --shadow-accent: 0 10px 34px var(--accent-glow);

    /* Ergonomics. The dvh clamps let the header, nav and score ring shrink
     * continuously on short screens, replacing a max-height query that hid
     * content abruptly below 690px. */
    --tap-min: 44px;
    --tap-comfy: 48px;
    --header-h: clamp(56px, 8.5dvh, 76px);
    --nav-h: clamp(64px, 9.5dvh, 74px);
    --ring-size: clamp(72px, 13dvh, 112px);
    --safe-b: env(safe-area-inset-bottom, 0px);
    --nav-clear: calc(var(--nav-h) + var(--safe-b) + var(--sp-2));
  }

  /* Kept as a comment rather than built: a light theme would be a token
   * override and nothing else, which is the point of putting every colour here.
   * @media (prefers-color-scheme: light) { :root { --bg: …; --text: …; } } */
}

/* ═════════════════════════════════════════════════════════════ base ══ */
@layer base {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  * {
    margin: 0;
  }

  html {
    background: var(--bg);
  }

  /* The app is a fixed viewport with its own internal scrollers; the page
   * itself never scrolls, so there is no rubber-banding over the nav bar. */
  html,
  body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
  }

  body {
    min-height: 100dvh;
    font-family: var(--sans);
    font-size: var(--fs-md);
    line-height: var(--lh-body);
    color: var(--text);
    background: transparent;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  [hidden] {
    display: none !important;
  }

  button,
  input {
    font: inherit;
    color: inherit;
  }

  button {
    cursor: pointer;
  }

  button:disabled {
    cursor: not-allowed;
    opacity: 0.5;
  }

  input::placeholder {
    color: var(--dim);
    opacity: 1;
  }

  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: var(--r-1);
  }

  .upload-button:focus-within {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }

  ::selection {
    background: var(--accent);
    color: var(--accent-ink);
  }

  ::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }

  ::-webkit-scrollbar-thumb {
    background: var(--line-strong);
    border-radius: 3px;
  }

  ::-webkit-scrollbar-track {
    background: transparent;
  }

  h1,
  h2,
  h3 {
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: var(--lh-tight);
  }

  h1 {
    font-size: var(--fs-2xl);
  }

  h2 {
    font-size: var(--fs-xl);
  }

  h3 {
    font-size: var(--fs-lg);
  }

  /* Backdrop: two drifting aurora blobs plus a masked grid and a noise wash,
   * all fixed and non-interactive. Reuses the two divs already in the markup. */
  .bg-orb {
    position: fixed;
    border-radius: 50%;
    z-index: var(--z-bg);
    filter: blur(70px);
    opacity: 0.38;
    pointer-events: none;
    mix-blend-mode: screen;
  }

  .bg-orb-a {
    width: 62vw;
    height: 62vw;
    left: -18vw;
    top: -8vh;
    background: radial-gradient(circle, color-mix(in srgb, var(--accent) 42%, transparent), transparent 65%);
    animation: drift-a 22s var(--ease) infinite alternate;
  }

  .bg-orb-b {
    width: 54vw;
    height: 54vw;
    right: -16vw;
    bottom: -10vh;
    background: radial-gradient(circle, color-mix(in srgb, var(--violet) 30%, transparent), transparent 65%);
    animation: drift-b 27s var(--ease) infinite alternate;
  }

  body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: var(--z-bg);
    pointer-events: none;
    opacity: 0.3;
    background-image:
      linear-gradient(var(--line) 1px, transparent 1px),
      linear-gradient(90deg, var(--line) 1px, transparent 1px);
    background-size: 48px 48px;
    mask-image: radial-gradient(ellipse 90% 45% at 50% 0%, black, transparent 78%);
  }

  body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: var(--z-noise);
    pointer-events: none;
    opacity: 0.045;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
  }
}

/* ═══════════════════════════════════════════════════════════ layout ══ */
@layer layout {
  .shell {
    width: min(980px, 100%);
    height: 100dvh;
    margin-inline: auto;
    padding: var(--sp-3) var(--gutter) var(--nav-clear);
    display: flex;
    flex-direction: column;
    gap: var(--gap-panel);
    overflow: hidden;
  }

  .app-header {
    flex: 0 0 var(--header-h);
    padding: var(--sp-3) var(--pad-card);
    border-radius: var(--r-5);
    position: relative;
    overflow: hidden;
  }

  .hero-top {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
  }

  .brand-lockup {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    min-width: 0;
  }

  .brand-mark {
    position: relative;
    flex: 0 0 auto;
    width: clamp(34px, 6dvh, 44px);
    height: clamp(34px, 6dvh, 44px);
    display: grid;
    place-items: center;
    border-radius: var(--r-3);
    background: var(--accent-grad);
    box-shadow: var(--shadow-accent);
  }

  .brand-mark span {
    width: 42%;
    height: 42%;
    border-radius: 50%;
    background: var(--accent-ink);
  }

  .brand-mark i {
    position: absolute;
    right: 18%;
    top: 20%;
    width: 16%;
    height: 16%;
    border-radius: 50%;
    background: var(--accent-ink);
    opacity: 0.55;
  }

  .app-header h1 {
    font-size: var(--fs-lg);
    letter-spacing: -0.03em;
  }

  .app-header .eyebrow {
    margin-bottom: 0;
  }

  .app-context {
    font-size: var(--fs-2xs);
    color: var(--dim);
    line-height: 1.2;
  }

  /* Every panel occupies the same grid cell so switching tabs can crossfade
   * rather than swap display, and each panel keeps its own scroll position. */
  .content {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template: minmax(0, 1fr) / minmax(0, 1fr);
    overflow: hidden;
  }

  .tab-panel {
    grid-area: 1 / 1;
    min-height: 0;
    height: 100%;
    overflow: hidden;
    display: grid;
    gap: var(--gap-panel);
    align-content: start;
  }

  .tab-panel[data-tab-panel="dashboard"] {
    grid-template-rows: auto minmax(0, 1fr) auto;
  }

  /* Non-dashboard tabs are a single card that fills the space and scrolls
   * internally, so the header and nav never move. */
  .tab-panel:not([data-tab-panel="dashboard"]) > .card {
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
}

/* ═══════════════════════════════════════════════════════ components ══ */
@layer components {
  /* ── surfaces ─────────────────────────────────────────────────────── */
  .card {
    background: var(--surface-1);
    border: 1px solid var(--line);
    border-radius: var(--r-5);
    padding: var(--pad-card);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }

  .stacked {
    padding: 0;
    overflow: hidden;
  }

  .split {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-5);
  }

  .section-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-4);
    margin-bottom: var(--sp-5);
  }

  .section-head h2 {
    font-size: var(--fs-xl);
  }

  /* ── type helpers ─────────────────────────────────────────────────── */
  .eyebrow {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--accent);
    margin-bottom: var(--sp-1);
  }

  .muted,
  .stat span,
  .stat small,
  .list-item small,
  .feed-item span,
  .recipe-card span,
  .detail-list small,
  .camera-status,
  .scanner-panel label,
  .manual-lookup label {
    color: var(--dim);
  }

  .muted {
    font-size: var(--fs-sm);
  }

  /* ── status chips ─────────────────────────────────────────────────────
   * One definition set, driven by custom properties, applied to both the
   * internal rating words and any future display vocabulary. Each state is
   * distinguished three ways — hue, glyph and border — so the meaning
   * survives a colour-blind reader or a monochrome screenshot. */
  .status,
  .ingredient-rating,
  .camera-badge,
  .pill {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border-radius: var(--r-full);
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--st-fg, var(--dim));
    background: var(--st-bg, var(--surface-2));
    border: 1px solid var(--st-line, var(--line));
    border-style: var(--st-border, solid);
  }

  .status::before,
  .ingredient-rating::before,
  .camera-badge::before {
    content: var(--st-glyph, "•");
    font-size: 0.95em;
    line-height: 1;
  }

  .is-good,
  .ingredient-rating.excellent,
  .ingredient-rating.good,
  .camera-badge[data-state="found"] {
    --st-fg: var(--good-fg);
    --st-bg: var(--good-bg);
    --st-line: var(--good-line);
    --st-glyph: "✓";
  }

  .is-know,
  .ingredient-rating.compare,
  .camera-badge[data-state="scanning"] {
    --st-fg: var(--know-fg);
    --st-bg: var(--know-bg);
    --st-line: var(--know-line);
    --st-glyph: "◎";
  }

  .is-consider,
  .ingredient-rating.moderate {
    --st-fg: var(--consider-fg);
    --st-bg: var(--consider-bg);
    --st-line: var(--consider-line);
    --st-glyph: "!";
  }

  .is-concern,
  .ingredient-rating.poor,
  .camera-badge[data-state="error"] {
    --st-fg: var(--concern-fg);
    --st-bg: var(--concern-bg);
    --st-line: var(--concern-line);
    --st-glyph: "▲";
  }

  .is-unknown,
  .ingredient-rating.unknown {
    --st-fg: var(--unknown-fg);
    --st-bg: var(--unknown-bg);
    --st-line: var(--unknown-line);
    --st-glyph: "?";
    --st-border: dashed;
  }

  .pill.live {
    --st-fg: var(--accent);
    --st-bg: var(--accent-soft);
    --st-line: var(--accent-line);
  }

  /* ── controls ─────────────────────────────────────────────────────── */
  .button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    min-height: var(--tap-comfy);
    padding: 0 var(--sp-6);
    border: 1px solid transparent;
    border-radius: var(--r-3);
    font-family: var(--mono);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    text-align: center;
  }

  .button.primary {
    background: var(--accent-grad);
    color: var(--accent-ink);
    box-shadow: var(--shadow-accent);
  }

  .button.secondary {
    background: var(--surface-2);
    color: var(--text);
    border-color: var(--line);
  }

  .button.quiet {
    background: transparent;
    color: var(--dim);
  }

  /* Was ~30px tall, on the Remove control of every shopping list row — the
   * smallest target in the app and the one most likely to be mis-tapped. */
  .mini-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap-min);
    min-width: var(--tap-min);
    padding: 0 var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-full);
    background: var(--surface-2);
    color: var(--dim);
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
  }

  .avatar {
    flex: 0 0 auto;
    width: clamp(34px, 6dvh, 44px);
    height: clamp(34px, 6dvh, 44px);
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--text);
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: var(--track-mono);
  }

  .add-row,
  .action-row,
  .result-actions,
  .camera-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-3);
  }

  .add-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .add-row input {
    min-width: 0;
    min-height: var(--tap-comfy);
    padding: 0 var(--sp-5);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    color: var(--text);
  }

  .add-row input:focus {
    border-color: var(--accent-line);
    background: var(--surface-3);
  }

  /* ── dashboard ────────────────────────────────────────────────────── */
  .stat-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--gap-tile);
  }

  .stat {
    min-width: 0;
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    display: grid;
    gap: 2px;
  }

  .stat span {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  .stat strong {
    font-size: var(--fs-xl);
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  .stat small {
    font-size: var(--fs-2xs);
    line-height: 1.25;
  }

  .score-ring {
    position: relative;
    flex: 0 0 auto;
    width: var(--ring-size);
    height: var(--ring-size);
  }

  .score-ring svg {
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
    overflow: visible;
  }

  .ring-track,
  .ring-value {
    fill: none;
    stroke-width: 9;
  }

  .ring-track {
    stroke: var(--line);
  }

  /* stroke-dasharray is set from JS off the circle's measured length, so the
   * geometry can never drift from a number hardcoded here. */
  .ring-value {
    stroke: var(--accent);
    stroke-linecap: round;
    filter: drop-shadow(0 0 8px var(--accent-glow));
  }

  .score-ring[data-band="good"] .ring-value {
    stroke: var(--accent);
  }

  .score-ring[data-band="ok"] .ring-value {
    stroke: var(--consider-fg);
  }

  .score-ring[data-band="poor"] .ring-value {
    stroke: var(--concern-fg);
  }

  .score-copy {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    align-content: center;
    text-align: center;
  }

  .score-copy strong {
    font-size: var(--fs-2xl);
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  .score-copy span {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-label);
    color: var(--dim);
  }

  .expand-row {
    width: 100%;
    min-height: 68px;
    padding: var(--sp-4) var(--pad-card);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    border: 0;
    background: transparent;
    color: inherit;
    text-align: left;
  }

  .expand-row > span:first-child {
    min-width: 0;
    display: grid;
    gap: 2px;
  }

  .expand-row strong {
    font-size: var(--fs-md);
  }

  .expand-row small {
    color: var(--dim);
    font-size: var(--fs-2xs);
  }

  /* Label comes from the button's expanded state rather than being written by
   * JS, which previously meant an unguarded querySelector into the dashboard
   * on every modal close. */
  .chev {
    flex: 0 0 auto;
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    color: var(--accent);
  }

  .chev::after {
    content: "More info";
  }

  [aria-expanded="true"] .chev::after {
    content: "Open";
  }

  /* ── lists ────────────────────────────────────────────────────────── */
  .list-stack,
  .community-feed {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    display: grid;
    gap: var(--gap-tile);
    align-content: start;
    padding-block: var(--sp-1);
  }

  .list-item,
  .feed-item {
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
  }

  .list-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--sp-4);
  }

  .list-item span {
    min-width: 0;
    font-weight: 600;
    overflow-wrap: anywhere;
  }

  .list-item small {
    grid-column: 1;
    font-size: var(--fs-2xs);
  }

  .list-item .mini-button {
    grid-column: 2;
    grid-row: 1 / span 2;
  }

  .feed-item {
    display: grid;
    gap: var(--sp-1);
  }

  .feed-item span {
    font-size: var(--fs-sm);
  }

  .tab-panel[data-tab-panel="list"] .add-row {
    flex: 0 0 auto;
    margin-top: var(--sp-4);
  }

  /* ── scanner ──────────────────────────────────────────────────────── */
  .choice-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--gap-tile);
    margin-bottom: var(--sp-5);
  }

  .choice {
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    color: inherit;
    text-align: left;
    display: grid;
    gap: 2px;
    min-height: var(--tap-comfy);
  }

  .choice strong {
    font-size: var(--fs-sm);
  }

  .choice span {
    font-size: var(--fs-2xs);
    color: var(--dim);
  }

  .choice em {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    font-style: normal;
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    color: var(--accent);
  }

  .scan-card {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .scanner-panel {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .scanner-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
  }

  .scanner-heading h3 {
    font-size: var(--fs-md);
  }

  .camera-stage {
    position: relative;
    border-radius: var(--r-4);
    overflow: hidden;
    background: #000;
    aspect-ratio: 4 / 3;
  }

  .camera-stage video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  .scan-target {
    position: absolute;
    inset: 18% 12%;
    border: 2px solid var(--accent-line);
    border-radius: var(--r-2);
    overflow: hidden;
    box-shadow: 0 0 0 999px rgba(3, 3, 8, 0.28);
  }

  .scan-target::before {
    content: "";
    position: absolute;
    left: 6%;
    right: 6%;
    top: 50%;
    height: 2px;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    box-shadow: 0 0 18px var(--accent-glow);
  }

  .camera-status {
    font-size: var(--fs-2xs);
    line-height: 1.35;
  }

  .manual-lookup {
    display: grid;
    gap: var(--sp-2);
  }

  .manual-lookup label,
  .scanner-panel label {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  #scan-hint {
    font-size: var(--fs-2xs);
  }

  /* ── recipes ──────────────────────────────────────────────────────── */
  .recipe-cards {
    display: grid;
    gap: var(--gap-tile);
    align-content: start;
  }

  .recipe-card {
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    color: inherit;
    text-align: left;
    display: grid;
    gap: 2px;
    min-height: var(--tap-comfy);
  }

  .recipe-card span {
    font-size: var(--fs-2xs);
  }

  /* ── overlays ─────────────────────────────────────────────────────── */
  /* On a phone this is a bottom sheet: anchored to the thumb, full width, with
   * a grab handle and drag-to-dismiss. It becomes a centred dialog only once
   * there is room for one. Same markup either way — the presenter is chosen by
   * viewport, not by a second component. */
  .app-modal {
    position: fixed;
    inset: auto 0 0 0;
    z-index: var(--z-modal);
    width: 100%;
    max-height: 88dvh;
    padding: var(--sp-7) var(--pad-card) calc(var(--sp-6) + var(--safe-b));
    display: grid;
    gap: var(--sp-5);
    align-content: start;
    overflow: hidden auto;
    overscroll-behavior: contain;
    background: var(--surface-float);
    border: 1px solid var(--line);
    border-bottom: 0;
    border-radius: var(--r-6) var(--r-6) 0 0;
    box-shadow: var(--shadow-float);
    touch-action: pan-y;
  }

  .app-modal::before {
    content: "";
    position: sticky;
    top: 0;
    justify-self: center;
    width: 38px;
    height: 4px;
    margin-top: calc(var(--sp-5) * -1);
    border-radius: 2px;
    background: var(--line-strong);
  }

  @media (min-width: 600px) {
    .app-modal {
      inset: 50% auto auto 50%;
      width: min(590px, calc(100vw - 2 * var(--gutter)));
      max-height: min(760px, calc(100dvh - 2 * var(--sp-8)));
      padding: var(--pad-card);
      border: 1px solid var(--line);
      border-radius: var(--r-6);
    }

    .app-modal::before {
      display: none;
    }
  }

  .modal-close {
    position: sticky;
    top: 0;
    z-index: 2;
    justify-self: end;
    width: var(--tap-min);
    height: var(--tap-min);
    margin-bottom: calc(var(--tap-min) * -1 + 2px);
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
    border-radius: var(--r-2);
    background: var(--surface-2);
    color: var(--text);
    font-size: 1.35rem;
    line-height: 1;
  }

  .result-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-5);
  }

  .result-score {
    flex: 0 0 auto;
    min-width: 56px;
    padding: var(--sp-3) var(--sp-4);
    display: grid;
    place-items: center;
    border: 1px solid var(--accent-line);
    border-radius: var(--r-3);
    background: var(--accent-soft);
    color: var(--accent);
    font-family: var(--mono);
    font-size: var(--fs-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }

  .result-foldouts {
    display: grid;
    gap: var(--sp-3);
  }

  .result-foldout {
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    overflow: hidden;
  }

  .result-foldout summary {
    min-height: var(--tap-comfy);
    padding: var(--sp-4) var(--pad-tile);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    list-style: none;
  }

  .result-foldout summary::-webkit-details-marker {
    display: none;
  }

  .result-foldout summary > span {
    min-width: 0;
    display: grid;
    gap: 1px;
  }

  .result-foldout summary strong {
    font-size: var(--fs-sm);
  }

  .result-foldout summary small {
    color: var(--dim);
    font-size: var(--fs-2xs);
  }

  .result-foldout summary::after {
    content: "+";
    flex: 0 0 auto;
    font-family: var(--mono);
    color: var(--accent);
  }

  .result-foldout[open] summary::after {
    content: "−";
  }

  .foldout-body {
    padding: 0 var(--pad-tile) var(--sp-4);
    display: grid;
    gap: var(--sp-3);
  }

  .info-grid > div {
    display: grid;
    gap: 1px;
    padding-block: var(--sp-2);
    border-bottom: 1px solid var(--line);
  }

  .info-grid > div span {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--dim);
  }

  .info-grid > div strong {
    font-size: var(--fs-sm);
    overflow-wrap: anywhere;
  }

  .info-grid p {
    font-size: var(--fs-sm);
    color: var(--dim);
  }

  .detail-list {
    list-style: none;
    padding: 0;
    display: grid;
    gap: var(--sp-2);
  }

  .detail-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    padding-block: var(--sp-2);
    border-bottom: 1px solid var(--line);
  }

  .detail-list li:last-child {
    border-bottom: 0;
  }

  /* ── score detail ─────────────────────────────────────────────────── */

  /* The band in words, immediately under the number. A score alone is a
   * measurement; the word is the part that answers the question. */
  .band {
    font-family: var(--mono);
    font-size: var(--fs-sm);
    font-weight: 700;
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    color: var(--st-fg, var(--text));
  }

  .confidence {
    display: grid;
    gap: var(--sp-2);
    justify-items: start;
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
  }

  .confidence small {
    color: var(--dim);
    font-size: var(--fs-2xs);
    line-height: 1.35;
  }

  .confidence .provisional {
    color: var(--consider-fg);
  }

  .dimensions {
    display: grid;
    gap: var(--sp-3);
  }

  /* ── profile ──────────────────────────────────────────────────────── */
  .profile-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    display: grid;
    gap: var(--sp-5);
    align-content: start;
  }

  .profile-identity {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
  }

  .profile-initials {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--accent-grad);
    color: var(--accent-ink);
    font-family: var(--mono);
    font-weight: 700;
  }

  .profile-identity strong {
    display: block;
    font-size: var(--fs-lg);
  }

  .profile-identity small {
    color: var(--dim);
    font-size: var(--fs-2xs);
  }

  .profile-heading {
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--dim);
  }

  .profile-rows {
    display: grid;
    gap: var(--gap-tile);
  }

  .profile-row {
    width: 100%;
    min-height: var(--tap-comfy);
    padding: var(--pad-tile);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
    color: inherit;
    text-align: left;
  }

  .profile-row > span:first-child {
    min-width: 0;
    display: grid;
    gap: 1px;
  }

  .profile-row strong {
    font-size: var(--fs-sm);
  }

  .profile-row small {
    color: var(--dim);
    font-size: var(--fs-2xs);
  }

  .profile-row.is-danger strong {
    color: var(--concern-fg);
  }

  .chev-arrow {
    flex: 0 0 auto;
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
    color: var(--accent);
  }

  .profile-row.is-danger .chev-arrow {
    color: var(--concern-fg);
  }

  .profile-note {
    font-size: var(--fs-sm);
    color: var(--dim);
  }

  .profile-note a {
    color: var(--accent);
  }

  /* ── Snerf Test ───────────────────────────────────────────────────────
   * Stacked vertically rather than as two side-by-side columns. A phone has
   * no room for a comparison table, and stacking makes the reading order —
   * yours, then the alternative, then the difference — the same for a screen
   * reader as it is by eye. */
  .versus {
    display: grid;
    gap: var(--sp-3);
  }

  .versus-side {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-2) var(--sp-4);
    padding: var(--pad-tile);
    border: 1px solid var(--line);
    border-radius: var(--r-3);
    background: var(--surface-2);
  }

  .versus-side.is-better {
    border-color: var(--good-line);
    background: var(--good-bg);
  }

  .versus-label {
    grid-column: 1 / -1;
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--dim);
  }

  .versus-side.is-better .versus-label {
    color: var(--good-fg);
  }

  .versus-side strong {
    min-width: 0;
    font-size: var(--fs-sm);
    overflow-wrap: anywhere;
  }

  .versus-score {
    font-family: var(--mono);
    font-size: var(--fs-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }

  .versus-side.is-better .versus-score {
    color: var(--good-fg);
  }

  .versus-arrow {
    justify-self: center;
    color: var(--dim);
    line-height: 1;
  }

  /* The answer, styled as the answer. */
  .verdict {
    padding: var(--pad-tile);
    border: 1px solid var(--accent-line);
    border-radius: var(--r-3);
    background: var(--accent-soft);
  }

  .verdict h4 {
    font-size: var(--fs-lg);
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--accent);
  }

  .verdict p {
    margin-top: var(--sp-2);
    font-size: var(--fs-sm);
  }

  .alt-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-4);
    padding-block: var(--sp-3);
    border-bottom: 1px solid var(--line);
  }

  .alt-row:last-child {
    border-bottom: 0;
  }

  .alt-score {
    min-width: 2.4em;
    padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--line);
    border-radius: var(--r-2);
    background: var(--surface-2);
    color: var(--dim);
    font-family: var(--mono);
    font-size: var(--fs-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: center;
  }

  .alt-score.is-better {
    border-color: var(--good-line);
    background: var(--good-bg);
    color: var(--good-fg);
  }

  .alt-name {
    min-width: 0;
    display: grid;
    gap: 1px;
  }

  .alt-name strong {
    font-size: var(--fs-sm);
    overflow-wrap: anywhere;
  }

  .alt-name small {
    color: var(--dim);
    font-size: var(--fs-2xs);
  }

  /* Where the comparison says what it does not cover. */
  .fineprint {
    font-size: var(--fs-2xs);
    line-height: 1.4;
    color: var(--dim);
  }

  .full-ingredient-list {
    display: grid;
    gap: var(--sp-2);
  }

  .ingredient-row {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr);
    align-items: center;
    column-gap: var(--sp-3);
    row-gap: var(--sp-2);
    padding-block: var(--sp-2);
    border-bottom: 1px solid var(--line);
  }

  .ingredient-row:last-child {
    border-bottom: 0;
  }

  .ingredient-number {
    font-family: var(--mono);
    font-size: var(--fs-2xs);
    color: var(--dim);
  }

  .ingredient-name {
    min-width: 0;
    font-size: var(--fs-sm);
    overflow-wrap: anywhere;
  }

  .ingredient-rating {
    grid-column: 2;
    justify-self: start;
  }

  /* ── bottom nav ───────────────────────────────────────────────────── */
  .bottom-nav {
    position: fixed;
    left: 50%;
    bottom: 0;
    z-index: var(--z-nav);
    width: min(980px, 100%);
    min-height: var(--nav-h);
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3) calc(var(--sp-2) + var(--safe-b));
    background: color-mix(in srgb, var(--bg) 78%, transparent);
    border: 1px solid var(--line);
    border-bottom: 0;
    border-radius: var(--r-5) var(--r-5) 0 0;
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
  }

  .nav-item {
    position: relative;
    min-height: var(--tap-min);
    display: grid;
    place-items: center;
    border: 0;
    border-radius: var(--r-3);
    background: transparent;
    color: var(--dim);
    font-family: var(--mono);
    font-size: var(--fs-3xs);
    letter-spacing: var(--track-mono);
    text-transform: uppercase;
  }

  .nav-item.active {
    color: var(--text);
    background: var(--surface-2);
  }

  /* Underline that grows from the centre, so a tab change reads as one
   * movement rather than two independent colour swaps. */
  .nav-item::after {
    content: "";
    position: absolute;
    inset: auto 22% 4px;
    height: 2px;
    border-radius: 2px;
    background: var(--accent-grad);
    transform: scaleX(0);
  }

  .nav-item.active::after {
    transform: scaleX(1);
  }

  .nav-core {
    background: var(--accent-grad);
    color: var(--accent-ink);
    font-weight: 800;
    box-shadow: 0 8px 24px var(--accent-glow);
  }

  .nav-core.active {
    background: var(--accent-grad);
    color: var(--accent-ink);
  }

  .nav-core::after {
    display: none;
  }
}

/* ═══════════════════════════════════════════════════════════ states ══ */
@layer states {
  /* Was toggled by JS with no rule to match it, so opening a modal did nothing
   * to the page behind it. */
  body.modal-open .bottom-nav {
    opacity: 0.3;
    pointer-events: none;
    filter: saturate(0.5);
  }

  /* The scanner panel is pulled out of the card into a centred overlay while
   * the camera runs. .shell must not be transformed in this state — a
   * transform would become the containing block for that fixed panel. */
  body.camera-open .tab-panel.active .scanner-panel {
    position: fixed;
    inset: auto var(--gutter) var(--nav-clear);
    z-index: var(--z-camera);
    max-height: 78dvh;
    padding: var(--pad-card);
    background: var(--surface-float);
    border: 1px solid var(--line);
    border-radius: var(--r-6);
    box-shadow: var(--shadow-float);
  }

  body.camera-open .camera-stage {
    aspect-ratio: 3 / 4;
  }
}

/* ════════════════════════════════════════════════════════ utilities ══ */
@layer utilities {
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* ── motion ───────────────────────────────────────────────────────────
   * The previous stylesheet had no transition at all: no button had a hover
   * or active state, and every tab change, modal close and foldout toggle
   * happened between one frame and the next. Everything below moves only
   * transform, opacity or filter so it stays off the layout path. */

  @keyframes drift-a {
    to {
      transform: translate(8vw, 6vh) scale(1.15);
    }
  }

  @keyframes drift-b {
    to {
      transform: translate(-7vw, -5vh) scale(0.88);
    }
  }

  @keyframes rise-in {
    from {
      opacity: 0;
      transform: translateY(14px);
    }
  }

  @keyframes item-in {
    from {
      opacity: 0;
      transform: translateY(12px);
    }
  }

  @keyframes fade-in {
    from {
      opacity: 0;
    }
  }

  @keyframes scan-line {
    from {
      transform: translateY(-42px);
    }
    to {
      transform: translateY(42px);
    }
  }

  @keyframes lock-on {
    0% {
      box-shadow: 0 0 0 0 var(--accent), 0 0 0 999px rgba(3, 3, 8, 0.28);
      border-color: var(--accent);
    }
    60% {
      box-shadow: 0 0 0 12px transparent, 0 0 0 999px rgba(3, 3, 8, 0.28);
    }
    100% {
      box-shadow: 0 0 0 0 transparent, 0 0 0 999px rgba(3, 3, 8, 0.28);
      border-color: var(--accent);
    }
  }

  @keyframes hit-flash {
    from {
      opacity: 0.55;
    }
    to {
      opacity: 0;
    }
  }

  @keyframes value-pop {
    40% {
      transform: scale(1.14);
      color: var(--accent);
    }
  }

  @keyframes shimmer-sweep {
    to {
      background-position: -200% 0;
    }
  }

  @keyframes spin {
    to {
      transform: rotate(1turn);
    }
  }

  @keyframes indeterminate {
    from {
      transform: translateX(-100%) scaleX(0.4);
    }
    to {
      transform: translateX(100%) scaleX(0.4);
    }
  }

  /* Press feedback. --lift keeps any baseline offset separate from the press
   * scale, so the two compose instead of one clobbering the other. */
  .button,
  .mini-button,
  .nav-item,
  .choice,
  .recipe-card,
  .expand-row,
  .avatar,
  .modal-close,
  .upload-button,
  .profile-row,
  .result-foldout summary {
    --lift: 0px;
    --press: 1;
    transform: translateY(var(--lift)) scale(var(--press));
    transition:
      transform var(--dur-press) var(--ease),
      background-color var(--dur-2) var(--ease),
      border-color var(--dur-2) var(--ease),
      box-shadow var(--dur-2) var(--ease),
      color var(--dur-2) var(--ease),
      filter var(--dur-2) var(--ease);
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    user-select: none;
  }

  :is(.button, .mini-button, .choice, .recipe-card, .expand-row, .avatar, .modal-close,
      .upload-button, .profile-row, .result-foldout summary):active {
    --press: 0.965;
  }

  .nav-item:active {
    --press: 0.9;
  }

  :is(.button, .nav-item, .choice, .recipe-card):not(:active) {
    transition-timing-function: var(--ease-spring);
  }

  .mini-button:active {
    color: var(--concern-fg);
    border-color: var(--concern-line);
  }

  @media (hover: hover) and (pointer: fine) {
    .button.primary:hover {
      --lift: -2px;
      filter: brightness(1.06);
    }

    .button.secondary:hover,
    .profile-row:hover,
    .choice:hover,
    .recipe-card:hover,
    .list-item:hover {
      border-color: var(--accent-line);
      background: var(--surface-3);
    }

    .nav-item:hover {
      color: var(--text);
    }

    .mini-button:hover {
      color: var(--concern-fg);
      border-color: var(--concern-line);
    }
  }

  .nav-item::after {
    transition: transform var(--dur-2) var(--ease-out);
  }

  /* Tab crossfade. visibility keeps inactive panels out of the tab order and
   * the accessibility tree, and is delayed on exit so the fade is visible. */
  .tab-panel {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(10px) scale(0.995);
    transition:
      opacity var(--dur-2) var(--ease-in),
      transform var(--dur-2) var(--ease-in),
      visibility 0s linear var(--dur-2);
  }

  .tab-panel.active {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: none;
    transition:
      opacity var(--dur-3) var(--ease-out),
      transform var(--dur-3) var(--ease-out),
      visibility 0s;
  }

  .tab-panel.active > .card {
    animation: rise-in var(--dur-3) var(--ease-out) both;
  }

  .tab-panel.active > .card:nth-child(2) {
    animation-delay: 60ms;
  }

  .tab-panel.active > .card:nth-child(3) {
    animation-delay: 110ms;
  }

  /* Staggered list entrances. The delay ladder is written out with nth-child
   * rather than an inline custom property, because the CSP forbids style
   * attributes and this markup is generated as an innerHTML string. Capped at
   * eight so a forty-ingredient list does not take two seconds to appear. */
  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > * {
    animation: item-in var(--dur-3) var(--ease-out) both;
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(1) {
    animation-delay: 0ms;
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(2) {
    animation-delay: calc(1 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(3) {
    animation-delay: calc(2 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(4) {
    animation-delay: calc(3 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(5) {
    animation-delay: calc(4 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(6) {
    animation-delay: calc(5 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(7) {
    animation-delay: calc(6 * var(--stagger));
  }

  :is(.list-stack, .community-feed, .recipe-cards, .full-ingredient-list, .detail-list) > *:nth-child(n + 8) {
    animation-delay: calc(7 * var(--stagger));
  }

  /* Stats are excluded from the translate stagger on purpose: the whole grid
   * is re-rendered whenever a shopping item is added, and three unrelated
   * tiles jumping every time you add a banana reads as a glitch. */
  .stat {
    animation: fade-in var(--dur-1) var(--ease) both;
  }

  .stat.is-updated strong {
    animation: value-pop var(--dur-4) var(--ease-spring);
  }

  /* Modal. @starting-style gives the entrance; allow-discrete lets the exit
   * play before [hidden] takes it out of flow. Where neither is supported the
   * modal simply appears and disappears, which is what happens today. */
  /* Phone: the sheet slides up from the bottom edge and back down again.
   * @starting-style supplies the entrance, allow-discrete lets the exit finish
   * before [hidden] pulls it from flow, and .is-closing is the fallback where
   * neither is supported — all three settle on the same end state. */
  .app-modal {
    opacity: 1;
    transform: translateY(0);
    transition:
      opacity var(--dur-2) var(--ease),
      transform var(--dur-3) var(--ease-out),
      overlay var(--dur-2) allow-discrete,
      display var(--dur-2) allow-discrete;
  }

  @starting-style {
    .app-modal:not([hidden]) {
      transform: translateY(100%);
    }
  }

  .app-modal[hidden],
  .app-modal.is-closing {
    transform: translateY(100%);
    pointer-events: none;
    transition-timing-function: var(--ease-in);
  }

  /* While a finger is on it the sheet follows directly, with no transition to
   * lag behind the drag. */
  .app-modal.dragging {
    transition: none;
  }

  @media (min-width: 600px) {
    .app-modal {
      transform: translate(-50%, -50%) scale(1);
    }

    @starting-style {
      .app-modal:not([hidden]) {
        opacity: 0;
        transform: translate(-50%, calc(-50% + 16px)) scale(0.97);
      }
    }

    .app-modal[hidden],
    .app-modal.is-closing {
      opacity: 0;
      transform: translate(-50%, calc(-50% + 10px)) scale(0.98);
    }
  }

  /* Dim and desaturate the page behind an open overlay. Applied to .shell
   * rather than a scrim element so no extra markup is needed; excluded while
   * the camera is open, because a filter would make .shell the containing
   * block for the fixed scanner panel inside it and shift it off centre. */
  body.modal-open:not(.camera-open) .shell {
    filter: brightness(0.45) saturate(0.75);
  }

  .shell {
    transition: filter var(--dur-3) var(--ease);
  }

  /* Score ring. Drawn from empty on first paint; JS writes the offset. */
  .ring-value {
    transition:
      stroke-dashoffset var(--dur-ring) var(--ease-out),
      stroke var(--dur-2) var(--ease);
  }

  /* Scanner. The sweep marks the reticle as live; the lock-on fires once on a
   * successful read, next to the haptic buzz that already happens. */
  .scan-target::before {
    animation: scan-line 2.4s var(--ease) infinite alternate;
  }

  .camera-stage.is-hit .scan-target {
    animation: lock-on 520ms var(--ease-out);
  }

  .camera-stage.is-hit::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: var(--accent);
    animation: hit-flash 380ms var(--ease-out) both;
  }

  /* Loading affordances. */
  .skeleton {
    border-radius: var(--r-2);
    background: linear-gradient(100deg, var(--surface-2) 30%, rgba(255, 255, 255, 0.1) 50%, var(--surface-2) 70%) 0 0 / 200% 100%;
    animation: shimmer-sweep 1.5s linear infinite;
    color: transparent;
    pointer-events: none;
    user-select: none;
  }

  .skeleton-line {
    height: 1em;
    margin-block: 0.25em;
  }

  .skeleton-line.short {
    width: 45%;
  }

  .button[data-busy="true"] {
    position: relative;
    color: transparent;
    pointer-events: none;
  }

  .button[data-busy="true"]::after {
    content: "";
    position: absolute;
    inset: 50% auto auto 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border: 2px solid var(--accent-ink);
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
  }

  .app-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: var(--accent-grad);
    transform: scaleX(0);
    transform-origin: left;
  }

  body.is-busy .app-header::after {
    animation: indeterminate 1.1s var(--ease) infinite;
  }

  /* ── responsive: additive only ────────────────────────────────────── */

  /* Wider than a phone: two-up recipes, ingredient rating back on the row. */
  @media (min-width: 600px) {
    .recipe-cards {
      grid-template-columns: 1fr 1fr;
    }

    .ingredient-row {
      grid-template-columns: 24px minmax(0, 1fr) auto;
    }

    .ingredient-rating {
      grid-column: auto;
      justify-self: end;
    }

    .info-grid > div {
      grid-template-columns: 140px minmax(0, 1fr);
      align-items: baseline;
      gap: var(--sp-4);
    }
  }

  /* Tablet and up: lift the nav off the bottom edge into a floating bar. */
  @media (min-width: 900px) {
    .bottom-nav {
      width: min(940px, calc(100% - 2 * var(--sp-6)));
      bottom: var(--sp-4);
      border: 1px solid var(--line);
      border-radius: var(--r-5);
    }

    .app-header h1 {
      font-size: var(--fs-xl);
    }
  }

  /* Tall enough to afford it: reveal the secondary text that short screens
   * cannot spare the room for. Inverted from a max-height rule that used to
   * hide these, so the phone case is the default rather than the exception. */
  .app-header .eyebrow,
  .app-context,
  .scan-card .choice span {
    display: none;
  }

  @media (min-height: 700px) {
    .app-header .eyebrow,
    .app-context,
    .scan-card .choice span {
      display: block;
    }
  }

  /* Installed as an app: no browser chrome, so pay the status bar directly. */
  @media (display-mode: standalone) {
    .shell {
      padding-top: calc(var(--sp-3) + env(safe-area-inset-top, 0px));
    }
  }

  /* ── preferences ──────────────────────────────────────────────────── */

  /* The previous rule neutered animation only, leaving every transition at
   * full duration. Press feedback is deliberately kept: it is an instant state
   * change rather than motion over time, and on a touch screen it is the only
   * confirmation that a tap registered. */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      animation-delay: 0ms !important;
      transition-duration: 0.01ms !important;
      transition-delay: 0ms !important;
      scroll-behavior: auto !important;
    }

    .tab-panel,
    .tab-panel.active {
      transform: none;
    }

    .skeleton {
      background: var(--surface-2);
    }

    .scan-target::before {
      opacity: 0.5;
    }
  }

  @media (prefers-reduced-transparency: reduce) {
    .card,
    .bottom-nav {
      -webkit-backdrop-filter: none;
      backdrop-filter: none;
    }

    .card {
      background: var(--surface-solid);
    }

    .bottom-nav {
      background: var(--bg);
    }
  }

  @media (prefers-contrast: more) {
    :root {
      --line: rgba(255, 255, 255, 0.2);
      --line-strong: rgba(255, 255, 255, 0.34);
      --dim: #b6b9d2;
    }
  }
}
