/* ============================================================================
   THE CARD.
   Implemented from me/family/meal-planner-app/design-spec.md (29 Jul 2026).

   Written after Daniel said "I can hardly read it". That was not taste, it was
   measurable: the old ingredient text computed to 9.3 arcminutes at a realistic
   65 cm bench distance, against ISO 15008's absolute floor of 12 for glance
   reading and its recommended 20. It was out of spec by 45 percent.

   Rules that must not be broken (grep for them before shipping):
     - no font-size in vw            (vw resolves against the ~794px A4 page box)
     - no font-weight below 400      (thin stems antialias to grey mush)
     - no opacity or rgba() on text  (a composited ratio is not computable)
     - no 100vh                      (use 100svh, or mobile chrome eats a step)
     - --quiet is used at >= 24px only, and never on --paper2
   ========================================================================== */

/* ============================================================================
   THE CARD. app.css tokens.
   Rules that must not be broken:
     - no font-size in vw          (vw resolves against the ~794px A4 page box)
     - no font-weight below 400    (thin stems antialias to grey mush)
     - no opacity or rgba() on text (ratio stops being computable)
     - --quiet is used at >= 24px only, and never on --paper2
   ========================================================================== */

:root {
  color-scheme: light;
  --ui-scale: 1;

  /* -appleSystem leads so Safari resolves SF before the system-ui keyword,
     which had a serif fallback bug in older Android WebViews. */
  --font: -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  /* Used on exactly two things: the meal title and the strapline. Both are read
     at rest, never while cooking. */
  --font-card: ui-serif, "New York", Georgia, "Noto Serif", "Times New Roman", serif;

  /* ---------- DAY. Cream card stock. THE DEFAULT FOR EVERY DEVICE. ---------- */
  --paper:  #FAF6EE;
  --paper2: #F0E9DA;   /* pressed rows and the cupboard block only */
  --ink:    #191512;   /* 16.83:1 on paper   15.01:1 on paper2   AAA */
  --ink-2:  #453D33;   /*  9.90:1 on paper    8.83:1 on paper2   AAA */
  --quiet:  #5A5244;   /*  7.15:1 on paper                       AAA, >=24px only */
  --accent: #6B2306;   /* 10.43:1 on paper    9.30:1 on paper2   AAA. THE NUMBER. */
  --rule:   #8F8271;   /*  3.48:1 on paper    3.10:1 on paper2   non-text, SC 1.4.11 */
  --focus:  #0A44B0;   /*  7.90:1 on paper */
  /* knockout: --paper on --ink band 16.83:1 · --paper on --accent 10.43:1 */

  /* ---------- spacing. Nothing under 8px is used in a kitchen view. -------- */
  --s1: 8px;  --s2: 12px; --s3: 16px; --s4: 24px;
  --s5: 32px; --s6: 48px; --s7: 64px;
  --pad: 20px;
  --tap: 112px;        /* the advance band */

  /* ---------- elevation. Depth, not a colour change: shadows key off --ink so
     they stay correct across all three lighting modes without a fourth token
     set. Radius is soft enough to read as "designed" without turning the
     rule-boxed stamps/chips into pills, which the stamp system deliberately
     is not (see the note above .chip). ------------------------------------- */
  --radius-card: 18px;
  --radius-btn:  14px;
  --radius-box:  8px;    /* chips, inputs: a softened corner, still "a box" */
  --shadow-card: 0 1px 2px rgba(25,21,18,.05), 0 10px 26px -8px rgba(25,21,18,.12);
  --shadow-btn:  0 1px 2px rgba(25,21,18,.06), 0 6px 16px -6px rgba(25,21,18,.14);
  --shadow-bar:  0 -6px 20px -4px rgba(25,21,18,.10);
  --press: translateY(1px);

  /* ---------- type, phone. Every value is a viewing-distance decision. ----- */
  --fs-dish:    calc(2.75rem  * var(--ui-scale));  /* 44 */
  --fs-strap:   calc(1.5rem   * var(--ui-scale));  /* 24 */
  --fs-readout: calc(3.5rem   * var(--ui-scale));  /* 56 */
  --fs-stepno:  calc(3rem     * var(--ui-scale));  /* 48 */
  --fs-step:    calc(2.75rem  * var(--ui-scale));  /* 44 */
  --fs-timer:   calc(3rem     * var(--ui-scale));  /* 48 */
  --fs-advance: calc(1.75rem  * var(--ui-scale));  /* 28 */
  --fs-qty:     calc(2rem     * var(--ui-scale));  /* 32 */
  --fs-name:    calc(1.75rem  * var(--ui-scale));  /* 28 */
  --fs-note:    calc(1.5rem   * var(--ui-scale));  /* 24 */
  --fs-peek:    calc(1.5rem   * var(--ui-scale));  /* 24 */
  --fs-label:   calc(1.375rem * var(--ui-scale));  /* 22 */
  --fs-buy:     calc(1.75rem  * var(--ui-scale));  /* 28 */
  --fs-item:    calc(1.625rem * var(--ui-scale));  /* 26 */
  --fs-sub:     calc(1rem     * var(--ui-scale));  /* 16 */
}

/* ---------- EVENING. Kraft stock. The 6pm answer nobody builds. -------------
   Dark ink on a dimmed ground: ~64% of the light a white screen throws, with
   the pupil-constricting positive polarity kept. Flipping to light-on-dark in
   a dim kitchen is the worst condition in the glance-reading literature. */
:root[data-card="evening"] {
  color-scheme: light;
  --paper:  #D9D0BE;
  --paper2: #CDC3AE;
  --ink:    #14100C;   /* 12.37:1 on paper   10.83:1 on paper2   AAA */
  --ink-2:  #2A241B;   /* 10.04:1 on paper    8.79:1 on paper2   AAA */
  --quiet:  #423A2D;   /*  7.32:1 on paper                       AAA, >=24px only */
  --accent: #521B03;   /*  9.04:1 on paper    7.92:1 on paper2   AAA */
  --rule:   #6E6350;   /*  3.85:1 on paper    3.37:1 on paper2   non-text */
  --focus:  #0B3FA8;   /*  5.99:1 on paper */
  /* knockout: --paper on --ink 12.37:1 · --paper on --accent 9.04:1 */
  --shadow-card: 0 1px 2px rgba(20,16,12,.08), 0 10px 24px -8px rgba(20,16,12,.16);
  --shadow-btn:  0 1px 2px rgba(20,16,12,.08), 0 6px 16px -6px rgba(20,16,12,.18);
  --shadow-bar:  0 -6px 20px -4px rgba(20,16,12,.14);
}

/* ---------- NIGHT. The card printed in reverse. -----------------------------
   NOT #FFF on #000. That is 21:1 and it is wrong: peak-white strokes on a
   zero-luminance ground bloom, and roughly a third of people have enough
   astigmatism to see the smear. This tops out at 16.12:1 with none of it.
   Negative polarity carries a measured penalty that is worst at small sizes,
   so the whole scale goes up 12% to pay it back. */
:root[data-card="night"] {
  color-scheme: dark;
  --ui-scale: 1.12;
  --paper:  #16120E;
  --paper2: #201B15;
  --ink:    #F4EEE1;   /* 16.12:1 on paper   14.78:1 on paper2   AAA */
  --ink-2:  #D8CEBC;   /* 11.96:1 on paper   10.96:1 on paper2   AAA */
  --quiet:  #B8AC99;   /*  8.34:1 on paper    7.65:1 on paper2   AAA */
  --accent: #FFC266;   /* 11.68:1 on paper   10.71:1 on paper2   AAA */
  --rule:   #786C5D;   /*  3.64:1 on paper    3.34:1 on paper2   non-text */
  --focus:  #FFC266;   /* 11.68:1 on paper */
  /* knockout: --paper on --ink 16.12:1 · --paper on --accent 11.68:1 */
  /* A dark shadow reads as mud on a dark ground, so depth here comes from the
     border and --paper2 fill instead of elevation. */
  --shadow-card: none;
  --shadow-btn: none;
  --shadow-bar: none;
}

/* prefers-color-scheme is DELIBERATELY NOT READ. Patricia's phone dark-mode
   setting is about reading in bed; it is not a statement about her kitchen at
   8am, where a dark screen becomes a mirror and a veiling reflection can
   collapse 16:1 to under 3:1 with no CSS fix. She picks once, from three big
   labelled buttons showing the actual stock. It never changes under her hands. */

:root[data-text="big"]    { --ui-scale: 1.15; }
:root[data-text="bigger"] { --ui-scale: 1.30; }
:root[data-card="night"][data-text="big"]    { --ui-scale: 1.28; }
:root[data-card="night"][data-text="bigger"] { --ui-scale: 1.45; }

/* 700 to 1023 only. This block scales type UP because it was written for ONE
   device: a tablet propped on the kitchen bench, read at arm's length with your
   hands busy. A 1280px laptop used to match it too, which is how the planning
   screens ended up with 64px headings sitting above 13px nav labels. A laptop is
   read at 50cm, sitting down, with a mouse. It gets its own block further down. */
@media (min-width: 700px) and (max-width: 1023px) {   /* tablet: the bench device */
  :root {
    --pad: 40px; --tap: 128px;
    --fs-dish:    calc(4rem     * var(--ui-scale));  /* 64 */
    --fs-strap:   calc(1.75rem  * var(--ui-scale));  /* 28 */
    --fs-readout: calc(4.5rem   * var(--ui-scale));  /* 72 */
    --fs-stepno:  calc(4rem     * var(--ui-scale));  /* 64 */
    --fs-step:    calc(4rem     * var(--ui-scale));  /* 64 */
    --fs-timer:   calc(4rem     * var(--ui-scale));  /* 64 */
    --fs-advance: calc(2rem     * var(--ui-scale));  /* 32 */
    --fs-qty:     calc(2.75rem  * var(--ui-scale));  /* 44 */
    --fs-name:    calc(2.375rem * var(--ui-scale));  /* 38 */
    --fs-note:    calc(1.75rem  * var(--ui-scale));  /* 28 */
    --fs-peek:    calc(1.625rem * var(--ui-scale));  /* 26 */
    --fs-label:   calc(1.5rem   * var(--ui-scale));  /* 24 */
    --fs-buy:     calc(2rem     * var(--ui-scale));  /* 32 */
    --fs-item:    calc(1.875rem * var(--ui-scale));  /* 30 */
  }
}

/* SHORT VIEWPORT: the CHROME yields, never the type. This is the graft that
   makes the one-step-per-screen promise honest. All three source designs
   budgeted against 667px on an iPhone SE; Safari's svh there is ~573px with
   the toolbars up, and all three broke their own no-scroll rule at their own
   character cap. This query recovers it. */
@media (max-height: 780px) {
  :root { --tap: 96px; }
  .peek { display: none; }
  .step-head { min-height: 48px; }
}

/* ------------------------------------------- THE INGREDIENT LINE (cook step 0) */
.ing {
  display: grid;
  grid-template-columns: 1fr 2.75rem;
  grid-template-areas: "line box"
                       "note box";
  column-gap: var(--s2);
  align-items: start;
  min-height: 72px;
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none; user-select: none;
}
.ing:last-of-type { border-bottom: 0; }
.ing:active { background: var(--paper2); }

.ing-line { grid-area: line; line-height: 1.2; text-wrap: pretty; hyphens: none; }

/* THE NUMBER. Larger, heavier, second ink, tabular. Four signals at once, so it
   still leads in greyscale, on paper, and for a colour-blind reader. */
.ing-qty {
  font: 700 var(--fs-qty)/1.2 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--accent);            /* 10.43:1 day · 9.04:1 evening · 11.68:1 night */
  letter-spacing: 0;
  white-space: nowrap;             /* "300 g" never breaks across two lines */
  margin-right: .18em;
}
.ing-name {
  font: 500 var(--fs-name)/1.2 var(--font);
  color: var(--ink);               /* 16.83:1 day · 12.37:1 evening · 16.12:1 night */
  letter-spacing: -.01em;
}

/* Row two of the grid. It starts under the line, it can NEVER move the number,
   and it is editorially capped at 24 characters. This is the fix: the note has
   its own grid area, so the quantity's position is structurally guaranteed no
   matter how long the note is. The old design put everything in one paragraph. */
.ing-note {
  grid-area: note;
  margin-top: 5px;
  font: 400 var(--fs-note)/1.35 var(--font);
  color: var(--ink-2);             /*  9.90:1 day · 10.04:1 evening · 11.96:1 night */
}
.ing-note b {                      /* "LEON", "CAREFUL" */
  font-weight: 700; text-transform: uppercase; letter-spacing: .12em;
  font-size: .82em; margin-right: .5em;
}

/* The tick goes on the RIGHT. The left edge is the number rail and nothing may
   sit to the left of a quantity. It also means her thumb never covers the text
   she is reading. The whole 72px row is the target, so nothing is aimed at. */
.ing input {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
.ing-box {
  grid-area: box; align-self: center; justify-self: end;
  width: 40px; height: 40px;
  border: 3px solid var(--rule);   /* 3.48:1, clears SC 1.4.11 non-text */
  display: grid; place-items: center;
  font: 700 24px/1 var(--font); color: transparent;
}
.ing:has(input:checked) .ing-box {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}
.ing:has(input:checked) .ing-box::after { content: "\2713"; }

/* Crossed off the way you cross a card off: a SHAPE change, not just a colour,
   and an explicit token rather than opacity so the ratio stays computable.
   Ticked rows stay fully legible, because "did I already get the masarepa" is
   asked constantly. Nothing moves, nothing reorders, nothing disappears. */
.ing:has(input:checked) .ing-line {
  color: var(--ink-2);
  text-decoration: line-through;
  text-decoration-thickness: 3px;
  text-decoration-color: var(--ink-2);
}
.ing:has(input:checked) .ing-qty  { color: var(--ink-2); }
.ing:has(input:checked) .ing-note { display: none; }

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 1rem/1.45 var(--font);
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
/* The 140px bottom clearance is for the fixed bottom bars, so it belongs to the
   PAGE, once. It used to live on .wrap, and the aisle-based screens nest a .wrap
   inside every section: Shopping has six, Pins three. That is
   ~700px of dead space per page, appearing as a large unexplained gap between
   every aisle band. Cook mode is excluded because .step does its own
   viewport-height maths and must not be pushed. */
.wrap { max-width: 760px; margin: 0 auto; padding: var(--s4) var(--pad); }
body:not(.cooking) { padding-bottom: 140px; }
.bleed { margin-inline: calc(var(--pad) * -1); }

:where(a, button, label, input, summary):focus-visible {
  outline: 4px solid var(--focus); outline-offset: 3px;
}

.eyebrow {
  margin: 0 0 var(--s1);
  font: 700 var(--fs-label)/1.2 var(--font);
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--ink-2);
}
.num { font-variant-numeric: tabular-nums lining-nums; letter-spacing: 0; }

/* A band. Reversed caps on solid ink at 16.83:1 (day) / 16.12:1 (night). This is
   the highest-contrast structural marker available and it is what the eye finds
   first at 60cm in poor light. */
.band {
  display: flex; align-items: baseline; gap: .6em;
  margin: var(--s6) 0 var(--s3);
  padding: 11px var(--pad);
  background: var(--ink); color: var(--paper);
  font: 700 var(--fs-label)/1.25 var(--font);
  text-transform: uppercase; letter-spacing: .13em;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}
.band:first-of-type { margin-top: var(--s4); }
.band .no { font-size: 1.2em; letter-spacing: .05em;
            font-variant-numeric: tabular-nums lining-nums; }

/* A stamp. Rule-boxed caps. Carries state as a WORD, never as a colour, and it
   is set SQUARE. A rotated stamp is the visual grammar of OVERDUE and REJECTED,
   and the graphic would say what the copy is careful not to. */
.stamp {
  display: inline-block; padding: 2px 8px;
  border: 2px solid var(--ink); color: var(--ink);
  font: 700 .8em/1.4 var(--font);
  text-transform: uppercase; letter-spacing: .16em;
  vertical-align: 2px;
}

/* nav.tabs. RESTYLED, because replacing app.css otherwise unstyles it. */
nav.tabs {
  position: fixed; inset: auto 0 0 0; z-index: 50;
  display: flex;
  background: var(--paper);
  border-top: 3px solid var(--ink);
  box-shadow: var(--shadow-bar);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
nav.tabs a {
  flex: 1; min-height: 66px;
  display: grid; place-items: center; align-content: center; gap: 2px;
  padding: 8px 4px;
  color: var(--ink-2);                    /* 9.90:1 day */
  font: 700 13px/1.1 var(--font);
  text-transform: uppercase; letter-spacing: .09em;
  border-top: 4px solid transparent; margin-top: -3px;
  touch-action: manipulation;
}
nav.tabs a .ico { font-size: 22px; line-height: 1; }
nav.tabs a.on { color: var(--ink); border-top-color: var(--accent); }
body.cooking nav.tabs { display: none; }  /* cook mode is full screen */

/* THE ADVANCE BAND. The one control that must never be missed. 112px can be hit
   with a knuckle, a wrist, or the back of a greasy finger. Greasy hands are
   solved with target size and forgiveness, not sensors. */
.advance {
  position: fixed; inset: auto 0 0 0; z-index: 40;
  display: grid; grid-template-columns: 1fr; gap: 2px;
  background: var(--rule);
  border-top: 3px solid var(--ink);
  box-shadow: var(--shadow-bar);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* On any screen that still shows the bottom tab bar (Tonight, Week), .advance and
   nav.tabs were both pinned to the exact same inset:auto 0 0 0. nav.tabs has the
   higher z-index (50 vs 40) and painted over the bottom of the action button, so
   its label read as cut off or hidden. Screens where nav.tabs is hidden (cook
   mode, rate, print) are unaffected: this rule only fires when nav.tabs actually
   exists on the page. */
body:has(nav.tabs) .advance {
  bottom: 66px;   /* matches .shopbar's own pre-existing "above nav.tabs" offset */
}
.advance.two { grid-template-columns: 6rem 1fr; }
.advance a, .advance button {
  display: grid; place-items: center;
  min-height: var(--tap); padding: 0 var(--s3); margin: 0; border: 0;
  font: 700 var(--fs-advance)/1.1 var(--font);
  letter-spacing: .02em; text-align: center;
  cursor: pointer; touch-action: manipulation;
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.advance .go   { background: var(--accent); color: var(--paper); } /* 10.43:1 */
.advance .go:active { background: var(--ink); }
.advance .back {
  background: var(--paper); color: var(--ink-2);
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .13em;
}

/* Centred by grid for the same reason as .btn-go: this class is worn by <button>,
   <a> and <summary>, and only <button> centres its own label. */
.btn-quiet {
  display: grid; place-items: center;
  width: 100%; min-height: 88px;
  padding: var(--s2) var(--s3);
  background: transparent; color: var(--ink);
  border: 3px solid var(--rule);
  border-radius: var(--radius-btn);
  font: 500 var(--fs-note)/1.2 var(--font);
  text-align: center; text-decoration: none;
  touch-action: manipulation;
  transition: background .12s ease, transform .12s ease;
}
.btn-quiet:active { background: var(--paper2); transform: var(--press); }
/* The escape hatch. A legal 64px target, deliberately unshouty, and 64px BELOW
   the advance band so a mistimed tap cannot reach it. */
.escape { margin: var(--s7) 0 0; text-align: center; }
.escape a {
  display: inline-block; padding: var(--s4) var(--s3);
  min-height: 64px;
  color: var(--quiet);                    /* 7.15:1 day, used at 24px */
  font-size: var(--fs-note);
  text-decoration: underline; text-underline-offset: 5px;
}

.dish {
  margin: var(--s1) 0 var(--s2);
  font: 400 var(--fs-dish)/1.08 var(--font-card);
  letter-spacing: -.015em; text-wrap: balance;
  color: var(--ink);
}
/* The strapline is the only appetite copy in the product and it is what a
   photograph would have done, for one TEXT column and no asset pipeline.
   If it is empty the block renders nothing, so it degrades to silence. */
.strap {
  margin: 0; max-width: 30ch;
  font: 400 italic var(--fs-strap)/1.35 var(--font-card);
  color: var(--ink-2);                      /* 9.90:1 day */
}
.strap:empty { display: none; }

/* Time and serves as tracked caps between two hairlines, not a row of pills.
   Pills are app furniture; this prints unchanged. */
.badges {
  margin: var(--s4) 0 0; padding: var(--s2) 0;
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  font: 700 var(--fs-label)/1.3 var(--font);
  text-transform: uppercase; letter-spacing: .13em;
  color: var(--ink-2);
}

/* ONE readout slot, reused by every state of this screen. */
.readout { margin: var(--s5) 0 0; }
.readout b {
  display: block; margin: 2px 0 4px;
  font: 700 var(--fs-readout)/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -.02em;
  color: var(--accent);                     /* 10.43:1 day */
}
.readout .sub { font-size: var(--fs-note); color: var(--ink-2); }

/* Grab these bags. Bound by a NUMBER first, colour second: a numeral survives a
   mono laser, poor light and colour blindness. */
.grab { list-style: none; margin: var(--s5) 0 0; padding: 0; }
.grab li {
  display: flex; align-items: center; gap: .7em;
  min-height: 64px; padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
  font: 500 var(--fs-item)/1.3 var(--font);
}
.grab li b {
  flex: 0 0 auto; padding: 6px 11px;
  background: var(--ink); color: var(--paper);    /* 16.83:1 */
  font: 700 var(--fs-label)/1 var(--font);
  text-transform: uppercase; letter-spacing: .1em;
  font-variant-numeric: tabular-nums lining-nums;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}

/* No new components. The readout slot swaps its number, the eyebrow swaps its
   label, and a ghost button appears. In this state a start-by clock is actively
   WRONG information, because there is no food to start with. */
.hint {
  margin: var(--s4) 0 0;
  font-size: var(--fs-note); color: var(--ink-2);
  max-width: 34ch;
}

hr.rule-heavy { border: 0; border-top: 3px solid var(--ink); margin: var(--s5) 0 var(--s4); }
hr.rule-hair  { border: 0; border-top: 1px solid var(--rule); margin: var(--s4) 0; }

.step-out {
  display: grid; place-items: center;
  width: 64px; height: 64px; margin-left: -14px;
  color: var(--quiet); font: 700 var(--fs-label)/1 var(--font);
  text-transform: uppercase; letter-spacing: .13em;
  touch-action: manipulation;
}

body.cooking { background: var(--paper); }

.step {
  position: relative; z-index: 2;
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  min-height: 100svh;                     /* svh, NEVER vh: on iOS 100vh is the
                                             tall viewport, so the step clips
                                             behind the toolbar, which is a
                                             scroll, which breaks the promise */
  padding: var(--s2) var(--pad) calc(var(--tap) + var(--s4));
  pointer-events: none;                   /* see the tap-zone note below */
}
.step a, .step button, .step label, .step input,
.step .step-text { pointer-events: auto; }

.step-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3); min-height: 64px;
}
.step-count { display: flex; align-items: baseline; gap: .35em; margin: 0; }
.step-n {
  font: 700 var(--fs-stepno)/.9 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--accent);
}
.step-count span {
  font: 700 var(--fs-label)/1 var(--font);
  text-transform: uppercase; letter-spacing: .13em; color: var(--ink-2);
}

.step-body { display: grid; align-content: center; justify-items: start; }
.step-text {
  margin: 0; max-width: 17ch;
  font: 500 var(--fs-step)/1.22 var(--font);
  letter-spacing: -.012em;
  color: var(--ink);
  text-align: left;                       /* left text, centred block: far easier
                                             to re-find your place after you
                                             look away at the pan */
  text-wrap: pretty; hyphens: none;
}
/* LENGTH TIERS. The 44px step assumed a 105-character instruction; the real
   library's median is 184 and 102 of 435 steps arrive with no usable headline
   split at all. At 44px in a 17ch column, a 369-character step is 1718px tall
   inside an 800px screen: 860px of it is clipped and unreachable, which is
   exactly what Daniel hit ("I can't read most of what's in those steps").

   Type this large is a real design decision, not decoration: it is what makes a
   step readable at arm's length with your hands in a bowl. So a short step keeps
   every pixel of it, and only a long one steps down, just far enough to fit. The
   measure widens as the size drops so the line length stays comfortable.
   Nothing is ever truncated. */
.step-text.mid   { font-size: calc(var(--fs-step) * .74); max-width: 24ch; }
.step-text.long  { font-size: calc(var(--fs-step) * .60); max-width: 30ch; }
.step-text.xlong { font-size: calc(var(--fs-step) * .48); max-width: 36ch; line-height: 1.3; }

/* THE PROMISE: the number is repeated inside the sentence and it pops. */
.step-text .qty {
  font-size: 1.1em; font-weight: 700;
  color: var(--accent);                   /* 10.43:1 day · 11.68:1 night */
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: 0; white-space: nowrap;
}
/* One string, two surfaces: the tick list and this. */
.step-text .it { font-weight: 700; }

/* Under "Bigger text" on a very short phone a 105-char step can exceed the
   free space. Scroll it. A scroll she opted into beats silent clipping. */
:root[data-text="big"] .step-body,
:root[data-text="bigger"] .step-body { overflow-y: auto; }

/* Tap the lower half to go on, the lower-left corner to go back. Nothing to aim
   at. The top half is inert so a tap near the step text can never skip it.
   .step spans the viewport, so it must not eat the taps: it is made
   pointer-transparent and its real controls take events back. */
.tapzones { position: fixed; inset: 50% 0 var(--tap) 0; display: flex; z-index: 1; }
.tapzones a { flex: 1; }
.tapzones a.back { flex: 0 0 26%; }

.mins {
  display: inline-block; padding: .16em .34em; margin: 0 .04em;
  background: transparent; border: 3px solid var(--accent); border-radius: 5px;
  color: var(--accent); font: inherit; font-weight: 700;
  font-variant-numeric: tabular-nums lining-nums;
  white-space: nowrap; cursor: pointer; touch-action: manipulation;
}
/* At a 44px font this box is ~64px tall, clearing the 44px enhanced target, and
   it prints as a black box around the duration, which is useful on paper. */

/* Inline to start, PINNED to persist. It survives the step change, which is the
   gap every app leaves and which a one-step-per-screen product cannot afford. */
.timerbar {
  position: fixed; z-index: 39;
  inset: auto 0 calc(var(--tap) + env(safe-area-inset-bottom, 0px)) 0;
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: var(--s3);
  padding: var(--s2) var(--pad);
  background: var(--paper); border-top: 3px solid var(--ink);
}
.timerbar .t {
  font: 700 var(--fs-timer)/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -.01em; color: var(--accent);
}
.timerbar .what {
  font: 700 var(--fs-label)/1.2 var(--font);
  text-transform: uppercase; letter-spacing: .13em; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.timerbar .hold {
  min-width: 88px; min-height: 64px; padding: 0 var(--s3);
  background: transparent; border: 3px solid var(--rule); color: var(--ink);
  font: 700 var(--fs-label)/1 var(--font);
  text-transform: uppercase; letter-spacing: .12em;
  touch-action: manipulation;
}
/* Finished: the digits go to --ink-2 and count UP with a + prefix. There is no
   red slab, no "TIME'S UP", no vibration alarm. Two soft notes, played twice,
   then it stops. A kitchen timer that keeps shouting gets turned off for good. */
.timerbar.done .t { color: var(--ink-2); }
.timerbar.done .what::after { content: ""; }
body.timing .peek { display: none; }     /* one thing at a time */

.tape { display: flex; gap: 4px; height: 8px; margin: var(--s2) 0 0; }
.tape i { flex: 1; background: var(--rule); }         /* 3.48:1 non-text */
.tape i.done { background: var(--ink-2); }
.tape i.now  { background: var(--accent); }
/* Redundant with the "3 OF 6" numeral in the head, on purpose. Colour is never
   the only signal, and redundancy here costs nothing. */

.rate-row { display: flex; gap: var(--s3); flex-wrap: wrap; margin: var(--s4) 0 var(--s6); }
.rate-row label {
  width: 88px; height: 88px;                 /* a four year old will use these */
  display: grid; place-items: center;
  border: 3px solid var(--rule); font-size: 40px; line-height: 1;
  cursor: pointer; touch-action: manipulation;
}
.rate-row input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.rate-row label:has(input:checked) { border-color: var(--ink); border-width: 6px; }

/* Skip carries the SAME weight as Save. The current screen has Skip as a tiny
   muted link and asks four questions per cook, which is a form, and forms are
   what she stops filling in by week five. */
.rate-actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.rate-actions .go   { background: var(--accent); color: var(--paper); }
.rate-actions .skip { background: transparent; color: var(--ink); border: 3px solid var(--rule); }
.rate-actions button, .rate-actions a {
  min-height: 96px; font: 700 var(--fs-advance)/1.1 var(--font);
  touch-action: manipulation; text-decoration: none; border: 0;
}

.aisle-band {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: baseline; gap: .6em;
  margin: var(--s5) 0 0; padding: 9px var(--pad);
  background: var(--ink); color: var(--paper);      /* 16.83:1 day · 16.12:1 night */
  font: 700 var(--fs-label)/1.3 var(--font);
  text-transform: uppercase; letter-spacing: .13em;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}
.aisle-band .n {
  margin-left: auto; font-weight: 500; letter-spacing: .04em;
  font-variant-numeric: tabular-nums lining-nums;
}

.sitem {
  display: grid;
  grid-template-columns: 1fr 2.75rem;
  grid-template-areas: "buy  box"
                       "what box"
                       "sub  box";
  column-gap: var(--s2);
  align-items: start;
  min-height: 84px; padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
  cursor: pointer; touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.sitem:active { background: var(--paper2); }
.sitem .buy {
  grid-area: buy;
  font: 700 var(--fs-buy)/1.15 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--accent);                             /* 10.43:1 day */
  white-space: nowrap;
}
.sitem .buy:empty { display: none; }                /* staples get no amount */
.sitem .what {
  grid-area: what; margin-top: 2px;
  font: 500 var(--fs-item)/1.25 var(--font);
  color: var(--ink);                                /* 16.83:1 day */
}
.sitem .sub {
  grid-area: sub; margin-top: 3px; max-width: 34ch;
  font: 400 var(--fs-sub)/1.4 var(--font);
  color: var(--ink-2);                              /* 9.90:1 day */
}
.sitem input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.sitem .box {
  grid-area: box; align-self: center; justify-self: end;
  width: 40px; height: 40px; border: 3px solid var(--rule);
  display: grid; place-items: center; font: 700 24px/1 var(--font); color: transparent;
}
.sitem:has(input:checked) .box { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.sitem:has(input:checked) .box::after { content: "\2713"; }
.sitem:has(input:checked) .buy,
.sitem:has(input:checked) .what {
  color: var(--ink-2); text-decoration: line-through; text-decoration-thickness: 2px;
}
.sitem:has(input:checked) .sub { display: none; }

/* A finished AISLE shrinks to its band. No individual row ever moves, reorders
   or disappears under a thumb. Items that jump cause mis-taps and destroy the
   sense of place. */
.aisle.done .sitem { display: none; }
.aisle.done .aisle-band {
  background: var(--paper); color: var(--ink-2);
  border-bottom: 1px solid var(--rule);
}
.aisle.done .aisle-band::after { content: "\2713 all"; margin-left: auto; }

.shopbar {
  position: fixed; inset: auto 0 66px 0; z-index: 40;   /* above nav.tabs */
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--pad);
  background: var(--paper); border-top: 3px solid var(--ink);
  box-shadow: var(--shadow-bar);
}
.shopbar .count {
  font: 700 var(--fs-item)/1.2 var(--font);
  font-variant-numeric: tabular-nums lining-nums; color: var(--ink-2);
}
.shopbar .tape { flex: 1; margin: 0; }
.shopbar.complete { display: block; padding: 0; inset: auto 0 66px 0; }
.shopbar.complete .count, .shopbar.complete .tape { display: none; }
.shopbar.complete button {
  display: block; width: 100%; min-height: var(--tap); border: 0;
  background: var(--accent); color: var(--paper);
  font: 700 var(--fs-advance)/1.1 var(--font);
}

/* The one place <details> is used, plus Meals. She is standing still, in a shop,
   with a trolley, and this is exactly when the information is worth something. */
.note summary {
  min-height: 64px; display: flex; align-items: center;
  color: var(--accent); font-weight: 700; font-size: var(--fs-sub);
  cursor: pointer; touch-action: manipulation;
}
.note p { margin: var(--s1) 0 0; max-width: 34ch; font-size: var(--fs-sub); color: var(--ink-2); }
.print-only { display: none; }

.mealcard {
  display: grid; grid-template-columns: 1fr auto; align-items: baseline;
  gap: var(--s2);
  min-height: 88px; padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
  touch-action: manipulation;
}
.mealcard h3 {
  margin: 0; font: 400 var(--fs-item)/1.15 var(--font-card);
  letter-spacing: -.01em; color: var(--ink);
}
.mealcard .strap {
  grid-column: 1; margin: 4px 0 0; font-size: var(--fs-sub);
  font-style: italic; color: var(--ink-2);
}
.mealcard .meta {
  grid-column: 1; margin: 6px 0 0;
  font: 700 13px/1.3 var(--font);
  text-transform: uppercase; letter-spacing: .12em; color: var(--ink-2);
}
.mealcard .fav { font-size: 24px; color: var(--accent); }
/* No average score is shown. This screen carries: title, strapline, time,
   serves, last cooked, and a fav mark. Scores stay in the database where the
   planner can use them; they are never displayed back to the person who cooked. */

@media print {
  :root {
    --ui-scale: 1;
    --paper: #fff; --paper2: #fff;
    --ink: #000; --ink-2: #222; --quiet: #444;
    --accent: #000;                 /* colour collapses; SIZE and WEIGHT carry the number */
    --rule: #000; --pad: 0;
    --fs-dish: 28pt;  --fs-strap: 13pt; --fs-readout: 24pt; --fs-label: 10pt;
    --fs-stepno: 30pt; --fs-step: 15pt; --fs-timer: 15pt;
    --fs-qty: 16pt;   --fs-name: 15pt;  --fs-note: 11pt;
    --fs-buy: 10.5pt; --fs-item: 10.5pt; --fs-sub: 8.5pt;
  }
  @page { size: A4 portrait; margin: 14mm 12mm; }
  /* padding-bottom clears the on-screen fixed bars, which do not exist on paper
     and would otherwise leave 140px of blank at the foot of every sheet. */
  body { background: #fff; color: #000; font-size: 11pt; padding-bottom: 0 !important; }

  nav.tabs, .tapzones, .advance, .timerbar, .shopbar, .escape, .noprint,
  .stocks, .peek, .step-out, .tape, .mins, .rate-actions { display: none !important; }
  .wrap, .step { max-width: none; padding: 0; min-height: 0; display: block; }
  .step-body { display: block; }
  .step-text { max-width: none; }

  /* Solid black bands with reversed caps. Browsers DROP background fills in
     print unless told not to, which is what turns a bag label into a blank
     sheet. Floor of 12pt bold for any reversed type. */
  .band, .aisle-band, .grab li b {
    position: static; background: #000 !important; color: #fff !important;
    padding: 2mm 3mm; margin: 5mm 0 2mm; break-after: avoid;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  /* NO GREY FILLS ANYWHERE. A laser halftone-dithers grey and a 20% block behind
     15pt text looks like a printing fault. White ground plus a solid rule. */
  .ing, .sitem, .step { break-inside: avoid; page-break-inside: avoid;
                        min-height: 0 !important; padding: 1.4mm 0; }
  .ing { border-bottom: .15mm solid #999; }
  .sitem { border-bottom: .15mm solid #999;
           grid-template-areas: "buy what box" ; grid-template-columns: auto 1fr 5mm; }
  .sitem .sub { display: inline; }

  /* Checkboxes are drawn in CSS: native checkbox print rendering is unreliable
     and often vanishes entirely on a mono laser. */
  .ing-box, .sitem .box {
    border: .35mm solid #000; width: 4mm; height: 4mm; background: none !important;
  }
  .ing:has(input:checked) .ing-box,
  .sitem:has(input:checked) .box { background: #000 !important; }
  .ing:has(input:checked) .ing-box::after,
  .sitem:has(input:checked) .box::after { content: "\2713"; color: #fff; }

  .aisle.done .sitem { display: grid; }   /* collapsed aisles expand on paper */
  .sitem-card { box-shadow: none !important; border-radius: 0 !important; }
  .shop-cols { columns: 2; column-gap: 10mm; }   /* a 34-item week fits one A4 page */
  .printhead { display: block !important; column-span: all;
               border-bottom: .5mm solid #000; padding-bottom: 2mm; margin-bottom: 3mm; }
  .print-only { display: block !important; }
  details { display: none; }              /* rendered twice from Jinja instead */
  a[href]::after { content: none; }
}

/* ============================================================================
   Shell styles for screens the design spec did not cover component by component
   (Week, Meals, Rate, Recipe). Same tokens, same rules: no opacity on text, no
   weight below 400, nothing under 8px, every colour a named token.
   ========================================================================== */

.sitem-card {
  margin: 0 0 var(--s3);
  padding: var(--s3);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  transition: box-shadow .15s ease, transform .15s ease;
}
.sitem-card:last-of-type { margin-bottom: 0; }
.sitem-card:has(a:active) { box-shadow: none; transform: var(--press); }
.sitem-card a { color: inherit; text-decoration: none; display: block; }
/* ...but NOT a primary button that happens to live inside a card.
   `.sitem-card a` is specificity 0,1,1 and `.btn-go` is 0,1,0, so the rule above
   silently won and repainted the button's label with the card's --ink. The flow
   widget's "Tonight's dinner" button was rendering near-black on the dark accent
   at about 2:1, against a 4.5:1 floor, on a stylesheet whose header promises AAA
   contrast everywhere. Found by reading the COMPUTED colour off the live page,
   not by looking at the markup, which is the only way this class of bug shows
   up: nothing in the HTML looks wrong. */
.sitem-card a.btn-go, .sitem-card a.btn-go:visited { color: var(--paper); }
.sitem-card b { font: 700 var(--fs-item)/1.25 var(--font); color: var(--ink); }

.row { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.row.between { justify-content: space-between; }
.grow { flex: 1 1 auto; }

/* Stamps are rule-boxed words, not coloured pills. A word survives mono print,
   colour blindness, glare and an off-axis screen. A hue does not. */
.stamp {
  display: inline-block; margin: 6px 6px 0 0; padding: 4px 9px;
  border: 2px solid var(--rule);
  border-radius: var(--radius-box);
  font: 700 var(--fs-sub)/1.2 var(--font);
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-2);
}

.band-group { margin: var(--s5) 0 0; }

.faces { display: flex; gap: var(--s2); }
.faces label {
  flex: 1; min-height: 88px; display: grid; place-items: center;
  border: 3px solid var(--rule); font-size: 44px; cursor: pointer;
}
.faces input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.faces label:has(input:checked) { border-color: var(--ink); background: var(--paper2); }

.stars { display: flex; gap: var(--s2); font-size: 40px; }
.stars label { cursor: pointer; min-width: 56px; min-height: 56px; }

.check {
  display: flex; align-items: center; gap: var(--s3);
  min-height: 64px; font: 500 var(--fs-item)/1.3 var(--font); color: var(--ink);
}
.check input { width: 32px; height: 32px; accent-color: var(--ink); }

table.list { width: 100%; border-collapse: collapse; }
table.list td {
  padding: var(--s2) 0; border-bottom: 1px solid var(--rule);
  font: 500 var(--fs-item)/1.3 var(--font); color: var(--ink); vertical-align: top;
}
table.list td.q {
  text-align: right; white-space: nowrap; font-weight: 700; color: var(--accent);
  font-variant-numeric: tabular-nums lining-nums;
}

select, input[type=text], input[type=time] {
  width: 100%; min-height: 64px; padding: var(--s2);
  background: var(--paper); color: var(--ink);
  border: 2px solid var(--rule);
  border-radius: var(--radius-box);
  font: 500 var(--fs-item)/1.3 var(--font);
}

/* The paste box. `textarea` had no rule at all, because until "paste a recipe"
   the app had no multi-line input anywhere -- and an unstyled browser-default
   textarea sitting in a page of 64px tokenised fields reads as a broken element
   rather than as somewhere to type. Same tokens as the group above; the only
   real difference is the height, because a recipe is twenty lines and a one-line
   box invites pasting half of one. */
textarea {
  width: 100%; min-height: 260px; padding: var(--s2);
  background: var(--paper); color: var(--ink);
  border: 2px solid var(--rule);
  border-radius: var(--radius-box);
  font: 500 var(--fs-item)/1.35 var(--font);
  resize: vertical;
}
/* On the review screen each box holds ONE instruction, not a whole recipe, so
   the 260px above would put three fields on a phone screen. */
textarea.one-step { min-height: 96px; }

/* ============================================================================
   Phase 0 repair. The redesign commit replaced this file wholesale and never
   migrated the templates to its vocabulary, so `.btn`, `.ghost`, `.block`,
   `.small`, `.tiny` and `.page-break` had no rules at all, and a bare
   `class="btn advance"` on an in-flow button made THAT ELEMENT ITSELF the fixed
   position:fixed bottom band (`.advance` is a container, `.go`/`.back` are its
   children), so several buttons across the app were stacked on top of each
   other at the bottom of the viewport. This block is the missing half of that
   migration, not a second design system: it uses only the existing tokens.
   ========================================================================== */

/* Secondary text, two sizes below body. Distinct from `.hint`, which also caps
   line length at 34ch for a block of prose; these are inline labels. */
.small { font-size: var(--fs-sub); color: var(--ink-2); }
.tiny  { font-size: calc(var(--fs-sub) * 0.85); color: var(--quiet); }

/* The in-flow accent action: same visual weight as `.advance .go`, but normal
   document flow, for the places two actions sit side by side in a card rather
   than pinned to the screen (Approve/Favourite, the couch-triage Yes button). */
/* grid + place-items, NOT display:block. A <button> centres its own label, so this
   looked fine for as long as it was only ever used on buttons; the first time it
   was put on an <a> the text jammed against the top edge and clipped. Centring
   here means the class behaves the same whichever element wears it. */
.btn-go {
  display: grid; place-items: center;
  width: 100%; min-height: 88px; border: 0;
  padding: var(--s2) var(--s3);
  background: var(--accent); color: var(--paper);
  border-radius: var(--radius-btn);
  box-shadow: var(--shadow-btn);
  font: 700 var(--fs-advance)/1.15 var(--font); letter-spacing: .02em;
  text-align: center; text-decoration: none;
  cursor: pointer; touch-action: manipulation;
  transition: background .12s ease, box-shadow .12s ease, transform .12s ease;
}
.btn-go:visited { color: var(--paper); }
.btn-go:active { background: var(--ink); box-shadow: none; transform: var(--press); }

/* The library's Active/Staged/Retired filter row. A word in a box, not a pill,
   same visual family as `.stamp` but interactive and with an "on" state. */
.chip {
  display: inline-block; padding: 9px 14px;
  border: 2px solid var(--rule); color: var(--ink-2);
  border-radius: var(--radius-box);
  font: 700 var(--fs-sub)/1.2 var(--font);
  text-transform: uppercase; letter-spacing: .08em;
  min-height: 44px;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
/* `.on` is the server-rendered state. `:has(input:checked)` is the LIVE one, and
   without it a chip wrapping a hidden checkbox looks completely dead: you tap a
   name, the checkbox flips, and nothing on screen moves until the form is
   submitted. The lunch chips masked this by auto-submitting on every tap; the
   headcount chips do not, so they read as broken buttons. Same mechanism the
   ingredient rows, shopping rows and rating faces already use. */
.chip.on,
.chip:has(input:checked) {
  border-color: var(--ink); color: var(--ink); background: var(--paper2);
}
/* A chip whose box is UNchecked must lose the server-rendered `.on` look, or
   un-ticking someone leaves the chip still lit. */
.chip.on:has(input:not(:checked)) {
  border-color: var(--rule); color: var(--ink-2); background: transparent;
}
.chip:active { transform: var(--press); }

/* ---------------------------------------------------------------- THE FLOW
   The week has four stages: pick, shop, cook, review. This is a LIST, not a
   progress bar: no bar, no percentage, no "2 of 4". Those imply a pace you are
   failing to keep, and the design brief bans them for that reason. It answers three
   questions only: what is done, what is now, what is next.

   The one live stage gets the accent and the only button on the card. Finished
   stages carry a tick, which is a SHAPE, so it survives greyscale, a mono
   laser and colour blindness the way a green dot would not. */
.flow { list-style: none; margin: 0; padding: 0; }
.flow li {
  display: grid;
  grid-template-columns: 34px 1fr;
  column-gap: var(--s2);
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
}
.flow li:last-child { border-bottom: 0; }
.flow .mark {
  grid-row: 1 / span 3; align-self: start; justify-self: center;
  width: 30px; height: 30px; margin-top: 2px;
  display: grid; place-items: center;
  border: 2px solid var(--rule); border-radius: 999px;
  font: 700 15px/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink-2);
}
.flow li.done .mark {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}
.flow li.done .mark::after { content: "\2713"; }
.flow li.done .mark .n { display: none; }
.flow li.now .mark { border-color: var(--accent); color: var(--accent); border-width: 3px; }

.flow .what { font: 700 var(--fs-item)/1.25 var(--font); color: var(--ink-2); }
.flow li.now .what { color: var(--ink); }
.flow .why { margin: 2px 0 0; font-size: var(--fs-sub); color: var(--ink-2); }
.flow li.done .why { color: var(--quiet); }
/* Only the live stage gets a button, so "what do I do now" has exactly one answer. */
.flow .go-now { margin-top: var(--s2); }

/* A SECONDARY action. `.btn-quiet` is full width and 88px tall, which is right
   for "the other main choice" and wrong for "the small get-out beside it": at
   full width it competes with the primary action and, on Tonight, the thing that
   marks a whole week's shopping done looked more important than the thing that
   opens the list. */
.btn-small {
  display: inline-grid; place-items: center;
  width: auto; min-height: 48px;
  padding: var(--s1) 18px;
  background: transparent; color: var(--ink-2);
  border: 2px solid var(--rule); border-radius: var(--radius-btn);
  font: 600 var(--fs-sub)/1.2 var(--font);
  cursor: pointer; touch-action: manipulation;
}
.btn-small:active { background: var(--paper2); transform: var(--press); }

/* ONE control, not three loose boxes. A minus, a number and a plus sitting apart
   read as three unrelated buttons; joined inside a single border they read as a
   quantity you are adjusting, which is what it is. */
.stepper {
  display: inline-flex; align-items: stretch;
  border: 2px solid var(--rule); border-radius: var(--radius-btn);
  overflow: hidden;
}
.stepper button {
  min-width: 52px; min-height: 48px; padding: 0 14px;
  background: transparent; color: var(--ink);
  font: 700 var(--fs-item)/1 var(--font);
  cursor: pointer; touch-action: manipulation;
}
.stepper button:active { background: var(--paper2); }
.stepper button[disabled] { color: var(--rule); cursor: default; }
.stepper .n {
  display: grid; place-items: center;
  min-width: 56px; padding: 0 6px;
  border-inline: 2px solid var(--rule);
  font: 700 var(--fs-item)/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}

@media print {
  .page-break { break-before: page; }
}

/* ---------------------------------------------------------------------------
   THE NO-SCROLL PROMISE, enforced rather than hoped for.

   Measured on a 375x812 phone the step grid came out at 847px, so the page
   scrolled by 35px. One step per screen was aspirational, not true.

   min-height alone cannot prevent that: a grid row grows to its content. So the
   box is CAPPED at the viewport and the body row is allowed to shrink below its
   content (min-height:0 is required for that in a grid) and scroll if it must.
   The chrome yields. The type never does.
   --------------------------------------------------------------------------- */
.step {
  max-height: 100svh;
  overflow: hidden;
}
.step-body {
  min-height: 0;          /* without this a grid item refuses to shrink */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* The pinned timer bar sits above the advance band, so the step needs to clear
   both or the next-step peek renders underneath it. */
body.timing .step,
.step:has(.timerbar) { padding-bottom: calc(var(--tap) + 72px); }

/* The supporting detail under a step headline. Two thirds the headline size, full
   ink, wider measure. It carries the doneness cues and the safety detail the recipe
   verifiers added, so it is never truncated and never hidden behind a tap. */
.step-detail {
  margin: var(--s3) 0 0; max-width: 30ch;
  font: 400 var(--fs-name)/1.32 var(--font);
  color: var(--ink-2);
  text-wrap: pretty; hyphens: none;
}
.step-detail .qty { font-weight: 700; color: var(--accent); white-space: nowrap; }

/* ============================================================================
   THE DESKTOP READING SCALE. Added 29 Jul 2026, after Daniel said "the font is
   all out of place".

   He was right, and it was measurable. On a 1280px laptop the planning screens
   were rendering a 64px page heading, 30px card titles and 28px body copy,
   directly above a 13px nav label and 16px stamps. Not uniformly large:
   MISMATCHED, by a factor of nearly five between two things a centimetre apart.

   The cause was that this stylesheet only ever had two reading contexts, both of
   them kitchen ones: a phone held while cooking, and a tablet propped on the
   bench. Both are read at arm's length with your hands busy, and both correctly
   scale type UP. A laptop was silently inheriting the tablet block, so the
   screens you plan on (Week, Shopping, Meals, Pins) got the largest
   type in the entire system, which is exactly backwards: those are read sitting
   down, close, with a mouse, and they are dense with detail.

   So this block is not "make it smaller". It is the third reading context the
   design always needed and never had.

   COOK MODE IS DELIBERATELY EXCLUDED (`body:not(.cooking)`). Its 44-64px step
   text is not decoration, it is the whole promise of the screen: readable from
   across the kitchen, and the original sizes trace to a real ISO 15008 glance-
   reading calculation documented at the top of this file. Shrinking it here
   would quietly undo the one thing that was measured properly.

   Tokens are set on `body` rather than `:root` so the cooking exclusion works:
   custom properties inherit, so everything inside the page picks these up.
   ========================================================================== */
@media (min-width: 1024px) {
  body:not(.cooking) {
    --pad: 28px;
    --tap: 64px;              /* the advance band, mouse-sized not thumb-sized */

    --fs-dish:    2.125rem;   /* 34  was 64 */
    --fs-strap:   1.125rem;   /* 18  was 28 */
    --fs-readout: 2.5rem;     /* 40  the start-by clock stays a hero number */
    --fs-advance: 1.125rem;   /* 18  was 32 */
    --fs-qty:     1.25rem;    /* 20  was 44 */
    --fs-name:    1.0625rem;  /* 17  was 38 */
    --fs-note:    1rem;       /* 16  was 28. THIS is the body-copy fix */
    --fs-peek:    1rem;       /* 16 */
    --fs-label:   0.8125rem;  /* 13  now MATCHES the nav label it sits beside */
    --fs-buy:     1.125rem;   /* 18  was 32 */
    --fs-item:    1.1875rem;  /* 19  was 30 */
    --fs-sub:     0.875rem;   /* 14 */
  }

  /* Touch targets are a phone concern. At 88px tall with 16px text a button is
     mostly empty space, which reads as unfinished rather than generous. These
     stay well above the 24px WCAG 2.2 minimum for a pointer. */
  body:not(.cooking) .btn-quiet,
  body:not(.cooking) .btn-go { min-height: 52px; }
  body:not(.cooking) select,
  body:not(.cooking) input[type=text],
  body:not(.cooking) input[type=time] { min-height: 44px; }
  body:not(.cooking) .sitem { min-height: 56px; }
  body:not(.cooking) .ing   { min-height: 52px; }
  body:not(.cooking) .grab li,
  body:not(.cooking) .check { min-height: 44px; }
  body:not(.cooking) .mealcard { min-height: 64px; }

  /* The tick boxes were sized for a thumb too. */
  body:not(.cooking) .ing-box,
  body:not(.cooking) .sitem .box { width: 28px; height: 28px; border-width: 2px; }
  body:not(.cooking) .ing,
  body:not(.cooking) .sitem { grid-template-columns: 1fr 2rem; }

  /* A 44px emoji face is a phone-sized target; 56 is plenty with a mouse. */
  body:not(.cooking) .faces label { min-height: 56px; font-size: 30px; }
  body:not(.cooking) .rate-row label { width: 56px; height: 56px; font-size: 28px; }
  body:not(.cooking) .stars { font-size: 28px; }
}

/* ---------------------------------------------------------------- LOGIN
   The only screen a stranger can reach, and the first thing Patricia meets on
   a new phone. Vertically centred and deliberately sparse: name, box, button.
   Same tokens as everywhere else, so it does not read as a bolted-on gate. */
.wrap.login {
  min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  max-width: 420px;
}
.wrap.login .dish { margin-bottom: 0; }
.wrap.login form { margin-top: var(--s5); display: grid; gap: var(--s3); }

/* Font-size must be >= 16px or iOS Safari zooms the whole page on focus, which
   on a phone looks like the layout breaking the moment you tap the box. */
.pw {
  width: 100%; min-height: 64px;
  padding: var(--s2) var(--s3);
  background: var(--paper2); color: var(--ink);
  border: 2px solid var(--rule); border-radius: var(--radius-box);
  font: 500 var(--fs-item)/1.2 var(--font);
}
.pw:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Never red-on-paper alarm styling: a mistyped password is an ordinary event,
   not a fault. Same weight as any other note on the system. */
.loginerr {
  margin: 0; color: var(--ink-2);
  font: 600 var(--fs-note)/1.35 var(--font);
}

/* ============================================================================
   THE WEEK SHEET  (/print/week)
   ----------------------------------------------------------------------------
   A cooking document, not an app screen. Read standing up, hands busy, glancing
   between bench and paper, so the whole design serves one job: getting the eye
   back to the right spot fast after looking away.

   Three decisions carry it:

   1. ONE COLUMN FOR QUANTITIES. Every amount sits right-aligned on the same
      vertical line, in tabular figures, bolder than the name beside it. Hunting
      for "how much" becomes travelling down a single edge instead of reading
      each row. This is why the ingredient list is a two-part row and not prose.

   2. STEP NUMBERS ARE A SHAPE, NOT A CHARACTER. A filled square with a knocked
      out numeral is findable in peripheral vision; a bare "3." in the text
      column is not. Same reasoning (and the same ink trick) as `.grab li b`:
      a numeral survives a mono laser, poor light and colour blindness where a
      colour code would not.

   3. GROUPS COME FROM THE FOOD, NOT FROM STORAGE. The previous sheet grouped by
      bag_group, which was built for packing Sunday bags and split 18 ingredients
      across six headings of one or two lines. Bag prep is gone; the groups now
      are What you need / To serve / From the cupboard, which are answers to
      questions a cook asks mid-recipe.
   ========================================================================== */

.sheet { max-width: 900px; }
.sheetbar { margin: 0 0 var(--s4); font-size: var(--fs-note); color: var(--ink-2); }

.sheet-head { margin-bottom: var(--s4); }
.sheet-title { margin: 0; font: 700 clamp(30px, 5vw, 44px)/1.05 var(--font); letter-spacing: -.02em; }
.sheet-sub { margin: var(--s1) 0 0; color: var(--ink-2); font-size: var(--fs-note); }

/* ---------------------------------------------------------------- page 1 */
.weekgrid { width: 100%; border-collapse: collapse; }
.weekgrid td { padding: var(--s3) 0; border-bottom: 1px solid var(--rule); vertical-align: baseline; }
.wg-day {
  width: 4.5em; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; font-size: var(--fs-note);
}
.wg-dish { font-weight: 500; }
.wg-when { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums lining-nums; }
.wg-when b { font-weight: 700; }
.wg-when span { display: block; color: var(--ink-2); font-size: var(--fs-note); }

/* ------------------------------------------------------------ recipe card */
.recipe { margin-top: var(--s6); }
.r-head { border-bottom: 2px solid var(--ink); padding-bottom: var(--s3); margin-bottom: var(--s4); }
.r-day {
  margin: 0 0 var(--s1); font-weight: 700; text-transform: uppercase;
  letter-spacing: .12em; font-size: var(--fs-note); color: var(--accent);
}
.r-title { margin: 0; font: 700 clamp(24px, 3.6vw, 34px)/1.1 var(--font); letter-spacing: -.015em; }
/* Facts on one line, separated by rules rather than punctuation: a "·" between
   values reads as part of the sentence, a rule reads as a boundary. */
.r-meta { display: flex; flex-wrap: wrap; gap: var(--s3); margin: var(--s2) 0 0;
          font-size: var(--fs-note); color: var(--ink-2); }
.r-meta span + span { border-left: 1px solid var(--rule); padding-left: var(--s3); }
.r-meta b { color: var(--ink); font-variant-numeric: tabular-nums lining-nums; }

.r-band {
  margin: var(--s5) 0 var(--s2); padding-bottom: 4px;
  border-bottom: 1px solid var(--ink);
  font: 700 var(--fs-note)/1 var(--font);
  text-transform: uppercase; letter-spacing: .1em;
}
.r-band:first-child { margin-top: 0; }
/* Secondary sections stay legible but stop competing with the main list. */
.r-band.quiet { border-bottom-color: var(--rule); color: var(--ink-2); }

/* ------------------------------------------------------------ ingredients */
.ings { list-style: none; margin: 0; padding: 0; }
.ings li {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: 7px 0; border-bottom: 1px solid var(--rule);
  break-inside: avoid; page-break-inside: avoid;
}
.i-name { flex: 1 1 auto; }
/* The prep note is the same information the app shows everywhere else, one step
   down in emphasis so the ingredient NAME stays the scannable thing. */
.i-name i { display: block; font-style: normal; color: var(--ink-2); font-size: var(--fs-note); }
.i-qty {
  flex: 0 0 auto; text-align: right; font-weight: 700;
  font-variant-numeric: tabular-nums lining-nums; white-space: nowrap;
}

/* ------------------------------------------------------------------ steps */
.steps { list-style: none; margin: 0; padding: 0; counter-reset: none; }
.steps li {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3) 0; border-bottom: 1px solid var(--rule);
  break-inside: avoid; page-break-inside: avoid;
}
.steps li:last-child { border-bottom: 0; }
.s-no {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 2em; height: 2em; border-radius: var(--radius-box);
  background: var(--ink); color: var(--paper);
  font: 700 var(--fs-note)/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}
.s-body { flex: 1 1 auto; }
.s-text { margin: 0; line-height: 1.5; }
.s-time {
  margin: 4px 0 0; color: var(--ink-2); font-size: var(--fs-note);
  font-variant-numeric: tabular-nums lining-nums;
}

/* ----------------------------------------------------------------- footer */
.r-foot { margin-top: var(--s4); padding-top: var(--s3); border-top: 1px solid var(--rule); }
.r-note { margin: 0 0 var(--s1); font-size: var(--fs-note); color: var(--ink-2); line-height: 1.45; }
.r-note b { color: var(--ink); }

/* Two columns only where there is genuinely room. Ingredients and steps side by
   side on a wide screen, stacked on a phone, because a 40-character step column
   is worse than scrolling. */
/* Steps sit BELOW the ingredients whenever the two are stacked (print, and any
   screen under 900px). `.r-band:first-child { margin-top: 0 }` zeroes the STEPS
   heading's own top margin because it is the first child of its section, so
   without this the heading lands hard against the last ingredient row. Only the
   side-by-side layout, where they are not stacked, puts it back to zero. */
.r-method { margin-top: var(--s5); }

@media screen and (min-width: 900px) {
  .r-cols { display: grid; grid-template-columns: 1fr 1.35fr; gap: var(--s6); align-items: start; }
  .r-method { margin-top: 0; }
}

@media print {
  /* Explicit pt, NOT the --fs-* tokens: those scale with --ui-scale for reading
     a phone at arm's length across a kitchen, which is the opposite of what a
     sheet held at 40 cm needs. */
  .sheet { max-width: none; }
  .sheet-title { font-size: 30pt; }
  .weekgrid td { padding: 3mm 0; }

  /* Each recipe owns its page and its heading never orphans from its list. */
  .recipe { margin-top: 0; break-inside: auto; }
  .r-head { break-after: avoid; page-break-after: avoid; }
  .r-band { break-after: avoid; page-break-after: avoid; font-size: 9.5pt; }
  .r-title { font-size: 22pt; }
  .r-day  { font-size: 9pt; color: #000; }
  .r-meta { font-size: 10pt; color: #000; }

  /* Ingredients in two columns: a 16 line list down one edge of A4 pushes the
     steps onto a second sheet, and the whole point is one dish, one page. */
  .r-cols { display: block; }
  .r-need { columns: 2; column-gap: 10mm; margin-bottom: 4mm; }
  .r-method { margin-top: 8mm; }
  .r-need .r-band { column-span: all; }
  .ings li { padding: 1.6mm 0; font-size: 10.5pt; border-bottom: .15mm solid #999; }
  .i-name i { font-size: 9pt; }

  .steps li { padding: 2.6mm 0; border-bottom: .15mm solid #999; }
  .s-text { font-size: 11.5pt; line-height: 1.45; }
  .s-time { font-size: 9pt; }
  .s-no {
    width: 7mm; height: 7mm; font-size: 10pt;
    background: #000 !important; color: #fff !important;
  }
  .r-note { font-size: 9.5pt; color: #000; }
  .sheetbar { display: none; }
}
