/* site/header/button-chrome.css
   The .btn/.face/.fill "rise" button system, copied verbatim from
   site/index.html (search "THE FRAME") so site/header/'s own buttons can
   use the real design system instead of unstyled defaults. This is a
   COPY, not a shared import — site/index.html's own copy is untouched and
   keeps working exactly as it does today. Every --u reference below
   resolves against site/header/tokens.css's `--u: var(--hu)` alias
   (added alongside this file), NOT the sitewide fluid --u, which is what
   keeps this chrome from re-introducing viewport-scaling into the
   header/footer. If site/index.html's button CSS is ever redesigned,
   this file needs the same update applied by hand — there is no
   automatic sync between the two copies. */

/* WHAT WAS LEFT BEHIND, and why (site/index.html keeps all of it):
   - `.rise-scope` (the container-query --u) — that IS the viewport
     scaling this scope deliberately does not want; tokens.css's
     `--u: var(--hu)` alias replaces it here.
   - the shape system (`.btn[data-shape]`, `.shape-svg`, `.shape-rotate-btn`)
     — needs shapes.js, which site/header/ does not load. The
     `var(--shape-clip, none)` / `var(--shape-rot, 0deg)` fallbacks below
     are what make its absence a no-op.
   - earned-state colours (`.is-selected` / `.is-complete` / `.is-danger`)
     and `.soft-lock` — features of the homepage's own wizard, not of the
     header/footer. `.is-quiet` / `.is-line` ARE kept: they are plain look
     variants. `.is-accent` / `.is-cream` were ALSO left behind by the
     original extraction and have since been pulled in (addendum §10.1) —
     they are what every header/footer button actually uses, not
     `.is-line`; see the rule near the bottom of this file.
   - the `.value-card` / `.container` card rules and the hero's
     button variants — page furniture, not button chrome.
   Everything else below is byte-identical to site/index.html, in the same
   source order (order matters: e.g. `.btn.sm`'s padding is deliberately
   overridden by the later `.btn.sq` rule at equal specificity). */

/* ---------------------------------------------------------------
   --v REGISTRATION (round 6, item 4). Plain custom properties jump
   instantly between values — only properties that consume them via
   calc() (like `transform`, when IT carries its own `transition`) ever
   visibly ease. That makes "where is --v RIGHT NOW, mid-animation"
   unanswerable from outside, which is exactly what the sink z-order fix
   needs (MEASURED fact 7: z-index doesn't interpolate, so JS has to
   stamp a class at the moment --v crosses zero — see lab.js). Registering
   --v with a typed syntax makes it a first-class animatable value: a
   CSS transition on --v itself now genuinely interpolates, frame by
   frame, and every dependent calc() (--lift, --push, and everything
   built from them) recomputes reactively in step — see .btn below,
   where --v's transition lives, and the STACKING v2 note further down.
   ------------------------------------------------------------- */
@property --v {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* site/index.html's own `:root` token block, copied verbatim. It has to
   come along: nothing else in site/header/ defines --sw, --offset, --dur,
   --ease, --stroke, --ground, --fill-default … and a `background:
   var(--edge)` whose --stroke is undefined is invalid at computed-value
   time, i.e. the whole chrome would paint nothing. VERIFIED after the
   copy: all 32 tokens the rules below read resolve to real values inside
   .bb-header-scope, none empty. Two of these are deliberately shadowed
   for the header/footer by tokens.css's `.bb-header-scope` — --u (by --hu)
   and --sw (by --hu-sw). That works by INHERITANCE, not specificity: the
   header element is closer than :root, so its own declaration wins for
   the whole subtree regardless of link order. */
:root {
  --u: 1.6rem; /* safety net if nothing ever establishes a .rise-scope container */
  --sw: 3px;
  --radius: 0px;
  --offset: 6px;
  --dur: 200ms;
  --dur-press: 70ms;
  --ease: cubic-bezier(.22, .68, .24, 1);

  /* the one signed scale + direction, per-button overridable */
  --rise-x: -1;
  --rise-y: -1;
  --v-rest: 0;
  --v-hover: .75;
  --v-press: -.25;

  /* theme: Light (default) */
  --ground: #FAF0D8;
  --panel: #FFF8E6;
  --panel-2: #F3E7CC;
  --stroke: #201A12;
  --text: #201A12;
  --muted: #6E5836;
  --faint: #9A8355;
  --accent: #FED700;
  --on-accent: #201A12;
  --ink: #201A12;
  --cream: #FFF8E6;
  --ok: #3F8A4C;
  --danger: #B33C24;

  /* round 6 — DARK MODE, A REAL DESIGN PASS. This indirection is the
     mechanism the redesign hangs on: --fill-default/--fg-default are
     what an ORDINARY (no state class) button's fill/text resolve to —
     see .btn's --fill/--fg below, which read these instead of hardcoding
     var(--accent)/var(--on-accent) directly. Light keeps the original,
     unchanged behaviour (every plain button is BB yellow with ink text —
     nothing here regresses light mode, byte for byte the same result).
     Dark overrides ONLY these two, right below, which is the entire
     redesign: no new colour VALUES anywhere, only where the existing
     ones get used. See the [data-theme="dark"] block for the reasoning. */
  --fill-default: var(--accent);
  --fg-default: var(--on-accent);

  /* Lexend everywhere (see font.css for the @font-face), mono reserved
     for things that specifically need it — code, technical readouts.
     Round 1/2 had button labels in mono for a "kit" look; round 3
     drops that in favour of the brief's explicit rule. */
  --font-body: 'Lexend', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
}

/* Dark theme token block (the long design-rationale comment that
   accompanies it in site/index.html is not reproduced here). Inert until
   something sets data-theme="dark"; kept so this file is self-sufficient
   for any page that does. */
[data-theme="dark"] {
  --ground: #201A12;
  --panel: #2A2318;
  --panel-2: #322818;
  --stroke: #F3E0BC;
  --text: #F3E0BC;
  --muted: #D8B987;
  --faint: #A0824B;
  --cream: #FFF3D2;
  --ok: #7BC47F;
  --danger: #E0654A;
  /* --accent / --on-accent / --ink stay put on purpose: the accent
     yellow and the ink it sits on are brand colour, not surface. */

  --fill-default: var(--panel-2);
  --fg-default: var(--text);
}

/* ============================================================
   THE FRAME
   ============================================================ */
.btn {
  position: relative;
  display: inline-block;
  line-height: 0;
  margin: 0; padding: 0; border: 0;
  background: none;
  cursor: pointer;
  appearance: none;
  font: inherit;
  vertical-align: top;
  overflow: visible;
  /* Round 6 (docs/ROUND6-BRIEF.md item 2) — a `<button>` never carries a browser
     underline, but a `.btn` built on an `<a>` (site/p/index.html's back/BrickLink/
     Rebrickable/card links, e.g.) does, and inherits the browser's link blue unless
     something overrides it. Only site/front/front.css's own `a.btn` rule did that,
     so any page that links this stylesheet without also linking front.css — the
     piece page is exactly that case — shipped underlined, blue-tinted "buttons".
     Moved here, onto `.btn` itself, so every consumer of this file gets it for
     free regardless of which other stylesheets it happens to load; nothing in the
     system relies on that underline existing (it was never a designed treatment,
     just a missing reset), so this is a pure bug fix, not a style change anyone
     depended on. front.css's own `a.btn` rule becomes a harmless duplicate. */
  text-decoration: none;
  color: inherit;

  /* state tokens live here (not on .face) so both the footprint AND
     the face/fill can read them — see the Bug 2 footprint-colour note.
     round 6: the no-state fallback now goes through --fill-default /
     --fg-default rather than hardcoding var(--accent)/var(--on-accent)
     directly — that indirection is what lets dark mode redefine what an
     ORDINARY button looks like without touching what a genuinely
     SELECTED one looks like (.is-selected still sets --fill-rest to
     var(--accent) explicitly, in both themes — see the dark-mode block
     above for why that one stays put). Light mode's fallback still
     resolves to --accent/--on-accent, so this is a no-op there. */
  --fill: var(--fill-rest, var(--fill-default));
  --fg: var(--fg-rest, var(--fg-default));
  --edge: var(--edge-rest, var(--stroke));

  --v: var(--v-rest);
  /* round 6, item 4: --v is registered via @property (top of file) with
     syntax:"<number>", which is what makes it a genuinely INTERPOLATING,
     sampleable value instead of one that jumps instantly while only its
     dependants (--lift, transform) visibly ease — see the STACKING v2
     note further down for why lab.js needs exactly that to fix the sink
     z-order bug. transitioning --v HERE, and nowhere else, is also why
     .face/.fill::before/.lab no longer carry their own `transition:
     transform|box-shadow ...` below: --lift/--push are calc()s of --v,
     so once --v itself eases smoothly, every dependant recomputes in
     lockstep, every frame, for free. Giving transform its OWN transition
     on top of an already-easing input would double-ease (the dependant
     transitions chasing an already-moving target) — MEASURED via
     Animation.currentTime scrubbing (this tab's document.hidden freezes
     both the compositor AND requestAnimationFrame, so real-time timing
     can't be observed directly, but the interpolation and every
     depending calc() were confirmed exact at 0/25/50/75/100% of a
     transition scrubbed programmatically). */
  transition: --v var(--dur) var(--ease);
  /* outward travel — only the positive half of the scale */
  --lift: calc(var(--offset) * max(0, var(--v)));
  /* inward depth — only the negative half, made positive */
  --push: calc(var(--offset) * max(0, calc(var(--v) * -1)));
}
.btn:hover { --v: var(--v-hover); }
.btn:active { --v: var(--v-press); transition-duration: var(--dur-press); }
.btn.block { display: block; width: 100%; }

/* THE FOOTPRINT — fact 2/3: exactly the frame box, no spread. Uses
   --edge (not a hardcoded --stroke) so a state-coloured button's
   shadow matches its edge instead of always reading as plain ink. */
.btn::before {
  content: "";
  position: absolute; inset: 0; z-index: 0;
  background: var(--edge);
  clip-path: var(--shape-clip, none);
  border-radius: var(--radius);
  pointer-events: none;
}

/* THE HIT-AREA EXTENDER — fact 6. Deliberately NOT clipped by
   --shape-clip: it has to be free to grow past the shape's
   silhouette on hover, or a shaped button reintroduces the vibration
   bug this exists to prevent. */
.btn::after {
  content: "";
  position: absolute; inset: 0; z-index: 3;
  pointer-events: none;
}
.btn:hover::after {
  pointer-events: auto;
  top:    calc(var(--lift) * max(0, calc(var(--rise-y) * -1)) * -1);
  bottom: calc(var(--lift) * max(0, var(--rise-y)) * -1);
  left:   calc(var(--lift) * max(0, calc(var(--rise-x) * -1)) * -1);
  right:  calc(var(--lift) * max(0, var(--rise-x)) * -1);
}

/* ============================================================
   THE FACE — the edge box. Plain background, no ring: painting the
   edge and painting the fill are now two different elements, so
   there is nothing left for a fractional-DPR rounding mismatch to
   happen BETWEEN (Bug 1).
   ============================================================ */
.btn > .face {
  position: relative; z-index: 1;
  /* align-items:stretch (the flex default — round 2 had left it at
     "center", found by measurement to be the cause of icon buttons and
     the flush row's short labels leaving an empty band top/bottom: a
     flex child with no grow/stretch takes only its OWN content size,
     so .fill never actually reached .face's edges on anything shorter
     than its own natural height). Stretch makes .fill's size come from
     .face's box, not from whatever happens to be inside it. */
  display: flex; align-items: stretch; justify-content: center;
  width: 100%; height: 100%; box-sizing: border-box;
  border: 0; border-radius: var(--radius);
  background: var(--edge);
  clip-path: var(--shape-clip, none);

  /* round 6: no `transition` of its own any more — `transform` is a pure
     calc() of --lift, and --lift is a pure calc() of --v, which now
     carries the ONLY transition in this chain (on .btn — see above). A
     second, independent transition here would retarget every frame as
     --v eased (a moving target), double-easing the motion; removing it
     makes rise/sink purely reactive to the one thing that's actually
     animating. Shape rotation (`rotate(var(--shape-rot))`, appended to
     this same property for [data-shape] buttons) loses its own smooth
     spin as a result — instant now instead of eased — a deliberate,
     minor trade-off flagged in the round-6 report, not an oversight. */
  transform: translate(calc(var(--lift) * var(--rise-x)), calc(var(--lift) * var(--rise-y)));
}
/* hover/press promotion targets .face ONLY — see the STACKING note
   in the header comment. .btn itself must never receive a z-index.
   round 6: promotion now has a THIRD level, not just on/off — see the
   STACKING v2 note below THE ONE's z-classes for the full reasoning.
   z-top (risen, promoted) and z-under (sunk, demoted) are stamped from
   JS by lab.js, which samples the SIGN of the live, now-interpolating
   --v every frame while its transition is in flight. z-under sits at
   z-index:0 rather than something negative — tied with the footprint's
   own z-index:0, but DOM order (face always follows ::before in the
   same element) still resolves that tie in the face's favour, so a
   sunk button never disappears behind its own shadow; it just can no
   longer outrank a resting neighbour's face (z-index:1).

   round 8: a FOURTH level, z-settle (4), added for the button lab.js is
   fixing. Round 7 held z-top for the whole ~240ms return-to-rest window
   after the pointer left, so a fast sweep across the grid left several
   buttons all sitting at z-index 5 at once — a genuine tie, resolved by
   DOM order, which is exactly the "later in the grid wins" bug the user
   reported. z-settle sits strictly between resting (0/1) and the single
   current z-top (5) so a button that's still easing back to rest stays
   above its resting neighbours without ever being able to tie the
   button actually under the pointer. Deliberately NOT 3, to avoid a new
   tie with the (invisible, paints nothing) hit-extender's z-index:3. */
.btn.z-top > .face { z-index: 5; }
.btn.z-settle > .face { z-index: 4; }
.btn.z-under > .face { z-index: 0; }

/* ============================================================
   THE FILL — a real, physically smaller box, centred in .face.
   Rectangles get there with a plain CSS margin (fully reactive to
   --sw, zero extra machinery). Shapes get an inline clip-path
   computed by shapes.js from a true polygon inset, because a
   percentage margin can't express "smaller by a constant number of
   pixels" on a non-rectangular path.
   ============================================================ */
.btn > .face > .fill {
  position: relative; z-index: 1;
  flex: 1 1 auto; /* along with .face's align-items:stretch above, this is
    what makes the fill region come from the BUTTON's box rather than
    its content — a flex child with neither grow nor stretch only ever
    takes its content's own size, which is the round-3 "fill doesn't
    reach the edges" bug. */
  /* round 5: flex items default to min-width/min-height:auto, which
     resolves to the MIN-CONTENT size (here, the label's own width) — so
     without an explicit override .fill refused to shrink below its own
     text at small button sizes, MEASURED stuck at 103.8px regardless of
     how small .face's box got, spilling out over the edge (the edge
     "disappearing" was the fill painting over it) and appearing not to
     "scale back" when grown again since it had never actually been
     tracking the button's box in the first place. min-width/min-height:0
     lets .fill's flex-basis/grow math (the thing that's supposed to size
     it from the button's box, per brief 3 §6) actually win over content
     size; overflow:hidden is the backstop brief 5 also asked for — if
     content ever still doesn't fit, it clips instead of spilling past
     the edge. */
  min-width: 0; min-height: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  margin: var(--sw);
  box-sizing: border-box;
  border-radius: max(0px, calc(var(--radius) - var(--sw)));
  background: var(--fill);
  color: var(--fg);

  font-family: var(--font-body); font-weight: 700; letter-spacing: .02em;
  text-transform: none; white-space: nowrap;
  font-size: calc(var(--u) * .36);
  /* round 5, second finding (not in the brief — found by measuring past
     it): min-width/min-height:0 fixes the flex min-CONTENT floor, but
     padding is a fixed length, not a content size — `border-box` height
     can never go below its own padding-top + padding-bottom (nor width
     below padding-left + padding-right), no matter what flex-shrink or
     min-*:0 say, because CSS won't render a negative content box. At
     --u's ceiling (2.75rem), padding alone is ~37px block / ~62px
     inline — bigger than a 1-grid-cell button's entire 35px box, so the
     residual overflow just moved from "text" to "padding" at the two
     smallest snap-test sizes (measured: 2x1 face 67x35 vs fill
     61.58x36.96; 1x1 face 35x35 vs fill 61.58x36.96 — same fill size
     both times, because padding, not content, was now the floor).
     Fix: cap padding by the button's own actual box instead of only by
     --u. grid.js writes --box-w/--box-h (the exact pixel box it already
     computes for every grid-placed button) as custom properties on the
     element; a button that isn't grid-placed never defines them, so the
     huge fallback (9999px) makes min() a no-op and behaviour is
     unchanged everywhere else. */
  padding-block: min(calc(var(--u) * .42), max(0px, calc((var(--box-h, 9999px) - var(--sw) * 2 - 1.2em) / 2)));
  padding-inline: min(calc(var(--u) * .7), max(0px, calc((var(--box-w, 9999px) - var(--sw) * 2 - 1.2em) / 2)));
}

/* THE SINK LIP — facts 5, 9. Lives on .fill now (it needs to read
   against the FILL colour, not against the edge colour it would be
   nearly invisible on).
   Round 3 fix: the lip used to default to --ink, which never flips
   with the theme — in dark mode a sunk button's lip stayed dark
   ink-on-dark instead of following the light stroke colour everything
   else uses. It now defaults to --edge, the SAME token the outline and
   the footprint read from (declared once on .btn), so outline,
   footprint and lip cannot drift apart from the theme, or from each
   other, again. --lip remains a distinct token in case a future state
   ever needs to override it — nothing sets it today. */
.btn > .face > .fill::before {
  content: "";
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  border-radius: inherit;
  box-shadow: inset calc(var(--push) * var(--rise-x) * -1) calc(var(--push) * var(--rise-y) * -1)
                     0 0 var(--lip, var(--edge));
  /* round 6: no transition of its own — see the note on .btn's --v
     transition above. --push is a calc() of --v, same as --lift, so the
     lip already eases in lockstep with the rise/sink translate; giving
     box-shadow its own separate transition here would be the identical
     double-easing risk. */
}

/* THE LABEL — travels inward on sink so depth still reads. */
.btn > .face > .fill > .lab {
  position: relative; z-index: 1;
  /* .lab is itself a flex item of .fill (which is display:flex) — same
     min-content trap as .fill described above, one level deeper (an icon
     + text label won't shrink below ITS OWN content either unless told
     to). min-width:0 here is what actually lets .fill's overflow:hidden
     backstop matter: without it, .lab would hold its full content size
     and .fill would just clip a label that never got smaller. */
  min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: .5em;
  /* round 6: no transition here either, same reasoning — .lab's push
     reacts to the same animating --v/--push chain as everything else. */
  transform: translate(calc(var(--push) * var(--rise-x) * -1), calc(var(--push) * var(--rise-y) * -1));
}
.btn > .face > .fill > .lab svg,
.icon {
  width: 1.1em; height: 1.1em; fill: none;
  stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}
/* round 9: MEASURED every icon button 4.01px too high (above 16.57px,
   below 20.58px), identical across every icon, ~0.01px off horizontally
   (noise, already within tolerance). Cause: an icon is authored as
   <i data-icon><svg class="icon">…</svg></i> — icons.js fills the <i>,
   never replaces it — so .lab (display:flex) blockifies its direct
   child, the <i>, not the <svg> sitting two levels down. The <i>
   becomes a block box, but the <svg> INSIDE it stays a plain inline
   replaced element in an ordinary anonymous line box, which reserves
   descender space below its baseline (the classic inline-image gap
   bug) — MEASURED: svg display:inline, vertical-align:baseline, line
   box at .lab's inherited 15.84px font-size/line-height:normal, ~0.25em
   descender ≈ 3.96px, matching the 4.01px almost exactly. CONFIRMED via
   the bare icon row, where the svg IS the flex container's direct
   child (no <i> in between) and is blockified for free: MEASURED
   above==below==0 there already, proof this is specific to the
   <i>-wrapped case, not the icon paths themselves.
   First fix tried and REJECTED by measurement: `display:block` on the
   svg. That does remove it from line-box layout (above/below balance
   to 21.573/21.583), but it also changes how the browser sizes the now
   block-level <i> as a flex item — MEASURED <i>'s width jump from
   ~31.6px (content-hugging) to the full 68.79px available in .fill,
   which broke horizontal centering (left 3px / right 40.156px, pinned
   to the edge instead of centred) on every non-grid .sq button. Fix
   used instead: `line-height: 0` on the <i> wrapper itself (below).
   That collapses the anonymous line box's strut to zero, so a
   vertical-align:baseline svg has nothing left to reserve underneath
   it, while leaving the <i>'s own WIDTH computation (and therefore
   .lab's centring) completely untouched — MEASURED above 21.573 / below
   21.583 (0.01px apart) AND left 21.573 / right 21.583 (0.01px apart),
   both within the 0.5px tolerance, on every icon button checked: the
   bare row (already 0/0, unaffected), the 5x5 icon grid, the button
   size grid's icon cells, and at both .sq default and .sq.sm/.sq.lg
   sizes. Text labels were already symmetric before this change (a text
   node is .lab's OWN direct flex child, no intervening inline wrapper)
   and this rule doesn't touch them — it's scoped to the icon's <i>
   wrapper only, not .lab itself, so .lab's line-height (inherited by
   any sibling text flex item) is untouched. */
.btn > .face > .fill > .lab > i {
  line-height: 0;
}

/* Logo/brickhead images inside a .lab slot — pulled from site/index.html
   (search "logo / brickhead images inside a .lab slot", around its own
   ".lab > img.logo-img" rule) AFTER the initial button-chrome.css
   extraction, which judged this brand/logo-specific and left it behind.
   It isn't: it's the same generic "size whatever's in .lab to fit the
   slot without distortion" mechanics as the icon svg/i rules just above,
   it just happens to size an <img> instead of an <svg>/<i>. Needed here
   because site/header/header.html's #logo-btn wraps its logo-img in a
   .lab (added alongside this rule, to match site/index.html's own
   markup) — without it the image has no sizing rule at all and computes
   to 0x0. Sized to fit the slot without distortion: the logo images this
   is used for have very different native aspect ratios and none of them
   should stretch. */
.lab > img.logo-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  pointer-events: none;
}

/* MEASURED, header-only addition the rule above doesn't need on
   site/index.html. There, every .lab > img.logo-img sits inside a
   grid-placed .sq button: .face/.fill/.lab all get an externally
   imposed pixel size before the image is ever considered, and the
   images themselves are raster PNG data-URIs with real intrinsic
   width/height — so "width:auto/height:auto + max-width/max-height:
   100%" has a real, definite box to resolve against and Just Works.
   #logo-btn is neither: it's a content-sized (non-.sq) button, and its
   image is /assets/bb-logo.svg, authored with only a viewBox (an
   aspect ratio, no intrinsic width/height). MEASURED live: a flex item
   (.lab) whose own main size is 'auto' (content-based), containing a
   replaced element ALSO sized by 'auto' constrained only by a
   percentage max-width/max-height (itself relative to that same
   indefinite ancestor), collapses to a hard 0x0 in this exact chain —
   confirmed by changing each variable independently: swap in a
   same-ratio raster image (still 0x0), give the image fixed px
   dimensions instead of the percentage (works), or give it real
   intrinsic width/height (renders, unclamped) — only the full
   combination (ratio-only intrinsic size + percentage constraint +
   auto-sized ancestor chain) is what breaks. Rather than requiring
   every future logo asset to carry explicit width/height attributes
   (easy to forget, and "content-sized non-.sq button" is exactly this
   header's own #logo-btn pattern, not an edge case), give the .lab
   that holds a logo image a genuinely definite box directly: pull it
   out of flex flow and pin it to .fill's own box instead. .fill's own
   size never actually depends on this image (MEASURED stable — padding
   alone — across every variant above) and is already `position:
   relative` (see .btn > .face > .fill), so `inset: 0` here resolves
   against a real, already-settled box instead of a circular one. */
.btn > .face > .fill > .lab:has(> img.logo-img) {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (prefers-reduced-motion: reduce) {
  /* round 6: --v's own transition (declared on .btn) is what actually
     drives all of rise/sink/lip/label now, so THIS is the one rule that
     has to be disabled for reduced motion to genuinely stop the
     animation — the child elements below no longer have transitions of
     their own to turn off, but are listed too in case a future change
     ever gives one of them a transition back. */
  .btn, .btn > .face, .btn > .face > .fill > .lab, .btn > .face > .fill::before { transition: none; }
}

/* ============================================================
   SIZES — every one a multiple of --u
   ============================================================ */
.btn.xs > .face > .fill { font-size: calc(var(--u) * .24); padding: calc(var(--u) * .26) calc(var(--u) * .5); }
.btn.sm > .face > .fill { font-size: calc(var(--u) * .28); padding: calc(var(--u) * .34) calc(var(--u) * .6); }
.btn.lg > .face > .fill { font-size: calc(var(--u) * .40); padding: calc(var(--u) * .50) calc(var(--u) * .85); }
.btn.xl > .face > .fill { font-size: calc(var(--u) * .46); padding: calc(var(--u) * .58) calc(var(--u) * 1); }

/* icon-only square — fixed footprint, no text to size it */
.btn.sq { width: calc(var(--u) * 1.7); height: calc(var(--u) * 1.7); }
.btn.sq > .face > .fill { padding: 0; }
.btn.sq > .face > .fill > .lab svg { width: 46%; height: 46%; }
.btn.sq.sm { width: calc(var(--u) * 1.3); height: calc(var(--u) * 1.3); }
.btn.sq.lg { width: calc(var(--u) * 2.2); height: calc(var(--u) * 2.2); }

/* ============================================================
   REAL STATES — the only things allowed to change colour. Not
   hover, not press: selected / complete / disabled / danger. Set on
   .btn (not .face) so the footprint/shadow picks up the same colour.
   ============================================================ */
/* Round 3 finding: is-complete and is-danger were setting --edge-rest,
   recolouring the outline (and, via it, the footprint) — the user
   flagged this as broken. Fixed rule now: earned state changes --fill
   and --fg only. --edge-rest exists as a token so a future state COULD
   theme it, but nothing here sets it — --edge always falls through to
   --stroke, the plain theme token, for every state below. */
/* (.is-selected / .is-complete / .is-danger left behind — homepage wizard
   states, see the "WHAT WAS LEFT BEHIND" note at the top of this file.) */
.btn.is-quiet { --fill-rest: var(--ground); --fg-rest: var(--muted); }
.btn.is-line { --fill-rest: var(--ground); --fg-rest: var(--text); }

/* ---- brand chrome fill tokens (addendum §10.1) -----------------------
   Copied VERBATIM from site/index.html (search "brand chrome fill
   tokens", its own `.btn.is-accent` / `.btn.is-cream` declarations).
   The original extraction skipped them because they live in
   site/index.html's *front*-chrome section rather than its button.css
   section, and the section header calls them "brand chrome" — but they
   are not page furniture: they are the two fill colours the REAL header
   and footer are built from (every icon button is `is-accent`, every
   text button is `is-cream`), so a copy of the button system that lacks
   them cannot reproduce the live header at all. Same shape as
   `.is-quiet`/`.is-line` directly above: set --fill-rest/--fg-rest and
   nothing else, so the outline, footprint and sink lip all still read
   from the one untouched --edge token. Deliberately NOT theme-switched
   (unlike --fill-default): BB yellow and cream are brand identity, not
   "ordinary button surface" — see site/index.html's own note. */
.btn.is-accent { --fill-rest: var(--accent); --fg-rest: var(--on-accent); }
.btn.is-cream { --fill-rest: var(--cream); --fg-rest: var(--ink); }

/* disabled: fact 10 — identical box, it just stops moving. */
.btn[disabled], .btn.is-disabled { pointer-events: none; }
.btn[disabled] > .face > .fill, .btn.is-disabled > .face > .fill { opacity: .42; }
.btn[disabled] > .face, .btn.is-disabled > .face,
.btn[disabled] > .face > .fill > .lab, .btn.is-disabled > .face > .fill > .lab,
.btn[disabled] > .face > .fill::before, .btn.is-disabled > .face > .fill::before { transform: none !important; }

/* ============================================================
   FLUSH ROWS — buttons that sit edge to edge and share one line.
   The overlap is exactly one stroke; nothing here ever changes
   the .btn box itself (only .face transforms), so a risen button
   and a sunk button can sit in the same row without ever breaking
   the shared line between them — see the brief's hardest case.
   ============================================================ */
.row-flush { display: inline-flex; align-items: stretch; max-width: 100%; }
.row-flush > .btn + .btn { margin-left: calc(var(--sw) * -1); }
