/* Diet App -- WS4 chat-first UI.
   Color discipline (ux-quick-reference.md "Color Discipline"): neutral
   grays for structure/backgrounds; saturated color reserved exclusively
   for the five semantic states (queued/running/done/failed/infeasible) --
   every one of those also carries an icon glyph + a text label, so color
   is never the only channel. No decorative animation anywhere below --
   the only transitions are opacity/background changes on interactive
   states (hover/disabled), which communicate a real state change. */

/* ============================================================================
   THE WORKING FOLIO -- foundation (design bible: docs/DESIGN_WORKING_FOLIO.md,
   "The Working Folio, finished" -- Sol's REVISED system, superseding the warm
   paper/green-black-ink draft this file previously held). One continuous
   dated document, amended in place. Crisp white paper (near-black in dark
   theme), a high-contrast near-black publication masthead, Public Sans
   carrying almost the entire application (headings, data, controls, tables,
   labels, statuses, timestamps), Source Serif 4 reserved for coach-authored
   prose only, Roboto Mono for record IDs/code only, and Fraunces (a display
   serif with a real optical-size axis) for the top-level display headers
   only -- masthead titles, chapter titles, recipe titles -- per the
   type-spec correction: the "no serif" problem was really "no TEXT serif at
   DISPLAY sizes"; a display serif at large sizes is the editorial look,
   distinct from the reading-serif used for coach prose. Numbered chapters
   with 4px opening rules and large sans/serif headings, 8rem of whitespace
   between chapters, a real numbered contents index. No section cards,
   background slabs, shadows, radii, pills, or gradients -- structure comes
   from type, rules, and whitespace alone.
   ============================================================================ */
@font-face { font-family: "Public Sans"; src: url("/static/fonts/public-sans-var.woff2") format("woff2");
  font-style: normal; font-weight: 100 900; font-display: swap; }
@font-face { font-family: "Source Serif 4"; src: url("/static/fonts/source-serif-4-var.woff2") format("woff2");
  font-style: normal; font-weight: 200 900; font-display: swap; }
@font-face { font-family: "Source Serif 4"; src: url("/static/fonts/source-serif-4-italic-var.woff2") format("woff2");
  font-style: italic; font-weight: 200 900; font-display: swap; }
@font-face { font-family: "Roboto Mono"; src: url("/static/fonts/roboto-mono-var.woff2") format("woff2");
  font-style: normal; font-weight: 100 700; font-display: swap; }
@font-face { font-family: "Fraunces"; src: url("/static/fonts/fraunces-var.woff2") format("woff2");
  font-style: normal; font-weight: 100 900; font-display: swap; }
@font-face { font-family: "Fraunces"; src: url("/static/fonts/fraunces-italic-var.woff2") format("woff2");
  font-style: italic; font-weight: 100 900; font-display: swap; }

/* -- 1. Palette (bible §1) -- */
:root,
:root[data-theme="light"] {
  color-scheme: light;

  --canvas: #e8ecef;
  --paper: #ffffff;
  --paper-subtle: #f2f4f5;

  --ink: #0a0c0e;
  --ink-muted: #4d5861;
  --ink-secondary: #65717a;

  --rule: #d3d8dc;
  --rule-strong: #68737c;

  --masthead-bg: #07090b;
  --masthead-ink: #ffffff;
  --masthead-muted: #c7cfd5;
  --masthead-link: #9ed0ff;

  /* Masthead-tuned status accent (item 3): the cover ground is near-black in
     BOTH site themes, so this always uses the LIGHT-legible (dark-theme)
     semantic value, never the dark-on-light --status--caution hue above,
     which is near-invisible on #07090b. Identical across all three variable
     blocks in this file for the same reason --masthead-link is.
     S2 color discipline: --masthead-good and --masthead-under are RETIRED --
     on-target/under-target verdicts are routine, rendered in plain masthead
     ink/muted (see .masthead-verdict--good/--under), so --caution is the
     only masthead status accent left; nothing else references the old
     tokens (grep before reviving either name). */
  --masthead-caution: #ffd166;

  --accent: #005ea8;
  --accent-hover: #004a86;
  --accent-soft: #e8f2fa;
  --accent-contrast: #ffffff;

  --good: #0b6b43;
  --good-bg: #e6f5ec;

  --caution: #7a4a00;
  --caution-bg: #fff2cc;

  --critical: #b42318;
  --critical-bg: #fdeae8;

  --info: #51459a;
  --info-bg: #f0edfa;

  /* A hue reserved solely for keyboard focus -- distinct from --caution
     (was off the same brownish family) and every other status token, so
     Tab-focus never doubles as a status cue. Picked at a mid luminance so
     the one global ring stays >=3:1 (WCAG 1.4.11) on white paper AND on
     the lightbox's dark scrim, the two surfaces it has to ring on. */
  --focus: #a8326e;
  --skeleton: #dce1e4;

  /* Three DISTINCT hues for the macro-distribution pie (Settings "Your
     goals"). Deliberately NOT the good/caution/critical status tokens -- a
     green "fat" slice would misread as a value judgement. Drawn from the
     Okabe-Ito colour-blind-safe palette (blue / orange / reddish-purple), which
     stays separable under deuteranopia and protanopia where a blue/violet pair
     collapses -- verified after a redteam flagged the earlier blue/amber/violet
     set. The legend also carries text labels, so the data never depends on
     colour alone. */
  --macro-protein: #0072b2;
  --macro-carbs: #e69f00;
  --macro-fat: #cc79a7;

  /* Migration aliases: pre-Folio component CSS further down this file
     (.topbar, .panel, .status-pill, .msg-bubble, .btn, etc.) references
     these older token names -- keeping them means the whole app re-skins
     to the new palette the instant this loads; nothing below has to
     change to stay visually correct. */
  --bg: var(--canvas); --panel-bg: var(--paper); --border: var(--rule); --border-strong: var(--rule-strong);
  --text: var(--ink); --text-muted: var(--ink-muted);
  --success: var(--good); --success-bg: var(--good-bg);
  --warning: var(--caution); --warning-bg: var(--caution-bg);
  --error: var(--critical); --error-bg: var(--critical-bg);
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --canvas: #050607;
  --paper: #0e1113;
  --paper-subtle: #171b1e;

  --ink: #f7f9fa;
  --ink-muted: #bbc4ca;
  --ink-secondary: #99a5ad;

  --rule: #343b40;
  --rule-strong: #8b969e;

  --masthead-bg: #000000;
  --masthead-ink: #ffffff;
  --masthead-muted: #c7cfd5;
  --masthead-link: #9ed0ff;
  --masthead-caution: #ffd166;

  --accent: #8cc8ff;
  --accent-hover: #b8dcff;
  --accent-soft: #142a3b;
  --accent-contrast: #06111a;

  --good: #73dba7;
  --good-bg: #10291d;

  --caution: #ffd166;
  --caution-bg: #33270f;

  --critical: #ff8d85;
  --critical-bg: #351918;

  --info: #b8aeff;
  --info-bg: #251f38;

  /* Was byte-identical to --caution here -- now a dedicated focus hue. */
  --focus: #ff9ecf;
  --skeleton: #262c30;

  /* Okabe-Ito macro hues, lightened for the dark ground (see the light block):
     sky blue / orange / pink -- separable under colour-vision deficiency. */
  --macro-protein: #56b4e9;
  --macro-carbs: #f0b95a;
  --macro-fat: #e58fc2;

  --bg: var(--canvas); --panel-bg: var(--paper); --border: var(--rule); --border-strong: var(--rule-strong);
  --text: var(--ink); --text-muted: var(--ink-muted);
  --success: var(--good); --success-bg: var(--good-bg);
  --warning: var(--caution); --warning-bg: var(--caution-bg);
  --error: var(--critical); --error-bg: var(--critical-bg);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;

    --canvas: #050607;
    --paper: #0e1113;
    --paper-subtle: #171b1e;

    --ink: #f7f9fa;
    --ink-muted: #bbc4ca;
    --ink-secondary: #99a5ad;

    --rule: #343b40;
    --rule-strong: #8b969e;

    --masthead-bg: #000000;
    --masthead-ink: #ffffff;
    --masthead-muted: #c7cfd5;
    --masthead-link: #9ed0ff;
    --masthead-caution: #ffd166;

    --accent: #8cc8ff;
    --accent-hover: #b8dcff;
    --accent-soft: #142a3b;
    --accent-contrast: #06111a;

    --good: #73dba7;
    --good-bg: #10291d;

    --caution: #ffd166;
    --caution-bg: #33270f;

    --critical: #ff8d85;
    --critical-bg: #351918;

    --info: #b8aeff;
    --info-bg: #251f38;

    --focus: #ff9ecf;
    --skeleton: #262c30;

    /* Okabe-Ito macro hues, lightened for the dark ground (see the light block):
       sky blue / orange / pink -- separable under colour-vision deficiency. */
    --macro-protein: #56b4e9;
    --macro-carbs: #f0b95a;
    --macro-fat: #e58fc2;

    --bg: var(--canvas); --panel-bg: var(--paper); --border: var(--rule); --border-strong: var(--rule-strong);
    --text: var(--ink); --text-muted: var(--ink-muted);
    --success: var(--good); --success-bg: var(--good-bg);
    --warning: var(--caution); --warning-bg: var(--caution-bg);
    --error: var(--critical); --error-bg: var(--critical-bg);
  }
}

* { box-sizing: border-box; }

/* -- 2. Typography (bible §2, + coordinator's display-serif correction) -- */
:root {
  --font-sans: "Public Sans", "Helvetica Neue", Arial, sans-serif;
  --font-serif: "Source Serif 4", Georgia, serif;
  --font-mono: "Roboto Mono", "SFMono-Regular", Consolas, monospace;
  --font-display: "Fraunces", Georgia, serif;

  --fs-caption: 1rem;
  --fs-label: 1rem;
  --fs-body: 1.0625rem;
  --fs-client-body: 1.125rem;
  --fs-reading: 1.1875rem;
  --fs-data: 1.125rem;
  --fs-data-lead: 1.5rem;
  --fs-subhead: 1.5rem;
  --fs-lead-title: 2rem;
  --fs-section: 2.75rem;
  --fs-recipe-title: 3.5rem;
  --fs-masthead: 4rem;
}

body {
  margin: 0;
  color: var(--ink);
  background: var(--canvas);
  font: 450 var(--fs-body)/1.58 var(--font-sans);
  font-synthesis: none;
}

.client {
  font-size: var(--fs-client-body);
}

h1,
h2,
h3,
h4 {
  margin: 0;
  font-family: var(--font-sans);
  text-wrap: balance;
}

/* Top-level display headers only: a display serif (Fraunces, optical-size
   axis) reads as editorial at these large sizes -- unlike a text serif,
   which is what actually produced the "2005 PDF" effect the bible warned
   against. Weights/line-heights/letter-spacing stay exactly as Sol
   specified; only the typeface + optical sizing changed. */
.publication-title,
.briefing__title,
.lead-title,
.recipe-title,
.recipe-section__title {
  font-family: var(--font-display);
  font-optical-sizing: auto;
}

.publication-title {
  font-size: var(--fs-masthead);
  font-weight: 800;
  line-height: 0.94;
  letter-spacing: -0.045em;
}

.recipe-title {
  font-size: var(--fs-recipe-title);
  font-weight: 800;
  line-height: 0.96;
  letter-spacing: -0.045em;
}

.briefing__title {
  font-size: var(--fs-section);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;
}

.lead-title {
  font-size: var(--fs-lead-title);
  font-weight: 780;
  line-height: 1.08;
  letter-spacing: -0.022em;
}

.subheading {
  font-size: var(--fs-subhead);
  font-weight: 750;
  line-height: 1.2;
}

.data,
.timestamp {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums lining-nums;
}

.data {
  font-size: var(--fs-data);
  font-weight: 700;
}

.data-lead {
  font-size: var(--fs-data-lead);
  font-weight: 750;
  line-height: 1.3;
}

.coach-prose {
  max-width: 60ch;
  font: 430 var(--fs-reading)/1.65 var(--font-serif);
}

.recipe-method {
  max-width: 58ch;
  font: 450 var(--fs-reading)/1.62 var(--font-sans);
}

.technical,
.record-id {
  font: 500 1rem/1.5 var(--font-mono);
}

@media (max-width: 48rem) {
  :root {
    --fs-masthead: 2.75rem;
    --fs-recipe-title: 2.75rem;
    --fs-section: 2.25rem;
    --fs-lead-title: 1.75rem;
    --fs-data-lead: 1.375rem;
  }
}

a { color: var(--accent); text-decoration-thickness: 1.5px; text-underline-offset: 0.16em; }
a:hover { color: var(--accent-hover); }

/* -- 3. Section system: THE BRIEFING -- five flowing jobs, never numbered
   chapters or boxed widgets (founder's own rejection of the earlier
   six-chapter design: "bento boxes jammed into a framework"). One rule +
   one Fraunces title + generous whitespace per job; `minimap-width` is the
   real gutter the side rail below reserves so it never overlaps text. -- */
:root {
  --gutter: clamp(1.25rem, 5vw, 4.5rem);
  --briefing-gap: 5rem;
  --subsection-gap: 3.5rem;
  --row-pad: 1rem;
  --minimap-width: 11rem;
}

.document-content {
  padding-inline: var(--gutter);
}

/* -- Two-pane workspace: the folio is the reference DOCUMENT (2/3, scrolls with
   the page) and the coach is a PERSISTENT sidebar (1/3, always on) -- the coach
   is the primary "do what the computer says" surface, never buried at the bottom
   of a scroll. Collapses to one stacked column below 64rem. --------------------- */
.workspace-split {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  align-items: start;
}
.workspace-split .workspace-folio {
  min-width: 0;
  padding-inline-end: var(--gutter); /* no minimap lane in the two-pane layout */
}
/* Item 7: the mobile-only reach affordance (styled below 64rem) -- hidden
   on desktop, where the persistent sticky sidebar already keeps the coach
   in view at all times. */
.coach-jump-bar { display: none; }
/* The coach grows with its content and scrolls with the PAGE -- it is not a
   fixed-height box with its own scrollbar (a long transcript was being
   crushed into ~140px on desktop). No 100vh cage, no internal overflow; the
   two-pane grid simply lets whichever column is taller set the row height. */
.workspace-coach {
  min-width: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.75rem clamp(1rem, 2.2vw, 2rem);
  border-left: 1px solid var(--rule);
  background: var(--paper);
}
.workspace-coach__title {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.6rem;
  font-weight: 600;
  margin: 0;
  flex-shrink: 0;
}
/* Item 6: the persistent "how's today going" line at the top of the coach
   rail, above #chat-pane -- the existing .status/.data status tokens, the
   SAME markup _render_day_status_summary emits for the folio Today panel's
   own verdict. S1 color discipline keeps that folio copy colorless (a
   routine on-target day reads neutral ink -- see .status--good above); this
   coach-rail copy is the one place a verified-good state still gets
   --good's saturated green (the .workspace-coach .status--good override
   below), per doctrine: green stays reserved for the coach pane. Empty (no
   ruled line at all) when there's nothing to report yet --
   _render_day_status_summary's own honest "" return. */
.workspace-coach #coach-day-status {
  flex-shrink: 0;
  padding-block-end: 0.75rem;
  border-bottom: 2px solid var(--ink);
}
.workspace-coach #coach-day-status:empty { display: none; }
.workspace-coach #coach-day-status .status,
.workspace-coach #coach-day-status .data { margin: 0; }
.workspace-coach #coach-day-status .data { margin-block-start: 0.25rem; color: var(--ink-muted); }
.workspace-coach .status--good { color: var(--good); }
/* The coach body fits the sticky column: transcript scrolls, composer pinned. */
.workspace-coach .chat-pane {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.workspace-coach .chat-log { min-height: 0; overflow-y: visible; }
.workspace-coach .chat-form,
.workspace-coach #chat-plan-status,
.workspace-coach .composer-hint,
.workspace-coach #chat-feedback { flex-shrink: 0; }
.workspace-coach #history-panel { flex-shrink: 0; max-height: 28vh; overflow-y: auto; }

@media (max-width: 64rem) {
  .workspace-split { grid-template-columns: 1fr; }
  .workspace-coach {
    border-left: none;
    border-top: 4px solid var(--ink);
    margin-block-start: var(--briefing-gap);
  }

  /* Item 8 (display-compliance audit, 2026-07-20): #coach-day-status is the
     client's only day-budget signal, and on desktop it's the first thing in
     the always-visible sticky coach rail -- but the rule right above this
     one drops the whole coach to position:static, DOM-last, so a recipe/
     kitchen/history reader never sees it in the initial viewport at all
     (playbook §4: a persistent overview during a focused sub-task prevents
     tunnel-vision problem-fixation). A later-DOM element cannot be lifted
     above earlier-DOM folio content with `position: sticky` (sticky only
     freezes an element within ITS OWN flow position, never earlier), so
     this pins the SAME element -- no duplicate id, no second data source,
     no new route -- to the viewport instead; #workspace reserves the
     matching space above the masthead so nothing is covered.
     :empty{display:none} (the existing rule above) still collapses it
     honestly whenever there's genuinely no day status to report. */
  #workspace { padding-block-start: 4rem; }
  .workspace-coach #coach-day-status {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 15;
    padding: 0.6rem var(--gutter);
    background: var(--paper);
    border-bottom: 2px solid var(--ink);
  }

  /* Item 7 (display-compliance audit, 2026-07-20): render_coach_sidebar's
     own docstring says the coach "must NEVER be buried at the bottom of a
     scroll" -- true on desktop (the persistent sticky sidebar above), but
     on this stacked mobile column reaching it means scrolling past the
     entire folio first. A real, always-reachable anchor to the SAME #coach
     id every other "Ask your coach" control already uses, fixed to the
     viewport (mobile-only; the desktop sidebar already solves this, hence
     display:none by default above this media block). Pure CSS, no JS --
     CSP-safe. */
  .coach-jump-bar {
    display: block;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 20;
    padding: 0.85rem var(--gutter);
    padding-bottom: max(0.85rem, env(safe-area-inset-bottom));
    text-align: center;
    font-weight: 750;
    background: var(--paper);
    border-top: 2px solid var(--ink);
  }
}

.briefing {
  scroll-margin-top: 1.5rem;
  margin-block-start: var(--briefing-gap);
  padding-block-start: 2rem;
  border-top: 4px solid var(--ink);
}

.briefing--first {
  margin-block-start: 3rem;
  padding-block-start: 0;
  border-top: none;
}

.briefing__title {
  margin: 0 0 2rem;
}

/* Item C (founder live-testing pass): a section whose title carries a
   right-justified aside (e.g. "How today's going"'s own pace-verdict chip,
   _render_pace_chip) wraps both in one flex row instead of the bare <h2>
   above -- see _render_briefing_section's title_aside_html. The title's own
   bottom margin moves onto this wrapper so the heading row still opens the
   section body the same distance every title-only section already does. */
.briefing__heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 2rem;
}
.briefing__heading .briefing__title { margin: 0; }

/* Hierarchy (Folio v2): the hero ("Cook your next meal", the first section)
   keeps its Fraunces head; the OTHER sections are overviews the eye skates off,
   so their heads recede to quiet eyebrows and their rules thin. Data-ink: the
   uniform 4px rule + big head on every section was the noise. */
.briefing:not(.briefing--first) {
  border-top-width: 1px;
  border-top-color: var(--rule);
  padding-block-start: 0.6rem;
  margin-block-start: 2.5rem;
}
.briefing:not(.briefing--first) .briefing__title {
  font-family: "Public Sans", system-ui, sans-serif;
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  margin: 0 0 1rem;
}
.briefing:not(.briefing--first) .briefing__heading { margin: 0 0 1rem; }
/* Beats the eyebrow rule immediately above on specificity (four classes vs
   its three) -- without this, that rule's own `margin: 0 0 1rem` on
   `.briefing__title` would win over the plain `.briefing__heading
   .briefing__title { margin: 0; }` reset above, double-spacing the heading
   row by stacking the title's own bottom margin under the wrapper's. */
.briefing:not(.briefing--first) .briefing__heading .briefing__title { margin: 0; }

.subsection {
  margin-block-start: var(--subsection-gap);
  padding-block-start: 1rem;
  border-top: 2px solid var(--ink);
}

.ledger-row {
  padding-block: var(--row-pad);
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 64.01rem) {
  /* Reserves the minimap's own lane -- real padding, not a fixed overlay
     guessing at empty space -- so the rail never sits over live text
     regardless of viewport width. */
  .document-content {
    padding-inline-end: calc(var(--gutter) + var(--minimap-width));
  }
}

@media (max-width: 64rem) {
  :root {
    --briefing-gap: 3.5rem;
    --subsection-gap: 3rem;
  }

  .briefing {
    padding-block-start: 1.5rem;
  }

  .briefing--first {
    margin-block-start: 2rem;
  }

  .briefing__title {
    margin-block-end: 1.5rem;
  }
}

/* -- 4. Composition (bible §4) -- */
.document {
  width: 100%;
  max-width: 90rem;
  min-height: 100vh;
  margin-inline: auto;
  color: var(--ink);
  background: var(--paper);
}

@media (min-width: 48rem) {
  .document {
    width: calc(100% - 4rem);
    margin-block: 2rem;
  }
}

@media (max-width: 48rem) {
  body {
    background: var(--paper);
  }
}

.publication-masthead {
  padding: 1.5rem var(--gutter) 3.5rem;
  color: var(--masthead-ink);
  background: var(--masthead-bg);
  border-bottom: 4px solid var(--accent);
}

.masthead-running-line {
  display: flex;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block-end: 1rem;
  border-bottom: 1px solid #4a5157;
  color: var(--masthead-muted);
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.35;
}

/* The workspace masthead's own title block is ONE flowing column (the
   date, then "Prepared for X · updated Y", then the orienting line) --
   NOT the two-column grid the recipe masthead still uses for its title +
   facts pair (.recipe-masthead-grid, below, overrides this for that page
   specifically, later in the cascade). */
.masthead-title-grid {
  padding-block-start: 3rem;
}

.publication-title,
.recipe-title {
  color: var(--masthead-ink);
}

.masthead-subtitle,
.masthead-amended {
  margin: 1rem 0 0;
  color: var(--masthead-muted);
  font-size: 1rem;
  font-weight: 650;
}

/* The masthead's ONE orienting sentence (north star: understand the whole
   folio at a glance from the cover alone) -- full masthead-ink contrast
   and a touch larger than the plain subtitle line above it, since this is
   the most important fact on the cover, not a quiet footnote. */
.masthead-orienting {
  margin: 1.25rem 0 0;
  color: var(--masthead-ink);
  font-size: 1.25rem;
  font-weight: 650;
  line-height: 1.4;
  max-width: 44rem;
}

/* Item 3: the orienting line's day verdict carries a shape (the glyph
   already in the text) AND a hue, never hue alone -- and the hue must be
   readable white-on-near-black, so the one state that DOES carry colour
   (--caution) repoints to the masthead-tuned accent above rather than the
   dark-on-light .status--caution (near-invisible on #07090b).
   S2 color discipline: on-target is routine, not an alert -- plain masthead
   ink (matching .status--good's own --ink treatment on paper), never a
   green fill. "Under target" is likewise routine, not an attention state --
   the muted masthead register (.masthead-subtitle's own colour), NOT the
   old --masthead-under, which was byte-identical to --masthead-link and
   manufactured a false link affordance on inert running text. */
.masthead-verdict--good { color: var(--masthead-ink); }
.masthead-verdict--caution { color: var(--masthead-caution); }
.masthead-verdict--under { color: var(--masthead-muted); }

/* Item 9: the trainer-roster masthead's one-line tally ("N clients · ⚠ M
   need attention") -- quiet like the subtitle above it when the roster is
   healthy (a masthead with nothing to flag stays colorless), the
   attention clause borrows the masthead's own caution modifier (never a
   new hue, never color alone -- the glyph+word carry the state). */
.masthead-tally {
  margin: 0.35rem 0 0;
  color: var(--masthead-muted);
  font-size: 1rem;
}
.masthead-tally__attention { color: var(--masthead-caution); }

.masthead-amended {
  justify-self: end;
  text-align: right;
}

.publication-masthead a {
  color: var(--masthead-link);
}

@media (max-width: 48rem) {
  .publication-masthead {
    padding-block: 1.25rem 2.5rem;
  }

  .masthead-running-line {
    align-items: flex-start;
  }

  .masthead-title-grid {
    padding-block-start: 2.25rem;
  }

  .masthead-orienting {
    font-size: 1.125rem;
  }

  .masthead-amended {
    justify-self: start;
    text-align: left;
  }
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.75rem 1rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  font: 750 1rem/1.2 var(--font-sans);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.client .button {
  min-height: 48px;
}

.button--primary {
  color: var(--accent-contrast);
  background: var(--accent);
  border-color: var(--accent);
}

.button--primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.button--secondary {
  color: var(--ink);
  background: transparent;
  border-color: var(--rule-strong);
}

.button:disabled,
.button[disabled] {
  opacity: 0.55;
  cursor: default;
}
/* A control mid-action (no JS currently sets this; defined so a future
   long-running .button has a real loading state to reach for, the same
   discipline as its own :disabled state above). */
.button[aria-busy="true"] {
  opacity: 0.7;
  cursor: progress;
}
.chat-form textarea:disabled {
  opacity: 0.55;
  cursor: default;
}

/* S2 focus-ring unification: ONE dedicated hue (--focus, distinct from
   every status token in both themes), ONE ring treatment, on every
   focusable element -- no per-control width/colour variant. :focus-visible
   (not :focus) so a mouse click never paints a ring but Tab always does;
   no `outline: none` anywhere, since these rules only ADD a ring, never
   remove the default. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

.muted { color: var(--text-muted); }

/* ============================================================================
   Recipe reader (design bible §B, "Recipe reader"). The recipe is its own
   route/document body -- a near-black masthead (title + a ruled "Per
   serving" fact list), then three numbered sections (Ingredients / Method /
   Record this meal), using the SAME 4.5rem-rail chapter grammar as the rest
   of the document but without a marginal column (recipe sections are a
   single 44rem reading column, per bible).
   ============================================================================ */
/* Display-compliance item F2: the facts column used to be minmax(14rem,1fr)
   against a 2fr title column -- on the recipe page's own narrower reading
   column (the persistent coach sidebar claims 1/3 of the page, see
   .workspace-split above), that left the per-serving figures too little
   room, wrapping mid-phrase ("799 cal ·" / "~35% of your" / "day"). Widened
   to a real minimum + a bigger share of the row so the figures get a fair
   two-line read instead of three cramped ones. */
.recipe-masthead-grid {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(17rem, 2fr);
  gap: 2rem;
  align-items: end;
}

.recipe-facts {
  margin: 0;
  padding-block-start: 1rem;
  border-top: 2px solid currentColor;
}

.recipe-facts div {
  /* `auto` (not `1fr`) on the dt column: "Calories"/"Protein" need only
     their own natural width, so the flexible track goes to dd (the figure
     column) instead -- dd is what actually needs the room, and the SAME
     item F2 fix gives it a real minimum so it wraps at whole-word breaks
     rather than being squeezed down to its own smallest word first. */
  display: grid;
  grid-template-columns: auto minmax(9rem, 1fr);
  gap: 1rem;
  padding-block: 0.5rem;
}

.recipe-facts dt {
  font-weight: 700;
}

.recipe-facts dd {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  /* Item F2: balance wrapped lines instead of a long line + a one-word
     orphan trailing on its own; degrades to the browser default (no-op)
     wherever text-wrap: balance isn't supported yet. */
  text-wrap: balance;
}

/* S2 data-ink convention: the live numeral keeps its full weight/contrast;
   a static unit token ("of", "calories today", "g protein", "cal", "g")
   recedes to the secondary ink so emphasis tracks importance, not just
   presence. One convention, applied at every data register that pairs a
   number with a unit -- the Today headline, the recipe masthead facts,
   an ingredient-amount cell's trailing unit (oz/lb/g/etc), the hero
   next-meal nutri line, and the week-grid meal lines. */
.data-lead .unit,
.recipe-facts dd .unit,
.ingredient-row__amount .unit,
.today-lead-nutri .unit,
.meal-nutri .unit,
.spark-readout .unit {
  color: var(--ink-secondary);
  font-weight: 400;
}

@media (max-width: 48rem) {
  .recipe-masthead-grid {
    grid-template-columns: 1fr;
    gap: 2rem;
  }
}

.recipe-body {
  padding-inline: var(--gutter);
}

/* Front door + error page: the Folio treatment for the pre-auth / error views
   -- same masthead + Fraunces + document body as the rest of the app. */
.front-body { padding-block-start: 3rem; }
.front-lead { font-size: 1.35rem; line-height: 1.55; max-width: 44rem; margin: 0 0 2rem; }
.front-footer {
  margin-block-start: 4rem;
  padding-block-start: 1.25rem;
  border-top: 1px solid var(--rule);
  color: var(--ink-secondary);
  font-size: 0.95rem;
}
.front-footer a { color: var(--accent); }
/* Landing "how it works" -- the real daily loop, as a numbered sequence. */
.front-steps {
  list-style: none; margin: 3rem 0 0; padding: 0;
  display: grid; gap: 1.75rem; max-width: 46rem;
}
@media (min-width: 720px) { .front-steps { grid-template-columns: repeat(3, 1fr); gap: 2rem; } }
.front-step { margin: 0; }
.front-step h3 {
  font-family: var(--font-sans); font-size: 1.05rem; font-weight: 700;
  margin: 0 0 0.4rem; display: flex; align-items: baseline; gap: 0.55rem; color: var(--ink);
}
.front-step__n {
  flex: none; font-family: var(--font-mono); font-size: 0.85rem; font-weight: 700;
  color: var(--accent-contrast); background: var(--accent);
  width: 1.5rem; height: 1.5rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
}
.front-step p { margin: 0; color: var(--ink-muted); line-height: 1.5; max-width: 34rem; }
.front-footer__agents { color: var(--ink-secondary); }
/* Legal pages (privacy / terms) -- plain, readable document prose. */
.legal-body { padding-block-start: 2.5rem; max-width: 46rem; }
.legal-section { margin: 0 0 1.9rem; }
.legal-section h2 {
  font-family: var(--font-sans); font-size: 1.1rem; font-weight: 700;
  margin: 0 0 0.5rem; color: var(--ink);
}
.legal-section p, .legal-section li { color: var(--ink-muted); line-height: 1.6; }
.legal-section ul { margin: 0; padding-inline-start: 1.2rem; }
.legal-section li { margin-block-end: 0.5rem; }
.legal-section a, .legal-body .front-footer a { color: var(--accent); }

/* Data-ink (Folio v2): the recipe's section furniture RECEDES so the dish
   (Fraunces masthead) is the hero and the record CTA is the loud second.
   Drop the decorative 01/02/03 numbers and the heavy 3px rules + huge gaps;
   "Ingredients"/"Method" become quiet eyebrow labels, not shouting heads. */
.recipe-section {
  margin-block-start: 2.75rem;
}

.recipe-section + .recipe-section {
  margin-block-start: 2.75rem;
}

.recipe-section__header {
  display: block;
  padding-block-start: 0.55rem;
  border-top: 1px solid var(--rule);
  margin-block-end: 1rem;
}

.recipe-section__number { display: none; }

.recipe-section__title {
  font-family: "Public Sans", system-ui, sans-serif;
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
/* The record block is the exception -- it is the loud CTA, not a quiet
   subsection: keep its heading present but let the control below carry weight. */
#recipe-record .recipe-section__header { border-top-color: var(--ink); border-top-width: 2px; }

@media (max-width: 48rem) {
  .recipe-section,
  .recipe-section + .recipe-section {
    margin-block-start: 2.25rem;
  }
}

/* Ingredient ledger: a strong column heading + 1px row rules, no enclosing
   box (bible §B). Amount / Ingredient / Pantry-availability columns collapse
   to a stacked two-line row (ingredient, then status below) on mobile. */
.ingredient-heading-row,
.ingredient-row {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr) 14rem;
  gap: 1.5rem;
  align-items: baseline;
}

.ingredient-heading-row {
  padding-block-end: 0.75rem;
  border-bottom: 2px solid var(--ink);
  font-size: 1rem;
  font-weight: 800;
}

.ingredient-row {
  padding-block: 1rem;
  border-bottom: 1px solid var(--rule);
}

.ingredient-row__status--good { color: var(--ink-secondary); }
.ingredient-row__status--caution { color: var(--caution); }
.ingredient-row__status--critical { color: var(--critical); }

@media (max-width: 48rem) {
  .ingredient-heading-row {
    display: none; /* column labels don't survive a single-column stack */
  }

  /* Bible §B mobile example: "1 cup Brown rice" stays one line; status
     moves below. Amount + ingredient share row 1 (two columns); status
     spans both columns on row 2 -- not a 3-line stack. */
  .ingredient-row {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 0.5rem;
    row-gap: 0.35rem;
  }

  .ingredient-row__amount {
    grid-column: 1;
    grid-row: 1;
    font-weight: 700;
  }

  .ingredient-row__name {
    grid-column: 2;
    grid-row: 1;
  }

  .ingredient-row__status {
    grid-column: 1 / -1;
    grid-row: 2;
  }
}

/* Record-this-meal action group: full 44rem measure on desktop, full-width
   stacked buttons on mobile (final production rule 9: controls stay >=48px
   in the client-facing view via .client .button). */
.recipe-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-start: 1.5rem;
}

.recipe-cooked-status {
  margin: 0 0 1.5rem;
}

@media (max-width: 48rem) {
  .recipe-actions {
    display: grid;
    grid-template-columns: 1fr;
  }

  .recipe-actions .button {
    width: 100%;
  }
}

/* The meal-record control -- Folio v2: RECORDING is the point of the recipe,
   the loud second after the dish, so this is a real CTA, not a quiet chip row.
   The rating itself stays understated (three equal PEER choices -- "Loved
   it" / "Not for me" / "Didn't eat this" -- no false default, and no choice
   quieter than another: a planned meal ending unrecorded is exactly as real
   an outcome as a rated one), but the control earns prominence by contrast: a
   large Public Sans prompt and large buttons that fill the accent on hover,
   against the now-quiet sections. Used in the recipe reader's/Briefing's
   record block AND each week-menu row (see
   templates._render_rating_record_control) -- the week row scopes it
   smaller. Display-compliance item F1: this was rendering in Fraunces, the
   app's ONE top-title-only display face (used solely for the dish title/page
   masthead) -- a second Fraunces head on the same page broke that rule.
   Public Sans at a large size/weight keeps the same visual loudness without
   competing with the masthead's own display type. */
.rating-record {
  margin-block-start: 1.25rem;
}

.rating-record__label {
  margin: 0 0 0.85rem;
  font-family: "Public Sans", system-ui, sans-serif;
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--ink);
}

.rating-record__choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
}

/* Each choice is its own real <form> (a real POST, no-JS-safe) -- `contents`
   drops the <form>'s own box so its one <button> lays out directly in the
   `.rating-record__choices` flex row, same as a bare row of buttons would. */
.rating-record__form {
  display: contents;
}

.rating-record__choice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 0.9rem 1.7rem;
  border: 2px solid var(--ink);
  border-radius: 2px;
  background: transparent;
  color: var(--ink);
  font: 700 1.1rem/1.2 var(--font-sans);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.rating-record__choice:hover,
.rating-record__choice:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  /* Item 2: the on-accent contrast token (same one .button--primary uses),
     not a hardcoded #fff -- --accent-contrast resolves to white in light
     but near-black (#06111a) in dark, where --accent itself is a light
     #8cc8ff. A hardcoded white label on that light-blue dark-theme fill
     was ~1.6:1 contrast, effectively invisible on hover/focus. */
  color: var(--accent-contrast);
}

.rating-record__choice:disabled {
  opacity: 0.55;
  cursor: default;
}

/* In a week-menu row the record control is a SECONDARY action, not the hero
   record -- keep it compact (the loud treatment is only for the next-meal
   recipe's own record block). */
.meal-line__actions .rating-record { margin-block-start: 0; }
.meal-line__actions .rating-record__label {
  font-family: "Public Sans", system-ui, sans-serif;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink-secondary);
  margin-block-end: 0.4rem;
}
.meal-line__actions .rating-record__choice {
  min-height: 44px;
  padding: 0.5rem 1rem;
  border-width: 1px;
  font-size: 0.95rem;
}


/* Collapsible panel: the <summary> carries the same visual weight as a .panel
   h2 (uppercase, muted), plus a disclosure caret, so a deferred advanced panel
   (e.g. AI assistant access) reads as a section heading a newcomer can ignore. */
details.panel-collapsible > summary.panel-summary {
  cursor: pointer; list-style: none; font-size: 0.95rem; margin: 0; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.04em; display: flex; align-items: center; gap: 0.45rem;
}
details.panel-collapsible > summary.panel-summary::-webkit-details-marker { display: none; }
details.panel-collapsible > summary.panel-summary::before { content: "\25B8"; font-size: 0.72em; }
details.panel-collapsible[open] > summary.panel-summary::before { content: "\25BE"; }
details.panel-collapsible[open] > summary.panel-summary { margin-bottom: 0.75rem; }

.client-list { list-style: none; padding: 0; margin: 0; }
.client-row { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.client-row:last-child { border-bottom: none; }
.client-row a { text-decoration: none; font-weight: 600; }

.empty-state { color: var(--text-muted); font-style: italic; margin: 0.25rem 0; }
/* The planner's live progress sentence under "Planning your week…": quiet, a
   sign of real activity, not something to read carefully -- but S1's 1rem
   floor sweep means it recedes via muted ink ALONE, never a sub-floor size
   (the old 0.68rem was never a binding decision, just an oversight). */
.plan-progress-detail { color: var(--ink-muted); font-size: var(--fs-label); font-variant-numeric: tabular-nums; margin: -0.1rem 0 0.6rem; letter-spacing: 0.01em; }

/* UX audit blocker #2 (mint_form, app/templates.py's
   render_agent_tokens_panel): a native <legend> straddles its fieldset's
   top border by design -- correct for a one-line legend, but a long one
   that wraps to two lines in a narrow sidebar card gets that border drawn
   through the middle of its own text. Fixed at the source: the fieldset
   is reset to contribute no visible box at all (border/padding/margin
   zeroed), the heading is a plain <p class="field-heading"> living
   OUTSIDE any bordered box, and only the actual controls sit inside one
   real box, ruled on the left (.field-box below). Global, not scoped --
   this is the only <fieldset> in the app (grep before adding a second).
*/
fieldset { border: 0; padding: 0; margin: 0; }
.field-heading { font-size: var(--fs-label); font-weight: 600; color: var(--text); margin: 0 0 0.5rem; }
/* S3 de-chrome: the Folio idiom (see .recipe-card below) -- a single
   3px semantic left-rule + padding-left, no all-round border/radius/
   background slab. Neutral --rule (this box has no single semantic
   state of its own). */
.field-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  border-left: 3px solid var(--rule);
  border-radius: 0;
  padding-left: 1rem;
}
.field-box label { flex-basis: 100%; font-size: var(--fs-label); color: var(--text-muted); }
.field-box input[type="text"] {
  flex: 1;
  min-width: 120px;
  padding: 0.5rem 0.65rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--panel-bg);
  color: var(--text);
}

/* -- forms / buttons -- */
.inline-form { display: flex; gap: 0.5rem; }
.inline-form input[type="text"] {
  flex: 1;
  padding: 0.5rem 0.65rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--panel-bg);
  color: var(--text);
}
.form-feedback { min-height: 1.2em; font-size: var(--fs-label); margin: 0.4rem 0 0; }
.form-feedback.is-error { color: var(--error); }
/* S2 color discipline: a successful save is routine, not an alert -- neutral
   ink, the '✓ ' glyph prefixed onto the message itself (never colour alone).
   is-error stays --error: a genuine attention state. */
.form-feedback.is-ok { color: var(--ink); }

/* -- workspace layout -- */
/* ============================================================================
   THE BRIEFING -- client workspace, five flowing jobs (this rebuild). The
   old numbered six-chapter design is gone; #workspace is now just
   `.document-content` (already styled by the foundation above), holding
   five `.briefing` sections in order of immediacy. Every job below reuses
   `.briefing`/`.button`/`.ledger-row` from that same foundation, adding
   only what a job genuinely needs.
   ============================================================================ */

/* -- Publication masthead: workspace-specific running-line bits ---------- */
.masthead-running-line__right {
  display: flex;
  align-items: baseline;
  gap: 1.25rem;
  flex-wrap: wrap;
}
.masthead-nav-link {
  font: inherit;
  color: var(--masthead-link);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.16em;
}
.masthead-signout { display: contents; } /* the <form> contributes no box of its own to the flex row */

/* -- Minimap jump-rail (founder's brief: a real Table of Contents reads as
   "LARP" -- a thin VS-Code-minimap-style rail instead; "KEEP IT... make it
   good"). Desktop: a narrow fixed column pinned to the viewport's right
   edge, sized to `--minimap-width` -- the SAME width `.document-content`
   reserves as real padding (above), so the rail has its own lane and never
   overlaps live text regardless of viewport width. Mobile: collapses to a
   thin sticky strip of the five jobs across the top -- never a big panel.
   ------------------------------------------------------------------------ */
.folio-minimap {
  position: fixed;
  top: 50%;
  right: 0;
  width: var(--minimap-width);
  box-sizing: border-box;
  padding-inline-end: 1.25rem;
  transform: translateY(-50%);
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  border-right: 1px solid var(--rule); /* the rail spine the active tick sits on */
}
.folio-minimap__entry {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 0.55rem;
  padding: 0.5rem 0.9rem 0.5rem 0;
  margin-right: -1px; /* pull the marker border onto the spine */
  color: var(--ink-secondary);
  text-decoration: none;
  border-right: 2px solid transparent;
  line-height: 1.15;
}
.folio-minimap__entry:hover { color: var(--ink); }
.folio-minimap__number {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.folio-minimap__label {
  font-size: 0.95rem;
  font-weight: 600;
  white-space: nowrap;
}
.folio-minimap__entry.is-current {
  color: var(--ink);
  border-right-color: var(--accent);
}
.folio-minimap__entry.is-current .folio-minimap__number { color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .folio-minimap__entry { transition: color 0.15s ease, border-color 0.15s ease; }
}

@media (max-width: 64rem) {
  /* Collapse to a thin sticky strip of the five job numbers, evenly spaced
     across the top -- never a big panel. Labels + line-density texture
     hide; the numbers alone are enough to jump, and the current one still
     highlights. */
  .folio-minimap {
    position: sticky;
    top: 0;
    right: auto;
    transform: none;
    flex-direction: row;
    justify-content: space-between;
    gap: 0;
    padding: 0.5rem var(--gutter);
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
    z-index: 20;
  }
  .folio-minimap__entry {
    flex-direction: row;
    align-items: center;
    padding: 0.25rem;
    border-right: none;
    border-bottom: 3px solid transparent;
  }
  .folio-minimap__entry.is-current { border-bottom-color: var(--accent); }
  .folio-minimap__entry { margin-right: 0; border-right: none; }
  .folio-minimap__label { display: none; }
  .folio-minimap__number { font-size: 0.9rem; color: var(--ink-secondary); }
}

/* -- Job #5, part 1: Your coach -- the correspondence, ruled blocks, never
   chat bubbles ------------------------------------------------------------ */
.chat-pane {
  display: flex;
  flex-direction: column;
}
.chat-log {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin-bottom: 1.5rem;
}
/* Each turn is a plain ruled block: a 1px top rule, an author + timestamp
   meta line, then the turn's own content -- explicitly NOT a coloured,
   rounded bubble (final production rule 10). */
.msg {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding-block: 1.25rem;
  border-top: 1px solid var(--rule);
}
.msg:first-child { border-top: none; padding-block-start: 0; }
.msg-meta {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin-block-end: 0.5rem;
}
.msg-meta__author { font-weight: 800; }
.msg-meta__time { color: var(--ink-secondary); font-size: 1rem; }
.msg-bubble {
  max-width: 60ch;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.msg-bubble.coach-html { white-space: normal; }
.msg-bubble.streaming { color: var(--ink-muted); }
.empty-hint { font-style: italic; color: var(--ink-muted); }

/* Skeleton bubble (coach-turn survival redesign): appended the instant
   POST .../coach's response lands (before any delta -- see app/static/
   app.js's submit handler) and reconstructed identically by app/templates.py's
   render_skeleton_bubble on an SSR reload/reconnect while a turn is still
   queued/running. */
.msg-skeleton-status { font-size: 1rem; color: var(--ink-secondary); font-style: italic; margin-block-end: 0.5rem; }
.msg-skeleton-status:empty { display: none; }
/* Ephemeral thinking scroll -- small, fixed-height, auto-scrolling; capped
   client-side (see app/static/app.js) so a long thinking phase can't grow
   the DOM unboundedly. Cleared the instant the first real reply token
   arrives -- never persisted, never shown after the turn ends. */
.msg-skeleton-thinking {
  max-height: 4.5rem;
  overflow-y: auto;
  font-size: 1rem;
  color: var(--ink-secondary);
  white-space: pre-wrap;
  border-left: 2px solid var(--rule);
  padding-left: 0.5rem;
  margin-block-end: 0.5rem;
}
.msg-skeleton-thinking:empty { display: none; }

/* -- image attachments: in the transcript, and in the composer ------------ */

/* Transcript: thumbnails big enough to recognize the item, small enough that a
   receipt + haul photo don't push the conversation off screen. Click opens the
   untouched original (see templates.render_attachments). */
.msg-images {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  justify-content: flex-end;
  margin-bottom: 0.35rem;
  max-width: 85%;
}
.msg-image {
  display: block;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  line-height: 0;
}
.msg-image:hover { border-color: var(--accent); }
.msg-image img {
  display: block;
  width: 160px;
  height: 160px;
  object-fit: cover;
  background: var(--bg);
}
/* Both an uploaded-image thumbnail and a composer preview chip are clickable
   to enlarge (app.js's initLightbox) -- the cursor says so, since neither is
   an obvious control otherwise. */
.msg-image, .preview-tile img { cursor: zoom-in; }

/* Image lightbox (app.js's initLightbox): an in-page overlay, NOT a new tab.
   Fixed, full-viewport, above everything. Deliberately a very dark scrim in
   BOTH themes -- an enlarged photo reads best against near-black regardless
   of the page's own light/dark mode, so this one surface does not follow the
   theme tokens the rest of the app does. */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  background: rgba(0, 0, 0, 0.82);
}
.lightbox-overlay[hidden] { display: none; }
/* Background scroll lock while the lightbox is open -- the page behind must
   not scroll under the scrim. Set/removed on <body> by initLightbox. */
body.lightbox-open { overflow: hidden; }
.lightbox-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
}
.lightbox-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  line-height: 1;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 50%;
  cursor: pointer;
}
.lightbox-close:hover { background: rgba(0, 0, 0, 0.8); }

/* Composer: what's in this strip is exactly what will be sent.
   DEFECT FIX: an in-spec large batch (up to MAX_IMAGES=99 -- see app.js)
   used to grow this strip unboundedly, pushing the Send button and any
   rejection message (#chat-feedback, below the form) off the bottom of the
   pane with no way to tell they were still there. Bounded height + its own
   scrollbar keeps the strip itself scrollable while Send/the composer
   form/feedback stay put directly beneath it, on any viewport. */
.image-previews {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  max-height: 35vh;
  overflow-y: auto;
}
.image-previews[hidden] { display: none; }
.preview-tile {
  position: relative;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
}
.preview-tile img {
  display: block;
  width: 84px;
  height: 84px;
  object-fit: cover;
}
.preview-meta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.1rem 0.25rem;
  font-size: 0.65rem;
  text-align: center;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
}
.preview-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 24px;
  height: 24px;
  padding: 0;
  line-height: 1;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  color: #fff;
  background: rgba(0, 0, 0, 0.65);
  font-size: 0.75rem;
}
.preview-remove:hover { background: var(--error); }

/* Per-chip upload status (uploading -> ready/failed) -- the honest
   "what will actually be sent" signal the founder asked for. Color is
   never the only channel: .preview-meta's own text carries an icon + a
   real word (see app.js's _previewMetaText), same discipline as
   .op-row-* below. */
.preview-tile-uploading { border-color: var(--border-strong); }
.preview-tile-ready { border-color: var(--success); }
.preview-tile-failed { border-color: var(--error); }
.preview-tile-failed img { opacity: 0.45; }
.preview-item { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; max-width: 84px; }
.preview-error {
  font-size: 0.68rem;
  color: var(--error);
  text-align: center;
  overflow-wrap: break-word;
}

.composer-hint { font-size: var(--fs-label); margin: 0.35rem 0 0; }

/* Drop target: the WHOLE chat pane, and it says so while a file is over it --
   a drag gesture with no visible target is a gesture nobody discovers. */
.chat-pane.drag-over {
  outline: 2px dashed var(--accent);
  outline-offset: -4px;
  background: var(--info-bg);
}

/* Per-op checklist -- replaces the old raw-XML "Coach ran an action" trace.
   Three states per row (running/ok/failed), color-coded per the file header
   rule above; skipped gets its own neutral treatment. Color is never the
   only channel: every state also carries a distinct icon glyph + text. */
/* S3 de-chrome: same Folio idiom as .field-box above -- a 3px neutral
   left-rule (the per-row colours already carry the real state, see the
   .op-row-* rules below), no all-round border/radius/background slab. */
.op-checklist {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  /* S1 floor sweep: this is the inherited base size for .op-row-text (each
     row's own PRIMARY label) -- .op-row-detail/.op-row-raw below are
     explicitly raised to the 1rem floor already, so leaving this container
     at the old 0.85rem would invert the row (its own sub-detail reading
     larger than its own headline). */
  font-size: var(--fs-label);
  border-left: 3px solid var(--rule);
  border-radius: 0;
  padding-left: 0.7rem;
  margin: 0.35rem 0;
}
.op-row { display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; }
.op-row-icon { flex: 0 0 auto; }
.op-row-detail { flex-basis: 100%; font-size: var(--fs-label); color: var(--text-muted); }
.op-row-raw { flex-basis: 100%; font-size: var(--fs-label); color: var(--text-muted); }
.op-row-raw summary { cursor: pointer; }
.op-row-raw pre { white-space: pre-wrap; overflow-wrap: break-word; background: var(--bg); border-radius: 4px; padding: 0.4rem; margin: 0.3rem 0 0; }
/* Consolidated vision-extraction row's own "which photos" disclosure (see
   app/static/app.js's renderVisionSummaryDone) reuses .op-row-raw's
   collapsed-by-default idiom but lists names/reasons rather than one <pre>
   blob, so it needs its own tidy list styling instead of .op-row-raw pre's. */
.op-row-raw ul { margin: 0.3rem 0 0; padding-left: 1.1rem; }

.op-row-running .op-row-icon,
.op-row-running .op-row-text { color: var(--text-muted); }
.op-row-ok .op-row-icon,
.op-row-ok .op-row-text { color: var(--success); }
/* Partial failure -- SOME images read fine, so this is deliberately NOT the
   same red as a total failure (op-row-failed below): collapsing "2 of 30
   couldn't be read" into the same visual severity as "none of your images
   could be read" would misrepresent a mostly-successful batch as a broken
   one (ux-quick-reference's "Status Must Be True" -- never collapse a real,
   distinct outcome into the wrong pole just because both involve a failure). */
.op-row-warning .op-row-icon,
.op-row-warning .op-row-text,
.op-row-warning .op-row-detail { color: var(--warning); }
.op-row-failed .op-row-icon,
.op-row-failed .op-row-text,
.op-row-failed .op-row-detail { color: var(--error); }
.op-row-skipped .op-row-icon,
.op-row-skipped .op-row-text,
.op-row-skipped .op-row-detail { color: var(--text-muted); }

/* S3 de-chrome: a 3px semantic left-rule + padding-left, no all-round
   border/radius/background slab -- the caution/critical hue moves onto the
   rule; the icon + word already carried inside every .callout-nudge/
   .callout-error usage still carries the meaning (never colour alone). */
.callout {
  border-left: 3px solid var(--rule);
  border-radius: 0;
  padding-left: 0.7rem;
  font-size: var(--fs-label);
}
.callout-nudge { border-left-color: var(--caution); }
.callout-error { border-left-color: var(--critical); }
/* S1 1rem floor: the read-once bearer secret (render_agent_tokens_panel)
   would otherwise inherit .callout's own font-size -- called out explicitly
   so it never silently drifts if .callout's size ever changes. */
.agent-token-value { font-size: var(--fs-label); }

/* Nutrition-oracle attribution -- the structural fix for a citation the
   oracle actually retrieved (app/oracle.py) never reaching the screen unless
   the model's own prose happened to retype it. Same visual language as
   .op-checklist above (--info accent for "here's real provenance") so it
   reads as part of the same "the coach shows its work" idiom, not a
   one-off. Rendered server-side only (app/templates.py's
   render_citation_panel) -- see that function's docstring for why every
   string inside it is treated as untrusted.
   S3 de-chrome: a 3px semantic left-rule + padding-left, no all-round
   border/radius/background slab (same Folio idiom as .op-checklist/
   .callout above). */
.citation-panel {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: var(--fs-label);
  border-left: 3px solid var(--info);
  border-radius: 0;
  padding-left: 0.7rem;
  margin: 0.35rem 0;
}
.citation-panel-header { display: flex; align-items: baseline; gap: 0.4rem; color: var(--info); font-weight: 600; }
.citation-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.citation-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem; }
.citation-item a { font-weight: 600; }
.citation-meta { font-size: var(--fs-label); color: var(--text-muted); }
.citation-none { margin: 0; color: var(--text-muted); font-style: italic; }
/* S1: this is a real safety notice (a retrieved source contained a
   suspicious embedded instruction) -- raised above the plain 1rem label
   floor to the reading-body size so it reads as substance, not fine print. */
.citation-warning { margin: 0; color: var(--warning); font-size: var(--fs-body); }

/* Pending/unresolved variants of .citation-panel (app/static/app.js's
   appendSearchPendingPanel / resolveStuckSearchPanels) -- same shell (size,
   padding) so the search_call -> search_result swap is a same-slot
   replace, never a resize. Deliberately NOT --info's blue: that accent
   means "here's a real, retrieved source," which neither of these states
   has yet (or ever will, for -unresolved). No background slab (S3). */
.citation-panel-pending { border-left-color: var(--rule-strong); }
.citation-panel-pending .citation-panel-header { color: var(--text-muted); font-weight: 400; }
.citation-panel-unresolved { border-left-color: var(--error); }
.citation-panel-unresolved .citation-panel-header { color: var(--error); font-weight: 400; }

.coach-status {
  font-size: var(--fs-label);
  color: var(--text-muted);
  padding: 0.35rem 0;
  border-top: 1px solid var(--border);
  margin-bottom: 0.5rem;
}
.coach-status[hidden] { display: none; }
.coach-status.is-error { color: var(--error); }
.coach-status.is-nudge { color: var(--warning); }

/* The composer as a full-width document footer (bible §6.F): a top rule,
   then the textarea + attach + Send controls -- not a boxed card. */
.correspondence-composer {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding-block-start: 1.5rem;
  border-top: 2px solid var(--ink);
}
.chat-form textarea {
  flex: 1 1 20rem;
  min-width: 0;
  resize: vertical;
  min-height: 3em;
  padding: 0.75rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
}
@media (max-width: 48rem) {
  .correspondence-composer { flex-direction: column; }
  .correspondence-composer .button { width: 100%; }
}

/* -- status badges: icon + word + colour, never colour alone. Rectangular
   (final production rule 10: no pills) -- used by the plan-status states
   below AND by the dashboard/roster (a shared, cheap fix: de-pilling this
   one rule removes the pill shape everywhere it's used, not just here).
   Item 14: font-size raised to the 1rem Folio type floor (was 0.85rem). */
.status-pill { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.6rem; border-radius: 0; font-size: var(--fs-label); margin: 0 0 0.5rem; }
.status-queued { background: var(--info-bg); color: var(--info); }
.status-running { background: var(--info-bg); color: var(--info); }
.status-failed { background: var(--error-bg); color: var(--error); }
/* Technical reason behind a plain failure headline -- muted, secondary, opt-in. */
.status-detail { color: var(--text-muted); font-size: var(--fs-label); margin: 0 0 0.5rem; }
.status-detail > summary { cursor: pointer; }

/* Offline banner: a dropped connection made visible, so the 3s-polled panels
   aren't silently frozen (status must be true). Warning-weight, not error-red --
   this is a degraded state, not a failure. Hidden via [hidden] until app.js
   toggles it on window 'offline'/'online'. */
.offline-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  padding: 0.5rem 1rem; text-align: center; font-size: var(--fs-label);
  background: var(--warning-bg); color: var(--warning);
  border-bottom: 1px solid var(--warning);
}
.offline-banner[hidden] { display: none; }

/* S4 honest degraded state: a specific fragment whose poll/refresh request
   failed (app/static/app.js's htmx:responseError/htmx:sendError handlers)
   -- degraded, not an error (the fragment keeps showing its last-known
   content; only this fragment is marked, the rest of the page stays live).
   No card/background -- a quiet ruled caption, muted like the offline
   banner's register but without its alarm weight. Cleared per-fragment by
   the htmx:afterSwap handler on the next successful refresh. */
.is-stale { opacity: 0.85; }
/* Item 17: now the panel's FIRST child (app/static/app.js's markFragmentStale
   insertBefore's it, rather than appendChild'ing it as the last child) so it
   sits with the top metric it's actually covering instead of trailing below
   the whole fragment -- the rule now separates it from the real content
   BELOW it, not above. */
.stale-caption {
  margin: 0 0 0.5rem;
  padding-block-end: 0.35rem;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-muted);
  font-size: var(--fs-label);
}
.status-infeasible { background: var(--warning-bg); color: var(--warning); }
.status-unknown { background: var(--skeleton); color: var(--text-muted); }
.status-warning { background: var(--warning-bg); color: var(--warning); }
/* Roster fact cell: a pill inline in a `.roster-fact`, not a standalone block
   -- drop the block-spacing margin .status-pill carries elsewhere so it sits
   on the fact's own baseline instead of pushing the row taller. */
.roster-fact .status-pill { margin: 0; }
.goal-summary { font-size: var(--fs-label); margin-top: 0.2rem; font-variant-numeric: tabular-nums lining-nums; }

/* -- Trainer roster (Working Folio rebuild, reworked 2026-07-21): a plain
   ruled document list, never a boxed `<table>` -- each client is one
   `.roster-row` (`.ledger-row` gives it the shared border-bottom rule).
   `.roster-row__main` is the AT-A-GLANCE line every trainer scans first: the
   headline name link, this client's own two sparklines (`.roster-sparklines`
   below), then the highest-signal facts (Last active/Cooked this week).
   Everything routine -- Invite/Plan/Goals, the goals+notes editor -- moves
   into `.roster-row__detail`, a native `<details>` disclosure (same idiom as
   `.goal-editor` one level in) so the collapsed row stays scannable down a
   long roster and the detail is one tap away, never gone. */
.roster-list { border-top: 2px solid var(--ink); }
.roster-row { display: flex; flex-direction: column; gap: 0.75rem; padding-block: 1.25rem; }
.roster-row__main { display: flex; flex-wrap: wrap; align-items: baseline; gap: 1.5rem; }
.roster-row__name { font-size: 1.25rem; font-weight: 700; }
.roster-row__facts { display: flex; flex-wrap: wrap; gap: 1.25rem 2rem; flex: 1 1 100%; }
.roster-fact { display: flex; flex-direction: column; gap: 0.3rem; min-width: 8rem; }
.roster-fact__label {
  font-size: var(--fs-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-secondary);
}

/* The roster's OWN small variant of `.folio-sparklines` (app/templates.py's
   _render_history_sparklines, called with wrapper_cls="roster-sparklines")
   -- the SAME sparkline markup/semantics (deviation-area fill, hatch
   greens/reds), just laid out side-by-side and capped narrow so a pair of
   them reads as one glance-height line item per client, never the full
   folio-pane-width pair the client's own workspace draws. */
.roster-sparklines {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  flex: 1 1 100%;
  max-width: 36rem;
}
.roster-sparklines .spark { flex: 1 1 13rem; max-width: 16rem; }

/* The roster's per-client detail disclosure -- collapsed by default (never
   open), holding the routine Invite/Plan/Goals facts plus the goals+notes
   editor, which nests one level further in as its own `.goal-editor`. Same
   quiet accent-link summary style as `.goal-editor`'s own summary, native
   marker kept (the plain triangle IS the "there's more" affordance -- no
   extra copy needed). */
.roster-row__detail > summary { cursor: pointer; color: var(--accent); font-size: 0.9rem; padding: 0.2rem 0; }
.roster-row__detail[open] > summary { margin-bottom: 0.5rem; }

/* Utility row: the "back / identity" + sign-out line shared by the two
   document headers this task Folio-ified (render_dashboard, render_settings_
   page) -- a ruled line right under the masthead, not a topbar. */
.utility-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block-end: 1.5rem;
  margin-block-end: 2.5rem;
  border-bottom: 1px solid var(--rule);
}

/* Agent tokens (Settings, owner-only): the same ruled-document-list idiom as
   the roster above -- one `.token-row` per token, never a boxed `<table>`. */
.token-list { border-top: 2px solid var(--ink); }
.token-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1rem;
  border-bottom: 1px solid var(--rule);
}
.token-row__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 1rem; }
.token-row__label { font-weight: 700; }

/* Item 12: revoke is irreversible, so it opens behind a native <details>
   disclosure instead of firing on the first click -- NOT revoking stays
   the resting default (the summary alone changes nothing), and the real
   destructive submit only appears once opened. The summary itself carries
   .button/.button--secondary (a real, obviously-clickable control, not a
   quiet toggle), so overriding `display` to the button's inline-flex is
   what actually removes the native marker in modern browsers; the
   ::-webkit-details-marker rule below is belt-and-braces for older engines
   that still render one under a flex display. */
.revoke-confirm > summary::-webkit-details-marker { display: none; }
.revoke-confirm__body { margin-block-start: 0.6rem; max-width: 22rem; }
.revoke-confirm__body p { margin: 0 0 0.6rem; }

/* Per-client goals+notes editor: a collapsed-by-default <details> tucked
   directly inside the client's own `.roster-row`, right under its facts --
   never a separate row now that the roster isn't a `<table>`. */
.goal-editor > summary { cursor: pointer; color: var(--accent); font-size: 0.9rem; padding: 0.2rem 0; }
.goal-editor[open] > summary { margin-bottom: 0.5rem; }
.goal-form { display: flex; flex-direction: column; gap: 0.6rem; max-width: 46rem; }
.goal-fields { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.goal-fields label,
.goal-notes-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: var(--fs-label);
  color: var(--text-muted);
}
.goal-fields label { flex: 1 1 7rem; }
.goal-fields input,
.goal-notes-label input,
.goal-notes-label textarea {
  padding: 0.4rem 0.55rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--panel-bg);
  color: var(--text);
  font: inherit;
}
/* The two block-level free-text fields (cooking time, standing orders, notes)
   fill their label; the flex .goal-fields inputs keep their own sizing. */
.goal-notes-label input,
.goal-notes-label textarea { width: 100%; }
.goal-notes-label textarea { resize: vertical; }
.goal-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.goal-actions .form-feedback { margin: 0; }

/* ---------------------------------------------------------------------------
   Settings "Your goals" panel (section 01): macro pie + weight projection +
   every planner input. Server-rendered SVG; app/static/app.js's initGoalsPanel
   updates the arcs/line/labels live as the inputs change.
   --------------------------------------------------------------------------- */
.goals-editor { display: flex; flex-direction: column; gap: 0.85rem; max-width: 48rem; }
.goals-sub { margin: -0.4rem 0 0.15rem; font-size: var(--fs-label); }
.goals-editor .field-heading { margin-top: 0.4rem; }
.goals-empty-note { margin-top: 0.75rem; }

/* Macro-targets row: inputs on the left, the donut on the right, wrapping to a
   single column on narrow screens. */
.goals-grid { display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: center; }
.goals-fields { flex: 1 1 18rem; min-width: 15rem; }
.goals-pie-wrap { flex: 0 0 auto; margin-inline: auto; }

.macro-pie { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; }
.macro-pie-svg { width: 168px; height: 168px; display: block; }
.macro-arc-track { stroke: var(--rule); stroke-opacity: 0.35; }
.macro-arc { transition: stroke-dasharray 0.25s ease, stroke-dashoffset 0.25s ease; stroke-linecap: butt; }
.macro-arc--protein { stroke: var(--macro-protein); }
.macro-arc--carbs { stroke: var(--macro-carbs); }
.macro-arc--fat { stroke: var(--macro-fat); }
.macro-pie-total {
  font-family: var(--font-sans, system-ui, sans-serif);
  font-size: 30px; font-weight: 700; fill: var(--text);
  font-variant-numeric: tabular-nums;
}
.macro-pie-unit { font-size: 12px; fill: var(--text-muted); letter-spacing: 0.04em; }

.macro-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; min-width: 12rem; }
.macro-legend__row { display: grid; grid-template-columns: 0.9rem 1fr auto auto; align-items: center; gap: 0.5rem; font-size: var(--fs-label); }
.macro-legend__swatch { width: 0.8rem; height: 0.8rem; border-radius: 2px; }
.macro-legend__swatch--protein { background: var(--macro-protein); }
.macro-legend__swatch--carbs { background: var(--macro-carbs); }
.macro-legend__swatch--fat { background: var(--macro-fat); }
.macro-legend__name { color: var(--text); }
.macro-legend__value { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.macro-legend__pct { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; min-width: 2.6rem; text-align: right; }

.goal-band { margin: 0.1rem 0 0; font-size: var(--fs-label); }
.goal-band strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* Projected-weight line chart (full-width, with a weight axis + week ticks). */
.weight-proj { display: flex; flex-direction: column; gap: 0.5rem; }
.weight-proj--empty { padding: 0.75rem 0; }
.weight-proj-summary { margin: 0; font-size: var(--fs-body); color: var(--text); font-weight: 600; }
.weight-proj-svg { width: 100%; height: auto; display: block; }
.weight-proj-grid { stroke: var(--rule); stroke-opacity: 0.45; stroke-width: 1; }
.weight-proj-ylabel { font-size: 13px; fill: var(--text-muted); font-variant-numeric: tabular-nums; }
.weight-proj-area { fill: var(--accent); stroke: none; opacity: 0.12; }
.weight-proj-line { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.weight-proj-node { fill: var(--accent); opacity: 0.9; }
.weight-proj-start-dot { fill: var(--paper); stroke: var(--accent); stroke-width: 2.25; }
.weight-proj-dot { fill: var(--accent); stroke: var(--paper); stroke-width: 2; }
.weight-proj-start, .weight-proj-end { font-size: 15px; fill: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.weight-proj-tick { font-size: 13px; fill: var(--text-muted); }
.weight-proj-basis { margin: 0; font-size: var(--fs-label); }
.weight-proj-basis strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* Auto-save status line (replaces the old Save button on the goals panel). */
.goals-autosave { min-height: 1.4rem; }

.goals-editor select {
  font: inherit; padding: 0.5rem 0.6rem; border: 1px solid var(--border);
  border-radius: 4px; background: var(--paper); color: var(--text); width: 100%;
}
.goals-editor select:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

@media (max-width: 40rem) {
  .goals-grid { flex-direction: column; align-items: stretch; }
  .goals-pie-wrap { margin-top: 0.5rem; }
}

/* ---------------------------------------------------------------------------
   Settings "Meal structure" (section 02): per-meal name + share (%) rows with
   a live per-meal calorie preview and a running-total guard. app.js's
   initMealStructure adds/removes rows and updates the previews/total.
   --------------------------------------------------------------------------- */
.meal-structure-form { display: flex; flex-direction: column; gap: 0.6rem; max-width: 46rem; }
.meal-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.meal-row {
  display: grid;
  grid-template-columns: minmax(8rem, 1fr) 6.5rem auto auto;
  gap: 0.6rem; align-items: end;
}
.meal-row__label { display: block; font-size: var(--fs-label); color: var(--text-muted); margin-bottom: 0.2rem; }
.meal-row input {
  width: 100%;
  padding: 0.4rem 0.55rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--panel-bg);
  color: var(--text);
  font: inherit;
}
.meal-row__share-input { position: relative; }
/* Hide the native number spinner so it can't collide with the custom "%"
   suffix (a redteam caught the up/down arrows overlapping the % on focus). */
.meal-row__share-input input {
  padding-right: 1.6rem;
  appearance: textfield;
  -moz-appearance: textfield;
}
.meal-row__share-input input::-webkit-outer-spin-button,
.meal-row__share-input input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.meal-row__pct { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
.meal-row__cal { font-size: var(--fs-label); color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; padding-bottom: 0.45rem; min-width: 4.5rem; }
.meal-row__remove { align-self: end; }
.meal-structure__controls { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-top: 0.3rem; }
.meal-total { margin: 0; font-size: var(--fs-body); }
.meal-total strong { color: var(--text); font-variant-numeric: tabular-nums; }
.meal-total__flag { font-weight: 600; }
.meal-total__flag--ok { color: var(--good); }
.meal-total__flag--off { color: var(--caution); }
.meal-structure-error { margin: 0.1rem 0 0; font-size: var(--fs-label); color: var(--critical); }

@media (max-width: 40rem) {
  .meal-row { grid-template-columns: 1fr 5.5rem; grid-auto-rows: auto; }
  .meal-row__cal { grid-column: 1 / -1; padding-bottom: 0; }
  .meal-row__remove { grid-column: 1 / -1; justify-self: start; }
}

/* Liveness proof for a running/queued job -- NOT a percentage, NOT a bar,
   NOT an ETA (we genuinely don't know how long is left; a bar that implied
   otherwise would be a lie). A plain pulsing dot alongside the real,
   sourced sentence (see app/templates.py's render_plan_status /
   app/static/app.js's renderPlanStatusFragment) is what "directly proves
   we are still working" without pretending to know more than we do. */
.status-pulse {
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  margin-left: 0.3rem;
  border-radius: 50%;
  background: currentColor;
  animation: status-pulse-beat 1.4s ease-in-out infinite;
  vertical-align: middle;
}
@keyframes status-pulse-beat {
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .status-pulse { animation: none; opacity: 0.7; }
}
/* The live 'planner is working' readout inside the chat (#chat-plan-status):
   sits just below the last message so a distracted user sees motion where they
   expect the coach to speak. Empty (no box) when nothing is running. */
.chat-working { margin: 0.1rem 0 0.5rem; }
.chat-working .status-pill { display: inline-flex; align-items: center; gap: 0.4rem; }
.counterfactual { font-size: var(--fs-label); color: var(--text-muted); }
.counterfactual p { margin: 0 0 0.35rem; }
.counterfactual-pending { color: var(--info); font-style: italic; }
.counterfactual-failed { color: var(--error); }
.shopping-list-top, .shopping-list-rest ul { margin: 0; padding-left: 1.2rem; }
.shopping-list-rest { margin-top: 0.3rem; }
.shopping-list-rest summary { cursor: pointer; }
.shopping-list-rest ul { margin-top: 0.3rem; }

/* -- the minimal-rescue answer ("buy a pack of ramen and an onion and this
   works", engine/counterfactual.py's find_minimal_rescue) -- a VERIFIED,
   stronger claim than .counterfactual's category probe above, so it gets
   its own visual weight (bordered callout, not muted small print) when a
   real fix was found; pending/failed/timeout states stay quiet like their
   .counterfactual-* counterparts so an in-progress or inconclusive search
   never competes visually with a real answer. -- */
.verified-rescue { font-size: var(--fs-label); margin-bottom: 0.5rem; }
.verified-rescue p { margin: 0; }
.verified-rescue-pending { color: var(--info); font-style: italic; }
.verified-rescue-failed { color: var(--error); }
.verified-rescue-timeout { color: var(--warning); font-style: italic; }
.verified-rescue-none { color: var(--text-muted); }
.verified-rescue-found {
  color: var(--text);
  border-left: 3px solid var(--good);
  border-radius: 0;
  padding-left: 0.65rem;
}

/* -- honest macro-uncertainty range (docs/UNCERTAINTY.md §4) -- computed
   from the coach's own stated <macro_bounds>, never a fabricated width --
   see app/templates.py's _render_week_macro_range. */
/* -- Calorie-vs-target chart (app/templates.py's _render_calorie_chart):
   inline server-rendered SVG, strict-CSP-safe (no script, no external lib).
   Neutral bars, ONE accent for the trendline, a dashed neutral target line,
   and an amber diamond for logged intake -- every series also carries a
   shape + legend swatch + <title>, so colour is never the only signal. The
   SVG scales by its viewBox; the figure is capped so it doesn't blow the
   axis text up huge on a wide desktop column, and shrinks to fit a phone. */
.cal-chart {
  margin: 0 0 0.7rem;
  max-width: 680px;
}
/* The at-a-glance headline: the one line a distracted reader takes in. Big
   and first; the chart below is the supporting detail. S2 color discipline:
   on-track is routine, not an alert -- plain ink (matching its
   .status--good sibling), never a green fill; amber high / muted under
   stay the two real attention-worthy departures from routine. */
.cal-chart-headline {
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -0.01em;
  margin: 0 0 0.15rem;
  text-wrap: balance;
}
.cc-head-good { color: var(--ink); }
.cc-head-over { color: var(--warning); }
.cc-head-under { color: var(--ink-muted); }
.cc-head-neutral { color: var(--text); }
.cal-chart-cap {
  font-size: 0.8rem;
  color: var(--text-muted);
  margin: 0 0 0.6rem;
  font-weight: 500;
}
.cal-chart svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
/* --- CALORIE BARS --------------------------------------------------------
   An array of FULL-HEIGHT rounded-capsule columns (all the same height), one
   per day, over a left calorie y-axis. A no-data day is a uniform grey column;
   a data day is a column (protein-tinted -- quiet grey when met/unknown, the
   ONE reserved caution hue when materially short, item 14) with a band over
   its low..high calorie range and a most-likely mark. A smooth trend joins
   the most-likely values; a dotted goal line marks the target; a subtle
   vertical rule marks today (item 13).
   The column + band `fill` (and their `fill-opacity`) are set PER-BAR as
   presentational attributes (protein-adequacy state, CSP-safe), so
   .cc-cap-col / .cc-cap-band set NO fill -- a CSS `fill` would override the
   attribute (presentational attrs are the lowest-priority source) and flatten
   every bar to one colour. */
.cal-bars { max-height: 200px; }
/* Left calorie axis: hairline gridlines that recede + light tick labels. */
.cc-axis { stroke: var(--border); stroke-width: 1; stroke-opacity: 0.35; }
.cc-axis-label { fill: var(--text-muted); font-size: 14px; opacity: 0.85; }
/* No-data day: a barely-there grey track -- present, but it recedes so the real
   bars carry the eye. */
.cc-cap-nodata { fill: var(--text-muted); fill-opacity: 0.09; }
/* Most-likely dot: a small white dot ringed in neutral ink so it reads on
   any bar hue AND on bare paper, and the trend threads cleanly through it.
   Item 19: the ring used to be `var(--panel-bg)` -- the SAME white as the
   paper itself on a no-range, protein-met, isolated day (no band behind it,
   no >=2-day trend line through it), so the mark was white-on-white
   (~1.05:1 contrast) and read as no-data. `--text-muted` gives it a real,
   theme-correct neutral-ink outline (dark ink on light paper, light ink on
   dark paper) instead of relying on whatever happens to sit behind it.
   `--short` (item 12) swaps it to a HOLLOW ring in the caution colour for a
   materially protein-short day -- a shape channel independent of the
   column's own fill, so a colour-deficient or screen-reader reader can
   tell a short day from an adequate one without relying on hue at all. */
.cc-cap-likely { fill: #ffffff; stroke: var(--text-muted); stroke-width: 1; }
.cc-cap-likely--short { fill: none; stroke: var(--caution); stroke-width: 1.5; }
/* Item 13: is_future columns/bands/marks render quieter (a plain opacity
   fade, NOT the protein fill/shape channel) so an upcoming planned day reads
   as intent, not a missed log -- orthogonal to, and stacks cleanly with,
   `--short` above. */
.cc-cap-col--future, .cc-cap-band--future, .cc-cap-likely--future { opacity: 0.5; }
/* Today (item 13): a quiet structural anchor, not a new hue -- so the window's
   right edge is never mistaken for "stopped logging" when today sits
   mid-window. Drawn under the columns; only peeks through their translucent
   fill. */
.cc-today { stroke: var(--text-muted); stroke-width: 1; stroke-opacity: 0.55; stroke-dasharray: 1 2; }
/* Logged-intake diamond: a solid accent mark at what the client ACTUALLY ate --
   the concrete datum the headline verdict rests on. Haloed in the panel colour
   so it stays crisp over any plan bar and reads on both themes. */
.cc-logged { fill: var(--accent); stroke: var(--panel-bg); stroke-width: 1.25; }
/* Smooth trend through the most-likely values -- a quiet neutral thread, not a
   competing accent. */
.cc-trend { fill: none; stroke: var(--text-muted); stroke-width: 1.75; stroke-opacity: 0.7;
  stroke-linejoin: round; stroke-linecap: round; }
/* Goal: a clean dashed reference line + a tiny "goal" tag at the right. */
.cc-goal { stroke: var(--text-muted); stroke-width: 1.25; stroke-dasharray: 4 3; opacity: 0.8; }
.cc-goal-label { fill: var(--text-muted); font-size: 14px; }

.cal-chart-skeleton .cal-chart-cap { opacity: 0.75; }
.cc-empty-note { fill: var(--text-muted); font-size: 14px; font-style: italic; opacity: 0.85; }
/* Chart key: names the bars, the logged-intake diamond, and the protein
   caution ring so none of them depends on colour/shape alone. Sits just
   under the chart, above the "Last 30 days" cap. */
.cal-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  margin: 0.1rem 0 0.15rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.cc-leg { display: inline-flex; align-items: center; gap: 0.3rem; }
.cc-leg-bar {
  width: 0.55rem; height: 0.85rem; border-radius: 2px;
  background: var(--text-muted); opacity: 0.5;
}
.cc-leg-diamond { color: var(--accent); font-size: 0.85rem; line-height: 1; }
/* Protein-short legend swatch: the SAME hollow-ring-in-caution-colour shape
   the chart itself draws on a short day (.cc-cap-likely--short), so the key
   can never drift from what's actually on the chart. */
.cc-leg-caution-ring {
  display: inline-block;
  width: 0.55rem; height: 0.55rem;
  border-radius: 50%;
  border: 1.5px solid var(--caution);
  background: none;
}

/* --- AT-A-GLANCE HISTORY SPARKLINES (item B, founder feedback batch 0720) --
   Two tiny Tufte sparklines (calories, protein), full folio-pane width, at
   the very top of the workspace -- app/templates.py's
   _render_history_sparklines/_render_sparkline. Deliberately NOT a re-skin
   of .cal-chart/.cal-bars above: no axis, no per-day columns -- just a thin
   line, a dotted goal line, and (the one place colour carries meaning) a
   hatched fill over the real "bad" excursion, replacing the old buried
   `<details>See the last 30 days</details>` disclosure (item E). */
.folio-sparklines {
  margin: 0 0 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.spark-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0;
}
.spark-label {
  font-size: var(--fs-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-secondary);
}
.spark-readout { font-size: 1rem; }
/* The readout is the last COMPLETED day's real intake -- its "yesterday"/
   date tag and the current-goal chip (red-team round 1: an unlabeled number
   beside a goal edit read as "my change didn't stick") stay quiet. */
.spark-when { font-size: 0.78rem; }
.spark-goal-now { font-size: 0.78rem; white-space: nowrap; }

/* Save confirmation happens AT the edited field (red-team rounds 1-2, S5/S2:
   a distant status line was missed; a sticky one overlapped other fields):
   the field flashes a quiet ring and a small "✓ saved" badge appears in the
   field's own status row, then fades. The in-flow status line at the form's
   end remains as the always-there answer to "how do I save?".

   Every goal-field label ends with a `.field-status` row of RESERVED height
   (founder feedback: pop-ins must not shift the layout) -- the saved badge,
   the "auto-adjusted" tag, and the restore chip all render inside it, sized
   to fit its one line. */
.field-status {
  display: block;
  /* FIXED height, not min-height: mixed font metrics on the line box can
     still grow a min-height row by a pixel and ripple the whole column
     (founder feedback: pop-ins must not move anything). */
  height: 1.25rem;
  line-height: 1.2rem;
  margin-top: 2px;
  overflow: hidden;
}
@keyframes field-saved-pulse {
  0% { box-shadow: 0 0 0 3px var(--good); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.field-saved-pulse { animation: field-saved-pulse 1.4s ease-out; }
@keyframes field-saved-badge {
  0% { opacity: 0; }
  12% { opacity: 1; }
  70% { opacity: 1; }
  100% { opacity: 0; }
}
.field-saved-badge {
  margin-right: 0.5rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--good);
  animation: field-saved-badge 2.2s ease-out forwards;
}
/* An app-made ADJUSTMENT to a field the user wasn't typing in (the Atwater
   link rewriting macros/calories) flashes amber -- visible, never silent
   (red-team round 2, S6). */
@keyframes field-adjusted-flash {
  0% { box-shadow: 0 0 0 3px var(--caution); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.field-adjusted-flash { animation: field-adjusted-flash 1.8s ease-out; }
/* ...and the tag STAYS (until the user hand-edits that field), so an
   adjustment made while they looked elsewhere is still discoverable on the
   next scan of the card -- the flash alone was gone in under 2 seconds. */
.field-adjusted-tag {
  margin-right: 0.5rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--caution);
}
/* One-click restore of a hand-typed calorie target after a macro edit moved
   it (the Atwater link doing its job, but the user's number is remembered).
   Compact enough to sit inside the reserved .field-status line. */
.goal-restore-cal {
  display: inline-block;
  padding: 0 0.45rem;
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1.05rem;
  color: var(--accent, #0b57a4);
  background: none;
  border: 1px solid currentColor;
  border-radius: 999px;
  cursor: pointer;
  vertical-align: top;
}
/* Fresh profiles keep the suggestion slot in flow from first paint (visible
   placeholder copy -> real suggestion swaps IN PLACE), tall enough for the
   full suggestion + button, so nothing below ever shifts. */
.goal-suggestion--reserve {
  /* Measured: the full suggestion (3 text lines + button line) renders
     ~10.1rem tall at the folio column width; 10.5rem covers a line of
     wiggle so the placeholder -> suggestion swap moves nothing below. */
  min-height: 10.5rem;
}
/* The "not sure what to aim for?" starting-point chip (renders only while
   calories is blank and a maintenance estimate exists). */
.goal-suggestion { margin: 0.5rem 0 0; }
.goal-suggestion .button { margin-left: 0.5rem; }
.spark-svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 700 / 40;
  overflow: visible;
}
/* A DOTTED (not dashed) goal line -- a near-zero dash length + round linecap
   draws a round dot at this scale, deliberately distinct from the big
   chart's own longer-dashed .cc-goal so this reads as its own, quieter
   mark. */
.spark-goal {
  stroke: var(--ink-secondary);
  stroke-width: 1.25;
  stroke-dasharray: 0.1 3;
  stroke-linecap: round;
  opacity: 0.8;
}
/* Filled deviation-area sparkline. The "good" side of the goal gets a calm,
   light on-track fill in the semantic --good green (the SAME green the
   pace/"ahead" indicators use), softened so it reads as quiet reassurance,
   not an alert -- the hatched "bad" side keeps the attention. The "bad"
   fill's colour comes from its inline pattern (spark-hatch-bg--*). */
.spark-area { stroke: none; }
.spark-area--good { fill: var(--good); opacity: 0.38; }
/* The smoothed trend itself: a thin ink stroke cut from the SAME monotone-
   cubic samples the fills are built on (templates._monotone_samples), so the
   line and its area always coincide exactly. */
.spark-trend {
  stroke: var(--ink-secondary);
  stroke-width: 1.1;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.spark-dot { fill: var(--ink-muted); }
/* A redundant THIRD channel on a bad day's own dot (the hatched fill below
   is the primary one) -- never the only signal on its own. */
.spark-dot--critical { fill: var(--critical); }
.spark-dot--caution { fill: var(--caution); }
/* Hatch base colours, set via the SAME semantic custom properties the rest
   of the app uses (never a hardcoded hex) so both themes stay correct; the
   black diagonal stripe drawn on top (inline in the pattern itself) is
   theme-invariant, matching .progress-fill--over's own flat rgba(0,0,0,.3)
   overlay -- the SAME "over/under target" stripe geometry as that bar, so
   the two read as one visual language. */
.spark-hatch-bg--critical { fill: var(--critical); }
.spark-hatch-bg--caution { fill: var(--caution); }
.spark-history-link { margin: 0.2rem 0 0; }

/* R1 de-chrome: the same Folio idiom S3 gave .recipe-card/.callout (a
   single 3px neutral left-rule + padding-left, no all-round border/radius/
   background slab) -- this box has no single semantic state of its own,
   so --rule, matching .field-box/.recipe-card above. */
.macro-range {
  font-size: var(--fs-label);
  color: var(--text-muted);
  border-left: 3px solid var(--rule);
  border-radius: 0;
  padding-left: 0.65rem;
  margin: 0 0 0.6rem;
}
.macro-range p { margin: 0 0 0.2rem; }
.macro-range p:last-child { margin-bottom: 0; }
.macro-range-headline strong { color: var(--text); }
.macro-bounds-note {
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-left: 0.4rem;
}
/* The rationale toggle (<details>/<summary>): reachable by tap on any
   device, never hover-only -- see app/templates.py's
   _render_macro_bounds_note. The dotted underline on the summary line is
   the tap/click affordance (replaces the old title= hover cue); the "why?"
   hint text is the second, non-color channel that it's expandable. */
/* Item 2 fix: bare `inline-block` alone left this box's WIDTH dictated by
   its own max-content -- fine for the closed one-line "range lo-hi (why?)"
   summary, but the open rationale sentence (a full sentence of free text)
   could demand hundreds of px, which an ancestor `auto`-sized grid column
   (render_plan_grid's `.meal-line`) then honored in full, crushing the
   dish column beside it to nothing and overlapping both (the reported
   layout break). Capping the width here bounds the disclosure's OWN
   max-content contribution to that ancestor grid track -- the closed
   summary line is always far under this cap (unaffected); the open
   rationale simply wraps inside it, in normal document flow, instead of
   dictating the row's width. */
details.macro-bounds-note { display: inline-block; vertical-align: top; max-width: 20rem; }
.macro-bounds-note summary {
  cursor: pointer;
  list-style: none;
}
.macro-bounds-note summary::-webkit-details-marker { display: none; }
.macro-bounds-why-hint {
  border-bottom: 1px dotted var(--text-muted);
}
.macro-bounds-why {
  display: block;
  margin-top: 0.2rem;
}

/* -- responsive tables (mobile pass): wide content scrolls inside its OWN
   container, never the page. None of app/templates.py's own SSR views emit a
   literal `<table>` any more (the pantry ledger, week menu, and trainer
   roster are all plain ruled document lists now -- Working Folio rebuild),
   but `.pantry-table` is still the class app/coach.py instructs the COACH to
   emit directly in its own markdown/HTML replies (a real `<table
   class="pantry-table">` a client message can render) -- this wrapper is
   for that real, still-live wide-table case. -- */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* Pantry height cap: a stocked kitchen (dozens of items) would otherwise make
   the sidebar card enormous, pushing "AI assistant access" far
   down and dragging the whole layout around on every refresh. Cap #pantry-panel
   so ~8-10 rows show, then it scrolls INSIDE the card. The header sticks so
   the Item/Qty/Macros labels stay visible while scrolling. */
#pantry-panel {
  max-height: 30rem;
  overflow-y: auto;
}
#pantry-panel .pantry-table thead th {
  position: sticky;
  top: 0;
  background: var(--panel-bg);
  z-index: 1;
}

/* -- shared table look, three template classes the coach may emit -- */
.plan-grid, .pantry-table, .recipe-card table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-label);
}
.plan-grid th, .plan-grid td,
.pantry-table th, .pantry-table td {
  border-bottom: 1px solid var(--border);
  padding: 0.4rem 0.5rem;
  text-align: left;
}
.plan-grid th, .pantry-table th { color: var(--text-muted); font-weight: 600; }
/* Item macros for the low-literacy / low-vision reader: whole numbers, words
   spelled out, protein (north-star) given weight, generous size + spacing so a
   distracted glance parses it. */
.pantry-macros { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.1rem 0.7rem; line-height: 1.4; }
.pantry-macros .pm-cal { font-weight: 700; font-size: 1.02em; }
.pantry-macros .pm-protein b { color: var(--ink); }
.pantry-macros .pm-basis { flex-basis: 100%; color: var(--text-muted); font-size: var(--fs-label); }
.plan-grid.skeleton .skeleton-cell { background: var(--skeleton); border-radius: 4px; height: 1em; }
/* A slot with no coach recipe yet: an honest "coming soon" label, with the
   planner's structural template demoted to a muted secondary hint -- never
   dressed up as the dish name itself (planner internals are not a menu). */
.dish-pending { font-style: italic; color: var(--text-muted); }
/* Per-meal nutrition in the week menu: protein (north-star) carries the
   accent. Deliberately NOT nowrap -- a slot with an <ate> lock's macro-range
   disclosure appended can run well past the meal-line's own nutri column
   width, and a forced nowrap there just overflows into the actions column
   next to it (a real bug this rule used to cause, back when it was a
   `<table>` cell whose column width was never fixed the way a CSS grid
   track is). Wrapping inside its own track is the fix. */
.meal-nutri { white-space: normal; }

/* -- rendered Markdown inside a coach bubble ------------------------------
   The coach writes Markdown and the server renders it (app/markdown_render.py),
   so a reply now arrives as ordinary <h2>/<ul>/<table>/<pre> with NO class on
   it -- unlike the three coach-emittable classes above, which the coach opts
   into by hand. Without these rules a markdown table would render with no
   borders at all and a heading would inherit the browser's enormous default
   margins: "renders as formatting" has to mean formatting that looks composed,
   not merely formatting that is technically present.

   Scoped to .coach-html so it can only ever touch coach content, and margins
   are collapsed hard -- a chat bubble is not a document page. */
.coach-html > :first-child { margin-top: 0; }
.coach-html > :last-child { margin-bottom: 0; }
.coach-html p { margin: 0.5rem 0; }
.coach-html h1, .coach-html h2, .coach-html h3,
.coach-html h4, .coach-html h5, .coach-html h6 {
  margin: 0.75rem 0 0.35rem;
  line-height: 1.25;
  font-weight: 600;
}
/* S1: these were previously SMALLER than the 1.1875rem (--fs-reading)
   .coach-prose body they sit inside -- an inverted scale where a "heading"
   read quieter than its own body text. h1/h2 are now well clear of that
   body size; h3/h4-h6 clear the 1rem floor and keep a monotonic h1>h2>h3>=
   h4-h6 scale (h4-h6 lands on --fs-body, the app's own ordinary paragraph
   size -- same-size-but-bold is a normal lowest-heading-tier convention,
   the shared rule above already carrying font-weight: 600). */
.coach-html h1 { font-size: 1.5rem; }
.coach-html h2 { font-size: 1.3rem; }
.coach-html h3 { font-size: 1.15rem; }
.coach-html h4, .coach-html h5, .coach-html h6 { font-size: var(--fs-body); }
.coach-html ul, .coach-html ol { margin: 0.5rem 0; padding-left: 1.35rem; }
.coach-html li { margin: 0.15rem 0; }
.coach-html li > ul, .coach-html li > ol { margin: 0.15rem 0; }
.coach-html blockquote {
  margin: 0.5rem 0;
  padding: 0.1rem 0 0.1rem 0.7rem;
  border-left: 3px solid var(--border-strong);
  color: var(--text-muted);
}
.coach-html hr { border: none; border-top: 1px solid var(--border); margin: 0.75rem 0; }
.coach-html code {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.05rem 0.3rem;
  font-size: 0.87em;
}
.coach-html pre {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
  margin: 0.5rem 0;
  overflow-x: auto;          /* a long code line scrolls the block, not the page */
}
.coach-html pre code { background: none; border: none; padding: 0; font-size: var(--fs-label); }
.coach-html a { color: var(--accent); }
.coach-html img { max-width: 100%; height: auto; }
/* A markdown table gets the same look as the three hand-authored classes --
   the client should not be able to tell which one the coach reached for.
   MOBILE PASS: a markdown table's HTML comes from app/markdown_render.py
   (out of scope for this pass), so there is no wrapping <div> to add here
   the way `.table-scroll` above wraps a real coach-emitted `<table
   class="pantry-table">` --  `display:block` turns the <table> ELEMENT
   ITSELF into the scrollable
   box (its thead/tbody/tr/td children keep their own table-* display
   roles, so the grid still renders correctly; this is a standard,
   markup-free responsive-table technique). Same contained-horizontal-
   scroll guarantee, no markup change needed. */
.coach-html table {
  display: block;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  margin: 0.5rem 0;
}
.coach-html th, .coach-html td {
  border-bottom: 1px solid var(--border);
  padding: 0.4rem 0.5rem;
  text-align: left;
}
.coach-html th { color: var(--text-muted); font-weight: 600; }

/* -- the settled "recorded" state: a trainer's read-only view (templates.
   _render_cooked_readonly) and an owner's own already-recorded slot alike
   (templates._render_recorded_rating -- once a meal is recorded there is no
   live control left to show, just this quiet badge, optionally with the
   rating word). -- */
.cooked-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  padding: 0.4rem 0.7rem;
  font-size: 1rem;
}
/* S1 color discipline: a settled "recorded" slot is routine, not an alert --
   neutral ink + bold (legitimate data-ink weight), never the --good fill;
   the "✓ Recorded" glyph+word (_render_recorded_rating) carries the state
   on its own, colour is never the only channel. */
.cooked-badge.cooked { color: var(--ink); font-weight: 700; }

/* -- settled <ate> lock (app/ops.py's _op_ate): the client logged what they
   ate in this slot, so it renders as a plain, settled row -- same shape as
   .cooked-badge (border + text, never colour alone). S2 color discipline:
   this is a settled routine record, not a status to flag -- --info is
   already reused for queued/running/provenance, so a logged meal borrowing
   it read as more "in-progress" than "done". Neutral --rule-strong border +
   --ink-secondary text (no fill), legible on its own, and it no longer
   competes visually with a real alert OR a real in-progress state. See
   templates.py's _render_ate_row. -- */
.ate-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  padding: 0.4rem 0.7rem;
  font-size: 1rem;
  color: var(--ink-secondary);
}
/* -- settled <skipped> lock (app/ops.py's _op_skipped): the client said they
   didn't eat this planned meal at all -- a real terminal record of ZERO
   intake, distinct from both a cooked slot (ate as planned) and an <ate>
   lock (ate something else). Same .cooked-badge shape (border + text,
   never colour alone); muted secondary ink -- neither the bold "cooked"
   ink nor the info blue "ate" uses, since this is a quiet factual record,
   not an achievement. See templates.py's _render_skipped_badge. -- */
.cooked-badge.skipped { color: var(--ink-secondary); }

.rating-na { color: var(--ink-secondary); font-size: 1rem; }

/* -- past-but-unlogged terminal marker (templates._render_unlogged_terminal):
   a slot whose day is already behind "today" and was never cooked or logged.
   Its window to be cooked has closed, so it shows this inert badge WHERE a
   live "Mark cooked" button would otherwise be -- muted, not an alert
   (nothing is wrong; the day simply passed), same badge shape as
   .cooked-badge, a dashed border to read as "not settled" without leaning on
   colour, and the "Unlogged" text carrying the state on its own. -- */
.unlogged-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 2px dashed var(--rule-strong);
  border-radius: 0;
  padding: 0.4rem 0.7rem;
  font-size: 1rem;
  color: var(--ink-secondary);
}

/* Item 14: the Folio type scale has NO register below 1rem ("nothing is
   below 1rem") -- this is the load-bearing invite URL the trainer must
   read/hand off, previously the smallest text in the whole panel.
   `overflow-wrap: anywhere` (not `word-break: break-all`) so a long URL
   still wraps to fit its row without fracturing mid-character everywhere;
   at the real body size it needs far less forced breaking anyway. */
.invite-link { font-size: var(--fs-label); overflow-wrap: anywhere; }

.invite-actions { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0.3rem 0; }
/* Item 14: same 1rem floor -- a copy confirmation is transient but must
   still be legible for a low-vision reader. */
.copy-feedback { font-size: var(--fs-label); min-height: 1em; }
/* S2 color discipline: a successful copy is routine, not an alert -- neutral
   ink, the '✓ ' glyph prefixed onto the message itself (never colour
   alone). is-error stays --error: a genuine attention state. */
.copy-feedback.is-ok { color: var(--ink); }
.copy-feedback.is-error { color: var(--error); }

/* -- recipe-card: the third coach-emittable class -- */
/* S3 de-chrome: the same Folio idiom as .field-box above -- a
   3px neutral left-rule + padding-left, no all-round border/radius/
   background slab. */
.recipe-card {
  border-left: 3px solid var(--rule);
  border-radius: 0;
  padding-left: 1rem;
  margin: 0.4rem 0;
}
.recipe-card h3, .recipe-card h4 { margin-top: 0; }

/* ============================================================================
   The Briefing -- jobs #1-5 own component CSS. Job #5 (Your coach) is
   styled above, alongside .chat-pane/.msg-*.
   ============================================================================ */

/* -- Job #1/#2: Cook your next meal / How today's going ------------------ */
/* A small muted data-style label ahead of a big value -- "Up next · Today ·
   Dinner" ahead of the dish title, same register the old .today-lead-label
   used. Generic (not Today-specific) so any future eyebrow reuses it. */
.eyebrow { color: var(--ink-muted); font-weight: 700; }
.today-lead-nutri { margin-block-start: 0.5rem; }

/* Item 6 (founder, live-testing pass): each analog bar in "How today's
   going" is now its OWN labelled unit -- a receding uppercase label
   ("Calories"/"Protein") beside its own bare, high-contrast "logged /
   target" value, sitting directly above that bar's own track. Replaces a
   single combined prose headline sitting above two unlabeled bars, which
   left a reader unable to tell which bar was which without re-reading it. */
.today-meter { margin-block-start: 0.9rem; }
.today-meter:first-child { margin-block-start: 0; }
.today-meter__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
}
.today-meter__label {
  font-size: var(--fs-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-secondary);
}

/* Generic rectangular progress meter: a plain track + fill, no rounded
   capsule, no gradient -- used by "How today's going" (calorie AND the
   parallel protein bar) and (nested inside its own <details>) nowhere else,
   but kept generic on purpose. `display: flex` lets a second `.progress-
   fill--over` segment sit immediately after the base fill, sharing the
   track's width instead of stacking underneath it (a plain block sibling
   would). */
.progress-track {
  position: relative;
  display: flex;
  height: 10px;
  margin-block: 1rem;
  background: var(--rule);
}
/* S2 color discipline: an on-track bar is routine, not a link or a status --
   neutral ink on its gray track (--accent is reserved for folio links), so
   the hatched over-segment below is the ONLY saturated colour on the meter. */
.progress-fill {
  height: 100%;
  background: var(--ink-muted);
}
/* Item 7 (original) / live-testing item 6, S3 severity-tier unification: the
   over-budget segment beyond the 100% mark -- an abnormal magnitude gets a
   distinct hue AND texture in the analog itself, never colour alone, and
   never only in the status word/text below it. One tier for routine
   over-target across the whole app: --caution (amber), matching the pace
   word/chart headline/masthead verdict, which were already amber -- this
   bar's red used to be the one outlier escalating "over target" to the same
   hue reserved for genuine failure. The repeating diagonal hatch stays as
   the colour-independent texture channel. */
.progress-fill--over {
  background-color: var(--caution);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(0, 0, 0, 0.3) 0,
    rgba(0, 0, 0, 0.3) 3px,
    transparent 3px,
    transparent 7px
  );
}

/* Item 1 (display-compliance audit, 2026-07-20): a thin vertical pace-tick
   hairline marking "expected by now" on the calorie track -- a data-ink
   hairline, not a card/gauge of its own. Absolutely positioned so it reads
   at a fixed fraction of the track regardless of the fill's own (flex)
   width; a touch taller than the track itself so it stays visible even
   when it lands right at the fill's own edge. */
.progress-tick {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 2px;
  background: var(--ink);
}

/* Generic icon+word+colour status line (never colour alone). */
.status { display: inline-flex; align-items: center; gap: 0.4rem; font: 700 1rem/1.3 var(--font-sans); margin: 0; }
/* S1 color discipline: on-target is routine, not an alert -- the Today
   panel's day verdict (_render_day_status_summary) reads neutral ink here,
   the ✓ glyph + "On target" word already carrying the state. The SAME
   markup also backs the persistent coach-rail status line
   (#coach-day-status), where green stays reserved -- see the
   .workspace-coach override just below. */
.status--good { color: var(--ink); }
.status--caution { color: var(--caution); }
.status--under { color: var(--ink-muted); }
.status--neutral { color: var(--ink-muted); }

/* Item C: _render_pace_chip's own right-justified copy (beside the "How
   today's going" heading) makes the chip _render_day_status_summary's body
   copy also carries redundant WITHIN the Today panel -- hidden only there.
   `.briefing__body` is never part of what the coach rail's hx-select pulls
   out of this same fragment (it extracts just `#today-status-summary`),
   so the persistent coach-rail line keeps showing this exact chip
   unaffected, both at first paint and after a live refresh -- see
   _render_day_status_summary's own docstring. */
.briefing__body #today-status-summary .status { display: none; }

/* Item D (founder live-testing pass): the "on plan you'll finish around…"
   projection recedes -- right-justified, plain body weight, muted ink --
   compact under the meters rather than a second full-weight `.data` row
   competing with them. The sentence itself is unchanged/truthful either way. */
.today-projection {
  margin: 0.5rem 0 0;
  text-align: right;
  color: var(--ink-secondary);
}

/* -- Batch prep (founder #36): the plan's structured weekly batch-cook plan,
   a scannable prep checklist above the week's menu. A quiet card -- it's a
   helpful heads-up, not the hero -- keyed off the "good/prep" green so it
   reads as a bulk-cook aid, not an alarm. Rendered by
   templates._render_batch_prep_card only when the plan has batch tasks. */
.batch-prep {
  margin: 0 0 1.5rem;
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--good);
  border-radius: 4px;
  background: var(--good-bg);
}
.batch-prep__title {
  margin: 0 0 0.6rem;
  font-size: var(--fs-label, 0.8rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--good);
}
.batch-prep__list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 0.55rem; }
.batch-prep__task { display: flex; flex-direction: column; gap: 0.1rem; }
.batch-prep__cook { font-weight: 650; color: var(--ink); }
.batch-prep__meta { font-size: 0.85rem; color: var(--ink-secondary); }
.batch-prep__when { font-weight: 650; color: var(--ink-muted); }

/* Warm-up progress line (founder #34): the plan-panel empty state while the
   coach is still collecting ~3 days of logged intake as seed data. Quiet,
   informative -- it names honest progress and points at logging, not planning. */
.warmup-progress {
  margin: 0.35rem 0 0.8rem;
  color: var(--ink-secondary);
  font-size: 0.9rem;
  max-width: 42ch;
}
.warmup-progress strong { color: var(--ink); }

/* -- Job #3: The week ahead -- the plan as a menu, not a table ----------- */
.week-menu { display: flex; flex-direction: column; }
.week-day {
  margin-block-start: 2.75rem;
  padding-block-start: 1.25rem;
  border-top: 2px solid var(--ink);
}
.week-day:first-child { margin-block-start: 0; }
.week-day--today { border-top-width: 3px; border-top-color: var(--accent); }
.week-day__name { margin: 0 0 0.5rem; font-size: 1.5rem; font-weight: 750; }
.week-day__today-badge {
  margin-inline-start: 0.75rem;
  color: var(--accent);
  font-size: 1rem;
  font-weight: 800;
  vertical-align: middle;
}
.meal-line {
  display: grid;
  /* Item 2 fix: the third (nutrition/record) column used to be a bare `auto`
     track -- unbounded, content-sized. That's fine for its everyday content
     ("700 cal · 30g protein"), but the (why?) macro-bounds-note disclosure
     (_render_macro_bounds_note) can open a full sentence of rationale text
     inside it -- an unbounded `auto` track then grabs however much space
     that sentence wants, at the DISH column's direct expense (its
     `minmax(0, 1fr)` floor of 0 let it get crushed all the way to zero
     width, collapsing every word onto its own line and visually
     overlapping the nutrition column next to it -- the reported layout
     break). Capping the third column at a sane ceiling (`minmax(0, 20rem)`)
     makes the rationale WRAP inside that budget instead of dictating the
     row's width, and giving the dish column a real floor (`minmax(8rem,
     1fr)`) guarantees it never collapses below a readable width even when
     the third column claims its full ceiling. */
  grid-template-columns: 9rem minmax(8rem, 1fr) auto;
  gap: 0.5rem 1.25rem;
  align-items: center;
  padding-block: 1.1rem;
  border-bottom: 1px solid var(--rule);
}
/* "Next" is ONE selected row, not a whole section -- --paper-subtle is
   explicitly allowed for exactly this (bible §1 colour rules). */
.meal-line--next { background: var(--paper-subtle); }
.meal-line__meal { font-weight: 700; }
.meal-line__badge {
  display: inline-block;
  margin-inline-start: 0.5rem;
  color: var(--accent);
  font-weight: 800;
  font-size: 1rem;
}
.meal-line__dish a { color: var(--ink); text-decoration-color: var(--accent); text-decoration-thickness: 2px; }
/* The rating-record control is far wider than the old star widget; drop it to
   its own full-width row below the meal summary (same as the recipe section's
   "How did it go?" placement) instead of a 4th column that overflows. */
.meal-line__actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; grid-column: 1 / -1; justify-self: start; margin-block-start: 0.4rem; }
.slot-past { opacity: 0.7; }
@media (max-width: 48rem) {
  .meal-line {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.5rem;
    align-items: start;
    padding-block: 1.25rem;
  }
  .meal-line__actions { justify-self: start; }
}
.week-menu-skeleton { display: flex; flex-direction: column; gap: 1rem; }
.meal-line-skeleton { height: 3.5rem; background: var(--skeleton); }

/* -------------------------------------------------------------------------
   Planning ceremony (_render_planning_ceremony) -- the flashy live display
   shown WHILE the planner solves, built entirely from real planner telemetry
   (which relaxation pass, pool candidates, A* nodes, slots placed). The
   "ceremony" is real counters that climb each poll + pure-CSS ambient motion;
   NO fake ETA/percent bar (the honest-progress rule). All motion is gated
   behind prefers-reduced-motion below.
   ------------------------------------------------------------------------- */
.planning-ceremony {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--border);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent-soft) 55%, var(--paper)) 0%, var(--paper) 70%);
  padding: 1.4rem 1.25rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  text-align: center;
}
.planning-ceremony--compact { padding: 0.85rem 0.85rem 0.9rem; gap: 0.6rem; text-align: left; }

/* Ambient conic aura -- the "summoning" glow behind the content. */
.pc-aura {
  position: absolute;
  top: 50%; left: 50%;
  width: 150%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  /* A calm cool wash -- accent + its analogous protein-blue, not the full
     macro rainbow (which read as a muddy warm/cool smear). */
  background: conic-gradient(from 0deg,
    color-mix(in srgb, var(--accent) 48%, transparent),
    color-mix(in srgb, var(--macro-protein) 30%, transparent),
    color-mix(in srgb, var(--accent) 20%, transparent),
    color-mix(in srgb, var(--macro-protein) 30%, transparent),
    color-mix(in srgb, var(--accent) 48%, transparent));
  filter: blur(40px);
  opacity: 0.45;
  z-index: 0;
  animation: pc-spin 9s linear infinite;
}
.planning-ceremony > *:not(.pc-aura) { position: relative; z-index: 1; }

.pc-head { display: flex; align-items: center; justify-content: center; gap: 0.55rem; }
.planning-ceremony--compact .pc-head { justify-content: flex-start; }
.pc-orb {
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, var(--accent-hover), var(--accent));
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent);
  animation: pc-orb-pulse 1.6s ease-out infinite;
  flex: none;
}
.pc-title {
  font-family: var(--font-sans);
  font-weight: 700;
  letter-spacing: 0.02em;
  font-size: var(--fs-body);
  color: var(--ink);
  background: linear-gradient(100deg, var(--ink) 30%, var(--accent) 50%, var(--ink) 70%);
  background-size: 220% 100%;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: pc-shimmer 3.2s linear infinite;
}
.pc-detail {
  margin: 0 auto;
  max-width: 36ch;
  color: var(--ink-muted);
  font-size: var(--fs-label);
  font-family: var(--font-sans);
  text-wrap: balance;
}
.planning-ceremony--compact .pc-detail { max-width: none; }

/* Pass ESCALATION indicator: strict -> flexible -> free-form. The current
   strategy is lit; earlier ones are RECEDED as "tried" (the planner relaxed
   past them), never a celebratory checkout-style "done" that would read as
   forward progress. The connector is a neutral track linking three peer
   strategies -- it never fills accent (which would imply "how far along"). */
.pc-stages {
  list-style: none; margin: 0 auto; padding: 0;
  display: flex; max-width: 26rem; width: 100%;
}
.pc-stage {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  position: relative; padding: 0 0.25rem;
  font-size: var(--fs-label); color: var(--ink-secondary); text-align: center;
}
.pc-stage + .pc-stage::before {
  content: ""; position: absolute; top: 0.5rem; left: -50%; width: 100%; height: 2px;
  background: var(--skeleton); z-index: 0;
}
.pc-stage__dot {
  width: 1.05rem; height: 1.05rem; border-radius: 50%;
  background: var(--paper); border: 2px solid var(--border);
  position: relative; z-index: 1; flex: none;
}
.pc-stage__label { font-weight: 600; line-height: 1.1; }
.pc-stage__sub { font-size: 0.7rem; color: var(--ink-secondary); line-height: 1.15; }
/* tried (relaxed past): a muted grey filled dot -- passed through, NOT accent. */
.pc-stage--tried { color: var(--ink-secondary); }
.pc-stage--tried .pc-stage__dot { background: var(--ink-secondary); border-color: var(--ink-secondary); }
/* active (current strategy): the ONE accent focal point. */
.pc-stage--active { color: var(--ink); }
.pc-stage--active .pc-stage__label { color: var(--accent); }
.pc-stage--active .pc-stage__dot {
  background: var(--accent); border-color: var(--accent);
  animation: pc-orb-pulse 1.6s ease-out infinite;
}
/* todo (not yet needed): a faint empty dot -- but label stays full-opacity
   --ink-secondary (AA), never an opacity multiplier that crushes contrast. */
.pc-stage--todo .pc-stage__dot { background: var(--paper); border-color: var(--border); }
.planning-ceremony--compact .pc-stages { max-width: none; }
.planning-ceremony--compact .pc-stage__sub { display: none; }
.planning-ceremony--compact .pc-stage__dot { width: 0.8rem; height: 0.8rem; }
.planning-ceremony--compact .pc-stage + .pc-stage::before { top: 0.4rem; }

/* Real counters. Each poll re-creates the <span>, replaying pc-pop, so the
   number visibly ticks. */
.pc-metric { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.pc-metric__num {
  font-family: var(--font-mono);
  font-size: 1.6rem; font-weight: 700; line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  animation: pc-pop 0.45s ease-out;
}
.pc-metric__label { font-size: var(--fs-label); color: var(--ink-muted); }

/* Slots-reached grid: one cell per real meal slot, laid out 7 across so a
   21-slot week reads as its actual 3-rows-of-7 structure (not a ragged wrap).
   Filled cells pop in as the search reaches deeper. */
.pc-slots-wrap { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
.pc-slots { display: grid; grid-template-columns: repeat(7, 1.1rem); gap: 0.3rem; margin: 0 auto; }
.pc-slot {
  width: 1.1rem; height: 1.1rem;
  border: 1px solid var(--border);
  background: var(--paper);
}
.pc-slot--filled {
  background: var(--accent);
  border-color: var(--accent);
  animation: pc-fill 0.4s ease-out;
}
.pc-slots__count { font-size: var(--fs-label); color: var(--ink-muted); font-family: var(--font-mono); }

@keyframes pc-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
@keyframes pc-orb-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  70% { box-shadow: 0 0 0 0.5rem color-mix(in srgb, var(--accent) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
}
@keyframes pc-shimmer { to { background-position: 220% 0; } }
@keyframes pc-pop { 0% { transform: scale(0.7); opacity: 0.2; } 60% { transform: scale(1.12); } 100% { transform: scale(1); opacity: 1; } }
@keyframes pc-fill { 0% { transform: scale(0.4); opacity: 0.3; } 100% { transform: scale(1); opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .pc-aura { animation: none; opacity: 0.35; }
  .pc-orb, .pc-stage--active .pc-stage__dot { animation: none; }
  .pc-title { animation: none; background: none; -webkit-text-fill-color: currentColor; color: var(--ink); }
  .pc-metric__num, .pc-slot--filled { animation: none; }
}

/* A day's "didn't eat this" records (item B, founder live-testing pass) --
   quiet by default, never a row in the main .meal-line list. Same collapsed-
   disclosure idiom as .week-menu-rest/.progress-chart-details (a plain
   <summary>, no button chrome), just smaller/muted since this is a lesser,
   occasional fact, not a chapter-level navigational choice. */
.skipped-recap { margin-block-start: 0.75rem; }
.skipped-recap summary { cursor: pointer; color: var(--ink-muted); font-size: 1rem; font-weight: 700; }
.skipped-recap[open] summary { margin-block-end: 0.5rem; }
.skipped-recap__list { margin: 0; padding: 0; list-style: none; }
.skipped-recap__list li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rule);
}
.skipped-recap__meal { font-weight: 700; }
.skipped-recap__undo .button { min-height: auto; padding: 0.35rem 0.75rem; font-size: 1rem; }

/* The pantry's removed-recap (templates.render_removed_recap) -- the same
   quiet collapsed acknowledgement-with-undo idiom as .skipped-recap above,
   sitting at the bottom of #kitchen-inventory. */
.removed-recap { margin-block-start: 1.25rem; }
.removed-recap summary { cursor: pointer; color: var(--ink-muted); font-size: 1rem; font-weight: 700; }
.removed-recap[open] summary { margin-block-end: 0.5rem; }
.removed-recap__list { margin: 0; padding: 0; list-style: none; }
.removed-recap__list li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--rule);
}
.removed-recap__name { font-weight: 700; }
.removed-recap__undo .button { min-height: auto; padding: 0.35rem 0.75rem; font-size: 1rem; }

/* The rest of the week, collapsed behind a native disclosure -- TODAY and
   TOMORROW render open above this (design: "The week ahead" shows what's
   coming without dumping all 7 days at once). Plain text link styling, no
   button chrome -- a native <summary> is already a real, keyboard- and
   screen-reader-accessible toggle. */
.week-menu-rest { margin-block-start: 2.75rem; padding-block-start: 1.25rem; border-top: 2px solid var(--ink); }
.week-menu-rest summary { cursor: pointer; font-weight: 700; font-size: 1.125rem; }
.week-menu-rest[open] summary { margin-block-end: 1rem; }
.week-menu-rest .week-day:first-child { margin-block-start: 0; }

/* The demoted 30-day chart disclosure ("How today's going" -- brief by
   default, the history is one click away, never a chart dump). */
.progress-chart-details { margin-block-start: 2.5rem; padding-block-start: 1.25rem; border-top: 2px solid var(--ink); }
.progress-chart-details summary { cursor: pointer; font-weight: 700; font-size: 1.125rem; }
.progress-chart-details[open] summary { margin-block-end: 1.5rem; }

/* -- Job #4: Your kitchen -- grouped-by-food-class Nutrition Facts cards,
   no boxes except the one deliberate exception (.nutrition-card, below).
   L1 (the workspace) shows only a one-line count + drill-down link
   (.data-lead, in render_pantry_summary); THIS is the L3 full-list page
   (render_pantry_table / render_kitchen_page). Deliberately no
   restock/attention styling -- an item depleting is normal for this
   product, not an alert (see render_pantry_table's own docstring);
   grouping by food class (Item L) is about FINDABILITY, never an urgency
   ranking. */
.pantry-groups { display: flex; flex-direction: column; }
.ledger-summary { margin: 0 0 2rem; }

/* Item L: one quiet eyebrow heading per food-class group -- the same
   quiet-uppercase idiom .recipe-section__title already uses elsewhere,
   so a new grouping device reads as the same document family, not a
   second visual language. */
.pantry-group { margin-block-start: 2.5rem; }
.pantry-group:first-of-type { margin-block-start: 0; }
.pantry-group-heading {
  font-family: "Public Sans", system-ui, sans-serif;
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  padding-block-start: 0.55rem;
  border-top: 1px solid var(--rule);
  margin: 0 0 0.25rem;
}

/* Item N: one <details> per pantry item -- collapsed (the resting default)
   to a dense two-line top-line (name + quantity, then the Calories/
   protein/carb/fat one-liner), expanding to the full Nutrition Facts card
   + real Edit/Delete controls (Item O). No enclosing box on the item
   itself (Folio's "no boxes" rule) -- only .nutrition-card, inside, earns
   the one deliberate exception. */
.pantry-item { border-bottom: 1px solid var(--rule); }
.pantry-item:last-child { border-bottom: none; }
.pantry-item__summary {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) auto;
  column-gap: 1.5rem;
  row-gap: 0.2rem;
  align-items: baseline;
  padding-block: 0.85rem;
  cursor: pointer;
  list-style: none;
}
.pantry-item__summary::-webkit-details-marker { display: none; }
.pantry-item__name {
  grid-column: 1;
  font-weight: 700;
}
.pantry-item__name::before {
  content: "\25B8";
  display: inline-block;
  margin-inline-end: 0.5rem;
  font-size: 0.7em;
  color: var(--ink-secondary);
}
.pantry-item[open] > .pantry-item__summary .pantry-item__name::before { content: "\25BE"; }
.pantry-item__qty {
  grid-column: 2;
  text-align: right;
  white-space: nowrap;
  /* S1 data-ink correction: the one live remaining-quantity figure keeps
     figure-emphasis (full ink + bold), not the secondary/receding register. */
  color: var(--ink);
  font-weight: 700;
  font-size: 0.92em;
}
.pantry-item__summary .pantry-macros { grid-column: 1 / -1; font-size: 0.92em; }
.pantry-item__body {
  padding-block-end: 1.75rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  align-items: flex-start;
}

/* The Nutrition Facts card -- the ONE deliberate exception to "no boxes"
   in this app's whole visual system (docs/DESIGN_WORKING_FOLIO.md
   AMENDMENT v3): a real facsimile of the official FDA label, because the
   founder explicitly asked to replicate that specific real-world printed
   document, not a Folio-native rendering of the same numbers. Fixed
   black-on-white regardless of the app's own light/dark theme -- like a
   printed insert, this card does not invert; a literal color, not a
   semantic --ink/--paper token, is the right call for a paper-label
   facsimile. */
.nutrition-card {
  background: #ffffff;
  color: #0a0a0a;
  border: 1px solid #0a0a0a;
  padding: 0.6rem 0.85rem 0.85rem;
  max-width: 22rem;
  width: 100%;
  font-family: var(--font-sans);
}
.nutrition-card__title {
  font-size: 1.85rem;
  font-weight: 900;
  line-height: 1.05;
  margin: 0 0 0.3rem;
  padding-block-end: 0.3rem;
  border-bottom: 9px solid #0a0a0a;
}
.nutrition-card__basis {
  margin: 0.4rem 0;
  padding-block-end: 0.35rem;
  border-bottom: 1px solid #0a0a0a;
  font-size: 0.9rem;
}
.nutrition-card__calories {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-block: 0.25rem 0.35rem;
  border-bottom: 6px solid #0a0a0a;
  margin-block-end: 0.3rem;
}
.nutrition-card__calories-label { font-size: 1.2rem; font-weight: 800; }
.nutrition-card__calories-value { font-size: 2rem; font-weight: 900; font-variant-numeric: tabular-nums; }
.nutrition-card__row {
  display: flex;
  justify-content: space-between;
  padding-block: 0.35rem;
  border-bottom: 1px solid #0a0a0a;
  font-variant-numeric: tabular-nums;
}
.nutrition-card__row--protein { border-bottom-width: 4px; }
.nutrition-card__row-label { font-weight: 700; }
.nutrition-card__footnote { font-size: 0.72rem; margin: 0.6rem 0 0; color: #333333; }
.nutrition-card-missing { margin-block-start: 0.5rem; }

/* Item O: Edit + Delete controls -- Edit reuses .field-box's own flex/
   border-left/label idiom (adding only the input[type=number] rule that
   idiom didn't already need); Delete reuses .revoke-confirm verbatim (the
   SAME destructive-action confirm pattern render_agent_tokens_panel's
   token revoke already established). */
.pantry-item__controls { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-start; }
.pantry-item-edit > summary::-webkit-details-marker,
.pantry-item-delete > summary::-webkit-details-marker { display: none; }
.pantry-item-edit__form { margin-block-start: 0.75rem; max-width: 26rem; }
.pantry-item-edit__form input[type="number"] {
  flex: 1;
  min-width: 100px;
  padding: 0.5rem 0.65rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--panel-bg);
  color: var(--text);
}
.pantry-item-edit__form .form-feedback { flex-basis: 100%; }
/* Per-food weekly cooking cap (F2): its optional hint spans the form width and
   recedes; the collapsed-summary badge is a quiet mono note next to the qty. */
.pantry-cap-hint { flex-basis: 100%; margin: 0.15rem 0 0.35rem; font-size: var(--fs-label); }
.pantry-item__cap {
  display: block; margin-block-start: 0.15rem;
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 400;
  color: var(--ink-muted);
}

/* Item G: a VERY vertically-compact "ingredients used today" table -- just
   item + amount, dense 1px-ruled rows, no availability badge (that's a
   different signal -- see render_pantry_table's own docstring on
   _pantry_availability). */
.today-ingredients { border-top: 1px solid var(--rule); }
.today-ingredients-row {
  display: grid;
  grid-template-columns: 6rem minmax(0, 1fr);
  gap: 1rem;
  padding-block: 0.3rem;
  border-bottom: 1px solid var(--rule);
  font-size: var(--fs-label);
}
.today-ingredients-row:last-child { border-bottom: none; }
.today-ingredients-row__amount { font-weight: 700; text-align: right; }
.today-ingredients-note { margin-block-start: 0.75rem; font-size: var(--fs-label); }

/* The History page's "What you've eaten" ledger (app/templates.py's
   render_eaten_log) -- a dense document row per settled cooked/ate meal,
   the SAME .ledger-row idiom as the correspondence archive and the plan
   grid's own .meal-line, never a card. De-chrome (§1): no radii, no
   background slab -- a search field and 1px row rules only. */
.eaten-log { margin-block-start: 4rem; }
.eaten-log-search { display: flex; flex-direction: column; gap: 0.4rem; margin-block: 1.5rem; max-width: 26rem; }
.eaten-log-search label { font-size: var(--fs-label); font-weight: 700; color: var(--ink-muted); }
.eaten-log-search input[type="search"] {
  appearance: none;
  -webkit-appearance: none;
  padding: 0.6rem 0.75rem;
  border: 2px solid var(--rule-strong);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  font: 450 var(--fs-body) / 1.4 var(--font-sans);
}
.eaten-log-search input[type="search"]:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.eaten-log-list { list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--ink); }
/* Two rows per entry, so a long off-plan description always gets the FULL
   column width instead of being crushed into a sliver by fixed date/meal/
   macros columns (the eaten log lives inside the ~600px folio reading column,
   where a 4-column dense grid left the dish ~28px wide and wrapped it one
   word per line): a header line (date left, macros right) over a body line
   (meal + dish, flowing full-width and wrapping normally). */
.eaten-log-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "date macros"
    "body body";
  gap: 0.15rem 1.25rem;
  align-items: baseline;
  padding-block: 0.85rem;
}
.eaten-log-row__date { grid-area: date; color: var(--ink-secondary); }
.eaten-log-row__macros { grid-area: macros; text-align: right; white-space: nowrap; }
.eaten-log-row__body { grid-area: body; margin: 0; }
.eaten-log-row__meal { font-weight: 700; }
.eaten-log-row__meal::after { content: " \00b7"; font-weight: 400; color: var(--ink-secondary); }
/* Verified real-browser defect: `.eaten-log-row`'s own `display: grid` above
   is an AUTHOR-origin rule, and author origin beats the user-agent
   stylesheet's `[hidden] { display: none }` on an equal-specificity tie --
   so app/static/app.js's `initEatenLogSearch` setting `row.hidden = true`
   left the row's DOM property correctly set but its computed style still
   `grid` (confirmed via a real headless-Chromium check: 4 "hidden" rows all
   still rendered). This rule is what actually hides a filtered-out row. */
.eaten-log-row[hidden] { display: none; }

@media (max-width: 34rem) {
  /* Very narrow: even the date + macros header line gets tight, so stack all
     three areas vertically. grid-template-areas MUST be reset alongside the
     single column -- leaving the two-column "date macros" areas against a
     one-column track is an invalid template. */
  .eaten-log-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "date" "macros" "body";
    gap: 0.1rem;
    padding-block: 1rem;
  }
  .eaten-log-row__macros { text-align: left; white-space: normal; }
}

@media (max-width: 48rem) {
  .pantry-item__summary { grid-template-columns: minmax(0, 1fr); }
  .pantry-item__qty { text-align: left; }
  .today-ingredients-row { grid-template-columns: 5rem minmax(0, 1fr); }
}

/* -- Job #5, part 2: Your coach -- an earlier-conversations archive index,
   not a collapsed toggle -------------------------------------------------- */
.archive-index { margin-block-end: 3rem; }
.archive-list { list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--ink); }
.archive-row { padding: 0; }
.archive-row__link {
  display: flex;
  align-items: baseline;
  gap: 1.25rem;
  width: 100%;
  min-height: 48px;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  font-size: 1.125rem;
  color: var(--ink);
  cursor: pointer;
  padding-block: 1rem;
}
.archive-row__link:hover { color: var(--accent); }
.archive-row__date { color: var(--ink-secondary); min-width: 9rem; }
/* Display-compliance item F4: a flex item's automatic min-width defaults to
   its own min-content (here, its longest unbroken word) -- on the coach
   sidebar's narrow column (it claims only 1/3 of the page, see
   .workspace-split above) that floor was still wider than the space left
   after the date column, so the title's overflow got hard-clipped by
   .workspace-coach's own `overflow: hidden` instead of wrapping ("Recipe
   Calories and…", "(toda[y]"). `min-width: 0` removes that floor so the
   title can shrink and wrap onto as many lines as it needs; overflow-wrap
   is a last-resort safety net for a single token too long to fit even at
   that width (never needed for ordinary multi-word titles). */
.archive-row__title { font-weight: 700; min-width: 0; overflow-wrap: break-word; }
/* Item F4: "(today)" used to be the ONLY blue text in the row, which read
   as "this badge is the link" rather than "the whole row is" (it's a real
   <button> -- see _render_history_item). A neutral badge, same ink as the
   date beside it, removes that false signifier; the row's own :hover/
   :focus-visible states (above) already carry the real "this is clickable"
   signal for the whole title. */
.archive-row__current { color: var(--ink-secondary); font-weight: 700; }
.archive-viewer {
  margin-block-start: 1rem;
  padding-block-start: 1.5rem;
  border-top: 2px solid var(--ink);
}
/* Placeholder shows only while the log is genuinely empty (no conversation
   selected yet) -- pure CSS, no JS needed: :empty flips the instant htmx
   swaps real content into #archive-viewer-log. */
.archive-viewer__placeholder { display: none; }
#archive-viewer-log:empty + .archive-viewer__placeholder { display: block; }
.archive-viewer .chat-log { max-height: 60vh; overflow-y: auto; }
