/* Page styles shared by index.html, tai-chinh.html and trader.html.
   Every colour and face below is a token. No raw values live in this file. */

*,*::before,*::after{box-sizing:border-box}
html,body{overflow-x:clip;margin:0}
/* iOS inflates text in landscape without this. */
html{scroll-behavior:smooth; -webkit-text-size-adjust:100%; text-size-adjust:100%}
body{
  background:var(--color-paper); color:var(--color-ink);
  font-family:var(--font-body); font-size:var(--text-base); line-height:1.6;
  -webkit-font-smoothing:antialiased;
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

/* There was no global link rule, so every unstyled link fell back to the browser's
   #0000EE - measured at 1.11:1 on the viahe paper, i.e. invisible. Accent was the
   obvious candidate and it failed too: 1.63:1 inside the warnstrip on thantai.
   currentColor inherits the surrounding text colour, which every zone has already
   had measured to AA, and the underline carries the affordance so a link never
   depends on hue at all (WCAG 1.4.1). */
a{
  color:currentColor;
  text-decoration:underline; text-underline-offset:2px;
  text-decoration-thickness:from-font;
}
a:hover{text-decoration-thickness:2px}
/* One focus ring for the whole page, deliberately two-tone. --color-focus is accent-2
   on three of the four skins, and the warning strip is filled with accent-2 as well, so
   a single themed ring measured 1:1 on that surface and vanished. The outer ring is
   currentColor, which every surface has already had measured to AA, so one of the two
   bands always clears the 3:1 WCAG 1.4.11 asks of a focus indicator. */
a:focus-visible,
.stamp:focus-visible,.stamp.is-focus,
.btn:focus-visible,
.cell:focus-visible,
form.khai input:focus-visible,form.khai select:focus-visible{
  outline:var(--rule-mid) solid var(--color-focus); outline-offset:1px;
  box-shadow:0 0 0 calc(var(--rule-mid) + 3px) currentColor;
}

/* --- grain: the page should feel printed, not rendered --- */
.grain{
  position:fixed; inset:0; z-index:var(--z-grain); pointer-events:none;
  opacity:var(--grain-opacity); mix-blend-mode:var(--grain-blend);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------------- warning: always-on strip + first-visit gate ---------------- */
/* The strip and the nav share one sticky wrapper. Making the nav sticky on its own
   would let it slide under the strip on scroll. */
.topbar{position:sticky; top:0; z-index:var(--z-nav); padding-top:env(safe-area-inset-top, 0)}
.warnstrip{
  background:var(--color-accent-2); color:var(--color-on-accent-2);
  font-size:var(--text-xs); font-weight:700; line-height:1.35;
  padding:var(--space-2xs) var(--space-md);
  border-bottom:var(--rule-mid) solid var(--color-rule);
  text-align:center; overflow-wrap:anywhere;
}
.warnstrip a{color:var(--color-on-accent-2); text-decoration:underline}

html.is-gated,html.is-gated body{overflow:hidden}
.gate{
  position:fixed; inset:0; z-index:var(--z-gate); display:grid; place-items:center;
  padding:var(--space-md); background:var(--color-paper);
  overflow-y:auto;
}
.gate__panel{
  max-width:60ch; width:100%;
  /* dvh, not vh: mobile address bars resize the viewport mid-scroll. */
  max-height:calc(100dvh - 2*var(--space-md)); overflow-y:auto;
  background:var(--color-paper-2); color:var(--color-ink);
  border:var(--rule-slab) solid var(--color-rule); border-radius:var(--radius);
  padding:var(--space-lg) var(--space-md);
}
.gate__eyebrow{
  font-family:var(--font-num); font-size:var(--text-xs); font-weight:700;
  letter-spacing:.16em; margin:0 0 var(--space-sm);
  background:var(--color-accent-2); color:var(--color-on-accent-2);
  display:inline-block; padding:var(--space-2xs) var(--space-xs);
}
.gate__title{
  font-family:var(--font-display); font-weight:400; font-style:normal;
  font-size:var(--text-xl); line-height:1.2; margin:0 0 var(--space-sm);
  overflow-wrap:anywhere; min-width:0;
}
.gate__lede{margin:0 0 var(--space-sm); color:var(--color-ink)}
.gate__list{margin:0 0 var(--space-md); padding-left:1.1em; color:var(--color-ink-dim);
  font-size:var(--text-sm)}
.gate__list li{margin-bottom:var(--space-2xs)}
.gate__actions{display:flex; flex-wrap:wrap; gap:var(--space-sm); align-items:center}
.gate__more{color:var(--color-ink-dim); font-size:var(--text-sm)}

/* ---------------- nav: N7 brutal slab ---------------- */
.slab{
  display:flex; flex-wrap:wrap; gap:var(--space-sm);
  align-items:center; justify-content:space-between;
  padding:var(--space-sm) var(--space-md);
  background:var(--color-paper);
  border-bottom:var(--rule-slab) solid var(--color-rule);
}
.slab__mark{
  font-family:var(--font-display); font-size:var(--text-xl);
  color:var(--color-ink); text-decoration:none; letter-spacing:.02em;
  /* The wordmark is the link home, so it is a real navigation target and owes the
     full 44px. Flex, not padding: padding would also widen the slab's left edge. */
  display:inline-flex; align-items:center; min-height:var(--tap);
  touch-action:manipulation;
}
.slab__themes{display:flex; gap:var(--space-2xs); flex-wrap:nowrap}

/* The page nav is body face, never more stamps. design.md's rule: the stamps are the skin
   picker and only the skin picker - three more bordered boxes here would read as seven
   skins. Three cues hold the groups apart and none of them is position: body face vs
   display face, no border vs a --rule-mid box on four sides, no rotation vs -4deg. */
.slab__pages{display:flex; flex-wrap:wrap; gap:var(--space-2xs) var(--space-md)}
.slab__to{
  font-family:var(--font-body); font-weight:800; font-size:var(--text-sm);
  color:var(--color-ink-dim); text-decoration:none;
  /* Same technique as .slab__mark, same reason: a real navigation target owes the full
     --tap, and flex gets there without padding widening the group's own left edge. It also
     lands all three underlines on one y, which is what makes them read as one group
     rather than three loose links - the footer's rule, reused. */
  display:inline-flex; align-items:center; min-height:var(--tap);
  border-bottom:var(--rule-hair) solid var(--color-rule);
  white-space:nowrap; touch-action:manipulation;
}
.slab__to:hover,.slab__to:focus-visible{color:var(--color-accent)}
/* Current page. The carrying cue is the rule going 1px -> 6px, a 6x step in the bar's own
   vocabulary; ink dim -> full is the SECOND cue, not the first. Colour alone would fail
   WCAG 1.4.1, and dim -> full is a luminance move, which 1.4.1 counts as colour too - so
   the weight has to be the one doing the work. Width alone in the override, not the
   shorthand: the shorthand would silently re-declare the colour and the style. */
.slab__to[aria-current="page"]{
  color:var(--color-ink);
  border-bottom-width:var(--rule-slab);
  cursor:default;
}

/* stamps: the theme picker is four rubber stamps, not a dropdown */
.stamp{
  font-family:var(--font-display); font-size:var(--text-xs);
  padding:var(--space-2xs) var(--space-xs);
  background:transparent; color:var(--color-ink);
  border:var(--rule-mid) solid var(--color-rule); border-radius:var(--radius);
  cursor:pointer; white-space:nowrap;
  min-height:var(--tap); touch-action:manipulation;
  transform:rotate(-2deg);
  transition:transform var(--dur-fast) var(--ease-out),
             background-color var(--dur-fast) var(--ease-out);
}
.stamp:hover,.stamp.is-hover{background:var(--color-paper-2); transform:rotate(0deg)}

.stamp:active,.stamp.is-active{transform:translateY(2px) rotate(-2deg)}
/* Selected stamp fills with accent-2 and takes the dark on-fill colour. Accent on
   paper only reaches 3.53:1 in veso, which is short of AA at this 12px size. */
.stamp[aria-checked="true"]{background:var(--color-accent-2); color:var(--color-on-accent-2);
  transform:rotate(0deg)}
.stamp[disabled]{opacity:.4; cursor:not-allowed}
.stamp__short{display:none}
@media (max-width:640px){
  .stamp__full{display:none}
  .stamp__short{display:inline}
  /* The label shrinks to one letter, so width has to be forced or the target collapses. */
  .stamp{padding:var(--space-2xs) var(--space-sm); min-width:var(--tap)}
}

/* ---------------- stages: Narrative Workflow ---------------- */
/* viewport-fit=cover means content can slide under a notch, so the insets are not
   optional once that meta is set. */
.wrap{max-width:1060px; margin:0 auto;
  padding-inline:max(var(--space-md), env(safe-area-inset-left), env(safe-area-inset-right))}
main.wrap{padding-bottom:var(--space-2xl)}
.masthead{padding-top:var(--space-xl)}
.masthead__title{font-family:var(--font-display); font-weight:400; font-style:normal;
  font-size:var(--text-display); line-height:var(--lh-display); margin:0 0 var(--space-sm);
  overflow-wrap:anywhere; min-width:0; color:var(--color-ink)}
.masthead__lede{font-family:var(--font-hand); font-size:var(--text-lg);
  color:var(--color-ink-dim); max-width:44ch; margin:0 0 var(--space-md)}
/* Not one padding for all five. 02 · PHÁN is the loud stage and gets air; 04 · SỰ THẬT is
   the cold-arithmetic stage where the fortune-teller goes silent, so it comes in tighter -
   the rhythm carries the joke the copy is making. */
.stage{padding-top:var(--space-2xl);
  /* --topbar-h is measured at runtime by warning.js; the fallback is the tallest
     the sticky wrapper gets, so a missed measurement over-clears rather than hides.
     204px -> 290px on 2026-09-22, when the two-page cross-link became a three-item page
     nav and the slab gained a row. Re-measured the same way the 204 was: across 14 widths
     x 4 skins x 3 pages, the tallest is y2k at 280px, where the warning strip needs three
     lines and the slab needs three rows. The runtime path self-corrects on every
     ResizeObserver tick; this literal does not, so a stale value here lands a #stage
     deep-link underneath the sticky bar. */
  scroll-margin-top:calc(var(--topbar-h, 290px) + var(--space-xs))}
/* Stage number ABOVE the title, same column. Never number-left / title-right. */
.stage__mark{
  font-family:var(--font-num); font-size:var(--text-sm); font-weight:700;
  color:var(--color-paper); background:var(--color-accent);
  display:inline-block; padding:var(--space-2xs) var(--space-xs);
  letter-spacing:.18em;
}
.stage__title{
  font-family:var(--font-display); font-weight:400; font-style:normal;
  font-size:var(--text-display-s); line-height:var(--lh-display); margin:var(--space-sm) 0 var(--space-xs);
  overflow-wrap:anywhere; min-width:0; color:var(--color-ink);
}
.stage__lede{color:var(--color-ink-dim); max-width:58ch; margin:0 0 var(--space-md)}
.stage + .stage{border-top:var(--rule-hair) solid var(--color-rule)}
/* The two stages that carry the page's joke get their own rhythm. */
#stage-02{padding-top:calc(var(--space-2xl) + var(--space-lg))}
#stage-04{padding-top:var(--space-xl)}

/* ---------------- blocks: uneven rules, never an even 1px box ---------------- */
.block{
  background:var(--color-paper-2);
  border-left:var(--rule-slab) solid var(--color-rule);
  border-top:var(--rule-hair) solid var(--color-rule);
  border-bottom:var(--rule-hair) solid var(--color-rule);
  border-radius:var(--radius);
  padding:var(--space-md); margin:0 0 var(--space-md);
}
.block--torn{position:relative; padding-bottom:var(--space-lg)}
/* torn ticket edge, built from a gradient - no image asset */
.block--torn::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:12px;
  background:repeating-linear-gradient(
    -45deg, var(--color-paper) 0 6px, transparent 6px 12px);
}
.row{display:flex; flex-wrap:wrap; gap:var(--space-md)}
.row > *{flex:1 1 320px; min-width:0}

/* A 10x10 heatmap needs a fixed width to keep its cells at --tap, and the default 320px
   basis does not say so - a third block in the row silently squeezed it to 280px. This
   basis is the arithmetic, not a guess: ten cells, nine gaps, and the block's own chrome.
   Anything else in the row wraps below rather than stealing from the grid. */
.block--heat{flex-basis:calc(10 * var(--tap) + 9 * var(--heat-gap)
  + 2 * var(--space-md) + var(--rule-slab))}

/* ---------------- lottery balls ---------------- */
.balls{display:flex; flex-wrap:wrap; gap:var(--space-xs); margin:var(--space-sm) 0}
.ball{
  width:52px; height:52px; border-radius:var(--ball);
  display:grid; place-items:center;
  font-family:var(--font-num); font-weight:700; font-size:var(--text-lg);
  background:var(--color-paper); color:var(--color-ink);
  border:var(--rule-mid) solid var(--color-rule);
}
.ball--pick{background:var(--color-accent); color:var(--color-paper); border-color:var(--color-ink)}
.ball--bonus{background:var(--color-accent-2); color:var(--color-ink); border-color:var(--color-ink)}
.ball--in{animation:ball-in var(--dur-base) var(--ease-out) backwards}
@keyframes ball-in{from{opacity:0; transform:translateY(10px)}to{opacity:1; transform:none}}

/* ---------------- the fortune-teller's own hand ---------------- */
.sermon{list-style:none; margin:var(--space-sm) 0 0; padding:0}
.sermon li{
  font-family:var(--font-hand); font-size:var(--text-base); line-height:1.45;
  color:var(--color-ink-dim); padding:var(--space-2xs) 0;
  border-bottom:var(--rule-hair) dashed var(--color-rule);
}
.sermon b{font-family:var(--font-num); color:var(--color-accent)}
.hand{font-family:var(--font-hand); color:var(--color-ink-dim)}

/* ---------------- data: deliberately plain, this is the cold layer ---------------- */
table{width:100%; border-collapse:collapse; font-variant-numeric:tabular-nums}
th,td{text-align:left; padding:var(--space-2xs) var(--space-xs);
  border-bottom:var(--rule-hair) solid var(--color-rule); font-size:var(--text-sm)}
th{color:var(--color-ink-dim); font-weight:600}
td.num,th.num{text-align:right; font-family:var(--font-num)}
.scroll{overflow-x:auto}
/* --font-num, matching every other figure on the page (.ball, .cell, .num, .pot__num, the
   tables). .kpi was the one number wearing a different face, and it renders two percentages
   stacked in a single column, so the digits have to line up.
   Three faces were tried and measured, per-digit, in the DOM:
     --font-display  spread 5.84-12.15px, proportional in all four skins
     --font-body     spread 77.13px in veso/thantai - Be Vietnam Pro ships NO tabular figure
                     set, so font-variant-numeric is a no-op there (77.13 -> 77.13)
     --font-num      spread 0 - JetBrains Mono is monospaced, so it cannot misalign
   Weight 700, not 800: only 400 and 700 are loaded, and 800 would be a synthesised bold. */
.kpi{font-family:var(--font-num); font-weight:700; font-variant-numeric:tabular-nums;
  font-size:var(--text-3xl); line-height:1;
  overflow-wrap:anywhere; min-width:0}
.kpi--bad{color:var(--color-bad)}
.kpi--good{color:var(--color-good)}
.note{color:var(--color-ink-dim); font-size:var(--text-sm); margin:var(--space-xs) 0 0}
.err{color:var(--color-bad)}

/* facts grid - image-free, but minmax(0,1fr) anyway so long words cannot push it wide */
.facts{display:grid; grid-template-columns:repeat(auto-fill,minmax(min(160px,100%),1fr));
  gap:var(--space-xs); margin:var(--space-sm) 0}
.fact{background:var(--color-paper); border:var(--rule-hair) solid var(--color-rule);
  border-left:var(--rule-mid) solid var(--color-accent); padding:var(--space-xs)}
.fact__k{font-size:var(--text-xs); color:var(--color-ink-dim)}
/* Data values use the body face on purpose. A 3D display face (Bungee Shade) turns
   to mush at 16px, and "ĐẠI LÂM MỘC" has to stay readable in every skin. */
.fact__v{font-family:var(--font-body); font-weight:800; font-size:var(--text-base);
  color:var(--color-ink); overflow-wrap:anywhere}
.block__head{font-family:var(--font-display); font-weight:400; font-style:normal;
  font-size:var(--text-lg); line-height:1.15; margin:0 0 var(--space-xs);
  color:var(--color-ink); overflow-wrap:anywhere; min-width:0}

/* the one place the fortune-teller touches the numbers */
.verdict-stamp{
  display:inline-block; font-family:var(--font-display); font-size:var(--text-xl);
  color:var(--color-bad); border:var(--rule-mid) solid var(--color-bad);
  padding:var(--space-2xs) var(--space-sm); transform:rotate(-4deg);
  margin:var(--space-sm) 0;
}

/* ---------------- privacy + disclaimer ---------------- */
/* --color-rule, not --color-good. A thick coloured edge is design.md's shape language and
   stays; a 6px GREEN one is the 2018-SaaS side-stripe card almost verbatim, and --color-good
   is the scoreboard's "this went right" signal, not a structural colour. */
.privacy{background:var(--color-paper); border-left:var(--rule-slab) solid var(--color-rule);
  padding:var(--space-sm) var(--space-md); margin:var(--space-sm) 0;
  color:var(--color-ink); font-size:var(--text-sm)}
.disclaim{background:var(--color-accent-2); color:var(--color-on-accent-2);
  padding:var(--space-md); margin:var(--space-md) 0;
  border:var(--rule-mid) solid var(--color-ink); font-weight:600}

/* ---------------- form ---------------- */
form.khai{display:flex; flex-wrap:wrap; gap:var(--space-sm); align-items:flex-end}
form.khai label{display:flex; flex-direction:column; gap:var(--space-2xs);
  font-size:var(--text-xs); color:var(--color-ink-dim)}
form.khai input,form.khai select{
  background:var(--color-paper); color:var(--color-ink); font:inherit;
  /* 16px is the iOS Safari threshold: anything smaller and Safari zooms the whole page
     on focus, stranding the user zoomed-in on one field. Never --text-sm here. */
  font-size:var(--text-base); padding:var(--space-xs);
  line-height:1.25;
  min-height:var(--tap);
  border:var(--rule-mid) solid var(--color-rule); border-radius:var(--radius);
  min-width:150px; max-width:100%;
}
/* Three controls on one form measured 50 / 48 / 44px: input[type=date] and input[type=text]
   each carry their own intrinsic height from the UA stylesheet, so the row looked misaligned.
   This is scoped to input on purpose - appearance:none on the select removes its dropdown
   arrow, which made the gender field indistinguishable from a text box. The select was
   already 44px and needs no help. */
form.khai input{appearance:none; -webkit-appearance:none}

.btn{
  font-family:var(--font-display); font-size:var(--text-sm); white-space:nowrap;
  padding:var(--space-xs) var(--space-md); cursor:pointer;
  min-height:var(--tap); touch-action:manipulation;
  background:var(--color-accent); color:var(--color-paper);
  border:var(--rule-mid) solid var(--color-ink); border-radius:var(--radius);
  transition:transform var(--dur-fast) var(--ease-out);
}
.btn:hover,.btn.is-hover{transform:translateY(-2px)}
.btn:active,.btn.is-active{transform:translateY(2px)}
.btn[disabled]{opacity:.45; cursor:not-allowed; transform:none}
.btn--ghost{background:transparent; color:var(--color-ink); border-color:var(--color-rule)}

/* ---------------- footer shell ---------------- */
/* The footer owns its vertical rhythm and its background; the .wrap inside owns the
   measure and the safe-area inset. Without that split the footer sat at whatever width
   its own content chose - 625px on any desktop - and stopped lining up with the page
   above it. .ft__statement and .ft__meta went with the Ft5 footer this replaced. */
.ft{border-top:var(--rule-slab) solid var(--color-rule); margin-top:var(--space-2xl);
  padding-block:var(--space-xl);
  padding-bottom:calc(var(--space-xl) + env(safe-area-inset-bottom, 0));
  background:var(--color-paper-2)}

/* ---------------- screen-reader only ---------------- */
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0}

/* ---------------- twemoji ---------------- */
/* Self-hosted so the glyphs look the same everywhere. OS emoji fonts diverge wildly,
   and a few of these have no glyph at all on some platforms. */
.tw{width:1em; height:1em; vertical-align:-.15em; display:inline-block}

/* ---------------- the fortune-teller ---------------- */
.thay{color:var(--color-ink); width:96px; max-width:30vw; line-height:0}
.thay svg{width:100%; height:auto; display:block; overflow:visible}
.thay--flip{position:relative}
.thay--flip .thay__b{position:absolute; inset:0}
/* Fourth motion primitive. Two conditions travel with it, both below: it pauses when
   scrolled out of view, and it does not exist under prefers-reduced-motion. */
@keyframes thay-idle{0%,45%{opacity:1} 55%,100%{opacity:0}}
.thay--flip .thay__b{animation:thay-idle 4s var(--ease-in-out) infinite}
.thay--flip.is-paused .thay__b{animation-play-state:paused}
.thay__say{font-family:var(--font-hand); color:var(--color-ink-dim);
  font-size:var(--text-sm); max-width:34ch; margin:0}
.thay-row{display:flex; gap:var(--space-md); align-items:flex-start; flex-wrap:wrap;
  margin:0 0 var(--space-md)}
.thay-row > *{min-width:0}

/* ---------------- a real print, pinned to the page ---------------- */
/* The woodblock has a light paper ground, so on the dark skins it needs a paper-coloured
   mount or it reads as a rectangle pasted on top. */
.print{background:var(--color-paper-2); border:var(--rule-mid) solid var(--color-rule);
  padding:var(--space-xs); max-width:160px; margin:var(--space-sm) 0;
  transform:rotate(-1.5deg)}
.print img{display:block; width:100%; height:auto}
.print figcaption{font-family:var(--font-hand); font-size:var(--text-xs);
  color:var(--color-ink-dim); margin-top:var(--space-2xs); line-height:1.3}

/* ---------------- heatmap: tappable, not hover-only ---------------- */
/* `title` does not exist on touch. Every cell is a button and writes to a live readout,
   so the frequency data is reachable without a mouse. */
.heat{display:grid; gap:var(--heat-gap); margin-top:var(--space-sm)}
.heat--pool{grid-template-columns:repeat(auto-fill,minmax(min(44px,100%),1fr))}
.heat--matrix{grid-template-columns:repeat(10,minmax(0,1fr))}

/* The matrix has exactly ten columns, so its tracks shrink with the container while a
   .cell asks for a fixed 44x44. `minmax(0,1fr)` lets a track go BELOW its item's minimum,
   and the cell then overflows its track and paints over its neighbour - measured at 15.7px
   of overlap per cell in a three-up row, and 13.4px on a 390px phone, which is where this
   had been shipping unnoticed since the 10x10 grid landed.

   So the matrix cell drops the floor and simply fills its track. The tap target is not
   abandoned: --block-heat-basis below keeps the whole grid at the full --tap wherever the
   row has room, and where it does not, the cell lands near 26-31px. That is the same AA
   line page.css already takes for colophon links - WCAG 2.5.8 asks 24x24 as AA, and 44 is
   the AAA figure - except here it applies only when the viewport forces it. */
.heat--matrix .cell{min-height:0}

.cell{
  aspect-ratio:1; min-height:var(--tap); touch-action:manipulation;
  display:grid; place-items:center; font-family:var(--font-num);
  font-size:var(--text-xs); border:var(--rule-hair) solid var(--color-rule);
  border-radius:var(--radius); color:var(--color-ink);
  cursor:pointer; padding:0;
  /* --heat is set per cell by render.js, and this declaration must stay last: an earlier
     `background:transparent` in this same rule silently won and erased the whole heatmap.
     color-mix keeps the tint a token expression rather than a computed colour string, and
     a browser without color-mix simply gets no tint, which is still perfectly readable. */
  background:color-mix(in oklab, var(--color-accent-2)
    calc(var(--heat, 0) * var(--heat-max, 30%)), transparent);
}
.cell:hover,.cell.is-hover{outline:var(--rule-mid) solid var(--color-accent); outline-offset:-3px}

.cell:active{transform:translateY(1px)}
.cell[aria-pressed="true"]{background:var(--color-accent-2); color:var(--color-on-accent-2)}
.heat__readout{font-family:var(--font-num); font-size:var(--text-sm);
  color:var(--color-ink); margin:var(--space-xs) 0 0; min-height:1.5em}

/* ---------------- tables: stack instead of scrolling inside a card ---------------- */
/* Horizontal scroll *inside* a card is one of the worst mobile patterns: nothing tells
   the reader there is more to the right. Label/value tables become stacked rows. */
@media (max-width:640px){
  .stack-sm,.stack-sm tbody,.stack-sm tr,.stack-sm th,.stack-sm td{display:block}
  .stack-sm tr{border-bottom:var(--rule-hair) solid var(--color-rule);
    padding:var(--space-xs) 0}
  .stack-sm tr:last-child{border-bottom:0}
  /* The matrix variant drops its header row and lets each cell name its own column,
     because two unlabelled numbers per row is worse than no table at all. */
  .stack-sm--matrix thead{display:none}
  .stack-sm--matrix td::before{content:attr(data-label) ": ";
    font-family:var(--font-body); font-size:var(--text-xs); color:var(--color-ink-dim)}
  .stack-sm--matrix td{display:flex; gap:var(--space-2xs); align-items:baseline;
    flex-wrap:wrap}
  .stack-sm th{color:var(--color-ink-dim); font-size:var(--text-xs); border:0; padding:0}
  .stack-sm td,.stack-sm td.num{font-family:var(--font-num); font-size:var(--text-lg);
    text-align:left; border:0; padding:2px 0 0}
  /* 12px is a label size, not a reading size. */
  .note,th,.sermon li{font-size:var(--text-sm)}
  .scroll{position:relative}
  .scroll::after{content:"cuộn ngang →"; position:absolute; right:0; top:-1.4em;
    font-family:var(--font-num); font-size:var(--text-xs); color:var(--color-ink-dim)}
}

/* The pot. accent-2 as a fill with ink on top, never as text: on veso it measures
   1.34:1 against paper, which the token block spells out. */
.pot{
  background:var(--color-accent-2); color:var(--color-on-accent-2);
  padding:var(--space-sm); margin-top:var(--space-sm);
  border:var(--rule-mid) solid var(--color-rule);
}
.pot__label{margin:0; font-size:var(--text-xs); letter-spacing:.1em; text-transform:none}
/* The figure people came for. --font-num, not --font-display: at this size the 3D and
   condensed display faces turn "31,41 tỷ" to mush, which was measured once already. */
.pot__figure{
  margin:var(--space-2xs) 0; font-family:var(--font-num); font-weight:800;
  line-height:1.05; display:flex; align-items:baseline; gap:var(--space-2xs);
  flex-wrap:wrap; min-width:0;
}
/* nowrap is safe here only because the unit was lifted out: the digits alone fit at every
   width down to 280px, verified against a three-digit pot. */
.pot__num{font-size:var(--text-figure); white-space:nowrap}
.pot__unit{font-size:var(--text-lg); font-weight:700}
.pot__sub{margin:0; font-size:var(--text-xs); font-family:var(--font-num)}
.pot__facts{
  display:grid; grid-template-columns:minmax(0,1fr) auto; gap:var(--space-2xs) var(--space-sm);
  margin:var(--space-sm) 0 0; padding-top:var(--space-xs);
  border-top:var(--rule-hair) solid currentColor;
  font-size:var(--text-sm);
}
.pot__facts dt{margin:0}
.pot__facts dd{margin:0; text-align:right; font-family:var(--font-num); font-weight:700}

/* The basic ticket. Set apart with a rule rather than a fill, because it is an
   alternative reading of the same twelve numbers, not a second product. */
.basic{border-top:var(--rule-mid) solid var(--color-rule);
  margin-top:var(--space-sm); padding-top:var(--space-sm)}
.basic__head{font-family:var(--font-display); font-size:var(--text-sm);
  margin:0 0 var(--space-xs); color:var(--color-ink)}
.basic__note{font-family:var(--font-hand); font-size:var(--text-sm);
  color:var(--color-ink); margin:var(--space-xs) 0}

/* ---------------- footer: Ft2 credit columns ---------------- */
/* Was Ft4 colophon - columns of credits above one prose block. The prose block is gone,
   and that is the whole fix. It carried max-width:70ch, so on a 1060px .wrap its 1px top
   rule stopped 440px short of the columns above it and closed nothing; the sentence inside
   wrapped mid-phrase with half the row empty. Its content now lives in a third column that
   is structurally identical to the other two, so the footer has ONE left axis repeated
   three times instead of four axes disagreeing. */
.colophon{font-family:var(--font-num); font-size:var(--text-sm); line-height:1.55}

/* The hairline is what makes three columns read as three columns. Three rules of equal
   length landing on the same y is the cheapest possible grid cue, and below 900px - where
   the columns stack - the same rule doubles as the section divider, so the single-column
   form does not collapse into one long undifferentiated list. Hair here, not slab: .ft
   already owns the slab edge, and design.md forbids an even box on four sides. */
.colophon h2{font-family:var(--font-num); font-size:var(--text-xs); font-weight:700;
  letter-spacing:.18em; margin:0 0 var(--space-sm); color:var(--color-ink);
  border-bottom:var(--rule-hair) solid var(--color-rule); padding-bottom:var(--space-xs)}

/* Columns are not equal fractions, and the reason was measured rather than eyeballed.
   "khiemdoan/vietnam-lottery-xsmb-analysis" needs 329.5px unwrapped at --text-sm in
   JetBrains Mono. Three even columns of the 1020px content box with --space-lg gutters
   give 318.66px each - checked by switching the live grid to repeat(3,minmax(0,1fr)) and
   re-measuring, not by arithmetic - and that dt then renders 43.6px tall, i.e. two lines.
   Because dt carries overflow-wrap:anywhere it breaks mid-word, so it is a wrap that never
   overflows and never gets reported, only seen. 1.25fr 1fr 1fr gives column 1 a measured
   367.69px and every dt stays on one line. The ratio is what the content asks for anyway:
   five credits left, three assets middle, three meta right.

   980px, not the 900px the two-column version used. Swept the viewport in 8px steps: at
   904px column 1 is 307.7px and the long dt is already two lines, and it stays two lines
   all the way to 960px (329.2px, still 0.3px short). 968px is the first width that clears
   it at 332.3px. Turning three columns on at 900 would have shipped a wrap for 68px of
   viewport that nobody would have thought to check, because the breakpoint was inherited
   rather than re-measured after the column count changed.

   Measuring note: do not count lines with Range.getClientRects() here. The footer links are
   display:inline-block for their tap target, so their rect sits 4px above the text rect and
   a naive top-count reports "MIT · 2026" as two lines when it is one. Divide the element
   height by line-height instead. */
.colophon__cols{display:grid; grid-template-columns:minmax(0,1fr); gap:var(--space-lg)}
@media (min-width:980px){
  .colophon__cols{grid-template-columns:1.25fr 1fr 1fr; gap:var(--space-lg)}
  .colophon__cols--2{grid-template-columns:1.25fr 1fr}
}

/* dt above dd at every width, not beside it. The side-by-side form needed a fixed dt
   column and no width fits both "EMOJI" and "khiemdoan/vietnam-lottery-xsmb-analysis":
   at 22ch the long repo names wrapped on every viewport, desktop included. Stacking
   removes the constraint instead of tuning it.

   dd is NOT indented. It used to carry padding-left:var(--space-sm), which put every
   second line on a 12px-offset axis of its own - too small to read as hierarchy, big
   enough to read as sloppiness, and the hierarchy was already carried by --color-ink-dim.
   One axis per column, no exceptions. */
.colophon dl{margin:0}
.colophon dt{color:var(--color-ink); overflow-wrap:anywhere}
.colophon dd{margin:0 0 var(--space-sm); color:var(--color-ink-dim);
  overflow-wrap:anywhere}
.colophon dd:last-of-type{margin-bottom:0}

/* Inline links at --text-sm are a 14px-tall hit area. WCAG 2.5.8 asks 24x24 as AA, and
   --tap (44px) is the AAA figure the page uses for real controls; a footnote link is not a
   control, so this takes the AA line and gets there with padding rather than by growing
   the type. inline-block is required for padding-block to affect the box at all. */
.colophon a{display:inline-block; padding-block:var(--space-2xs)}

/* The base strip spans the full .wrap - no measure. That is the difference between a rule
   that closes the footer and the one that used to stop halfway across it. */
.colophon__base{margin:var(--space-lg) 0 0; padding-top:var(--space-sm);
  border-top:var(--rule-hair) solid var(--color-rule); color:var(--color-ink-dim)}

/* ---------------- reveal ---------------- */
.reveal{opacity:0; transform:translateY(14px);
  transition:opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out)}
.reveal.is-in{opacity:1; transform:none}
@media (prefers-reduced-motion:reduce){
  .reveal,.reveal.is-in{transition:opacity 150ms linear; transform:none}
  .ball--in{animation:none}
  .stamp,.btn,.cell{transition:none}
  /* Not slowed down — removed. This is an accessibility requirement, not a taste knob. */
  .thay--flip .thay__b{animation:none; opacity:0}
  .print{transform:none}
}

/* ---------------- finance page ---------------- */
/* Body face at 800, never the display face: a delta is a data value, and Bungee Shade
   at 14px is a smudge. Same rule as .fact__v. */
/* Figures, so --font-num: design.md's rule is that any column of them uses it, because
   Be Vietnam Pro ships no tabular figure set and a column of prices in the body face
   misaligns by up to 77px. Weight 700, not 800 - only 400 and 700 are loaded, and 800
   would be a synthesised bold. */
.fact__v--num{font-family:var(--font-num); font-weight:700;
  font-variant-numeric:tabular-nums}

/* Sits on paper-2, not paper. On thantai the paper IS lacquer red, so --color-bad on it
   measured 4.26:1 - under AA. The same colour on paper-2 measures 5.32, which is where
   .kpi--bad already lives. Chip, not bare text, for that reason alone. */
.delta{font-family:var(--font-num); font-weight:700; font-variant-numeric:tabular-nums;
  font-size:var(--text-xs);
  color:var(--color-ink-dim); background:var(--color-paper-2);
  display:inline-block; margin-top:var(--space-2xs); padding:1px 5px;
  overflow-wrap:anywhere}
.delta--up{color:var(--color-good)}
.delta--down{color:var(--color-bad)}
/* Provenance sits under the number it belongs to, so it reads as part of the figure. */
.src{margin-top:var(--space-2xs)}
.src a{color:inherit}

/* ---------------- trader page ---------------- */
/* The chart is the only canvas on this site, so it is also the only element whose contrast
   cannot be measured from the DOM. Its height is fluid rather than fixed: a fixed pixel
   height at 320px leaves a candle chart four candles tall, and 45vh at 1440px leaves it
   taller than the reading column. The library's autoSize handles the width. */
.chart{height:clamp(280px, 45vh, 460px); width:100%}
/* The chart sits INSIDE a .block, so it inherits --color-paper-2 - which is the only
   reason its axis text can reuse the .note-inside-a-.block contrast measurement instead
   of claiming one nobody made. trader.js forces that token rather than letting the
   library paint its default white. */
.chart__foot{display:flex; flex-wrap:wrap; gap:var(--space-xs) var(--space-md);
  margin-top:var(--space-xs); align-items:baseline}

/* Vocabulary. A dl, not a row of cards: one left axis, hierarchy by colour and not by
   indent - the footer's rule, reused, for the reason it was written there. */
.gloss{margin:var(--space-sm) 0 0}
.gloss dt{font-family:var(--font-body); font-weight:800; margin-top:var(--space-sm)}
.gloss dd{margin:var(--space-2xs) 0 0; color:var(--color-ink-dim); font-size:var(--text-sm)}
/* The threshold that defines a pattern, printed beside its name. --font-num because it is
   a figure, and design.md's rule is that any figure uses it. */
.gloss__rule{font-family:var(--font-num); font-weight:700; font-size:var(--text-xs);
  color:var(--color-ink)}

/* The verdict column of the honest table. Deliberately not a colour-only cue: the word
   carries it, --font-num sets it apart, and it survives a monochrome print. */
.cmp__flat{font-family:var(--font-num); font-weight:700; color:var(--color-ink-dim)}
