/* ============================================================================
   CORE — identical in every product. Do not edit per product.
   ============================================================================
   Everything metric is shared; everything hued is not. A game and a tax app
   should not look the same — they should look like they were built by the same
   people. So the type scale, the radii, the shadow geometry, the frame and the
   token NAMES are fixed here, and each product supplies only colour, below the
   PALETTE banner.

   The naming rule, which is the whole naming policy:

       the CSS variable is the Tailwind token path with dots as dashes.
       line.strong <-> --line-strong.  ink.2 <-> --ink-2.  brand.500 <-> --brand-500.

   It ends every naming argument and it is checkable by eye. Before it, sets
   broke it in both directions at once — Tailwind key `line` with CSS var
   `--border`, key `ink` with `--text-primary` — and neither name could be
   trusted to find the other.
   ============================================================================ */

:root {
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Type. The first four steps were byte-identical across every set that had
     a token layer — the strongest existing convention in the tree. */
  --fs-2xs: 10px;     --lh-2xs: 1.3;
  --fs-xs: 11.5px;    --lh-xs: 1.4;
  --fs-sm: 13px;      --lh-sm: 1.45;
  --fs-base: 14px;    --lh-base: 1.5;
  --fs-body: 16px;    --lh-body: 1.5;
  --fs-lead: 18px;    --lh-lead: 1.45;
  --fs-h3: 22px;      --lh-h3: 1.25;    --ls-h3: -0.01em;
  --fs-h2: 28px;      --lh-h2: 1.15;    --ls-h2: -0.02em;
  --fs-h1: 36px;      --lh-h1: 1.08;    --ls-h1: -0.02em;
  --fs-display: 46px; --lh-display: 1.02; --ls-display: -0.03em;

  --tracking-label: 0.18em;

  --radius-sm: 8px;
  --radius: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-2xl: 24px;
  --radius-3xl: 28px;
  --radius-pill: 999px;
  --radius-phone: 40px;

  /* Geometry is shared; tint and strength are per product, because a dark
     palette needs far more alpha to read at all. --shadow-tint is an `r g b`
     triple, declared in the palette block. */
  --shadow-a1: 0.05;
  --shadow-a2: 0.07;
  --shadow-a3: 0.18;
  --shadow-sm: 0 1px 2px rgb(var(--shadow-tint) / var(--shadow-a1));
  --shadow:    0 4px 14px rgb(var(--shadow-tint) / var(--shadow-a2));
  --shadow-lg: 0 14px 34px -10px rgb(var(--shadow-tint) / var(--shadow-a3));
  --shadow-phone: 0 0 0 1px var(--line-strong), 0 30px 60px -20px rgb(var(--shadow-tint) / var(--shadow-a3));

  --frame-w: 390px;
  --frame-h: 844px;
}

html, body {
  font-family: var(--font-sans);
  background: var(--page);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  margin: 0;
}

.mono { font-family: var(--font-mono); }
.num  { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; letter-spacing: -0.01em; }

/* ---- Phone frame — a device, not a utility --------------------------------
   390 wide is the spec and never changes. The HEIGHT is clamped to whatever is
   left under the chrome bar, and the content scrolls inside the frame the way
   it scrolls on the phone.

   This replaces a transform: scale() approach that looked right and was not —
   a transform does not change layout height, so the frame still occupied its
   full 844px, and the page scrolled to reveal a frame that was already drawn
   smaller. --chrome-h is measured by _chrome.js, because the bar wraps to two
   rows on a narrow window and a guessed constant is wrong exactly when it
   matters. */
.phone {
  width: var(--frame-w);
  max-width: 100%;
  height: min(var(--frame-h), calc(100dvh - var(--chrome-h, 0px) - var(--stage-pad, 32px)));
  background: var(--frame);
  /* The frame re-resolves ink itself. Without this, text with no colour class
     inherits body's COMPUTED colour, which was resolved in the light scope —
     so a dark frame kept black body text while everything token-driven around
     it turned white. A variable remap only reaches declarations that read the
     variable; inheritance carries the old value, not the new name. */
  color: var(--ink);
  display: flex;
  flex-direction: column;
  position: relative;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: var(--radius-phone);
  box-shadow: var(--shadow-phone);
}
.phone::-webkit-scrollbar { width: 0; }
/* A footer pinned to the bottom of the DEVICE, not the bottom of the content:
   sticky keeps it in view while the content above it scrolls, which is what a
   tab bar or a primary action actually does on the phone. */
.phone > .pin-bottom { margin-top: auto; position: sticky; bottom: 0; }

/* The stage is exactly the frame plus its padding, so the page itself never
   scrolls on a screen that already fits. */
.stage {
  display: grid;
  place-items: start center;
  /* The same variable the frame subtracts from its own height, so the two
     cannot disagree. A literal 16px here left the frame 32px shorter than the
     space it was given. */
  padding: calc(var(--stage-pad, 32px) / 2) 0;
  height: 100dvh;
  box-sizing: border-box;
}

/* ?bare is what the screenshots sheet embeds and what you capture a single
   screen with. The gutter and the device rounding are framing for a screen
   being *looked at*; inside a 390x844 tile they are a white margin and four
   clipped corners, and the frame no longer fills the tile it was sized for. */
[data-bare] .stage { padding: 0; }
[data-bare] .phone {
  border-radius: 0;
  box-shadow: none;
  max-width: none;
}

/* A theme scope is a token remap, never a second set of utilities. A utility
   written against a variable follows whichever scope it lands in, so the theme
   toggle changes no markup and nothing can be dark in one place and light in
   another. */
/* A screen with a modal over it does not scroll. `position: absolute; inset: 0`
   resolves against the frame's padding box, and in a scroll container that is
   the visible box pinned to the top of the scrollable area — so scrolling the
   frame slid the backdrop off the screen and left the card floating over
   nothing. A modal blocks the content underneath in the app too. */
.phone--modal { overflow: hidden; }

.phone--page   { background: var(--page); }
.phone--sunken { background: var(--sunken); }

/* The device status bar. The OS draws this, not the app — but the app pads for
   it (pt-safe), so a wireframe that omits it puts every screen ~44px too high
   and makes the first row look like it has more air than it will. */
.statusbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
  padding: 12px 20px 2px;
  font-size: 13px;
  color: var(--ink);
}
.statusbar-time { font-weight: 600; letter-spacing: 0.02em; }
.statusbar-icons { display: flex; align-items: center; gap: 6px; }

/* ---- Dev chrome — injected by _chrome.js, not part of the product ---------
   Everything around the screen and nothing in it. One bar, same controls, same
   order, in every product.

   And the same COLOURS in every product. These are declared here rather than
   read from the palette because the bar is tooling: reading --brand-500 made
   the active chip blue in tic-tac-toe and navy in charades, so the same
   control looked like a different control depending on which set you opened,
   and the bar read as part of the product rather than as the thing holding
   it. Nothing below this line may reference a product token. */
:root {
  --chrome-bg:      #ffffff;
  --chrome-surface: #ffffff;
  --chrome-line:    #d4d4d8;   /* zinc-300 */
  --chrome-ink:     #3f3f46;   /* zinc-700 */
  --chrome-ink-2:   #71717a;   /* zinc-500 */
  --chrome-ink-3:   #a1a1aa;   /* zinc-400 */
  --chrome-active:  #18181b;   /* zinc-900 */
  --chrome-soft:    #f4f4f5;   /* zinc-100 */
}

.chrome {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid var(--chrome-line);
  background: color-mix(in srgb, var(--chrome-bg) 92%, transparent);
  backdrop-filter: blur(8px);
  padding: 7px 16px;
}
.chrome-row {
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  font-size: 11px;
}
.chrome-group { display: flex; align-items: center; gap: 6px; }
/* The middle group scrolls; the row does not wrap. A wrapped row is two
   stacked bars, which reads as two bars — chitragupt's settings flow has
   twenty-three screens and pushed the bar onto a second line on a full-width
   window. Scrolling the names keeps the row one line at every width, so the
   nav on the left and the display controls on the right never move, and
   --chrome-h stays put instead of doubling. min-width:0 is what actually lets
   a flex child shrink below its content and scroll. */
.chrome-mid {
  flex: 0 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.chrome-mid::-webkit-scrollbar { display: none; }
.chrome-left, .chrome-right { flex: 0 0 auto; }
/* Navigation left, settings right, and a gap between them that grows. */
.chrome-right { margin-left: auto; }
/* There is no .chrome-label. The breadcrumb it styled — `mobile · /game ›` —
   said what the surface toggle on the right already says and what the flow
   tint in the chart already says, and it sat between the nav chips and the
   screen chips pushing the two apart for no information. */
.chrome-chip {
  border-radius: 6px;
  border: 1px solid var(--chrome-line);
  background: var(--chrome-surface);
  padding: 2px 8px;
  color: var(--chrome-ink);
  text-decoration: none;
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
}
.chrome-chip:hover { border-color: var(--chrome-active); }
.chrome-chip.is-current {
  border-color: var(--chrome-active);
  background: var(--chrome-soft);
  color: var(--chrome-active);
  font-weight: 600;
}

/* Icon controls ------------------------------------------------------------
   The controls that mean the same thing on every page — all routes,
   screenshots, back, next, theme, surface, notes — are icons. They are lucide,
   the estate's one icon set, asked for by name: nothing here carries a copy of
   any path data, and there is no second icon library on the page.

   The chip keeps its label in `title` and `aria-label`, so hovering still says
   which control this is. An icon whose meaning you have to click to learn is
   worse than the word it replaced. */
.chrome-chip--icon {
  width: 24px;
  height: 22px;
  padding: 0;
  display: inline-grid;
  place-items: center;
}
.chrome-ico { display: grid; place-items: center; line-height: 0; }
.chrome svg { width: 14px; height: 14px; }
/* The fallback when the lucide CDN did not load: the placeholder renders its
   own label instead of an empty square. Wider than an icon slot, so the chip
   grows to fit rather than clipping the word. */
.chrome-ico--text {
  font-size: 9px;
  font-weight: 700;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 0 4px;
  white-space: nowrap;
}
.chrome-chip--icon:has(.chrome-ico--text) { width: auto; padding: 0 4px; }
.seg-chip--icon:has(.chrome-ico--text) { padding: 2px 6px; }

/* Segmented controls: surface, theme, and this screen's states. Always
   rendered in the same slot — a control that appears only sometimes moves
   everything beside it the day a product gains a surface or a dark scope. */
.seg {
  display: inline-flex;
  border: 1px solid var(--chrome-line);
  border-radius: 999px;
  overflow: hidden;
}
.seg--states { margin-left: 4px; }
.seg-chip {
  padding: 2px 10px;
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--chrome-ink-2);
  background: var(--chrome-surface);
  text-decoration: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
}
.seg-chip + .seg-chip { border-left: 1px solid var(--chrome-line); }
.seg-chip:hover { color: var(--chrome-active); }
.seg-chip.is-current { background: var(--chrome-active); color: #fff; }
.seg-chip--icon { padding: 2px 8px; display: inline-grid; place-items: center; }

/* A surface that is not drawn is struck through, never omitted: a missing
   thing has to look missing, or its absence reads as "no such thing". */
.seg-chip.is-absent { color: var(--chrome-ink-3); text-decoration: line-through; cursor: default; }
/* line-through does not cross an <svg>, so an icon half draws its own strike:
   a hairline at the same angle, in the same colour, so a struck icon and a
   struck word read as the same statement. It applies to a dead back/next as
   well as to an undrawn theme or surface — §4 says the ends of the set are
   struck through, and dimming alone did not say it. */
.seg-chip--icon.is-absent,
.chrome-chip--icon.is-absent { position: relative; text-decoration: none; }
.seg-chip--icon.is-absent::after,
.chrome-chip--icon.is-absent::after {
  content: "";
  position: absolute;
  left: 5px;
  right: 5px;
  top: 50%;
  border-top: 1px solid currentColor;
  transform: rotate(-20deg);
}
/* A dead back/next at the ends of the set: shown, not hidden, so the bar does
   not reflow as you step through it. */
.chrome-chip.is-absent { color: var(--chrome-ink-3); border-color: var(--chrome-line); cursor: default; }
.chrome-chip.is-absent:hover { border-color: var(--chrome-line); }

/* ---- Notes drawer --------------------------------------------------------
   The rationale lives in the wiki and only in the wiki. This renders it beside
   the screen on demand, collapsed by default — the screen is the thing under
   review, and an essay next to it competes for the eye.

   Dev chrome, so the same fixed colours as the bar. */
.notes {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(400px, 92vw);
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--chrome-surface);
  border-left: 1px solid var(--chrome-line);
  box-shadow: -12px 0 32px rgb(24 24 27 / 0.12);
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--chrome-ink);
}
/* A panel that covers a third of the screen needs a way out that is not
   "find the button you opened it with", which the bar may have scrolled. */
.notes-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--chrome-line);
  flex: none;
}
.notes-title {
  flex: 1;
  margin: 0;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-weight: 700;
  color: var(--chrome-ink-2);
}
.notes-close {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid var(--chrome-line);
  border-radius: 7px;
  background: var(--chrome-surface);
  color: var(--chrome-ink);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.notes-close:hover { background: var(--chrome-soft); border-color: var(--chrome-active); }
.notes-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 18px 28px; }
.notes-body h3, .notes-body h4, .notes-body h5 {
  font-size: 13px; color: var(--chrome-active); margin: 16px 0 6px;
}
.notes-body h3:first-child, .notes-body h4:first-child { margin-top: 0; }
.notes-body p { margin: 0 0 10px; }
.notes-body ul { margin: 0 0 10px; padding-left: 18px; }
.notes-body li { margin-bottom: 4px; }
.notes-body code {
  font-family: var(--font-mono); font-size: 11.5px;
  background: var(--chrome-soft); padding: 1px 4px; border-radius: 4px;
}
.notes-body a { color: var(--chrome-active); }

/* The info button lives at the far right of the bar, past the display
   controls: it is about the page you are on, and it is the only control that
   opens something over the screen rather than changing the screen. */
/* The notes button is a .chrome-chip--icon like the rest; it only differs in
   being round, because it is the one control that opens something OVER the
   screen rather than changing which screen you are looking at. */
.chrome-info { border-radius: 999px; margin-left: 2px; }
.chrome-info.is-current { background: var(--chrome-active); border-color: var(--chrome-active); color: #fff; }
/* A screen with no wiki page still renders the button, struck out, for the
   same reason a missing theme is struck out rather than omitted: the gap has
   to look like a gap. */
.chrome-info.is-absent { color: var(--chrome-ink-3); cursor: default; }

/* There is no .screen-layout and no .rationale any more. One screen per file:
   a second frame in a file has no URL, cannot appear in routes.js, and cannot
   be linked to. Themes are the toggle in the chrome bar, states are their own
   files, and the reasoning is in the wiki behind the notes drawer. */

/* ---- The flow chart ------------------------------------------------------
   The gallery is this and nothing else: no title, no description, no tables.
   React Flow draws it; the node positions come from routes.js. */
/* The chart fills what is left under the bar. _chrome.js measures the bar and
   publishes --chrome-h, so this stays right when it wraps to two rows. */
.chart-page { height: 100dvh; display: flex; flex-direction: column; }
.chart-canvas { flex: 1; min-height: 0; background: var(--page); }

/* React Flow paints its own node chrome; this replaces it rather than sitting
   on top, so the nodes look like the rest of the estate. */
.react-flow__node.fnode {
  width: auto;
  padding: 10px 14px 10px 12px;
  border: 1px solid var(--line);
  /* The flow's colour, on the edge a reader's eye follows down a column. */
  border-left: 4px solid var(--line-strong);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  font-family: var(--font-sans);
  opacity: 0.88;
  display: flex;
  align-items: center;
  gap: 12px;
}
.react-flow__node.fnode:hover { border-color: var(--brand-500); opacity: 1; }
/* The normal way through the product is the thing a flow chart has to answer
   first, so the screens on it are the ones that read as solid. */
.react-flow__node.fnode.is-main { opacity: 1; box-shadow: var(--shadow); }
/* The two ends say so: where a session starts, and where it finishes. */
.react-flow__node.fnode.is-start { box-shadow: 0 0 0 2px var(--brand-500), var(--shadow); }
.react-flow__node.fnode.is-end   { box-shadow: 0 0 0 2px var(--success), var(--shadow); }
/* Hovering a screen isolates its own paths: everything it does not touch fades
   back. With thirty edges on one chart this is the difference between tracing
   a route and guessing at one. Declared after .is-main deliberately — same
   specificity, and the dim has to win over the spine's own opacity. */
.react-flow__node.fnode.is-dim { opacity: 0.16; }
.react-flow__node.fnode.is-lit {
  opacity: 1;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 2px var(--brand-500), var(--shadow);
}
/* The screen whose routes are being traced, as against the screens that lead
   to it — otherwise "how do I reach X" lights up a dozen boxes and X is not
   one of them to look at. */
.react-flow__node.fnode.is-sel {
  border-color: var(--brand-700);
  background: var(--brand-50);
  box-shadow: 0 0 0 3px var(--brand-500), var(--shadow-lg);
}
.fnode-label { display: flex; flex-direction: column; gap: 2px; text-align: left; }
/* The box asks the chart a question; only this opens the screen. Without a
   separate control there was no way to interrogate a node without leaving the
   page you were interrogating it from. */
.fnode-go {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-left: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--frame);
  color: var(--ink-3);
}
.fnode-go:hover { background: var(--brand-500); border-color: var(--brand-500); color: #fff; }
.fname { font-size: 13px; font-weight: 700; color: var(--ink); line-height: 1.2; }
.fflow { font-size: 9.5px; color: var(--ink-3); letter-spacing: 0.02em; }
.react-flow__handle { opacity: 0; }

/* The key. Six swatches, so the colours mean something without a paragraph. */
.react-flow__panel.fkey {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  max-width: 260px;
  margin: 12px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  box-shadow: var(--shadow-sm);
  font-size: 10px;
  color: var(--ink-2);
}
.fkey-item { display: inline-flex; align-items: center; gap: 5px; }
.fkey-dot { width: 9px; height: 3px; border-radius: 2px; }
.fkey-line { width: 14px; height: 0; border-top: 2px solid var(--brand-500); }
.fkey-toggle {
  flex: 0 0 100%;
  margin-bottom: 2px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font: inherit;
  font-weight: 700;
  color: var(--ink-2);
  cursor: pointer;
}
.fkey-toggle.is-on { background: var(--brand-500); border-color: var(--brand-500); color: #fff; }
.fkey-hint { flex: 0 0 100%; margin-top: 2px; color: var(--ink-4); font-size: 9.5px; }

.react-flow__controls-button {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  fill: var(--ink-2);
}

/* ---- Gallery -------------------------------------------------------------
   The route table IS the screens inventory. A second inventory in Markdown is
   a copy that drifts the day a screen is added. */
.route-table { width: 100%; font-size: 13px; border-collapse: collapse; }
.route-table th {
  text-align: left;
  padding: 9px 13px;
  font-size: 10px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--line);
  background: var(--muted);
}
.route-table td {
  padding: 9px 13px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.route-table tr:last-child td { border-bottom: none; }

.section-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--ink-3);
  font-weight: 700;
}

/* Status of a screen, on its gallery row and nowhere else. A screen must not
   move sections the day it is approved. */
.status { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 6px; white-space: nowrap; }
.status--built    { background: var(--success-bg); color: var(--success-fg); }
.status--approved { background: var(--muted);      color: var(--ink-2); }
.status--review   { background: var(--warn-bg);    color: var(--warn-fg); }

.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }

/* lucide injects an <svg> in place of the <i> and inherits currentColor. */
.lucide { display: inline-block; vertical-align: -0.125em; stroke-width: 2; }

/* ---- a11y baseline -------------------------------------------------------- */
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* ============================================================================
   PALETTE — AskCal. The only block that differs between products.
   ============================================================================
   RESERVED, and reserved hard:

   1. ACCENT · amber is the ASK. It appears when the app is asking the user a
      question and at no other time. Not premium, not warning, not decoration.
      It is the one signature colour this product has and spending it elsewhere
      flattens the only signal that separates "we know" from "we need you".

   2. CONFIDENCE · high / med / low are the product's whole idea. A number is
      never rendered without one. The camera can read a bread roll; it cannot
      read the butter. These three tints say which is which, so they may not be
      borrowed for status, for emphasis, or for anything else.

   3. MACROS · protein / carbs / fat are fixed hues used by the ring, the bars
      and every legend. Once learned they must never swap.

   Amber is the accent and green is the brand. `ask` was this set's own role
   name before the estate settled on `brand` and `accent`; same colour, the
   name every other set uses.

   Every value below is the one the shipped app renders — apps/mobile/src/
   global.css is the fact and this block mirrors it token for token.
   ============================================================================ */

:root {
  /* rgba(22,32,28,...) — the ink this product shadows with. */
  --shadow-tint: 22 32 28;

  /* Brand · deep green — primary actions, on-track states, the calorie ring. */
  --brand-50:  #e9f7f0;
  --brand-100: #cdedde;
  --brand-200: #9adcbe;
  --brand-400: #3faa79;
  --brand-500: #1f8b5c;
  --brand-700: #136141;
  --brand-900: #0a3a27;

  /* Accent · amber — RESERVED for the ask. See the banner above.
     Six steps and not eight: these are the six the app declares, and a step
     nobody has picked would be a value invented here rather than mirrored. */
  --accent-50:  #fff8e8;
  --accent-100: #ffefc6;
  --accent-200: #ffdd8c;
  --accent-400: #f7b731;
  --accent-500: #e89c0d;
  --accent-700: #9c650a;

  /* Text */
  --ink:   #16201c;
  --ink-2: #4d5b55;
  --ink-3: #7d8b85;
  --ink-4: #adb8b3;
  --ink-inverse: #ffffff;

  /* Surfaces */
  --page:    #f4f6f5;
  --frame:   #ffffff;
  --surface: #ffffff;
  --muted:   #fafbfa;
  --sunken:  #eceff0;

  /* Borders */
  --line:        #e4e8e6;
  --line-strong: #d0d6d3;

  /* Status. `success` is the brand green and its 50 tint: this product's
     success IS on-track, and a second green beside the ring would read as a
     different meaning rather than the same one. */
  --success: #1f8b5c; --success-bg: #e9f7f0; --success-fg: #136141;
  --warn:    #e89c0d; --warn-bg:    #fff8e8; --warn-fg:    #9c650a;
  --danger:  #e5484d; --danger-bg:  #ffefef; --danger-fg:  #c62a2f;
  --danger-900: #7f1d21;   /* the danger toast, which is a dark surface */
  --info:    #0091ff; --info-bg:    #e6f4ff; --info-fg:    #006adc;

  /* Product · CONFIDENCE — RESERVED, see the banner above. */
  --conf-high: #1f8b5c;  --conf-high-bg: #e9f7f0;
  --conf-med:  #e89c0d;  --conf-med-bg:  #fff8e8;
  --conf-low:  #e5484d;  --conf-low-bg:  #ffefef;

  /* Product · MACROS — RESERVED, see the banner above. */
  --protein: #7c5cf0;  --protein-bg: #f1edff;
  --carbs:   #f7b731;  --carbs-bg:   #fff8e8;
  --fat:     #e5757a;  --fat-bg:     #ffeff0;

  /* Product · CAMERA — the viewfinder ground. Darker and cooler than
     brand-900: it sits behind a live photo and has to disappear. */
  --camera: #0e1412;
  /* The scrim an `r g b` triple, so a modal backdrop and the viewfinder
     vignette can take their own alpha from one colour. */
  --scrim: 10 20 16;

  /* The capture button's lift. Brand-tinted rather than neutral because it is
     the one control that floats above the tab bar. */
  --shadow-fab: 0 8px 20px -4px rgb(31 139 92 / 0.5);
}

/* ---------------------------------------------------------------------------
   ONBOARDING · warm paper — a token remap, not a second vocabulary.

   The four onboarding screens are drawn on warm paper rather than the app's
   green-cast grey, and on a deeper emerald than the in-app brand: a siren red
   and a neon amber both glow against #faf9f7. This is a SCOPE, the same
   mechanism a dark theme uses — the role names do not change, only the values
   behind them, so `bg-brand-500` still means "this is the action" and every
   component class works unaltered inside it.

   It used to be a separate stylesheet, onboarding/onboarding.css, loaded by
   five screens and by nothing else. A per-flow stylesheet has no slot in the
   layout and no way to be checked, and its own `--accent-*` meant emerald here
   and amber everywhere else — the one thing a role name may never do.

   Under review: if the direction is signed off, these values replace the ones
   above rather than scoping beside them.
   --------------------------------------------------------------------------- */
.paper {
  --page:    #faf9f7;   /* paper */
  --surface: #ffffff;
  --frame:   #faf9f7;
  --sunken:  #f2f0ec;   /* the well */
  --muted:   #f2f0ec;

  --line:        #eae7e1;
  --line-strong: #dbd7cf;

  --ink:   #17181a;
  --ink-2: #6b6f76;
  --ink-3: #9a9ea6;
  --ink-4: #bcbfc4;

  /* The deep emerald. brand-400 is deliberately not remapped: no onboarding
     screen uses it, and a value nobody picked would be invented here. */
  --brand-50:  #e8f4ee;
  --brand-100: #cfe7db;
  --brand-200: #7cbda1;
  --brand-500: #0f7a52;
  --brand-700: #0a5d3e;
  --brand-900: #072f20;

  /* The same three signals, deepened for paper. seen = high, asked = med,
     unknown = low — the wording differs on these screens, the concept does not. */
  --conf-high: #0f7a52;  --conf-high-bg: #e8f4ee;
  --conf-med:  #9a6c0b;  --conf-med-bg:  #f7efdf;
  --conf-low:  #b4543f;  --conf-low-bg:  #f8ece7;

  /* Macros, deepened to sit on paper rather than buzz on it. Onboarding shows
     them as caps only, never as fills. */
  --protein: #5c4bc9;
  --carbs:   #b8850f;
  --fat:     #c1636a;

  /* The one accent step onboarding spends: the edge of the "we are asking you"
     card. The in-app amber glows against paper, so the scope deepens it the
     same way it deepens the three signals. */
  --accent-200: #ecdcb8;

  --shadow-tint: 23 24 26;
  --shadow-a2: 0.14;
  --shadow-fab: 0 8px 18px -8px rgb(15 122 82 / 0.55);

  background: var(--page);
  color: var(--ink);
}

/* ============================================================================
   COMPONENTS — AskCal. This product's own classes, and nothing shared.
   ============================================================================
   Adding a class means declaring it here in the same edit. There is no lint,
   and a class that resolves to nothing does not throw — it quietly does not
   paint, which is how `text-2xs` came to be used 186 times across three apps
   while declared in exactly one.
   ============================================================================ */

/* THE CAMERA FRAME, which was used by two screens and declared by none.

   scan/camera.html and food/barcode.html have carried `phone--camera` since the
   set was written; it resolved to nothing, and both got away with it because
   each paints its own dark area with an absolutely positioned viewfinder and an
   inline hex. Anything OUTSIDE that block — a permission refusal, a message —
   landed as dark text on the default page white.

   It is here rather than in CORE because askcal is the only product in the
   estate with a lens. `--camera` was already declared and waiting for it. */
.phone--camera { background: var(--camera); color: var(--ink-inverse); }

/* The status bar over it. Same omission, same two screens: the OS draws this
   white on a camera view and the wireframe was drawing it in ink. */
.statusbar--on-dark { color: var(--ink-inverse); }
.statusbar--on-dark .statusbar-time { color: var(--ink-inverse); }

/* Reviewer-only branch rows are set chrome, not app UI, so `?bare` drops them:
   one shipped into a store screenshot. Here, not in CORE — only askcal has them. */
[data-bare] .branches { display: none; }

/* App header */
.appheader {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 20px 12px;
}
.appheader-left { display: flex; align-items: center; gap: 10px; }
.iconbtn {
  width: 40px; height: 40px; border-radius: 999px;
  background: var(--sunken);
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-2); border: none; cursor: pointer;
  text-decoration: none; transition: opacity 0.15s;
}
.iconbtn:active { opacity: 0.7; }
.iconbtn--on-dark { background: rgb(255 255 255 / 0.15); color: var(--ink-inverse); }

/* Sub-header — back + title + optional action */
.subheader {
  display: grid; grid-template-columns: 40px 1fr 40px;
  align-items: center; gap: 8px; padding: 4px 16px 12px;
}
.subheader-title { text-align: center; font-weight: 800; font-size: 15px; }
.subheader-sub { text-align: center; font-size: 11px; color: var(--ink-3); margin-top: 1px; }
.backbtn {
  width: 40px; height: 40px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink); text-decoration: none; background: transparent; border: none;
}

/* Sticky bottom action area */
.sticky-bottom {
  position: sticky;
  bottom: 0;
  margin-top: auto;
  background: color-mix(in srgb, var(--frame) 98%, transparent);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 8px));
}
.sticky-bottom--dark {
  background: rgb(var(--scrim) / 0.9); border-top-color: rgb(255 255 255 / 0.1);
}

/* Bottom tab bar */
.tabbar {
  position: sticky;
  bottom: 0;
  margin-top: auto;
  background: color-mix(in srgb, var(--frame) 98%, transparent);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
}
.tabbar-row { display: flex; align-items: flex-end; }
.tab {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 4px; color: var(--ink-4); text-decoration: none;
  font-size: 10px; font-weight: 600; transition: color 0.15s;
}
.tab.is-active { color: var(--brand-700); font-weight: 800; }
.tab-icon { width: 22px; height: 22px; }
/* Centre capture button — the app's primary verb */
.tab-fab { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 4px 10px; text-decoration: none; }
.tab-fab-inner {
  width: 54px; height: 54px; border-radius: 999px;
  background: var(--brand-500); color: var(--ink-inverse);
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-fab);
  margin-top: -18px; border: 4px solid var(--frame);
}
.tab-fab-label { font-size: 10px; font-weight: 800; color: var(--brand-700); }
.tabbar-handle { display: flex; align-items: center; justify-content: center; padding: 4px 0 8px; }
.tabbar-handle-bar { height: 4px; width: 128px; border-radius: 999px; background: var(--line-strong); }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 20px; border-radius: var(--radius-2xl);
  font-weight: 800; font-size: 15px; letter-spacing: 0.01em;
  border: none; cursor: pointer; text-decoration: none; width: 100%;
  transition: opacity 0.15s, transform 0.05s;
}
.btn:active { opacity: 0.85; transform: scale(0.99); }
.btn-primary   { background: var(--brand-500); color: var(--ink-inverse); }
.btn-secondary { background: var(--surface); color: var(--ink); border: 1px solid var(--line-strong); }
.btn-ask       { background: var(--accent-500); color: var(--ink-inverse); }
.btn-dark      { background: var(--brand-900); color: var(--ink-inverse); }
.btn-ghost     { background: transparent; color: var(--ink-2); font-weight: 700; }
.btn-danger    { background: var(--surface); color: var(--danger-fg); border: 1px solid var(--line-strong); }
.btn-sm        { padding: 10px 14px; font-size: 13px; border-radius: var(--radius-lg); }

/* Cards */
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 16px;
}
.card--muted  { background: var(--muted); }
.card--sunken { background: var(--sunken); border-color: transparent; }
.card--brand  { background: var(--brand-50); border-color: var(--brand-100); }
.card--ask    { background: var(--accent-50); border-color: var(--accent-200); }
.card--interactive { cursor: pointer; transition: border-color 0.15s, transform 0.05s; }
.card--interactive:active { transform: scale(0.995); }

/* Chips */
.chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 6px;
  font-size: 10.5px; line-height: 1.3; font-weight: 700;
}
.chip-lg      { padding: 5px 10px; font-size: 11.5px; }
.chip-brand   { background: var(--brand-50);  color: var(--brand-700); }
.chip-ask     { background: var(--accent-100);   color: var(--accent-700); }
.chip-neutral { background: var(--sunken);    color: var(--ink-2); }
.chip-danger  { background: var(--danger-bg); color: var(--danger-fg); }
.chip-info    { background: var(--info-bg);   color: var(--info-fg); }

/* Confidence — the product's core signal.
   high: the camera established it. med: reasonable inference.
   low: the camera cannot know, and we say so rather than pretending. */
/* A confidence mark, not a label.

   This was a pill of uppercase text — SEEN, GUESSED, UNKNOWN — repeated on
   every row of every plate. Four rows meant four shouted words competing with
   the food's name and its calories, which are what the eye is actually there
   for, and the loudest of the three was the one that said everything was fine.

   It is an icon now: 18px, the glyph carrying the meaning and the colour
   reinforcing it. The word survives in `title` and `aria-label`, so nothing is
   lost to a screen reader or to a long press — the rule is that a number is
   never rendered without its confidence, and it still is not. */
.conf { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 999px; flex: none; }
.conf svg { width: 11px; height: 11px; }
/* Settled marks are quiet; the unresolved one is not.

   With the "Lunch has an unknown" bar and the "One thing still unknown" card
   both gone, this mark is the ONLY thing that says a row needs attention — and
   it is what you tap to fix it. A mark carrying that much weight cannot look
   the same as the two that mean "nothing to do here", so `low` is filled and
   the other two are tinted. */
.conf-high { background: var(--conf-high-bg); color: var(--conf-high); }
.conf-med  { background: var(--conf-med-bg);  color: var(--conf-med); }
.conf-low  { background: var(--conf-low); color: var(--ink-inverse); box-shadow: 0 0 0 3px var(--conf-low-bg); }
.conf-dot { width: 7px; height: 7px; border-radius: 999px; display: inline-block; }
.conf-dot--high { background: var(--conf-high); }
.conf-dot--med  { background: var(--conf-med); }
.conf-dot--low  { background: var(--conf-low); }

/* ---------------------------------------------------------------------------
   The day bar — home/today.html

   Replaces the calorie ring. A ring is a COMPLETION metaphor borrowed from
   fitness: closing it is the win. Eating is not a completion task, and a circle
   cannot draw "over" — 2,480 of 2,272 wraps the arc and reads as 9%.

   The bar's full width IS the target, so the empty tail is the food still
   available. One segment per meal, in the order eaten, sized by calories.
   --------------------------------------------------------------------------- */
.daybar-wrap { position: relative; }
.daybar {
  display: flex; height: 14px; border-radius: 4px;
  background: var(--sunken); overflow: hidden; position: relative;
}
.daybar > i { display: block; height: 100%; }
/* Two meals of the same tint left a sub-pixel seam that read as a gap. A
   deliberate hairline says "meal boundary" instead. */
.daybar > i + i { box-shadow: -1.5px 0 0 var(--surface); }
.daybar-tick {
  position: absolute; top: -3px; bottom: -3px; width: 2px;
  background: var(--ink); border-radius: 2px; right: -1px;
}

/* Colour is the meal's place in the day, not its confidence: pale at
   breakfast, deepest at dinner, so the bar darkens as the day fills. The end
   user has no concept of how sure the estimator was, and does have one of
   breakfast. Confidence stays on the meal rows, as a badge. */
.meal-breakfast { background: var(--brand-200); }
.meal-lunch     { background: var(--brand-400); }
.meal-snack     { background: var(--brand-500); }
.meal-dinner    { background: var(--brand-700); }
.meal-over      { background: var(--danger); }

/* The key names real meals, so it is not a scale anyone has to learn.
   nowrap and a shrunken gap because all four must hold one line: a fourth
   meal wrapping to a second row grew the card 7px at dinner time, which is
   exactly when it should be steady. */
.mealkey {
  display: flex; flex-wrap: nowrap; justify-content: space-between;
  gap: 9px; margin-top: 8px;
}
.mealkey-item {
  display: inline-flex; align-items: center; gap: 5px; min-width: 0;
  font-size: 9px; font-weight: 700; color: var(--ink-3); white-space: nowrap;
}
.mealkey-item b { color: var(--ink-2); font-weight: 800; }
.mealkey-dot { width: 6px; height: 6px; border-radius: 999px; flex: none; }

/* The unit beside the headline figure — "left" or "over". */
.stat-unit {
  font-size: 8.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--ink-3);
}

/* Three macro cells side by side rather than stacked. This is what buys the
   height back against the ring card, and it is the shape an iOS medium widget
   is built to: fixed small box, no scroll, readable at arm's length. */
.macrocell { flex: 1; }
.macrocell-k {
  font-size: 8.5px; font-weight: 900; text-transform: uppercase;
  letter-spacing: 0.08em;
}
.macrocell-v { font-size: 12.5px; font-weight: 900; margin: 2px 0 4px; }

/* Macro bars */
.macro-track { flex: 1; height: 6px; border-radius: 999px; background: var(--sunken); overflow: hidden; }
.macro-track--sm { height: 4px; display: block; }
.macro-fill { height: 100%; border-radius: 999px; }
.macro-dot { width: 8px; height: 8px; border-radius: 999px; }
.m-protein { background: var(--protein); }
.m-carbs   { background: var(--carbs); }
.m-fat     { background: var(--fat); }

/* Component row — one identified part of a meal */
.comp {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.comp--low { border-color: var(--conf-low); background: var(--conf-low-bg); }
.comp-body { flex: 1; min-width: 0; }
.comp-name { font-weight: 800; font-size: 14px; }
.comp-sub  { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
.comp-kcal { font-weight: 900; font-size: 15px; }
.comp-puck {
  width: 38px; height: 38px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center; flex: none;
}

/* Meal list row (the day's log) */
.meal { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
.meal-thumb { width: 48px; height: 48px; border-radius: var(--radius); object-fit: cover; flex: none; background: var(--sunken); display: flex; align-items: center; justify-content: center; }
.meal-body { flex: 1; min-width: 0; }
.meal-title { font-weight: 800; font-size: 14px; }
.meal-sub { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }

/* Big selectable option row — used by the quiz and the clarify screen */
.opt {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 16px; border-radius: var(--radius-lg);
  border: 1.5px solid var(--line-strong); background: var(--surface);
  text-align: left; cursor: pointer; text-decoration: none; color: inherit;
  transition: border-color 0.12s, background 0.12s, transform 0.05s;
}
.opt:active { transform: scale(0.99); }
.opt.is-selected { border-color: var(--brand-500); background: var(--brand-50); }
.opt--ask.is-selected { border-color: var(--accent-500); background: var(--accent-50); }
.opt-title { font-weight: 800; font-size: 15px; }
.opt-sub { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; }
.opt-mark {
  width: 22px; height: 22px; border-radius: 999px; flex: none;
  border: 2px solid var(--line-strong);
  display: flex; align-items: center; justify-content: center;
}
.opt.is-selected .opt-mark { border-color: var(--brand-500); background: var(--brand-500); color: var(--ink-inverse); }
.opt--ask.is-selected .opt-mark { border-color: var(--accent-500); background: var(--accent-500); }

/* Segmented control */
.segmented { display: inline-flex; background: var(--sunken); border-radius: 999px; padding: 3px; gap: 2px; }
.segmented--full { display: flex; width: 100%; }
.segmented > * {
  flex: 1; text-align: center; padding: 8px 14px; border-radius: 999px;
  font-size: 12.5px; font-weight: 700; color: var(--ink-2);
  border: none; background: transparent; cursor: pointer; text-decoration: none;
}
.segmented > .is-active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); font-weight: 800; }

/* The slot and the day/time under a dish title. Both open a sheet, so both have
   to look like controls: they were plain text and nothing said they were live. */
.whenbtn {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 12px; color: var(--ink-3); background: none; border: 0;
  padding: 2px 0; cursor: pointer; text-transform: capitalize;
  border-bottom: 1px dashed var(--line-strong);
}
.whenbtn--on { color: var(--brand-700); font-weight: 700; border-bottom-color: var(--brand-200); }

/* List row (settings + generic) */
.row { display: flex; align-items: center; gap: 12px; padding: 14px 0; text-decoration: none; color: inherit; }
.row-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--sunken); display: flex; align-items: center; justify-content: center; color: var(--ink-2); flex: none; }
.row-body { flex: 1; min-width: 0; }
.row-title { font-weight: 700; font-size: 14px; }
.row-sub { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
.divider { height: 1px; background: var(--line); }

/* ---------------------------------------------------------------------------
   THE PORTION CARD'S FOOTER ROWS
   ---------------------------------------------------------------------------
   The item editor asks at most three questions and they are all one question:
   how much of this did you have. It used to ask them in three separate cards
   with three headings and three sets of padding — "How many", "One egg = 46g",
   "How much of it did you eat?" — and then a fourth for the macros, which are
   just that portion restated. Four blocks, one topic, and the Save button off
   the bottom of the screen.

   One card now, and the split is by FREQUENCY rather than by subject. The
   quantity is what you came to change, so its stepper and presets stay inline
   and full size. What one piece weighs, and what share of it you ate, are
   corrections you make occasionally — they collapse to a summary that states
   the current answer and opens a sheet. Reading them costs a glance; changing
   them costs a tap, which is the right price for something you rarely touch.
   --------------------------------------------------------------------------- */
.pmod { display: flex; align-items: stretch; }
.pmod-item {
  flex: 1; display: flex; flex-direction: column; gap: 1px;
  padding: 9px 2px; text-align: left; min-width: 0;
}
.pmod-item + .pmod-item { border-left: 1px solid var(--line); padding-left: 12px; }
.pmod-label { font-size: 10px; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3); }
.pmod-value { font-size: 13px; font-weight: 800; color: var(--ink); display: flex; align-items: center; gap: 3px; }
.pmod-value svg { width: 13px; height: 13px; color: var(--ink-4); flex: none; }

/* The macros, as one line rather than three columns of dot-over-number. */
.pmacro { display: flex; align-items: center; gap: 8px; }
.pmacro-bar { display: flex; height: 5px; border-radius: 999px; overflow: hidden; flex: 1; }
.pmacro-legend { display: flex; gap: 9px; font-size: 11px; font-weight: 700; color: var(--ink-2); white-space: nowrap; }
.pmacro-legend span { display: inline-flex; align-items: center; gap: 3px; }

/* Form fields */
.field { display: block; width: 100%; }
.field-label { font-size: 11px; font-weight: 700; color: var(--ink-2); margin-bottom: 6px; display: block; }
.field-input {
  width: 100%; padding: 13px 14px; border-radius: var(--radius);
  border: 1.5px solid var(--line-strong); background: var(--surface);
  font-size: 15px; font-weight: 600; color: var(--ink); box-sizing: border-box;
}
.field-input::placeholder { color: var(--ink-4); font-weight: 500; }
.field-help { font-size: 11px; color: var(--ink-3); margin-top: 5px; }

/* Search bar */
.search {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 14px; border-radius: var(--radius-lg);
  background: var(--sunken); color: var(--ink-3);
}
.search input { border: none; background: transparent; outline: none; font-size: 14px; width: 100%; color: var(--ink); }

/* Toggle */
.toggle { width: 44px; height: 26px; border-radius: 999px; background: var(--line-strong); position: relative; flex: none; transition: background 0.15s; }
.toggle.is-on { background: var(--brand-500); }
.toggle::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 999px; background: var(--surface); box-shadow: var(--shadow-sm); transition: transform 0.15s; }
.toggle.is-on::after { transform: translateX(18px); }

/* Modal / bottom sheet.
   Named `bottomsheet` and not `sheet`: the shared screenshots.html uses `.sheet`
   for its tile grid, and that file is copied byte for byte between products, so
   a product class may not take the name. Two `.sheet` rules, one of them
   `position: absolute`, collapsed the whole contact sheet into one screenful
   and painted nothing that looked like an error. */
.backdrop { position: absolute; inset: 0; background: rgb(var(--scrim) / 0.55); }
.bottomsheet {
  position: absolute; inset-inline: 0; bottom: 0;
  background: var(--surface);
  border-radius: var(--radius-3xl) var(--radius-3xl) 0 0;
  padding: 8px 20px calc(20px + env(safe-area-inset-bottom, 8px));
}
.bottomsheet-grip { width: 40px; height: 4px; border-radius: 999px; background: var(--line-strong); margin: 6px auto 14px; }

/* The analysing spinner. It was a <style> block inside scan/analyzing.html;
   a rule declared in one screen is checked by nothing and reusable by nobody. */
@keyframes spin { to { transform: rotate(360deg); } }
.spin { animation: spin 0.8s linear infinite; }

/* A colour swatch on the components sheet. */
.sw { height: 44px; border-radius: 10px; }

/* ---------------------------------------------------------------------------
   Specimen — patterns/sheets.html

   A pattern shown on its own, at a size that fits two to a row. Deliberately
   NOT `.phone`: a second `.phone` in a file is a second frame, and a frame
   with no route entry has no URL, cannot be linked to and gets no tile on the
   contact sheet. A specimen is an illustration of a component; the device is
   where you go to see it in place.
   --------------------------------------------------------------------------- */
.specimen {
  position: relative; overflow: hidden;
  height: 400px; max-width: 320px;
  background: var(--page);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

/* Empty state */
.empty { text-align: center; padding: 40px 28px; }
.empty-illus { width: 72px; height: 72px; border-radius: 999px; background: var(--sunken); display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; color: var(--ink-4); }
.empty-title { font-weight: 800; font-size: 16px; }
.empty-sub { font-size: 13px; color: var(--ink-3); margin-top: 6px; line-height: 1.5; }

/* Skeleton */
.skel { background: linear-gradient(90deg, var(--sunken) 25%, var(--page) 50%, var(--sunken) 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; border-radius: var(--radius-sm); }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Quota meter — metering is visible, never a surprise */
.quota { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; color: var(--ink-3); }
.quota-track { flex: 1; height: 5px; border-radius: 999px; background: var(--sunken); overflow: hidden; }
.quota-fill { height: 100%; border-radius: 999px; background: var(--brand-500); }
.quota-fill--low { background: var(--accent-500); }
.quota-fill--out { background: var(--danger); }

/* Camera scrim + framing */
.viewfinder { position: absolute; inset: 0; background: var(--camera); display: flex; align-items: center; justify-content: center; }
.vf-frame { width: 260px; height: 260px; border-radius: var(--radius-2xl); box-shadow: 0 0 0 9999px rgb(var(--scrim) / 0.55); position: relative; }
.vf-corner { position: absolute; width: 26px; height: 26px; border: 3px solid var(--ink-inverse); }

/* Toast — bottom, above the tab bar, auto-dismiss. Every destructive or
   silent action gets one with an Undo; logging a meal is reversible. */
.toast {
  display: flex; align-items: center; gap: 11px;
  padding: 12px 14px; border-radius: var(--radius-lg);
  background: var(--ink); color: var(--ink-inverse);
  box-shadow: var(--shadow-lg);
}
.toast-body { flex: 1; min-width: 0; }
.toast-title { font-size: 13px; font-weight: 800; }
.toast-sub { font-size: 11px; opacity: 0.7; margin-top: 1px; }
.toast-action { font-size: 12px; font-weight: 800; color: var(--brand-200); text-decoration: none; flex: none; }
.toast-success { background: var(--brand-700); }
.toast-danger  { background: var(--danger-900); }
.toast-ask     { background: var(--accent-700); }
.toast-ask .toast-action { color: var(--accent-100); }

/* ---------------------------------------------------------------------------
   Day strip — home/today.html

   Seven days as seven small rings. The day bar below is a length, so the strip
   is the one place a ring still earns its keep: a day is a closed quantity
   against a fixed target, which is exactly what a circle says well.

   It replaces a chip-plus-dot strip whose four dot colours needed a four-item
   legend underneath — a row of the home screen spent teaching its own
   alphabet. Fill needs no key, and says how MUCH of the target was met rather
   than only which band it fell in.
   --------------------------------------------------------------------------- */
.dayring {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: 4px 0;
  background: none; border: 0; cursor: pointer; color: inherit;
}
.dayring svg { width: 30px; height: 30px; transform: rotate(-90deg); }
.dayring circle {
  cx: 15px; cy: 15px; r: 12px;
  fill: none; stroke-width: 3.5; stroke-linecap: round;
}
/* 2*pi*12 = 75.4, so a dasharray of `n 75.4` fills n/75.4 of the circle and
   the data is one number per day. */
.dayring .t { stroke: var(--sunken); stroke-dasharray: 75.4 75.4; }
.dayring .f { stroke: var(--brand-500); }
.dayring-over .f { stroke: var(--danger); }
.dayring-hold { position: relative; display: grid; place-items: center; }
.dayring-num { position: absolute; font-size: 10px; font-weight: 700; }
.dayring-letter {
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-3); line-height: 1;
}
/* SELECTION IS WEIGHT, NOT COLOUR.

   It used to fill the ring's centre with brand and invert the date to white.
   That made selection the loudest colour in a row whose colours already mean
   something else: the arc is how much of the target the day came to, and
   `--danger` on it means over. A green disc under a red arc put two unrelated
   messages inside one 30px object, and on the day you went over it read as
   approval.

   Bold carries it instead. The number steps 700 to 900 and the weekday letter
   goes with it from ink-3 to ink, which is the same "this one" the flow chart
   and the chrome bar use, and it changes nothing about the box — the reason
   the disc replaced an outer ring in the first place was that a ring cost 3px
   of padding and stepped the strip's baseline. Weight costs nothing at all. */
.dayring.is-current .dayring-num { font-weight: 900; }
.dayring.is-current .dayring-letter { color: var(--ink); font-weight: 900; }
.dayring.is-blank .dayring-num { color: var(--ink-4); }
/* A zero-length stroke with round caps still paints a dot, so every blank day
   wore a green mark claiming something was logged. */
.dayring.is-blank .f { display: none; }

/* ---------------------------------------------------------------------------
   Trend chart — trends/trends.html

   A scroller, not a fitted row. The week was never the question people had;
   "what was I doing in August" is, and a toggle between two windows somebody
   else chose is a worse answer than being able to drag.

   Bars are a FIXED width. flex-1 over a variable number of days means the
   chart silently redraws itself every morning as one more day arrives, and by
   ninety days each bar is a hairline with a label under it nobody can read.
   --------------------------------------------------------------------------- */
.trendchart { position: relative; }
.trendscroll {
  display: flex; align-items: flex-end; gap: 8px;
  height: 126px;                      /* 104 of plot + 18 of label */
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  /* The bars run to the card's edge on both sides, so a bar half in view is
     the affordance — a chart that stops neatly inside its padding looks
     finished and nobody drags it. */
  margin-inline: -15px; padding-inline: 15px;
  scrollbar-width: none;
}
.trendscroll::-webkit-scrollbar { display: none; }
.trendbar {
  flex: 0 0 22px; display: flex; flex-direction: column;
  align-items: center; gap: 6px; height: 100%; justify-content: flex-end;
}
.trendbar-fill { width: 100%; border-radius: 6px; }
.trendbar-label { font-size: 9.5px; font-weight: 700; line-height: 12px; white-space: nowrap; }
/* The target, behind the bars and across the whole plot — the one horizontal
   in the chart, so "over" is a place rather than only a colour. It sits
   outside the scroller on purpose: the target does not move when you drag. */
.trendline {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1.5px dashed var(--line-strong);
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   Month grid — home/calendar.html

   The seven-day strip with the ceiling taken off. Same ring, same fill, same
   blank rule, in seven columns — a month has to be read with the alphabet the
   week above it already taught, or the sheet is a second thing to learn in
   order to reach the first.

   Sunday first, and not by taste: constants/day.ts is
   ["S","M","T","W","T","F","S"] indexed straight off Date.getDay(), so a
   Sunday-first grid needs no week-start constant and no locale guess, which the
   app has nowhere to get — toLocaleDateString does not report one.
   --------------------------------------------------------------------------- */
.monthnav { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.monthnav-title { font-size: 15px; font-weight: 900; letter-spacing: -0.01em; }
/* A step is a button and not a link: paging the month re-renders this sheet
   against a different month, the same way the strip's chips re-render Today
   against a different day. Nothing navigates. */
.monthnav-step {
  width: 32px; height: 32px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--sunken); color: var(--ink-2);
  border: none; cursor: pointer;
}
/* Struck through rather than removed, the same as the chrome bar's own ends: a
   control that vanishes at the edge of its range slides the title sideways, and
   "there is no next month" has to look like a reason rather than an absence.
   You cannot page into the future for the same reason you cannot log it. */
.monthnav-step.is-off { opacity: 0.35; cursor: default; }

.monthhead, .monthgrid { display: grid; grid-template-columns: repeat(7, 1fr); }
.monthhead {
  font-size: 9px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--ink-4); text-align: center;
}
.monthgrid { row-gap: 2px; }
/* 390/7 is 55px across; 30px of ring plus this padding is a 44px target down,
   which is the floor for a thing you tap thirty of. */
.monthgrid .dayring { padding: 7px 0; }

/* A day after today. Drawn, because a month with its last ten days missing
   stops looking like a month and starts looking like a bug; not tappable,
   because you cannot log the future — the same rule that ends the strip at
   today rather than centring it.

   It loses the RING, not just the fill. Dimming the track was the first
   attempt and it failed the only test that matters: a blank past day — which
   is tappable, and is how you log a meal you forgot three weeks ago — looked
   exactly like an untappable future one at 30px. A ring means "a day with a
   target you can still put food against". No ring means the day has not
   happened. `visibility` and not `display`, so the hidden svg still sets the
   cell's box and the grid keeps its rhythm. */
.dayring.is-future { cursor: default; }
.dayring.is-future svg { visibility: hidden; }
.dayring.is-future .dayring-num { color: var(--ink-4); }

/* ---------------------------------------------------------------------------
   Stepper — log/confirm.html

   Portion adjustment without a keyboard. "Having to edit every item" is the
   most repeated usability complaint about the category leader, and a numeric
   field is the slowest possible way to change 150g to 200g.
   --------------------------------------------------------------------------- */
.stepper {
  display: flex; align-items: center; gap: 2px;
  background: var(--sunken);
  border-radius: 10px;
  padding: 3px;
}
.stepper-btn {
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-2);
}
.stepper-value {
  min-width: 58px; text-align: center;
  font-size: 13px; font-weight: 900;
  color: var(--ink);
}

/* ---------------------------------------------------------------------------
   Cached-result badge — scan/same-photo.html

   "Same meal, different numbers, varying by 400 calories" is the credibility
   complaint about the category leader. An identical photo returns an identical
   answer here, and that is worth saying out loud rather than leaving as
   invisible plumbing.
   --------------------------------------------------------------------------- */
.samebadge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px;
  background: var(--info-bg); color: var(--info-fg);
  font-size: 10px; font-weight: 800;
}

/* Informational card — scan/result-same.html. Sits alongside card--brand and
   card--ask, for statements of fact rather than praise or a prompt. */
.card--info {
  background: var(--info-bg);
  border-color: var(--info);
}

/* ── Food gallery ─────────────────────────────────────────────────────────
   The photo wall. Everything here is local: meal photos live in the app's
   own document directory (lib/photos.ts) and no screen in the app has ever
   uploaded one. The gallery does not change that — the only way a photo
   leaves the phone is the user choosing a destination in the OS share sheet.
*/
.gal-day { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; }
.gal-tile {
  position: relative; aspect-ratio: 1; overflow: hidden;
  background: var(--sunken); display: block; cursor: pointer;
}
.gal-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The number sits ON the photo rather than under it: the wall is a record of
   what was eaten, and a grid of pictures with no figures is a mood board. */
.gal-kcal {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 14px 6px 5px; color: var(--ink-inverse); font-size: 11px; font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(to top, rgb(var(--scrim) / 0.78), rgb(var(--scrim) / 0));
}
/* A meal logged without a photo still happened. Shown as a gap rather than
   omitted, so a day of four meals does not read as a day of two. */
.gal-tile--none {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-4); background: var(--muted);
  border: 1px dashed var(--line-strong);
}
.gal-dayhead {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 0 20px 8px;
}

/* The composed post. Drawn at the proportions a phone camera actually
   produces, because what is previewed here is what gets rendered to an image
   and handed to the share sheet. */
.post-card {
  border-radius: var(--radius-lg); overflow: hidden; background: var(--brand-900);
  position: relative; aspect-ratio: 4 / 5;
}
.post-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.post-overlay {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 40px 16px 14px;
  background: linear-gradient(to top, rgb(var(--scrim) / 0.88), rgb(var(--scrim) / 0));
  color: var(--ink-inverse);
}
.post-kcal { font-size: 30px; font-weight: 900; line-height: 1; letter-spacing: -0.03em; }
.post-macros { display: flex; gap: 12px; margin-top: 8px; font-size: 11px; font-weight: 700; opacity: 0.85; }
.post-mark {
  display: flex; align-items: center; gap: 6px; margin-top: 12px;
  padding-top: 10px; border-top: 1px solid rgb(255 255 255 / 0.18);
  font-size: 11px; font-weight: 700; opacity: 0.8;
}

/* ---------------------------------------------------------------------------
   ONBOARDING · the `o-` components

   Every class is prefixed `o-` and every colour it reads is a role token, so
   the whole flow re-tints by the `.paper` scope in the palette block above and
   by nothing else. These lived in onboarding/onboarding.css until the set was
   migrated: a per-flow stylesheet is loaded by some screens and checked by
   nothing, and its rules were either component classes — which belong here —
   or utilities that were never written as utilities.

   The rule the flow is built on: paper and ink carry the interface. Colour is
   spent on exactly two jobs — the brand, which means "this is the action", and
   the three confidence signals, which are the product's actual idea.
   --------------------------------------------------------------------------- */

/* ─── Header ────────────────────────────────────────────────────────────────
   One row: back, progress, count. The old header stacked a centred title, a
   "Step 2 of 5" subtitle AND a five-segment bar — three ways of saying the
   same thing, costing ~90px above the fold on a 390pt screen. The bar and the
   count say it once each; the screen's own <h1> is the title. */
.o-nav {
  display: flex; align-items: center; gap: 14px;
  padding: 6px 20px 0;
}
.o-back {
  width: 36px; height: 36px; margin-left: -8px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center; flex: none;
  color: var(--ink); background: transparent; border: none; cursor: pointer;
  text-decoration: none;
}
.o-back:active { background: var(--sunken); }
.o-steps { flex: 1; display: flex; align-items: center; gap: 5px; }
.o-step { height: 3px; flex: 1; border-radius: 999px; background: var(--line-strong); }
.o-step.is-done { background: var(--brand-500); }
.o-count {
  flex: none; font-size: 11px; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
}

/* ─── Type ──────────────────────────────────────────────────────────────── */
.o-pad { padding-left: 20px; padding-right: 20px; }

.o-h1 {
  font-size: 27px; font-weight: 860; line-height: 1.14;
  letter-spacing: -0.033em; color: var(--ink); margin: 0;
}
.o-hero {
  font-size: 40px; font-weight: 860; line-height: 1.0;
  letter-spacing: -0.04em; color: var(--ink); margin: 0;
}
.o-lede {
  font-size: 14.5px; line-height: 1.55; color: var(--ink-2); margin: 10px 0 0;
}
.o-sub {
  font-size: 13px; line-height: 1.5; color: var(--ink-2); margin: 7px 0 0;
}
.o-note { font-size: 11.5px; line-height: 1.55; color: var(--ink-3); margin: 8px 0 0; }
.o-label {
  font-size: 10.5px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.14em; color: var(--ink-3);
}
.o-fieldlabel { font-size: 12px; font-weight: 700; color: var(--ink-2); }
.o-num { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }

/* ─── Wordmark ──────────────────────────────────────────────────────────── */
.o-mark { display: flex; align-items: center; gap: 10px; }
.o-mark-tile {
  width: 34px; height: 34px; border-radius: 11px; flex: none;
  background: var(--brand-500); color: var(--ink-inverse);
  display: flex; align-items: center; justify-content: center;
}
.o-mark-name {
  font-size: 15px; font-weight: 800; letter-spacing: -0.01em; color: var(--ink);
}

/* ─── Signal ledger (welcome) ───────────────────────────────────────────────
   The product's whole claim, as three rows of a ledger. A tinted tag carries
   the state; the sentence stays ink so the tag is the only thing that shouts. */
.o-ledger {
  border: 1px solid var(--line); border-radius: 18px;
  background: var(--surface); overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.o-ledger-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 15px 16px;
}
.o-ledger-row + .o-ledger-row { border-top: 1px solid var(--line); }
.o-tag {
  flex: none; min-width: 74px; text-align: center;
  padding: 4px 9px; border-radius: 7px;
  font-size: 10px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.1em;
}
.o-tag--seen    { background: var(--conf-high-bg);    color: var(--conf-high); }
.o-tag--unknown { background: var(--conf-low-bg); color: var(--conf-low); }
.o-tag--asked   { background: var(--conf-med-bg);   color: var(--conf-med); }
.o-ledger-text { font-size: 13px; line-height: 1.5; color: var(--ink-2); margin: 1px 0 0; }
.o-ledger-text strong { color: var(--ink); font-weight: 700; }

/* ─── Cards ─────────────────────────────────────────────────────────────── */
.o-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 16px; padding: 16px;
}
.o-card--accent { background: var(--brand-50); border-color: var(--brand-100); }
.o-card--asked  { background: var(--conf-med-bg);  border-color: var(--accent-200); }
.o-card--well   { background: var(--sunken); border-color: transparent; }
.o-card--flush  { padding: 0; overflow: hidden; }
.o-card--tap { cursor: pointer; transition: border-color .12s, transform .05s; text-decoration: none; color: inherit; display: block; }
.o-card--tap:active { transform: scale(.995); border-color: var(--line-strong); }

/* ─── Buttons ───────────────────────────────────────────────────────────── */
.o-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 15px 20px; border-radius: 16px;
  font-size: 15px; font-weight: 800; letter-spacing: -0.005em;
  border: none; cursor: pointer; text-decoration: none;
  transition: opacity .15s, transform .05s;
}
.o-btn:active { opacity: .9; transform: scale(.99); }
.o-btn--primary { background: var(--brand-500); color: var(--ink-inverse); box-shadow: var(--shadow-fab); }
.o-btn--quiet   { background: var(--surface); color: var(--ink); border: 1px solid var(--line-strong); }
.o-btn--plain   { background: transparent; color: var(--ink-2); font-weight: 700; }
.o-btn--sm      { padding: 10px 14px; font-size: 13px; border-radius: 12px; width: auto; }
.o-link { color: var(--brand-700); font-weight: 700; text-decoration: none; border-bottom: 1px solid var(--brand-200); }

/* ─── Sticky footer ─────────────────────────────────────────────────────── */
.o-foot {
  position: sticky; bottom: 0; margin-top: auto;
  background: color-mix(in srgb, var(--page) 92%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 8px));
}
.o-foot-note { text-align: center; font-size: 11.5px; color: var(--ink-3); margin: 10px 0 0; }

/* ─── Option rows ───────────────────────────────────────────────────────── */
.o-opt {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 14px; border-radius: 14px; text-align: left;
  border: 1px solid var(--line-strong); background: var(--surface);
  color: inherit; cursor: pointer; text-decoration: none;
  transition: border-color .12s, background .12s, box-shadow .12s, transform .05s;
}
.o-opt:active { transform: scale(.99); }
.o-opt.is-on {
  border-color: var(--brand-500); background: var(--brand-50);
  box-shadow: inset 0 0 0 1px var(--brand-500);
}
.o-opt-title { font-size: 14.5px; font-weight: 750; color: var(--ink); }
.o-opt-sub   { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; line-height: 1.4; }
.o-opt-meta  { font-size: 12px; font-weight: 750; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.o-opt.is-on .o-opt-meta { color: var(--brand-700); }
.o-puck {
  width: 34px; height: 34px; border-radius: 10px; flex: none;
  background: var(--sunken); color: var(--ink-2);
  display: flex; align-items: center; justify-content: center;
}
.o-opt.is-on .o-puck { background: var(--surface); color: var(--brand-700); }
.o-tick {
  width: 21px; height: 21px; border-radius: 999px; flex: none;
  border: 1.5px solid var(--line-strong); background: var(--surface);
  display: flex; align-items: center; justify-content: center; color: transparent;
}
.o-opt.is-on .o-tick { border-color: var(--brand-500); background: var(--brand-500); color: var(--ink-inverse); }

/* ─── Segmented ─────────────────────────────────────────────────────────── */
.o-seg {
  display: flex; width: 100%; gap: 2px; padding: 3px;
  background: var(--sunken); border-radius: 999px;
}
.o-seg > * {
  flex: 1; text-align: center; padding: 8px 12px; border-radius: 999px;
  font-size: 12.5px; font-weight: 700; color: var(--ink-2);
  border: none; background: transparent; cursor: pointer; text-decoration: none;
}
.o-seg > .is-on {
  background: var(--surface); color: var(--ink); font-weight: 800;
  box-shadow: var(--shadow-sm);
}

/* ─── Measurement rows ──────────────────────────────────────────────────────
   Age, height and weight as three rows of one card instead of three floating
   boxes in a 2+1 grid that never lined up. Label left, value right, unit
   pinned — the layout of a form you can fill without reading it twice. */
.o-mrow {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
}
.o-mrow + .o-mrow { border-top: 1px solid var(--line); }
.o-mrow-label { flex: 1; font-size: 14px; font-weight: 650; color: var(--ink); }
.o-mrow-field { display: flex; align-items: baseline; gap: 6px; }
.o-mrow-input {
  width: 66px; text-align: right; border: none; background: transparent;
  font-size: 19px; font-weight: 800; color: var(--ink);
  font-variant-numeric: tabular-nums; padding: 0; outline: none;
  border-bottom: 1.5px solid var(--line-strong);
}
.o-mrow-input:focus { border-bottom-color: var(--brand-500); }
.o-mrow-unit { font-size: 12.5px; font-weight: 700; color: var(--ink-3); width: 24px; }

/* ─── Slider ────────────────────────────────────────────────────────────── */
.o-slider { position: relative; height: 26px; }
.o-slider-track {
  position: absolute; top: 10px; left: 0; right: 0; height: 6px;
  border-radius: 999px; background: var(--line);
}
.o-slider-fill {
  position: absolute; top: 10px; left: 0; height: 6px;
  border-radius: 999px; background: var(--brand-500);
}
.o-slider-knob {
  position: absolute; top: 0; width: 26px; height: 26px; margin-left: -13px;
  border-radius: 999px; background: var(--surface);
  box-shadow: 0 0 0 1.5px var(--brand-500), var(--shadow);
}
.o-scale {
  display: flex; justify-content: space-between; margin-top: 7px;
  font-size: 10.5px; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* ─── Derivation stepper (targets) ──────────────────────────────────────────
   Four stacked cards gave four equal-weight facts. The arithmetic is a chain —
   rest, then movement, then the goal — so it is drawn as one, with a rule
   running through the numbers. */
.o-chain { position: relative; }
.o-chain::before {
  content: ""; position: absolute; left: 15px; top: 16px; bottom: 16px;
  width: 1.5px; background: var(--line);
}
.o-link-row { position: relative; display: flex; gap: 14px; padding-bottom: 14px; }
.o-link-row:last-child { padding-bottom: 0; }
.o-node {
  width: 31px; height: 31px; border-radius: 999px; flex: none; z-index: 1;
  background: var(--surface); border: 1.5px solid var(--line-strong);
  color: var(--ink-3);
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 800;
}
.o-node--on { background: var(--brand-500); border-color: var(--brand-500); color: var(--ink-inverse); }
.o-link-body { flex: 1; min-width: 0; padding-top: 2px; }
.o-link-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.o-link-title { font-size: 14px; font-weight: 780; color: var(--ink); }
.o-link-value { font-size: 16px; font-weight: 860; color: var(--ink); font-variant-numeric: tabular-nums; }
.o-work {
  margin-top: 9px; padding: 9px 11px; border-radius: 10px;
  background: var(--sunken); color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: 10.5px; line-height: 1.65;
}
.o-work strong { color: var(--ink); }

/* ─── Target hero ───────────────────────────────────────────────────────── */
.o-target { text-align: center; padding: 22px 16px 18px; }
.o-target-value {
  font-size: 52px; font-weight: 860; line-height: 1;
  letter-spacing: -0.045em; color: var(--brand-700);
  font-variant-numeric: tabular-nums; margin: 8px 0 0;
}
.o-macros { display: flex; border-top: 1px solid var(--line); }
.o-macro { flex: 1; padding: 13px 6px 14px; text-align: center; }
.o-macro + .o-macro { border-left: 1px solid var(--line); }
.o-macro-cap { display: block; width: 22px; height: 3px; border-radius: 999px; margin: 0 auto 8px; }
/* block: the app stacks these (two Text nodes in a column View), and
   .o-macro-name’s margin-top never applied while they were inline. */
.o-macro-g { display: block; font-size: 16px; font-weight: 850; color: var(--ink); font-variant-numeric: tabular-nums; }
.o-macro-name {
  display: block;
  font-size: 9.5px; font-weight: 750; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--ink-3); margin-top: 3px;
}
.m-protein-cap { background: var(--protein); }
.m-carbs-cap   { background: var(--carbs); }
.m-fat-cap     { background: var(--fat); }

/* ─── Price rows ────────────────────────────────────────────────────────── */
.o-feat { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; line-height: 1.45; color: var(--ink); }
.o-feat i { color: var(--brand-500); flex: none; margin-top: 1px; }
.o-pill {
  padding: 4px 9px; border-radius: 7px; font-size: 10px; font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.08em;
}
.o-pill--quiet  { background: var(--sunken); color: var(--ink-2); }
.o-pill--accent { background: var(--brand-100); color: var(--brand-700); }

/* Status bar on paper */
.paper .statusbar { color: var(--ink); }
