@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wdth,wght@12..96,75..100,400..800&family=Newsreader:ital,opsz,wght@0,6..72,300..600;1,6..72,300..500&display=swap');

/* ═══════════════════════════════════════════════════════════════════
   TERRI — v3

   Two registers, and they must not look alike.

   MOMENTS  (home, the answer, the score) — dark, full-bleed, poster-like.
            Scale and colour live here. You LOOK at these.
   READING  (the pour, a module, a saved telling) — warm paper, serif,
            generous measure. You READ these.

   v2 had four surfaces all above Y 0.77, so nothing could recede and nothing
   could advance: every screen was one flat plane of pale green. The range
   below runs Y 0.009 to 0.927 and that range IS the design.

   Bans, learned the hard way:
   - No tracked-uppercase section labels. THE VERDICT / RIGHT NOW / LAST IN THE
     GLASS was the single most recognisable "internal tool" tell in the old
     build. `.eyebrow` is gone from this file entirely — with zero uses left,
     the ban is enforceable by grep instead of by good intentions.
   - No `grow` on a hero. Heroes size to their content. Stretching one box to
     absorb the slack produced a giant empty rectangle, not a full screen.
   - No card inside a card.
   - The accent means ONE thing. `--good` is the Mark and map coverage, which
     never share a screen. It is not selection, not progress, not "tap this".
   - One type scale: 11 / 13 / 15 / 17 / 21 / 27 / 34. Nothing sets a size that
     is not on it, here or in views.js.
   ═══════════════════════════════════════════════════════════════════ */

:root{
  /* ground — dark, warm, wine-adjacent without being purple */
  --ink:#1F1518;          /* Y .009  the app */
  --ink2:#2C1F23;         /* Y .016  raised */
  --ink3:#3A292E;         /* Y .026  pressed, hairline fills */

  /* reading */
  --paper:#FBF6EF;        /* Y .927  sustained reading only */
  --paperInk:#241A1C;
  /* The paper register's floor, and it gets the SAME treatment as the dark
     one below. #8A7873 measured 3.89:1 on --paper — worse than the 4.39:1 that
     was condemned and replaced on the dark ground, and left behind because only
     half of `.t3` was looked at. It is not a buried preference any more: the
     register control is now the first thing on Settings, one tap, and it repaints
     the subtitle under every row on all seven reading routes. */
  --paperFaint:#776359;   /* 5.26:1 on --paper — measured, not asserted */

  /* type on dark — measured against --ink */
  --cream:#F6EDE3;        /* 15.4:1 */
  --creamDim:#C4B0A6;     /*  8.6:1 */
  --creamFaint:#9A857C;   /* 5.11:1 — the floor, measured, never go below.
                             The old #8E7A72 claimed 4.6:1 in this comment and
                             actually measured 4.39:1, i.e. it failed AA on all
                             53 uses of `.t3` — the subtitle under every row in
                             the app, at 13px, in a dim restaurant. */

  /* the Mark */
  --good:#C9DC8B;         /* 11.96:1 */
  --steep:#E5A945;        /*  8.56:1 */
  --skip:#E0703F;         /*  5.56:1 */

  --line:rgba(246,237,227,.14);
  --lineStrong:rgba(246,237,227,.28);
  /* The lit top edge of a raised surface. `--ink2` measures 1.13:1 against
     `--ink` — a null channel at 40% brightness — so a panel needs an EDGE to
     read as one object rather than as a slightly different nothing. */
  --edge:rgba(246,237,227,.10);
  --radius:20px; --radiusLg:28px;
  --safeT:env(safe-area-inset-top,0px); --safeB:env(safe-area-inset-bottom,0px);
}

*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overscroll-behavior:none;background:var(--ink)}
body{
  color:var(--cream);
  font-family:'Bricolage Grotesque',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing:antialiased;
}
button{font:inherit;color:inherit;background:none;border:none;cursor:pointer;text-align:inherit}
input,textarea{font:inherit;color:inherit}
::selection{background:var(--creamDim);color:var(--ink)}

/* ── shells ─────────────────────────────────────────────────────────
   THE COLUMN IS THE WHOLE LAYOUT, and every child of it is a real flex item.
   Nothing floats on top of the scroller any more: `.top`, `.scroll`, `.foot`
   and `.tabs` are four rows of one column, in that order, and the scroller gets
   exactly the height the other three do not take.

   `height:100dvh` is the iOS hardening. `inset:0` on a fixed element resolves
   against the LARGE viewport, and because the body never scrolls Safari never
   retracts its toolbars — so the box was permanently taller than what you can
   actually see, and the bottom of it was behind the browser chrome. */
.screen{position:fixed;inset:0;height:100dvh;max-width:560px;margin-inline:auto;
  display:flex;flex-direction:column;background:var(--ink)}
.screen.paper{background:var(--paper);color:var(--paperInk)}

/* Opaque and above the scroller. Zero bottom padding on an identical surface
   put the clip line 0px under the header's descender, so the first row of every
   list was sheared through the middle of its letters and prose scrolled
   UNDERNEATH the title. No border-bottom: a hairline under every screen title
   is the internal-tool tell this file's own header bans. */
.top{flex:0 0 auto;padding:calc(10px + var(--safeT)) 20px 12px;
  background:var(--ink);position:relative;z-index:2}
.screen.paper .top{background:var(--paper)}

/* `flex:1 1 0` — BASIS ZERO, and the zero is the whole point.
   With `flex-basis:auto` the scroller's basis is its FULL content height, so on
   any screen with something to scroll the column runs a deficit of thousands of
   pixels, and a flex container settles a deficit across every shrinkable item
   in proportion to its basis. The foot was shrinkable, so it took a share: on
   `card` at 375×812 it held 119px of buttons in a 79px box and "Photograph
   something else" rendered past the bottom of the phone, unreachable — the
   defect this pass set out to remove, relocated from the tab bar to the foot.
   At basis 0 the scroller carries the deficit alone: the foot keeps its natural
   height whenever there is free space, and the floor below still stops the
   scroller collapsing to nothing. */
.scroll{flex:1 1 0;min-height:120px;overflow-y:auto;overflow-x:hidden;
  overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:0 20px}
.scroll.flush{padding:0}

/* The scroll column's own edges, drawn.
   iOS has no persistent scrollbar and two identical dark surfaces meeting at a
   hard line is not an edge, it is nothing — which is how half of Today sat
   below a fold that nobody could see was there. At 0px both ends the gradient
   is fully opaque, so a screen with nothing to scroll looks exactly as it did.
   app.js sets `data-more` on scroll, one dataset write behind a change check. */
.scroll{--fadeT:0px;--fadeB:0px;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fadeT),
     #000 calc(100% - var(--fadeB)),transparent 100%);
          mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fadeT),
     #000 calc(100% - var(--fadeB)),transparent 100%)}
.scroll[data-more~="up"]{--fadeT:20px}
.scroll[data-more~="down"]{--fadeB:28px}

/* NEVER SHRINKS. The foot is content-sized and its children are `flex:none`, so
   crushing the box does not reflow them — it just renders them past the bottom
   edge of a box that does not scroll, which is content you cannot reach at all.
   The 188×406 case that once argued for a shrinkable foot is handled properly
   by the `max-height:600px` escape at the end of this file, where the shell
   stops being a viewport-locked column and becomes a document. */
.foot{flex:0 0 auto;padding:12px 20px calc(12px + var(--safeB));
  background:var(--ink);position:relative;z-index:2}
.screen.paper .foot{background:var(--paper)}
.stack{display:flex;flex-direction:column}
.gap-s>*+*{margin-top:10px} .gap>*+*{margin-top:18px} .gap-l>*+*{margin-top:30px}
.center{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:16px;flex:1}
.spacer{flex:1;min-height:12px}

/* ── type ─────────────────────────────────────────────────────────
   ONE SCALE: 11 / 13 / 15 / 17 / 21 / 27 / 34. Nothing in this file, and
   nothing in views.js, sets a size that is not on it.

   The build had eighteen sizes, twelve of them between 10.5 and 20px — an
   average step of 1.06, below the just-noticeable difference. `.t1` 16, `.opt`
   16 and `.btn` 16.5 were three body sizes nobody could tell apart and which
   could therefore never optically align. Each was defensible alone; none had
   ever been compared side by side, which is exactly what "AI-slop" looks like
   from six inches away.

   `overflow-wrap:break-word` is not cosmetic. A 42-character German token
   ("Weissburgunderspaetlesetrockenbeerenauslese") produced scrollWidth 679 in a
   335px box, and since `.scroll` sets overflow-y the x-axis computed to auto —
   the entire screen went horizontally scrollable. This is a wine app; Alsace,
   Austria and Germany are not edge cases. `break-word` and not `anywhere`, so
   the line-breaker still prefers a space and only splits a token that genuinely
   cannot fit. */
.d1{font:800 clamp(30px,8.6vw,34px)/1.04 'Bricolage Grotesque',sans-serif;letter-spacing:-.032em;
  font-variation-settings:'wdth' 92,'opsz' 48}
.d2{font:800 27px/1.12 'Bricolage Grotesque',sans-serif;letter-spacing:-.026em;font-variation-settings:'wdth' 94}
.d3{font:700 21px/1.24 'Bricolage Grotesque',sans-serif;letter-spacing:-.018em}
.d1,.d2,.d3{overflow-wrap:break-word}
.t1{font:600 17px/1.45 'Bricolage Grotesque',sans-serif}
.t2{font:400 15px/1.5 'Bricolage Grotesque',sans-serif;color:var(--creamDim)}
.t3{font:500 13px/1.4 'Bricolage Grotesque',sans-serif;color:var(--creamFaint)}
.num{font-variant-numeric:tabular-nums;font-variation-settings:'wdth' 92}

/* Reading. A different face on purpose — you are not scanning, you are reading. */
.read{font:400 17px/1.62 'Newsreader',Georgia,serif;letter-spacing:.002em;font-variation-settings:'opsz' 20}
.read p+p{margin-top:.85em}
.read-lead{font:400 21px/1.5 'Newsreader',Georgia,serif;font-variation-settings:'opsz' 32}
.paper .t2{color:#6B5A55} .paper .t3{color:var(--paperFaint)}

/* ── the Mark — a struck seal, not a numeral in a box ───────────── */
.seal{position:relative;width:100%;aspect-ratio:1;max-width:min(74vw,300px);margin:0 auto;
  display:flex;flex-direction:column;align-items:center;justify-content:center}
.seal svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
.seal .val{font:800 clamp(78px,22vw,104px)/1 'Bricolage Grotesque',sans-serif;letter-spacing:-.06em;
  font-variant-numeric:tabular-nums;font-variation-settings:'wdth' 90,'opsz' 96}
.seal .band{font:700 12.5px/1 'Bricolage Grotesque',sans-serif;letter-spacing:.16em;
  text-transform:uppercase;margin-top:10px}
.seal.good .val,.seal.good .band{color:var(--good)}
.seal.steep .val,.seal.steep .band{color:var(--steep)}
.seal.skip .val,.seal.skip .band{color:var(--skip)}
.seal.none .val,.seal.none .band{color:var(--creamFaint)}
.seal .ring{fill:none;stroke-width:3;stroke-linecap:round}
.seal .ring.track{stroke:var(--line)}
.seal.good .ring.fill{stroke:var(--good)} .seal.steep .ring.fill{stroke:var(--steep)}
.seal.skip .ring.fill{stroke:var(--skip)} .seal.none .ring.fill{stroke:transparent}

/* The same score, small, in a list — and NOT A BADGE any more. Spec §8: the
   Mark is never the hero of a screen again. Boxed, filled, right-aligned and
   set at weight 800, it was the only saturated object in a row and the eye
   landed on the number before it landed on the wine's name. The colour channel
   IS the verdict and it survives; the box that made it shout does not. */
.chip-score{display:inline-flex;align-items:baseline;justify-content:flex-end;
  min-width:30px;height:auto;padding:0;flex:none;background:none;border-radius:0;
  font:600 15px/1 'Bricolage Grotesque',sans-serif;font-variant-numeric:tabular-nums;
  color:var(--creamFaint)}
.chip-score.good{background:none;color:var(--good)}
.chip-score.steep{background:none;color:var(--steep)}
.chip-score.skip{background:none;color:var(--skip)}

/* ── the photograph, used as the asset it is ────────────────────── */
.shot{position:relative;width:100%;border-radius:var(--radiusLg);overflow:hidden;background:var(--ink2)}
.shot img{width:100%;height:100%;object-fit:cover;display:block}
.shot.dim img{opacity:.32}
.shot .over{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;
  padding:20px;background:linear-gradient(to top,rgba(31,21,24,.92) 12%,rgba(31,21,24,.25) 62%,rgba(31,21,24,.5))}
.thumb{width:52px;height:52px;border-radius:12px;object-fit:cover;flex:none;background:var(--ink2)}

/* ── surfaces ─────────────────────────────────────────────────────
   THE PANEL NEEDED AN EDGE. Its fill measures 1.13:1 against the ground, which
   is a null channel at 40% screen brightness — so "these things belong
   together" could not be said at all, and the codebase used `.panel` exactly
   once in 3,456 lines and reached for 39 hairlines instead. Every screen could
   stack and none could group, which is simultaneously the "cluttered" complaint
   and the "too basic and cold" one. A lit top edge and a dropped shadow give
   the same fill a border, a light source and a position in space. */
.panel{background:var(--ink2);border-radius:var(--radius);padding:18px;
  border:1px solid var(--line);box-shadow:inset 0 1px 0 var(--edge),0 1px 2px rgba(0,0,0,.5)}
.panel.flat{background:none;border:1px solid var(--line)}
.rule{height:1px;background:var(--line);flex:none}
.row{display:flex;align-items:center;gap:12px}
.between{display:flex;align-items:center;justify-content:space-between;gap:12px}

/* ── controls ───────────────────────────────────────────────────── */
.btn{display:block;width:100%;text-align:center;border-radius:999px;padding:17px 0;flex:none;
  font:700 17px/1 'Bricolage Grotesque',sans-serif;background:var(--cream);color:var(--ink);
  box-shadow:0 1px 2px rgba(0,0,0,.5);
  transition:transform .12s ease,opacity .12s}
.btn:active{box-shadow:none;transform:scale(.976)}
.btn[disabled]{opacity:.4}
.btn.quiet{background:none;color:var(--cream);border:1px solid var(--lineStrong)}
.paper .btn{background:var(--paperInk);color:var(--paper)}
.paper .btn.quiet{background:none;color:var(--paperInk);border-color:rgba(36,26,28,.25)}

/* THE ONE BRIGHT THING. Cream and not `--good`: in a dim room luminance is the
   only channel that survives, so the primary action is the brightest large
   object on the screen and nothing else is allowed to be. It also frees green
   to mean the Mark and only the Mark. */
/* 17px, not 18. The scale at the top of this file is a rule and this was the
   one line breaking it — an off-scale size on the app's most-used button, which
   is exactly where an inconsistency is most visible and least defensible. */
.btn.shoot{display:flex;align-items:center;justify-content:center;gap:10px;padding:22px 0;font-size:17px}
.btn.shoot svg{width:20px;height:20px;stroke:currentColor;fill:none;
  stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}

/* 44px, because 37px was the measured height of every back link, "How it
   works", "See the whole map" and "Erase everything I have saved".
   The underline goes: an underlined inline text link is a hyperlink, no iOS app
   has them, and it is a direct cause of "this doesn't look like a normal app".
   The <u> stays in the markup; this kills the line.
   `--cream` and not `--creamDim`, because in this palette brightness IS the
   affordance — a link has to be brighter than the prose around it, not the same
   weight in the same grey. `.danger` is declared later, in views.js's injected
   SHIM_CSS, so "Erase everything" is still red. */
.link{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:11px 0;
  font:600 15px/1 'Bricolage Grotesque',sans-serif;color:var(--cream)}
.link u{text-decoration:none;border-bottom:none;padding-bottom:0}
.paper .link{color:var(--paperInk)}

/* The way out of a screen, dragged to the physical edge so the thumb cannot
   miss it on the outside. */
.top .link:first-child{margin-left:-20px;padding-left:20px;padding-right:14px}

.tap{display:flex;align-items:center;gap:12px;width:100%;padding:14px 0;text-align:left}

/* DRAWN, NOT TYPESET. `›` (U+203A) in Bricolage at wdth 92 has a measured
   advance of 3.45px — against 7.14px for the same character in system-ui — so
   its ink landed exactly on the 20px content edge with no optical margin and
   read as a rendering artefact rather than as an affordance. Two borders and a
   rotation cost nothing and are the same shape at every size. `font-size:0`
   suppresses the text node the markup still carries. */
.chev{margin-left:auto;flex:none;align-self:center;
  width:8px;height:8px;margin-right:3px;
  border-right:1.6px solid var(--creamFaint);
  border-top:1.6px solid var(--creamFaint);
  transform:rotate(45deg);font-size:0}

.shutter{width:76px;height:76px;border-radius:50%;background:var(--cream);margin:0 auto;flex:none;
  box-shadow:0 0 0 4px var(--ink),0 0 0 6px var(--lineStrong);transition:transform .12s}
.shutter:active{transform:scale(.93)}

/* ── tab bar — a real one ─────────────────────────────────────────
   A MEMBER OF THE COLUMN, not a pane floating on it. `position:fixed` put a
   99.5px opaque band permanently on top of a scroller that still ran to the
   bottom of the phone: on Poured at scrollTop 260 a subtitle was sliced through
   and the row after it was erased outright, and `padding-bottom` on the
   scroller could only ever rescue the last row. The blur and the .92 alpha go
   with it — they existed to soften content passing underneath, and nothing
   passes underneath any more. Its height was also written in three places that
   had to agree; now it is written nowhere. */
.tabs{flex:0 0 auto;position:relative;display:flex;
  padding:6px 12px calc(6px + var(--safeB));
  background:var(--ink);border-top:1px solid var(--line)}
.screen.paper .tabs{background:var(--paper);border-top-color:rgba(36,26,28,.13)}
.tabs button{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 0;
  font:600 11px/1 'Bricolage Grotesque',sans-serif;color:var(--creamFaint);letter-spacing:0}
.tabs button.on{color:var(--cream)}
.tabs svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}

/* ── chips / choices ──────────────────────────────────────────────
   The chip was 34px tall, which is every recall grade — the interaction the app
   asks you to repeat twelve times in a sitting, one-handed, at a table. */
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{display:inline-flex;align-items:center;min-height:44px;border-radius:999px;padding:0 16px;
  font:600 15px/1 'Bricolage Grotesque',sans-serif;background:var(--ink2);color:var(--cream);
  transition:transform .12s}
.chip:active{transform:scale(.96)}
.chip.on{background:var(--cream);color:var(--ink)}
.paper .chip{background:rgba(36,26,28,.07);color:var(--paperInk)}
.paper .chip.on{background:var(--paperInk);color:var(--paper)}

.opt{display:block;width:100%;text-align:left;border-radius:16px;padding:16px 18px;background:var(--ink2);
  font:600 17px/1 'Bricolage Grotesque',sans-serif}
/* `--good` is a READING, not an affordance. It was simultaneously saying "the
   Mark is good value", "you can recall this", "this option is selected",
   "progress", "tap this" and "text is selected" — six meanings, three of them
   on Today at once. In a shipped app the accent means one thing and the user
   learns it in four seconds. Selection borrows the file's existing grammar for
   "this one, definitively": cream on ink. */
.opt.on{background:var(--cream);color:var(--ink);font-weight:800}
.paper .opt{background:rgba(36,26,28,.06);color:var(--paperInk)}

/* ── data ───────────────────────────────────────────────────────── */
.figs{display:flex;align-items:flex-end;gap:22px}
.fig-v{font:800 27px/1 'Bricolage Grotesque',sans-serif;letter-spacing:-.018em;font-variant-numeric:tabular-nums}
.fig-k{font:500 13px/1.3 'Bricolage Grotesque',sans-serif;color:var(--creamFaint);margin-top:5px}
.bar{height:5px;border-radius:3px;background:var(--ink3);overflow:hidden;flex:none}
/* Single-fill bars are neutral. Green on this one meant "progress", which is
   the fifth thing green was being asked to say. The map's two-tone bar keeps
   its green below, where green means coverage and only coverage. */
.bar i{display:block;height:100%;border-radius:3px;background:var(--creamDim)}
.sqs{display:grid;grid-template-columns:repeat(auto-fill,minmax(70px,1fr));gap:6px}
.sq{border-radius:11px;padding:9px 7px;min-height:48px;display:flex;align-items:center;justify-content:center;
  text-align:center;background:var(--ink2);color:var(--creamFaint);
  font:600 11px/1.25 'Bricolage Grotesque',sans-serif}
.sq.met{background:rgba(201,220,139,.15);color:var(--good);font-weight:800}

/* Three states, and the third one costs no new colour.
   Green stops meaning "you were there" and starts meaning "you can produce
   it" — so met steps DOWN into greys the file already owns, and green appears
   in exactly one state on the whole screen. No rung depends on hue, so the
   ladder holds in greyscale, at 30% brightness, and on a red-green eye.
     unlit  --ink2 tile, --creamFaint text   Y .0164   4.54:1   w600
     met    --ink3 tile, --creamDim  text   Y .0268   6.58:1   w600
     known  --good tile, --ink       text   Y .6547  11.96:1   w800
   The unlit figure read 3.90:1 until `--creamFaint` was raised from #8E7A72 to
   #9A857C to clear AA on `.t3`. Nobody came back and remeasured the squares, so
   this block described a token that no longer existed — the same species of
   quiet lie as a comment claiming a ratio it never had. Raising it took the
   bottom rung to 4.54:1, over the 4.5:1 line, which is a real improvement that
   was going unrecorded. Recompute this table whenever a token in it moves.
   BE HONEST ABOUT WHICH CHANNEL CARRIES WHICH STEP — measured in the browser,
   greyscaled and dimmed to 34%, not calculated on paper:
     known vs met    9.17:1 on the TILE. Survives anything. This is the read.
     met vs unlit    1.16:1 on the tile — a null channel. That step is carried
                     ENTIRELY by the text, --creamDim over --creamFaint, a 2.18x
                     luminance step and nothing else.
   That is deliberate, not an oversight: green is the count you are meant to
   take at a glance, and met-vs-unlit is a deliberate second look. But anyone
   who later restyles `.sq` must know the middle rung is standing on one leg —
   drop the text colours to a single value and two of the three states become
   the same square.
   The known declaration is the one `.opt.on` (line 183) already uses — this
   file's grammar for "this one, definitively". Nothing new to learn.
   Both rules are (0,3,0), so neither depends on source order against
   `.sq.met` (0,2,0). A known square ships BOTH classes: if `.sq.met.known`
   is ever lost, it falls back to met, never to grey. */
.sq.met:not(.known){background:var(--ink3);color:var(--creamDim);font-weight:600}
.sq.met.known{background:var(--good);color:var(--ink);font-weight:800}

/* The gap as a length, not as two numbers to subtract. Two absolute children
   in one track, both measured from the same left edge against the same total,
   so they cannot disagree and knownPct <= metPct holds by construction. The
   met segment is the neutral --creamFaint, never green, or green means two
   things again. The unclassed <i> inherits background:var(--good) from `.bar i`
   — the known fill is the rule that was already there.
   `position:relative` is load-bearing: without it the known segment positions
   against some other ancestor and lands elsewhere on the page while the met
   segment still renders correctly, so the bar looks plausible and reads wrong. */
.bar.duo{position:relative}
.bar.duo i{position:absolute;top:0;bottom:0;left:0}
/* The known fill, stated rather than inherited. It used to ride on `.bar i`'s
   green; that rule is now neutral, because "progress" was one of the six things
   green was saying. Coverage is the one place green survives besides the Mark,
   and the two never appear on the same screen — so this rung is written out in
   full here and the ladder above it is untouched. (0,3,1) beats `.bar i`. */
.bar.duo i:not(.met){background:var(--good)}
.bar.duo i.met{background:var(--creamFaint)}

/* ── pour: the swatches ─────────────────────────────────────────── */
.swatches{display:flex;gap:10px}
.swatch{flex:1;height:66px;border-radius:14px}
.swatch-k{font:600 11px/1.35 'Bricolage Grotesque',sans-serif;color:var(--creamFaint);margin-top:7px}
.paper .swatch-k{color:var(--paperFaint)}

/* ── the wait ───────────────────────────────────────────────────── */
.stage{display:flex;align-items:center;gap:12px;padding:11px 0}
.stage .dot{width:7px;height:7px;border-radius:50%;background:var(--ink3);flex:none}
.stage.on .dot{background:var(--cream)}
.stage.done .dot{background:var(--creamFaint)}
.stage .lbl{font:600 15px/1.25 'Bricolage Grotesque',sans-serif;color:var(--creamFaint)}
.stage.on .lbl{color:var(--cream)}
.pulse{display:inline-block;width:8px;height:1.05em;background:currentColor;vertical-align:-2px;
  margin-left:2px;animation:blink 1.05s steps(2,start) infinite;opacity:.6}
@keyframes blink{to{visibility:hidden}}
.spin{width:24px;height:24px;border-radius:50%;border:2.5px solid var(--line);
  border-top-color:var(--cream);animation:sp .8s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}

/* OPACITY ONLY. `.fade` used to animate `transform` on `.screen`, and a
   transformed element becomes the containing block for its `position:fixed`
   descendants — so on every single navigation the tab bar slid 8px off the
   bottom of the viewport and back, taking its home-indicator padding with it.
   Navigation chrome that moves is the next chapter of "it feels like I'm having
   a seizure". §3.3 removed the fixed positioning; this removes the movement. */
.fade{animation:fd .3s ease-out both}
@keyframes fd{from{opacity:0}to{opacity:1}}
.hidden{display:none !important}

/* ── the toast ──────────────────────────────────────────────────────
   THE APP'S ONLY ERROR CHANNEL, and it had no rule in either stylesheet. The
   core appends it as the last child of `.screen`, which is the flex column, so
   an unstyled toast became a FIFTH ROW: it took its height out of the tab bar
   and the foot and then rendered itself half off the bottom edge. "I couldn't
   save that — this phone's storage is full" was sheared through the middle of
   its letters at the screen edge.
   `position:absolute` takes it out of the column entirely — `.screen` is
   already a positioned box — so nothing above it moves when it appears.
   `pointer-events:none`: it is a message, never a target, and it must not eat
   the tap that lands under it in the 2.6s it is alive. */
.toast{position:absolute;left:20px;right:20px;bottom:calc(84px + var(--safeB));
  z-index:30;pointer-events:none;padding:14px 16px;border-radius:14px;
  background:var(--ink3);border:1px solid var(--lineStrong);
  box-shadow:0 4px 16px rgba(0,0,0,.5);
  font:600 15px/1.35 'Bricolage Grotesque',sans-serif;color:var(--cream)}
@media (prefers-reduced-motion:reduce){*{animation:none !important;transition:none !important}}

/* ── landscape, and a phone put flat on a table ────────────────────
   At 812×375 the fixed column gave `.scroll` 83px to hold 669px of content.
   Under 600px of height the shell stops being a viewport-locked column and
   becomes an ordinary document that scrolls — the one case where the page
   itself is allowed to move. */
@media (max-height:600px){
  html,body{height:auto;overflow-y:auto;overscroll-behavior:auto}
  /* `relative` and not `static`: it changes nothing about the flow of a block
     element, and it keeps the toast — the one absolutely positioned child —
     anchored to the column instead of to the page. */
  .screen{position:relative;height:auto;min-height:100dvh;display:block}
  .scroll{overflow:visible;min-height:0;flex:none;-webkit-mask-image:none;mask-image:none}
  .foot,.tabs{position:static}
}
