/* Design tokens — the single source of truth for colour and size.
   No hard-coded colours or sizes anywhere else in this codebase.

   Spec §9.1: the shared values below are MIRRORED from the workout tracker's
   token file, not re-derived. Any token declared in both files must hold an
   identical value — test/tokens.test.js enforces that mechanically, so drift
   between the two apps fails a test rather than being noticed by eye.

   Source: ../Workout Tracker/src/css/tokens.css, extracted 2026-08-14.

   Workout-specific tokens (tier scale, body map, set-row steppers, gym and
   equipment column widths, --header-h) are deliberately NOT mirrored — this app
   has no such surfaces, and --header-h in particular is a measured value tied to
   that app's header, which holds nav links where ours does not.

   Tokens this app adds are grouped at the end under §9.2 and are DERIVED from
   the palette above, never introduced as new hues. */

:root {
  /* Declares the palette below as already-dark. Without this, Chromium's
     auto-dark heuristic (Samsung Internet's "force dark mode on websites")
     reads the page as light-themed and inverts everything. Paired with
     <meta name="color-scheme" content="dark"> in index.html so the opt-out
     applies before the stylesheet lands.

     §9.1a: v1 is dark-only. There is no light palette to extract from the
     source file, and inventing one is forbidden by §9.1. settings.theme
     persists as a reserved field with no UI. Light theme is BACKLOG N11. */
  color-scheme: dark;

  /* ---- Colour: warm dark greys, a gym at night rather than a terminal ---- */
  --bg: #14110F;
  --surface: #1E1A17;
  --surface-alt: #292420;
  --line: #3A3330;
  --text: #F0EBE5;
  --text-dim: #9A918A;
  --accent: #E8833A;
  --good: #6FA86A;
  --warn: #C9A227;
  --bad: #B5503C;
  /* Elevation needs a tone BELOW --bg — on a dark UI a shadow cannot be
     derived from the surfaces it sits under. Warm-shifted like the rest. */
  --shadow: #0A0806;

  /* Derived, so the palette above stays the only place real colours live.
     --bad at full strength is 3.4:1 on --surface, under the §9.3 floor, so
     body text in the "bad" role uses the lifted variant (5.5:1). The raw token
     stays for fills, borders and bars where the floor does not apply. */
  --bad-text: color-mix(in srgb, var(--bad) 70%, var(--text));
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 45%, var(--line));
  --warn-line: color-mix(in srgb, var(--warn) 40%, var(--line));
  --good-line: color-mix(in srgb, var(--good) 45%, var(--line));
  --scrim: color-mix(in srgb, var(--bg) 88%, transparent); /* sticky header */
  --veil: color-mix(in srgb, var(--bg) 78%, transparent); /* modal backdrop */

  /* ---- Elevation ----
     Three surface levels read as layers, not swatches. Light is implied from
     above: a lifted hairline on the top edge, a soft shadow below. Kept
     deliberately low-contrast — cumulatively this gives the screen structure,
     but no single card should announce itself. */
  --shadow-soft: color-mix(in srgb, var(--shadow) 50%, transparent);
  --shadow-softer: color-mix(in srgb, var(--shadow) 28%, transparent);

  /* Edges: lit catches implied light, shade turns away from it. */
  --edge-lit: color-mix(in srgb, var(--line) 62%, var(--text));
  --edge-shade: color-mix(in srgb, var(--line) 55%, var(--shadow));

  /* Rows inside a card separate on a hairline, not on spacing alone. */
  --line-soft: color-mix(in srgb, var(--line) 55%, transparent);

  /* Raised: cards and rows. Higher: the modal sheet. */
  --elev-1: 0 var(--hair) var(--s-1) calc(-1 * var(--hair)) var(--shadow-softer),
            0 var(--s-1) var(--s-3) calc(-1 * var(--s-2)) var(--shadow-soft);
  --elev-2: 0 var(--s-1) var(--s-3) calc(-1 * var(--s-1)) var(--shadow-softer),
            0 var(--s-2) var(--s-8) calc(-1 * var(--s-3)) var(--shadow-soft);

  /* Recessed: inputs sit IN the surface, so the edge inverts — shade on top,
     lit along the bottom. */
  --inset-1: inset 0 var(--hair) var(--s-1) calc(-1 * var(--hair)) var(--shadow-soft);

  /* Active state is an accent EDGE, never a filled block — a fill reads as a
     button, an edge reads as "you are here". Carried as a transparent border
     on the base rule so turning it on shifts no layout. */
  --edge-active: var(--s-1);

  /* ---- Space: 4px base unit ---- */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-8: 32px;

  /* Card padding: 16px mobile, 12px desktop */
  --card-pad: var(--s-4);

  /* ---- Radius ---- */
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-pill: 999px;
  --hair: 1px;

  /* ---- Type ---- */
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --fs-label: 0.8125rem; /* labels and units */
  --fs-body: 0.9375rem;
  --fs-name: 1.125rem; /* food and meal names */
  --fs-input: 1.125rem; /* gram entry and numeric fields */
  --fs-num: 2rem; /* the day's headline totals */
  --lh-tight: 1.2;
  --lh-body: 1.45;

  --ls-caps: 0.04em;
  --ls-tight: -0.01em;
  --underline-offset: 2px;

  /* ---- Controls ---- */
  --tap: 48px; /* §9.3 floor, one-handed with poor grip */
  --focus: 2px;

  /* ---- Layout: column minimums and container widths.
     Tokenised for the same reason as everything else — these are the values
     that otherwise drift step to step. Each grid uses min(track, 100%) so a
     360px screen collapses to one column instead of overflowing (§9.3). ---- */
  --w-main: 900px;
  --w-sheet: 640px;
  /* `dvh`, not `vh`, and on a phone they are different numbers. `vh` resolves
     against the LARGE viewport — the one with Chrome's URL bar retracted —
     while a fixed-position overlay gets the viewport as it is right now. With
     the bar showing, 88vh can exceed the box the sheet sits in, the overlay
     starts overflowing, and `align-items: flex-end` then clips the top of the
     sheet where nothing can scroll it back. The `vh` line stays as the
     fallback. */
  --h-sheet: 88vh;
  --h-sheet: 88dvh;
  --col-num: 72px; /* numeric field, fluid on mobile */
  --w-field-max: 116px; /* its desktop cap — denser */
  --w-search: 220px;

  /* ---- Charts (§7.7) ----
     Geometry as well as colour, because SVG takes r/stroke-width from CSS: the
     chart's literals belong here with everything else. */
  --chart-band: color-mix(in srgb, var(--good) 16%, transparent);
  --chart-grid: color-mix(in srgb, var(--line) 70%, transparent);
  --stroke-series: 2px;
  --dot-r: 3px;

  /* ---- Opacity ---- */
  --op-muted: 0.7;
  --op-dim: 0.6;
  --op-hidden: 0.62;

  /* ================================================================
     §9.2 — tokens this app needs and the workout tracker does not.
     Every one is derived from the palette above. No new hues.
     ================================================================ */

  /* ---- Target state (§6.3) ----
     Neutral and informational. There is no scoring here, no failure state, and
     no streak — under-target and over-target are both just facts about a day.

     The prominence ordering is deliberate and comes straight from §6.3: an
     exceeded Upper Level is the only direction carrying real risk, so it is the
     only one that gets the alarm colour. A missed minimum is the quietest state
     in the set — it uses --text-dim, the same tone as any other secondary
     text, because "you have not eaten this yet today" is not a warning.

     §9.3: every one of these must ALSO be carried by something other than
     colour — a glyph, a label, or bar position. Colour alone is never the
     signal. */
  --state-under: var(--text-dim);
  --state-in-range: var(--good);
  --state-over: var(--warn);
  --state-upper-limit: var(--bad);
  /* As with --bad-text: the raw --bad is under the 4.5:1 text floor, so text in
     the upper-limit role uses the lifted variant. Fills, bars and borders use
     the raw token above. */
  --state-upper-limit-text: var(--bad-text);

  /* ---- Partial data (§3.3) ----
     "Some foods in this total had no value for this nutrient." Distinct from
     --state-under (a known value that is low) and from --state-over (a warning
     about intake). This is a statement about the DATA, not the diet, so it sits
     between the warn hue and the dim neutral rather than on either. */
  --data-partial: color-mix(in srgb, var(--warn) 55%, var(--text-dim));

  /* ---- Custom food marker (§7.3) ----
     A user-entered food is visually distinguishable from a sourced one. Uses
     the same accent-lifted-toward-text derivation the workout tracker uses for
     its highest tier band, which was already validated as readable body text. */
  --source-custom: color-mix(in srgb, var(--accent) 42%, var(--text));

  /* ---- Chart series (§9.2, minimum 6) ----
     Derived from the existing four hues, because §9.1 forbids introducing an
     unrelated hue for the sake of a wider categorical scale. §9.1a suspends the
     "distinguishable in both themes" clause for v1, so these are chosen for the
     dark palette only.

     PROVISIONAL — must be validated at step 15 against a real chart.

     These six were not picked by eye. Every colour derivable from the palette
     was enumerated, those below the §9.3 3:1 floor were discarded, and the rest
     were ranked on worst-case pair separation, scoring lightness ratio and hue
     distance together. This set is that measured optimum: the closest pair is
     --chart-4 against --chart-6 at 1.59:1 lightness — two reds at clearly
     different brightness, which stays legible.

     The floor is measured against --surface-alt, not --bg. A chart sits on a
     card, not on the page, and --surface-alt is the lightest ground and so the
     hardest case: the first candidate for --chart-6 cleared 3.14:1 on --bg and
     failed at 2.57:1 on --surface-alt. An earlier attempt also paired a lifted
     red with --text-dim at 1.02:1 — two colours of effectively identical
     brightness separated only by red chroma, which is exactly the pair that
     disappears for a red-weak viewer. Anything replacing these values must be
     re-measured, not eyeballed; test/contrast.test.js holds both floors.

     KNOWN LIMITATION: a four-hue palette cannot yield six strongly distinct
     categorical colours, and this set leans on lightness more than hue. Charts
     must therefore carry series identity by more than colour — direct
     labelling, dash pattern, or marker shape — which §9.3 requires regardless.
     Raised with the planning instance; see PROGRESS.md spec issue F.

     Note for step 15: --chart-6 is raw --bad, the same value as
     --state-upper-limit. On a nutrient chart carrying a target line (§7.7) that
     is ambiguous, so --chart-6 is ordered last and should be the first series
     dropped if fewer are needed. */
  --chart-1: color-mix(in srgb, var(--accent) 42%, var(--text)); /* pale orange */
  --chart-2: var(--good); /* green */
  --chart-3: var(--warn); /* amber */
  --chart-4: var(--bad-text); /* red */
  --chart-5: color-mix(in srgb, var(--good) 55%, var(--text)); /* pale green */
  --chart-6: var(--bad); /* dark red — see note above */

  /* ---- Shell ----
     The bottom nav's height, so main content can reserve room for it. The
     workout tracker's equivalent (--header-h) is not mirrored: it is a measured
     value for a header carrying nav links, and this app's nav sits at the
     bottom instead. See PROGRESS.md step 1 for why the layouts differ. */
  --nav-h: 56px;
}

/* Desktop is the denser context — same tokens, tighter cards and names. */
@media (min-width: 700px) {
  :root {
    --card-pad: var(--s-3);
    --fs-name: 1rem;
    --fs-input: 1rem;
  }
}

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

html,
body {
  margin: 0;
  /* §9.3: no horizontal scroll at 360px.
     `clip`, NOT `hidden` — `overflow-x: hidden` forces overflow-y to `auto`,
     which makes html/body a scroll container and silently kills every
     `position: sticky` descendant. `clip` leaves overflow-y `visible`, so
     sticky survives. Carried forward from the workout tracker build
     (CLAUDE.md §4) — do not rediscover this. */
  overflow-x: clip;
}

/* Base font goes on body, NOT html: setting it on the root element rescales
   every rem, which would shrink the 2rem headline totals. */
body {
  background: var(--bg);
  color: var(--text);
  font: var(--fs-body) / var(--lh-body) var(--font-ui);
}

/* Numerals are the content of this app — tabular everywhere they appear, so
   columns align and values don't jitter as they change. */
.num,
input[type='number'] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

:focus-visible {
  outline: var(--focus) solid var(--accent);
  outline-offset: 2px;
}

/* §9.3: motion is functional only. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
