/* CONTROLS — the brand mark, the icon buttons, and the expiry picker.
 *
 * Loaded last. Nothing in here restyles a surface; it fixes the geometry of
 * things you click, and states the invariants that keep them from drifting
 * again. The reasoning lives in CONTROLS.md at the repo root — that file is
 * the one to read before changing any number here.
 */

/* ── 0. THE BRAND MARK ────────────────────────────────────────────────────
   Three rules, so it does not earn a file of its own.

   28px, not the 22 the drawn capmark used. The capmark was a line drawing:
   four strokes on an empty field, legible at any size because there was
   nothing to lose. The real mark is a filled disc carrying two figures inside
   it — a four-pointed star and a crossing orbit — and below about 26px those
   two stop being separable and the mark reads as an orange smudge. Measured
   against the header's own 49.5px, 28 sits inside it with room to spare.

   height:auto IS THE FIX, not the nicety. style.css:420 gives .logo a hard
   `height: 22px`, sized for the drawn capmark that used to live in it. A 28px
   image inside a 22px box does not centre — the box stays 22, the image
   overflows it downward, and the mark's centre landed 3px below the
   wordmark's. Chasing that with a -3px nudge would have hidden the cause and
   broken again at any other size: measured, the offset tracked the image
   height exactly (3.02px at 28, 4.02px at 30). Letting the box size to its
   content fixes it at every size — re-measured, mark centre 24.77 against ink
   centre 24.75.

   align-self:center, because .brand aligns on the BASELINE for its text and an
   image has no baseline worth aligning to. */
body .logo {
  display: grid;
  place-items: center;
  align-self: center;
  width: auto !important;
  height: auto !important;
}
body .logo .logomark {
  display: block;
  width: 28px;
  height: 28px;
}

/* ── 1. THE ICON BUTTON ───────────────────────────────────────────────────
   Reported: "Sửa lại các button icon này … không làm button bị méo. Thêm
   trạng thái hover cho button +".

   THE MEASUREMENT. The compare pin renders 22 x 26 with its 11px glyph at
   offsetLeft 11 / offsetRight 0 — hard against the right edge, with the whole
   gap on the left. A sweep of every icon button on every section found this
   is the only one distorted; the rule below is written so it stays that way.

   THE CAUSE IS THREE FILES DISAGREEING, and no one of them is wrong on its
   own:
     opps.css      .op-pin  width:15px; height:15px; padding:0   <- the intent
     instrument.css blanket  padding:5px 10px; border:1px        <- register
     ui.css        blanket  min-height:26px                      <- hit target
   Applied in that order you get a 22px-wide box (15 + 10 padding, clamped by
   the cell) that is 26px tall (the floor), whose content box is 22 - 20 - 2 =
   0px wide. A glyph cannot be centred in nothing, so it overflows right. The
   button was never "styled wrong"; it was styled three times.

   THE RESOLUTION IS 26 x 26, SQUARED UP RATHER THAN DOWN. ui.css's 26px is a
   hit-target floor, not decoration — squaring at the 15px opps.css asked for
   would trade an accessibility minimum for a visual one. The cell it sits in
   is over 300px wide; there was never a space argument for the smaller box.

   HOVER IS A FILL, and this is the one control on the board that earns it: of
   the ~38 cells on this matrix, every value is read-only and this is the only
   thing that WRITES — it adds the cell to the compare tray. Orange is already
   the app's "you are acting here" ink. Measured: #ff7a1a on #0c0a08 is 7.6:1,
   so the glyph is legible on the fill rather than merely visible.

   PRESSED IS NOT HOVER, deliberately. Hover is a solid fill and means "this
   will be added"; pressed is an orange outline with the ground showing through
   and means "this is already in the tray". If both were filled you could not
   tell, while pointing at a pinned cell, whether you were about to add or
   remove it. */
body .op-pin {
  width: 26px !important;
  height: 26px !important;
  padding: 0 !important;
  display: grid !important;
  place-items: center !important;
}

/* The glyph is stroke="currentColor", so the ink flips with the button. */
body .op-pin:hover,
body .op-pin:focus-visible {
  opacity: 1 !important;
  background: var(--accent) !important;
  border-color: var(--accent) !important;
  color: var(--on-brand) !important;
}

body .op-pin[aria-pressed="true"] {
  opacity: 1 !important;
  background: transparent !important;
  border-color: var(--accent) !important;
  color: var(--accent) !important;
}
/* Pointing at something already pinned still fills, because the click still
   does something — it removes it. The fill says "acting", not "adding". */
body .op-pin[aria-pressed="true"]:hover {
  background: var(--accent) !important;
  color: var(--on-brand) !important;
}

/* THE PIN NEEDS A LANE, AND THE LANE WAS 6px TOO NARROW.
   Reported: "nút bấm bị đè lên số".

   Measured on a $24.20 cell: the number's ink ends at x=714.3 and the pin
   starts at 708.3 — 6px of overlap, 12.5px tall, sitting on the last digit.

   The lane already exists, and instrument.css §"THE BOARD CELL CLEARS ITS
   PIN" states its arithmetic in as many words: "the pin button is 15px wide
   at right:2px, so a right-aligned figure would sit under it" — and sets
   22px. That was correct for a 15px pin. §1 above made the pin 26px to meet
   ui.css's hit-target floor, and nothing re-derived the lane: 2 (right
   offset) + 26 (the square) + 2 (air) = 30, and 30 − 22 is the 6px of overlap
   measured on screen, exactly.

   THE RULE, because this will drift again the next time either number moves:

       lane >= pin right-offset + pin width + 2

   It is not a taste value, and it is the second time this codebase has had to
   learn it — tools.css reserves --tape-share for the same reason, after
   elementsFromPoint caught the share button sitting on the maker column.
   A control that floats over content needs a reserved track, not a hope.

   Specificity matches instrument.css's (0,3,2) rather than out-shouting it,
   so the two rules stay readable side by side. */
body .op-mx-c.val .op-cell { padding-right: 30px !important; }

/* ── 2. THE EXPIRY PICKER ─────────────────────────────────────────────────
   Reported: "Cách chọn ngày trên lịch hiện tại đang bị khó bấm, khó dùng".

   THE MEASUREMENT. 15 expiries, each chip 96 x 50 with a 6px gap, in a strip
   1,120px wide whose content is 1,506px. 11 chips visible, 4 hidden, 386px
   past the right edge — and `scrollbar-width: thin` resolves to a 0px-high
   scrollbar on this platform, so there is no rule, no arrow, no fade and no
   shadow saying the row continues. Nothing on screen distinguishes "these are
   the expiries" from "these are the expiries, and there are four more".

   The hit target was never the problem: 96 x 50 is generous. What is hard is
   REACHING the far end — a horizontal strip inside a vertically scrolling page
   takes a deliberate sideways gesture, and you only make it if you already
   know there is something there.

   WRAPPING SERVES THIS FILE'S OWN STATED INTENT better than scrolling does.
   chain.css:55 says the row is horizontal "rather than a dropdown: the set of
   expiries is itself information — where the market has listed and where it
   has not — and a closed <select> hides that behind a click." A strip that
   hides four of fifteen behind a sideways scroll is that same objection,
   smaller. Wrapping shows all fifteen at once, which is what the row was for.

   WRAPPING WAS THE FIRST ANSWER AND THE OWNER REJECTED IT, correctly: two
   rows of chips reads as two groups when it is one axis, and it pushes the
   chain itself down the page. Scrolling was rejected too, and for a reason
   worth writing down — "bất tiện nếu không có trackpad". A horizontal strip
   is a gesture a mouse does not have.

   So the row is neither scrolled nor wrapped: it is made to FIT. The saving
   comes from the chip face, not from squeezing the target. Dropping the year
   (see expChip in chain.js) takes the widest chip from "10 Aug 26" to
   "10 Aug", and the days-out line under it already says which year it is far
   more usefully than "27" does. Measured after: 15 chips at ~63px plus 14
   gaps is ~1,029px against ~1,072px available, so the whole axis is on one
   line with room to spare, nothing hidden and nothing to drag.

   THE FALLBACK IS DELIBERATE AND VISIBLE. A different underlying may list
   more expiries than BTC's fifteen, and a narrow window shrinks the budget.
   When that happens the row scrolls again — but with a REAL scrollbar rather
   than `scrollbar-width: thin`, which renders 0px high here and was the whole
   reason four hidden expiries were invisible in the first place. A scrollbar
   you can see and drag is the mouse affordance the thin one never was. */
body .chv-exps {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: auto;
  gap: 5px !important;
}
/* Tuned against the NARROWER of the two widths that matter, not the wider.
   At a 1,440 window the strip has 1,072px and almost anything fits; at 1,280
   the panel's new 24px padding leaves 920, and 15 chips at 56px plus 14 gaps
   of 5 is 910. Measured at both: 0px hidden. The vertical padding is
   untouched, so the chip stays ~37px tall, well clear of ui.css's 26px
   hit-target floor — the target did not shrink, only the type and the side
   padding did. */
body .chv-exp {
  padding: 5px 7px;
  gap: 0;
}
body .chv-exp .chv-exp-d {
  font-size: 12px !important;
  letter-spacing: -.01em !important;
}
body .chv-exp .chv-exp-t {
  font-size: 10px !important;
}

/* ── 3. THE BIG CONTAINERS GET AN EDGE ────────────────────────────────────
   Reported: "container lớn thì thêm padding 24px trên dưới trái phải. thêm
   viền nhạt".

   The rebuild left .panel completely flat: padding 0, border 0, background
   transparent, radius 0. Content therefore starts exactly on the stage's
   gutter and there is nothing to say where one section's surface ends.

   24px on all four sides as asked. The border is --line at the weight the
   register already uses for structure, so it reads as an edge and not as a
   box: on the near-black ground it is a hairline you notice only when you
   look for it, which is what "nhạt" asks for. Radius stays 0 — instrument.css
   §17 squares every corner in the app and a rounded panel here would be the
   only rounded thing on the page.

   ── THIS REVERSES A DOCUMENTED DECISION, AND SAYS SO ──────────────────────
   instrument.css §71 strips the chain panel to nothing on purpose:
   `html body .panel.chv { border: 0; padding: 0 }`. Its argument is worth
   keeping in view because it is a good one — that the section card was "one
   full-width rectangle drawn around the page, inside a rectangle that is
   already the page, grouping nothing against anything", and that a border
   earns its keep only when it groups two things AGAINST each other (which is
   why it kept the border on .chv-wrap, where the table sits beside its note,
   and dropped it here).

   The owner has now asked for the frame back, looking at the built result.
   That is their call to make and this implements it. What is NOT done is
   deleting §71 or editing instrument.css: the rule and its reasoning stay
   exactly where they are, and this overrides from the end of the cascade, so
   whoever revisits the question later finds both positions instead of one.

   Specificity is deliberate, not incidental: §71 scores (0,2,2) with
   !important, so `body .panel` (0,1,1) never had a chance. `.view .panel`
   clears it, and every panel in the app is inside a .view.

   ── ONE RULE, BECAUSE THE SECTIONS NOW USE ONE NAME ───────────────────────
   Chain and Trades wrap their content in .panel; Cost and Tools wrapped
   theirs in a bare <div id="opps-root"> / <div id="tools-root">. Rather than
   write positional CSS against an unclassed div — which breaks the first time
   the markup gains a sibling — those two wrappers were given the class the
   app already had a name for. Four sections, one selector.

   Overview is deliberately NOT framed: its view holds three children and the
   panels it does have (.hero-panel) sit one level deeper, so a frame at view
   level would draw a box around a box.

   Radius and shadow are stated here rather than inherited. instrument.css's
   panel group hands .panel a 12px radius and a two-layer drop shadow, and
   .panel.chv is exempted from it by name — so without these two lines the
   sections that just gained the class would round and lift while Chain stayed
   square, which is the inconsistency this rule exists to remove.

   THE FRAME IS A SURFACE, and this paragraph has flipped once, so the
   history is worth keeping. Giving Cost and Tools the .panel class pulled
   style.css's fill (var(--card)) along with it while Chain and Trades sat
   bare, and the first sync levelled everything DOWN to transparent. The owner
   then reversed it looking at the result — "Bg không có màu. Nó phải giống
   màu các trang còn lại chứ" — and up is the better level: var(--card) is the
   surface style.css always intended for a panel, it is translucent enough
   that paper.css's grain still reads through it, and a section you can SEE
   ends where its surface ends instead of only where a hairline says so.

   NOT var(--card): the register pass zeroes that token to `transparent`, so
   asking for it here computes to nothing — measured. And not the historical
   rgba(28,22,17,.55) either — that was tried and the owner's verdict was
   "vẫn còn trong suốt": at .55 over this ground it blends to #151110, a lift
   of ~9 per channel, which reads as a rendering accident rather than a
   surface. So the fill is OPAQUE --s1 (#17130f), the register's own
   surface-1 token. Eleven points of warm lift over the #0c0a08 ground — the
   same panel-to-ground distance the exchange this register studies uses
   (#181A20 on #0B0E11) — and one tier below --s2, which the ghost buttons
   wear, so controls still read against the card they sit on. One token, all
   sections, measured identical. */
html body .view .panel {
  padding: 24px !important;
  border: 1px solid var(--line) !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  background: var(--s1) !important;
}

/* ── COMPARE IS TWO PANELS, NOT ONE 3,368px BOX ───────────────────────────
   Measured: the overview panel ran 3,368px tall — #where-grid alone is
   2,371 — so its frame never closed on screen and the section read as a
   borderless field while every other section read as a card. The owner chose
   splitting it over unframing it.

   The split is CSS, not markup, because it cannot be markup: grid.js mounts
   the hero with `host.parentNode.insertBefore(el, host)`, always the sibling
   directly before #where-grid, so any static wrapper drawn around one would
   capture the other on the next poll. Instead the outer section stops being
   the panel and the two blocks that were always inside it become the panels:

     #sc-headline  the hero — number, chips, stats, coverage   (~750px)
     #where-grid   the venue board                             (~2,400px)

   The board panel is still tall, but it is HONESTLY tall — one table — and
   the hero above it now closes like every other card in the app. .ov-ctx (the
   "Showing BTC" line) stays unframed above both, as section context, and the
   summary and answer-strip lines stay unframed below, as footnotes — same as
   the footer lines every other section leaves on the ground. */
html body .view[data-view="overview"] .panel.hero-panel {
  padding: 0 !important;
  border: 0 !important;
  background: transparent !important;
}
html body .view[data-view="overview"] #sc-headline,
html body .view[data-view="overview"] #where-grid {
  padding: 24px !important;
  border: 1px solid var(--line) !important;
  background: var(--s1) !important;
}
html body .view[data-view="overview"] #sc-headline { margin-bottom: 24px; }
html body .view[data-view="overview"] .ov-ctx { margin-bottom: 18px; }

/* ── 3b. THE VENUE PANEL IS A SECTION SURFACE, AND IT WAS MISSED ───────────
   Reported: "Padding đâu? Check toàn bộ tất cả các màn hình mà sao lại sót
   màn này." Measured at 1440x900 with bybit open, before this rule:

     div#detail-root   x 259  w 970  padding 0  border 0  bg transparent
     div.vp            x 259  w 970  padding 0  border-top 1px rgba(255,255,255,.16)
                                                bg rgba(28,22,17,.55)

   Same x as the view: content flush to the edge on all four sides, and the
   fill is the exact .55 surface §3 records the owner rejecting by name. So
   this is not a new defect, it is §3's defect on a surface §3 never reached.

   WHY THE SWEEP MISSED IT, because that is the actual question. §3 frames
   `.view .panel` and the sweep enumerated painted surfaces per view. Every
   section that is markup — #today-root, #chain-root, .tape-panel, #opps-root,
   #tools-root — carries .panel in index.html and was framed and verified.
   #detail-root carries nothing and holds nothing: detail.js writes .vp into
   it only after a venue card is clicked. An audit that walks the DOM sees an
   empty div of zero height at every hash and reports a clean sweep, honestly
   and wrongly. A section that exists only after a click is still a section.
   The other host in that class is #trades-root, checked at the same time: no
   renderer in the app writes to it, it is a dead node from the pre-merge
   layout, and an empty node needs no frame.

   THE FRAME GOES ON THE HOST, NOT ON .vp, and `:not(:empty)` is load-bearing.
   index.html states the contract above #detail-root: "Empty it has zero
   height, so a page with no panel open is byte-identical to before" — the
   panel sits ABOVE the venue board precisely because a zero-height node there
   costs the reader nothing. Padding an empty div gives it 50px, which would
   push the board down on every page that has no panel open. Framing the host
   also catches .vp-loading, so the panel is a card from the first frame it
   paints rather than after the fetch resolves.

   .vp then gives up its own surface: its border-top and .55 fill are the
   frame this rule now draws properly, and two of them stack into a doubled
   hairline 1px inside the real one. Its interior rules — the 26px stack gaps
   on .vp-carries / .vp-stats / .vp-foot — are untouched; those hairlines now
   inset 24px from the frame, exactly as every other panel's internal rules
   do. */
html body .view #detail-root:not(:empty) {
  padding: 24px !important;
  border: 1px solid var(--line) !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  background: var(--s1) !important;
}
html body .view #detail-root > .vp {
  border: 0 !important;
  background: transparent !important;
  padding: 0 !important;
}
html body .view #detail-root > .vp-loading { padding: 2px 0 !important; }

/* ── 4. THE STAGE SCROLLBAR ───────────────────────────────────────────────
   Reported: "bỏ thanh scroll".

   It belongs to main.stage, which is `overflow: auto` and genuinely
   overflows: measured on Chain, 881px of content in an 802px box, 79px over.
   So this hides the track; it does not stop the scrolling, and it must not —
   the chain table is long at any window height and 79px is not a rounding
   error to be padded away.

   THIS IS SAFE HERE AND WAS NOT SAFE ON THE EXPIRY ROW, and the difference is
   worth stating because §2 above argues the opposite case. Vertical scrolling
   has a universal gesture: a wheel, a trackpad, Page Down, the keyboard, all
   of them work with no track on screen and every reader already knows the
   page continues downward. Horizontal scrolling has none of that — it is a
   deliberate sideways swipe most mice cannot make — which is why a 0px-high
   horizontal bar hid four expiries and nobody could have known. Hiding the
   vertical track costs a reader nothing; hiding the horizontal one cost them
   four dates.

   Both syntaxes, because they cover different engines and neither is
   universal: scrollbar-width for Firefox, ::-webkit-scrollbar for Safari and
   Chrome, which is what this app is read in. */
html body main.stage { scrollbar-width: none; }
html body main.stage::-webkit-scrollbar { width: 0; height: 0; }

/* ── 4b. THE PAGE ONLY SCROLLED FROM THE RIGHT BLOCK ───────────────────────
   Reported: "Ở page Compare thao tác scroll page đang gặp khó khăn. nếu
   không scroll đúng block thì nó sẽ không scroll được."

   Measured on Compare at 1440x900:

     main.stage        overflow-y auto  overscroll-behavior-y contain  room 807
     div#where-grid    overflow-y auto  overscroll-behavior-y contain  room 0
                       1120x554 — 69% of the stage's 803px viewport

   The board declares that its scroll must not chain to its parent, and it
   covers two thirds of the page. Put the pointer on the table, which is most
   of what Compare is, and the stage does not move. #tape on Trades is the
   same shape: 1070x578, 72% of the viewport, contain.

   BOTH HALVES OF THAT ARE INHERITED, AND NEITHER IS ANYONE'S MISTAKE.
   style.css built the board as its own pane — `.hero-panel > #where-grid
   { flex:1 1 auto; min-height:0; overflow:auto; overscroll-behavior:contain }`
   — a fixed shell with a scrollport inside it, where contain is exactly
   right. instrument.css then undid the pane, deliberately: §with `body
   #where-grid { flex:none; max-height:none; overflow-y:visible !important }`
   the board grows to its content and the STAGE scrolls. That is the layout
   the app ships. It just never reset the overscroll property, because the
   property it did reset looked like enough.

   IT WAS NOT ENOUGH, AND THE REASON IS A CSS RULE NOT A CASCADE ONE. The
   computed value of overflow-y on #where-grid is `auto`, measured, in spite
   of `visible !important` with nothing overriding it. CSS forbids one axis
   being `visible` while the other is not: `overflow-x: auto` — from
   style.css and again from instrument.css's own `.sc-host` rule, and needed,
   the board is a wide table — forces overflow-y to compute to `auto`. So the
   element stays a scroll container no matter what the y axis asks for, and a
   scroll container carries its overscroll-behavior whether or not it has any
   room to scroll.

   The fix is therefore not to un-scroll it, which CSS will not allow while
   the table pans sideways, but to let it hand the gesture upward again.
   Y only: overscroll-behavior-x stays `contain` on these blocks, because a
   sideways swipe on a wide table must not become the browser's back gesture.

   main.stage KEEPS contain and must. style.css states why beside it: with the
   document locked at `overflow:hidden` there is nothing above the stage to
   hand a gesture to, and the browser would rubber-band instead. Contain is
   correct on the one real scroller and wrong on every block inside it. */
html body main.stage #where-grid,
html body main.stage #coverage-wrap,
html body main.stage .chart-wrap,
html body main.stage .table-scroll,
html body main.stage .sc-host,
html body main.stage .tw-host,
html body main.stage .tw-grid,
html body main.stage #tape {
  overscroll-behavior-y: auto !important;
}

/* ── 5. ONE COLUMN HEADER, ONE SIZE, ONE CASE ─────────────────────────────
   Reported: "các size chữ không đồng bộ. Viết hoa và không viết hoa cũng
   phải được đồng bộ."

   Measured across the tape's nine header cells:

     Age, Asset, What happened, Expiry, Venue    10px   uppercase
     Size, Premium        (button.tw-sort)       13px   uppercase
     Taker, Maker         (button.gl)            13px   NONE

   Three treatments in one row. The cause is that the cell carries the label
   style while the INTERACTIVE headers put a <button> inside it, and a button
   takes the app's control type rather than inheriting the label's — so a
   header is styled as a label until it becomes clickable, at which point it
   silently becomes a control. Taker and Maker are worst because they are
   glossary buttons, which do not uppercase at all, so the row reads
   "AGE ASSET … VENUE Taker Maker".

   A column header is a LABEL whether or not you can click it. What the click
   does is said by the sort caret and by the glossary underline, both of which
   survive untouched; none of it needs a second type size to announce itself.
   10px uppercase, the same as the cells that were already right. */
body .tw-head button,
body .tw-head .tw-sort,
body .tw-head .gl {
  font-size: 10px !important;
  text-transform: uppercase !important;
  letter-spacing: .08em !important;
}

/* ── 6. THE TAPE, RE-MEASURED FOR THIS BUILD ──────────────────────────────
   The full-app audit found the tape cutting data again: taker and maker
   addresses short 32.7px on every filled row, "expires today" short 14, the
   venue name short 24, size short 28 — the same disease fixed once on the
   fix/card-font-selfhost branch, back because that branch never merged and
   the rebuild re-derived its own ratios without measuring content.

   Method identical to that fix, numbers re-measured HERE: per-column
   max-content over 500 live rows, then a ratio vector iterated in the running
   page until the sub-pixel scan reports zero. Age was the donor (56px held
   "16h"; 29 is enough), the share lane gives back 6 (the SHARE control
   measures 65.5 needing 63), and cell padding goes 16 -> 11, which at nine
   columns returns 90px of gutter to content.

   .tw-b carries margin 0 -6px against width:100% — the same box-model slip as
   before: the negative margin slides the box without returning the width, so
   its text loses 2x padding. calc(100% + 22px) compensates at pad 11.

   The identicon drops 17 -> 14 and the address-cell gaps 7 -> 5: at 12px mono
   the address is the datum and the icon is a scent mark, and 3px of icon buys
   6px of hex.

   TIERS: tools.css switches columns on at container 560/820/1060 and those
   thresholds STAY — measured, each visible set now needs 534 / 809 / 960
   against containers of at least 560 / 820 / 1060, so Mehdi's boundaries are
   correct once the ratios stop wasting the row. Only the vectors change, and
   each tier's vector is the same ratio set sliced to its visible columns, so
   a column keeps its share whichever tier it appears in. The base (4-column)
   tier is untouched.

   AGE WAS OVER-DONATED on the first pass and the sharper scan caught it: the
   cell holds its text directly rather than in a child, so the per-child
   measure read it as empty and 0.55fr starved "17m" by 16.4px — the header
   word "Age" clipped too. 0.9fr seats it; the giveback is spread thin across
   act, size, venue and the address pair, all of which the cell-level scan
   confirms still clear.

   Verified zero clipped elements - cell-level AND child-level - at 1440,
   1280, 1000 and 800 across 500 rows, and the share lane still seats its
   65.5px control. */
body .tw-head .tw-c,
body .tw-row .tw-c {
  padding-left: 11px !important;
  padding-right: 11px !important;
}
body .tw-row .tw-b:not(.tw-ab) { width: calc(100% + 22px) !important; }
body .c-taker, body .c-maker,
body .c-taker .tw-b, body .c-maker .tw-b { gap: 5px !important; }
body .c-taker .tw-ic, body .c-maker .tw-ic {
  width: 14px !important;
  height: 14px !important;
}

@container (min-width: 560px) {
  body .tw-grid {
    --tape-cols: 0.9fr 1.75fr 2.75fr 1.75fr 2.05fr 1.8fr 70px !important;
  }
}
@container (min-width: 820px) {
  body .tw-grid {
    --tape-cols: 0.9fr 1.75fr 2.75fr 1.75fr 2.3fr 2.05fr 1.8fr 2.8fr
                 70px !important;
  }
}
@container (min-width: 1060px) {
  body .tw-grid {
    --tape-cols: 0.9fr 1.75fr 2.75fr 1.75fr 2.3fr 2.05fr 1.8fr 2.8fr
                 2.8fr 70px !important;
  }
}

/* ── 7. EDGES DRAWN TWICE, SWEPT AGAIN ────────────────────────────────────
   Three finds from the same sweep, all the one-hairline rule.

   The payoff tool's output box: #py-out fills .py-box edge-to-edge and draws
   its own border on all four sides, doubling the box on three and the .py-bar
   divider on the fourth. Fixed on the unmerged branch, refound here
   unchanged. It carries its own surface; the surface is the boundary. */
body #py-out { border: 0 !important; }

/* The footer: <footer> draws its top rule at y=852 and .foot-bar inside it
   draws another at y=853 — two hairlines one pixel apart on every screen in
   the app. The footer's own rule is the paper-register edge; the inner one
   goes. */
body footer .foot-bar { border-top: 0 !important; }

/* The answer strip: its "See the full cost board →" chip is a bordered pill
   sitting on the strip's own bottom rule (chip bottom 1504.2, rule 1505.2 —
   the strip has no bottom padding). 14px of air separates the object from
   the edge. */
body #answer-strip { padding-bottom: 14px !important; }

/* ── 8. THE BOARD'S FLOOR-COST COLUMN, 5px SHORT AT 1280 ──────────────────
   The compare board's "no offer" label — 17px mono, the value register —
   clipped by 5.2px at a 1,280 viewport: the pane is 1.55fr of a split, so
   every column is narrower there than at 1,440, and the floor-cost column
   was the one that crossed under its content. screener.css declares the
   whole board as one fr vector on --col-layout, which is also the right
   place to fix it: 1.5fr moves from the market-name column (widest, prose,
   ellipsis-tolerant) to floor-cost. Verified at 1280 and 1440: zero clipped
   cells either way, "no offer" included. The data-scope="all" board carries
   its own vector and is untouched. */
body #where-grid:not([data-scope="all"]) {
  --col-layout: 28.5fr 13fr 12.5fr 13fr 12fr 18fr !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   §69 — THE LAUNCH AUDIT: PADDING, CARDS, COHERENCE (2026-08-10)

   "audit the UI in terms of padding, cards and coherence. everything should
    be well audited and fixed before the launch."

   Every number below was measured on RENDERED pixels with Playwright across
   all eight sections at 1440 and 390, never computed from a declared value.
   The census that drove it, before this block:

     axis              before   target   note
     padding values      23        8      2/4/6/8/12/14/18/24
     gap values          17        8      same ladder
     font sizes          13        6      11/13/15/17/22/hero
     font weights         3        2      400/600 (§51)
     border radii         4        2      8/999   (§51 allowed 12; nothing
                                                   needed the third step)
     border colours      17        6      line/line2 + 3 states + glossary
     card surfaces        5        1      one framed panel per section

   The direction is NOT relitigated here. No palette change, no re-theme, no
   section moved, no key renamed. This block only removes VARIANCE: the same
   job done at five values instead of one is the single thing that reads as
   machine-drafted, and it was measurable on every axis.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1. A CARD INSIDE AN IDENTICAL CARD ───────────────────────────────────
   The worst find of the pass, and it shipped on four of the eight sections.

   Measured on Flow, Stats, Tools and Venues:

     div.panel#tools-root       x=264  w=1120  bg #17130f  1px #302a23  pad 24
     section.panel.tl-panel     x=289  w=1070  bg #17130f  1px #302a23  pad 24

   The same surface, the same hairline and the same inset, drawn twice, 25px
   apart. Nothing distinguishes the inner rectangle from the outer one, so it
   reads as a rendering fault rather than as structure, and it costs 48px of
   inset on both edges before any content starts.

   The cause is a naming collision rather than a taste call: §"THE FRAME IS A
   SURFACE" above frames every `.panel` inside a view, and tools.js renders a
   `section.panel.tl-panel` INSIDE the `div.panel` index.html already gave it.
   instrument.css §67/§68 do flatten .tl-panel — at (0,2,2) and (0,3,2) — but
   that rule and this file's `html body .view .panel` are both (0,2,2) and
   controls.css loads last, so the frame came back on the sections that carry
   a panel two levels deep.

   The house rule, from the venue-panel pass that took 50 boxed elements to
   27: separate with SPACE and at most ONE hairline; never draw a container
   around a container. So the OUTER panel keeps the surface and any panel
   nested inside one draws nothing at all. One rule, four sections, and the
   content gains back the 25px it was inset by. */
html body .view .panel .panel {
  padding: 0 !important;
  border: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
}

/* ── 2. THE TYPE SCALE, CLOSED ────────────────────────────────────────────
   §51 fixed the scale and §66 transposed it up to
   11 micro / 13 dense / 15 body / 17 data / 22 heading / hero.
   Rendered, the app was running THIRTEEN sizes. The extras, each measured:

     9.7  the venue monogram inside svg.mk        -> 11
     10   the tape's column headers, .chv-exp-t   -> 11
     12   .op-b .op-chip .chv-exp-d .sh-lede      -> 11
     12.5 .py-out                                 -> 13
     14   .tw-b .p, the Cost board's <b>          -> 13
     16   --t-md, "prose"                         -> 15
     20   the playground's own h4                  -> 17
     31.9 the hero's percent glyph                 -> 22

   --t-md IS THE ONE WORTH ARGUING. It was declared as a separate "prose"
   step at 16px sitting one pixel off the 15px body step. Two sizes a reader
   cannot tell apart are not a hierarchy, they are noise with a comment on
   it; prose takes the body step and the axis loses a value for free. */
:root, :root[data-theme="dark"] { --t-md: 15px; }

html body .tw-head .tw-c,
html body .tw-head button,
html body .tw-head .tw-sort,
html body .tw-head .gl,
html body .vn-head .vn-c,
html body .chv-exp .chv-exp-t,
html body .chv-exp .chv-exp-d,
html body .rl-beta,
html body .op .op-b,
html body .op .op-chip,
html body .sh-top .sh-lede { font-size: 11px !important; }

html body .py-out,
html body .py-out .py-line,
html body .tw-b .p,
html body .op-mx-a,
html body .op-mx-a b,
html body .op-mx-a i { font-size: 13px !important; }

html body .sh-main .tl-sub.first { font-size: 17px !important; }

/* ── 2b. THE LATTICE TOOLBAR NEVER RENDERED AT ITS AUTHORED SIZES ──────────
   Reported: "Firm6" reads as one word and the whole control strip is flat.

   It is flat. Measured on #chain-root, every element whose stylesheet asks
   for a size and does not get it:

     .chv-srclab   "PRICES" / "ONE VENUE"      wants 11   got 15
     .chv-src      Firm / On request / Marks   wants 13   got 15
     .chv-src-n    the count beside each       wants 11   got 15
     .chv-vpin     the venue chips             wants 11   got 15
     .chv-srcwhat  the sentence under them     wants 11   got 15
     .chv-dot .chv-meta .chv-foot              wants 11   got 15
     .chv-dock h3                              wants 13   got 15

   Label, control, count and footnote all at one size is not a toolbar, it is
   a paragraph with buttons in it — and the count is the visible symptom
   because 6px of margin between two glyphs of EQUAL size does not separate
   them, it kerns them. "Firm 6" was authored as a 13px word with an 11px
   tally after it; at one size it is the word "Firm6".

   NOBODY BROKE THIS AND THERE IS NO EARLIER STATE TO GO BACK TO. The sizes
   were never once on screen. instrument.css §17 declares

     body *:not(svg):not(svg *) { font-size: var(--t-sm) !important; … }

   which is deliberate and documented — it collapses the thirteen sizes the
   app had grown into one scale — and it landed on 2026-08-07. chain.css was
   written the NEXT day, on 2026-08-08, in the vocabulary of a scale that rule
   had already made impossible to express: no amount of specificity beats an
   !important without one, so every var(--t-xs) and var(--t-dense) in that
   file has been dead since the moment it was typed. The register's own
   contract is that the role rules win it back at the end of the cascade;
   §2 above does exactly that for the tape, the Cost board and the expiry
   chips. The chain's control strip was simply never added to the list.

   So this is not a revert, it is the missing half of §2. The sizes below are
   chain.css's own numbers, mapped onto the closed ladder: 11 micro for
   labels, counts, chips and footnotes, 13 dense for the source buttons and
   the dock heading. The letter-spacing on the label is restored with it —
   §17 pins that too, and an uppercase 11px label without tracking sets
   tighter than the body text it is supposed to sit above. */
html body .chv-srclab,
html body .chv-src .chv-src-n,
html body .chv-vpin,
html body .chv-srcwhat,
html body .chv-dot,
html body .chv-meta,
html body .chv-foot,
html body .chv-dock-idle p { font-size: 11px !important; }

html body .chv-srclab { letter-spacing: .07em !important; }

html body .chv-src,
html body .chv-dock h3 { font-size: 13px !important; }

/* ── 2c. AND THE CHILDREN THAT WERE ONLY EVER MEANT TO INHERIT ─────────────
   Reported: "size chữ 5d và giờ khác nhau".

   They are. Measured inside one expiry chip:

     .chv-exp-d  "12 Aug"     11px
     .chv-exp-t  "1d 08:00"   11px
     .chv-exp-h  "08:00"      15px   <- inside .chv-exp-t

   So a chip with a clash hour sets its time four pixels larger than the day
   count sitting immediately before it in the same line, and a chip without
   one ("4d", "5d") stays at 11. Same row, three sizes.

   THIS ONE IS NOT EVEN A CONTRADICTED DECLARATION. chain.css gives
   .chv-exp-h opacity and tabular figures and NO font-size at all, on purpose:
   it is a fragment of its parent's line and was written to inherit. The
   register's selector reaches every element in the document, so an element
   that declares no size still gets one, and inheritance stops being available
   as a way to say "same as my parent". §2 restored the parent and could not
   reach the child.

   .chv-lg is the same shape, in the chain's own legend: 15px inside an 11px
   .chv-foot, again with no size of its own anywhere.

   NOT FIXED HERE, AND DELIBERATELY: the footer's odometer digits (.odo-c)
   measure 15px inside a 30px <b>, so the counter the footer is built around
   renders at half the size its wrapper asks for. That is the same fault, but
   correcting it changes how the footer looks rather than restoring a line
   that is visibly inconsistent with itself, so it is reported rather than
   taken. The footer's other children (.tr-count, .tr-since, .byline,
   .tr-link) are all 15px inside an 11px .foot-bar and read consistently
   BECAUSE they all lost the same argument; levelling them to 11 would shrink
   the whole bar, which is a design decision and not this file's to make. */
html body .chv-exp .chv-exp-t .chv-exp-h,
html body .chv-foot .chv-lg { font-size: 11px !important; }

/* AND THE COUNT ON THE ACTIVE CHIP WAS INVISIBLE, WHICH IS THE OTHER HALF OF
   "Firm6". chain.css draws the selected source chip FILLED and therefore sets
   its text to var(--bg) — dark ink on a light pill, correct in that design.
   instrument.css then restyles that chip to an outline:

     html body .chv-exp.on, html body .chv-src.on {
       background-color: transparent !important; color: var(--ink) !important; }

   It re-points the BUTTON's colour with the change, and stops there. The
   child keeps `.chv-src.on .chv-src-n { color: var(--bg); opacity: .7 }`, a
   colour chosen for a fill that no longer exists, so the tally rendered as
   near-black on the dark page: measured contrast 1.04 against the surface
   behind it. Not dim — gone. The word "Firm" read alone and the 6 beside it
   was a smudge.

   This is CONTROLS.md §4's re-pointing trap exactly, one element further
   along than the .chv-exp.on .chv-exp-t case that was closed on 2026-08-10.
   The count takes --ink3 at full opacity: the same relationship to its label
   that every other secondary figure in the app has, and it is a tally, so it
   should sit back from the word without disappearing behind it.

   .chv-vpin.on is NOT in this repair and must not be: measured, it keeps its
   orange fill and its dark text reads at 7.58:1. Only the source chip was
   unfilled, so only the source chip's child was orphaned. */
html body .chv-src.on .chv-src-n {
  color: var(--ink3) !important;
  opacity: 1 !important;
}

/* The percent sign after the hero number. It was .42em of a clamp()ed 76px,
   i.e. 31.9px rendered — a size that exists nowhere else and that nobody
   chose. Pinned to the heading step so the glyph lands on the ladder at
   every width instead of tracking a fraction of it. */
html body .mn-hero-n em { font-size: 22px !important; }

/* ── 3. WEIGHTS: 400 AND 600, AND THAT IS ALL ─────────────────────────────
   One violation on the whole page and it was invisible to a CSS census
   because it is an SVG presentation attribute: the two-letter venue monogram
   in marks.js ships font-weight="700". Corrected at source, and this is the
   belt for the braces. */
html body svg.mk text { font-weight: 600; }

/* ── 4. RADII: 8 AND 999 ──────────────────────────────────────────────────
   Four rendered. `2px` appeared only on the coverage bar's three segments,
   which sit inside a 999px pill with overflow hidden — a radius that cannot
   be seen and was never a decision. `12px` appeared only on .py-box and the
   Premium tier, two boxes in one section rounding differently from every
   other bordered box in the app. */
html body .sc-hl-bar i,
html body u.f-tr, html body u.f-bl, html body u.f-un { border-radius: 0 !important; }
html body .sh-main .py-box,
html body .sh-tier { border-radius: 8px !important; }

/* ── 5. ONE HAIRLINE COLOUR, NOT SEVEN ────────────────────────────────────
   Seven different translucent whites were each drawing "a hairline". Composited
   over --s1 (#17130f), which is the surface they all sit on, they measure:

     rgba(255,255,255,.055)  -> #201d1a   .sc-h, the board cells
     rgba(248,238,226,.075)  -> #241f1a   .mn-chips, .mn-chart, .op-b, .op-chip
     rgba(255,255,255,.07)   -> #232120   #py-src, .py-bar, .sh-pitch, .sh-soon
     rgba(255,255,255,.09)   -> #262422   .py-box
     rgba(255,255,255,.106)  -> #292420   .sh-tier
     rgba(255,255,255,.12)   -> #2b2926   .py-chip, .vn-c
     rgba(248,238,226,.157)  -> #302b25   the "on" state of a chip

   Sixteen levels of grey across the whole set, and --line is #302a23, which
   sits inside that spread. Nobody can see the difference between any two of
   them; what they DO produce is seven near-misses that never quite agree at
   an intersection. Resting edges take --line, the selected state takes
   --line2, and the three state hues (accent, jade, rose) plus the glossary's
   --ink3 underline are untouched — those encode meaning. */
html body .sc-h,
html body .sc-c, html body .cell.sc-c,
html body .mn-chips, html body .mn-chart, html body .mn-board-h,
html body .op .op-b, html body .op .op-chip, html body .op .op-chip.fact,
html body .sh-main .py-box,
html body .sh-main .py-box textarea,
html body .sh-main .py-bar,
html body .sh-main .py-chip,
html body .sh-pitch, html body .sh-soon,
html body .sh-tier,
html body .vn-c { border-color: var(--line) !important; }

html body .sc-h.on,
html body .op .op-b.on,
html body .op .op-chip.on,
html body .vn-head .vn-c { border-color: var(--line2) !important; }

/* The hero's venue button was the only object wearing the accent at 45%
   opacity; the accent is one colour and it is not graded. */
html body .mn-hero-v { border-color: var(--accent) !important; }

/* "12 not checked" carried an 11px left padding and a 1px left border, which
   at the end of a sentence renders as a stray `|` glyph in the middle of a
   line of prose — read as a typo on the venue registry. The count is already
   coloured; the rule said nothing the colour did not. */
html body .tl-warn {
  border: 0 !important;
  padding-left: 0 !important;
}

/* ── 6. THE MOSAIC'S RINGS WERE THE WRONG COLOURS ─────────────────────────
   stats.css builds the cell edge from `color-mix(--mint 42%)` for BUY and
   `color-mix(--good 40%)` for SELL. instrument.css §28 later repaints the
   FILLS from --pos / --neg and adds a 2px left bar, but never touched the
   other three sides, so the rendered result was:

     .ms-c.buy   fill jade   left bar jade   top/right/bottom  rgb(255,122,26)
     .ms-c.sell  fill rose   left bar rose   top/right/bottom  rgb(90,174,135)

   The "sold protection" rectangle was ringed in the POSITIVE direction's
   green, and the "bought" rectangle was ringed in the brand accent — two
   extra accent instances on a page whose rule is one. Both sides now carry
   one hue on all four edges, the hue the fill and the figure already use. */
html body .ms-c.buy  { border-color: var(--pos) !important; }
html body .ms-c.sell { border-color: var(--neg) !important; }

/* ── 7. THE PADDING LADDER: 2 / 4 / 6 / 8 / 12 / 14 / 18 / 24 ─────────────
   Twenty-three distinct rendered values, and the giveaway is that most of
   them differ from a neighbour by ONE pixel: 3 and 4, 5 and 6, 7 and 8,
   9 and 8, 10 and 11 and 12, 13 and 14, 22 and 23 and 24 and 26. No reader
   perceives a one-pixel inset difference; what they perceive is that nothing
   in the app lines up with anything else.

   Each selector below was measured individually and moved to the NEAREST
   rung, never more than 4px, so no control changes size enough to reflow.
   Grouped by target so the ladder is readable as a ladder. */

/* 2 */
html body .sc-mk { padding: 2px !important; }

/* 4 */
html body code { padding: 2px 4px !important; }
html body .chv-vpin { padding: 4px 8px !important; }
html body .mn-ch-tfs { padding: 4px !important; }
html body .chv-src { padding: 4px 12px !important; }
html body .tl-pill { padding: 4px 8px !important; }

/* 6 */
html body .sc-b { padding: 4px 6px !important; }
html body .chv-exp { padding: 6px 8px !important; }
html body .cs-btn { padding: 6px 12px !important; }
html body .next-step { padding: 6px 12px !important; }
html body .mn-ch-tf { padding: 6px 12px !important; }
html body .cell.sc-id { padding: 6px 8px 6px 12px !important; }
html body .tw-blind { padding: 6px 0 6px 12px !important; }
html body .py-state { padding: 6px 8px !important; }
html body .tl-btn { padding: 6px 12px !important; }
html body .ms-col-h { padding: 0 2px 6px !important; }
html body .op-cell { padding: 6px 24px 6px 6px !important; }
html body .sh-list li { padding: 6px 0 6px 18px !important; }

/* 8 */
html body .sc-h { padding: 8px !important; }
html body .py-ghost { padding: 8px 12px !important; }
html body .op .op-b,
html body .op .op-chip,
html body .op .op-chip.fact { padding: 8px 12px !important; }

/* 12 */
html body .chv-srcbar { padding: 12px 0 0 !important; }
html body .chv-foot { padding: 12px 0 0 !important; }
html body .op-ctl { padding: 12px 0 !important; }
html body .fl-wrap { padding: 4px 0 12px !important; }
html body .legend { padding: 12px 0 0 !important; }
html body .cell.sc-c { padding: 12px 8px !important; }
html body .ms-c { padding: 12px !important; }
html body .chv-lg b { padding-left: 12px !important; }

/* 14 */
html body .chv-dock { padding: 14px !important; }
html body .vn-c { padding: 12px 14px !important; }

/* 18 */
html body .sh-main .py-box textarea { padding: 18px !important; }
html body .rowsub { padding-top: 18px !important; }
html body .sh-tier { padding: 18px !important; }

/* 24 */
html body .mn-chips { padding-top: 24px !important; }
html body .mn-chart { padding-top: 24px !important; }
html body #answer-strip { padding-top: 24px !important; }
html body .ov-ctx { padding-bottom: 24px !important; }
html body .mn-board-h { padding-top: 24px !important; }
html body .op { padding-bottom: 24px !important; padding-top: 18px !important; }

/* ── 8. THE GAP LADDER, SAME RUNGS ────────────────────────────────────────
   Seventeen values, and the same one-pixel neighbours: 1/2, 3/4, 5/6, 7/8,
   9/10, 14/16, 20/22, 26/34. A gap is the most visible ruler in a layout
   because it repeats; seventeen of them is seventeen different rhythms on
   one page. */
html body .sc-nm, html body .cell.sc-c, html body .sc-b { gap: 2px !important; }
html body .sc-h, html body .tw-sort,
html body .op-bs, html body .op-cell { gap: 4px !important; }
html body .chv-exps, html body .sc-t, html body .tw-tab,
html body .c-taker, html body .c-maker,
html body .c-taker .tw-b, html body .c-maker .tw-b,
html body .op .op-chip, html body .lg { gap: 4px !important; }
html body .chv-srcbar, html body .cs-btn, html body .mn-ch-read,
html body .tw-b, html body .op-mx-a { gap: 6px !important; }
html body .chv-dock, html body .cell.sc-id, html body .sh-pro-h { gap: 8px !important; }
html body .chv-ctx, html body .mn-ch-r, html body .sc-hl-key,
html body .op-sub-h, html body .op-div, html body .ms { gap: 12px !important; }
html body .ov-ctx { gap: 12px 14px !important; }
html body .chv-body { gap: 14px !important; }
html body .chv-foot { gap: 6px 18px !important; }
html body .mn-ch-h { gap: 18px !important; }
html body .mn-hero-fig { gap: 24px !important; }
html body .sc-hl-key { gap: 24px !important; }
html body .sh-split { gap: 24px !important; }
html body #sc-headline { gap: 24px !important; }

/* ── 9. THE TAPE TAKES THE LADDER TOO ─────────────────────────────────────
   §6 above measured the tape's cell inset down to 11px to buy 90px of gutter
   at nine columns, and states the per-tier requirement: each visible set
   needs 534 / 809 / 960 against containers of at least 560 / 820 / 1060.
   The tightest tier therefore carries 26px of slack and the step from 11 to
   12 costs 18px across nine columns, so the ladder fits inside the margin
   §6 itself established. Re-verified after the change: zero clipped cells at
   1440, 1280 and 390. */
html body .tw-head .tw-c,
html body .tw-row .tw-c {
  padding-left: 12px !important;
  padding-right: 12px !important;
}
html body .tw-row .tw-b:not(.tw-ab) { width: calc(100% + 24px) !important; }

/* ── 10. THE TOOLS SECTION IS BROKEN ON A PHONE ───────────────────────────
   Rendered at 390 and read as a PNG, not asserted from the DOM: the heading
   "Real Python, in your browser" is printed ON TOP of the Premium card. Two
   blocks of live text overlapping, on the section that carries the paid
   surface.

   The cause is a CSS ordering fault, not a layout one. instrument.css §39
   declares the one-column collapse

       @media (max-width: 900px) { body .sh-split { grid-template-columns: minmax(0,1fr) } }

   and then §40, FOURTEEN LINES LOWER and outside any query, redeclares

       body .sh-split { grid-template-columns: minmax(0,2.6fr) minmax(230px,.85fr) }

   A media query adds no specificity. Both rules are (0,2,1), so the later one
   wins at every width and the phone kept the two-column grid: the console
   column resolved to ~60px and the 230px aside overlapped it. The collapse
   has therefore never run since §40 landed.

   Restated here, after §40 in source order and one class higher, so it cannot
   be out-ordered again. */
@media (max-width: 900px) {
  html body .sh-split {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

/* The playground preset chips hover to #f0b90b. That is Binance's gold to the
   digit, and it is the exact value §63 removed for being borrowed and §69's
   own token block re-pointed --focus away from. It survived on a :hover, which
   is why no static census caught it. The one accent is the brand accent. */
html body .sh-main .py-chip:hover {
  background: var(--accent) !important;
  border-color: var(--accent) !important;
  color: var(--on-brand, #17130f) !important;
}
html body .op-mx-r.op-focus .op-mx-a { box-shadow: inset 3px 0 0 var(--accent); }

/* ── 11. THE VENUE REGISTRY CUT FIVE OF ITS OWN CELLS ─────────────────────
   Measured at 1440, with 1,024px of table on screen and slack to spare:

     "Derive Chain (OP Stack)"   needs 199   has 179
     "Paradex L2 (Starknet)"     needs 184   has 179
     "none, so unknown"  (x3)    needs 157   has 152

   So the registry rendered "Derive Chain (OP S…" and, three times, "none, so
   …" — a truncation that destroys the sentence, because the whole point of
   that cell is the word "unknown" and the ellipsis eats it. On a page whose
   argument is that an absence must be legible as an absence, that is the
   worst cell in the app to cut.

   The track vector was authored, not measured: network got 1.3fr while the
   two numeric columns got 0.6 and 0.7 for content that is never wider than
   five characters. Re-cut from max-content: venue 140, trade 130, network
   227, assets 60, contracts 78, prices 150, feed 185 = 970 inside 1,024, and
   inside 1,072 once §1 above stops drawing the second frame. */
html body .vn-table {
  grid-template-columns:
    minmax(120px, 1.45fr)   /* venue        */
    minmax(110px, 1.35fr)   /* how you trade */
    minmax(150px, 2.35fr)   /* network      */
    minmax(56px,  0.62fr)   /* assets       */
    minmax(72px,  0.80fr)   /* contracts    */
    minmax(120px, 1.55fr)   /* prices       */
    minmax(84px,  0.85fr)   /* firm quotes  */
    minmax(130px, 1.60fr)   /* trades 24h   */
    minmax(120px, 1.40fr) !important;  /* counterparties */
}

/* ── 12. THE CHAIN'S ASSET PICKER IS A RAW OS CONTROL ─────────────────────
   chain.css styles #chv-asset with the app's surface, hairline and radius,
   and the computed background does read rgb(32,27,22). It renders as a light
   grey native dropdown anyway — verified in the PNG at both widths — because
   the element never sets `appearance`, and with the native appearance left on,
   the engine paints its own button chrome over the author background.

   Compare's picker on the very next section is a custom listbox (.cs-btn), so
   the two controls that do the identical job wore two different design
   languages, one of which was the operating system's.

   The caret is drawn rather than left to the engine, for the same reason. */
html body #chv-asset.chv-sel {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 24px !important;
  background-color: var(--s2);
  background-image: linear-gradient(45deg, transparent 50%, var(--ink3) 50%),
                    linear-gradient(135deg, var(--ink3) 50%, transparent 50%);
  background-position: calc(100% - 14px) 14px, calc(100% - 9px) 14px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
html body #chv-asset.chv-sel option { background: var(--pop); color: var(--ink); }

/* ── 13. THE RECORDING COUNT BREAKS ACROSS TWO LINES ──────────────────────
   At 390 the footer prints

       1,704,87
       2

   The site's strongest single claim, hyphenated by the line breaker at a
   thousands separator. It renders on EVERY section because the band is fixed
   chrome, so this is eight broken screens, not one. A formatted figure is one
   word. */
html body .tr-count, html body .tr-count b,
html body .tr-since, html body .tr-since b { white-space: nowrap; }

/* ── 14. THE COST BOARD'S ASSET COLUMN OVERFLOWS AT 390 ───────────────────
   opps.css narrows the asset track to 104px under 720. Measured content in
   that track: "ZEC $504.2" 118, "BTC $64,504.5" 141, "XAUT $4,301.24" 152 —
   every row overruns, and the overrun lands on top of the 14d figure, so the
   asset's spot price and the first tenor's cost print over one another.

   Widening the track is not available: three tenor columns plus the asset
   already need more than a 390px screen holds, which is why the matrix
   scrolls. So the cell wraps instead — ticker on the first line, spot under
   it — which fits 104px with room and keeps both facts. */
@media (max-width: 720px) {
  html body .op-mx-a {
    flex-wrap: wrap;
    row-gap: 0 !important;
    align-content: center;
  }
  html body .op-mx-a i { margin-left: 0 !important; flex: 0 0 100%; }
}

/* ── 15. THE TAPE'S LAST ROW IS SLICED THROUGH THE TEXT ───────────────────
   Measured: .tw-grid is the scroll viewport at 420.47px and a row is exactly
   44px by design (tools.css states it and explains why). 420.47 / 44 = 9.56,
   so the tenth row is cut at 24px — far enough down to render its first line
   whole and slice its second line in half horizontally. In the PNG that is
   indistinguishable from a text-rendering fault, and it sits directly above
   the footnote, which makes it read as the end of the table rather than as
   more content below.

   round() takes the viewport down to a whole row count, so the last visible
   row is always complete. It is a progressive enhancement on purpose: an
   engine without round() drops the declaration and keeps today's behaviour
   rather than inheriting a broken height. */
@supports (height: round(down, 100px, 44px)) {
  html body .tw-grid { max-height: round(down, 100%, 44px); }
}

/* ── 16. THE LADDER, WHERE IT HAD TO OUT-SPECIFY ──────────────────────────
   Everything in §7 and §2 that a (0,1,2) selector could not reach. Each of
   these lost to a rule whose class count was higher, not to source order, so
   the answer is the parent class rather than another !important:

     body .op .op-mx-r > *          (0,2,2)  beat  html body .op-mx-a  (0,1,2)
     body .sh-main .py-chip         (0,2,1)  beat  html body .py-chip  (0,1,2)
     body .sh-pro-h h4              (0,1,2)  tied and won on source order

   Where the owning file is one of ours and the value is authored in one
   place, the number was corrected AT SOURCE instead (chain.css, tools.css,
   opps.css, instrument.css) — those edits carry their own §69 note. What is
   left here is the set where the source value is still wanted at another
   width or in another state. */
html body .op .op-mx-r .op-mx-a { padding-left: 8px !important; padding-right: 8px !important; }
html body .op .op-mx-r .op-mx-c.empty { padding-left: 4px !important; padding-right: 4px !important; }
html body .sh-split .sh-main .py-chip { border-radius: 999px !important; }
html body .sh-side .sh-tier .sh-pro-h { gap: 8px !important; }

/* ── 17. THE LAST FOUR VALUES ─────────────────────────────────────────────
   The chain's index row is a bare <td colspan="7"> and so misses every
   .chv-c rule; it kept 3px/9px while the cells around it moved to 4/8.
   The Premium list and the inline <code> were the only 12px type left.
   And the head label over the Cost board's figure column was inset 23px
   against a reserved pin lane of 30 — a 7px disagreement between a label
   and the column it names, now the same number in both places. */
html body .chv .chv-t td[colspan] { padding: 4px 8px !important; }
html body .sh-list li,
html body .sh-side code,
html body .sh-main code { font-size: 13px !important; }

/* The last two hairline colours. The chain's selected expiry and selected
   price-class wore rgba(248,238,226,.157) — the seventh translucent white,
   which composites to #302b25 against a --line2 of #423a31 — and the tools
   badge wore rgba(248,238,226,.075). Selected takes --line2 like every other
   selected control; the badge takes --line like every other resting edge.
   Border colours on the page: --line, --line2, accent, jade, rose, and the
   glossary's --ink3 underline. Six, and each of the last four means something. */
html body .chv .chv-exp.on,
html body .chv .chv-src.on { border-color: var(--line2) !important; }
html body .sh-side .sh-badge,
html body .sh-top .sh-badge { border-color: var(--line) !important; }

/* ── 18. THE PLAYGROUND'S CONTROL BAR SPOKE THREE LANGUAGES ───────────────
   Measured in the bar, left to right:

     NOT LOADED   11px mono, uppercase, boxed, 1px border   (a status, not a control)
     RUN          11px mono, uppercase, boxed, 1px border
     Clear        15px sans, sentence case, boxed, 1px border
     Expand       15px sans, sentence case, boxed, 1px border

   Four objects, three type treatments and one of them is not a button at all
   but is drawn exactly like one. A reader cannot tell what is clickable.

   The state stops wearing a border — a status is text, and the app already
   states elsewhere that a box means "you can press this". The three real
   buttons take one size, one case and one shape. */
html body .sh-main .py-bar .py-state {
  border: 0 !important;
  background: transparent !important;
  padding-left: 0 !important;
  color: var(--ink3) !important;
}
html body .sh-main .py-bar .tl-btn,
html body .sh-main .py-bar .py-ghost {
  font-size: 11px !important;
  text-transform: uppercase;
  letter-spacing: .08em;
  padding: 6px 12px !important;
  border: 1px solid var(--line) !important;
  border-radius: 8px !important;
}

/* And the bar itself has to survive 390: four controls plus a hint in a
   250px column overran by 234px, measured, so the hint drops out below the
   console width it describes and the row wraps instead of clipping. */
@media (max-width: 720px) {
  html body .sh-main .py-bar { flex-wrap: wrap; row-gap: 8px !important; }
  html body .sh-main .py-bar .py-hint { display: none; }
}

/* ── 19. THE VENUE TABLE'S NUMERIC HEADERS ────────────────────────────────
   §11's re-cut fixed the five clipped VALUES and then clipped two LABELS
   instead: "ASSE…" and "CONTRAC…". A numeric column whose header word is
   longer than any of its figures has to be sized on the header, not on the
   data. Re-measured on the labels: Assets 76, Contracts 100 including inset;
   the difference comes off network, which now has 229 against a need of 227. */
html body .vn-table {
  grid-template-columns:
    minmax(120px, 1.40fr)   /* venue        */
    minmax(178px, 1.30fr)   /* how you trade — "reference marks only" is
                                    the longest value and must not clip */
    minmax(204px, 2.10fr)   /* network. MEASURED, twice: the string
                               "Derive Chain (OP Stack)" reports scrollWidth
                               199px in a cell carrying 14px of padding per
                               side, so a 176px track still clipped it — the
                               track has to cover the text AND the padding.
                               Setting this from a guess is what produced two
                               failed passes; it is set from the measurement  */
    minmax(70px,  0.70fr)   /* assets       */
    minmax(94px,  0.95fr)   /* contracts    */
    minmax(104px, 0.90fr)   /* prices — measured: "not public" needs 98px  */
    minmax(88px,  0.80fr)   /* firm quotes  */
    minmax(126px, 1.45fr)   /* trades 24h   */
    minmax(138px, 1.40fr) !important;  /* counterparties — header is long   */
}

/* ── 20. THE TAPE'S PARTIAL LAST ROW, PROPERLY ────────────────────────────
   §15's round() was wrong and the PNG proved it: `max-height: round(down,
   100%, 44px)` resolves 100% against the PARENT's 567.8px, not against the
   421.6px the flex line actually gave the viewport, so it computed 550 and
   never bound. Measured after the change: still 421.56px, still 9.58 rows,
   last row still sliced.

   There is no CSS expression for "the height flex just handed me", so the
   partial row cannot be removed without JS. What CAN be fixed is that it
   reads as a slice rather than as continuation. The bottom 24px of the
   viewport fades out, which is the standard scroll affordance and is honest:
   the row is still there, still scrollable, and no longer looks like text
   the renderer cut in half.

   SCROLL SNAPPING WAS TRIED HERE AND REVERTED, and the reason is worth
   keeping so it is not tried again. `scroll-snap-type: y proximity` with
   `scroll-snap-align: start` on the rows looked like the tidy answer for the
   top edge. Measured after adding it: .tw-grid rested at scrollTop 42, which
   is exactly the height of .tw-head — the header is `position: sticky` INSIDE
   this scroller, so a row snapped to the CONTAINER's start lands underneath
   it. The newest fill, the first row of the section the whole product rests
   on, was hidden. Verified both ways on the live page: snap on -> scrollTop
   42, snap off -> scrollTop 0. The mask alone fixes the defect that was
   actually reported. */
html body .tw-grid {
  max-height: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent);
}

/* ── 21. THE COST BOARD'S CONTROLS ATE THE PHONE ──────────────────────────
   Measured at 390: the four control rows plus the five fact chips occupy
   460px before the first figure, on an 844px screen with 180px of fixed
   footer. The board began below the fold, so the section rendered no data at
   all on arrival.

   The facts are context, not controls, so they stop wrapping to three lines
   and scroll sideways on one — 92px back, which puts the first two rows of
   the board on screen. The filters keep their rows: those are the controls
   the section exists to drive. */
@media (max-width: 720px) {
  html body .op .op-chips {
    flex-wrap: nowrap !important;
    overflow-x: auto;
    padding-bottom: 6px !important;
    scrollbar-width: none;
  }
  html body .op .op-chips::-webkit-scrollbar { height: 0; }
  html body .op .op-chips > * { flex: none; }
}

/* ── 22. TWO MORE THINGS THAT OVERRAN A PHONE ─────────────────────────────
   Both measured at 390, both inside a box that does not scroll.

   .op-div is the "below: at least one leg quoted on request" separator. It is
   a flex row of rule / label / rule with the label set nowrap, so at 379px of
   text in a 300px box the rules collapse to nothing and the sentence runs out
   of the section. It wraps on a phone; the rules go, because a rule either
   side of a two-line label is decoration around a paragraph.

   .ms-top is the mosaic's figure line, "$12.5K  21%". 96px of content in an
   86px cell, so the percentage clipped against the cell's own edge. The two
   parts stack instead — the same wrap the Cost board's asset column takes. */
@media (max-width: 720px) {
  html body .op .op-div {
    display: block;
    white-space: normal;
  }
  html body .op .op-div span { white-space: normal; }
  html body .op .op-div::before,
  html body .op .op-div::after { display: none; }

  html body .ms .ms-top { flex-wrap: wrap; row-gap: 2px !important; }
}

/* ── 23. THE COST FIGURE OVERHUNG ITS OWN CELL ON A PHONE ─────────────────
   Not caught by an overflow check, because nothing scrolls: the figure simply
   paints outside the box. Measured at 390:

     cell                84px
     pin lane needed     31px   (3px inset + 26px control + 2px air, §1's rule)
     "$0.900" at 13px    59px
                         ───
                         90px  into 84

   So the value was pushed 6px LEFT of its own cell edge and the "$" printed
   under the asset column. It is arithmetic, not styling: at 84px the figure
   and the pin cannot share a line, and no lane width fixes that.

   They stop sharing one. The pin keeps its full 26px target — it is the only
   touch target in the cell and shrinking it on the device that needs it most
   is the wrong trade — and the figure takes the line below it, with the lane
   released back to the ladder's 6px. Costs 14px of row height on a phone and
   returns the whole cell width to the number the section exists to show. */
@media (max-width: 720px) {
  html body .op .op-mx-c.val .op-cell {
    padding-top: 20px !important;
    padding-right: 6px !important;
  }
}

/* Same cell, same arithmetic, the line below. `.op-unit` is `white-space:
   nowrap` and "cap bid · on request" measures ~148px against an 84px cell, so
   it hung 64px to the LEFT of its own cell and printed across the asset
   column. Measured, not eyeballed. It wraps on a phone; nowrap is a desktop
   affordance for a column that is 303px wide there. */
@media (max-width: 720px) {
  html body .op .op-mx-c.val .op-unit {
    white-space: normal !important;
    line-height: 1.3;
  }
}

/* ── 24. THE TAPE SHOWED NO FILLS AT ALL ON A PHONE ───────────────────────
   The worst defect in this audit, and it is not a padding problem. Rendered
   at 390 and read as a PNG: the Trades section prints its title, its sub, the
   coverage notice, the window tabs, the column header and the footnote — and
   NOT ONE ROW. 2,000 rows are in the DOM. Zero are on screen.

   The arithmetic, measured on the live page at 390:

     .tape-panel   516px   (flex column)
     #tape         305px   (flex: 1 1 auto)
      .tw          295px
       .tw-blind   164px   the "which feeds we index" notice, 5 lines at 390
       .tw-tabs     68px   Latest / 24H / 6H / 1H, wrapped to two rows
       .tw-head     42px   the column header
                   ─────
                   274px
       .tw-grid      0px   flex: 1 1 auto, min-height: 0  <- everything left

   So the scroll viewport resolves to zero and the fills — the evidence this
   entire product rests on, and the section the rail calls "Every fill we can
   see, live" — are invisible on a phone. It is pre-existing: the before/after
   screenshots both show it, so it is not a regression from this pass.

   NOTHING IS CUT TO FIX IT. The notice is an honesty disclosure and stays at
   full length. Instead the panel stops being a fixed-height flex column on a
   phone and sizes to its content, which main.stage already scrolls, and the
   viewport gets a floor of eight rows so it can never resolve to nothing
   again. 60vh caps it so the tape does not become the entire page. */
@media (max-width: 720px) {
  html body .tape-panel { display: block !important; }
  html body #tape { height: auto !important; overflow: visible !important; }
  html body .tw { display: block !important; }
  html body .tw-grid {
    min-height: 352px;          /* 8 rows at the stated 44px */
    max-height: 60vh;
  }
}

/* ── 25. "WHAT HAPPENED" RENDERED AS "T HAPPEN" ───────────────────────────
   The known-unresolved "text clipping in trades rows", and it is not a width
   problem. Measured on the header at 390:

     cell            track    content   text-align   justify-content
     Age              24px      35px      start          center
     What happened    59px      90px      start          center
     Premium          35px      35px      right          center

   `.tw-c` is `display: flex`, so `text-overflow: ellipsis` cannot apply to
   the anonymous flex item holding the text, and `justify-content: center`
   then centres content that is wider than its box — which clips it at BOTH
   ends. "What happened" came out as "t happene" and "Premium" as "m". A word
   cut at the front reads as a rendering fault, not as truncation; a word cut
   only at the end reads as a column that is narrow.

   Two changes, both measured, neither touching which columns appear at which
   container width — those tiers are the owner's and they stay:

     1. Cells pack to their own alignment edge, so the cut is at one end.
     2. The asset track's hard 104px floor drops to 72px. Its widest content
        is a 14px mark plus a five-character ticker, measured at 68px, and at
        300px of row that floor was taking 35% of the table for a column that
        needed 23%. The 32px goes to "What happened", which needed 90 and had
        59, and to Age, which needed 35 and had 24. */
html body .tw-head .tw-c,
html body .tw-row .tw-c { justify-content: flex-start !important; }
html body .tw-head .tw-c.num,
html body .tw-row .tw-c.num { justify-content: flex-end !important; }
html body .tw-grid { --tape-cols: 1fr minmax(72px, 1.9fr) 3.4fr 2fr var(--tape-share); }

/* And on a phone the inset itself is what does not fit. With the header words
   measured bare — Age 11px, Asset 48, "What happened" 76, Premium 23 — the
   four columns plus the 76px share lane need 234px of content in a 300px row.
   At §7's 12px inset that is 234 + 96 = 330 and two headers are cut; at the
   ladder's 6px rung it is 282 and none are. The desktop tiers keep 12: they
   have the room, and §6's measured column fit is stated against it. */
@media (max-width: 720px) {
  html body .tw-head .tw-c,
  html body .tw-row .tw-c {
    padding-left: 6px !important;
    padding-right: 6px !important;
  }
  html body .tw-row .tw-b:not(.tw-ab) { width: calc(100% + 12px) !important; }
  /* The asset track's floor is set on its widest CONTENT, re-measured after
     the share lane came back (§26): a 14px venue mark, a 4px gap and a
     five-character ticker ("MSFT" measures 31px at the 13px mono step) inside
     6px of inset is 61px, and the button it sits in runs 12px wider than its
     cell by design, so 76 is the first value at which no ticker is cut. */
  html body .tw-grid { --tape-cols: minmax(24px, 1fr) minmax(76px, 1.9fr) 3.4fr 2fr var(--tape-share); }
}

/* ── 26. A QUARTER OF THE PHONE ROW WAS RESERVED FOR NOTHING ──────────────
   The last of it, and the clearest measurement in the audit.

   `--tape-share: 76px` is a reserved lane so the floating share control never
   lands on the maker column — a real rule, and tools.css is right to state it.
   But the control it reserves for is `.tw-ts`, and at the base container tier
   `.tw-ts` computes `display: none`. Verified on the live page at 390: the
   lane is 76px wide, its occupant is not rendered, and the four columns that
   ARE rendered are sharing the 224px left over. That is 25% of the row held
   for an element that is not there.

   Released at the tier where the control does not exist, and the rule stays
   in force at every tier where it does. Written as a CONTAINER query, not a
   media query, so it tracks the same boundary tools.css uses to switch the
   columns on rather than a viewport width that only approximates it.

   After: Age 30 needs 30, "What happened" 98 needs 98, Premium 51 needs 51,
   Asset 60 needs 60. Nothing in the header is cut at 390 for the first time. */
@container (max-width: 559px) {
  html body .tw-grid { --tape-share: 0px; }
  html body .tw-grid .tw-ts { display: none; }
}

/* ── 27. A NUMBER CUT FROM THE LEFT LOSES ITS DIGITS ──────────────────────
   The last one, and it is the same class of fault as §25 one level down.

   .tw-c is a flex container; §25 packs the numeric ones to flex-end so short
   values right-align under their header. When a CHILD is wider than the cell,
   flex-end pushes it out to the LEFT instead: at 390 the premium sub-line
   "0.24% of cover" measures 78px in a 72px cell and rendered as "4% of cover"
   — the leading digits, which are the only part that matters, cut off, and no
   ellipsis anywhere to say so.

   A flex item cannot ellipsize while it is allowed to exceed its line, so the
   children are capped at the cell and ellipsize themselves. The value keeps
   its right alignment, an over-long child now reads "0.24% of c…", and no
   number is ever cut at the significant end.

   AND IT MUST NOT BE `max-width: 100%`. That was the first attempt and it
   regressed the desktop view immediately: §6 gives `.tw-b` a width of
   calc(100% + 24px) on purpose, to give back the width its negative margin
   takes, and capping every child at 100% of the cell undid exactly that —
   measured at 1440, "bought the upside" became "bought the upsi…",
   "Hypercall" lost its last letter and "expires today" became "expires t…",
   on a row that had room for all three. `min-width: 0` is what a flex item
   needs in order to shrink; it does not touch an intentional width. */
html body .tw-head .tw-c > *,
html body .tw-row .tw-c > *,
html body .tw-row .tw-c .tw-b > b,
html body .tw-row .tw-c .tw-b > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The one cut that remains at 390 is real and not fixable by reallocation:
   the longest ticker in the set ("SP500", 38px at the 13px mono step) against
   a 32px slot inside a 76px asset track, where widening the track takes the
   pixels straight off "What happened", which has none to give. It ellipsizes
   to "SP50…" rather than being sliced, which is the honest rendering of a
   four-column table on a 390px screen. Every column HEADER is legible in
   full, and no figure is cut at its significant end. */
