/* ==========================================================================
   app.css — simpler.run · THE WIDE FLOW SHELL (rebuilt whole, 2026-08-19;
   re-skinned to the sign, 2026-09-03; put on paper, 2026-09-06)

   The previous file was four generations deep — the ask screens, the board,
   the withdrawn panels, then two cutover blocks pasted on the end — with
   ~600 lines styling markup that no longer existed and 46 rules spending
   amber on decoration. This file starts from the repo's own written law,
   as the landing re-stated it on 2026-09-06:

   THE GROUND IS PAPER. THE YELLOW IS THE MARK. The owner's ruling that day
   ("paper ground, yellow as the mark") put the landing on #F6F3EA with the
   sign yellow spent only where something IS a mark — the primary button,
   the caution block, the hazard hatch, the brand tile — and the app follows
   on the same tokens, verbatim, with the landing's measured ratios. For
   three days (2026-09-03 to 09-06) this sheet was the sign itself, a yellow
   field with black severity; the ruling reversed the ground and kept the
   rule underneath it, which is the rule it has always been: fix dilution,
   never remove the yellow. Concentrated, the yellow means something. As a
   field it meant nothing in particular.

   So: the rail, the bars, the drawer and the tools are PAPER, ruled in the
   hairline, with the recess (--track-bg) for a lit row, a slot or a form's
   cell; the ONE primary button on each screen is the sign under the ink
   (.btn — the landing's .go); and the ONE caution mark is black wearing
   the brightest yellow (.badge.ask). Clause 418 A8 in tests/test-landing.js
   holds the list of selectors allowed to spend either yellow.

   THE MAP PANE STAYS THE PLATE, and that is the one deliberate exception:
   the island is shaded by a red-to-green ramp ("keep red to green", owner,
   2026-09-03) and a data ramp reads against a dark, quiet ground. Everything
   that sits ON the map — the key, the zoom stack, the hover readout, the
   credits — and the one thing that floats over the page, the tooltip, is a
   plate with the plate's own inks: the cordon plate landing.css carries.

   THE CAUTION MARK IS BLACK WEARING THE MARK YELLOW. In this app exactly one
   thing is a caution: the ASK badge, a landlord's opening position. On paper
   the mark yellow is 1.27:1, invisible, so severity is carried the way the
   landing carries it — a black plate, and on the plate #FFD60A at 13.13:1.
   The old --amber (#E0A33E) is long retired; its 2026-09-03 heir --sunk, the
   sign's recessed tint, retired with the field, and the recess is now the
   landing's own --track-bg. --plate-2 went the same day: nothing on paper
   needed a second black, and a hover on the plate is a keyline now.

   TYPE follows the two-voice rule proven in ui-v2: MONO carries what the
   RECORD says (instrument names, grains, dates, planning-area names);
   SANS carries what WE say; SERIF carries the one judgement headline.
   Numbers that can change are tabular. The sans stack is the landing's —
   Inter is dropped for the same reason it was dropped there: a font the
   page never loads is a stack that lies about itself.
   ========================================================================== */

/* EVERY RATIO BELOW IS RECOMPUTED FROM THESE HEX DIGITS by clause 418 in
   tests/test-landing.js, against the ground each comment names (the paper,
   the recess or the plate). A token with no ratio is a token nobody
   measured, and the same clause refuses it. Measure first, write second:
   the numbers here were computed on 2026-09-06 (scratch pal-app.js) before
   the tokens were written — they are the landing's tokens and the landing's
   numbers, because the two surfaces are one stock. */
:root {
  --paper:   #F6F3EA;   /* THE GROUND — the landing's, verbatim: warm paper at the mark's own hue (45.0 against 46.6), chroma 0.047, luminance 0.8963. The one token that states no ratio: it is what the rest are measured against */
  --sign:    #F2C21B;   /* THE MARK — 1.51:1 on --paper, so it is never a ground for type; 10.65:1 under --ink. Spent by ONE selector in this sheet, the primary button (.btn); 418 A8 holds the list */
  --hi:      #FFD60A;   /* the mark's brightest — 13.13:1 on --plate; 1.27:1 on --paper and 1.19:1 on --sign, so it reads on the plate and nowhere else: the caution badge */
  --zbtn:    32px;      /* a zoom button's side; 44px under a coarse pointer (the thumb floor), and the open key's reservation is derived from it */
  --ink:     #1C1707;   /* type — 16.11:1 on --paper; 10.65:1 on --sign */
  --ink-2:   #42360F;   /* secondary type — 10.72:1 on --paper; 9.34:1 on --track-bg */
  --ink-3:   #655526;   /* micro labels, captions — 6.57:1 on --paper; 5.72:1 on --track-bg */
  --rule:    #91804E;   /* hairlines and keylines — 3.50:1 on --paper; 3.05:1 on --track-bg, over the 1.4.11 floor */
  --track-bg:#E8E4D5;   /* THE RECESS — 1.15:1 on --paper: the range slot, a lit row, the form's cells. The one tint the paper has, and it is the landing's own */

  --plate:   #171308;   /* the map pane, the chosen chip, the caution mark's ground — 16.70:1 on --paper */
  --p-ink:   #DDD8C9;   /* type on the plate — 13.01:1 on --plate */
  --p-ink-2: #B4AA8A;   /* secondary type on the plate — 7.99:1 on --plate */
  --p-ink-3: #90855C;   /* micro labels on the plate — 5.03:1 on --plate */
  --p-rule:  #6A6144;   /* keylines on the plate — 3.01:1 on --plate */

  --cast: color-mix(in srgb, var(--ink) 14%, transparent);   /* a shadow is the ink, thinned — 1.34:1 on --paper (#D7D4CA composited) */

  /* THERE IS NO --rust, AND THAT IS THE DECISION. A red was declared here for
     the shortfall line and never once consumed — app.js writes "is short
     <b>$1,240</b>/month" with the figure in a <b> and no class on it, and
     that is how it has always shipped. Colouring a shortfall red would make
     it a verdict; this product reports measured facts and lets the reader
     do the judging, which is the same reason there are no scores, no
     rankings and no survival numbers anywhere in it. The sentence says the
     door is short. It does not also shout. */

  /* ONE COLOUR LANGUAGE (owner 2026-08-19): the map's standardised
     red-to-green scale is the only data colouring in the product. The
     panel's answer lines are NEUTRAL — a second per-fact hue family was
     one language too many. Neither re-skin touched the ramp: the yellow is
     the chrome's mark, never a data colour. */

  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
  --sans:  "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono:  "Cascadia Mono", "Cascadia Code", ui-monospace, "SF Mono", "Segoe UI Mono", Menlo, Consolas, monospace;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 22px; --s6: 30px; --s7: 42px;

  /* THE TYPE ANSWERS THE READER, NOT THE DEVICE (a11y pass 2026-09-12).
   * Every size in this sheet was a device pixel: 74 of them, and not one rem
   * against the landing's 53 var() uses over five rem tokens. A reader who
   * had set a larger default type size in their browser — the setting most
   * people with low vision actually use, because it costs one click and needs
   * no extension — got the same 9px credit line and the same 10px legend as
   * everybody else. The sheet knew, and said so where .wordmark .brand is
   * sized ("in px here and in em on the landing, because this surface does
   * not rem-scale"). That sentence has been a defect with a comment on it.
   *
   * --tpx is one sixteenth of the reader's own type size: at the 16px default
   * it is exactly 1px, so EVERY number below and everywhere else in this file
   * renders at the value it always did and nothing moves for a reader who has
   * changed nothing. At a 24px setting each one is 1.5x. The numbers stay the
   * numbers a designer typed, which is why they are spelled `calc(12.5 *
   * var(--tpx))` rather than `0.78125rem` — the same figure, still legible.
   *
   * THE GEOMETRY IS STILL px, AND THAT IS THE TRADE: --rail-w, --bar-h, the
   * chrome bar's 44px rows, the s1..s7 spacing and every breakpoint below
   * hold their device size while the type grows inside them. Growing text in
   * a fixed box is how text overflows a box, so the enlarged setting is a
   * MEASURED state and not an assumed one — clause F8 in test-app-facts.js
   * holds the sheet to rem, and the phone walk at 16, 20, 24 and 32 is what
   * says whether the containers gave. Seventeen distinct sizes in one sheet
   * is a separate finding, left for the owner: collapsing 12.5 onto 12 and
   * 10.5 onto 10 is a design call, not an accessibility one.
   *
   * THE FLOOR MOVED 2026-09-16, AND THE COLLAPSE CAME WITH IT. The a11y pass
   * above put every size on the reader's unit and changed no size. This pass
   * changes the sizes. The reason is the one the landing carries: 9px and
   * 10px were too small to read and nobody had said so with a number.
   *
   * THE SAME READER READS BOTH SURFACES. The landing's floor went to 12 on
   * the same day, so 12 is where this one goes, and the ceiling — 22, 26 and
   * 52 — does not move, exactly as it did not there. The map, applied to all
   * 74 declarations in one pass:
   *
   *     9, 9.5, 10, 10.5 -> 12      11, 11.5 -> 13      12 -> 14
   *     12.5, 13 -> 15              13.5, 14 -> 16      15 -> 17
   *     16 -> 18                    17 -> 19            18 -> 20
   *
   * WHAT THE MAP PROTECTS. It is order-preserving everywhere a step carried a
   * meaning. .aline's answer line and the figure inside it keep their step
   * (16 and 17), .aline.dim2 keeps its step back (15), #tip's primary stays
   * over --t-small (14 over 13), #maphover's likewise (15 over 13), and
   * .z-trust's promotion of .fc-why stays a promotion (14 over 13). The one
   * place it does NOT preserve a distinction is the bottom cluster: 9, 9.5,
   * 10 and 10.5 all become 12. Fourteen small mono labels were wearing four
   * sizes, and four sizes inside three quarters of a pixel of each other is
   * accretion, not a hierarchy. So the owner's deferred design call above is
   * partly answered by this pass and should be read as answered: eighteen
   * distinct sizes are now twelve. The half-steps that remain (14 between
   * small and body, 16 and 20) are the ones a rule actually spends.
   *
   * WHAT IT DOES NOT ANSWER. --t-lede is still declared and still read by
   * nothing (0 uses at this commit), kept because F8 counts the tokens and a
   * token deleted is a rung nobody can reach for. And the geometry is still
   * px, so the trade named above is now a LARGER trade: the text inside the
   * fixed boxes is a rung bigger than the boxes were drawn for. That is a
   * render question and audit-type-boxes.js in this repo is what answers it;
   * it was run before and after this pass.
   *
   * AND EVERY px IN A COMMENT BELOW DATED BEFORE TODAY NAMES A PRE-RAISE
   * SIZE. This sheet argues in numbers, so it is full of sentences like
   * "type 12px against 11px" and "10.5px, up from 9.5" — each one a true
   * record of a decision on the day it was taken, and each one naming a size
   * that has since moved by this map. They are records, not specifications,
   * and are left standing: re-base a law, never delete it. The three that
   * were doing live work rather than keeping history have had the current
   * figure added beside them. Read a px in a dated comment as "the size on
   * that date", and read this block as where the sizes are. */
  --tpx:     calc(1rem / 16);

  --t-micro: calc(12 * var(--tpx));   /* was 10.5 */
  --t-small: calc(13 * var(--tpx));   /* was 11.5 */
  --t-body:  calc(15 * var(--tpx));   /* was 13   */
  --t-lede:  calc(17 * var(--tpx));   /* was 15, and still unused */
  --t-read:  calc(19 * var(--tpx));   /* was 17   */

  --r: 3px;
  --rail-w: 404px;
  /* RE-BASED 52 -> 60 (2026-09-16). The topbar's own re-base to the fleet
   * lockup set `min-height: 60px` on .topbar and stopped there; this token,
   * the one #app's grid row is cut from, kept the 52px the row measured back
   * when it took its height from the address input. So the bar measured 60
   * inside a 52px row, overflowed it by 8, and — carrying z-index 60 — sat
   * on the map. Measured across 1024, 1280, 1440, 1706 and 1990: .topbar
   * [0,0,W,60] with .main starting at y=52 at every one of them, so the top
   * 8px of the island was under the header on every screen this product has.
   * The phone block already answered this by making the row `auto` and says
   * so in its own note; the desktop never got the same fix. */
  --bar-h: 60px;
  --ease: cubic-bezier(.4, 0, .2, 1);
  /* THE MOTION BUDGET (concept pass 2026-09-09): one duration for every
   * unfold and lift, the camera's own 620ms fly excepted; the reduced-motion
   * block at the foot zeroes all of it */
  --motion: .26s;
  /* the field sheet's peek on a phone is a FLOOR, not the rest (2026-09-11):
   * the grip, the eyebrow and a two-line headline, which is all a stylesheet
   * can promise before anything has measured a pane. app.js raises it to
   * whatever the island's own fit does not want — see THE SHEET TAKES THE
   * SEA, NEVER THE ISLAND. This is also the resting transform's fallback
   * below, so it is what the very first frame shows. */
  --sheet-peek: 168px;
  /* lets a <details> animate to its own height (the unfold rule below) */
  interpolate-size: allow-keywords;
  color-scheme: only light;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font: 400 var(--t-body)/1.55 var(--sans);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
b { font-weight: 620; }
button { font: inherit; cursor: pointer; }
input, select { font: inherit; }
/* a focus ring says "you are here", not "danger": on paper it is the ink,
   the landing's own ring, and every control that resets :focus below spells
   the same ring again for :focus-visible (B58 derives the expected ring from
   this line, so changing it here changes it everywhere or goes red).
   16.11:1 on --paper, which is the ink's own ratio: the ring is the type. */
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: var(--r); }
/* AND ON THE MAP IT IS THE PLATE'S INK, WITH THE PLATE BEHIND IT (a11y pass
   2026-09-12). Five controls sit on the map rather than on the paper — the
   zoom pair, the legend's key button and the two licence links — and the ring
   above measures 1.04:1 there. It was not a dim ring on those five, it was no
   ring: a keyboard reader tabbing the map chrome had nothing to look at.
   --p-ink is 13.01:1 on --plate, the ratio the plate's own type already runs
   at. But the map pane is not ONE ground, it is the ramp, and a pale ring
   measures 1.11:1 on the lightest band (#C0EFC8) exactly as the ink measures
   1.04:1 on the plate — no single colour clears the floor on every pixel this
   canvas can paint. So the ring is TWO TONES: the plate's ink, with the plate
   itself filling the offset gap and standing 2px past the ring. The two are
   13.01:1 against each other, and whichever one the ground swallows, the
   other one stands — the halo is 14.49:1 on that same lightest band, and the
   worst ground either tone meets is the ramp's second step (#A05E00), where
   the ring holds 3.6:1 and the halo 3.61:1. 418 A9 recomputes every one of
   those against --plate, --paper and all five bands, and holds each ring in
   this sheet to 3:1 on the ground it names (WCAG 1.4.11, --rule's floor). */
.map-ui :focus-visible {
  outline-color: var(--p-ink);          /* 13.01:1 on --plate */
  box-shadow: 0 0 0 6px var(--plate);
}
::selection { background: var(--plate); color: var(--paper); }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
/* thinned ink, not a token: the thumb runs on paper in the rail and in the
 * drawer alike (2.19:1 composited), and a thumb is furniture — it needs finding, never
 * a keyline of its own (the ~800px stripe of 2026-08-31 was a keyline). */
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--ink) 35%, transparent); border-radius: 9px; }

.dim { color: var(--ink-3); }
/* a section's note about itself: how the list is ordered, how much of the
   island it covers. One grey line in the body face, sitting under the label
   it qualifies — the place the clauses that used to be crammed into a mono
   caps eyebrow now live (concept pass 2026-09-09). */
.seccap { margin: -4px 0 var(--s2); font-size: calc(13 * var(--tpx)); line-height: 1.45; }
.eyebrow {
  display: block; font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-3);
  margin: 0 0 var(--s2);
  /* balanced, like the headline under it (2026-09-08): the island's eyebrow
   * wrapped "RENT" alone onto its second line at 1440, and the list eyebrows
   * ("… ACHIEVED ROWS BEFORE ASKS") ran long-short; a two-line eyebrow now
   * breaks near its middle, no orphan. One-line eyebrows are untouched. */
  text-wrap: balance;
}
.pl { font-size: var(--t-body); line-height: 1.6; color: var(--ink-2); margin: 0 0 var(--s2); }

/* ==== the frame: topbar row, then map + rail in one grid ================ */
/* THE FRAME NAMED ITS ROWS AND NEVER ITS COLUMN. The column was therefore the
   implicit `auto` track, and an auto track sizes to the min-content of what it
   holds. On the island the header holds "Singapore" and the row fits; at the
   bottom of the ladder it holds "Singapore / Kallang / Lavender", and on a
   390px phone the row's min-content is 527: wordmark 102, crumbs 193, address
   box 164, three gaps 36, padding 32. Measured 2026-08-30 through the DevTools
   protocol with a real device-metrics override, not eyeballed — the plain
   --screenshot flag lays out at Chrome's ~500px minimum window and had been
   quietly reporting a phone that does not exist. The track grew to 527, .main
   and the rail took it, #app is overflow:hidden, and the receipt button of
   every fact row sat at x=511 on a screen 390 wide: the receipts, the thing a
   corner exists to open, unreachable on every phone in the field. Hiding
   either the crumbs or the address box alone brought the frame back to 390,
   which is how the header was identified; minmax(0, 1fr) on the RAIL changed
   nothing, because the rail was never the thing that was wide.
   A bare `1fr` here is minmax(auto, 1fr) and grows the same way — the
   landing's own track-floor law says exactly this about bare fr units.
   minmax(0, 1fr) is the spelling that clamps the column to the frame and lets
   the header's flex row shrink its items the way it was written to. At 1440
   the split stays 835 / 605 to the pixel. Held by the FRAME clauses in
   tests/test-app-boot.js, whose red arm is this declaration deleted. */
/* minmax(--bar-h, auto), not a fixed --bar-h: the token states the bar's
   FLOOR, and a floor that cannot give is how a 60px bar ends up inside a
   52px row. The bar grows if its own content ever needs more (enlarged type,
   a longer trail wrapping), and the map begins where the bar actually ends
   rather than where a constant said it would. */
#app {
  display: grid; grid-template-rows: minmax(var(--bar-h), auto) 1fr; grid-template-columns: minmax(0, 1fr);
  height: 100vh; height: 100dvh; overflow: hidden;
}
.main { display: grid; grid-template-columns: 1fr var(--rail-w); min-height: 0; }

/* ---- topbar ------------------------------------------------------------ */
/* THE HEADER IS THE SEARCH (concept pass 2026-09-09). The one box on this
   surface a reader arrives wanting — the address of the shop they are
   thinking about — was a 300px input floated to the right end of the bar,
   reading as the last item of a navigation row, with 850px of empty paper to
   its left. Three columns now: identity and the trail at the left, the box
   in the middle at the width its own job deserves, the charter door at the
   right. The outer columns are equal fractions, so the box is centred on the
   window rather than on whatever the trail happens to measure today. */
/* 60px is the fleet rail's floor (ID-T --nav-h), and it is the 38px badge plus
   11px of air top and bottom. The row used to take its height from whatever
   the address input measured, which was 52px — fine for a 20px tile and seven
   pixels short of enough for the mark this bar carries now. min-height, not
   height: the phone block below stacks this into two rows, and a stated height
   would cut the second one off. */
.topbar {
  display: grid; align-items: center; gap: var(--s3);
  /* 480, OR A THIRD OF THE WINDOW, WHICHEVER IS LESS (2026-09-16). The three
     columns above are right and the centring above is right — measured, the
     box's centre and the window's centre agree to the pixel at every width.
     What was wrong is that the middle column took its full 480 no matter how
     little was left: the outer tracks are minmax(0, 1fr) and an fr track
     yields to a fixed one, so at 1024 the left column came out 244px, the
     wordmark took 169 of it, and the trail was left 63px to say "Singapore /
     Bedok / Bedok North" in. It printed "dok North".
     The cap is not a fitted number: 480 IS one third of 1440, so this says
     the box keeps the share the 2026-09-09 ruling already gave it and stops
     claiming a share the window cannot pay. Above 1440 nothing moves. Below
     it the box gives width back to the trail, which folds whole rungs (app.js
     setCrumbs) rather than slicing a name — 348px and a full "… / Bedok
     North" at 1024, against 480 and a shaved word before.
     A SHARE, NOT A FLOOR, on purpose: a px floor on the outer tracks is the
     2026-08-30 outage waiting to happen. Two 316px floors plus the box's own
     240px minimum plus the gaps is 896 in the 869 a 901px window has, and the
     frame would grow and take the receipts off the screen again, which is the
     failure the #app note above exists to describe. A vw share cannot do that:
     it shrinks with the window instead of standing against it. */
  grid-template-columns: minmax(0, 1fr) minmax(240px, min(480px, 100vw / 3)) minmax(0, 1fr);
  min-height: 60px;
  padding: 0 var(--s4); border-bottom: 1px solid var(--rule);
  background: var(--paper); position: relative; z-index: 60;
}
/* the trail rides with the wordmark in the first column: one identity block,
   "where you are" reading straight off "what this is". The trail is the part
   that gives — it already hides its own overflow and cuts under a fade. */
.tlead { grid-column: 1; display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.topbar .addr.top { grid-column: 2; }
/* THE DOORS SHARE THE THIRD CELL. It held one control until 2026-09-16 and
   now holds two, so the cell is a flex row and the rule below places the row
   rather than the buttons. The .tbtn line is kept because dockChrome falls
   back to appending the charter door straight to the bar if a build ever
   ships without .tdoors, and in that world the button is a grid child again.
   :empty and not a media query on purpose: on a phone both doors are carried
   down to the sheet's grip, and an empty grid child still claims a track and
   a 12px gap out of a 390px bar that a measurement puts 44px of slack in. */
.topbar .tdoors { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.tdoors:empty { display: none; }
.topbar .tbtn { grid-column: 3; justify-self: end; }
.topbar .spacer { display: none; }
/* THE PRODUCT'S NAME WAS BROKEN IN THE PRODUCT'S OWN HEADER, by `gap: 7px`.
   The markup is <img class="brand"><b>Simpler</b>.Run, and ".Run" is an
   ANONYMOUS flex item. A gap falls between every pair of items, so it fell
   between "Simpler" and ".Run" too and the topbar read "Simpler .Run" — the
   one string on this page that is not allowed to be two words. The spacing
   belongs to the mark, which is the thing that needs it, so the mark carries
   it. landing.css got this right from the start; og-card.html shipped the
   same bug and was fixed the same way. Held now by the W clauses in
   tests/test-landing.js. The two weights are the landing's: the name in
   the ink, its suffix one step back. */
/* RE-BASED 2026-09-14 TO THE FLEET LOCKUP. The name was 14px beside a 20px
   tile, which ID-T's nav note calls a caption bolted to a logo; the fleet's
   figure is a 22px word to a 38px badge, and ID-T carries that SAME rail onto
   its own working surface (web/landing.tsx, M21) rather than shrinking it
   there. So this surface takes it too, in --tpx so the sheet still scales.
   Letter-spacing comes to -.012em with it: .01em was set for a 14px name and
   a 22px one at positive tracking reads loose. */
.wordmark {
  display: inline-flex; align-items: center;
  color: var(--ink-2); text-decoration: none; font-size: calc(22 * var(--tpx)); letter-spacing: -.012em;
  font-weight: 600;
}
.wordmark b { font-weight: 680; color: var(--ink); }
/* The brand mark, from the one file the tab already loads. RE-BASED
   2026-09-12: this said "sized in px here and in em on the landing, because
   this surface does not rem-scale and that one does". The reason went away
   when the type above it did — the sheet scales now — so the mark is sized in
   em like the landing's, 1.4286em of a 14px name, which is the same 20px it
   has always drawn and 30px for a reader at 24. A wordmark whose tile stayed
   put while its name grew would be the one string on this page that is not
   allowed to be two words, coming apart a second way. The tile is the mark
   yellow with its own black keyline, because #FFD60A on this paper is 1.27:1
   and without the edge the tile is not a tile — the same reason the ask badge
   below is a black plate.

   NO border-radius HERE, re-based 2026-09-14 the same afternoon it landed. The
   9px added with the 38px mark was ID-T's number, and ID-T's badge is drawn at
   rx 38 of 168 where this file is drawn at rx 24. Laying the larger corner over
   the smaller one cuts into the artwork and shaves the tile's own keyline thin
   at the corners, which on 1.27:1 yellow is the one edge that cannot be spent.
   The file rounds itself. See landing.css for the full argument and the two
   renders it rests on. */
.wordmark .brand {
  width: 1.7273em; height: 1.7273em; margin-right: 10px; display: block;
  flex: none;
}
/* THE TRAIL CUT A NAME IN HALF ON A DESKTOP, AND TOOK THE WAY UP WITH IT.
   app.js setCrumbs runs `cr.scrollLeft = cr.scrollWidth` so the rung you
   stand on is the one that survives, and toggles .cut when the trail truly
   overflows — both right. But the fade .cut names lived inside the phone
   block, and this rule said `overflow: hidden`. So from 1280 down the trail
   overflowed, scrolled to its end against a hard edge with no mask, and
   printed a mid-glyph slice: measured at 1024, a 63px box holding 232px of
   trail reading "dok North", mask-image none. That is the exact failure the
   setCrumbs note says was answered in 2026-08-31 ("ukit Timah"), answered
   only for the phone. Worse than the slice: `hidden` is not scrollable by a
   reader, so "Singapore" and "Bedok" — the only "up" this surface has — were
   not merely cut but unreachable, against a note two rules below that says
   the crumbs are NEVER hidden. overflow-x: auto at every width, the thumb
   hidden the way this file hides its other two scrollers, and the fade
   below moved out of the phone block to where the cut can happen. */
.crumbs {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
  /* the 4px the focus ring needs, on the axis this scroller clips — same
     reason as .metricbar above, and every rung here is a real link */
  padding: 4px 0;
}
.crumbs::-webkit-scrollbar { display: none; }
/* scrolled to its end on purpose (setCrumbs: the crumb naming where you
   stand is the last one), so it is the START of the trail that hides — the
   fade sits on the left edge and says "more this way", where the bare
   mid-glyph slice read as a typo. */
.crumbs.cut {
  mask-image: linear-gradient(90deg, transparent 0, #000 16px);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 16px);
}
/* a crumb is an anchor since 2026-09-03 (app.js setCrumbs): the rung you
   stand on is text, every rung above it is a real link with the rung's own
   hash.

   THE UNDERLINE IS BACK, 2026-09-16. It was dropped here on the reasoning
   that an anchor's default underline was noise in a bar of quiet controls,
   and the hover's lit tint would carry the affordance instead. A hover does
   not exist on a phone, and what was left at rest was a colour step: --ink-2
   on a rung you can press against --ink on the rung you stand on, which
   measures 1.50:1 — two near-blacks of the same hue, told apart by weight.
   So the trail read as a caption, and the owner reported this bar as having
   no way back in it at all while three of its words were links.
   WCAG 2.2 SC 1.4.1 is the standard: a distinction carried by colour alone
   is not a distinction. The underline is drawn in --rule rather than the ink
   so it stays quiet, and the rung you stand on drops it below, which is the
   difference doing the work rather than the hue. */
.crumb {
  background: none; border: 0; padding: 3px 6px; border-radius: var(--r);
  color: var(--ink-2); font-size: calc(15 * var(--tpx)); white-space: nowrap;
  text-decoration: underline; text-decoration-color: var(--rule);
  text-decoration-thickness: 1px; text-underline-offset: 3px;
  display: inline-block;
}
/* the fold's door (app.js setCrumbs paint): the rungs this bar could not fit
   handed their highest address to one control, so it is a real link and wears
   the trail's own face. Tabular width so the trail does not twitch as the
   fold takes one rung or two. */
.crumb.up { font-family: var(--mono); letter-spacing: .04em; color: var(--ink-3); }
.crumb.up:hover { color: var(--ink); }
.crumb:hover { color: var(--ink); background: var(--track-bg); }
/* the rung you STAND on is not a control, so it drops the underline the
   pressable rungs wear. This is the pair that carries the distinction now;
   the weight and the ink are what they always were, and they were never
   enough on their own. */
.crumb.on { color: var(--ink); font-weight: 600; cursor: default; text-decoration: none; }
.crumb.on:hover { background: none; }
.crumbs .sep { color: var(--ink-3); font-size: calc(13 * var(--tpx)); }
.spacer { flex: 1; }
.tbtn {
  font-size: calc(15 * var(--tpx)); color: var(--ink-2); padding: 6px 11px;
  background: none; border: 1px solid var(--rule); border-radius: var(--r);
}
.tbtn:hover { color: var(--p-ink); border-color: var(--plate); background: var(--plate); }
/* THE WAY OUT OF THE APP (2026-09-16). It borrows .tbtn rather than inventing
   a second affordance, because the bordered pill is the one shape in this bar
   a reader already reads as pressable — the charter door proved that, and the
   trail beside it proved the opposite.

   Three things an <a> needs that the <button> got for nothing. The UA
   underline comes off: inside a bordered pill an underline reads as two
   affordances stacked, and here the border IS the affordance, which is the
   reverse of the crumb rule above and for the same reason — each control
   carries exactly one. inline-flex with a centred cross axis, because an
   inline anchor's line box is the font's, not the button's, and the two would
   sit at different heights in the same row. And flex: none, so when .tlead
   runs out of room at 1280 and under it is the TRAIL that takes the squeeze
   and scrolls, never the exit: a way out that folds is the defect this
   control was added to answer. */
.thome {
  flex: none; display: inline-flex; align-items: center;
  text-decoration: none; white-space: nowrap;
}

/* ---- the address jump (typed, optional — the one box in the chrome) ---- */
/* A TEXT BOX IS PAPER WITH A KEYLINE. The page is paper now, so a box is
   told from the page by its rule (3.50:1) and by the ink it takes on focus,
   never by a second white — the landing's "no second paper" law (400 L11)
   is the app's too. */
.addr { position: relative; }
/* the box fills its column now (the grid caps it at 480 and floors it at
   240); the old min(300px, 26vw) was the width of a nav item, not of the
   product's one search */
.addr.top { width: 100%; }
.addr input {
  width: 100%; background: var(--paper); border: 1px solid var(--rule);
  border-radius: var(--r); color: var(--ink); font-size: calc(15 * var(--tpx)); padding: 6px 11px;
}
.addr input::placeholder { color: var(--ink-3); }
.addr input:focus { outline: none; border-color: var(--ink); background: var(--paper); }
/* THE ONE TEXT BOX WAS THE ONE CONTROL WITHOUT A KEYBOARD RING (e2e swarm
 * 2026-09-01, A15/A111): the :focus rule above kills the outline for mouse
 * users — fine — but it killed it for the Tab key too, and a keyboard
 * reader landing here had no idea where they were. Same ring as the global
 * rule at :focus-visible; equal specificity, so ORDER carries the law:
 * this rule must stay BELOW the :focus reset ("a landed rule is not an
 * applied rule", 2026-08-31). */
.addr input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.addr-drop {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 90;
  background: var(--paper); border: 1px solid var(--rule); border-radius: var(--r);
  overflow: hidden; box-shadow: 0 18px 40px color-mix(in srgb, var(--ink) 30%, transparent);
}
/* Two columns on one baseline: the address (which may wrap) and its postal
 * pinned right. As inline text the postal wrapped onto its own indented
 * second line on long rows — a lone six-digit number reading as a stray
 * fragment — and row heights went ragged (review 2026-08-31). */
.addr-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; padding: 9px 12px;
  background: none; border: 0; border-bottom: 1px solid var(--rule);
  color: var(--ink-2); font-size: calc(15 * var(--tpx)); line-height: 1.45;
}
.addr-row:last-child { border-bottom: 0; }
.addr-row:hover, .addr-row.sel { background: var(--track-bg); color: var(--ink); }
/* --ink-2, not --ink-3: on the lit (--track-bg) row the dimmer token is
 * 5.72:1 and the postal is the shortest string in the row (review
 * 2026-08-31, the same trap two palettes ago); the second ink holds 9.34:1
 * there. On a lit row it lifts with the row. */
.addr-row .pz { color: var(--ink-2); font-size: calc(13 * var(--tpx)); flex: 0 0 auto; font-family: var(--mono); }
.addr-row:hover .pz, .addr-row.sel .pz { color: var(--ink); }
/* the miss state's box must CLEAR the page heading it floats over (review
 * 2026-08-31, measured): the corner and address panels end at CSS y≈271/276,
 * well past the H1 line under the bar — but this one-sentence panel's bottom
 * border landed at y≈106.5, one row below the H1's x-height line (ascender
 * zone 100.5-105.5, descender 121.5), slicing the ascenders flush with ZERO
 * gap on a ground where a black cast cannot show. "Where is good for your
 * next shop?" read "vvnere is gooa ior y" — damaged type, not text behind a
 * panel. The bottom padding puts the border past the descender, so the H1's
 * first line hides whole: an overlay, never a cut. */
.addr-none { padding: 14px 12px 34px; font-size: calc(14 * var(--tpx)); color: var(--ink-2); line-height: 1.55; }

/* ==== the map column ==================================================== */
/* THE PANE IS THE PLATE (see the header): the one dark surface in the app,
   because a red-to-green data ramp reads against a quiet ground and not
   against the sign. Everything painted over it below is plate-on-plate. */
.mapstage { position: relative; min-width: 0; background: var(--plate); }
#mapcanvas { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }
#mapcanvas.drag { cursor: grabbing; }
/* a corner under the pointer lifts off the plate (app-map.js) and the
 * cursor says the same thing: this is a thing you can open */
#mapcanvas.hot { cursor: pointer; }
.mapbase { position: absolute; inset: 0; }
.mapbase canvas { display: block; }

.map-ui {
  position: absolute; inset: 0; pointer-events: none;
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 5px;
  padding: var(--s3);
  /* ABOVE THE DRAWING (2026-09-16). This rule carried no z-index, so it
   * stacked at auto while .drawnstage sits at 10 and fills opaquely. Clause
   * 427 below reads every `display: none` that lands on the key and had
   * nothing to say about this, because a thing can be taken away by being
   * COVERED as well as by being hidden, and the law was written about only
   * one of those. Measured with elementFromPoint at 1440 and at 390, with
   * the drawing on by default: the legend, its toggle, both zoom buttons and
   * the OpenStreetMap credit all returned the city canvas. The attribution
   * is a licence term, not a convenience.
   *
   * 15 cannot create a click-eating band — the 2026-08-19 outage class the
   * next comment is about — because this layer is pointer-events: none and
   * only .legend, .zoombox and the attribution link opt back in below. It
   * raises what is already painted; it catches nothing new. Clause 440 in
   * tests/test-landing.js holds this above .drawnstage by COMPUTED order. */
  z-index: 15;
  /* NO MAGIC FLOOR. This read `padding: var(--s3) var(--s3) 38px` — a
   * reservation sized for an attribution of "three lines" that is five on a
   * phone. The attribution is now the last child of this column, so the
   * legend is pushed up by whatever the line actually measures. A gap in a
   * flex column cannot be out-grown; a hardcoded floor always can. */
}
/* POINTER EVENTS GO TO THE VISIBLE CHROME ONLY — a transparent layout band
 * with pointer events is a click-eating strip (the 2026-08-19 outage class,
 * found twice). The chrome BAR is a real, visible surface, so it may catch
 * pointers honestly; inside the map body only the legend and zoom may. */
.map-ui > * { pointer-events: none; }
.legend, .zoombox { pointer-events: auto; }

/* THE CHROME BAR (the ui-v2 lesson): the pickers live in a glass strip the
 * map slides under — a surface, not a scatter of pills floating on the
 * island. Each row scrolls sideways under a fade mask instead of wrapping
 * over the map. The glass is PAPER: the bar is chrome, so it is the page's
 * stock like the rail and the topbar, and it thins toward the map so the
 * plate shows through its foot (the chips' ink-2 holds 8.75:1 on the 90%
 * band and 5.75:1 on the 72% foot, composited over the plate). */
/* ONE LINE (concept pass 2026-09-09). Two rows cost 87px of a 848px pane —
   a tenth of the map, permanently, to hold controls. One row is 44, and the
   43px it hands back go straight into the island's own fit. The row wraps
   below its measured floor rather than being cut; the phone block keeps the
   stacking it already had. */
.mapchrome {
  position: absolute; top: 0; left: 0; right: 0; z-index: 20;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  /* 4/4 HERE AND 4/4 INSIDE .metricbar, WHERE IT WAS 7/8 HERE AND 0 THERE.
   * The global focus ring is 2px at 2px offset, so a control needs 4px of
   * clearance inside whatever clips it. .metricbar declares overflow-x: auto
   * and CSS computes the other axis to auto with it — the lesson this sheet
   * already wrote down for hit boxes at the .barmore note and gate 433 T6,
   * never carried across to outlines, which are worse off because an outline
   * is not part of a scroll container's scrollable overflow and so cannot
   * even be scrolled back into view. The ring on all six shade and show
   * chips was cut top and bottom; on the chosen chip what survived was two
   * 2px ticks of ink at 1.04:1 against the plate they were marking, which is
   * a keyboard reader losing their place. The air moves from the bar into
   * the scrollport so the ring has room where the clip actually happens, and
   * 4 + 38 + 4 + 1 keeps the band at the 47px it measured before. */
  padding: 4px var(--s3);
  /* THE SEAM (concept pass 2026-09-09). The bar used to fade from 96% paper
     to 72% over its own height with a 14px blur behind it, so its foot was a
     three-quarter wash of paper over the plate and the map's top edge was
     not an edge at all — a grey band of neither material, right where the
     island's north coast runs. A control surface has a real edge or it is a
     smear. Solid paper, one rule at the bottom, and the header and the bar
     read as one block of stock with the map beginning cleanly under it. */
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  pointer-events: auto;
}
/* ---- the trade menu ----------------------------------------------------
   The button IS the state: the question while nothing is chosen, the choice
   once one is, and the plate fill of a chosen chip so the two read as one
   control language. The panel hangs off the bar's own bottom rule. */
.catmenu { position: relative; flex: 0 0 auto; display: flex; align-items: center; }
.catbtn {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-size: calc(14 * var(--tpx)); color: var(--ink-2); padding: 5px 10px 5px 12px;
  background: transparent; border: 1px solid var(--rule); border-radius: 999px;
  cursor: pointer;
  transition: background var(--motion) var(--ease), color var(--motion) var(--ease), border-color var(--motion) var(--ease);
}
.catbtn:hover { color: var(--ink); border-color: var(--ink); }
.catbtn .cv { font-size: calc(12 * var(--tpx)); opacity: .7; }
.catbtn.on { color: var(--p-ink); background: var(--plate); border-color: var(--plate); font-weight: 620; }
.catbtn[aria-expanded="true"] { border-color: var(--ink); }
.catpanel {
  position: absolute; top: calc(100% + 9px); left: 0; z-index: 30;
  width: max-content; max-width: min(560px, 78vw);
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  padding: 12px; border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--paper);
  /* the panel is over the plate, so it needs its own weight to sit on it —
     the only place on this bar that carries a shadow, because it is the
     only thing that leaves the bar's plane */
  box-shadow: 0 10px 28px rgba(12, 10, 4, .34);
}
.catpanel[hidden] { display: none; }
.catpanel .mlabel { flex: 1 0 100%; min-width: 0; margin-bottom: 1px; }
.metricbar {
  display: flex; flex-wrap: nowrap; align-items: center; gap: 6px;
  overflow-x: auto; scrollbar-width: none;
  flex: 0 1 auto; min-width: 0;
  /* room for the last chip to clear the .barmore control on the way to max
   * scroll — the control hides once the row's end is reached, so nothing is
   * ever erased at rest. (This clearance predates the control: it lived only
   * in the phone block while only the phone overflowed; display:contents
   * below made the trade row a desktop scroller too, review 2026-08-31.) */
  padding: 4px 28px 4px 0;
}
.metricbar::-webkit-scrollbar { display: none; }
/* THE ROW SAYS IT SCROLLS (e2e swarm 2026-09-01). The fade mask that stood
 * here was the only cue that a chip row continues, and it ERASED the cut
 * chip ("Money" read "Mone", then nothing) instead of presenting it — a cue
 * that destroys its own evidence. The cue is now a control: a chevron stuck
 * to the row's right edge, carrying the gradient the mask used to paint,
 * shown by app.js only while there is genuinely more row to the right
 * (scroll, resize and every level change re-ask). Tapping it advances the
 * row. It stretches to the row's own height so a thumb can hit it. */
.barmore {
  /* right:-28px, not 0: sticky offsets pin to the scrollport's PADDING edge,
   * and the bar keeps 28px of end clearance — at 0 the chevron floated 28px
   * in from the visual edge with unfaded chips showing past it (measured
   * 2026-09-01 at 390: rect right 350 in a 378 bar) */
  position: sticky; right: -28px; z-index: 2; flex: 0 0 auto; align-self: stretch;
  width: 32px; border: 0; padding: 0 4px 0 0;
  display: flex; align-items: center; justify-content: flex-end;
  color: var(--ink); font-size: calc(19 * var(--tpx)); line-height: 1; cursor: pointer;
  background: var(--paper);
}
/* THE FADE IS NOT A BUTTON (Track A, [64]): the gradient used to be the
 * chevron's own padding, so the chip sliding out from under it was visible
 * for 64px and tappable for 9 — the fade ate the taps. The pseudo passes the
 * pointer through; only the solid 32px glyph plate takes the tap, and a
 * coarse pointer widens that plate below.
 *
 * RE-BASED 2026-09-16: THE ERASING CUE WAS STILL HERE, WEARING A DIFFERENT
 * SPELLING. B55 bans a mask over anything that holds a chip, in four
 * spellings, because a cue that destroys its own evidence is not a cue —
 * "Money" read "Mone", then nothing. The same erasure came back as a
 * background gradient on a pseudo, which B55 cannot see: it bans mask-image,
 * not linear-gradient, and it goes green. Photographed at 1024 on 2026-09-16,
 * where the strip scrolls: the Drawn chip dissolves under a 16px wash of
 * paper laid over a black pill and reads as a smear rather than a chip that
 * continues. The chevron sits on its own solid paper plate and is the cue;
 * the plate's left edge is where the row cuts, so that edge becomes a
 * hairline — the same rule the .tgrp fence uses to make a boundary visible,
 * and one that can never wash out the thing behind it. Nothing is removed
 * from the bar: the chip is cut cleanly instead of being dissolved. */
.barmore::before {
  content: ''; position: absolute; right: 100%; top: 6px; bottom: 6px; width: 1px;
  background: var(--rule); pointer-events: none;
}
.barmore:hover { color: var(--ink); }
.barmore[hidden] { display: none; }
.legend[hidden] { display: none; }
/* THE CHIPS ARE FLEX ITEMS OF THE BAR, NOT OF A SHRINKING SPAN (2026-08-31).
 * #cat-chips is the plain span the renderer writes chips into, and with no
 * rule anywhere it was itself the flex item: min-width:auto let it shrink to
 * the leftover width and its buttons WRAPPED inside it — four rows of chips
 * over the island at 390 (measured 2026-08-30, render), while both comments
 * on this block promise a row that scrolls. display:contents lifts the
 * buttons into the bar's own flex row, where flex:0 0 auto, overflow-x and
 * the fade mask already do what the comments say. The span keeps its
 * delegated click listener — contents removes the box, not the element. */
#cat-chips { display: contents; }
/* the chrome bar's chip GROUPS (review 2026-08-31): chips that share a
 * meaning share an element, and the element wears the group's name
 * (app.html). .tgrp replaces the old .msep spacer — an unlabelled 10px of
 * glass was the only thing ending "SHADE BY"'s scope, 3.2x LESS space than
 * the 70px that JOINS that label to its own chips, so the row read as one
 * five-chip radio set. A rule is scope made visible, and it survives the
 * phone, where the label words fold. */
.mgrp { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
/* THE STRIP HAD ONE FENCE AND THREE SCOPES (measured 2026-09-16). .tgrp's
 * note below argues that a rule is scope made visible, and it is, but it was
 * spent on only one of the two boundaries in this bar. The trade menu — a
 * different question from a different taxonomy — was separated from SHADE BY
 * by 14px of air against the 6px that joins a label to its own chips: a 2.3x
 * step where the fenced boundary gets a hairline. Two boundaries, one
 * treatment. The fence rides .catmenu's right rather than the metricbar's
 * first group because the metricbar is the thing that scrolls, and a fence
 * that scrolls away is not a fence. */
.catmenu { border-right: 1px solid var(--rule); padding-right: 14px; margin-right: 2px; }
.tgrp { border-left: 1px solid var(--rule); padding-left: 12px; margin-left: 4px; }
/* the 119px floor below aligns the two ROWS' leading labels down the bar's
 * left edge; an in-row label must not push its own fence 119px out */
.mlabel.tlab { min-width: 0; }
/* MONO MEANT A FIGURE EVERYWHERE ELSE (concept pass 2026-09-09). These are
   the only mono words on the surface that are not a measurement, and there
   were three of them across the top of the map in caps at .11em. The face is
   the sans now; caps and the tracking stay, because a label that names a
   group is still not a sentence. */
.mlabel {
  font-family: var(--sans, inherit); font-size: calc(12 * var(--tpx)); letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3); flex: 0 0 auto;
  /* THE 119px FLOOR IS RETIRED WITH THE SECOND ROW. It existed to align two
   * rows' chips down one left edge — with each label at its own width the
   * first trade chip began at x=136 against the first shade chip at x=74, a
   * 62px stagger reading as a ragged control edge (review 2026-08-31,
   * measured). There is one row now, so there is no second edge to align to,
   * and 119px of reserved glass per label was 238px the bar could not spend
   * on the controls it had to fit. */
  min-width: 0;
}
/* A CHIP IS A KEYLINE ON PAPER; THE CHOSEN CHIP IS A PLATE. The idle state
   is an outline in the rule, the pressed state is black with the plate's
   own ink — the landing's door-and-cordon pair. The yellow is not a state
   colour here: it is spent on the one primary button and the caution mark
   (418 A8), so a chosen chip can never be mistaken for either. */
.mchip {
  flex: 0 0 auto; white-space: nowrap;
  /* inline-flex and a stated floor so every control on this bar shares one
     baseline. Measured 2026-09-16 at 1706: the shade chips were 30px tall at
     y=59 and the show chips 28px at y=60, a 1px ragged edge running the width
     of the strip, because the toggles' smaller type shrank their boxes. */
  display: inline-flex; align-items: center; min-height: 30px;
  font-size: calc(14 * var(--tpx)); color: var(--ink-2); padding: 5px 11px;
  background: transparent; border: 1px solid var(--rule);
  border-radius: 999px;
  transition: background var(--motion) var(--ease), color var(--motion) var(--ease), border-color var(--motion) var(--ease);
}
.mchip:hover { color: var(--ink); border-color: var(--ink); }
.mchip.on { color: var(--p-ink); background: var(--plate); border-color: var(--plate); font-weight: 620; }
/* ==== A PILL IS A CHOICE, A SQUARE IS A SWITCH =========================
 * THE BAR DREW TWO LOGICS WITH ONE PAINT. "Rent" is one of three — picking
 * Footfall puts Rent out — while Hubs, Basemap and Drawn are three separate
 * switches that do not touch each other. Both wore var(--plate), byte for
 * byte, in the same 999px pill, on the same line: measured at 1706, four
 * solid black lozenges reading as a four-way choice with four things chosen.
 * The four ways they did differ were all differences of DEGREE — ink 13.21:1
 * against 8.12:1, weight 620 against 500, type 12px against 11px, and a
 * keyline at 3.01:1 drawn on its own fill. None of those is visible on a
 * single 30px chip, and none of them says "this one excludes the others".
 * Worse in the other direction: an OFF toggle fell through to the base rule,
 * so it was byte-identical to an unpicked radio chip.
 *
 * The distinction moves off degree and onto KIND, so that it can be read
 * from one chip alone with no neighbour to compare against:
 *   shape     the switch squares its corners against the choice's pill
 *   material  the switch sits on the sheet's own lit row, not on the plate
 *   mark      the switch carries a box that fills when it is on
 * After this the strip holds exactly ONE solid black lozenge and it means
 * the one thing the strip has to say: which measure the map is shaded by.
 * That also answers the 390px fold, where the group labels hide and the
 * .tgrp fence scrolls out of the port — the old distinction was carried
 * entirely by that label and that fence, so on a phone it was carried by
 * nothing. A shape and a mark survive a scroller.
 *
 * The 11px type goes with it. It was the sixth size in this 98px band and
 * the smallest text on the surface, below the sheet's own --t-small, and
 * with shape and a mark doing the work it was distinguishing nothing. (The
 * two figures in the DEGREE list above, 12px against 11px, are the sizes
 * this paragraph was written at. The floor pass of 2026-09-16 makes them 14
 * against 13; the ratio, and the argument, are unchanged.) The
 * trade is width: the mark and the type cost about 18px a chip, roughly 54px
 * across the three, which the strip has at every width it does not already
 * scroll at and does not have at 1024, where the chevron already stood.
 *
 * NOT YELLOW, by the 2026-09-10 ruling: --sign and --hi are rationed to the
 * one primary button and the caution mark (418 A8), so a switch cannot
 * borrow either. --track-bg is the sheet's one tint, already the language of
 * a lit row, and --ink on it MEASURES 14.03:1 — read off the live chip with
 * getComputedStyle on 2026-09-16, by a walk that reproduces this sheet's own
 * printed 16.11, 16.70 and 6.57 to the hundredth before it is believed. */
.mchip.tog { gap: 7px; border-radius: var(--r); }
.mchip.tog::before {
  content: ''; flex: none; width: 8px; height: 8px;
  border: 1px solid var(--rule); border-radius: 1px; background: transparent;
  transition: background var(--motion) var(--ease), border-color var(--motion) var(--ease);
}
.mchip.tog.on {
  color: var(--ink); background: var(--track-bg); border-color: var(--ink); font-weight: 600;
}
.mchip.tog.on::before { background: var(--ink); border-color: var(--ink); }
/* aria-disabled, not disabled: a disabled button suppresses the very
 * events that would show its data-note explaining WHY it is off.
 * .75 on --ink-3, not .55: composited over the paper that is 3.69:1
 * (#897D57, sRGB channels, 2026-09-06) — plainly dimmer than the live
 * chips, still a word the state can be read from. */
.mchip[aria-disabled="true"] { opacity: .75; cursor: not-allowed; color: var(--ink-3); }
.mchip.cat.on { background: var(--plate); color: var(--p-ink); }
.mchip.cat.back { border-style: dashed; border-color: var(--ink); color: var(--ink); }

/* ==== FLAGGED, NOT SHIPPED: SPEND THE YELLOW ONCE ========================
   An owner ruling, built so it can be seen rather than argued.

   The finding: on the app at rest there is no yellow anywhere but the 20px
   favicon tile in the wordmark. The identity this product was given is that
   black and yellow together mean caution, and the app spends the black
   everywhere — the plate, the chosen chip, the map itself — and the yellow
   nowhere. The landing keeps its half of the pair (the button, the caution
   block, the hatch, the tape); the app dropped it.

   The proposal: one chip. The chosen SHADE-BY chip is the single mark on
   screen that says what the whole map is about, and it is the only place a
   reader's eye needs pulling. Not the trade menu (a filter), not the
   toggles (overlays), not a hover, not a border.

   Why it was not on: it breaks the 418 A8 ration, which spends --sign on
   .btn and nothing else. That ration is a law with a red arm, and a design
   preference is not a reason to walk past one — the owner re-bases a law or
   it stands. #F2C21B on --ink is 8.06:1, so the chip was legible either way
   and the question was identity, not contrast.

   RULED 2026-09-10: NO, and the rule is out of this sheet. Both renders stay
   in research/concept-ten-moves-2026-09-09.md if it is ever wanted back, so
   nothing is lost but the selector. Three reasons, in the order they weigh.
   The mark's job on this product is CAUTION, and a chip that says "this
   control is on" spends a hazard mark on a UI state. It arrived as an
   ADDITION to a rationed token, which is how dilution actually happens here:
   across five independently written design passes the app gained at least
   eight new spenders of --sign, and every one of them was written up as
   "still rationed", because each pass counted only its own screen. And it
   sat here switched off with no grant, which is the worst of the three — 418
   A8 stood red for a week while no visitor ever saw a yellow chip, dead CSS
   wearing the uniform of a live rule and one hand-typed body class from
   being real. Removing it changes no screen: `body.sign-chip` is set by no
   markup and no script, and never was. */

/* ---- bottom band: legend left, zoom right, attribution under -----------
   The legend is a CAPTION, not a control panel: one title line, one swatch
   row, one small count — it stopped squatting over Sentosa. It sits ON the
   map, so it is a plate with the plate's inks. */
.map-bottom { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s3); }
.legend {
  background: color-mix(in srgb, var(--plate) 86%, transparent); backdrop-filter: blur(10px);
  border: 1px solid var(--p-rule); border-radius: var(--r);
  padding: 7px 10px; max-width: 400px;
}
.legend .lh { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.legend .lt { font-size: calc(13 * var(--tpx)); font-weight: 620; color: var(--p-ink); }
/* THE KEY FOLDS ON EVERY SIZE NOW (concept pass 2026-09-09). It used to fold
   only in the phone block, so a desktop reader got nine rows of the smallest
   type on the surface — five band values, an empty-band note, three no-colour
   sentences — parked in the water south of the island. The face keeps the
   scale and the three no-colour keys; the cuts and the paragraph are one
   button away. So the button is no longer display:none by default. */
.lk {
  flex: none;
  font: 600 calc(12 * var(--tpx)) var(--sans, inherit); letter-spacing: .04em; text-transform: uppercase;
  color: var(--p-ink); background: transparent;
  border: 1px solid var(--p-rule); border-radius: var(--r); padding: 1px 6px;
  cursor: pointer;
}
.lk:hover { border-color: var(--p-ink-2); }

/* ---- the scale ----------------------------------------------------------
   The bar carries the ramp in five hard stripes (app.js writes the stops)
   and the two ends carry the extreme values the map paints. Height 7px: at
   5 it reads as a rule, at 10 it reads as a chart. The inset hairline is the
   plate's own rule, so the pale end has an edge and does not bleed into the
   blur behind it. */
.lscale { margin-top: 6px; }
.lgrad {
  height: 7px; border-radius: 2px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--p-rule) 70%, transparent);
}
.lends {
  display: flex; justify-content: space-between; gap: 8px; margin-top: 3px;
  font-family: var(--mono); font-size: calc(12 * var(--tpx)); color: var(--p-ink);
  font-variant-numeric: tabular-nums;
}
/* the no-colour keys: on the face, in a row, wrapping only when they must */
.lkeys { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 6px; }
.lkeys:empty { display: none; }
/* the aside swatch is the grey it edges plus the edge itself — the same
   dashed keyline app-map.js draws, at the size a 12x9 chip can hold it */
.lband i.dash { border: 1px dashed currentColor; }
.legend .ramp { display: flex; flex-wrap: wrap; gap: 3px 9px; }
/* the fold: the band cuts and the paragraph. Closed by default on every
   size; app.js's toggle flips aria-expanded on .lk and .open on .legend. */
.lmore { display: none; }
.legend.open .lmore { display: block; margin-top: 7px; padding-top: 6px; border-top: 1px solid var(--p-rule); }
.lband.lnote { color: var(--p-ink-3); flex: 1 0 100%; }
.lband {
  display: inline-flex; align-items: center; gap: 4px; font-family: var(--mono);
  /* 10.5px, up from 9.5: the ramp's only decoder was the smallest type on
   * the whole surface, at 1280 and on the phone alike (Track A, a walk over
   * every text node) — the bands still sit two to a row at the 400px cap.
   * 12px as of the floor pass 2026-09-16, which took 9, 9.5, 10 and 10.5 to
   * one rung. The argument above is why this rule was singled out then; it is
   * no longer singular, because the whole bottom of the sheet came with it. */
  font-size: calc(12 * var(--tpx)); color: var(--p-ink); font-variant-numeric: tabular-nums;
}
.lband i { width: 12px; height: 9px; border-radius: 2px; display: inline-block; }
/* .lbreak is gone with the row it broke (concept pass 2026-09-09). It was a
 * zero-height full-width flex item forcing a hard return, because the grey
 * "none published" swatch sat inline at the end of the cheapest band's row —
 * the one swatch out of the column, reading as an annotation on that band
 * (review 2026-08-31). The no-colour keys are now their own row on the face
 * and the band values are behind the button, so the two are never in one
 * flex line for a spacer to separate. */
.legend .lb { font-size: calc(12 * var(--tpx)); color: var(--p-ink-2); line-height: 1.45; }
.zoombox { display: flex; flex-direction: column; gap: 6px; }
.zbtn {
  width: var(--zbtn); height: var(--zbtn); border: 1px solid var(--p-rule); border-radius: var(--r);
  /* solid, not 90%: a name under a translucent button was neither readable
   * nor cleanly covered — half a glyph bleeding through a control (Track C) */
  background: var(--plate); color: var(--p-ink); font-size: calc(17 * var(--tpx));
}
.zbtn:hover { border-color: var(--p-ink-2); }
.attrib {
  /* IN FLOW, last in .map-ui's column — see the note in app.html. It is no
   * longer dodging the zoom stack (that sits in the .map-bottom row above),
   * so it gets the full width and wraps to fewer lines than the old 58%. */
  align-self: flex-start; max-width: 100%;
  /* AND A MEASURE ON TOP OF THE 100% (2026-09-16), on its own line because
     the line above it is quoted verbatim by clause 421's B2 arm and must stay
     the bytes it is. max-width: 100% says "do not exceed the column"; it does
     not say "do not run to 195 characters", which is what this credit did at
     2560, and 100.5 at 900, measured. 60ch is roughly 73 characters at this
     stack, the same cap and the same arithmetic as the reading column above.

     min(100%, 60ch) AND NOT A BARE 60ch, because two max-widths in one rule
     is not "the narrower wins" — the second simply replaces the first, and a
     bare 60ch is 388px at this size, which is wider than the whole column on
     a 390px phone. The line above would have looked like it was still doing
     its job and would have been doing nothing. @media print takes the cap
     back off: paper has its own column. */
  max-width: min(100%, 60ch);
  font-size: calc(12 * var(--tpx)); color: var(--p-ink-3); line-height: 1.4; pointer-events: none;
  /* its own ink is kept, but as legibility over the MAP now, not as a screen
   * between two pieces of chrome — that overlap is structurally gone. */
  background: color-mix(in srgb, var(--plate) 88%, transparent); padding: 2px 6px; border-radius: var(--r);
}
/* the licence links must be REACHABLE: .attrib keeps pointer-events:none so
 * the line never eats a map drag, and its anchors alone opt back in (deploy
 * audit 2026-08-31 — a credit whose link can never be clicked is no route
 * to the licence it names) */
.attrib a { pointer-events: auto; color: inherit; }

/* ---- hover readout ----------------------------------------------------- */
#maphover {
  position: absolute; display: none; pointer-events: none; z-index: 40;
  background: color-mix(in srgb, var(--plate) 92%, transparent); border: 1px solid var(--p-rule);
  border-radius: var(--r); padding: 7px 10px; max-width: 280px;
}
#maphover.on { display: block; }
#maphover .h { font-size: calc(15 * var(--tpx)); font-weight: 620; color: var(--p-ink); }
#maphover .v { font-size: var(--t-small); color: var(--p-ink-2); margin-top: 2px; line-height: 1.45; }

/* ==== the rail: one panel whose contents ARE the current level ========== */
.rail {
  border-left: 1px solid var(--rule); background: var(--paper);
  min-height: 0; display: flex; flex-direction: column;
}
.rail-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; }
/* the sheet's grip is a phone thing (the phone block); the header keeps the
 * trail and the charter door on a desktop */
.grip { display: none; }

/* ---- THE SHEET ARRIVES, THE FIGURES COUNT IN (concept pass 2026-09-09) ----
   Every rung's sections rise a step onto the page in order, the ranked rows
   a beat behind their eyebrow; a hero figure counts in over the same half
   second (app.js animateFigures). One keyframe, one easing, one budget. */
@keyframes sheet-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.rail-body > .zone > .zsec { animation: sheet-in .36s var(--ease) both; animation-delay: calc(var(--i, 0) * 45ms); }
.rail-body > .zone > .zsec:nth-child(2) { --i: 1; }
.rail-body > .zone > .zsec:nth-child(3) { --i: 2; }
.rail-body > .zone > .zsec:nth-child(4) { --i: 3; }
.rail-body > .zone > .zsec:nth-child(5) { --i: 4; }
.rail-body > .zone > .zsec:nth-child(6) { --i: 5; }
.rail-body > .zone > .zsec:nth-child(n + 7) { --i: 6; }
.rrow-w { animation: sheet-in .3s var(--ease) both; animation-delay: calc(var(--i, 0) * 28ms + 60ms); }
.rrow-w:nth-child(2) { --i: 1; }  .rrow-w:nth-child(3) { --i: 2; }  .rrow-w:nth-child(4) { --i: 3; }
.rrow-w:nth-child(5) { --i: 4; }  .rrow-w:nth-child(6) { --i: 5; }  .rrow-w:nth-child(7) { --i: 6; }
.rrow-w:nth-child(8) { --i: 7; }  .rrow-w:nth-child(9) { --i: 8; }  .rrow-w:nth-child(10) { --i: 9; }
.rrow-w:nth-child(11) { --i: 10; } .rrow-w:nth-child(12) { --i: 11; } .rrow-w:nth-child(n + 13) { --i: 12; }
/* a receipt unfolds instead of appearing: every <details> in the rail — the
 * how-read ledger, the founder fold, the lede's "more", the demoted-rent
 * fold — grows to its own height. Older engines ignore the pseudo and open
 * as before; nothing is lost. */
.rail details::details-content {
  block-size: 0; overflow: clip;
  transition: block-size var(--motion) var(--ease), content-visibility var(--motion) allow-discrete;
}
.rail details[open]::details-content { block-size: auto; }

.zone { padding-bottom: var(--s7); }
.zsec { padding: var(--s4) var(--s5); border-bottom: 1px solid var(--rule); }
.zsec:last-child { border-bottom: 0; }

/* THE READING COLUMN (2026-09-16). The rail is 404px on a desktop and the
   prose inside it was never capped, because at 404px nothing needed a cap.
   Two states break that. Under 900 the rail goes full-bleed, and above 1440
   it grows with the window. Rendered and measured by audit-type-boxes.js on
   2026-09-16, BEFORE this rule: the answer line ran 107 characters a line at
   900, the city note 139.5 at 2560, the trust note 104, the source line 94.5.
   The landing holds its own prose to 45..75 characters and this is the same
   reader half a click away.

   60ch AND NOT 75. A ch is the width of a zero, not of a character: at this
   sheet's stack a cap in ch buys about 1.21 characters, so 60ch is roughly 73
   characters and 62ch would land on 75 exactly — the boundary of the band,
   which is no place for a measure to sit. It is the same number the landing's
   colophon took on the same day, for the same arithmetic.

   :where() AND NOT A PLAIN SELECTOR, so this carries ZERO specificity. It is
   a floor that any rule already in the sheet overrides without a fight, and
   it cannot win an argument it was not meant to be in. .aline .at is listed
   because the answer line is the longest prose on the card and it is a span,
   not a p; .attrib is NOT in here because it sits over the map rather than in
   a section, and carries its own cap where it is declared. */
:where(.zsec) :where(p, dd, .fc-why, .fc-src, .aline .at) { max-width: 60ch; }

/* AT THE BOTTOM OF THE LADDER THE PANEL IS THE SUBJECT: on a door the rail
 * widens and the map becomes context, because the reader came down here to
 * READ, not to look at dark basemap. */
#app.zone-open .main { grid-template-columns: 1fr minmax(460px, 42%); }

/* ---- THE CORNER, DRAWN (2026-09-13) ------------------------------------
   The buildings of this corner at their published heights, axonometric, on
   the paper. It sits UNDER the rent hero: the rank ruling puts a measured
   number on the first screen and this is the place, not the record.

   The stage is paper, not plate. Every other canvas in this product is the
   dark map pane, and making this one dark too would have read as a second
   map and invited the reader to look for the choropleth on it. It is a
   drawing in a book, so it stands on the page's own ground inside a keyline.

   The height is an aspect-ratio, not a pixel count, so the drawing keeps its
   proportion from a 320px phone to a 42% desktop rail without a breakpoint
   per width. 16/11 is wide enough to hold a long corner turned side-on and
   short enough that the ledger under it stays in view on a phone. */
.z-city { padding-top: var(--s4); }
.z-city .city-stage {
  margin: var(--s2) 0 var(--s3);
  border: 1px solid var(--rule);
  background: var(--paper);
  aspect-ratio: 16 / 11;
  max-height: 60vh;
  overflow: hidden;
}
.z-city .city-cv { display: block; width: 100%; height: 100%; cursor: grab; }
.z-city .city-cv.is-drag { cursor: grabbing; }
/* IT IS OPERABLE BY KEYBOARD, so it must SHOW that it has focus: the canvas
   is the control, and the a11y pass ruled a ring nobody can see is no ring. */
.z-city .city-cv:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* the key IS the argument, so it is type on the page, not a legend floating
   over the drawing where a long corner would collide with it */
.z-city .city-key { margin: 0 0 var(--s2); padding: 0; list-style: none; font-size: calc(13 * var(--tpx)); line-height: 1.5; color: var(--ink-2); }
.z-city .ck-row { display: flex; align-items: flex-start; gap: var(--s2); margin-bottom: var(--s1); }
.z-city .ck-row b { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.z-city .ck-sw {
  width: 22px; height: 12px; flex: 0 0 auto; margin-top: 3px;
  border: 1px solid var(--ink); box-sizing: border-box;
}
/* THE THREE SWATCHES ARE THE THREE STATES, and they are told apart by SHAPE
   (solid, dashed, dotted) before colour, because the whole claim is that a
   guess must never pass for a measurement — and it must not pass for one
   under a colourblind eye, a monochrome print, or a forced-colours mode. */
.z-city .ck-m { background: #E6E1D2; }
.z-city .ck-e { background: transparent; border-style: dashed; border-color: var(--rule); }
.z-city .ck-f { background: var(--track-bg); border-style: dotted; border-color: var(--ink-3); }

@media (max-width: 760px) {
  .z-city .city-stage { aspect-ratio: 4 / 3; max-height: 48vh; }
}
@media (prefers-reduced-motion: reduce) {
  .z-city .city-cv { transition: none; }
}

/* ---- THE CORNER DRAWN OVER THE MAP -------------------------------------
   Owner ruling 2026-09-14: "it's a waste adding into the side, we should
   cover the actual map of it, not as a small report on the side." In the rail
   the drawing got 276 by 207 points on a phone and about 490 by 430 on a
   desk, while the reader's eye was on the map beside it. It now takes the
   glass, and the figure in the rail keeps the key and the why as the caption
   for it.

   Two insets, because the pane is not the stage. --drawn-cover is the sheet's
   cover at its REST: --map-cover itself moves every frame of a drag, and this
   box is the box the drawing is fitted to, so following the drag would squash
   the drawing on every frame and redraw 4,240 buildings to do it. The sheet
   slides over the drawing exactly as it slides over the map, and the drawing
   re-centres once, when the sheet lands. --chrome-h is measured by app.js:
   the bar is SOLID paper over the top of the stage (see .mapchrome, "solid
   paper, one rule at the bottom"), and a drawing under it is a drawing with
   its roof cut off.

   No ground of its own. The plate paints --paper across its whole canvas as
   its first move, so the stage is the map until the drawing lands on it: a
   corner that is still loading, or one that folded, shows the map rather than
   a sheet of blank paper claiming to be a drawing. */
.drawnstage {
  position: absolute; left: 0; right: 0;
  top: var(--chrome-h, 0px);
  bottom: var(--drawn-cover, 0px);
  z-index: 10;
  display: none;
  overflow: hidden;
}
#app.drawn-on .drawnstage { display: block; }
.drawnstage .city-cv { display: block; width: 100%; height: 100%; cursor: grab; }
.drawnstage .city-cv.is-drag { cursor: grabbing; }
.drawnstage .city-cv:focus-visible { outline: 2px solid var(--ink); outline-offset: -3px; }

/* NOTHING ON THE MAP IS TAKEN AWAY TO MAKE ROOM FOR THE DRAWING. The first cut
   of this block hid two things while the drawing was up — the key, because it
   keys a shading the drawing stands on top of, and the zoom, because an
   axonometric has no answer for it. Clause 427 in test-landing.js went red on
   both inside the hour, and it was right about the first and useful about the
   second.

   THE KEY. 427 reads every `display: none` that lands on .legend or any part
   of it, at the base rule as well as in the media blocks, and allows exactly
   two ways for one to be legal: hidden only while the fold is closed, or
   hidden always and restored by .open. A third mechanism has to be argued, not
   assumed, and this one does not survive the argument. The corner under the
   drawing is still shaded, the card is still printing that corner's rent, and
   "greener = cheaper" is what tells a reader what that figure means against
   the rest of the island. The key already folds at every width, so a reader
   who wants the drawing uncovered has had the control all along. Taking it
   away on a toggle is the deletion the clause exists to stop, dressed as
   tidiness.

   THE ZOOM is a different finding, and it is about the clause's instrument
   rather than about this rule. 427 derives the key's parts from the product
   instead of a typed list, which is right, but one of its three readers walks
   `.legend[^{,]*?\s(\.[a-z][\w-]*)` across a COMBINATOR: the shipped rule
   `.legend.open + .zoombox` therefore enters .zoombox into the key's part set,
   and the zoom is a SIBLING of the key, not a part of it. Measured on this
   file at HEAD and in the working tree: identical part sets, .zoombox present
   in both, arriving through that one selector. Nothing had ever hidden the
   zoom, so an over-collection that has been there since that rule was written
   had never once fired. It is not fixed here. A clause's reader is not
   something to re-cut while the feature that surfaced it is unrated, and the
   zoom's own question changes shape entirely if the drawing stops being an
   overlay and becomes the map. */

/* in the rail the figure is a caption now, not a picture */
.z-city-away { padding-top: var(--s4); }

/* ---- the DOOR HERO — the landing's specimen anatomy, carried in --------
   One number leads. The range track under it is the published spread,
   end to end; the tick is the median, off-centre when the record is skewed.
   The slot and the tick are the landing's own (--track-bg, the ink). */
/* THE FIGURE IS THE FACE (concept pass 2026-09-09): one number at display
   size, its unit and its marks on the line beneath it, not beside it —
   34px shared a line with "per sqft, a month" and three badges and read as
   a caption; at 52 it is the thing the door is about */
.z-hero .hero-read { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin: 4px 0 2px; }
.z-hero .fig {
  font-size: calc(52 * var(--tpx)); font-weight: 750; letter-spacing: -.03em;
  color: var(--ink); font-variant-numeric: tabular-nums; line-height: 1;
}
.z-hero .hero-sub { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.z-hero .per { font-size: calc(15 * var(--tpx)); color: var(--ink-2); }
.z-hero .spread { margin: 8px 0 2px; }
.z-hero .track {
  position: relative; height: 6px; border-radius: 3px;
  background: var(--track-bg);
  box-shadow: inset 0 0 0 1px var(--rule);
}
/* centred on its own left: app.js writes left:N% for the median, and a tick
   placed by its left edge sat a width to the right — at 100% clear of the
   track's rounded end, a median printed outside its own range (Track C) */
.z-hero .tick { position: absolute; top: -3px; width: 2px; height: 12px; background: var(--ink); transform: translateX(-50%); }
.z-hero .ends {
  display: flex; justify-content: space-between; margin-top: 4px;
  font-family: var(--mono); font-size: calc(12 * var(--tpx)); color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  /* THE THREE CAPTIONS OF A RANGE, ONE PER LINE — promoted from the phone
   * block (2026-08-31): at the door rail's own width the row jumbled at 1280
   * too, wrapping "psf" off both end figures while a 9px gutter read the
   * left figure and the explainer as one string ("S$13.79 the published
   * spread" — review, measured in the pixels). Each end keeps its corner on
   * one unbreakable line; the explainer takes its own line under them at
   * every width, so the anatomy never depends on the rail's width. */
  flex-wrap: wrap;
}
.z-hero .ends > span { white-space: nowrap; }
.z-hero .ends > .dim { flex: 1 0 100%; order: 3; text-align: left; white-space: normal; }
/* the explainer is what WE say, not what the record says (the two-voice
 * rule): the two end figures keep the record's mono, the sentence under
 * them speaks in the sans, set left like every other sentence in the rail —
 * a forty-word monospace caption, centred over five ragged lines, was the
 * longest run of typewriter on the page (owner, 2026-09-03: "its fucking
 * bad"; re-skin render at 1280 and 390 confirmed five centred lines).
 * Since 2026-09-06 the caption is the SHORT form (app.js endsShort, one
 * line: the shape of the ends); the forty words that say which street or
 * pool set each end print in the how-read ledger below, where they are a
 * record and not a caption. */
.z-hero .ends > .dim { font-family: var(--sans); font-size: var(--t-small); letter-spacing: 0; line-height: 1.5; margin-top: 2px; }
/* ---- HOW THIS FIGURE WAS READ — the hero's ledger (owner ruling
   2026-09-06: number, range, date and one plain qualifier on the face; the
   instrument id and the pooling arithmetic behind an (i); nothing removed).
   A <details> in the rail, because its contents are a small ledger — what n
   counts, the units, how long the ask has stood, the shape of the ends,
   the source — and a ledger is read in place, not in a tooltip. Each badge
   is printed with its own note beside it, so the record is reached without
   a pointer; the summary wears the landing's (i) glyph. */
.z-hero .how { margin-top: 8px; }
.z-hero .how summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
  font-size: calc(14 * var(--tpx)); color: var(--ink-2); padding: 2px 0;
}
.z-hero .how summary::-webkit-details-marker { display: none; }
.z-hero .how summary:hover, .z-hero .how summary:focus-visible { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.z-hero .how[open] summary { color: var(--ink); }
.z-hero .how .i {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.3em; height: 1.3em; flex: none;
  border: 1px solid currentColor; border-radius: 50%;
  font-family: var(--serif); font-style: italic; font-size: .9em; line-height: 1;
}
.z-hero .how dl { margin: 4px 0 0; padding: 0; }
.z-hero .how .row { padding: 6px 0; border-top: 1px solid var(--rule); }
.z-hero .how dt { margin: 0 0 2px; }
.z-hero .how dd { margin: 0; min-width: 0; font-size: var(--t-small); line-height: 1.5; color: var(--ink-2); overflow-wrap: anywhere; }
.z-hero .how dd .fc-src { display: inline; margin: 0; font-size: var(--t-small); }

/* ---- the founder tool folds until asked for ---------------------------- */
/* A FOLD DRESSED AS A LINK (e2e swarm 2026-09-01, A116): the summary's only
 * affordance was an underline — the costume of navigation — while the native
 * disclosure marker was hidden, so readers reported expecting a page and
 * getting a toggle. The caret says "this opens in place"; the underline goes,
 * because two affordances telling two stories is how it misread. */
.ft-details summary {
  cursor: pointer; list-style: none; font-size: calc(15 * var(--tpx)); color: var(--ink-2);
  padding: 2px 0;
}
.ft-details summary::before { content: '\25B8\00A0'; color: var(--ink-3); }
.ft-details[open] summary::before { content: '\25BE\00A0'; }
.ft-details summary::-webkit-details-marker { display: none; }
.ft-details summary:hover { color: var(--ink); }
.ft-details[open] summary { color: var(--ink); margin-bottom: var(--s2); }

/* the headline speaks up (concept pass 2026-09-09): 21px serif under a
   10.5px eyebrow was the same voice as the lines below it; at 26 it is the
   one judgement on the sheet, as the landing's own headline is.
   AND IT IS THE PAGE'S h1 (a11y pass 2026-09-12): it was an h3 on a document
   with no h1 and no h2 outside the dialog, so a reader navigating by heading
   was handed a subsection of nothing. The rule below is unchanged apart from
   the element it names — the headline looks exactly as it did. */
.z-head h1 {
  font-family: var(--serif); font-size: calc(26 * var(--tpx)); font-weight: 600; letter-spacing: -.015em;
  line-height: 1.15; margin: 2px 0 8px; color: var(--ink); text-wrap: balance;
}
.z-head .dim, .z-head p { font-size: var(--t-body); line-height: 1.6; margin: 0; }
/* the lede's last line is never one word (2026-09-08): at 1440 the folded
 * island lede ended "…to count / competitors." — a 4-line paragraph with a
 * one-word tail. `pretty` lets the browser rebreak the last lines; where it
 * is not supported the paragraph wraps as before, nothing is lost. */
.z-head p { text-wrap: pretty; }
/* the island lede's fold (2026-09-08, app.js PICK): the pick sentence's tally
 * rides under the app's own caret (.ft-details), a step under the face, the
 * paragraph a step under the summary */
.z-head .lede-more { margin-top: var(--s1); }
.z-head .lede-more[open] summary { margin-bottom: 0; }
.z-head .lede-more p { margin-top: var(--s1); }

/* ---- the answer lines: one fact, one sentence, one receipt tap ---------
   Neutral by ruling: the red-green map scale is the product's ONE colour
   language, so the lines carry weight and type, never a second palette. */
.aline {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 10px 0 10px 11px; margin-left: -11px;
  border-bottom: 1px solid var(--rule); border-left: 3px solid var(--rule);
  font-size: calc(16 * var(--tpx)); line-height: 1.6; color: var(--ink-2);
}
.aline:last-child { border-bottom: 0; }
.aline .at { flex: 1 1 auto; min-width: 0; }
/* the figure in a sentence is a step larger than the sentence (concept pass
   2026-09-09): the eye finds the number first, the words say what it is */
.aline .at b { color: var(--ink); font-weight: 680; font-size: calc(17 * var(--tpx)); font-variant-numeric: tabular-nums; }
.aline.dim2 { font-size: calc(15 * var(--tpx)); border-left-color: transparent; }
.aline.dim2 .at b { color: var(--ink-2); }

/* WHERE THIS CORNER SITS AMONG THE OTHERS (2026-09-15).
   ONE SCALE: the island's full published span, with both middle halves laid
   on it and this corner's own lead marked. Two bars at two scales would
   compare nothing, because the eye reads width and width would mean a
   different number on each row.
   NO NEW TOKEN, and no colour of its own. Every value here is one the card
   already uses, so the colour law (418 A3, hue 20-70 or chroma under 0.02)
   has nothing new to weigh, and the sign yellow stays rationed to the
   caution it exists for (418 A8) instead of being spent on a comparison
   that is deliberately not a verdict.
   It is NOT the hero's bar. That one spans this corner's own row, lo to hi,
   in a filled recess with a tick; this one is the other corners, drawn as an
   axis with two nested bands. They sit a line apart and must not be read as
   the same kind of thing. */
.z-hero .among { margin: 10px 0 2px; }
/* the caption is the hero's own micro voice, the one under its first bar
   ("25th to 75th percentile; the tick is the median"), because the two bars
   must be told apart by a reader who is glancing and not studying */
.among-cap {
  margin: 4px 0 0; font-family: var(--sans); font-size: var(--t-small);
  line-height: 1.5; color: var(--ink-3);
}
.z-hero .aline.ctx { border-bottom: 0; padding-left: 0; border-left: 0; margin-top: 2px; }
.among { position: relative; height: 18px; margin: 2px 0 8px; }
.among .am-axis {
  position: absolute; left: 0; right: 0; top: 8px; height: 2px;
  background: var(--track-bg); border-radius: 1px;
}
.among .am-isl { position: absolute; top: 6px; height: 6px; border-radius: 3px; background: var(--rule); }
.among .am-area { position: absolute; top: 5px; height: 8px; border-radius: 4px; background: var(--ink-3); }
/* centred on its own left, for the reason the hero's tick carries: app.js
   writes left:N%, and a mark placed by its left edge sits a width to the
   right — at 100% it would stand clear of the scale it is meant to be on */
.among .am-me {
  position: absolute; top: 0; width: 2px; height: 18px;
  background: var(--ink); transform: translateX(-50%);
}
/* A BADGE INSIDE AN ANSWER LINE IS A BADGE, NOT A FIGURE. The two rules above
 * lift a line's figures to the ink, and app.js writes some stamps as <b> —
 * `<b class="badge ask">withheld</b>`, `<b class="badge ask">spread</b>` —
 * so they were repainted as figures: the ask mark in a demoted line computed
 * ink-2 on the plate, black on black at 1.56:1 (measured live through CDP,
 * 2026-09-03, the day of the re-skin; before it the same cascade had painted
 * the mark bone on its amber tint and nobody looked). The static colour law
 * (418 A6) reads .badge.ask and cannot see a cascade; these two lines sit
 * BELOW .aline.dim2 .at b at equal specificity so order carries the first,
 * and the second outranks everything above it. */
.aline .at b.badge { color: var(--ink-2); font-weight: 400; }
.aline .at b.badge.ask { color: var(--hi); }
/* the demoted-rent fold (app.js, 2026-08-31): sits in the answer column as
   one more line — same border rhythm as .aline — with the ft-details summary
   styling (caret marker, A116) carrying the open/close affordance */
.ctx-fold { padding: 10px 0; border-bottom: 1px solid var(--rule); }
.ctx-fold:last-child { border-bottom: 0; }
.ctx-fold summary { font-size: calc(15 * var(--tpx)); color: var(--ink-2); line-height: 1.6; }
.ctx-fold[open] summary { margin-bottom: 0; }
.ctx-fold .aline:first-of-type { border-top: 1px solid var(--rule); margin-top: 8px; }

/* the receipt tap — hover on desktop, CLICK/TAP everywhere (the receipts
   are the product's promise; a promise you cannot reach on the phone you
   are standing in the shop with is not a promise)

   THE CIRCLE IS CUT FROM .aline's LINE BOX, and that is why it moved on
   2026-09-16. It is a flex item beside a sentence at align-items: flex-start,
   so a circle taller or shorter than the first line sits proud of it. .aline
   was 13.5/1.6 — a 21.6px line — and the circle was 22. .aline is 16/1.6 now,
   a 25.6px line, so the circle is 26. It is a px constant and not a calc for
   the same reason --bar is one on the landing: the sign law's loose reader
   adds the lengths in a calc and ignores the multiplication, and would go on
   printing green about a number it had got wrong. If .aline's rung or leading
   changes again, this number changes with it — it is derived, not chosen. */
.info {
  flex: 0 0 auto; width: 26px; height: 26px; margin-top: 1px; border-radius: 50%;
  border: 1px solid var(--rule); background: transparent; color: var(--ink-2);
  font: italic 620 calc(14 * var(--tpx)) Georgia, serif; line-height: 1; padding: 0;
}
.info:hover, .info.pinned { color: var(--p-ink); border-color: var(--plate); background: var(--plate); }

/* ---- badges: the ONE caution mark, and the neutral stamps -------------- */
.badge {
  display: inline-block; font-family: var(--mono); font-size: calc(12 * var(--tpx));
  letter-spacing: .06em; padding: 1px 6px; border-radius: var(--r);
  border: 1px solid var(--rule); color: var(--ink-2); vertical-align: 1px;
}
.badge.ask {
  /* THE CAUTION MARK. An ask is a landlord's opening position — the one
     thing on this page that has earned the hazard colours, and they are
     BLACK carrying THE MARK YELLOW: the plate, and on it #FFD60A at
     13.13:1. The same yellow on the paper would be 1.27:1, invisible —
     which is why this mark can never be mistaken for the page it sits on,
     for the brand tile (the mark yellow on paper, with a keyline) or for
     the primary button (the sign yellow under the ink). Clause 418 A6
     measures the first two; A8 keeps the button the only other yellow. */
  color: var(--hi); background: var(--plate); border-color: var(--plate);
}
.badge.dim { border-color: var(--rule); color: var(--ink-3); }

/* ---- the size converter + quoted rent (typed, optional) ---------------- */
.sizer { padding: 6px 0 10px 11px; margin-left: -11px; border-bottom: 1px solid var(--rule);
  font-size: calc(15 * var(--tpx)); color: var(--ink-2); }
.sizer label { display: inline-flex; align-items: center; gap: 6px; }
.sizer .ft-in { width: 90px; }
#size-out { display: block; margin-top: 5px; line-height: 1.55; font-variant-numeric: tabular-nums; }
.money { display: flex; align-items: center; gap: 6px; margin: 6px 0; }
.money .pre { color: var(--ink-3); font-size: calc(15 * var(--tpx)); }
.z-quote .pl b { color: var(--ink); font-variant-numeric: tabular-nums; }

/* the quote box's unit select + the capture flow (owner ruling 2026-09-01).
 * #ask-unit is NOT .ft-in on purpose — a select fires input on choice, and
 * ft-in + input is the founder tool's debounce path. */
#ask-unit, .qsel {
  background: var(--paper); border: 1px solid var(--rule); border-radius: var(--r);
  color: var(--ink); padding: 5px 8px; font-size: calc(15 * var(--tpx));
}
#ask-unit:focus-visible, .qsel:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.money #ask-rent { flex: 1 1 110px; min-width: 0; }
.money #ask-unit { flex: 0 1 auto; max-width: 48%; }
#quote-out .pl { margin: 6px 0 0; line-height: 1.6; }
.qgive { margin-top: var(--s2); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.qgive .dim { font-size: calc(13 * var(--tpx)); }
.qgive .btn, .qgive .btn-ghost { margin-top: 0; }
/* the preview of what a reader is about to send is a RECORD: paper */
.qprev {
  margin-top: var(--s2); border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--paper); padding: 10px 12px;
}
.qprev > .eyebrow { display: block; margin-bottom: 2px; }
.qrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-top: var(--s2);
  font-size: calc(14 * var(--tpx)); color: var(--ink-2); }
.qrow label { display: inline-flex; align-items: center; gap: 6px; }
.qdl {
  display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; margin: var(--s2) 0 0;
  font-size: calc(14 * var(--tpx)); line-height: 1.5;
}
.qdl dt { color: var(--ink-3); }
.qdl dd { margin: 0; color: var(--ink-2); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.q-settled { border-left: 2px solid var(--ink); padding-left: 8px; }

/* ---- the trade picker (a click-list, not a menu) ----------------------- */
.z-picker .pchip {
  display: inline-block; margin: 4px 4px 0 0; padding: 5px 10px;
  font: 400 calc(14 * var(--tpx))/1.4 var(--sans); text-transform: none; letter-spacing: 0;
  color: var(--ink-2); background: transparent; border: 1px solid var(--rule);
  border-radius: 999px; height: auto;
}
.z-picker .pchip:hover { color: var(--p-ink); background: var(--plate); border-color: var(--plate); }

/* ---- ranked corner rows (the next rung down) --------------------------- */
/* an anchor since 2026-09-03 (app.js islandPanel/areaPanel): a row has a real
   href now, so the anchor's underline and link colour are set back to the
   row's own — the row is a row, the rung's address is in its href */
/* A ROW IS A GRID OF THREE (2026-09-08, the app's 9-work). It was a flex row
   of two columns, and the right one — the mono value column, capped at
   170px — held the figure, the contested mark AND the rank qualifier, so
   "n≥3 contracts · quarter to 2026-06 · on BAYFRONT AVENUE" stood as three
   or four right-aligned lines with street names broken mid-name; rows ran
   86 to 103px and six of them were 530px of ragged mono (measured at 1440,
   app-probe). Now the name and its area share the first line, the figure
   sits at the right on the same baseline, and the qualifier is the row's
   own caption under both — sans, one step dimmer, one line for most. */
.rrow[data-zone] {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 2px;
  align-items: baseline; width: 100%;
  padding: 8px 4px; background: none; border: 0;
  border-bottom: 1px solid var(--rule); color: var(--ink-2);
  font-size: calc(15 * var(--tpx)); text-align: left; text-decoration: none; cursor: pointer;
}
.rrow[data-zone]:last-child { border-bottom: 0; }
/* a row under the pointer steps toward the reader as well as lighting: the
   lit recess alone was a colour-only hover (L8b), the 3px is the affordance */
.rrow[data-zone] { transition: background var(--motion) var(--ease), transform var(--motion) var(--ease); }
.rrow[data-zone]:hover { color: var(--ink); background: var(--track-bg); transform: translateX(3px); }
.rrow[data-zone] .rl { min-width: 0; }
.rrow[data-zone] .rl b { font-weight: 600; color: var(--ink); }
.rrow[data-zone] .rl .dim { font-size: calc(13 * var(--tpx)); }
.rrow[data-zone] .rv {
  font-family: var(--mono); font-size: calc(14 * var(--tpx)); white-space: nowrap;
  font-variant-numeric: tabular-nums; color: var(--ink);
  /* a column, so a provenance tag drops UNDER the figure instead of widening
   * the value block: "(asks differ)" pushed one row's figures ~97px left and
   * crushed the name column to 52px — "Upper Paya Lebar" rendered one word
   * per line while the longer "Jurong West Central" sat whole (review
   * 2026-08-31, measured). Untagged rows are one flex line, unchanged. */
  display: flex; flex-direction: column; align-items: flex-end;
}
.rrow[data-zone] .rv .dim { font-size: calc(13 * var(--tpx)); }
/* the metric's class/ask tag ("office space, asking") and a dash row's
 * stand-in figure are parts of the VALUE and stay in its column, wrapped
 * under the nowrap figure and capped so the name keeps about 40% of a 390px
 * row (sub-wave 4, 2026-09-03; row 319 − padding 8 − gap 10 − 170 = 131px
 * for the name; measured, sw4-cdp2). The rank qualifier — a size badge, an
 * n, a date and a street — left this column on 2026-09-08: see the next
 * rule. */
.rrow[data-zone] .rv .rq { white-space: normal; text-align: right; max-width: 170px; }
/* the rank qualifier is the row's CAPTION: the third cell, spanning both
 * columns, in the row's own sans at the area line's size, one step dimmer.
 * The street inside it is one word (app.js rankRowExtras), so a wrap falls
 * between the pieces. B73 (e) reads this rule and the cell's place. */
.rrow[data-zone] > .rq { grid-column: 1 / -1; white-space: normal; font-size: calc(13 * var(--tpx)); line-height: 1.45; color: var(--ink-3); }
/* the ranked row and its receipt (i) are SIBLINGS in a wrapper (e2e swarm
 * 2026-09-01, TR8) — a button may not contain a button, so the wrapper takes
 * over the row's border and the row gives its own up */
.rrow-w { display: flex; align-items: center; gap: 4px; border-bottom: 1px solid var(--rule); }
.rrow-w:last-child { border-bottom: 0; }
.rrow-w .rrow[data-zone] { border-bottom: 0; flex: 1; min-width: 0; }

/* ---- the founder tool (be316f0 — starts empty, stays theirs) ----------- */
/* the grid's 1px gaps ARE its rules (the gap shows the --rule ground) and
   each cell is the recess with a paper box in it: a form on paper, sunk a
   step, with the writing surface the page's own. */
.ft-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--rule);
  border: 1px solid var(--rule); border-radius: var(--r); overflow: hidden;
  margin-top: var(--s2);
}
.ft-grid label {
  display: flex; flex-direction: column; gap: 3px; padding: 8px 10px;
  background: var(--track-bg); font-size: calc(13 * var(--tpx)); color: var(--ink-2);
}
.ft-in {
  background: var(--paper); border: 1px solid var(--rule); border-radius: var(--r);
  color: var(--ink); padding: 5px 8px; font-size: calc(15 * var(--tpx)); font-variant-numeric: tabular-nums;
}
.ft-in::placeholder { color: var(--ink-3); font-size: calc(13 * var(--tpx)); }
.ft-in:focus { outline: none; border-color: var(--ink); }
/* keyboard focus stays VISIBLE: the rule above erases the ring for pointer
 * users, so the ring returns for :focus-visible — and it must sit AFTER that
 * line, where equal specificity resolves by order (a first cut landed the
 * fix above its own defeat and shipped a no-op; verified live since) */
.ft-in:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.ft-res { margin-top: var(--s3); }
.ft-clump {
  background: var(--paper); border: 1px solid var(--rule); border-radius: var(--r);
  padding: 10px 12px; margin-bottom: var(--s2); font-size: calc(15 * var(--tpx)); line-height: 1.65;
  color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.ft-clump b { color: var(--ink); }

/* ---- prose bits inside sections ---------------------------------------- */
.fc-why { font-size: var(--t-small); color: var(--ink-2); line-height: 1.6; margin: 6px 0 0; }
.fc-src { font-family: var(--mono); font-size: calc(12 * var(--tpx)); color: var(--ink-3); margin: 4px 0 0; letter-spacing: .02em; }
.z-trust .fc-why { font-size: calc(14 * var(--tpx)); }
.z-trust b, .z-notpub b { color: var(--ink); }
.z-notpub .fc-why { margin-bottom: var(--s2); }

/* THE ONE PRIMARY BUTTON, and the one place this sheet spends the sign: the
   landing's .go carried in — the mark yellow under the ink, a keyline in
   the ink, and on hover it lifts a pixel and leaves its cast on the paper.
   Three buttons wear it (boot's retry, the door's "every receipt", the
   quote box's send) and each is the one thing to do on its screen; a
   second yellow control beside any of them would be a second mark. */
.btn {
  display: inline-flex; align-items: center; gap: 7px; margin-top: var(--s2);
  padding: 8px 14px; font-size: calc(15 * var(--tpx)); font-weight: 600;
  color: var(--ink); background: var(--sign); border: 1px solid var(--ink);
  border-radius: var(--r); text-decoration: none;
  transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
.btn:hover { transform: translateY(-1px); box-shadow: 3px 3px 0 var(--cast); }
.btn:active { transform: translate(2px, 2px); box-shadow: none; }
.btn .arr { color: var(--ink-2); font-family: var(--mono); }
.btn-ghost {
  background: none; border: 0; padding: 2px 0; margin-right: var(--s3);
  color: var(--ink-2); font-size: calc(14 * var(--tpx)); text-decoration: underline;
  text-underline-offset: 3px; text-decoration-color: var(--rule);
}
.btn-ghost:hover { color: var(--ink); text-decoration-color: var(--ink); }
/* THE TOOLS ARE ANNOUNCED WHERE THE READER IS (Track B, [58] [76]): the
   founder fold and the quote box sat five pane-heights down a phone door and
   only their own summaries said they existed. The hero carries a line naming
   both; each is a BUTTON that scrolls (app.js jumpTo), never an anchor to an
   id — a same-page "#ft-fold" would be a hash the router reads as a dead
   rung and answers with the island. */
.jumps { margin: 8px 0 0; font-size: calc(14 * var(--tpx)); color: var(--ink-3); }
.jumps .btn-ghost { margin-right: 0; font-size: calc(14 * var(--tpx)); }

/* ==== the record sheet (the receipts) =================================== */
/* A RECEIPT IS A RECORD: the drawer is the page's own paper, ruled in the
   hairline, lifted off the page by a scrim of thinned ink (72%, and the
   sheet reads 6.72:1 against it) so the page it rose from is still there. */
.sheet {
  position: fixed; inset: 0; z-index: 200; display: none;
  background: color-mix(in srgb, var(--ink) 72%, transparent); backdrop-filter: blur(3px);
}
.sheet.on { display: flex; align-items: flex-start; justify-content: center; }
.sheet-inner {
  width: min(760px, 94vw); margin: 4vh 0 6vh; background: var(--paper);
  border: 1px solid var(--rule); border-radius: 6px;
  /* the INNER box scrolls, so the sticky head has a scroll container to
   * stick to — with overflow:hidden here the close button scrolled away */
  max-height: 90vh; overflow-y: auto;
}
.sheet-inner.mid { width: min(600px, 94vw); }
.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s4) var(--s5); border-bottom: 1px solid var(--rule);
  position: sticky; top: 0; background: var(--paper); z-index: 2;
}
.sheet-head h2 { font-family: var(--serif); font-size: var(--t-read); font-weight: 600; margin: 0; color: var(--ink); }
.xbtn {
  width: 30px; height: 30px; background: none; border: 1px solid var(--rule);
  border-radius: var(--r); color: var(--ink-2); font-size: calc(17 * var(--tpx));
  /* a flex item's width is a suggestion: the head squeezed the phone's 44px
   * close to 36.4 beside a long title (Track B, measured on the charter) */
  flex: 0 0 auto;
}
.xbtn:hover { color: var(--ink); border-color: var(--ink); }
.sheet-body { padding: var(--s2) var(--s5) var(--s6); }
.rsec { padding: var(--s4) 0; border-bottom: 1px solid var(--rule); }
.rsec:last-child { border-bottom: 0; }

.frow { padding: 8px 0; border-bottom: 1px solid var(--rule); }
.frow:last-child { border-bottom: 0; }
.frow .fl { font-size: calc(15 * var(--tpx)); color: var(--ink-2); }
.frow .fv { font-size: calc(16 * var(--tpx)); color: var(--ink); margin-top: 2px; font-variant-numeric: tabular-nums; }
.frow.absent .fv { color: var(--ink-3); }
.frow.absent .fl { color: var(--ink-3); }

.rt { width: 100%; border-collapse: collapse; margin-top: var(--s2); font-variant-numeric: tabular-nums; }
.rt th {
  text-align: left; font-family: var(--mono); font-size: calc(12 * var(--tpx)); letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-3); padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--ink);
}
.rt td { font-size: calc(14 * var(--tpx)); color: var(--ink-2); padding: 4px 8px 4px 0; border-bottom: 1px solid var(--rule); }
/* a cell may break a long identifier (sub-wave 4, 2026-09-03): the absences
 * ledger's "householdSpend.monthly" and "suppressed_at_source" cannot break
 * on their own, and the table ran 339px in a 312px section at 390px — the
 * drawer scrolled sideways by 5px (measured, sw4-cdp2). A figure never
 * breaks under this rule unless it alone exceeds the cell. */
.rt td, .rt th { overflow-wrap: anywhere; }

/* ==== the tooltip (one system, all receipts) ============================ */
/* a plate, like every surface that floats over another: the plate's inks
   read over the paper and over the map alike */
#tip {
  position: fixed; z-index: 300; max-width: 340px;
  background: var(--plate); border: 1px solid var(--p-rule); border-radius: var(--r);
  padding: 9px 11px; box-shadow: 0 14px 34px color-mix(in srgb, var(--ink) 40%, transparent);
  /* NEVER interactive: a tooltip that swallows clicks killed the category
     bar on 2026-08-19 — pinned over the chips, it ate every first click */
  pointer-events: none;
  /* HIDDEN BY OPACITY, NOT display:none (evidence pass 2026-09-02, Track A):
     #tip is a role=status live region, and a live region that is out of the
     tree at the instant it is filled announces nothing — showTip filled and
     showed it in one tick, so every receipt was silent to a screen reader.
     Invisible and inert here; in the tree always; emptied by app.js hideTip
     so nothing stale sits behind the opacity. */
  opacity: 0;
}
#tip.on { opacity: 1; }
#tip .g { font-size: calc(14 * var(--tpx)); color: var(--p-ink); line-height: 1.55; }
#tip .s { display: block; font-size: var(--t-small); color: var(--p-ink-2); line-height: 1.55; }

/* ==== mobile: the ladder keeps its "up", the rail becomes the sheet ===== */
@media (max-width: 900px) {
  /* 46vh of rail left the island 380px tall on a 844px phone, under a 52px
   * bar and a 40px chip row — a map the width of the screen and the height
   * of a postcard, with the legend and the attribution stacked over its
   * bottom third (measured 2026-08-30 at 390×844). 40vh is a judgement, not
   * a measurement: the island gets 50px more, the rail keeps its 200px
   * floor, and on a door #app.zone-open below still takes 62vh. */
  /* THE RAIL IS A SHEET OVER THE ISLAND (concept pass 2026-09-09). The two
   * rows above — map, then 40vh of rail (55vh on a door) — gave the island
   * 227px of an 844px phone under 187px of chrome, a letterbox. The map now
   * takes the whole column and the rail is a sheet drawn over it, pulled by
   * its grip (app.html .grip, app.js bindSheet): at PEEK the grip, the
   * eyebrow and the headline show and the island has everything else; at
   * HALF a rung reads with the map still framed above it; at FULL the card
   * is the page. app.js sets --sheet-y on the rail as it rests or moves and
   * --map-cover on the stage, so the key, the zoom and the credits sit on
   * the sheet's edge and every camera fit lands in the uncovered glass. */
  .main { display: block; position: relative; }
  .mapstage { position: absolute; inset: 0; }
  .map-ui { bottom: var(--map-cover, var(--sheet-peek)); }
  .rail {
    position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 30;
    border-left: 0; border-top: 1px solid var(--rule); border-radius: 14px 14px 0 0;
    box-shadow: 0 -8px 28px color-mix(in srgb, var(--ink) 24%, transparent);
    transform: translateY(var(--sheet-y, calc(100% - var(--sheet-peek))));
    transition: transform .32s var(--ease); will-change: transform;
  }
  .rail.dragging { transition: none; }
  /* A FIXED HEIGHT THAT COULD NEVER HOLD ITS CONTENTS (found by render,
     2026-09-16). This was `height: 48px` with `align-items: flex-end`, which
     does not clip what does not fit — it pushes it out of the TOP of the box,
     over the pill. The dock inside measured 53.8px before today's type raise
     and 56.1 after, in a content box of 41 (48 less the 6px of bottom padding
     and the 1px rule), so 12.8px of the trail stood outside the grip before
     this pass and 15.1 would have stood outside it after.

     THE ARITHMETIC WAS NEVER POSSIBLE. The trail's rungs are held to the 44px
     target floor by clause 433, and 44 plus this rule's own 6px of bottom
     padding is 50 — already over 48 on the day the touch pass landed, with no
     type raise needed. A number that cannot be satisfied by the numbers beside
     it is not a height, it is a wish, and nothing in the repo could see it
     because both batteries read stylesheets and this only exists rendered.

     min-height, so the bar is as tall as what it holds and no taller: 62.1px
     at 390 today. Two things downstream are now approximate rather than
     wrong, and are named here rather than quietly carried:
       - the "18px and not 44" argument under .grip-bar assumed a 48px grip.
         The strip is still 18px at the top and the dock is still bottom
         aligned, so the reasoning holds; the figure in it does not.
       - --sheet-peek's 168px floor is "the grip, the eyebrow and a two-line
         headline", and the grip in that sum is 14px taller now. MEASURED at
         360, 390 and 430 on 2026-09-16 rather than assumed: grip 63.1, then
         the eyebrow at 18.6 and a headline line at 29.9, which puts a
         two-line head's bottom 161.5px under the grip's top edge. The floor
         still covers it, by 6.5px. It was worth checking and it did not move;
         app.js's own rest is 375px on an 844 phone in any case, and the floor
         only ever shows on the first frame. */
  .grip {
    display: flex; align-items: flex-end; flex: none;
    min-height: 48px; padding: 0 var(--s4) 6px; position: relative;
    border-bottom: 1px solid var(--rule); touch-action: none; cursor: grab; user-select: none;
  }
  /* `grab` and `grabbing` stay on .grip and are NOT re-pointed at .grip-bar,
     although the role moved there on 2026-09-16. The cursor describes the DRAG
     surface, and the drag surface is still this whole bar: every pointer
     handler in bindSheet is bound to #rail-grip. Moving the cursor onto the
     bar would strip it off the 390x48 area a reader actually drags and paint
     it on a strip. `position: relative` above is also .grip-bar's containing
     block, so it does not move either. */
  .grip:active { cursor: grabbing; }
  /* THE HANDLE IS NOW A CONTROL, SO IT HAS TO BE SHAPED LIKE ONE.
     It was a 40x4 pill: 160 square pixels, decorative, aria-hidden. When the
     button role moved onto it (app.html, and the reason is written there) it
     became the thing a keyboard reader tabs to and the thing the sheet's own
     focus ring has to draw around, and neither works on a four-pixel bar.
     So the ELEMENT takes the strip across the top of the grip and the PILL
     becomes its ::before, which keeps the drawing identical and gives the role
     a box.

     18px and not 44: the strip is at the TOP of a 48px grip whose dock is
     bottom-aligned, and a 44px target centred on it would cover the crumbs
     across the middle of the bar. Being positioned, it would also paint and
     hit-test ABOVE the non-positioned .grip-dock and swallow their taps —
     which is the exact trap clause 434 T6 already watches for on .metricbar
     and .crumbs. The thumb keeps the whole 48px bar, because the pointer
     handlers are still on the wrapper; it is only the tab stop that lives
     here. */
  .grip-bar {
    position: absolute; left: 0; right: 0; top: 0; height: 18px; display: block;
  }
  .grip-bar::before {
    content: ''; position: absolute; left: 50%; top: 7px; width: 40px; height: 4px; margin-left: -20px;
    border-radius: 2px; background: var(--rule);
  }
  /* the sheet's own ring, spelled again rather than varied: the token is the
     same --ink the bare :focus-visible rule uses, and clause 418 A9 lets a
     ring that names no ground through on exactly that condition — it is the
     sheet's ring repeated, not a new colour nobody has measured. The offset
     is 0 because the strip is flush with the sheet's rounded top edge and a
     2px outset ring would be clipped by it. */
  .grip-bar:focus-visible { outline: 2px solid var(--ink); outline-offset: 0; }
  .grip-dock { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); width: 100%; min-width: 0; }
  .grip-dock .crumbs { flex: 1 1 auto; }
  .grip-dock .tbtn { flex: none; }
  /* the chrome bar tightens a step: 99px of chips over the island were the
   * larger half of the old letterbox's cost */
  .mapchrome { padding: 6px var(--s3) 7px; gap: 4px; }
  /* the trade menu's fence is for the boundary it shares a LINE with, and
     here it shares none: this block stacks the menu on its own row above the
     shade and show groups, so the rule would hang off the end of row one
     separating nothing. The .tgrp fence below stays, because that boundary
     is still mid-row. */
  .catmenu { border-right: 0; padding-right: 0; margin-right: 0; }
  .mchip { font-size: calc(13 * var(--tpx)); }
  /* THE TRAIL GETS ITS OWN LINE (e2e swarm 2026-09-01). One shared row gave
   * the crumbs 119px and the address box 101px at 390: the trail rendered as
   * a lone faded glyph — "e / Bayfront Subzone" — and the box cut its own
   * placeholder mid-word ("address or po"). Sharing was the defect, not the
   * shares: the bar now wraps, the box takes what the wordmark leaves on the
   * first line, and the trail takes the whole second line, where a
   * three-deep trail fits at 11.5px without the fade ever firing. The
   * scroller + .cut fade below stay as the backstop for the island's longest
   * names. #app's bar row is auto for exactly this (it was --bar-h, a fixed
   * 52px that would have clipped the second line). */
  #app { grid-template-rows: auto 1fr; }
  /* THE BAR IS TWO ROWS BY NAME, NOT BY WRAP (founder walk 2026-09-04, item
   * 5). The wrapped flex bar (2026-09-01, then a 96px basis floor on
   * 2026-09-03) gave the address box what the wordmark and the "What this
   * is" button left of the first line: 137px at 390 and 107px at 360
   * (measured, scratch sw10-phone.js), against a placeholder that is 133px
   * wide at the 16px every typed field keeps on a phone (the coarse block
   * below). "address or posta" on the one phone width every review used,
   * and at 360 no wording of "address or postal" fits the 85px of content
   * left at 16px; at 320 the button wrapped to a third line, the bar 121px.
   * The width was there, on the wrong row. Grid names the rows: the wordmark
   * and the box share the first, the trail and the button the second, and
   * the box takes every pixel between the mark and the edge: re-measured
   * after the change, 172px at 320, 212 at 360, 242 at 390, 266 at 414, the
   * bar 88px at all four (it was 86, and 121 at 320) and the 133px
   * placeholder whole on every one of them. The button keeps the right
   * end, one line under where a thumb found it before. The
   * empty spacer has no cell and no job here. The landscape block below puts
   * the bar back on one flex line; B70 holds that block after this one. */
  /* ONE ROW AGAIN (concept pass 2026-09-09): the trail and the charter door
   * moved down onto the sheet's grip (app.js dockChrome), where a rung's
   * trail sits beside the thing it names. The bar keeps the mark and gives
   * the address box every pixel to the edge, which is the measure the
   * second row was made for (242px at 390, the 133px placeholder whole). */
  .topbar {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "mark addr";
    column-gap: var(--s3); padding-top: 6px; padding-bottom: 6px;
  }
  /* the identity block is the cell now, not the wordmark inside it — and on
     a phone it holds only the wordmark, because dockChrome has already
     carried the trail down to the sheet's grip */
  .tlead { grid-area: mark; }
  .addr.top { grid-area: addr; width: auto; min-width: 0; }
  /* THE ONE "WHAT IS THIS" DOOR STAYS ON THE PHONE (Track B): it was hidden
   * here on the theory that the rail's own copy is one tap away, but on a
   * phone the rail's copy is at the FOOT of a scroll, and a stranger arriving
   * from a shared link on a phone is exactly the reader who needs it first.
   * Compacted, not deleted (the sign's own rule: a narrow width is a reason
   * to set something tighter, never to remove it). */
  .tbtn { padding: 4px 8px; font-size: calc(13 * var(--tpx)); }
  .spacer { display: none; }         /* no cell in the grid and no job on it (in the flex bar it crushed the crumbs to ~30px) */
  /* NEVER hidden: the crumbs are the only "up". Their scrollbar THUMB is,
   * though — .catbar and .metricbar hid theirs twice over and this third
   * scroller was missed, so a grey capsule floated under "Singapore"
   * reading as a stray skeleton (review 2026-08-31). The cut edge's cue is
   * the .cut fade, set by app.js setCrumbs only when the trail truly
   * overflows.
   * RE-BASED 2026-09-16: the scroll, the hidden thumb and the .cut fade all
   * moved up to the base .crumbs rule. They were written here because only a
   * phone was believed to overflow; the desktop overflows from 1280 down and
   * had none of them (measured: "dok North" at 1024). This block keeps the
   * one thing that is genuinely the phone's — the grid cell. */
  .crumbs { grid-area: trail; }
  .crumb { font-size: calc(13 * var(--tpx)); padding: 3px 4px; }
  /* the shade row scrolls sideways instead of wrapping over half the map.
   * The trade row used to be here too — 11 retail trades stacked four rows
   * deep buried the island — and is now a menu that opens over the map
   * instead of a row that lives on it, which is the same finding answered
   * at the source (concept pass 2026-09-09). */
  .metricbar { flex-wrap: nowrap; overflow-x: auto; max-width: 100%;
    scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  /* the 28px padding-right that clears the row's end moved to the BASE rule
   * (2026-08-31): once display:contents made the trade row overflow on
   * desktop too, the clearance was a law of the bar, not of the phone */
  .metricbar::-webkit-scrollbar { display: none; }
  /* the panel is anchored to the button, so on a narrow pane it must be
   * held inside the pane by its own measure rather than by the button's
   * position — 78vw of a 390px phone is 304, and the panel's widest row is
   * two chips */
  .catpanel { max-width: calc(100vw - 2 * var(--s3) - 24px); }
  /* the shade-by row's label fold moved to its OWN block below, keyed to the
   * row's measured width, not to this layout breakpoint (Track A, [55]) */
  .mchip { white-space: nowrap; flex: 0 0 auto; }
  .info { width: 26px; height: 26px; }
  /* THE DOOR RULE HAS TO BE ANSWERED HERE, IN THIS BLOCK, BY NAME.
   * `#app.zone-open .main` (see "AT THE BOTTOM OF THE LADDER THE PANEL IS THE
   * SUBJECT") carries one id and two classes; a media query adds no specificity
   * at all, so the bare `.main { grid-template-columns: 1fr }` above loses to it
   * and the DESKTOP two-column layout reasserts itself on a phone. Measured at
   * 390px with the class applied: `grid-template-columns: 0px 460px` — the map
   * gone entirely and 460px of rail against a 390px viewport, 70px of the
   * reading surface off the side of a screen that cannot scroll, because #app
   * is overflow:hidden. The same at 414px. That is the second click, the one
   * the whole ladder exists to reach, broken on every phone in the field.
   *
   * The desktop rule's INTENT carries over — on a door the panel is the
   * subject — but in one column that intent is a ROW, not a second column:
   * the reading surface takes more height instead of more width. 62vh over the
   * default 46vh was a judgement, not a measurement, and the number to argue
   * with; the two-column collapse is not.
   *
   * ARGUED, WITH A MEASUREMENT (sub-wave 3, probe-fit 2026-09-03). At 62vh
   * on 390×844 the door's pane was 253px: the chrome bar covered 87 from the
   * top, and the foot — a 70px zoom stack, the folded key, three rows of
   * credits and the gaps — 129 from the bottom. 37px of glass. The corner
   * the door is ABOUT could not be framed in that at any zoom (fit against
   * the true pane: z 12.04, the corner a speck under the bar). Two things
   * move: the rail to 55vh, and the zoom stack to one row (below), so the
   * pane is 312 and the uncovered glass 122 — a subzone frames at street
   * scale. The rail is still the larger half; the door stays the subject.
   *
   * AND THE PANE HAS A FLOOR OF ITS OWN (same probe, second pass). With the
   * door's trail on the bar the pane measured 294 at 844 tall and 214 at
   * 667; the chrome bar (87) and the foot band (44 + 42 of credits + the
   * gaps, 103) are fixed costs, so the glass between them was 92px on the
   * first and 24px on the second — a door on a small phone had no map at
   * all, only chrome. The pane's floor is those two costs plus 120px of
   * glass, and the rail gives way to it before its own 200px floor: at 844
   * the rail is 448 (still the larger half), at 667 it is 271. */
  /* the door's row split is gone with the rows (concept pass 2026-09-09):
   * the sheet rests at half on a corner and the reader takes it to full;
   * the desktop rule's column split is inert under display:block */
  #app.zone-open .main { grid-template-columns: 1fr; }
  /* THE ZOOM STACK LIES DOWN (sub-wave 3, 2026-09-03). Two buttons stacked
   * were 70px of the foot band (94 with the coarse-pointer floor); side by
   * side they are one button tall and the band is the key's own height. The
   * stack stays IN the band, in flow, so the map's insets and the corner
   * fit measure it rather than guess at it, and the open key's reservation
   * on the right is the row's width in the same token the buttons are. */
  .zoombox { flex-direction: row; }

  /* THE LEGEND MAY TAKE EVERYTHING THE ZOOM STACK DOES NOT. 66vw put its
   * five bands on two rows and the block over the island's south coast
   * (measured 2026-08-30 at 390px); the row beside it holds a 32px zoom
   * stack and a 12px gap, so the legend's ceiling is derived from those, and
   * the five bands stay on one row that scrolls if a ramp ever grows. */
  .legend { max-width: calc(100% - 44px); }
  /* the ramp still WRAPS here: a nowrap row was tried the same hour and each
   * band, holding a psm span and its psf twin, folded to three lines inside
   * its own box — five three-line bands in one row, taller than the two rows
   * they replaced (render 2026-08-30 at 390×844) */
  .lband { font-size: calc(12 * var(--tpx)); }
  /* THE FOLD MOVED UP TO THE BASE RULE (concept pass 2026-09-09), and the
   * three rules that used to live here — `.lk{display:inline-block}`,
   * `.legend:not(.open) .ramp`, `.legend:not(.open) .lb` — went with it.
   *
   * What they were for is unchanged and still true, so it is worth keeping
   * on the record rather than losing with them. From 2026-08-30, re-derived
   * 2026-09-03: the open rent ramp is 243px tall at its 400px cap, and in
   * every one-column layout a door's pane is 38vh less the 68px bar — 321px
   * at 1024 tall, 253 at 844, with a 310px floor after the door rule below —
   * so an unfolded key covers three quarters of the pane at any height a
   * phone or tablet has. And from 2026-08-31: the caption FOLDS with the key
   * rather than being deleted by it, or a reader who opens the key gets five
   * bands and no line saying what the tick is.
   *
   * Both hold on a desktop too, which is the finding that moved them. The
   * band values and the caption are now inside .lmore, folded at every size,
   * and the qualifier is on that one wrapper instead of on each child. What
   * stays here is only what is genuinely narrow: the type size, the width
   * ceiling, and the open key leaving the flow. */
  .legend:not(.open) .lh { margin-bottom: 0; }
  /* the no-colour keys fold here too, and only here. On a wide pane they are
     three short items on one line and worth their room; at 390 they wrap to
     two rows of a legend that already reserves 29% of the pane's height from
     the island's own fit. Closed, the phone keeps what a scale is for — the
     ramp and its two ends — and the counts are one tap away with the cuts. */
  .legend:not(.open) .lkeys { display: none; }
  /* THE OPEN KEY LEAVES THE FLOW (evidence pass 2026-09-02, Track A). Open,
   * the key measured 252.6px inside a door's 252.5px map pane at 390 and
   * grew UPWARD from the bottom band, under the chrome bar: five of seven
   * bands, the title and the control that closes it were off the pane. Open
   * it pins to the pane's corner, takes the pane's height as its ceiling,
   * scrolls inside itself, and stacks above the chrome (z 20) so nothing it
   * says is under the chips. Closed, nothing here applies. B71 holds it. */
  .legend.open {
    position: absolute; left: var(--s3); right: calc(var(--s3) + 2 * var(--zbtn) + 6px + 12px); bottom: var(--s3);
    max-width: none; max-height: calc(100% - 2 * var(--s3)); overflow-y: auto; z-index: 30;
  }
  .legend.open + .zoombox { margin-left: auto; }
  /* THE CREDITS WRAP (Track A [30] [65], Track C [48] [52]; measured). The
   * 2026-08-31 answer was one nowrap line scrolling sideways under a fade,
   * and it put the ODbL credit at x=584 on a 390px screen, in an 11px strip
   * a thumb had to flick, and at 844x390 crushed it to 0px of content — the
   * one line app.js calls a condition of using the data was on no phone
   * screen at rest. The line is 843px folded (the source roll hides below);
   * at 9px on a 366px bar it wraps to three rows, about 40px, over a door
   * pane of 253 at 844 tall — a sixth, at the pane's foot where the zoom
   * stack already is, and every credit and both licence links on screen.
   * (The door's pane is 310 since 2026-09-03 and the zoom stack a row
   * beside the key; the credits are the foot's larger part, still.) */
  .attrib { font-size: calc(12 * var(--tpx)); }
  /* the source roll folds too — it is on every card — and the two licence
   * credits stay (app.js boot): three lines of credits became one */
  .attrib .src { display: none; }
  /* the three-captions-one-per-line rule moved to the BASE .ends block
   * (2026-08-31): the jumble it fixed at 390 reproduced at 1280 the moment
   * a door narrowed the rail — it was a law of the row, not of the phone */
  .zsec { padding: var(--s3) var(--s4); }
  .ft-grid { grid-template-columns: 1fr; }
  /* the drawer's only dismiss a thumb can reach measured 30x30 — under the
   * 44px touch floor (e2e swarm 2026-09-01). The backdrop tap and Escape
   * both close it too, but at 94vw the backdrop is a 12px sliver a side and
   * a phone has no Escape, so the one real control grows to the floor. */
  .xbtn { width: 44px; height: 44px; font-size: calc(19 * var(--tpx)); }
}

/* ==== A PHONE ON ITS SIDE (evidence pass 2026-09-02, Track A) ============
   The phone block above is keyed to WIDTH, but everything it changes is
   governed by HEIGHT: at 844×390 the rail's 200px floor beat its 40vh and
   the 87px chrome bar covered all but 19px of a 122px island; on a door the
   rail took 62vh and the 80px map sat wholly under the bar. A wide, short
   viewport is a desktop shape, so it gets the two columns back — map left,
   rail right — with the trail on the bar's one line. This block sits AFTER
   the phone block on purpose: same specificity, later wins (the .door
   lesson, 2026-08-31). The height bound is the phone block's own island
   floor (200px rail + the bar + the chrome) rounded to the breakpoint every
   phone on its side sits under. B70 holds the order and the rows. */
@media (max-width: 900px) and (max-height: 500px) {
  .main { display: grid; grid-template-columns: 1fr minmax(260px, 46%); grid-template-rows: 1fr; }
  #app.zone-open .main { grid-template-columns: 1fr minmax(300px, 52%); grid-template-rows: 1fr; }
  /* the sheet is a portrait thing: on its side the rail is a column again,
   * static, unrounded, and the grip goes (app.js PHONE() is false here) */
  .mapstage { position: relative; inset: auto; }
  .map-ui { bottom: 0; }
  .rail { position: static; transform: none; border-radius: 0; box-shadow: none; z-index: auto; border-top: 0; border-left: 1px solid var(--rule); }
  .grip { display: none; }
  /* the phone block's grid bar goes back to one flex line here; the box
   * keeps the 96px floor the flex bar had (2026-09-03) since the grid's
   * "every pixel to the edge" is a grid rule and means nothing under flex */
  .topbar { display: flex; flex-wrap: nowrap; }
  .crumbs { order: 0; flex: 1 1 auto; }
  .addr.top { flex: 1 1 96px; min-width: 96px; }
}

/* ==== THE SHADE-BY ROW FOLDS ITS LABELS BY ITS OWN MEASURE ===============
   (Track A, [55]; 2026-08-31 derivation re-based 2026-09-03.) The fold used
   to ride the 900px layout block, so at 768 and 844 both group labels
   vanished into 400px of empty bar and the 119px label floor that aligns
   the two rows' chips (the base .mlabel rule) broke on one row only. The
   row's own width, measured at 1280 with nothing folded: the shade-by group
   339px (label 119, three chips), the overlay group 172px (fence, label,
   two toggles), the 10px gap between them and the bar's 28px end clearance
   — 549px of bar, 573px of viewport with the chrome's 12px a side. It folds
   below that and nowhere above it. Folded, each GROUP keeps its name
   (app.html gives both role=group aria-labels), the fence keeps its rule
   but gives up its indent, and every chip gives up 3px of padding a side;
   re-measured at 390x844 the five chips and the fence fit the 366px bar to
   the pixel. The catbar keeps its label at every width: "What do you sell?"
   is the question the whole bar exists to ask, not a caption on it — so
   below the fold its chips start 119px in and the shade chips at the edge;
   that stagger is the price of keeping the question, stated here. */
@media (max-width: 580px) {
  .metricbar .mlabel { display: none; }
  .tgrp { padding-left: 8px; margin-left: 0; }
  .mchip { padding-left: 8px; padding-right: 8px; }
}

/* ==== A THUMB, NOT A POINTER (Track A [52] [53] [64], Track B [55] [56]
   [60], Track C [105]) ===================================================
   The phone block above raised ONE control to the 44px touch floor and
   left the rest at 18 to 32 — the key toggle at 60x18, the receipt (i) at
   26, the unit select the quote box will not answer without at 28, the
   zoom stack at 32. Keyed to the DEVICE, not the width: a tablet in
   landscape is wider than 900 and still a thumb. Two ways to the floor:
   a standalone control grows, and an inline one — the (i) in a line of
   text, the key toggle in the ramp's title row — keeps its size and gains
   an invisible pseudo hit box, centred by inset arithmetic so the base
   22px and the phone's 26px both land on 44. The pseudo is ::before,
   because print already owns .info::after for the receipt note. iOS zooms
   the layout on focusing any text field under 16px, and never zooms back:
   every typed field is 16px here. */
@media (pointer: coarse) {
  :root { --zbtn: 44px; }
  .zbtn { font-size: calc(20 * var(--tpx)); }
  #ask-unit, .qsel { min-height: 44px; }
  .ft-in, #ask-unit, .qsel, .addr input { font-size: calc(18 * var(--tpx)); }
  .ft-in::placeholder { font-size: calc(15 * var(--tpx)); }
  /* at 16px the quote row's field and select have a min-content of ~400px
   * between them, and a flex row's min-content is what a 1fr grid track
   * takes as its floor: the whole column went to 432px in a 390px viewport
   * (measured 2026-09-03, the door). The select may shrink like the field
   * beside it (min-width 0), and the rail, a grid item whose body scrolls,
   * never widens its own track (min-width 0 on the item, the grid rule). */
  .money #ask-unit { min-width: 0; }
  .rail { min-width: 0; }
  .info { position: relative; }
  .info::before { content: ''; position: absolute; inset: calc((100% - 44px) / 2); }
  .lk { position: relative; }
  .lk::before { content: ''; position: absolute; inset: calc((100% - 44px) / 2) 0; }
  /* the trail's rungs and the chips grow in height only, where the width
   * is what the bar has to share: 32px each, the chrome 12px taller */
  .crumb { padding: 9px 6px; }
  .tbtn { padding: 8px 10px; }
  .mchip { padding-top: 7px; padding-bottom: 7px; }
  .barmore { width: 44px; }

  /* ---- AND THEN IT STOPPED, TWELVE CONTROLS SHORT (a11y pass 2026-09-12)
     The block above raised four things to the floor and left the rest at the
     values it had just finished calling too small. Measured at 390x844 with
     the pointer actually coarse: THIRTEEN of the island's thirty-two controls
     were still under 44px, fifteen of a corner's forty-eight. The chips at
     33.8, the trade button at 30.6, the address box at 38.8, the trail's
     rungs and the charter door at 35.8, the folds at 23.4, the wordmark at
     21.7, and the legend's key button 32.3 WIDE — its pseudo pad above pads
     the height and states `0` for the sides, so the one control this block
     already fixed was fixed in one axis.

     (The rig is worth writing down too, because it nearly published a wrong
     number: (pointer: coarse) does not follow CDP's mobile:true or its touch
     event emulation. It follows setTouchEmulationEnabled, and a census run
     without that knob reports a MOUSE's boxes at a phone's width — .zbtn at
     32x32, the thumb block entirely off, every figure above overstated.)

     Two mechanisms, as above, and WHICH ONE A CONTROL GETS IS DECIDED BY
     WHETHER ITS CONTAINER CLIPS, which is a thing to measure and not to
     assume. A pad is free where the container has slack and paints nothing
     over it: the wordmark in a 52px header, the charter door on the grip.
     It is worth NOTHING inside a scroller. `overflow-x: auto` cannot leave
     the other axis visible — CSS computes it to auto too — so .metricbar and
     .crumbs, which scroll sideways on a narrow phone, cut a vertical pad off
     at their own edge. The first cut of this block padded the chips and the
     rungs and measured them afterwards at 35 and 36: the rule had landed and
     was not applied, which is the trap this repo keeps a law about.

     So the bar's controls GROW, and the 24.6px they take is the honest price
     of the floor. The bar is the one furniture app.js measures live (insets,
     furnitureCost), so this is not 24.6px of decoration — the sheet's half
     rest reads it and gives way, keeping the island's 120px of glass and
     showing that much less card at the middle rest. Two 44px rows is also
     what this bar's own design note asks for ("one row is 44"); at 82.4 for
     two rows it was under its own spec before any of this.

     THE TWO LICENCE LINKS ARE EXEMPT, and the exemption is the reason they
     cannot be padded: they are inline links inside a three-line credit
     sentence at 9px, 134.4x12 each. A 44px pad on the link in the first line
     would cover the second and the third. This is the case both target-size
     criteria carve out by name — a target in a sentence — and the sentence
     is there because the ODbL and the Open Data Licence require it.

     433 in test-landing.js holds all of it: the set, the mechanism each
     control gets, that every number below is 44 (the floor is written in the
     test, never read from here), that the measurements above still sit
     beside the rules they bought, that no grant outlives its control, that
     nothing inside a scroller is handed a pad, and that the exemption stays
     one exemption. It cannot see a rendered box — no test in this repo opens
     a browser — and it says so in its own first paragraph. */
  .wordmark, .tbtn { position: relative; }
  .wordmark::before, .tbtn::before {
    content: ''; position: absolute; inset: calc((100% - 44px) / 2) 0;
  }
  .mchip, .catbtn {
    min-height: 44px; min-width: 44px;             /* 33.8 and 30.6 tall; the Rent chip 41.8 wide */
    display: inline-flex; align-items: center; justify-content: center;
  }
  .catbtn { justify-content: flex-start; }         /* it is a question, not a chip: it reads from the left */
  .crumb { padding: 14px 6px; }                    /* 35.8, inside a trail that scrolls sideways */
  .lk { min-width: 44px; }                         /* 32.3, and the pad above is vertical only */
  .addr input { min-height: 44px; }                /* 38.8 in a header with 4px to give */
  .z-hero .how summary, .ft-details summary { padding-top: 13px; padding-bottom: 13px; }
  .ctx-fold summary { padding-top: 11px; padding-bottom: 11px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  html { scroll-behavior: auto; }
}

/* ==== PAPER (evidence pass 2026-09-02, Track D/E) =========================
   This sheet had no print rule at all, so A4 fell through the phone branch:
   #app kept height 100vh and overflow hidden, the rail kept its own scroll,
   and a door printed ONE page and lost 57% of its card (measured in Chrome's
   real Page.printToPDF); the licence line was cut mid-token and the agency
   roll folded away as it does on a phone. On paper the map is not a map —
   it cannot be panned, and its key describes paint that is not there — so
   the map, its chrome and its key go, and the card is the page: it flows,
   paginates, keeps every section whole where it can, prints every (i)'s
   note after its line and every link's address after its text, and the
   credits wrap in full. The palette is the same tokens re-declared for
   white paper — the mark and the plate both go white, every ink darkens to
   hold its contrast on white — so nothing below spells a colour of its own,
   and the one caution mark keeps a keyline so it is still a mark. B69
   holds it. */
@media print {
  :root {
    --sign: #FFFFFF; --paper: #FFFFFF; --track-bg: #EEEEEE;
    --rule: #999999;
    --ink: #111111; --ink-2: #222222; --ink-3: #444444;
    --plate: #FFFFFF; --hi: #111111;
    --p-ink: #111111; --p-ink-2: #333333; --p-ink-3: #555555; --p-rule: #999999;
  }
  html, body { background: #FFFFFF; color: #111111; }
  #app { display: block; height: auto; overflow: visible; }
  .main { display: block; }
  .mapstage, .map-ui, .mapchrome, #maphover, .zoombox, .legend, .sheet, .xbtn, .tbtn, .addr,
  #cell-notpub, #to-report, .z-quote, .z-tool, .barmore { display: none; }
  .rail { border: 0; display: block; }
  .rail-body { overflow: visible; }
  .zsec { break-inside: avoid; page-break-inside: avoid; }
  .badge.ask, .btn, .mchip.on { border-color: #111111; }
  /* THE TWO SCREEN MEASURES COME OFF ON PAPER (2026-09-16). A reading column
     is a screen's answer to a window that can be any width; a sheet of A4 is
     a sheet of A4, which is the same sentence the 9px below is justified by.
     The credits are asked to wrap IN FULL here, and a 60ch cap would set them
     in a narrow ribbon down one side of the page with white beside it. */
  .attrib, :where(.zsec) :where(p, dd, .fc-why, .fc-src, .aline .at) { max-width: none; }
  .attrib { position: static; white-space: normal; overflow: visible; mask-image: none; -webkit-mask-image: none; padding-right: 0; font-size: 9px; }
  .attrib .src { display: inline; }
  .info { font-size: 0; border: 0; background: none; padding: 0; margin: 0; }
  .info::after { content: ' ' attr(data-note); font-size: 9px; color: var(--ink-3); }
  a[href^="http"]::after { content: ' (' attr(href) ')'; font-size: 9px; }
}
