@keyframes wm-tap-hint{0%,100%{opacity:0}35%,65%{opacity:.9}}
/* deck-common.css -- auto-extracted 2026-09-04.
   Every rule here is byte-identical (whitespace/comments aside) and
   conflict-free across all 29 in-app + work-course decks: no deck
   redefines this exact selector anywhere else, so nothing loses a
   cascade fight by this file loading before each deck's own <style>.
   Built by _dev/tools/consolidate_css.py -- re-run it rather than
   hand-editing a rule here, since editing here changes it for all
   29 decks at once and a hand-edit would drift from the safety check.
*/

/* Light only, in every device colour scheme (owner, 2026-09-14). The dark-mode --stage override (#0E1216) that sat here until then is removed, not overridden. */
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
/* Hand-added, not extracted (2026-09-10) -- consolidate_css.py only pulls
   rules that already exist byte-identically in every deck, and ::selection
   existed in none: every engine's default blue text-selection highlight was
   showing, uncustomized, the one live blue this app never meant to keep.
   Most card copy is already immune (.pane{user-select:none} below), but
   nav labels, the More menu and prototype-mode text are not. var(--line) is
   the app's existing warm-ink wash (16%, already used for hairline borders)
   reused here rather than a new value. If consolidate_css.py is ever re-run,
   carry this rule forward -- it is correct to be identical everywhere. */
::selection{background:var(--line);color:var(--ink)}
html,body{margin:0;height:100%;overflow:hidden;background:var(--stage);color-scheme:light}
body{font-family:var(--sans);color:var(--ink);-webkit-font-smoothing:antialiased;touch-action:manipulation}
.mda-nav-item svg{width:24px;height:24px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.mda-nav-item:hover{opacity:.85;transition:opacity .15s ease}
.mda-nav-item:active,.mda-more-btn:active{opacity:.75}
.mda-nav-item:focus-visible,.mda-more-btn:focus-visible,.more-menu-item:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.mda-nav-item.mda-more{position:relative;padding:0}
.mda-more-btn{all:unset;touch-action:manipulation;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;width:100%;min-height:54px;padding:4px 8px;border-radius:10px;cursor:pointer;color:inherit;font:inherit}
.mda-more-btn[aria-expanded="true"]{color:var(--ink)}
.mda-more-btn[aria-expanded="true"] .nav-ico{border-color:var(--ink)}
.more-menu{position:absolute;bottom:calc(100% + 10px);right:-4px;min-width:176px;background:var(--paper-raised,#F2F2F2);border:1px solid var(--line,#BFBFBF);border-radius:14px;box-shadow:0 14px 34px rgba(0,0,0,.2);padding:6px;display:flex;flex-direction:column;gap:2px;z-index:70}
.more-menu[hidden]{display:none}
.more-menu-item{display:flex;align-items:center;gap:10px;min-height:44px;padding:10px 12px;border-radius:10px;text-decoration:none;color:var(--ink,#1A1A1A);font-family:var(--sans,inherit);font-size:16px;font-weight:600;line-height:1.3}
.more-menu-item:active{background:var(--line,#eee)}
.more-menu-item svg{width:18px;height:18px;flex-shrink:0;color:var(--soft,#404040);fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.more-menu-item:hover,.more-menu-item:focus-visible{background:var(--line,#eee)}
.card-shell{position:relative;width:auto;display:flex;flex-direction:column;align-items:center;gap:0}
.card.reset-pulse{animation:reset-pulse .5s ease}
@keyframes reset-pulse{0%{transform:scale(1)}40%{transform:scale(.965);opacity:.7}100%{transform:scale(1);opacity:1}}
.stagearea{position:relative;flex:1;min-height:0}
/* Row raised and its targets rebuilt, 2026-09-09 (owner: "the left and right
   are so small, and there's a possibility of misclicking that versus the home
   button in the navbar").

   The measurement was worse than the complaint: .navbtn was 18x18. The minimum
   is 44 -- under a fifth of the area -- sitting 20px above a 70px nav bar, which
   is exactly the geometry that produces a mis-tap. Both problems are the same
   problem, so both are fixed together:

     row     bottom 90 -> 86, height 18 -> 44, so it spans 573-617
     target  18x18 -> 44x44, the full row
     glyph   16 -> 20px, stroke 2.2 -> 2.6 (the owner asked for thicker)
     gutter  left/right 32 -> 15, which puts the 20px MARK on the 27px gutter,
             the same line the top chrome's mark takes

   Net effect: the marks rise 9px, the hit area grows 6x, and the gap to the nav
   bar is 16px of dead space instead of a 20px gap between two live targets.
   573 clears the 569 content floor, so nothing can steal a tap from copy. */
/* ======================================================================
   SKY PALETTE, 2026-09-12. The progress line moved from the bottom row to
   the top edge and became the sun, and the two pager arrows went with it.

   WHY THE TRACK IS GONE, and it is not a style choice. Gold and orange are
   LIGHT colours, so they need a dark ground, and a light card has none.
   Measured against the old track (--line rendered #D5D0C5): bright yellow
   1.06:1, golden 1.25:1, lamplight 1.46:1, deeper gold 1.80:1 -- all far
   under the 3:1 a graphic needs, and darkening the track makes it WORSE
   (1.02:1 as the fill passes through it, still only 2.28:1 at a 62% track).
   Against the CARD instead of a grey track, #D96B2C measured 3.05:1 on the
   CREAM card -- but on the sky it falls to 2.81:1, and to 2.44:1 against its
   own green track, so it was darkened. #C25A1E cleared the bar and read as
   orange-RED (hue 22); the owner's note was that red "looks like failure" on
   what is a progress indicator, which is exactly backwards as a signal.
   #B0730F is hue 37 -- amber, the sun -- and is the MOST yellow tone that
   still clears 3:1 (3.01 against the track, 3.50 against the sky's lightest
   band). Everything more yellow fails: #A8780A 2.99, #B8860B 2.48, #C08A1E
   2.32. That is not a taste ceiling, it is a physics one -- yellow is a light
   colour and this is a light card. If the line ever needs to be sunnier than
   this, the TRACK has to get darker, not the fill lighter.

   The line first went flush on the card's TOP EDGE, into the free 0 -> 44 band
   (close control starts at 44, eyebrow rested 59 -> 73.4, content at 98), which
   moved nothing. The owner then placed it on the old eyebrow line instead, so
   everything below it drops 31px -- see .eyebrow.eyebrow-top and .pane, which
   must move together with it. Re-measured after: 0 overflows across 21 decks.
   ====================================================================== */
/* .card-foot is the whole TOP ROW now: full width, 44 -> 88, the same band the
   close control occupies. It must stay full width because #navback is its child
   and is positioned against it -- inset the row and the arrow moves with it.
   pointer-events:none here is load-bearing, not tidiness: this box spans the
   card, and without it taps near x=20 and x=320 hit the row instead of reaching
   #stagearea. The arrow re-enables pointer events on itself. */
.card-foot{position:absolute;left:0;right:0;top:44px;bottom:auto;height:44px;z-index:10;pointer-events:none}
/* Inset to clear both 44px targets: back occupies 16->60, close 283->327, so 72
   each side leaves a 12px gap to each and is symmetric. top:20 centres the 4px
   line on 66 -- the same line the two controls centre on. */
.progress-bar{position:absolute;left:72px;right:72px;top:20px;bottom:auto;transform:none;width:auto;height:4px;display:block}
/* The track is back, as almost-transparent green (owner, 2026-09-12). It was
   dropped when the line was a full-bleed edge, where an unfilled remainder
   had nothing to sit in; between two controls it needs to read as a channel.
   Green at this alpha also gives the nav pill the RELATIVE the palette panel
   said it lacked -- four seats independently flagged that navy registers left
   the pill as the only green on screen. Now the track and the pill are the
   same green, top and bottom of the same card. */
.progress-bar-track{width:100%;height:4px;border-radius:999px;background:var(--nav-pill,rgba(188,212,174,.26));overflow:hidden}

/* THE TOP ROW, 2026-09-12 (owner: tracker "in line with < and X close").
   One row at the 44->88 band: back arrow left, the sun line between, close
   right. The FORWARD arrow stays retired -- that is the half of the owner's
   2026-09-11 instruction that survives ("removed the right and left arrow at
   the bottom ... people will tap"): tapping the right half advances, so
   forward needs no control, but BACK earns one because the left-half tap is
   the app's least discoverable gesture and this is the only way out of a
   mis-tap. Hidden rather than cut from 47 files, so it is one line to reverse,
   and #navnext stays in the DOM so every listener and every test that drives a
   deck by clicking it still works. */
.navbtn.navnext{display:none}
.navbtn.navback{top:0;bottom:auto;left:16px;color:var(--soft)}
/* The fill's motion -- consolidated 2026-09-11 (design-system pass). The 34 decks
   carried this rule themselves in two variants: .3s on the At Home, Switching Off
   and Everyday Awareness decks (bar driving excepted), .9s on At Work, First
   Breath, the Introduction and the four work courses -- so the same tap animated
   the bar at two speeds depending on which area the reader was in. No document or
   the spec sheet named a value; .9s is kept because it is what the newer decks and
   the reference card were built with, and because the app's other easing runs
   slow. One line to change if the owner prefers the quicker fill. */
.progress-bar-fill{height:100%;border-radius:999px;width:0%;transition:width .9s cubic-bezier(.4,0,.2,1)}
@media (prefers-reduced-motion: reduce){ .progress-bar-fill{transition:none} }
/* Choice cards align like every other card (owner, 2026-09-05, second pass):
   the rows follow the question directly and ALL the leftover space collects at
   the bottom. Two earlier versions are retired -- margin-top:auto on the heading
   (which pushed the question down the card) and margin-top:auto on .optlist
   (which left a weird empty band between the question and the rows). Nothing is
   pinned now; .pane-bottom-anchor is kept only as a marker on those cards. */


/* The line under a Reflection question that tells the reader nothing is being
   asked of them. Quieter than .body on purpose: it is a note about the card,
   not part of the reflection. --quiet is the de-emphasis token. */
/* The accordion the Choice card used to be was removed on 2026-09-12 with the
   card itself; its 15 rules were removed from this sheet on the same day, after
   a sweep found zero pages using them. Restoring it means restoring the card,
   not the CSS. History: _dev/history/. */
.reflect-note{margin:24px 0 0;font-size:var(--body-size-sm);line-height:1.45;color:var(--quiet);max-width:30em;text-align:left}
/* One top anchor for every card (owner, 2026-09-05). The pinned eyebrow sits
   at 16px; the first line of real content starts at the pane's 56px padding
   and nowhere else, so the heading lands on the same line from card to card.
   Without this the first element's own top margin (a .takeaway's 14px, an
   h2's 24px) moved that line by up to 24px between cards. */
/* 40px -> 10px (2026-09-08, owner: "only ten pixels gap between that eyebrow").
   This is the gap between the eyebrow's bottom edge -- which sits ON the content
   anchor at 71px, see .eyebrow.eyebrow-top above -- and the first line under it.
   40px put a lot of air there; the eyebrow and its line now read as one unit.
   Every card with a top eyebrow gains 30px of vertical room from this, so it can
   only relieve overflow, never cause it. */
/* 10px -> 0, 2026-09-09. This spacer existed because the eyebrow sat directly
   above the first line, ON the 71 anchor. The eyebrow has moved up into the
   header band and content starts at 88 regardless, so the gap is already in the
   padding -- keeping this too would push eyebrow'd cards 10px below the rest
   and break the one thing the anchor guarantees. */
.pane > .eyebrow-top + *{margin-top:0}
.pane > :first-child:not(.eyebrow-top){margin-top:0}

/* One exception to the top anchor (owner, 2026-09-05): a card whose entire
   content is a single italic line -- Reframe, Takeaway, the quote card -- centres
   in the card instead. Top-anchoring a lone sentence left two thirds of the card
   empty beneath it and read as a page that had failed to load. Anything with a
   heading, body copy, rows, art or the breath bloom keeps the 56px anchor.

   WARNING before you touch this selector: a centred flex column with
   overflow:hidden clips content at BOTH ends and scrollHeight never exceeds
   clientHeight, so the usual overflow check reports nothing wrong. That is how
   the Home quote card lost its first line unnoticed. To test a change here,
   sum the flow children's heights and compare against the padded box; do not
   trust scrollHeight. fitPaneVertical() is the safety net for deck panes only
   -- it forces flex-start inline the moment a pane overflows -- and hub pages
   have no such net. */
/* THE DRAWING NO LONGER OPTS A CARD OUT OF CENTRING, 2026-09-12. The two
   clauses that stood here -- :not(:has(.wm-illo)):not(:has(svg)) -- were
   written when no lone-line card carried a drawing. The drawings sweep then
   gave 17 Takeaway cards one, and every one of them fell out of this rule and
   top-anchored at 116.5 with 267-332px of empty card beneath it (measured
   across every card at the time, _dev/data/design_audit.json) -- the largest hole in the
   corpus, and precisely the "page that had failed to load" the rule above
   exists to prevent. A drawing over one italic line is still a small block in a
   big box. Verified from the same measurement that dropping the two clauses
   moves EXACTLY those 17 cards and nothing else: every other pane holding an
   svg also holds an h1, a heading or body copy and is excluded above. */
/* padding-top back to 129 on a CENTRED card, whatever it leads with. The
   116.5px anchor below exists to land a drawing's ink 16px under the eyebrow,
   which is a statement about where the TOP of the card is -- and a centred card
   has no top anchor to honour. Left in place it just moved the centre: measured
   live on dishes#4, the content centred at 305.3 instead of 311.5, a 46px lift
   where every other centred card had 40. Higher specificity than the 116.5 rule
   AND it wins on order too; both were checked in a browser, not assumed. */
.pane:has(.takeaway, .quote-text):not(:has(h1)):not(:has(h2)):not(:has(.body)):not(:has(.wm-breath)):not(:has(.wm-art)):not(:has(.wm-steps)){justify-content:center;padding-top:129px;--pane-lift-spacer:75px}

/* THE 40px LIFT, PUT BACK, 2026-09-12. "A centred card sits 40px ABOVE true
   centre" is the standing rule -- it is written in 20 deck files and is live on
   Home (hub-pages/home/index.html:255) -- but its mechanism, an 80px trailing
   spacer, had disappeared from the decks, so "centred" meant the middle of the
   PADDED box instead. With padding 129/134 that box's centre is 349 against the
   card's 351.5: a lift of 2.5px. Nobody decided that; it fell out of the anchor
   moving 98 -> 129.
   The arithmetic, so the next person can re-derive it rather than trust it: a
   shrinkable spacer S at the end of a centred column puts the content's centre
   at 129 + (440 - H - S)/2 + H/2 = 349 - S/2, independent of the content height
   H. For a centre at 351.5 - 40 = 311.5, S = 75. **If .pane's padding changes,
   this number changes with it.**
   It is a spacer and not padding-bottom on purpose: flex-shrink lets it give
   way on a card that is already tight, so the lift can never push a line out of
   a box that cannot scroll -- and fitPaneVertical() never sees a fresh overflow
   and demotes the card to top-pinned. Every other pane resolves the fallback
   0px and gets a zero-height item it cannot see. */
/* THE NUMBERED LIST, 2026-09-12 (owner: "almost like a bulleted or numbered
   list ... I think the numbered ones work here, but just a tap and reveal").
   It carries the cards that used to ASK the reader something -- Choice, Tap
   action, Interaction -- now that they only tell. Each item is a .wm-beat, so
   the list reveals one row at a time on a tap and nothing already read moves.
   The number is drawn by the list itself rather than typed into the copy: a
   typed "1." is content and would be read aloud twice by a screen reader.
   16px between rows is the spacing scale; the marker sits in the 27px gutter
   rather than indenting the text off the spine (CARD_RULES 3f). */
.wm-list{list-style:none;counter-reset:wm-item;margin:0;padding:0;width:100%}
.wm-list > li{counter-increment:wm-item;position:relative;padding-left:27px}
.wm-list > li::before{content:counter(wm-item) ".";position:absolute;left:0;top:0;
  font-family:var(--mono,ui-monospace,monospace);font-size:13px;line-height:1.95;color:var(--soft)}

/* THE ACCUMULATING CARD, shared 2026-09-12 (owner: "if there's more on it,
   just make a tap and go to the next part of that slide ... the one slide with
   the paragraph and paragraph and paragraph is what I'm looking for").
   Every beat is in the layout from the moment the card is built; a reveal
   changes OPACITY and nothing else. That is the whole reason an accumulating
   card is legal in a box that cannot scroll: the first line holds the same y in
   every state, nothing reflows under the reader, and one measurement at the
   FULLEST state covers all of them (CLAUDE.md -- test the maximum state, not
   the resting state). Measured off the reference recording, the same product
   does exactly this: between its one-block and two-block states the first
   paragraph does not move by a pixel.
   16px between beats is the spacing scale. This was `.sci-beat`, private to
   `redlight`, until the mechanism was needed on every card type. */
.wm-beat{opacity:0;transition:opacity .4s ease}
.wm-beat.is-on{opacity:1}
.wm-beat + .wm-beat{margin-top:16px}
@media (prefers-reduced-motion: reduce){ .wm-beat{transition:none} }

.pane::after{content:"";display:block;width:100%;flex:0 1 var(--pane-lift-spacer,0px);min-height:0;pointer-events:none}
/* THE PANE CARRIES THE SKY, 2026-09-12. This rule used to paint a FLAT
   var(--card) over the whole 343x703 box at z-index 3, which meant the sky
   gradient declared on .card was never seen on a deck -- it showed only for the
   240ms of a slide, as a strip beside the moving pane. Three separate agents hit
   this independently today: one measured a perfect anchor box for a drawing that
   painted nothing because the pane covered it, one found every contrast figure
   had been computed against #DCEAF4 when the painted surface was #C2D6E8, and a
   third spotted it while chasing an unrelated z-index bug.
   The pane CANNOT simply go transparent: panes stack during the card flip, and a
   see-through outgoing pane shows the incoming one through it. So the pane takes
   the gradient itself. It is the same box as the card, so the two align exactly
   and the seam is invisible.
   This also restored the contrast the palette was designed around -- body copy
   went 4.43:1 (failing) back to 4.88:1, because it is once again sitting on the
   ground it was measured against. */
.pane-cur{z-index:3;background-color:var(--paper);
  background-image:linear-gradient(180deg,var(--sky-1,#F5FAFD) 0%,var(--sky-2,#DCEAF4) 55%,var(--sky-3,#D3E4F2) 100%)}
.pane-cur,.pane-next{will-change:transform;backface-visibility:hidden}
.pane-next{z-index:2}
.ibadge-bare svg{width:28px;height:28px}
/* top:40px, raised from 16px on 2026-09-05. The card now runs edge to edge
   with no gutter, so distance from the card top IS distance from the screen top --
   at 16px the label sat right against the Wix header bar. The content anchor moved
   58 -> 64 with it, keeping a 10px gap. Two larger values were measured and
   rejected: 48/80 overflowed inbox's breath card by 7px, and 40/70 left it only
   4px -- too thin to trust across devices. That card is the binding constraint
   because its takeaway runs to two lines above the bloom. */
/* The closing card used to end on a tick and nothing else -- a reviewer walking
   it cold said the one thing that would have brought them back was missing at
   the exact moment they'd have said yes. This line answers "how many are left
   here", and the path hub now opens the next unfinished moment when you land on
   it, so the two halves meet. (2026-09-05) */
/* Adopted from the V13 research build (2026-09-06).
   Section 9 of the research chose left-aligned editorial: centre only genuinely
   short reflective pauses, left-align longer reading copy. Our takeaway card
   centred everything, and the visual reviewer independently measured the result
   at 18-22 characters a line -- a long sentence chopped into five fragments.
   10 of 24 deck takeaways run past 72 characters and now set flush left. */
.takeaway.long-form{text-align:left;align-self:flex-start}

/* Defensive only: creative copy may wrap, but must never paint outside its
   column. No visual change unless something would already have overflowed. */
.pane h1,.pane h2,.pane .body,.pane .takeaway,.pane .eyebrow{
  max-width:100%;min-width:0;overflow-wrap:break-word;word-break:normal;box-sizing:border-box}

.done-next{margin:14px 0 0;font-family:var(--sans);font-size:var(--body-size-sm);
  line-height:1.4;color:var(--quiet);text-align:left}
/* Centered + top:40px->50px (2026-09-08, owner): every eyebrow across every
   card type gets the same treatment "One breath"'s did. Text-align:center
   is new -- the base .eyebrow rule never set one, so it inherited left from
   its container. The 10px drop needs .pane's own top padding to grow by the
   same 10px (see below) or the gap between eyebrow and the first heading
   would just shrink by 10px instead of the whole block moving down. */
/* 50px->35px (2026-09-08, owner: "move it all up another fifteen") -- shifted
   back up 15px along with .pane below, same amount, so the gap between
   eyebrow and the first heading is unchanged from the last settled value. */
/* top:35px -> 57px (2026-09-08, owner: "the anchor is a line, and the eyebrow
   is right as close as it can be to that"). The eyebrow is 14px tall (12px/1.2),
   so 57 + 14 = 71 -- its bottom edge lands exactly on the content anchor
   instead of floating 22px above it. It is position:absolute, so it does not
   push the content down: the two just meet. If the eyebrow's type size ever
   changes, this number has to change with it -- it is 71 minus the rendered
   height, not a free-standing value -- and it has to move whenever .pane's top
   padding moves, or the eyebrow stops resting on the line. */
/* top 57 -> 49, 2026-09-09. The eyebrow now sits ON the header line, centred
   between the back button and the close button and sharing their vertical
   centre (the 44px rings run 34-78, centre 56; a 14px eyebrow at 49 centres
   there too). It is the title slot, exactly as a settings screen puts its page
   title level with its back button -- the owner's reference. */
/* 59 -> 90 (2026-09-12): the sun line took the 64->68 band the eyebrow used
   to sit on, so the eyebrow drops 31px.
   MEASURED, 2026-09-12, because the line above said "keeps a 22px gap under it"
   and that is wrong on both counts. Live on a deck: the sun runs 64-68, the two
   44px control rings run 44-88 (their drawn glyphs about 56-76), the eyebrow
   90-104.4, the content anchor 129. So the gap UNDER the eyebrow is 24.6, not
   22; the 22 is the gap ABOVE it, and only if you measure from the sun line
   rather than from the chrome that is actually nearest -- the rings, whose box
   ends 2px above the eyebrow.
   That leaves the eyebrow nearer the chrome than the content it labels, which
   is the wrong way round and is logged as an open item in TASKS.md. It is NOT
   corrected here: this value carries four dated owner instructions (2026-09-08
   x3, 2026-09-09) and 90 is the owner's own 59 carried up by the sun move, so
   it is his to change. Do not "fix" it from the scale alone. */
.eyebrow.eyebrow-top{position:absolute;top:90px;left:27px;right:27px;width:auto;margin:0;text-align:center}
h3{font-family:var(--serif);font-weight:500;font-size:1.5rem;line-height:1.25;margin:0 0 14px;color:var(--ink)}
.body{font-size:var(--body-size);line-height:1.5;color:var(--soft);margin:0;max-width:30em;text-align:left}
.navbtn{all:unset;touch-action:manipulation;pointer-events:auto;position:absolute;bottom:0; width:44px;height:44px; display:inline-flex;align-items:center;justify-content:center;cursor:pointer; transition:opacity .2s ease}
.navbtn svg{width:20px;height:20px;stroke-width:2.6}
/* The two .navbtn.navback / .navbtn.navnext rules that stood here were folded
   into the top-row block above on 2026-09-12 -- they set left:15px / right:15px
   for the old BOTTOM row and were being silently overridden, which hygiene_check
   correctly flagged as one selector defined twice. The surviving copy sets
   left:16px so the arrow's 44px target matches the close control's inset. */
.navbtn:focus-visible{outline:2px solid var(--ink);outline-offset:3px;border-radius:4px}
.navbtn:disabled{opacity:.28;cursor:default;pointer-events:none}
.card.flip-out{animation:card-flip-out .16s ease-in forwards;box-shadow:none}
.card.flip-in{animation:card-flip-in .24s cubic-bezier(.2,.7,.3,1) both;box-shadow:none}
@keyframes card-flip-out{from{transform:rotateY(0deg)}to{transform:rotateY(90deg)}}
@keyframes card-flip-in{from{transform:rotateY(-90deg)}to{transform:rotateY(0deg)}}
/* The sun, not the register: the line must stay one colour all the way
   across, and the register changes from card to card. Falls back to the
   literal so this renders correctly on any page whose token block has not
   been swept yet. */
.card .progress-bar-fill{background:var(--sun,#B0730F)}
@keyframes wm-rise{from{opacity:0;transform:translate3d(0,10px,0)}to{opacity:1;transform:translate3d(0,0,0)}}
@keyframes wm-pop{from{transform:scale(.4);opacity:0}70%{transform:scale(1.1);opacity:1}to{transform:scale(1);opacity:1}}
@keyframes wm-draw{to{stroke-dashoffset:0}}
.pane-next .wm-in,.pane-next .wm-pop{opacity:0}
.pane-cur .wm-in{animation:wm-rise .42s cubic-bezier(.2,.7,.3,1) backwards}
.pane-cur .wm-in.d1{animation-delay:.08s}
.pane-cur .wm-in.d2{animation-delay:.16s}
.pane-cur .wm-in.d3{animation-delay:.24s}
.pane-cur .wm-in.d4{animation-delay:.32s}
.wm-pop{transform-box:fill-box;transform-origin:center}
.pane-cur .wm-pop{animation:wm-pop .5s cubic-bezier(.2,.7,.3,1) backwards}
.pane-cur .wm-pop.d1{animation-delay:.3s}
.pane-cur .wm-pop.d2{animation-delay:.55s}
.pane-cur .wm-pop.d3{animation-delay:.8s}
.pane-cur .wm-pop.d4{animation-delay:1.05s}
/* A fill is not a stroke: it has no length to wipe on, so it fades in once the
   strokes it belongs to have drawn. Resting state is visible, and the animation
   runs FROM transparent with `backwards`, so a renderer that ignores animation
   shows a filled drawing rather than an empty one. See VISUAL_SYSTEM.md §1b. */
@keyframes wm-fade{from{opacity:0}to{opacity:1}}
.wm-fill{opacity:1}
.pane-next .wm-fill{opacity:0}
.pane-cur .wm-fill{animation:wm-fade .6s ease .45s backwards}
.wm-draw{stroke-dasharray:var(--len,500);stroke-dashoffset:var(--len,500)}
.pane-cur .wm-draw{animation:wm-draw .9s cubic-bezier(.4,0,.2,1) forwards}
.pane-cur .wm-draw.d1{animation-delay:.2s}
.pane-cur .wm-draw.d2{animation-delay:.45s}
/* THE BREATH ZOOM -- one image carried across the three breath beats: scaling up
   through "One breath in.", held through "Hold.", scaling back down through
   "One breath out." Owner instruction, 2026-09-13.

   It is deliberately NOT a second animation system. It reuses wm-pop's existing
   convention exactly -- one shot, gated on .pane-cur, reset by .pane-next, and
   `forwards` so the beat ENDS at the value the next card starts from. The only
   new idea is WHERE the transform runs: on the .wm-illo wrapper, not on the
   drawing's own elements. Those already carry .wm-pop/.wm-draw/.wm-fill, and
   .wm-pop animates `transform` on them; scaling the wrapper composes with that
   instead of fighting it for the same property.

   4s is not a new number either -- it is the breath duration the retired
   wm-bloom arc used (`transition:stroke-dashoffset 4s linear`), kept so the
   pacing the reader feels does not change along with the picture.

   Continuity is the point, so the drawing must NOT replay its draw-on entrance
   on the hold and out beats. The markup for those two beats is emitted with the
   .wm-fill/.wm-draw/.wm-pop classes stripped (see any at-home deck's
   BREATH_STATIC) -- a .wm-draw element outside .pane-cur is fully dash-offset
   and therefore INVISIBLE, so leaving the classes on and merely not animating
   them is not an option.

   IT BREATHES DOWN FROM FULL SIZE, NEVER UP, AND THAT IS THE WHOLE TRICK.
   The inhale ENDS at scale(1) -- the drawing's own declared size -- and every
   other moment is smaller. Measured 2026-09-13, because the obvious version is
   wrong in a way no gate can see: scaling UP from the declared size grows the
   painted box around its centre while the LAYOUT box stays put, so the drawing
   creeps into its own margins. On the at-home breath cards, scaling a 180px
   drawing to 1.18 collapsed the gap below the drawing from 17px to 0.8px and
   the gap above it from 24.6px to 8.4px, while `pane.scrollHeight ===
   pane.clientHeight` stayed true and ./qc.sh stayed green the entire time --
   a transform does not reflow, so nothing mechanical can report it. Breathing
   DOWN keeps the peak exactly equal to the box the card already reserves, so
   the proven 24px/16px optical gaps hold at the maximum state and every
   other frame only has more air. It also means an existing drawing takes the
   zoom with no resize and no new overflow risk. */
/* THE BREATH IMAGE IS TWO OVERLAPPING LEAVES ("hanging-leaves"), not the
   three-stone stack, an autumn oak leaf, or any of the other shapes tried
   2026-09-13/14 -- see any converted deck's BREATH_SVG for the current
   markup, `_dev/design-studio/RECURRING_IMAGE_ARTWORK.md` for the full
   description. Same mechanic, same box.
   FALLING SWAY ADDED 2026-09-14, owner instruction: the image should drift and
   tilt during breath-in/out, not just scale, so it reads as falling rather
   than inflating. Both keyframes start and end at translateX(0) rotate(0deg)
   -- the neutral value `.breath-hold` already sits at -- so the sway never
   leaves a mismatch for the next card's static state to snap out of; only
   the mid-beat frames drift. */
@keyframes wm-breath-in{
  0%{transform:scale(var(--breath-rest,.85)) translateX(0) rotate(0deg)}
  35%{transform:scale(.93) translateX(-3px) rotate(-4deg)}
  70%{transform:scale(.98) translateX(2px) rotate(3deg)}
  100%{transform:scale(1) translateX(0) rotate(0deg)}
}
@keyframes wm-breath-out{
  0%{transform:scale(1) translateX(0) rotate(0deg)}
  35%{transform:scale(.95) translateX(2px) rotate(3deg)}
  70%{transform:scale(.9) translateX(-3px) rotate(-4deg)}
  100%{transform:scale(var(--breath-rest,.85)) translateX(0) rotate(0deg)}
}
/* FOURTH BEAT ADDED, 2026-09-14, owner instruction: "One breath out." and "Then
   let your breathing find its own rhythm." split into their own cards. The new
   card carries no directional in/out state -- it settles at the same
   var(--breath-rest,.85) breath-out already ends on, then loops a small,
   continuous, EASE-IN-OUT pulse in size and position for as long as the reader
   stays on the card, reading as an ongoing rhythm rather than one more
   one-shot beat. */
@keyframes wm-breath-idle{
  0%,100%{transform:scale(var(--breath-rest,.85)) translate(0,0)}
  50%{transform:scale(calc(var(--breath-rest,.85) + .04)) translate(1.5px,-1.5px)}
}
/* .breath-rest RETIRED 2026-09-14: the context/rest card(s) that used to
   carry the image at this scale no longer carry an image at all (context
   and mechanic cards split the same day) -- .breath-in already starts at
   var(--breath-rest,.85), so the constant survives, only the class doesn't. */
.wm-illo.breath-in,.wm-illo.breath-hold,.wm-illo.breath-out,.wm-illo.breath-idle{transform-origin:center}
.wm-illo.breath-hold{transform:scale(1)}
.wm-illo.breath-idle{transform:scale(var(--breath-rest,.85))}
.pane-cur .wm-illo.breath-in{animation:wm-breath-in 4s cubic-bezier(.4,0,.2,1) forwards}
.pane-cur .wm-illo.breath-out{animation:wm-breath-out 4s cubic-bezier(.4,0,.2,1) forwards}
.pane-cur .wm-illo.breath-idle{animation:wm-breath-idle 4.5s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){
  /* Hold each beat at the state it would have ENDED on, so the sequence still
     reads as one image at two sizes rather than four identical ones. The breath
     is paced by the reader's own tap either way. */
  .pane-cur .wm-illo.breath-in,.pane-cur .wm-illo.breath-out,.pane-cur .wm-illo.breath-idle{animation:none}
  .wm-illo.breath-out,.wm-illo.breath-idle{transform:scale(var(--breath-rest,.85))}
}
/* THE DRAWING'S TWO GAPS ARE OPTICAL, NOT BOX VALUES. Owner decision 2026-09-10
   after the designers' panel (_dev/experiments/placement/PANEL.md): 24px of
   VISIBLE air below the drawing's ink, 16px above it, and the drawing stays
   centred on a left-ranged card.

   The margin below reads 16 and renders 24, and that is correct, not a mistake
   to tidy: a trimmed drawing's box carries half a stroke width of pad (~1.2px)
   and the text below it carries ~6.7px of half-leading above its cap height, so
   16 + 1.2 + 6.7 = 24 of air the reader can actually see. Both numbers were
   measured, not assumed. If you change the body or heading type scale, remeasure
   -- the leading moves and the seen value moves with it. Panel: "the declared
   value and the seen value are not the same thing, and the seen value is the
   spec."

   This only comes out right on a drawing whose box is trimmed to its own ink.
   On an untrimmed drawing the box adds 20-45px of invisible slack on EVERY side
   -- see _dev/VISUAL_SYSTEM.md section 6. */
/* THE DRAWING SITS DIRECTLY ON THE CARD, NO PANEL -- owner, live, 2026-09-13:
   "this slide has a darker blue block behind it... I said we don't want any
   backgrounds... all straight on as if they're just the image with the
   background that's already on the card, kinda like a translucent
   background." This said panel (background:var(--illo-panel), 14px radius,
   14px/12px padding) from 2026-09-12 until this date -- that panel was a
   six-seat-tested production default, not a placeholder; the reasoning is
   kept for context in _dev/history/illo-panel-superseded-2026-09-13.md, not
   because it still governs. _dev/VISUAL_SYSTEM.md section 6g owns the rule. */
.wm-illo{width:100%;display:flex;justify-content:center;align-items:center;margin:0 0 17px;flex-shrink:0;pointer-events:none}
.wm-illo svg{width:136px;height:136px;overflow:visible}
.wm-art{flex:1 1 0;min-height:0;width:100%;display:flex;align-items:center;justify-content:center;padding-top:24px;box-sizing:border-box;pointer-events:none}
.wm-art.above-trigger{margin-bottom:68px}
.wm-art svg{max-width:100%;max-height:100%;width:auto;height:auto;overflow:visible;display:block}
.wm-art path,.wm-art line,.wm-art polyline,.wm-art circle,.wm-art ellipse,.wm-art rect{stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.wm-art :is(path,line,polyline,circle,ellipse,rect):not([stroke]){stroke:var(--ink)}
.wm-art :is(path,line,polyline,circle,ellipse,rect):not([fill]){fill:none}
.wm-art .ghost{opacity:.32}
.wm-art .hair{stroke-width:1.2}
/* gap 12px->8px, margin-top 24px->8px (2026-09-08, owner): compensates for
   .pane's top padding growing 56px->76px the same day. This card type ("One
   breath", all 24 in-app decks) was already documented as the tightest
   content stack in the app -- its takeaway text was the deck's own binding
   constraint when the eyebrow-top position was tuned earlier. The extra
   20px of top padding left only ~2px of clearance above the nav row, not
   a safe margin. Trimmed 20px back out of this card's own internal spacing
   (16px off margin-top, 4px off gap) rather than reducing the top padding
   change for every card type -- restores this card's original ~22px
   clearance without touching anything else. The animation timing/rhythm
   itself is untouched, only static spacing. */
/* gap 8px->6px, margin-top 8px->4px (2026-09-08, re-applied): .pane's
   86px top padding is back, so this card (the documented tightest content
   stack in the app) needs the same compensating trim it needed the first
   time that value was in effect -- restores clearance above the ~20px
   floor this session settled on. */
.wm-breath{display:flex;flex-direction:column;align-items:center;gap:6px;margin-top:4px;width:100%}
.wm-breath-tap:focus-visible{outline:2px solid var(--ink);outline-offset:4px}
/* pointer-events:none: this is ART, not a control. Both classes sit in the
   tap handler's closest() exclusion list, which stops them triggering a page
   turn -- but without this they still SWALLOW the tap, so the card does
   nothing at all. .wm-art and .wm-illo already carry it; these two were
   missed. The bloom straddles the card's midline, so the dead patch landed
   dead centre of the breathing card. (2026-09-05) */
.wm-bloom{width:164px;height:164px;display:block;overflow:visible;pointer-events:none}
.wm-lobes{transform-box:fill-box;transform-origin:center;transform:scale(.86);transition:transform 4s cubic-bezier(.4,0,.2,1)}
@keyframes wm-idle{0%,100%{transform:scale(.84)}50%{transform:scale(.92)}}
.wm-breath.idle .wm-lobes{animation:wm-idle 5.2s ease-in-out infinite}
.wm-breath.idle .wm-arc-fill{transition:none}
.wm-breath.phase-in .wm-lobes{transform:scale(1.1);transition-duration:4s}
.wm-breath.phase-in .wm-arc-fill{stroke-dashoffset:385;transition-duration:4s}
.wm-breath.phase-hold .wm-lobes{transform:scale(1.1);transition-duration:2s}
.wm-breath.phase-hold .wm-arc-fill{stroke-dashoffset:289;transition-duration:2s}
.wm-breath.phase-out .wm-lobes{transform:scale(.78);transition-duration:6s}
.wm-breath.phase-out .wm-arc-fill{stroke-dashoffset:0;transition-duration:6s}
.wm-breath.done .wm-lobes{transform:scale(.9);transition-duration:1.2s}
.wm-breath.done .wm-arc-fill{stroke-dashoffset:0;transition-duration:.4s}
/* ---------------------------------------------------------------------------
   WIDENING FIELD -- the Awareness counterpart to the breath bloom.
   (Awareness is Introduction step 05; its slug and folder stay
   `open-awareness`, which is what every stored progress record uses. The
   display name lost its first word on 2026-09-09, owner: it was the longest
   label in the app and repeating "awareness" across the app is fine.)

   CARD_DESIGN.md 7 and 8: One Breath is not the universal mindfulness button,
   and by Awareness the breath should "reduce or transform" while the
   interaction visually WIDENS. Reusing the bloom there for consistency is the
   mistake 8 names -- it would keep teaching "return to the breath" on the one
   card whose lesson is that attention can rest on anything.

   So the mechanism changes and the language does not. Same 164px figure, same
   ambient CSS loop with no control to press, same slow cubic-bezier, same ink
   and spot colours, same label and note beneath. What differs is the motion:
   the bloom scales in and out around one centre; this opens outward and lets
   go at the edge. Nothing contracts, and nothing has to be followed.

   The centre dot never animates. It is the breath -- still there, no longer
   the whole picture.
   --------------------------------------------------------------------------- */
.wm-open{display:flex;flex-direction:column;align-items:center;gap:6px;margin-top:4px;width:100%}
.wm-open-fig{width:164px;height:164px;display:block;overflow:visible;pointer-events:none}
.wm-open-ring{transform-box:fill-box;transform-origin:center;opacity:0}
@keyframes wm-widen{
  0%{transform:scale(.16);opacity:0}
  10%{opacity:.85}
  60%{transform:scale(1);opacity:.5}
  92%{transform:scale(1.09);opacity:0}
  100%{transform:scale(1.09);opacity:0}
}
.wm-open .wm-open-ring{animation:wm-widen 14s cubic-bezier(.4,0,.2,1) infinite}
.wm-open .wm-open-ring.r2{animation-delay:1.8s}
.wm-open .wm-open-ring.r3{animation-delay:3.6s}
.wm-open .wm-open-ring.r4{animation-delay:5.4s}
.wm-open-label{font-family:var(--sans);font-size:var(--body-size-sm);color:var(--soft);margin:0;text-align:center;min-height:1.4em}
.wm-open-label strong{color:var(--ink)}
.wm-open-note{font-family:var(--sans);font-size:var(--body-size-sm);color:var(--quiet);margin:2px 0 0;text-align:center}
/* At rest the field is simply open -- every ring visible, nothing moving. The
   card still says what it means without the motion carrying it. */
@media (prefers-reduced-motion: reduce){
  .wm-open .wm-open-ring{animation:none;opacity:.5;transform:none}
}
.wm-breath-label{font-family:var(--sans);font-size:var(--body-size-sm);color:var(--soft);margin:0;text-align:center;min-height:1.4em}
.wm-breath-label strong{color:var(--ink)}
body > .stage{touch-action:none}
.wm-row h3{font-family:var(--serif);font-weight:400;font-size:19px;line-height:1.25;margin:0;color:var(--ink);display:flex;align-items:center;gap:8px}
.wm-row h3 svg{width:20px;height:20px;flex-shrink:0;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.guess-hint:empty,.build-hint:empty,.wm-breath-label:empty,.twin-status:empty,.sort-status:empty{display:none}
.wm-breath.idle .wm-arc-track{opacity:0}
/* Always in motion (owner, 2026-09-05). The bloom is no longer a control: it
   breathes on its own, the whole time the card is up, and the copy asks for ONE
   deliberate breath before the reader returns to their own rhythm. So: one CSS
   animation, no tap, no timers, no count. In 4s, hold 2s, out 6s -- the same
   shape as the old guided breath -- on a 12s loop. The progress arc is hidden:
   a ring that fills and empties every twelve seconds reads as a timer to obey,
   and the whole point is that nobody has to follow it. */
@keyframes wm-breathe{0%{transform:scale(.78)}33.3%{transform:scale(1.1)}50%{transform:scale(1.1)}100%{transform:scale(.78)}}
.wm-breath.loop .wm-lobes{animation:wm-breathe 12s cubic-bezier(.4,0,.2,1) infinite;transition:none}
.wm-breath.loop .wm-arc-track,.wm-breath.loop .wm-arc-fill{opacity:0}
.wm-breath.loop .wm-breath-tap{cursor:default}
.wm-breath-note{font-family:var(--sans);font-size:var(--body-size-sm);line-height:1.4;color:var(--quiet);margin:0;text-align:center;max-width:26em}
@media (prefers-reduced-motion: reduce){ .wm-breath.loop .wm-lobes{animation:none;transform:scale(.94)} }
[class$="-status"]:empty{display:none}

/* ============================ CARD GEOMETRY ============================
   ONE source of truth for every deck's box, spacing and gaps (2026-09-07).

   These values are Home's -- hub-pages/home/index.html is the reference card
   for the whole app, and CARD_CHROME_GUIDE.md section 0 records the numbers
   and the reasoning. They used to be repeated in all 29 deck files, which is
   29 chances to get one wrong; change them HERE and every deck follows.

     .card       343x703. The deck files no longer state a width or height.
     .mda-nav    a 70px absolute overlay on the card's own bottom band, INSIDE
                 .card, so the card's overflow+radius clips it to the same
                 shape (that is why it must not be a sibling). No radius and no
                 bleed shadow of its own -- both were props for the old
                 separate-box treatment.
     .mda-nav-item  raised 10px inside the bar, as on Home.
     .pane       content sits in the same rectangle as Home's .pager-page:
                 56px down from the top, 22px sides (Home's 299px column),
                 122px clear of the bottom (26px padding + Home's 96px
                 chevrow band). The 56px top used to be written by JS as an
                 inline style on every pane, which beat any stylesheet rule and
                 made CSS edits here look applied when they were not -- those
                 writes are gone, CSS owns the anchor.
     .card-foot  the back/next + progress row stands in for Home's chevrow,
                 8px clear of the nav's top edge (70 + 8 = bottom:78px above).

   If you add a new card TYPE to a deck, it inherits all of this. Only override
   something here inside that card type's own rule, and never by re-stating the
   box.
   ====================================================================== */
/* THE SKY, 2026-09-12 (owner: "light blue for BG", "maybe even lighter like a
   sky blue", "but dark enough to sit on white", "maybe both with some
   inconsistent gradient to make it a bit creative").
   It is a gradient because a real sky is one -- lighter at the top where the
   light comes from, deeper at the horizon. 1.13:1 against the white page at its
   LIGHTEST point, which is the demanding end; the old cream measured 1.12, so
   the card separates from the page slightly better than it did.
   Declared here, once, rather than in 34 files. The per-page
   .card{background-color:var(--paper)} stays as the flat fallback underneath --
   different property, so the two do not fight, and a browser that drops the
   gradient still gets the right blue. */
.card{--illo-panel:#C7DCEC;width:343px;height:703px;background-image:linear-gradient(180deg,var(--sky-1,#F5FAFD) 0%,var(--sky-2,#DCEAF4) 55%,var(--sky-3,#D3E4F2) 100%)}
/* The nav is a floating pill, not a band (owner, 2026-09-11: "make the nav
   just a pill", then "almost transparent light green for nav pill").
   rgba(188,212,174,.55) composites to 1.12:1 against the card where it sits
   -- QUIETER than the cream band's 1.31 and still readable as an object.
   The saturated green band that all 20 seats rejected measured 3.85. Below
   about 1.10 it stops reading as a thing at all, so .55 is near the floor,
   not a round number. Icons on it measure 9.8:1. */
.mda-nav{position:absolute;left:12px;right:12px;bottom:22px;width:auto;display:flex;align-items:center;justify-content:space-around;gap:0;background:var(--nav-pill,rgba(188,212,174,.26));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:4px;border:1px solid rgba(255,255,255,.70);border-radius:999px;box-shadow:0 4px 14px rgba(20,40,70,.08);z-index:5}
/* top:-10px removed 2026-09-12. It was compensating for the old full-width
   BAND, which sat flush on the card's bottom edge and needed its icons lifted
   off that edge. The pill is a centred flex row, so the offset now pushes the
   icons ABOVE the pill's own centre line instead. Zero centres them. */
.mda-nav-item{position:relative;top:0}
/* Top padding 56px -> 76px (2026-09-08, owner): the first heading on every
   card sits 20px lower than before. 56px matched Home's own top anchor
   (12px .wrap padding + 44px chevron-clearance reserve); this is now a
   deck-only difference from that reference number -- hub-pages' own anchor
   is untouched. Re-walk the tightest card types (Reflection, Choice/accordion
   with many rows, the "Another way" trigger) after this change: 20px less
   vertical room is available for content than before. */
/* 76px->86px->76px->86px (2026-09-08, owner, changed mind twice): first
   tried with the heading also dropping 10px to preserve the eyebrow-to-
   heading gap; reverted to "only the eyebrow moves"; then re-decided the
   heading should drop too after all. Current value: everything under the
   eyebrow is 10px lower than it was before today's eyebrow change, same as
   .wm-breath's compensating trim below needing to be re-applied with it. */
/* 86px->71px (2026-09-08, owner: "move it all up another fifteen") -- shifted
   up 15px along with the eyebrow above, same amount, so the gap between them
   is unchanged. This also gives every card 15px MORE clearance to the nav
   band at the bottom (content starts higher, ends higher), so no card gets
   newly tighter from this change -- only the earlier drops needed the
   breath-card compensation re-checked, not this one. */
/* 71px is the anchor. (2026-09-08: two experiments came and went the same day --
   81px, then 61px -- both reverted. 71 stands.) The EYEBROW rests on this line
   rather than the band changing height: see .eyebrow.eyebrow-top above. */
/* Side padding 22px -> 27px (2026-09-08, owner: "left and right margins
   increase by five pixels"). The text column narrows 299 -> 289px. This is the
   one direction that CAN push copy onto an extra line, so unlike the vertical
   changes today it is not risk-free -- re-walk anything that was already tight.
   Hub-pages follow in .wrap/.pager/.chevrow, which must all carry the same
   number or the pager stops bleeding to the card's true edge. */
/* padding-top 71 -> 88, 2026-09-09. Content now begins BELOW the header band
   rather than level with it: the band runs to 78 (the rings' bottom edge) and
   88 leaves the app's 10px gap under it. One anchor again across all 41 pages --
   the hub-pages were briefly at 83 and moved to 88 with this. */
/* 88 -> 98 (owner, 2026-09-09): "a little more gap between the eyebrow and
   whatever's underneath it... especially with the x and the back there, it's
   way too low, too close." The band is 44-88, so content at 88 sat flush
   against the bottom of the ringed controls. 10px puts it clear of them again.
   This costs 10px of content height on every card -- see HANDOFF for what had
   to be trimmed to pay for it. */
/* padding-top 98 -> 129 (2026-09-12), the SAME 31px the eyebrow moved, so the
   eyebrow-to-first-line gap is unchanged at 24.6px. This is 31px less room
   above the 569 floor on every card -- every deck was re-measured after. */
.pane{padding:129px 27px 134px}
/* A card that LEADS with a drawing under its eyebrow starts 12.5px higher, so
   the drawing's ink lands 16px below the eyebrow's baseline instead of 21
   (measured live on a deck, not computed: the eyebrow baseline sits at 70.7 and
   the ink carries ~1.2px of stroke pad). The eyebrow is uppercase, so its
   baseline IS its visible bottom edge. The fractional value is deliberate --
   rounding it to 85 or 86 moves the seen gap off 16, and this gap is specified
   optically. Cover cards have no eyebrow and are deliberately not matched:
   nothing sits above their drawing, so "16 above" has no referent there and
   their anchor is unchanged. */
.pane:has(> .eyebrow-top):has(> .wm-illo:first-child){padding-top:116.5px}  /* 85.5 + the same 31 */

/* COVERS GO BACK TO 98 (2026-09-12, owner: "gaps and spacing ... like on the
   cover page seem off" -- and they were right).
   The 129 anchor above exists to clear the EYEBROW, which now rests 90 -> 104.4
   because the sun line took its old place. A cover has no eyebrow, so on a cover
   that 31px buys nothing and simply opens a hole: measured, the sun ended at 68
   and the illustration did not begin until 129, a 61px void under the chrome
   where every other card has a word. Before today that gap was 24.6.
   Exactly ONE card per deck is affected -- walked `redlight` end to end: 9 of its
   10 cards carry an eyebrow, and the one that does not is the cover. So this is a
   narrow rule, not a second anchor competing with the first.
   The cover keeps its slack BELOW the last line: it is top-anchored by design
   (four beats -- icon, name, hook, summary) and has always ended high. That is
   not the fault; the void above it was. */
.pane:not(:has(> .eyebrow-top)){padding-top:98px}

/* ===================== PROTOTYPE MODE (?proto=1) =====================
   A card's own identity, drawn on the card. OFF unless the query string asks
   for it -- a normal reader never gets this element at all, because the deck
   script only creates it when the flag is set. See PROTOTYPE_MODE.md.

   It is chrome, and it obeys the chrome rules: flat, not a ring, so it cannot
   read as a control; --quiet ink, never the register colour; absolutely
   positioned in the header band, whose space is reserved anyway, so it moves
   nothing. It sits top-right because the band's left and centre are the back
   and up arrows.

   It is a <span>, not a link, in stage 1 -- there is nowhere to link to yet.
   When stage 2 gives it a destination it becomes an <a>, and the deck's 50/50
   tap handler will skip it for free: that handler already excludes `a` and
   `button` via closest(), so the badge cannot steal a tap to advance.
   ==================================================================== */
/* Stage 2: the badge is a LINK now, so it takes pointer events. It cannot
   steal a tap to advance the card -- the deck's 50/50 handler already excludes
   `a` via closest(), which was checked before making it one. It stays flat and
   un-ringed: a ring would claim it is one of the card's own controls. */
.proto-badge{position:absolute;top:14px;right:14px;z-index:20;text-decoration:none;
  font:500 10px/1 var(--mono,ui-monospace,Menlo,monospace);letter-spacing:.04em;
  color:var(--quiet,rgba(33,28,21,.62));background:var(--paper,#FFFCF5);
  border:1px solid var(--line,rgba(33,28,21,.16));border-radius:4px;padding:3px 5px;
  max-width:72%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
a.proto-badge:hover{color:var(--ink);border-color:var(--ink)}
a.proto-badge:active{opacity:.6}
a.proto-badge:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* Prototype mode, stage 4: an editable line shows a faint dotted underline so
   you can see what can be retyped without anything shouting.

   THE FOCUSED FIELD IS PLAIN WHITE, 2026-09-09 (owner: "why the blue line and
   light BG ... why not just make the typing area white and nothing else").
   It used to take a 2px ink outline plus a 5% ink wash, which drew a ringed,
   tinted box around the very thing you were trying to read while you retyped
   it -- two decorations competing with the words.

   White IS the focus signal here, and it needs no help: every surface in this
   app is cream (#FFFCF5 paper, #F7F2E7 panels), so a white field is the only
   white thing on screen. The box-shadow is not a shadow -- it is a 4px spread
   of the same white, which pads the field out past the glyphs so it reads as
   an area to type in rather than a highlight behind the text.

   Accessibility is preserved WITHOUT a ring: the background genuinely changes
   on focus, so keyboard users still see where they are. Do not add an outline
   back on top; if this ever needs to be stronger, widen the white, don't ring
   it. Prototype-mode only -- no reader ever sees this. */
[contenteditable]{border-bottom:1px dotted rgba(33,28,21,.28);border-radius:2px}
/* [contenteditable]:hover{background:rgba(33,28,21,.045)} was REMOVED here,
   2026-09-09. It is the same 4.5% ink wash the owner had just had removed from
   :focus -- so on any device that reports hover (a tablet with a trackpad, a
   desktop browser, a hybrid laptop) the tint reappeared on the exact element it
   was deleted from. A touch-only app should not carry a hover surface change at
   all; the white focus field below is the whole affordance. */
[contenteditable]:focus{outline:none;border-bottom-color:transparent;
  background:#FFFFFF;box-shadow:0 0 0 4px #FFFFFF;border-radius:2px}

/* ============================ H1 ============================
   One h1 for the whole app (2026-09-08, owner: "anything that's h1, reduce it
   to subheading -- subheading size, the exact same position, and reduce it so
   it's always on one line").

   Before this it was FOUR different rules: 28px on the 29 decks, and 32px,
   34px or a centred 32px across the twelve hub-pages. Now one rule, matching
   CARD_TEMPLATE.html's Subheading sample exactly: Fraunces 23px/1.25, centred,
   14px below.

   Composition rule that came with it: what follows an h1 is BODY, not the
   larger body-heading treatment.
   ============================================================ */
/* h1 weight 500 -> 600 (2026-09-08, owner: "make h1 600 too so they match").
   h2 had gone to 600 to separate headings from body; leaving h1 at 500 made a
   section heading heavier than the page title above it. Both are 600 now, so
   the two heading levels are one style differing only in alignment and margins
   -- h1 centred with text-wrap:balance, h2 left. Same 600-not-700 constraint:
   Fraunces is loaded at 400/500/600 only. */
/* ---------------------------------------------------------------------------
   TYPE SCALE, rebuilt 2026-09-09 after a three-designer review (layout, UX/UI,
   art direction) working independently from screenshots.

   What was wrong, measured rather than judged: the heading and the body were
   BOTH 23px, and the largest text on a deck card was the takeaway at 26-28.
   There was no size hierarchy at all -- rank was carried only by typeface and
   colour, which is why every card read as a flat slide. All three reviews
   diagnosed it independently.

   The ladder is now  heading 25  >  takeaway 22  >  body 17  >  small 15.

   Why 25 and not the 27-28 all three asked for: at 27 the app's longest
   heading ("You're halfway through something and your mind is elsewhere.",
   60 chars) breaks from three lines to four, +45px on a card that has no room
   for it. 25 keeps every existing heading at three lines and still takes the
   heading:body ratio from 1.00 to 1.47. Going higher is a COPY job first --
   the designers' own rule is that a long title never shrinks, it splits into a
   short header plus a subheader. That pass has not been done.
   --------------------------------------------------------------------------- */
/* LEFT SPINE, 2026-09-09. Headings were centred while the body, the hook, the
   lists and every control under them were left-aligned, so a single card
   carried two axes and the eye re-found the start of the line on every tap.
   Six designers across two independent panels named this, unprompted, as the
   single change they would make. The owner then sent a card he disliked; its
   most visible fault was exactly this -- a centred h2 over a left-aligned list,
   control and hint.
   TWO deliberate exceptions survive, and they are objects rather than prose:
     .takeaway (below) -- the standalone italic payoff card; and
     the COVER TITLE, .pane:has(.cover-title) h1, in each deck's own style
     block -- the deck's NAME, which is a title plate, not a line to read
     along. Verified 2026-09-10 across all 34 decks; recorded here because a
     reviewer flagged it as a spine violation, which it is not. Everything
     else on the cover -- hook, summary -- stays on the spine. */
h1{max-width:100%;font-family:var(--serif);font-weight:600;font-size:25px;
   line-height:1.25;letter-spacing:normal;margin:0 0 14px;color:var(--ink);
   text-align:left;align-self:flex-start;text-wrap:balance}

/* ================= CONSOLIDATED FROM THE 29 DECKS (2026-09-08) =================
   Each of these was repeated identically in all 29 deck files. Verified before
   moving: exactly one occurrence per deck, byte-identical after whitespace
   normalisation, and not already declared anywhere in this file.

   NOT moved, and why -- so nobody retries them without re-measuring:
     .wm-breath-tap   3 variants, 2 occurrences per deck, already in this file
     .mda-nav-item    2 variants, 2 occurrences per deck, already in this file
     .nav-ico         2 variants, already in this file
     .opt / .wm-lobe  2 variants each
     (.quote-text WAS here -- moved into this file 2026-09-11 once it was
      verified that the same 7 decks declare it AND are the only ones that
      build the element, so nothing gains a rule it never had.)
     .wm-arc-track    only 4 of 29
   A rule that only some decks carry, or that differs between them, changes
   pages that never had it.

   These now load BEFORE each deck's own <style>, so any deck rule of equal
   specificity would win over them. Checked: none of the 29 redeclares these.
   ============================================================================ */
/* h2 28px -> 23px (2026-09-08, owner). Follows h1 down to subheading size so
   the scale stops reading backwards: h1 had been reduced to 23 while h2 stayed
   28, which made a page title smaller than the headings under it. Every
   heading in the app is now 23px; what separates a heading from body copy is
   the FAMILY and WEIGHT (Fraunces 500 against Inter 400), not the size. */
/* h2 weight 500 -> 600 (2026-09-08, owner: "make h2 bolder so it reads as a
   heading"). With every size flattened to 23px, weight is the only thing left
   separating a heading from body copy -- and Inter's larger x-height meant a
   500-weight serif heading was reading SMALLER than the 400-weight sans
   paragraph under it.
   600, not 700: the Fraunces web font is loaded at 400/500/600 only, so 700
   would be synthesised by the browser -- faux bold, which is worse than the
   problem it fixes. If 700 is ever wanted, the font URL has to add it first. */
/* Centred 2026-09-08 (owner: "centre the deck header label / sub-header").
   The eyebrow and h1 were already centred; h2 was the one part of the header
   block still ranged left, so a card with an eyebrow over an h2 read as two
   different alignments stacked. align-self is needed as well as text-align:
   .pane is a flex column with align-items:flex-start, so a block-level h2
   shrinks to its content width and text-align alone has nothing to act on --
   the same reason .takeaway carries both. */
h2{max-width:100%;font-family:var(--serif);font-weight:600;font-size:25px;line-height:1.2;margin:0 0 20px;color:var(--ink);text-align:left;align-self:flex-start;text-wrap:balance}
.takeaway{font-family:var(--serif);font-style:italic;font-size:var(--takeaway-size);line-height:1.35;color:var(--ink);margin:14px 0 0;max-width:26em;text-align:center;align-self:center}
/* .quote-text -- the Quote card's payoff line, consolidated here 2026-09-11.
   It sat in 7 of the 34 decks as Inter italic 26 while _dev/VISUAL_SYSTEM.md §4's
   type table gives takeaway/quote as SERIF italic 22. The owner settled it in the
   table's favour, so the shipped 26 was drift. Moving it here also removes the
   last reason a Quote card could differ deck to deck.
   It is safe to live here even though only 7 decks carry the CARD: the same 7 are
   the only files that build an element with the class -- verified 2026-09-11 -- so
   the other 27 gain a rule for an element they do not have. (The note at line ~613
   listing .quote-text under "NOT moved" was written before that check and is now
   wrong; it has been corrected there.)
   align-self:center is not optional: .pane is a flex column, so text-align alone
   does nothing -- §5b, caught three times in one day. */
.quote-text{font-family:var(--serif);font-style:italic;font-size:var(--takeaway-size);line-height:1.35;color:var(--ink);margin:0;max-width:26em;text-align:center;align-self:center}
/* The One Breath line -- consolidated 2026-09-11 (design-system pass). This exact
   rule sat in 24 of the 34 decks and in none of the other ten, so the breath card
   on introduction/coming-back rendered its line at 22px while every other deck
   showed 26. The modifier exists so a future edit to .takeaway's alignment is made
   once; that argument applies to the size too. --takeaway-breath is declared in
   every deck's :root (26px). */
.takeaway.breath{font-size:var(--takeaway-breath);line-height:1.4}
/* ---------------------------------------------------------------------------
   TEXT SITS ON THE SPINE. FIGURES CENTRE.  (settled 2026-09-09)

   _dev/CARD_RULES.md 3f put every text element and in-content control on the
   x=27 spine. Figures are the stated exception, and they were already the
   exception in practice: .wm-breath and .wm-open centre their contents, and
   .wm-steps centres internally. When .wm-check and .wm-ask were added they were
   set flush left, which split the figures 3-centred / 2-left -- an
   inconsistency introduced by the same pass that removed one.

   A figure is an object, not a line of type: centring it under left-aligned
   text is ordinary editorial practice and is what the app already did. So all
   five centre. Do not "correct" a figure onto the spine without moving all of
   them.
   --------------------------------------------------------------------------- */
/* ============= THE OPEN QUESTION (.wm-ask) — Reflection cards =============
   Added 2026-09-09 (owner: motion that fills the white space and "match what is
   being taught"). The Reflection card was the emptiest card in the app -- 298px
   of blank card under its last line -- and the only major card type with no
   graphic at all.

   WHAT IT MEANS, which is the whole reason it is allowed to exist: a Reflection
   ends by saying the question stays open and nothing has to be solved. So this
   is the one mark in the app that NEVER RESOLVES. The ring carries a permanent
   gap and turns slowly forever; it never becomes a closed loop. The dot at the
   centre is the question, and it does not move.

   Every other motion here completes and settles -- the close tick lands
   (.wm-pop/.wm-draw), the breath returns (.wm-breathe), the field settles
   (.wm-widen). This one deliberately does not, which is what makes it teach
   rather than decorate. If someone ever "fixes" it to close the ring, the card
   starts contradicting its own copy.

   28s is slow on purpose: at a glance it reads as still, and only a reader who
   rests on the card sees it move. It is NOT a control -- no ring treatment, no
   tap target, aria-hidden in the markup, and it must never become tappable.
   Reduced motion stops the rotation and leaves the open ring, which still says
   "unclosed" without moving. */
/* margin 36 -> 32 on 2026-09-11: the app's two closing figures were spaced by two
   different numbers for the same job (.wm-check is 32), and 36 is off _dev/CARD_RULES.md
   §6's 8/16/24/32 scale with no reason written beside it. The 31 Reflection cards each
   gain 4px of clearance; none was near the floor. */
.wm-ask{width:120px;height:120px;margin:32px 0 0;overflow:visible;flex-shrink:0;align-self:center}
.wm-ask-ring{fill:none;stroke:var(--spot-takeaway);stroke-width:2.4;stroke-linecap:round;
  stroke-dasharray:214 75;transform-origin:60px 60px;
  animation:wm-ask-turn 28s linear infinite}
.wm-ask-dot{fill:var(--ink);stroke:none}
@keyframes wm-ask-turn{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){ .wm-ask-ring{animation:none} }

/* =================== THE CLOSE MARK (.wm-check) ===================
   The one graphic every deck ends on. Consolidated here 2026-09-09; before
   that it was defined per-deck and had drifted into THREE renderings:
     24 decks  40x40   (--spot-takeaway / --spot-tint)
      4 decks  64x64   (--wm-accent / --wm-tint, tokens only that family has)
      6 decks  NO RULE at all -- the five Introduction decks and First Breath,
               all generated from a shell that never carried one, so the SVG
               fell back to its intrinsic size and rendered 178x178. Four times
               the intended mark, on the first deck a new reader finishes.
   Now one rule, one size, one pair of tokens (--spot-takeaway/--spot-tint are
   defined in every family; --wm-accent is not, which is why it is not used).

   SIZE 96, not 40 (owner, 2026-09-09: motion graphics should "add for the white
   space ... use the same one every time to show that is the closed side"). The
   close card ends its text around y=292 and the floor is 569, so a 40px tick
   sat marooned above ~277px of empty card. At 96 with a 32px gap it occupies
   that space deliberately and becomes the app's visual full stop -- the same
   mark, same motion, every deck, which is what makes it read as "this is the
   end" rather than as decoration.

   It already animates and needs nothing new: the circle carries .wm-pop and the
   check .wm-draw (see the keyframes above), so it draws itself closed on
   arrival. Both respect prefers-reduced-motion via the shared media query. */



/* =================== THE INTERACTION CONTAINER ===================
   Consolidated here 2026-09-11. It lived in 26 deck files under TWO names --
   .trigger-cta in the 22 in-app decks, .cta-wrap in the 4 work-course decks --
   byte-identical in both, flex:1 and the centring included. This is the rule that
   composes 27 interaction cards, and it was declared nowhere shared.

   BOTH names are kept for one pass so no deck's markup has to change in the same
   edit as its CSS. Retire .cta-wrap when its four files are next touched.

   flex:1 STAYS. It distributes the leftover space, which _dev/CARD_RULES.md §3e
   warns about in general -- but here it is the corpus's own behaviour on all 27
   cards, not a work-tier deviation, so removing it from four files would break the
   component rather than fix it. Where a card looks like it floats (leadership#2's
   68px gap) the cause is that card having less copy above the control, and the fix
   is that card's content, not this rule.

   The work tier's extra `.pane .cta-wrap{margin-top:16px}` is NOT carried over:
   the same component was spaced differently by tier for no recorded reason, and
   _dev/STYLE_GUIDE.md §6 scopes that tier's deliberate differences to deck length
   and per-row colour, not to this. One component, one spacing. */
.cta-wrap{width:100%;flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px}

/* =================== THE STEPS RAIL (.wm-step*) ===================
   Consolidated here 2026-09-11. It lived in 28 deck files -- the largest single
   block of duplicated geometry in the corpus -- while this sheet declared none of
   it and only mentioned the component in a comment. Six rules, byte-identical in
   24 of the 28; the other four differed ONLY by the alias --wm-accent, which
   resolves to --spot-takeaway, so nothing changes on screen.

   text-align:center on the label is NEW and is the point of doing this now:
   .wm-step centres the label BOX, but the label set no text-align, so a label that
   WRAPS ranged left inside a box grown to the cell width and sat visibly off-centre
   under its own icon (commute#6 and laptop#6). The 26 single-line rails are
   unaffected. Do not "fix" that by shortening the two labels.

   The label is sans 15 w600 -- a control-label role _dev/VISUAL_SYSTEM.md §4's
   table did not carry until this date. 84 labels agree on it; the table was
   incomplete, the code had not drifted. */
.wm-steps{display:flex;justify-content:space-between;gap:8px;width:100%;margin:auto 0;padding-top:24px;pointer-events:none}
.wm-step{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:8px}
.wm-step-ico{width:56px;height:34px;display:flex;align-items:center;justify-content:center;color:var(--spot-takeaway)}
.wm-step-ico svg{width:30px;height:30px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.wm-step:not(:first-child)::before{content:"";position:absolute;top:16px;right:calc(50% + 26px);width:calc(100% - 52px);height:1.5px;background:var(--line)}
.wm-step-lbl{font:600 15px/1.2 var(--sans);color:var(--ink);text-align:center}

.wm-check{width:96px;height:96px;margin:32px 0 0;overflow:visible;flex-shrink:0;align-self:center}
.wm-check path{fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
/* The box: ink, like the structure line in every deck mark. Its own path is
   deliberately imperfect -- the four corners do not sit on a true rectangle --
   so it reads as drawn rather than printed, but it is a CLOSED shape: closed
   2026-09-13 (owner: "one side of the check box is missing"), corpus-wide
   across all 34 decks. It had shipped open at the top right since the
   2026-09-09 consolidation, read as a genuine missing side rather than a hand-
   drawn imperfection. The fourth stroke returns to a point near, not exactly
   on, the start (44.7,18.5 vs the opening 45,17), which keeps the wonk without
   leaving a visible gap. Do not reopen it. */
.wm-check .wm-box{stroke:var(--ink);opacity:.55}
/* The tick: green, the same green everywhere (owner, 2026-09-09 -- this mark is
   the app's full stop and must not change colour deck to deck, so it does NOT
   take --spot-takeaway). It starts inside the box and OVERSHOOTS the top-right
   corner, which is what a real hand-drawn check does and what stops it looking
   like a UI glyph. --teal is #3D7A68 and is defined in all 34 decks. */
.wm-check .wm-tick{stroke:var(--teal,#3D7A68)}

.nav-lbl{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---------------------------------------------------------------------------
   Close, 2026-09-09 (owner: "the x button close in the top right", and earlier
   "the next button's needed in a course to get out of the course without having
   to click back a thousand times").

   Top-RIGHT, and deliberately not a second back arrow top-left: the bottom-left
   chevron already means "previous card", and putting a back mark in both places
   would rebuild the duplication this pass removed. One leave-control per screen,
   ringed; in-deck movement stays bare. Hub-pages get the ringed back arrow;
   decks get this.

   NO RING as of 2026-09-09 (owner: "the x at the top right and the back at the
   top left don't need the circles on them ... just use the x and the chevron by
   itself"). The 44x44 box, the tap target, is unchanged -- only the circle,
   border, card background and shadow are gone, here and in the .hub-close /
   .back-arrow copies on the six hub pages that carry their own. This RETIRES
   the old "bare mark means movement, a ring means leaving" rule; position and
   glyph now carry that distinction on their own. Do not reinstate the ring to
   restore it. Sits on the header line (34-78), which measurement showed is clear:
   the widest eyebrow in the app ("Spot the difference") spans x 87-256 and this
   occupies x 272-316, so it clears by 16px, and a walk of inbox, leadership,
   dinner and scroll found no card content in that band at all. That is why the
   decks did NOT need the hub-pages' 71 -> 83 anchor shift.
   --------------------------------------------------------------------------- */
.deck-close{position:absolute;top:calc(44px + env(safe-area-inset-top));right:16px;z-index:6;
  width:44px;height:44px;background:none;border:0;display:flex;align-items:center;justify-content:center;
  padding:0;cursor:pointer;color:var(--ink);opacity:.85;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
  transition:opacity .15s ease}
.deck-close svg{width:19px;height:19px}
.deck-close:hover{opacity:1}
.deck-close:active{opacity:.55}
.deck-close:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
