@font-face {
  font-family: "Bitter";
  src: url("/fonts/bitter-700.woff2") format("woff2");  /* busted with app.css itself */
  font-weight: 700; font-style: normal; font-display: swap;
  /* latin subset, 18.6KB. SIL Open Font License 1.1 — self-hosting permitted. */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

/* THE BEACH OFF 111 — visual system v1.0
   Built the way the room is built: warm material on a matte-black ground, everything held
   by an EDGE rather than a shadow. Wood never appears as a surface, only as a 1px line —
   the real desktops span a four-stop luminance range that is unreadable behind text.
   Zero gradients. Zero shadows. One radius (0). One accent at two values.
   Spec + provenance: hometown-blueprint/deal/design-spec-2026-07-25.md */

:root {
  /* SURFACES — sampled from the room. Walls are warm greige, not white, so paper matches. */
  --paper:        #F7F4ED;
  --sand:         #E9E3D6;  /* the ONLY panel fill. A tint, not a box. No white cards exist */
  --field:        #FDFCF9;
  --field-focus:  #F2E9D4;  /* warm tint = state, never decoration */
  --black:        #0E0F12;  /* the matte ceiling. Flat, never lifted — the real one is harsher */
  --lake:         #0B3C45;

  --ink:          #23262B;
  --ink-strong:   #0E0F12;
  --ink-muted:    #635C52;  /* darkened from the spec's #6E665C, which failed AA on --sand */
  --bone:         #D8D2C6;  /* body copy on black */
  --bone-dim:     #8B8579;

  /* One accent at two values. #b4541f retired: unattributed, and failed on warm paper. */
  --amber:        #F4A841;  /* verbatim from the badge. DARK GROUNDS AND FILLS ONLY */
  --amber-hover:  #E0952F;
  --ember:        #9B4C22;  /* same brand orange, dark value, from the wordmark's own ramp */
  --ember-hover:  #7E3C1B;
  --oak:          #B98A55;  /* wood, from the slat wall. A LINE, never a field */
  --oak-dark:     #8A6538;
  --led:          #FBFAF7;  /* LED tube core. Neutral ~4000K. Hairlines on black only */
  --black-lift:   #191B20;  /* ONE step off the ceiling. It existed for exactly one job — the
                               alternating board row on dark — and AS OF 2026-08-22 THAT JOB
                               IS GONE: the row tint was removed (see "THE ROW TINT IS GONE"
                               further down). This token and --surface-cut-alt, which it
                               feeds, currently have NO CONSUMER.
                               Both are kept rather than deleted because the removal note
                               contemplates a tint returning behind a min-width:1024px guard,
                               and this value is the researched one: every other near-black in
                               the system was already spoken for — --surface (#1E2126) is the
                               floor and drops --bone-dim to 4.40:1, under AA, which is what
                               ruled it out. This holds 4.70:1.
                               Do not reach for it for anything else; a second use means the
                               system needs a real surface token, not a shade. */

  --rule:         #DCD3C1;
  --rule-strong:  #6E665C;
  --line-input:   #8A6538;
  --rule-dark:    #33302B;

  --error-ink:#8A2018; --error-fill:#F6E4DF; --error-line:#D9A99C;
  --warn-ink:#6B4A12;  --warn-fill:#F6EBD6;  --warn-line:#D9C08A;
  /* THE THIRD STATE. There were two — warn and error — and both are for things that went
     wrong, so a completed action had no colour of its own and would have had to borrow one.
     Green rather than the brand amber: amber fails AA on a light ground (that is why --amber
     is dark-grounds-and-fills only), and 'it worked' is exactly the message you cannot afford
     to render in the one hue this palette cannot make legible here. */
  --ok-ink:#1F5A34;    --ok-fill:#E4F0E7;    --ok-line:#A8C9B3;
  --focus:        #9B4C22;  /* overridden to --led on every dark ground */

  --radius: 0;
  --radius-part: 2px;       /* only inputs and the progress bar — fabricated parts */

  /* ELEVATION: zero. A shadow is a claim about light; this room's light is a hard edge. */

  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px;
  --s6:32px; --s7:48px; --s8:80px; --s9:128px; --s10:176px;

  /* --w-page WAS 1120px UNTIL 2026-08-22. Mark: "It seems really left-heavy for some reason."
     He was right and it was measurable: at 1120 every prose block sat at --w-text (640px)
     left-aligned, leaving 456px of dead ground on its right — 41% of the container — while
     the product rows and the photo strip ran the full 1072px. The page alternated between
     full-width pictures and half-width text hugging the left edge.
     THE MEASURE IS NOT THE PROBLEM: 640px is about seventy characters, which is right. The
     container was simply wider than the content justified, so it came down to meet the text
     instead of the text stretching to meet it. The void is now 272px.
     WHY NOT CENTRE A NARROWER BAND FOR PROSE, which was the first idea: every element on the
     page shares one left edge — header logo, hero, board intro, product rows, photo strip,
     charter prose, footer logo, all at the same x. Centring prose in a narrower band would
     have knocked it off that spine while the header and footer stayed on it. Measured before
     building it, which is the only reason it was not built. */
  --w-page:960px; --w-text:640px; --w-form:480px; --w-auth:340px; --w-doc:720px;
  --dur:140ms; --ease:cubic-bezier(.2,0,.2,1);

  /* THE CEILING USED TO LAND AT A 875px VIEWPORT. clamp(2.5rem, 1.75rem + 3.2vw, 3.5rem)
     reaches its 3.5rem max when 3.2vw = 1.75rem — that is 875px wide — so a 16" laptop
     rendered the h1 at exactly the size a small laptop did, and the extra 850px of screen
     became empty cream beside a 569px headline. Ceiling raised and the slope eased so it
     keeps growing to roughly 1500px, which is where real desktops actually sit. Floor is
     unchanged: the 390px phone layout must not move. */
  --t-display: clamp(2.5rem, 1.55rem + 4.1vw, 4.5rem);
  --t-h2:      clamp(1.75rem, 1.4rem + 1.5vw, 2.25rem);
  --t-price:   clamp(2.25rem, 1.9rem + 1.5vw, 2.75rem);
  /* A PRODUCT NAME IS NOT A PARAGRAPH LEAD. --t-lede topped out at 1.5rem, which set
     "Walled Desk" at the same weight as the sentence under it — four rows with nothing for a
     skimming eye to catch except the amber. This sits between the lede and the h2. */
  --t-tier:    clamp(1.75rem, 1.3rem + 1.9vw, 2.5rem);
  --t-lede:clamp(1.1875rem, 1.06rem + .55vw, 1.5rem); --t-body:clamp(1.0625rem, 1.0rem + .28vw, 1.1875rem); --t-small:.9375rem; --t-label:.8125rem;

  /* Bitter 700, self-hosted latin subset. Georgia is the metric-adjacent fallback. */
  --font-display: "Bitter", Georgia, "Times New Roman", serif;
  --font-text: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;

  /* ── SEMANTIC LAYER ────────────────────────────────────────────────────────────
     Everything above is the PALETTE: literal names for literal colours, sampled from
     the room. Everything below is ROLES. Components consume roles only, never the
     palette, so a theme is a redefinition of this block and nothing else.

     The room has two grounds — the matte-black ceiling and the warm-greige wall — and
     the design cuts between them. That cut is the composition, so it survives in every
     theme: SURFACE is the page, SURFACE-CUT is the black interruption. Only the surface
     side ever flips. Everything already styled for the cut is theme-invariant.

     Splitting --paper was the point of this refactor: it was doing two unrelated jobs,
     page ground and bright-text-on-black, which are the same colour by coincidence in
     light and must diverge in dark. */

  --surface:            var(--paper);
  --surface-panel:      var(--sand);        /* the ONLY panel fill */
  --surface-field:      var(--field);
  --surface-field-focus:var(--field-focus);
  --surface-cut:        var(--black);       /* the hard cut. Dark in EVERY theme. */

  --text:               var(--ink);
  --text-strong:        var(--ink-strong);
  --text-muted:         var(--ink-muted);

  /* on the cut. Theme-invariant, because the cut is. */
  --text-cut:           var(--bone);
  --text-cut-strong:    var(--paper);
  --text-cut-muted:     var(--bone-dim);

  /* Two accents exist only because amber fails AA on warm paper, so the surface needs a
     darker one. On a dark surface that constraint lifts and both collapse to amber. */
  --accent:             var(--ember);
  --accent-hover:       var(--ember-hover);
  --on-accent:          var(--paper);       /* text ON an accent fill */
  --accent-cut:         var(--amber);
  --accent-cut-hover:   var(--amber-hover);
  --on-accent-cut:      var(--black);

  --rule-surface:       var(--rule);
  --rule-surface-strong:var(--rule-strong);
  --rule-cut:           var(--rule-dark);
  /* --rule-cut is 1.46:1 on the cut — correct for a hairline that divides, useless for a
     boundary that has to be SEEN. This is the visible edge on black: 6.23:1 in both themes,
     comfortably over the 3:1 WCAG 1.4.11 non-text bar. It adds no new colour to the system,
     and it gives .spot's oak divider a role token to point at instead of the palette. */
  --rule-cut-strong:    var(--oak);
  /* THE SCREENING GROUND, for the tour band and nothing else. Mark: "it might be grayscale-ish
     to offset it from the cream sections, since it's a video presentation." It is #1E2126 —
     already in the palette as the dark theme's --surface, described there as "neutral charcoal
     — the floor, not the ceiling" — so this names an existing value rather than inventing one.
     Lighter than --black, which is what makes it read as a screen surround against cream
     rather than as a hole in the page. Overridden below on dark, where the page IS this colour
     and the band has to go the other way. */
  --screen:             #1E2126;
  /* The board's frame — the line that used to be var(--led) hard-coded. It needed a role of
     its own the moment the cut stopped being dark in every theme: near-white is the ceiling
     tube on black and an invisible line on paper. */
  --edge-cut:           var(--led);
}

/* ── DARK THEME ────────────────────────────────────────────────────────────────────
   Follows the system setting only; there is no toggle, because the OS already knows the
   answer and a toggle costs a control, persisted state and a flash of the wrong theme.

   This is arguably the truer rendering of the room. The Beach is a matte-black ceiling
   with a hex LED array over grey floor and honey oak — the paper theme is the abstraction,
   not this one.

   Only the SURFACE side flips. The cut, and everything on it, is already dark and carries
   over untouched — which is the whole return on the role layer.

   The composition problem: in light the cut is a 17:1 slam, paper to black. Dark charcoal
   against black is 1.16:1 and would erase the site's main structural device. So in this
   theme the cut is held by an EDGE instead of by luminance — which is the design's stated
   principle anyway ("everything held by an edge rather than a shadow"), and it's what the
   real LED tubes do against the real ceiling. */
/* ── THE READER'S CHOICE ───────────────────────────────────────────────────────
   Dark follows the system by default; a reader can override it and we remember. Three
   selectors, and each one earns its place:
     :root:not([data-theme="light"]) inside the media query — system dark applies UNLESS
       someone has explicitly asked for light. Without the :not(), the system would win over
       the reader, which is backwards.
     :root[data-theme="dark"] outside it — an explicit dark choice on a system set to light.
   The two token lists are duplicated because plain CSS has no way to share a declaration
   block across a media query boundary. bin/smoke.js asserts they stay identical, so the
   duplication cannot rot into a half-themed page. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface:            #1E2126;   /* neutral charcoal — the floor, not the ceiling */
    --surface-panel:      #262A30;
    --surface-field:      #14161A;   /* inputs sink, they don't lift */
    --surface-field-focus:#2A2318;   /* warm tint = state, same rule as light */
    /* --surface-cut stays --black. The ceiling is the ceiling. */

    --text:               var(--bone);
    --text-strong:        var(--led);
    --text-muted:         #9A9488;   /* lifted off --bone-dim, which fails AA on the panel */

    /* Amber exists at all because it's the badge's own colour; it was confined to dark
       grounds because it fails AA on warm paper. Here the ground IS dark, so the surface
       and cut accents collapse into one and --ember has no job. */
    --accent:             var(--amber);
    --accent-hover:       var(--amber-hover);
    --on-accent:          var(--black);

    --rule-surface:       #313640;
    --rule-surface-strong:#7D7466;
    --line-input:         var(--oak);
    --focus:              var(--led);

    --warn-ink:#F0D9A6; --warn-fill:#2C2413; --warn-line:#6B5827;
    --error-ink:#F5B8AE; --error-fill:#2E1614; --error-line:#7A2E26;
    --ok-ink:#A8DCBB; --ok-fill:#132318; --ok-line:#2E5C3E;
  }

  /* The cut is now a 1.16:1 luminance step, so it needs a line to exist. One hairline of
     the real LED white, top and bottom — the same edge the ceiling tubes draw. */
  .band-black { border-block:1px solid var(--rule-cut); }
  .site-foot { border-top:1px solid var(--rule-cut); }

  /* The board's own frame would double up with the band edge directly above it. */
  .board { border-top-width:1px; border-bottom-width:1px; }
}

:root[data-theme="dark"] {
    --surface:            #1E2126;   /* neutral charcoal — the floor, not the ceiling */
    --surface-panel:      #262A30;
    --surface-field:      #14161A;   /* inputs sink, they don't lift */
    --surface-field-focus:#2A2318;   /* warm tint = state, same rule as light */
    /* --surface-cut stays --black. The ceiling is the ceiling. */

    --text:               var(--bone);
    --text-strong:        var(--led);
    --text-muted:         #9A9488;   /* lifted off --bone-dim, which fails AA on the panel */

    /* Amber exists at all because it's the badge's own colour; it was confined to dark
       grounds because it fails AA on warm paper. Here the ground IS dark, so the surface
       and cut accents collapse into one and --ember has no job. */
    --accent:             var(--amber);
    --accent-hover:       var(--amber-hover);
    --on-accent:          var(--black);

    --rule-surface:       #313640;
    --rule-surface-strong:#7D7466;
    --line-input:         var(--oak);
    --focus:              var(--led);

    --warn-ink:#F0D9A6; --warn-fill:#2C2413; --warn-line:#6B5827;
    --error-ink:#F5B8AE; --error-fill:#2E1614; --error-line:#7A2E26;
    --ok-ink:#A8DCBB; --ok-fill:#132318; --ok-line:#2E5C3E;
  }



*, *::before, *::after { box-sizing: border-box; min-width: 0; }
/* Tells the UA to render its own furniture — scrollbars, autofill, date pickers, the
   checkbox — in the matching theme. Without it those stay light and give the game away. */
html { -webkit-text-size-adjust: 100%; color-scheme: light dark; }
body { margin:0; background:var(--surface); color:var(--text);
       font:400 var(--t-body)/1.6 var(--font-text); -webkit-font-smoothing:antialiased; }
@media (min-width:640px){ body{ font-size:1.125rem; } }
input, button, select, textarea { font: inherit; color: inherit; }
img, svg { max-width:100%; height:auto; display:block; }

h1,h2,h3 { margin:0 0 .5em; font-family:var(--font-display); font-weight:700;
           color:var(--text-strong); letter-spacing:-0.015em; }
h1 { font-size:var(--t-display); line-height:1.08; }
h2 { font-size:var(--t-h2); line-height:1.15; }
h3 { font-size:var(--t-lede); line-height:1.3; font-weight:600; font-family:var(--font-text); }
p { margin:0 0 1em; max-width:68ch; }
a { color:var(--accent); text-underline-offset:2px; }
a:hover { color:var(--accent-hover); }

/* The previous build had NO focus styles at all — the largest a11y gap in it. */
:focus-visible { outline:3px solid var(--focus); outline-offset:2px; }
:focus:not(:focus-visible) { outline:none; }
.band-black, .site-foot, .dock, .dock-sheet { --focus: var(--led); }

.skip { position:absolute; left:-9999px; }
.skip:focus { left:var(--s4); top:var(--s4); z-index:10; background:var(--surface-cut);
              color:var(--led); padding:var(--s3) var(--s4); }
/* ── SMOOTH SCROLLING, IN CSS, NOT JAVASCRIPT ─────────────────────────────────
   The site has one script and it earns its place by fixing something CSS cannot.
   This is not that: one declaration does the whole job, in every browser this
   site supports, with no listener, no library and nothing to keep in sync.

   It exists for one link in particular. The re-cut notice on /plan says "here is
   what changed" and points at #revisions, which is 400-odd lines further down.
   Teleporting there tells a reader nothing about where they went; gliding tells
   them the log is part of the same page and roughly how far down it lives. Mike
   and his partner are the readers who most need that, because they are the two
   least likely to already know the shape of this document.

   THE SKIP LINK IS DELIBERATELY LEFT ALONE at #main. A skip link exists to move a
   keyboard or screen-reader user instantly past the chrome, and animating it is
   the opposite of the favour it does them — but scroll-behavior does not animate
   focus, only the scroll, and the reduced-motion opt-out below covers the users
   most affected. Revisit if this ever grows a sticky header, which is when the
   animated skip becomes genuinely disorienting rather than merely slower. */
html { scroll-behavior: smooth; }

/* Targets land with a little air rather than jammed against the viewport edge.
   :target rather than [id], so this only applies to the element actually being
   scrolled to and cannot quietly change layout anywhere else. */
:target { scroll-margin-top: var(--s6); }

/* The existing rule killed animation and transition but NOT scroll-behavior, which
   is a separate property and would have kept animating for the people who asked
   the loudest not to be moved. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{ animation:none!important; transition:none!important; }
  html{ scroll-behavior:auto!important; }
}

/* bands — layout comes from .band, never from an element selector */
.band { padding-block:var(--s8); }
.band--tight { padding-block:var(--s7); }
/* The nav mark is deliberately large, so the first band doesn't also need a full 80px above
   it. On a phone that reclaimed space is the difference between the CTA landing above the
   fold or below it — the mark got bigger without costing the conversion.
   The second selector is the same rule for the same reason: when the preview notice renders it
   takes the first slot, and the hero behind it would otherwise snap back to the full --s9/--s10
   top padding and push the h1 off a phone screen — under a notice whose whole value is that a
   reader sees it AND the thing it is about in one screen. */
main > .band:first-child,
main > .preview-band + .band { padding-block-start:var(--s7); }
@media (max-width:640px) {
  main > .band:first-child,
  main > .preview-band + .band { padding-block-start:var(--s5); }
}
@media (min-width:640px){ .band{padding-block:var(--s9)} .band--tight{padding-block:var(--s8)} }
@media (min-width:1024px){ .band{padding-block:var(--s10)} .band--tight{padding-block:var(--s9)} }
.band-inner { max-width:var(--w-page); margin-inline:auto; padding-inline:var(--s5); }
.band-text { max-width:var(--w-text); }
.band-black { background:var(--surface-cut); color:var(--text-cut); }

/* THE CUT IS NO LONGER THEME-INVARIANT, 2026-08-21, and the tokens above still say it is —
   read this before believing them.

   The comment on --surface-cut says "the hard cut. Dark in EVERY theme," and the reasoning was
   good: The Beach IS a matte-black ceiling, and a black band through a paper page is the room
   quoted in the layout. What that reasoning missed is that the band is not a stripe on this
   page. It is the WHOLE selling surface — every price, every photograph, every button — so a
   reader who picks the light theme picks it and then meets a black screen anyway.
   Mark: "even in light mode the meat of the site still has a black bg."

   So the cut follows the theme, and it is scoped HERE rather than at :root because
   .band-black has exactly one user (views/index.ejs, #memberships). The footer and the admin bar
   keep their own grounds; nothing else in the system moves.

   ON LIGHT, TWO SUBSTITUTIONS ARE FORCED, not stylistic: --amber fails AA on warm paper (it
   is why --ember exists at all), and --led is invisible on it. Both have surface-side
   counterparts already in the palette, so this adds no new colour. */
.band-black {
  /* --sand, NOT --paper. The first pass at this made the band the same paper as the page
     above it, which removed the black but also removed the band: one flat cream field from
     the headline to the footer, with the selling surface no longer a place. Mark: "now the
     light theme is all white all the time."
     --sand is the palette's one panel fill, described there as a tint and not a box, and it
     is what this is — a 1.16:1 step that says "different ground" without drawing a card. The
     board's own ink frame does the rest of the work, which is why no band border is added
     here the way the dark theme adds one: the two lines would sit a few pixels apart and
     double up, exactly what .board's thinner border in the dark block exists to prevent.
     Measured on sand: text 11.9:1, strong 15.0:1, muted 5.2:1, ember price 4.8:1, the
     divider 4.4:1 against a 3:1 bar. --ink-muted only clears it because it was darkened off
     the spec for this exact ground -- see the palette note. */
  --surface-cut:      var(--sand);
  --surface-cut-alt:  var(--paper);
  --text-cut:         var(--ink);
  --text-cut-strong:  var(--ink-strong);
  --text-cut-muted:   var(--ink-muted);
  --accent-cut:       var(--ember);
  --accent-cut-hover: var(--ember-hover);
  --on-accent-cut:    var(--paper);
  --rule-cut:         var(--rule);
  --rule-cut-strong:  var(--rule-strong);
  --edge-cut:         var(--ink-strong);
  --focus:            var(--ember);
}
/* Dark restores the room. Duplicated across the media query and the explicit choice for the
   same reason the :root blocks above are — a custom property cannot be shared across that
   boundary. Edit one, edit both. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .band-black {
    --surface-cut:      var(--black);
    --surface-cut-alt:  var(--black-lift);
    --text-cut:         var(--bone);
    --text-cut-strong:  var(--paper);
    --text-cut-muted:   var(--bone-dim);
    --accent-cut:       var(--amber);
    --accent-cut-hover: var(--amber-hover);
    --on-accent-cut:    var(--black);
    --rule-cut:         var(--rule-dark);
    --rule-cut-strong:  var(--oak);
    --edge-cut:         var(--led);
    --focus:            var(--led);
  }
}
:root[data-theme="dark"] .band-black {
    --surface-cut:      var(--black);
    --surface-cut-alt:  var(--black-lift);
    --text-cut:         var(--bone);
    --text-cut-strong:  var(--paper);
    --text-cut-muted:   var(--bone-dim);
    --accent-cut:       var(--amber);
    --accent-cut-hover: var(--amber-hover);
    --on-accent-cut:    var(--black);
    --rule-cut:         var(--rule-dark);
    --rule-cut-strong:  var(--oak);
    --edge-cut:         var(--led);
    --focus:            var(--led);
}
/* h3 was missing here, so any h3 on the cut inherited --text-strong — black on black in the
   light theme. .spot-name set its own colour and hid the bug; the rate strip found it. */
.band-black h2, .band-black h3 { color:var(--text-cut-strong); }
/* `.band-black p { color:var(--text-cut) }` USED TO LIVE HERE AND IS DELETED ON PURPOSE.
   At (0,1,1) it beat every single-class rule on the band regardless of source order, so five
   rules that declare a colour were silently overridden and the whole band rendered one value:
   .preview-note, .dayrate, .fineprint and .stock--out lost their muting, and .stock--low lost
   its AMBER — which killed the only commercial urgency cue on the board, since views/index.ejs
   emits it at remaining <= 2 against an inventory of 26. Measured in Chrome: all five computed
   #D8D2C6; removing this one declaration snapped all five back to their authored values with
   no other visual change. The line above already sets `color:var(--text-cut)` on .band-black
   itself and <p> has no UA colour, so every paragraph inherits it without help. This was
   invisible to both gates — check-contrast.js resolves TOKENS, never computed styles. */
.band-black a:not(.btn) { color:var(--accent-cut); }

.nav { max-width:var(--w-page); margin-inline:auto; padding:var(--s5) var(--s5) var(--s4); }
.brand { display:inline-block; }
.brand img { width:auto; height:clamp(88px,28vw,140px); }

/* 17px sentence case. A 13px label on the control that takes $897 was the spec's
   own worst call, and the red-team killed it. */
.btn { display:inline-flex; align-items:center; justify-content:center;
       border-radius:var(--radius); border:1px solid transparent;
       padding:var(--s4) var(--s6); min-height:52px;
       font:600 var(--t-body)/1 var(--font-text); letter-spacing:0;
       text-decoration:none; cursor:pointer; background:var(--accent); color:var(--on-accent);
       transition:background-color var(--dur) var(--ease); }
.btn:hover { background:var(--accent-hover); color:var(--on-accent); }
.btn-amber { background:var(--accent-cut); color:var(--on-accent-cut); }
.btn-amber:hover { background:var(--accent-cut-hover); color:var(--on-accent-cut); }
.btn[disabled] { background:transparent; border-color:var(--text-cut-muted); color:var(--text-cut-muted); cursor:not-allowed; }

/* Text-only hero. The badge does recognition duty in the nav; a second mark here was
   decoration, and on mobile it pushed the only brand asset a full screen below the fold. */
.hero-copy h1 { max-width: 16ch; }
/* 16ch is the right measure for the headline's shape — three lines, deliberate breaks. At
   1024px+ the type is larger, so the same measure occupies more of the band and the hero
   stops looking like it stopped halfway. 18ch keeps the break pattern while filling more. */
@media (min-width:1024px){ .hero-copy h1 { max-width: 18ch; } }
.hero-copy .lede { max-width: 46ch; }
.lede { font-size:var(--t-lede); line-height:1.55; max-width:46ch; }
.kicker { font-weight:600; color:var(--text-strong); }
.kicker span { font-weight:400; color:var(--text-muted); }
/* The address, above the fold. The h1 says "off 111" and the brand says "Off 111", but the
   building is on Pontoon Rd with ~zero drive-by visibility — a stranger navigating by the
   name drives up Route 111 and does not find it. */
.whereat { font-size:var(--t-small); color:var(--text-muted); max-width:46ch; }

/* ── THE PREVIEW NOTICE ────────────────────────────────────────────────────────────────────
   Renders above everything else on the home page while CHECKOUT_OPEN is unset, and says the
   one thing a reader has to leave with: none of this is for sale. views/index.ejs.

   A BAND, NOT A BOXED ALERT. Every framing device on this site is a full-width band; a
   bordered notice box on a page that has no boxes reads as chrome, and chrome is the shape
   eyes are trained to skip. Sand on paper with one strong rule beneath it is the same
   vocabulary the offer grid already uses, so it reads as part of the page rather than as
   something bolted to the top of it — which is exactly how it should be read, because it is
   the most load-bearing sentence here.

   NOT the --warn-fill .notice treatment either. That is the site's error affordance (it is
   what a failed claim form wears), and dressing a deliberate, correct state as a fault would
   invite somebody to "fix" it.

   Body-size type, not fine print, for the reason stated in lib/tiers.js about the booth's
   not-built clause: a disclaimer smaller than the thing it disclaims is not a disclaimer.
   Existing role tokens only, so it introduces no new contrast pair — --text-strong, --text and
   --text-muted on --surface-panel are all three already asserted in bin/check-contrast.js. */
.preview-band { background:var(--surface-panel); padding-block:var(--s5);
                border-bottom:1.5px solid var(--rule-surface-strong); }
.preview-mark { margin:0 0 var(--s2); font:600 var(--t-label)/1 var(--font-text);
                letter-spacing:.08em; text-transform:uppercase; color:var(--text-muted); }
.preview-say { margin:0; max-width:var(--w-text); font-size:var(--t-lede); line-height:1.5;
               color:var(--text); }
.preview-say strong { color:var(--text-strong); }

/* offer — rules, not cards */
/* THE THREE-COLUMN OFFER GRID IS GONE, 2026-08-22, and .offer-grid with it.

   It held "Lock today's rate", "You hear it first" and "Fully refundable" as three equal
   columns. Mark called it "a remnant layout" and he was right about more than the layout:
   they were never three of a kind. One was a reason to act, one was a reassurance that it is
   safe to act, and one promised an email. A grid of three gives all three the same weight,
   which is why it read as furniture.

   What replaced it is prose in reading order — definition, cohort, rate, refund — because the
   section's job is an ARGUMENT and an argument has a sequence. Three columns are read in any
   order or none. See views/index.ejs for the reasoning behind each sentence. */
.charter { margin-top:var(--s6); max-width:var(--w-text); }
.charter p { margin:0 0 var(--s4); font-size:var(--t-lede); line-height:1.5; }
.charter p:last-child { margin-bottom:0; }
/* The definition carries the section, so it is set one step up from the sentences under it —
   the same move the tier ledes make against their blurbs. */
.charter-lede { font-weight:600; color:var(--text-cut-strong); }
/* The contact line is the last thing in the board and answers a question rather than making
   the argument, so it steps down rather than up. */
.charter-ask { margin-top:var(--s5); color:var(--text-cut-muted); font-size:var(--t-small); }
/* The sentence that covers all three unknowns. Body weight, not fine print — a disclaimer
   smaller than the thing it disclaims is not a disclaimer. */
.unknowns-cover { margin:var(--s6) 0 0; max-width:var(--w-text); }

/* Framing copy between the heading and the board. Reads as prose, not as a row. */
.board-intro { margin-top:var(--s5); max-width:var(--w-text); }
.board-intro p:last-child { margin-bottom:0; }
.preview-note { color:var(--text-cut-muted); font-size:var(--t-small); }

/* THE BOARD — rows of ONE object, closed underneath by the ceiling tube.

   IT USED TO BE FRAMED top AND bottom, and the top edge was the line Mark asked to have
   removed between "What's in the room" and the Walled Desk row, 2026-08-22.

   I told him that line was the tint edge and not a border. THAT WAS WRONG — it is this rule,
   and at 1.5px in --edge-cut it was the HEAVIEST divider in the section, half again the
   weight of the 1px --rule-cut-strong rules between the rows themselves. Deleting the tint
   would not have touched it. Found by looking at a screenshot after claiming otherwise,
   which is the only reason it was found at all.

   Only the TOP edge goes. The intro now runs into the first row with nothing between them,
   which is what was asked for. The bottom edge stays and does real work: it closes the
   priced memberships before the conference strip, which is a rate and not a membership.

   The cost is that a deliberate symmetry is now asymmetric — the comment above this one used
   to read "framed by the ceiling tube", two tubes, top and bottom. If the frame is wanted
   back, the move is NOT to restore this line: it is to lift it above the <h2> so the frame
   encloses the heading and the rows together. That keeps the frame and still leaves nothing
   sitting between the intro and the first product. */
.board { border-bottom:1.5px solid var(--edge-cut); margin-top:var(--s6); }
/* Shown INSTEAD of the rows when the database and this repo disagree about what is for sale.
   Body weight on --text-cut, not fine print: it is standing where a price used to be and a
   reader has to be able to read it. Existing role tokens only, so it adds no contrast pair. */
.board-held { margin:0; padding:var(--s6) 0; color:var(--text-cut); max-width:var(--w-text); }
.spot { display:grid; gap:var(--s3); padding:var(--s5) 0; position:relative; }
.spot + .spot { border-top:1px solid var(--rule-cut-strong); }

/* THE ROW TINT IS GONE, 2026-08-22, AND THIS IS WHY IT IS NOT COMING BACK CASUALLY.

   It shipped 2026-08-21 to answer a real problem Mark named: "the monotony of all the
   similar-looking thumbnails of desks, frosted glass, and black ceilings with honeycomb
   lighting." The room is one room, and twelve photographs of it look like twelve
   photographs of it. A tint made the rows trackable, and it was the system's one sanctioned
   decorative move.

   It was removed after the llm-council pass (council-product-2026-08-22/), unanimous from
   three directions. The finding that decided it:

     `.is-tinted` was a TOP-LEVEL rule, and `.spot` does not become a three-column grid
     until min-width:1024px. Below that every row stacks — carousel, name, lede, price,
     button — so a "subtle row shade" was a full-width block of colour roughly 600px tall
     on every phone and every tablet. Zebra striping is for dense table rows; on tall
     stacked cards it reads as a false bottom and costs the reader their place.

   It was authored and reviewed on a desktop, where it was exactly what it was meant to be,
   and it was never that on most of the traffic. The lesson is the general one: a decorative
   rule with no width guard is a promise about a layout you have not looked at.

   `.spot + .spot` is now the only separator inside the board, which is also what makes the
   section intro and the first row share a ground with no line between them — the thing Mark
   asked for, arrived at by deleting a mechanism rather than flipping its parity.

   IF A TINT EVER RETURNS: put it behind min-width:1024px, and check it at 390px first.

   overflow-x:clip is KEPT. It was one of three pieces that made the full-bleed pseudo-element
   safe (it stopped inset-inline:-100vmax becoming rightward scrollable overflow), and with the
   tint gone nothing needs it today — but it costs nothing and the board is full of
   photography, so it stays as a guard on the 390px no-horizontal-overflow rule. CLIP, not
   hidden: hidden would make the band a scroll container and coerce the block axis to auto. */
.band-black { overflow-x:clip; }

/* THE OFFER CARDS MOVED INSIDE THE BOARD the same day (views/index.ejs). Two consequences
   here: the block needs its own vertical rhythm now that it is not a .band, and the card
   rules it inherits are drawn with the PAGE's rule token, which is nearly invisible against
   the cut ground. Both are one line each. */
.offer-block { padding-block:var(--s7); }
/* A .band-black .offer-grid border-colour fix lived here for about ninety minutes today and
   is deleted with the grid it patched. Worth one line of memory: the card borders MOVED
   between breakpoints — each card owned its own under 640px, the grid took over above it — so
   scoping only `> div` recoloured the phone and left the desktop rules in a blue-grey page
   token against a warm near-black. If a component ever crosses into .band-black again, check
   which element actually paints the border at each width before scoping the token. */
/* A price you cannot act on must not wear the colour that means "buy this". The row's own
   copy says it isn't for sale; the amber would argue with it. */
.spot--preview .price strong { color:var(--text-cut-muted); }
/* THE DISPLAY FACE AT A DISPLAY SIZE, 2026-08-21 — twice, the second time because the first
   was too timid. Bitter at lede size still weighed what the sentence under it weighed. Mark:
   "this typesetting is way too conservative or something -- like we're printing a newspaper
   and not trying to sell these questionably-priced products in an untested and slightly
   depressed market."

   That is the real brief, and it is not decoration: $249 is about 5% of the local median
   household income and nobody here has bought a desk like this before. A page that sets
   every sentence at one weight makes the reader do the deciding. Three sizes now — the name,
   the one sentence that says what it is, and the detail — so the row can be understood at a
   glance and still reward reading. */
.spot-name { margin:0 0 var(--s3); font:700 var(--t-tier)/1.05 var(--font-display);
             letter-spacing:-.02em; color:var(--text-cut-strong); }
/* The loud half of the blurb. Not a new sentence — see `says()` in lib/tiers.js. */
.spot-lede { margin:0 0 var(--s3); font-size:var(--t-lede); line-height:1.4;
             color:var(--text-cut-strong); max-width:32ch; }
.blurb { margin:0; font-size:var(--t-body); line-height:1.55; color:var(--text-cut);
         max-width:42ch; }
.price { margin:0; display:flex; align-items:baseline; flex-wrap:wrap; gap:var(--s2); }
.price strong { font:700 var(--t-price)/1 var(--font-display); letter-spacing:-.02em;
                color:var(--accent-cut); font-variant-numeric:tabular-nums lining-nums; }
.price span { font:400 var(--t-small)/1 var(--font-text); color:var(--text-cut); }
/* Was .anchor, holding an "$X at opening" rate. That rate is gone — nobody has committed to
   one — and the slot now carries a day figure, which is how this town prices things: $249 is
   5% of the local median household income and $12 is lunch. Same treatment either way, muted
   and small, so it never competes with the price above it. */
.dayrate { margin:var(--s1) 0 0; font-size:var(--t-small); color:var(--text-cut-muted);
           letter-spacing:.01em; }
.billing { margin:var(--s2) 0 0; font-size:var(--t-small); line-height:1.45; color:var(--text-cut); }
.billing strong { color:var(--text-cut-strong); font-weight:600; }
/* .16em tracking at label size is newspaper agate — it read as a filing reference rather
   than as scarcity. Same words, set to be read. */
.stock { margin:var(--s4) 0 var(--s3); font:600 var(--t-small)/1.3 var(--font-text);
         letter-spacing:.08em; text-transform:uppercase; color:var(--text-cut); }
.stock--low { color:var(--accent-cut); }
.stock--out { color:var(--text-cut-muted); }
.spot .btn { width:100%; }
/* THE BUTTON'S AIR USED TO BELONG TO THE COUNT (2026-09-08). The decision column below is a
   flex column with gap:0, so every gap inside it is one element's margin and nothing collapses.
   The button had no margin of its own and stood on the stock line's 12px; with the count hidden
   (SHOW_STOCK, lib/checkout-flag.js) it sat on the charge line with nothing between them. It
   carries its own margin now, and the count keeps owning the gap wherever it still prints, so
   the closed board and the preview rows are unmoved to the pixel. */
.spot-buy .btn { margin-top:var(--s5); }
.spot-buy .stock + .btn { margin-top:0; }
/* THE DECISION BLOCK. Price, what leaves the account, how many are left, and the button —
   one column, one rule down its left edge, so the row has a spine instead of three small
   things adrift in it. An EDGE and not a card: no fill, no shadow, no rounded box and no
   left-border accent stripe, all of which this system rejects everywhere else. */
.spot-buy { display:flex; flex-direction:column; justify-content:center; gap:0; }
.spot-media .shot:last-child, .spot-media .shot-strip { margin-bottom:0; }

@media (min-width:1024px){
  /* The photograph gets a FIXED track and the description gets the flexible one. It was the
     other way round, which is how a 340px picture ended up sitting in a 456px column with
     the slack spent on nothing. The decision column is bounded at both ends: wide enough
     that "$747 a quarter" sets on two lines, narrow enough that it cannot sprawl. */
  /* STRETCH, NOT START. The photograph and its strip make the row tall; the two columns
     beside it were short blocks pinned to the top of all that height, which is most of what
     "flat" was. Now the rule is a full-height spine and the text centres against the
     picture. */
  /* THE FLEXIBLE TRACK GOES TO THE PHOTOGRAPH, not to the prose. Giving it to the
     description was still wrong: three short sentences cannot fill a 700px column, so the
     slack just moved from beside the picture to underneath the words. The copy is bounded at
     a readable measure, the decision column is bounded at both ends, and everything left over
     goes to the picture — which is the thing actually doing the selling on a page about a
     room. The page is capped at 1120px, so it tops out around 450px and cannot run away.

     THE MEASURES ARE PX AND NOT ch ON PURPOSE. 38ch here read as roughly 400px, because ch
     resolves against the GRID CONTAINER's font — which is --t-body, and --t-body is fluid.
     The column meant to be the modest one came out the widest and left the photograph the
     narrowest of the three. A track measurement should not change meaning with the viewport. */
  .spot { grid-template-columns:minmax(0,1fr) minmax(0,300px) minmax(210px, 240px);
          gap:var(--s6); align-items:stretch; padding:var(--s7) 0; }
  .spot-media .shot, .spot-media .shot-strip { max-width:none; }
  .spot > * { margin:0; }
  .spot-detail { display:flex; flex-direction:column; justify-content:center; }
  .spot .btn { width:100%; }
  .spot-buy { border-left:1px solid var(--rule-cut-strong); padding-left:var(--s5); }
  /* The blurb is beside the photograph now rather than under it, so it may run wider than
     the 46ch it was set to when it lived in a 340px column. */
  .spot-detail .blurb { max-width:52ch; }
}
/* Below the breakpoint the three blocks stack, and the rule that separated them horizontally
   would read as a stray mark. The button goes full width because at that size it is the only
   thing to press. --shot-w is a DESKTOP column width and has no business here: between a
   phone and 1024px it stranded a 380px photograph in a column twice that wide. */
@media (max-width:1023px){
  .spot-buy { padding-top:var(--s2); }
  .spot-media .shot, .spot-media .shot-strip,
  .rate-strip .shot, .rate-strip .shot-strip { max-width:min(100%, 560px); }
}
.fineprint { margin-top:var(--s5); font-size:var(--t-small); color:var(--text-cut-muted); max-width:68ch; }

/* PHOTO SLOTS. Text in a box, and nothing else — see views/partials/shot.ejs for why the
   empty state contains no <img> at all.

   DASHED because it is the one universal "this is a placeholder" signal, and because it is a
   LINE, which is the system's stated principle: everything held by an edge rather than a
   shadow. Transparent fill, because a transparent box cannot be mistaken for an image and
   because --surface-panel is a surface-side token with no business on the cut. Radius 0, no
   shadow, no gradient.

   4:3, not 3:2 — originally because Mike would shoot these on a phone and 4:3 is the iPhone's
   native landscape frame. It outlived that reason: the professional set is 3:2 and is centre-
   cropped to 4:3 on the way in, so the grid box never reflows no matter who holds the camera.
   Width now lives in --shot-w (see above), raised from 240px on 2026-08-21.

   Both text roles are already asserted in bin/check-contrast.js (--text-cut-muted 5.23:1,
   --text-cut 12.73:1 on the cut) and the frame is asserted there too. Anyone reaching for a
   new colour here owes check-contrast.js a new pair in the same commit. */
/* --shot-w: ONE number for the hero and its strip, so they can never drift apart.
   240px was chosen when these slots held placeholders and phone snapshots, to stop the
   black band reading as a gallery. 2026-08-21 changed the input: the rooms are finished and
   professionally photographed, and this row is the page's selling surface. 340px is the
   largest the flexible first column takes at the 1024px breakpoint without crowding the
   210/140/210 price columns beside it. */
:root { --shot-w: 380px; }
.shot { aspect-ratio:4/3; display:grid; align-content:center; gap:var(--s2);
        padding:var(--s4); margin:0 0 var(--s4); background:transparent;
        border:1px dashed var(--rule-cut-strong); border-radius:var(--radius);
        max-width:var(--shot-w); }
/* Deliberately identical to .stock, so a placeholder reads as a status and not decoration. */
.shot-label { font:500 var(--t-label)/1.3 var(--font-text); letter-spacing:.16em;
              text-transform:uppercase; color:var(--text-cut-muted); }
.shot-brief { margin:0; font-size:var(--t-small); color:var(--text-cut); }
/* The real photograph, when there is one: same box, same ratio, no dashed frame. */
.shot--real { padding:0; border:0; aspect-ratio:auto; }

/* ── tap to enlarge ─────────────────────────────────────────────────────────
   Pure :target — no JavaScript anywhere on this site, and the CSP has no inline
   allowance. Hidden until the URL names it; the scrim and the × are both plain
   links back to "#", so there are two ways out and the phone back button is a
   third. */
.shot-zoom { display:block; cursor:zoom-in; }
.shot-zoom img { display:block; transition:opacity .12s ease; }
.shot-zoom:hover img, .shot-zoom:focus-visible img { opacity:.88; }

.lightbox { display:none; }
/* TWO TRIGGERS, AND ONLY ONE OF THEM IS EVER LIVE AT A TIME. That is the fix for a real
   trap shipped on 2026-08-21: the gallery added strip thumbnails and prev/next links that
   the script did not know about, so those opened by :target while the close button, the
   scrim and Esc all went through the script — which had no record of an open box and
   silently did nothing. preventDefault() then killed the fallback too. A reader who tapped
   a thumbnail got a full-screen photograph with no way out but the back button, on the page
   that charges $747.
   So: lightbox.js stamps html.lb-js at boot, which RETIRES the :target path entirely. With
   JS on, .is-open is the only thing that opens a box and the script cannot desync from the
   URL. With JS off, :target behaves exactly as it always did. Never let both drive at once. */
html:not(.lb-js) .lightbox:target,
.lightbox.is-open {
  display:grid; place-items:center; position:fixed; inset:0; z-index:50;
  padding:var(--s5);
}
/* Stops the page scrolling behind the overlay on a phone, which reads as the lightbox
   drifting rather than the page moving. */
html.lb-open { overflow:hidden; }
.lightbox-scrim { position:absolute; inset:0; background:rgba(14,15,18,.94); cursor:zoom-out; }
html:not(.lb-js) .lightbox:target img,
.lightbox.is-open img {
  position:relative; max-width:min(100%, 1600px); max-height:88vh;
  width:auto; height:auto; box-shadow:0 24px 80px rgba(0,0,0,.6);
}
.lightbox-close {
  position:absolute; top:var(--s4); right:var(--s5); z-index:1;
  font:400 2.2rem/1 var(--font-text); text-decoration:none;
  color:var(--text-cut); padding:var(--s2) var(--s3);
}
.lightbox-close:hover, .lightbox-close:focus-visible { color:var(--accent-cut); }
/* The board is black, so the overlay inherits the cut palette for its focus ring. */
.lightbox { --focus: var(--led); }

/* ── the thumbnail strip, 2026-08-21 ────────────────────────────────────────
   A tier can carry several photographs now. The hero keeps the 4:3 box it always
   had — nothing above it reflows — and the rest sit beneath as a row of small
   links. Each one is a real link to a real overlay, so this works with JS off,
   exactly like the hero does. max-width matches .shot so the strip never
   outgrows the column it belongs to. */
/* FLEX, NOT A FIXED COLUMN COUNT. A three-column grid left a dead third cell whenever a
   tier carried two supporting shots, which is the common case. Flex with `flex:1 1 0` makes
   whatever is there share the width evenly — two thumbs or four, no CSS change. */
.shot-strip { display:flex; gap:var(--s2);
              list-style:none; margin:var(--s2) 0 0; padding:0; max-width:var(--shot-w); }
.shot-strip li { flex:1 1 0; min-width:0; }
.shot-strip a { display:block; cursor:zoom-in; border-radius:calc(var(--radius) / 2);
                overflow:hidden; }
.shot-strip img { display:block; width:100%; height:auto; aspect-ratio:4/3;
                  object-fit:cover; transition:opacity .12s ease; }
.shot-strip a:hover img, .shot-strip a:focus-visible img { opacity:.75; }

/* Stepping through a set. Links, not script — each overlay points at its siblings,
   so the whole gallery walks with the keyboard and survives JS being off. The
   controls sit above the scrim (which is inset:0) or they cannot be clicked. */
.lightbox-step {
  position:absolute; top:50%; transform:translateY(-50%); z-index:2;
  font:400 3rem/1 var(--font-text); text-decoration:none; color:var(--text-cut);
  padding:var(--s3) var(--s4); user-select:none;
}
.lightbox-prev { left:var(--s2); }
.lightbox-next { right:var(--s2); }
.lightbox-step:hover, .lightbox-step:focus-visible { color:var(--accent-cut); }
.lightbox-count {
  position:absolute; bottom:var(--s4); left:50%; transform:translateX(-50%); z-index:2;
  margin:0; font-size:var(--t-small); color:var(--text-cut-muted); letter-spacing:.08em;
}
/* On a phone the arrows would sit over the photograph's edges; drop them to the
   bottom corners where a thumb already is, and keep the count centred between. */
@media (max-width: 560px) {
  .lightbox-step { top:auto; bottom:var(--s3); transform:none; font-size:2.4rem; }
  .lightbox-prev { left:var(--s3); }
  .lightbox-next { right:var(--s3); }
}

/* The conference room is a rate, not a membership — so it sits BELOW the board's frame and
   wears none of the row treatment. No price column, no stock line, no button. */
.rate-strip { display:grid; gap:var(--s4); margin-top:var(--s6); }
.rate-strip h3, .rate-strip h2 { margin:0 0 .3em; font:600 var(--t-lede)/1.3 var(--font-text); }
.rate-strip p { margin:0; font-size:var(--t-small); color:var(--text-cut); max-width:46ch; }
.rate-strip h3, .rate-strip h2 { font:700 var(--t-tier)/1.05 var(--font-display); letter-spacing:-.02em;
                 margin:0 0 var(--s3); color:var(--text-cut-strong); }
.rate-strip p { font-size:var(--t-lede); line-height:1.45; }
/* THE ACTION ROW AND THE FINE PRINT UNDER IT. The rule above gives every paragraph in the block
   the lede size and no margin, and at (0,1,1) it beat .hero-do and .muted at (0,1,0): the fine
   print rendered at lede size and the button sat flush on the paragraph above it (owner,
   2026-09-07). Named with the element so these win on specificity, not on source order. The
   phone link takes the body size here because it stands beside a 52px button, not in a table. */
.rate-strip p.hero-do { margin:var(--s4) 0 0; }
.rate-strip p.muted { font-size:var(--t-small); color:var(--text-muted); margin-top:var(--s3); max-width:46ch; }
.rate-strip .hero-do .linkish { font-size:var(--t-body); }
/* THE ROOM BAND (2026-09-07): the conference block's own ground on the open site, cut into the
   board between the tiers and the hospitality row. It is the PAGE ground, not the sand: the board
   is sand in the light theme (.band-black swaps its cut for --sand there), so a sand band was the
   same colour as the board and only added padding, which is what the owner saw. --surface is paper
   against the sand board in light and charcoal against the black board in dark, so the band reads
   in both. The block's own rules colour their text for the board (--text-cut), so here they take
   the page palette back, and the top margin that spaced the block under the tier rows goes. */
.band-room { background:var(--surface); color:var(--text); padding-block:var(--s7); }
/* THE SEAM. Two full band paddings met at each edge of the room band (the board's bottom, the
   band's top) and left about 130px of nothing between the last card and the reel. The board
   sections either side give up half their padding at the seam; the band keeps the tight one. */
.band-black:has(+ .band-room) { padding-bottom:var(--s6); }
.band-room + .band-black { padding-top:var(--s6); }
/* AND NO STRANDED RULES AT THE SEAMS. The board's bottom edge and the hospitality row's top
   rule were dividers inside one continuous board; with the ground change doing the dividing
   they floated on their own (owner, 2026-09-07). Off at the seams only, in both themes. */
.band-black:has(+ .band-room) .board { border-bottom:0; }
.band-room + .band-black .extras { margin-top:0; padding-top:0; border-top:0; }
/* THE ROOM AS A PRODUCT: eyebrow, name, and the rate large in the accent (the ember on paper,
   where the amber would fail contrast; the amber on the dark ground), the way a tier card leads
   with its price. On phones the words come before the pictures, so a scroller meets the name
   and the price before the reel; from 1024px the grid puts media left and words right, as the
   cards do, and the order rule steps aside. */
/* The eyebrow takes the site's one eyebrow size by name: .rate-strip p sizes every paragraph in
   the block at the lede size and caught this one (19px against the board's 13px, owner). */
.band-room .rate-strip .eyebrow { margin:0 0 var(--s2); color:var(--text-muted); font-size:var(--t-label); line-height:1; }
.band-room .rate-strip .rate-price { margin:0 0 var(--s4); font-size:var(--t-lede); color:var(--text); }
.band-room .rate-strip .rate-price strong { font:700 var(--t-h2)/1 var(--font-display); color:var(--accent); letter-spacing:-.02em; }
@media (max-width: 1023px) { .band-room .rate-strip > div:last-child { order:-1; } }
.band-room .rate-strip { margin-top:0; }
.band-room .rate-strip h2 { color:var(--text-strong); }
.band-room .rate-strip p { color:var(--text); }
.band-room .rate-strip p.muted { color:var(--text-muted); }
@media (min-width:1024px){
  /* The board's first two tracks, so the conference room sits in the same rhythm as the
     things above it. Its photograph used to be a 240px afterthought pinned to the right of
     500px of nothing. */
  .rate-strip { grid-template-columns:minmax(0,1fr) minmax(0,380px); gap:var(--s6); align-items:start; }
  .rate-strip .shot, .rate-strip .shot-strip { max-width:none; }
  .rate-strip .shot { margin-bottom:0; }
  .rate-strip p { max-width:52ch; }
}

/* THE REST OF THE BUILDING. Two rooms, two photographs, neither subordinate to the other —
   so an even two-up grid rather than the hero-and-strip the priced rows use. Same tokens,
   same edges, no new colour; it reads as part of the board because it is built out of the
   board's parts. */
.extras { margin-top:var(--s7); padding-top:var(--s6); border-top:1px solid var(--rule-cut-strong); }
.extras-copy { max-width:var(--w-text); }
.extras-copy h3 { margin:0 0 .3em; font:700 var(--t-tier)/1.05 var(--font-display);
                  letter-spacing:-.02em; color:var(--text-cut-strong); }
.extras-copy p { margin:0; font-size:var(--t-lede); line-height:1.45; color:var(--text-cut);
                 max-width:46ch; }
.extras-cta { margin:var(--s5) 0 0; }
/* THESE THREE STOPPED BEING A LIST, 2026-08-22.

   They were three captioned thumbnails in a row under a heading above a button, which is the
   grammar of a product listing — and this block sells nothing. Mark: "this section isn't a
   product listing, so let's not list the three static, captioned thumbnails down here like
   they're product shots." The captions came off in lib/tiers.js; this is the other half.

   ONE IMAGE'S WORTH OF HEIGHT ON A PHONE, NOT THREE. The old rule stacked them into a single
   column below 640px: three 4:3 photographs at 390px is about 876px of image, one under
   another, which is the same mistake as the row tint deleted this morning arriving by a
   different road. A horizontal scroll-snap strip holds the vertical cost to one photograph
   and breaks the vertical-list reading outright. Native CSS, no script — this page is under
   script-src 'self' and the lightbox is the only JS on it.

   NOT FULL BLEED, deliberately. A bleed here is a bleed inside the board's own ground and
   needs negative margins fought against the board's padding — fragile, and precisely the
   class of bug removed this morning.

   The strip is scrolled, so it needs to be reachable without a mouse: tabindex="0" would make
   it focusable, but each photograph already contains a focusable zoom link, so keyboard users
   reach every item by tabbing and the browser scrolls the strip to follow. Do not add a
   tabindex here without re-checking that. */
.extras-shots { display:grid; grid-auto-flow:column; grid-auto-columns:78%;
                gap:var(--s4); margin-top:var(--s6);
                overflow-x:auto; scroll-snap-type:x mandatory;
                scroll-padding-inline:var(--s5); overscroll-behavior-x:contain;
                scrollbar-width:none; }
.extras-shots::-webkit-scrollbar { display:none; }
.extras-shots > * { scroll-snap-align:start; }
.extras-shots .shot { max-width:none; margin-bottom:0; }

/* THREE ACROSS from the width where the board's own rows stop stacking, so the photographs
   and the product rows change shape at the same breakpoint instead of at two different ones.
   auto-fit, not a fixed 3 — a fourth photograph would otherwise orphan onto a second row. */
@media (min-width:1024px){
  .extras-shots { grid-auto-flow:row; grid-auto-columns:auto; overflow-x:visible;
                  grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); gap:var(--s5); }
}

/* .promise-inner IS DELETED, 2026-08-22. It styled "How the charter works", the page's one
   boxed panel, which has been absorbed into the board as .charter. The box went with it: this
   system frames with bands, and the note on the row tint further up calls a box "the thing
   this system refuses everywhere else." It was carrying an exception nobody had argued for.
   --surface-panel now has no consumer on this page; left defined, because it is a real token
   and the admin views use it. */

/* THE CAPTURE IS CENTRED, and it is the only centred block on a left-aligned page — which is
   the point rather than an inconsistency. Mark: "that section may look better centered
   horizontally, so that it marries up nicely with the footer container and really grabs
   attention before someone may decide to bounce at the end of the scroll."

   It is a terminal node. Breaking the reading line that every band above it shares is what
   says the page is over, and it lines the block up with the footer directly beneath it.

   The measure is constrained AND the text is centred inside it. A left-aligned column
   floating in the middle of a wide band reads as a mistake rather than a decision, so it
   commits. The FORM keeps its own narrower width, centred, so the field does not stretch the
   full measure and the button has an edge to sit against.

   At 390px everything is single-column already, so this reads as emphasis, not as layout. */
.keep-posted { max-width:44rem; margin-inline:auto; text-align:center; }
.keep-posted .form { max-width:26rem; margin-inline:auto; text-align:left; }
.keep-posted .btn { width:100%; }

.page-head { max-width:var(--w-page); margin-inline:auto; padding:var(--s7) var(--s5) var(--s2); }
/* Seven views were already wearing this class before it had a rule — it rendered as an
   ordinary paragraph. Same label idiom as .preview-mark, sized to sit above an h1. */
.eyebrow { margin:0 0 var(--s2); font:600 var(--t-label)/1 var(--font-text);
           letter-spacing:.08em; text-transform:uppercase; color:var(--text-muted); }
.doc { max-width:var(--w-page); margin-inline:auto; padding:var(--s2) var(--s5) var(--s7); }
.page-head > *, .doc > * { max-width:var(--w-doc); }
/* TABLES BREAK OUT OF THE PROSE MEASURE. The rule above capped EVERY child at 720px,
   .scroll wrappers included, so on a wide screen a four- or five-column comparison scrolled
   sideways inside a 720px box while four hundred pixels of the same container sat empty.
   /plan carries twelve of them and is the page the counterparty reads. Prose keeps its
   measure — that is a readability limit and correct — and only the tables take the band. */
.doc > .scroll, .doc > .walkthrough { max-width:100%; }

/* The walkthrough on /plan. Breaks out of the prose measure like a table does — it is the
   same content as the page, and a 720px video beside 400px of empty band would read as an
   afterthought rather than the summary. */
.walkthrough { margin:var(--s7) 0; max-width:100%; }
.walkthrough video { width:100%; height:auto; display:block; background:var(--surface-cut);
                     border:1px solid var(--rule-surface); }
.walkthrough figcaption { margin-top:var(--s3); font-size:var(--t-small);
                          color:var(--text-muted); max-width:var(--w-doc); }
/* The re-cut notice sits INSIDE the caption but on its own line and in full-strength ink —
   it is addressed to someone who may have watched the superseded cut, so it cannot be the
   quietest thing on the page. A rule rather than a colour band: this is a correction, not a
   warning, and the warn tokens would overstate it. */
.recut { display:block; margin-top:var(--s2); padding-top:var(--s2);
         border-top:1px solid var(--rule-surface); color:var(--text); }
.recut strong { color:var(--text-strong); }
.doc h2 { font-size:var(--t-lede); margin-top:1.9em; }
.muted { color:var(--text-muted); font-size:var(--t-small); }
/* THE ACKNOWLEDGEMENT BAND — what the last click actually did.

   Sits directly under the admin chrome, above the page's own heading, so it is the first thing
   read after a redirect. Same visual grammar as .notice (tinted fill, 3px ink edge on the
   leading side) because it IS a notice — it just reports the past rather than warning about
   the future, which is what earns it the third colour.

   TWO KINDS AND NO MORE. 'done' is green and reversible; 'warn' is the amber of a warning and
   is used for the one action that destroyed something. A palette of five severities is a
   palette nobody learns.

   The mark is a pseudo-element rather than a character in the markup, so a screen reader gets
   the sentence and nothing else — role="status" already carries the meaning that a tick glyph
   would only repeat. */
.flash { display:flex; gap:var(--s3); align-items:flex-start;
         max-width:var(--w-page); margin:0 auto; padding:var(--s4) var(--s5);
         font-size:var(--t-small);
         background:var(--ok-fill); color:var(--ok-ink);
         border-bottom:1px solid var(--ok-line); border-left:3px solid var(--ok-ink); }
.flash p { margin:0; }
.flash--warn { background:var(--warn-fill); color:var(--warn-ink);
               border-bottom-color:var(--warn-line); border-left-color:var(--warn-ink); }
/* A disc, not a tick: the same mark reads correctly for both states, and a tick beside
   "Deleted permanently" would be cheerful about the wrong thing. */
.flash-mark { flex:0 0 auto; width:8px; height:8px; margin-top:.42em;
              border-radius:50%; background:currentColor; }
/* Pushed to the far end so the sentence reads first and the control is where a control goes.
   It inherits the band's colour rather than taking an accent: an undo beside "Deleted X" is
   not a call to action, it is a way back, and colouring it like a primary button would make
   the destructive path look like the recommended one. */
.flash-undo { margin-left:auto; flex:0 0 auto; }
/* The contact sheet is a document, not a decoration — full width, its own rule, and a caption
   that says to open it, because the filenames on it are the reason it exists. */
/* ── THE LIBRARY SHELF ─────────────────────────────────────────────────────────────────────
   SIX ROWS, ONE COLUMN, AT EVERY WIDTH. This was a 2-up grid of cards carrying a sentence
   each, and on a phone that is one column of six paragraphs: Mark, 2026-08-24, on his own
   iPhone — "Now all the links are below the fold." A row is a spine on a shelf. It gets a
   title and one line of small type, and the sentence lives on the page it describes.

   THE BORDER TOKEN IS --rule-surface AND IT ALWAYS SHOULD HAVE BEEN. The cards were written
   with border:1px solid var(--line). There is no --line in this stylesheet — the tokens are
   --rule, --rule-strong, --rule-surface, --line-input — so the declaration was invalid and
   every card was drawn with no border at all, in both themes, from the hour it shipped. It
   looked deliberate, which is exactly why it survived: a phantom custom property does not
   throw, does not warn, and does not fall back to anything visible. bin/smoke.js now greps
   this file for var(--x) with no matching --x: definition, because the next one will be as
   quiet as this one was.

   WHOLE-ROW TAP TARGETS, and the arrow is decoration inside the same link rather than a
   second thing to hit. 60px minimum on a phone — comfortably over the 44px floor, because
   this page is reached one-handed mid-job. */
.shelf { display:grid; gap:0; margin:var(--s5) 0 0; }

/* THE CLUSTER HEADINGS. Quiet on purpose — they are a shelf label, not a page heading, and the
   row titles are what somebody is scanning for. The top border moved off .shelf and onto the
   heading so each cluster is ruled off from the one above it rather than the whole list having
   one line at the very top. */
.shelf-group { font-family:var(--font-text); font-size:var(--t-label); font-weight:600;
               letter-spacing:.08em; text-transform:uppercase; color:var(--text-muted);
               margin:var(--s6) 0 0; padding-top:var(--s3);
               border-top:1px solid var(--rule-surface); }
.shelf-group:first-child { margin-top:var(--s2); }
.shelf-group + .shelf-row { border-top:0; }
.shelf-row { display:flex; align-items:center; gap:var(--s3);
             min-height:58px; padding:var(--s2);
             border-bottom:1px solid var(--rule-surface);
             text-decoration:none; color:inherit;
             transition:background-color .12s, padding-left .12s; }
.shelf-row:hover { background:var(--surface-panel); padding-left:var(--s3); }
.shelf-row:active { background:var(--surface-panel); }
.shelf-row:focus-visible { outline:2px solid var(--focus); outline-offset:-2px; }
.shelf-text { display:block; min-width:0; flex:1 1 auto; }
.shelf-title { display:block; font-family:var(--font-display); font-size:1.08rem;
               line-height:1.25; }
/* COUNT AND HINT ON ONE LINE, and the count is the darker half. It is the part that changes
   when the data changes, so it reads as a fact; the hint stays quiet because it is a signpost.
   NOT amber. Amber is this site's accent and the count used to wear it, above the title, in a
   column of six — six accent-coloured numbers down the left edge is six things to read before
   the first word that could be the one you came for. */
.shelf-meta { display:block; font-size:.82rem; line-height:1.35; margin-top:.15em;
              color:var(--text-muted); }
.shelf-n { color:var(--text); font-weight:600; }
.shelf-go { flex:0 0 auto; color:var(--text-muted); font-size:1.05rem; }
.shelf-row:hover .shelf-go { color:var(--accent); }

/* THE ROW YOU ARE STANDING ON. It stays in the list rather than being dropped: six rows in the
   same order on every page is a shape you learn, and a list that silently loses one is five
   rows that move. It is not a link to anywhere new, so it says so. */
.shelf-row.is-here { color:var(--text-muted); background:var(--surface-panel); }
.shelf-row.is-here .shelf-title { font-weight:600; }
.shelf-row.is-here .shelf-go { visibility:hidden; }

/* The names line at the foot of the index, under the six. */
.shelf-names { margin:var(--s6) 0 0; font-size:.9rem; line-height:1.6; color:var(--text-muted); }
.shelf-names strong { color:var(--text); font-weight:600; }
/* NO white-space:nowrap HERE. It was, and at 320px the link ran 4px past the right edge —
   caught by the overflow probe, not by looking. The arrow is kept off a line of its own with a
   non-breaking space in the template instead, which is the part that actually needed holding
   together. */
.shelf-names a { color:var(--accent); text-decoration:none; font-weight:600; }
.shelf-names a:hover { text-decoration:underline; text-underline-offset:3px; }

/* The shelf repeated at the foot of a section. Set apart from the page's own content, quieter
   than the index's copy of the same six. */
.shelf-foot { margin:var(--s8) 0 0; }
.shelf-foot-head { margin:0; font-size:var(--t-label); letter-spacing:.08em;
                   text-transform:uppercase; color:var(--text-muted); }
.shelf-foot .shelf { margin-top:var(--s3); }

.sheet { margin:var(--s5) 0; }
.sheet img { display:block; width:100%; height:auto; border:1px solid var(--rule-cut); }
.sheet figcaption { margin-top:var(--s2); font-size:var(--t-small); color:var(--text-cut-muted); }
.flash-undo button { color:inherit; font-weight:600; text-decoration:underline;
                     text-underline-offset:3px; min-height:44px; padding:0 var(--s2); }

.notice { background:var(--warn-fill); border:1px solid var(--warn-line);
          border-left:3px solid var(--warn-ink); color:var(--warn-ink);
          padding:var(--s4); font-size:var(--t-small); margin-bottom:var(--s6); }
.notice.alarm { background:var(--error-fill); border-color:var(--error-line);
                border-left-color:var(--error-ink); color:var(--error-ink); }

.form { display:grid; gap:var(--s4); max-width:var(--w-form); }
.form--auth { max-width:var(--w-auth); }
.field { display:grid; gap:var(--s2); }
.form label { font-weight:600; font-size:var(--t-small); }
.form input { padding:var(--s3) var(--s4); min-height:48px; background:var(--surface-field);
              border:1px solid var(--line-input); border-radius:var(--radius-part); }
.form input:focus-visible { background:var(--surface-field-focus); }
/* display:grid WAS MISSING UNTIL 2026-09-08, so the two grid properties below did nothing and a
   label that wrapped put its second line under the checkbox instead of hanging in its own column.
   Measured at 390px on /room: the label's computed display was "block" and line two started at
   x=24, beneath the 24px box. Same defect on all three ticks of /claim, and it now lands on the
   control that forms the room's contract. */
.form .check { display:grid; grid-template-columns:auto 1fr; grid-auto-flow:column;
               align-items:start; gap:var(--s3); font-weight:400; }
/* THE BOX SITS ON THE FIRST LINE, NOT ON TOP OF IT (owner, 2026-09-08, on the claim page).
   align-items:start puts the 24px box at the top of the row, where the first line's glyphs sit
   lower inside their own line box, so the box floated about 6px above the words and the gap
   grew with the type size. Half the difference between the line box and the box, in em, follows
   the type at every size and leaves a wrapped label aligned to its first line rather than
   centred on the whole paragraph. */
.form .check input { width:24px; height:24px; min-height:0; margin-top:calc((1.6em - 24px) / 2 + 0.2em); }
.opt { font-weight:400; color:var(--text-muted); }
.hp { position:absolute; width:1px; height:1px; clip-path:inset(50%); overflow:hidden; }

/* Demo-state marker. Deliberately unmissable but out of the reading column, and it only
   ever renders pre-launch. */
/* Bottom-RIGHT: the primary CTA is left-aligned, and a demo marker that covers the button
   you're demonstrating is worse than no marker. */
.demo-flag { position:fixed; right:var(--s4); bottom:var(--s4); z-index:50; margin:0;
             background:var(--amber); color:var(--black); font:600 var(--t-label)/1.2 var(--font-text);
             letter-spacing:.04em; padding:var(--s2) var(--s3); border-radius:var(--radius-part); }
.demo-flag a { color:var(--black); text-underline-offset:2px; }
/* IT MOVES OFF THE DOCK BUTTON BELOW 821px. The flag is fixed bottom-right at z-index 50 and
   the phone dock control is fixed bottom-right at z-index 40 — same 16px corner, and the flag
   wins. On the one device this site is mostly read on, the only way into the admin menu was
   underneath a "preview" label. Left, not up: up would put it under the thumb that is reaching
   for the menu. */
@media (max-width: 820px) {
  /* THE OTHER CORNER, not a measured offset from the button. Nudging it left by the button's
     width would be a second place that has to know how wide that button is, and it just got
     wider — it carries the word "Menu" now. Opposite corner cannot drift. */
  .demo-flag { right:auto; left:calc(var(--s4) + env(safe-area-inset-left, 0px));
               bottom:calc(var(--s4) + var(--dock-inset, env(safe-area-inset-bottom, 0px))); }
}

/* footer — the second black region */
.site-foot { background:var(--surface-cut); color:var(--text-cut); }
.foot-inner { max-width:var(--w-page); margin-inline:auto; padding:var(--s7) var(--s5);
              display:grid; gap:var(--s5); justify-items:start; }
.foot-brand { display:grid; gap:var(--s4); justify-items:start; }
.foot-brand img { width:auto; height:72px; }
/* <address> is the right element for a physical location but browsers italicise it.
   SCOPED TO .site-foot, NOT .foot-brand, AND THAT IS THE FIX FOR A BUG THIS FILE CAUSED. When
   the address moved out of the brand block into its own column on 2026-08-22, these three
   rules stayed pointed at .foot-brand — so the address rendered ITALIC with an unstyled link,
   which is the browser default and looked like a mistake because it was one. Caught by looking
   at the rendered footer rather than at the diff.
   A rule scoped to a parent is a rule that breaks when the child moves. These are scoped to
   the footer, which is the thing that actually owns the styling. */
.site-foot address { font-style:normal; margin:0; color:var(--text-cut); line-height:1.7; }
.site-foot address a { color:var(--text-cut); text-decoration:underline;
                       text-underline-offset:3px; text-decoration-color:var(--rule-cut); }
.site-foot address a:hover { color:var(--accent-cut); text-decoration-color:currentColor; }
/* A page that takes money needs a human on it. Amber, because this is the one thing in the
   footer someone should be able to find while scanning. */
/* One eyebrow idiom for both non-brand columns, same voice as .arc .when and .revhead. */
.foot-eyebrow { margin:0 0 var(--s3); font:600 var(--t-label)/1 var(--font-text);
                letter-spacing:.08em; text-transform:uppercase; color:var(--text-cut-muted);
                flex-basis:100%; }
.foot-tag { margin:0; color:var(--text-cut-muted); }
/* THE FACEBOOK LINK, under the tagline in the brand column (2026-09-09). Not amber: .foot-reach
   is the one thing in this footer meant to be found while scanning, and a second amber item
   beside it would split that. It reads at the address's weight and takes the accent on hover,
   which is the idiom the address links above already use.
   inline-flex on the anchor so the mark and the word are one target, and min-height 44px so
   that target is thumb-sized on a phone without a second rule in the phone tier. */
.foot-social { margin:0; }
.foot-social a { display:inline-flex; align-items:center; gap:.5em; min-height:44px;
                 color:var(--text-cut); text-decoration:none; }
.foot-social a:hover { color:var(--accent-cut); text-decoration:underline;
                       text-underline-offset:3px; }
.foot-social-mark { display:block; flex:none; }
.foot-contact { display:grid; align-content:start; color:var(--text-cut-muted); }
.foot-reach { margin:0; line-height:2; }
.foot-reach a { color:var(--accent-cut); text-decoration:none; font-size:var(--t-body);
                font-weight:600; }
.foot-reach a:hover { text-decoration:underline; text-underline-offset:3px; }

/* SIX links, and at phone width they wrap — "Refunds" lands on its own row. The note that
   used to sit here claimed they were "sized to sit on one row down to 360px"; that stopped
   being true and nothing noticed, because a claim in a comment is not a measurement. Six
   labels need about 343px of text before any gaps, against roughly 342px of usable width at
   390px, so they cannot fit and the claim was never recoverable. They are allowed to wrap.

   row-gap STAYS 0, and that is deliberate rather than inherited. Wrapping was blamed for the
   active underline looking like a divider between the two rows — it wasn't; the border-bottom
   was, see the active rule below. With the mark now drawn 5px under the word, the rows read
   correctly at gap 0 and the 24px the paddings already contribute is the right separation. A
   row-gap on top of that was tried and made the orphan float.

   Underlines are still dropped on the inactive links — a labelled nav landmark of grouped
   links reads as links without them, and six underlines on black was a fence. */
.foot-links { display:flex; flex-wrap:wrap; gap:0 var(--s4);
              margin-block:calc(var(--s3) * -1); }
/* inline-block so the vertical padding actually counts toward the tap target — on an inline
   <a> it paints but doesn't measure, which is how these were sitting at 40px. */
.foot-links a { display:inline-block; color:var(--text-cut); text-decoration:none;
                padding:var(--s3) 0; font-size:var(--t-small); letter-spacing:.02em;
                position:relative; --nav-pad:var(--s3); }
.foot-links a:hover { color:var(--accent-cut); text-decoration:underline; text-underline-offset:3px; }
/* ONE LINE UNDER THE CURRENT PAGE, NOT TWO (owner, 2026-09-08, the same fault the header had in
   v1.0.59). The current page already wears the mark below; hovering it added the hover's own
   underline on top, so the word sat over two rules three pixels apart. The mark is the state,
   so the hover keeps the colour and drops the underline. (0,3,1) over the hover rule's (0,2,1),
   and it is also later in the sheet, so neither specificity nor order can flip it back. */
.foot-links a[aria-current="page"]:hover { text-decoration:none; }
/* THE ACTIVE MARK, SHARED BY BOTH NAVIGATIONS — see .dock-nav below, which opts in by
   setting --nav-pad. Two navigations on one site that mark the current page differently is
   two things to learn, so the rule lives once and both wear it.

   Weight AND colour, not colour alone — colour by itself fails anyone who cannot separate
   amber from bone.

   THE UNDERLINE IS A PSEUDO-ELEMENT, NOT border-bottom, and that is the fix. A border paints
   at the edge of the PADDING box, so it sat the full vertical padding away from the word —
   12px in the footer, which read as detached even before wrapping, and read as a divider once
   "Refunds" appeared underneath it. The padding cannot simply be reduced: it is what makes
   the tap target, and this nav already sat at 40px once. So the padding stays and the mark
   moves, drawn 5px under the text where an underline belongs, with the tap target untouched. */
.foot-links a[aria-current="page"], .dock-nav a[aria-current="page"] {
  color:var(--accent-cut); font-weight:600; }
.foot-links a[aria-current="page"]::after, .dock-nav a[aria-current="page"]::after {
  content:""; position:absolute; left:0; right:0; height:2px; background:var(--accent-cut);
  bottom:calc(var(--nav-pad) - 5px); }
/* ── THE HEADER NAV — THE SAME MARK ON THE OTHER GROUND ────────────────────────────────────
   Third navigation, same rule, one difference that matters: the footer and the dock sit on the
   dark cut ground and read --text-cut / --accent-cut; this one sits on the page's own light
   surface, where --accent-cut is amber and amber fails AA on light. It takes --accent, which
   resolves to --ember, the same brand orange at a dark value from the wordmark's own ramp.
   Getting that wrong is not a style slip — it is the current page becoming the hardest word in
   the nav to read. */
.head-links { display:flex; flex-wrap:wrap; gap:0 var(--s5); }
.head-links a { display:inline-block; color:var(--text); text-decoration:none;
                padding:var(--s3) 0; font-size:var(--t-small); letter-spacing:.02em;
                position:relative; --nav-pad:var(--s3); }
.head-links a:hover { color:var(--accent); text-decoration:underline; text-underline-offset:3px; }
.head-links a[aria-current="page"] { color:var(--accent); font-weight:600; }
/* The current item already carries its bar; the hover underline on top of it drew two lines
   under one word (owner, 2026-09-07). Same specificity as the hover rule, later in the sheet. */
.head-links a[aria-current="page"]:hover { text-decoration:none; }
.head-links a[aria-current="page"]::after {
  content:""; position:absolute; left:0; right:0; height:2px; background:var(--accent);
  bottom:calc(var(--nav-pad) - 5px); }
/* THE LOCKUP IS THE SIZE PROBLEM, NOT THE LINKS. .brand img is clamp(88px,28vw,140px) tall —
   at 390px that is a 110px-tall logo, and three links cannot sit beside it. So below the same
   820px the dock uses, the header stacks: mark on its own line, links centred beneath it. One
   number for both pieces of chrome rather than a second one to keep in step. */
@media (max-width: 820px) {
  /* TWO ROWS, NOT THREE. Giving .head-links flex-basis:100% pushed the theme control onto a
     third row of its own and took the header from 180px to 273px on a 390px phone — a third of
     the viewport of chrome above a headline. The brand takes the full row instead, and the
     links and the toggle share the one below it, which is what the space-between rule was
     already doing at desktop width. */
  /* FOUR LINKS, ONE LINE, TWO ROWS (2026-09-07, after the owner saw the phone). With the fourth
     link the old stack broke into two ragged lines of links beside a floating toggle, 265px of
     header at 390px. Now the LOCKUP AND THE TOGGLE SHARE THE FIRST ROW (lockup left at its
     floor height, toggle right), and the links take the whole second row on one line, a step
     smaller and spread edge to edge. Measured with device emulation: scrollWidth 390, header
     about 150px. If a fifth link ever arrives, this line is the first thing to re-measure. */
  .nav { flex-wrap:wrap; justify-content:space-between; align-items:center; row-gap:var(--s3); }
  .nav .brand { flex:0 1 auto; text-align:left; }
  /* AS BIG AS THE ROW ALLOWS, capped at 96px. With the theme control in the menu the first row
     is the lockup and one 44px button, so the lockup may take the rest: the viewport less the
     two 24px gutters, the button, and a 16px gap, divided by the lockup's 210:72 ratio. At 390
     that is 96px (owner: 72 was "a bit too small"); at 360 it is 86; at 320 it is 72; it never
     wraps the row, which is what the earlier fixed heights did. */
  .nav .brand img { height:min(96px, calc((100vw - 108px) / 2.917)); width:auto; }
  .theme-toggle { order:1; margin-left:auto; }
  .menu-toggle { order:2; }
  /* ROUND AND WORDLESS ON THE PHONE, a pair with the theme control; the word is for the desktop
     that never shows it and for assistive tech through aria-label, which nav.js keeps current. */
  .nav--js .menu-toggle { display:inline-flex; width:44px; padding:0; justify-content:center; }
  .nav--js .menu-toggle .menu-word { position:absolute; width:1px; height:1px; overflow:hidden;
                                     clip:rect(0 0 0 0); white-space:nowrap; }
  /* WITHOUT THE SCRIPT the links are a plain wrapped row under the lockup, every page reachable.
     WITH IT they fold behind the Menu button and open as a stacked list, full width, one link
     per line at a 48px tap height, hairlines between, the current page in the accent as it is
     inline. The list is in flow and pushes the page down while open: no overlay, no stacking
     context, nothing to trap a scroll or a tap underneath. */
  .head-links { order:3; flex-basis:100%; justify-content:space-between; gap:0 var(--s2); }
  .nav--js .head-links { display:none; }
  .nav--js.is-open .head-links { display:flex; flex-direction:column; gap:0; margin-top:var(--s2);
                                  border-top:1px solid var(--rule); }
  .nav--js.is-open .head-links a { padding:12px 0; border-bottom:1px solid var(--rule);
                                    font-size:var(--t-body); }
  .nav--js.is-open .head-links a[aria-current="page"]::after { display:none; }
  /* THE THEME CONTROL AS A MENU ROW. nav.js moves the button into the list on phones; here it
     drops the ring and takes the row's shape, icon then a written label, so it reads as one more
     item rather than a stray widget. The written label is CSS-generated because theme.js
     rewrites the button's contents when the theme flips; aria-label carries it for readers. */
  .nav--js .head-links .theme-toggle { width:100%; margin-left:0; height:auto; border:0; border-radius:0;
                                       border-bottom:1px solid var(--rule); padding:12px 0;
                                       justify-content:flex-start; gap:12px; opacity:1;
                                       font:400 var(--t-body)/1.3 var(--font-text); color:var(--text); }
  .nav--js .head-links .theme-toggle::after { content:'Switch theme'; }
}
/* The bar spans and splits: copyright left, Admin right — the full width doing a job at
   every size. At 390px the two ends still share one line. */
.copyright { margin:0; padding-top:var(--s5); border-top:1px solid var(--rule-cut);
             font-size:var(--t-small); color:var(--text-cut-muted); width:100%;
             display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4);
             justify-content:space-between; align-items:baseline; }
/* Quiet by design — it inherits the copyright's muted tone rather than the nav's, because it
   is a staff door, not a destination. Underlines on hover so it still reads as a control. */
.admin-link { color:var(--text-cut-muted); text-decoration:none; }
.admin-link:hover { color:var(--accent-cut); text-decoration:underline; text-underline-offset:3px; }
/* THE FOUR-COLUMN FOOTER, 2026-08-22. Mark: "The HOURS seems kind of slapped together as it
   is, and the mailing address is breaking across two lines awkwardly."

   WHAT WAS WRONG. The old shape was brand | contact | pages, and Hours had been added into the
   contact column — which was built for one eyebrow and one line. Two eyebrows and two facts in
   a narrow tower reads as something wedged in rather than placed. The masthead now takes the
   logo and the tagline across the full width, and the four facts below it each get a column
   and an eyebrow, sharing one baseline instead of one of them hanging off a 72px image.

   THE ADDRESS WAS NEVER A WIDTH PROBLEM — see the note in partials/foot.ejs. A deliberate
   <br> after the street fixes it at every width; no column sizing was involved.

   THE LADDER: one column on a phone, two at 640 (where four would be about 80px each and
   would break the locality line again in a new place), four at 1000. There is no three-column
   step because the content has no three-way split — forcing one orphans whichever fact came
   fourth, which is how Hours ended up wedged into the contact column in the first place. */
.foot-cols { display:grid; gap:var(--s6) var(--s5); grid-column:1 / -1; width:100%; }
.foot-col  { min-width:0; }
/* The hours line is prose, not a link, so it does not wear .foot-reach's amber and 2.0
   line-height — those exist to make one action findable while somebody scans. */
.foot-plain { margin:0; color:var(--text-cut); line-height:1.7; }

@media (min-width:640px){
  .foot-cols { grid-template-columns:repeat(2, minmax(0, 1fr)); gap:var(--s6) var(--s7); }
  /* max-width:none defeats the global p{max-width:68ch} measure cap — it truncated the
     hairline at ~642px of a 1072px grid. A rule above a copyright is a border, not prose. */
  .copyright { grid-column:1 / -1; justify-self:stretch; width:auto; max-width:none; }
  .foot-brand img { height:80px; }
}
@media (min-width:1000px){
  /* Four columns, one fact each. The nav keeps its vertical stack and its eyebrow — the
     aria-current mark is a positioned pseudo-element under the word, indifferent to flex
     direction, so it survives the row-to-column turn. */
  .foot-cols { grid-template-columns:repeat(4, minmax(0, 1fr)); gap:var(--s7); }
  /* NOWRAP, AND THAT IS TASK #14. The row layout above needs flex-wrap:wrap; inherited into
     a column it turns the nav into a column-wrap container whose height is whatever the grid
     row stretched it to — so the eyebrow sat alone in column one and "Home" through
     "Accessibility" wrapped into a second column beside it. Seen on /thanks because that is
     the page somebody was looking at; it was every page at 1000px and up. */
  /* FLEX-START, OR THE MARK IS A RULE. Column flex items stretch to the column by default, so
     the aria-current ::after (left:0; right:0) ran the width of the column instead of the word
     (Mark, 8/28 12:35 — "should only be as wide as the link label"). --nav-pad follows the
     column padding so the line still sits 5px under the text, not 5px under the row padding. */
  .foot-links { flex-direction:column; flex-wrap:nowrap; align-items:flex-start; gap:var(--s1) 0; margin-block:0; }
  .foot-links .foot-eyebrow { display:block; }
  .foot-links a { padding:var(--s1) 0; --nav-pad:var(--s1); }
}

/* dashboard */
/* .dash-head IS GONE, 2026-08-22, and it is worth saying what it was doing wrong.

   It was `display:flex; justify-content:space-between`, built for a header with the label on
   the left and a "Signed in as ..." line on the right — that is what its `.dash-head .who`
   child rule was for. The who-line later moved into the admin bar, where it belongs, and
   nobody removed the flex. So the header was left with exactly two children, the eyebrow and
   the h1, and space-between did what it is supposed to do: it pushed the h1 to the far right
   edge of the page.

   That is why the books and the list had a right-aligned headline while /plan and /library had
   an ordinary left-aligned one — not a decision anybody made, the residue of one that was
   half-undone. Both pages now use plain .page-head like the rest of the admin, which already
   sets the identical width, margin and padding; the flex was the only difference and it was
   the bug. Nothing else used either rule. */
.dash { max-width:var(--w-page); margin-inline:auto; padding:var(--s2) var(--s5) var(--s8); }

/* Four numbers of equal rank are four rows of ONE panel, not four cards. */
.kpis { background:var(--surface-panel); border-top:1px solid var(--rule-surface-strong);
        border-bottom:1px solid var(--rule-surface-strong); margin:0 0 var(--s7); }
.kpi { display:grid; gap:var(--s1); padding:var(--s5); margin:0; }
.kpi + .kpi { border-top:1px solid var(--rule-surface); }
.kpi dt { font:600 var(--t-small)/1.3 var(--font-text); color:var(--text-muted); }
.kpi dd { margin:0; font:700 var(--t-h2)/1 var(--font-display); color:var(--text-strong);
          font-variant-numeric:tabular-nums; }
.kpi dd span { font:400 var(--t-small)/1 var(--font-text); color:var(--text-muted); margin-left:var(--s2); }
.kpi--alert { border-left:3px solid var(--accent); }
@media (min-width:900px){ .kpis{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); }
  .kpi + .kpi { border-top:0; border-left:1px solid var(--rule-surface); } }

.mini { list-style:none; margin:var(--s2) 0 0; padding:0; font-size:var(--t-small); }
.mini li { display:flex; justify-content:space-between; gap:var(--s3); padding:var(--s1) 0; color:var(--text-muted); }
.mini b { color:var(--text-strong); font-variant-numeric:tabular-nums; }

.floor, .table-block { margin-top:var(--s7); }
.floor h2, .table-block h2 { font-size:var(--t-lede); }
.days { font-size:var(--t-small); font-weight:500; color:var(--accent); margin-left:var(--s2); }
/* PHONE DENSITY. Three floor rows at three lines each — label, bar, formula — plus four KPI
   cards above them pushed the member table clean off a phone screen. Mark's screenshots showed
   him scrolling past the whole floor section to reach the one table with names in it.
   The note becomes a hover/long-press title on narrow screens: it explains the number, and an
   explanation is worth less than the thing it explains being visible at all. */
.bar-row { margin-bottom:var(--s5); }
@media (max-width:640px) {
  .bar-row { margin-bottom:var(--s4); }
  .bar-row .formula { display:none; }
  .kpi { padding:var(--s4) var(--s5); }
  .kpi .mini { margin-top:var(--s1); }
}
.bar-label { display:flex; justify-content:space-between; gap:var(--s3); flex-wrap:wrap;
             font-size:var(--t-small); margin-bottom:var(--s2); }
.bar-label span { color:var(--text-muted); font-variant-numeric:tabular-nums; }
.bar { height:10px; background:var(--surface-panel); border:1px solid var(--rule-surface-strong);
       border-radius:var(--radius-part); overflow:hidden; }
.bar-fill { height:100%; background:var(--accent); }
.formula { color:var(--text-muted); font-size:var(--t-small); }
.bar-row .formula { margin:var(--s1) 0 0; }

/* The horizontal scrollbar needs somewhere to live that is not on top of the table.

   macOS paints overlay scrollbars — they are drawn OVER the content rather than taking space
   from it — so on a table wide enough to scroll, the bar sat directly across the last row's
   bottom rule and, while dragging, across the last row's text. Windows and Linux reserve space
   instead, which put the bar hard against the final border with nothing between them. Both
   read as cramped; the overlay case reads as broken.

   padding-bottom is the fix rather than margin: padding is inside the scroll box, so the gap
   travels with the content and the bar gets a lane of its own. Applied to every .scroll and
   not only the ones that overflow, because which tables overflow depends on the viewport and
   an inconsistent gap between tables is more noticeable than a uniform one. */
.scroll { overflow-x:auto; -webkit-overflow-scrolling:touch; padding-bottom:var(--s4); }
.data-table { border-collapse:collapse; width:100%; font-size:var(--t-small); }
.data-table th, .data-table td { text-align:left; padding:var(--s3) var(--s4);
                                 border-bottom:1px solid var(--rule-surface); white-space:nowrap; }
.data-table th { font:600 var(--t-label)/1.3 var(--font-text); text-transform:uppercase;
                 letter-spacing:.08em; color:var(--text-muted); border-bottom:1px solid var(--rule-surface-strong); }
.data-table td { font-variant-numeric:tabular-nums; }

.activity { list-style:none; margin:0; padding:0; font-size:var(--t-small); }
.activity li { padding:var(--s3) 0; border-bottom:1px solid var(--rule-surface); color:var(--text-muted); }
.activity .when { display:inline-block; min-width:190px; color:var(--text); font-variant-numeric:tabular-nums; }
.activity .actor { font-size:var(--t-label); color:var(--text-muted); margin-left:var(--s2); }
.drift-line { display:block; font-size:var(--t-small); margin-top:var(--s1); }

/* pricing-comparison view — read on a phone, mid-conversation */
.plainlist { margin:0 0 1em; padding-left:1.1em; }
.plainlist li { margin-bottom:.6em; max-width:68ch; }
.callout { background:var(--surface-panel); border-left:3px solid var(--accent);
           padding:var(--s4) var(--s5); max-width:68ch; }
.data-table .good { color:var(--accent); font-weight:600; }
.data-table .warn { color:var(--warn-ink); font-weight:600; }
.data-table .bad  { color:var(--error-ink); font-weight:600; }
.data-table td { white-space:normal; vertical-align:top; }
/* Without a floor the columns crush instead of scrolling, and the first cell wraps one word
   per line at phone width. The .scroll wrapper is already there to catch the overflow. */
.doc .data-table { min-width:32rem; }

/* ── the arc timeline on /plan ─────────────────────────────────────────────
   Read DOWN, not across, so it stacks on a phone instead of scrolling sideways.
   Columns only from 640px, where "23 Jul" costs a seventh of the width rather
   than half of it. Same rule idiom as .data-table so the page still reads as
   one system. */
.arc { list-style:none; margin:0; padding:0; display:grid; gap:var(--s5); }
.arc > li { display:grid; gap:var(--s2); padding-bottom:var(--s5);
            border-bottom:1px solid var(--rule-surface); }
.arc > li:last-child { border-bottom:0; padding-bottom:0; }
.arc .when { margin:0; font:600 var(--t-label)/1 var(--font-text); letter-spacing:.08em;
             text-transform:uppercase; color:var(--text-muted); }
.arc .what { margin:0; }
.arc .changed { margin:0; color:var(--text-muted); }
@media (min-width:640px) {
  .arc > li { grid-template-columns:6rem 1fr 1fr; gap:var(--s5); align-items:start; }
  .arc .when { padding-top:.2em; }
}

/* ── the revision log on /plan ────────────────────────────────────────────────
   Reuses .arc for the rule-between-entries rhythm, and EXPLICITLY UNDOES the
   three-column layout .arc takes at 640px — an entry here is a stack of prose
   and a table, not a when/what/changed triplet, and inheriting those columns
   squeezed the before/after table into a third of the page.

   No colour carries meaning. "terms changed" is marked with weight and a rule
   rather than the brand gold, which fails AA on light (bin/check-contrast.js
   would catch it, but the point is not to write it in the first place). */
.revstamp { margin:var(--s3) 0 0; font:var(--t-small)/1.4 var(--font-text); color:var(--text-muted); }
.revstamp strong { color:var(--text-strong); }

.revlog > li { gap:var(--s3); }
@media (min-width:640px) { .revlog > li { grid-template-columns:1fr; gap:var(--s3); } }

.revhead { margin:0; display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--s2) var(--s3);
           font:600 var(--t-label)/1.2 var(--font-text); letter-spacing:.08em;
           text-transform:uppercase; }
.revnum { color:var(--text-strong); }
.revtag { border:1px solid var(--rule-strong); border-radius:var(--radius-part);
          padding:.2em .6em; color:var(--text-strong); letter-spacing:.06em; }
.revtitle { margin:0; font-size:var(--t-lede); }

/* The old wording is quieter than the new one but still fully legible — it is
   evidence, not a footnote, and a partner reading cold has to be able to read it. */
.revtable .was { color:var(--text-muted); }
.revwhy { display:block; margin-top:var(--s2); }

/* Answered items in the open-questions table. Marks the row without colour so it
   survives both themes and stays legible to anyone who can't distinguish it. */
.data-table td.done { position:relative; }
.data-table td.done::before { content:"✓ "; color:var(--text-muted); font-weight:700; }
.doc .data-table td:first-child { min-width:12rem; }
.data-table .row-mine > td { background:var(--surface-panel); }
.data-table .row-rule > td { border-top:2px solid var(--rule-surface-strong); }
.data-table .span-note { vertical-align:middle; }

/* ── admin chrome — the dashboard is an internal tool and wears the black ──── */
/* STICKY, 2026-08-22, at Mark's asking. These pages are long — the dashboard scrolls past
   several screens of numbers and the public home page is longer still — and a nav that leaves
   the viewport is a nav you scroll back up to find.

   z-index 40 puts it under the lightbox (50) and over everything else. That ordering is
   deliberate: an admin bar floating on top of a full-screen photograph would be the one piece
   of chrome that follows you into a modal.

   NO SHADOW AND NO BLUR. It carries its own dark ground in both themes, which is enough to
   separate it from anything that scrolls beneath — and on the admin pages it sits against the
   page's own dark surface, where a shadow would render as a smudge. */
/* ── THE DOCK ──────────────────────────────────────────────────────────────────────────────
   A floating pill at the bottom, not a bar at the top. The bar it replaces measured 196px on a
   390px phone — 30% of the viewport, four wrapped rows — and being sticky made that worse, not
   better: a third of the screen followed you down the page.

   z-index 40 keeps it under the lightbox (50) and over everything else, unchanged from the bar.
   Chrome that floats on top of a full-screen photograph is chrome that follows you into a modal.

   --dock-inset IS PINNED BY public/js/dock.js AND CARRIES env() AS ITS OWN FALLBACK. On iOS the
   bottom inset can report 0 after a sheet or a keyboard closes and not recover, which drops a
   fixed element into the home-indicator strip for the rest of the session. The script reads it
   once in a known-good state and only ever lets it grow. A browser that never runs the script
   still gets env() straight from the fallback.                                                */
.dock { position:fixed; z-index:40; left:50%; transform:translateX(-50%);
        bottom:calc(var(--s4) + var(--dock-inset, env(safe-area-inset-bottom, 0px)));
        /* width:max-content OR THE PILL LIES ABOUT ITS OWN SIZE. A shrink-to-fit fixed box
           settled at 645px on the first layout pass — the width its labels needed in the
           FALLBACK font — and never grew when the webfont swapped in and the same labels needed
           725px. The dark rounded background stayed 645 wide while "Sign out" rendered 62px
           outside it, in mid-air. max-content makes the box track its contents rather than a
           stale measurement. */
        width:max-content; max-width:calc(100% - var(--s6));
        display:flex; align-items:center; gap:0 var(--s4);
        padding:var(--s2) var(--s4);
        background:var(--surface-cut); color:var(--text-cut);
        border:1px solid var(--rule-cut); border-radius:999px;
        box-shadow:0 14px 32px -8px rgba(0,0,0,.45), 0 4px 10px -2px rgba(0,0,0,.28); }
/* The page has to be able to scroll clear of it, or the last line of every admin page sits
   under the pill forever. */
body:has(.dock) { padding-bottom:calc(var(--s9, 5rem) + var(--dock-inset, env(safe-area-inset-bottom, 0px))); }

.dock-mark { display:inline-flex; align-items:center; flex:0 0 auto; }
.dock-mark img { display:block; height:20px; width:auto; }
.dock-mark:focus-visible { outline:2px solid var(--focus); outline-offset:3px; }

/* flex:0 0 auto ON THE CONTAINER TOO, not just on the links inside it.
   Stopping the links from shrinking was half a fix and it hid the other half: .dock-nav was
   still a shrinkable flex item, so the pill hit its max-width, the NAV gave up 80px, and the
   links — now unshrinkable — simply overflowed their own container, straight through the theme
   toggle and "View site". The measurement that missed it is the lesson: the first pass was
   taken before the webfont loaded, when "The books" was 53px instead of 72px and the whole nav
   fitted in 290px instead of 370px. A layout measured on fallback metrics is a layout measured
   for a page nobody sees. */
.dock-nav { display:flex; flex-wrap:nowrap; flex:0 0 auto; gap:0 var(--s4); }
/* Transparent border in the resting state so the mark on the current item costs no height —
   without it the row jumps by two pixels the moment a page marks itself. */
/* flex:0 0 auto SO A LABEL CAN NEVER BE SQUEEZED INTO ITS NEIGHBOUR. Without it the links are
   shrinkable flex items: the pill hits its max-width, every link gives up a few pixels, and the
   five labels close on each other until they read as one word. Measured at 641px before the
   breakpoint moved — the nav lost 36px of its natural 290 and the gaps went from 16px to 7px.
   With this, a dock that does not fit overflows, which is a thing you can SEE and then fix. */
.dock-nav a, .dock-exit { flex:0 0 auto;
                          color:var(--text-cut); text-decoration:none; font-size:var(--t-small);
                          white-space:nowrap; padding:var(--s2) 0; position:relative; --nav-pad:var(--s2); }
.dock-nav a:hover, .dock-exit:hover { color:var(--accent-cut); }

.dock-rule { width:1px; align-self:stretch; background:var(--rule-cut); }
.dock-tail { display:flex; align-items:center; gap:0 var(--s3); flex:0 0 auto; }
.dock-who { font-size:var(--t-small); color:var(--text-cut-muted); white-space:nowrap; }
.dock form { display:inline; }

/* ── THE PHONE CONTROL AND THE SHEET — ONE BREAKPOINT, ONE BLOCK ───────────────────────────
   Everything that changes below 641px is in the single @media below. That is not tidiness: MHS
   collapsed its dock at 700px and revealed its hamburger at 768px, two rules written
   separately, and between those numbers an admin got both the full nav AND a hamburger opening
   the same items. Nobody owned the seam. One block means there is no second number to drift.

   820, AND IT TOOK THREE TRIES TO GET AN HONEST NUMBER.

   640 first, on the argument that app.css uses 640 seven times and 768 never — the number this
   stylesheet already agreed with. Then the pill was measured and 640 was wrong: the links were
   compressing between 641 and 677 to absorb a fit that was not there. So 700.

   Then Mark looked at it on a desktop and the theme toggle was sitting on top of "Library".
   BOTH earlier measurements had been taken before the webfont loaded. In the fallback font
   "The books" is 53px and the nav fits in 290; in the real one it is 72px and the nav needs
   370. Every number the first two passes rested on described a page nobody ever sees.

   The pill's real natural width is 725px and it needs 32px of margin, so 757 is the floor. 820
   leaves room for a longer signed-in name than "mark" — Mike's account renders here too —
   without another round of this. iPad portrait at 768 gets the sheet, which is correct: 768
   cannot hold this pill.

   THE LESSON, WRITTEN DOWN BECAUSE IT WILL HAPPEN AGAIN: measure layout with
   document.fonts.ready awaited, or do not trust the number. A fallback-metrics measurement is
   not a rough measurement, it is a measurement of the wrong thing.                            */
.dock-open { display:none; }
.dock-sheet[hidden] { display:none; }

@media (max-width: 820px) {
  /* The pill goes entirely. There is nothing to put in a compressed one — no save state, no
     role chip — so a phone gets ONE floating control rather than two. */
  .dock { display:none; }
  body:has(.dock) { padding-bottom:calc(var(--s8, 4rem) + var(--dock-inset, env(safe-area-inset-bottom, 0px))); }

  .dock-open { display:inline-flex; align-items:center; justify-content:center;
               gap:var(--s2);
               position:fixed; z-index:40;
               right:calc(var(--s4) + env(safe-area-inset-right, 0px));
               bottom:calc(var(--s4) + var(--dock-inset, env(safe-area-inset-bottom, 0px)));
               min-width:52px; height:52px; padding:0 var(--s4); border-radius:999px;
               background:var(--surface-cut); color:var(--text-cut);
               border:1px solid var(--rule-cut); font-size:1.3rem; cursor:pointer;
               box-shadow:0 14px 32px -8px rgba(0,0,0,.45); }
  .dock-open[aria-expanded="true"] { display:none; }
  .dock-open-label { font:600 var(--t-label)/1 var(--font-text); letter-spacing:.02em; }

  .dock-sheet { position:fixed; inset:0; z-index:41; overflow-y:auto;
                /* Solid, not 96%. The 4% let a serif headline ghost through behind the menu
                   items, and there is no blur here for it to sit on — this is a panel, not a
                   scrim over a photograph. */
                background:var(--surface-cut);
                padding:var(--s7) var(--s5)
                        calc(var(--s7) + var(--dock-inset, env(safe-area-inset-bottom, 0px))); }
  .dock-sheet-inner { max-width:26rem; margin-inline:auto; display:grid; gap:var(--s2); }
  .dock-sheet a, .dock-sheet button { color:var(--text-cut); text-decoration:none;
                                      font-size:var(--t-body); text-align:left;
                                      padding:var(--s3) var(--s4); border-radius:var(--radius-part);
                                      min-height:48px; display:flex; align-items:center; }
  .dock-sheet a[aria-current="page"] { color:var(--accent-cut); font-weight:600; }
  .dock-sheet a:hover, .dock-sheet button:hover { background:rgba(255,255,255,.06); }
  .dock-sheet-who { margin:0 0 var(--s3); font-size:var(--t-small); color:var(--text-cut-muted); }
  .dock-sheet-who strong { color:var(--text-cut); }
  .dock-sheet-head { margin:var(--s4) 0 0; font-size:var(--t-small);
                     color:var(--text-cut-muted); letter-spacing:.08em; text-transform:uppercase; }
  .dock-sheet-close { margin-top:var(--s4); border:1px solid var(--rule-cut);
                      justify-content:center; cursor:pointer; background:none; }
  .dock-sheet form { display:block; }
  .dock-sheet form button { width:100%; background:none; border:0; cursor:pointer; }
  /* The page behind must not scroll under an open sheet. */
  body.dock-sheet-open { overflow:hidden; }
}
/* ── THE TOUR BAND ─────────────────────────────────────────────────────────────────────────
   A dark surround in BOTH themes, which is the one place on this site that is deliberately not
   theme-following. Two reasons, and the second is the real one: it is the only separator
   between the hero and the board, and a video needs a dark ground to read as a picture rather
   than as a control embedded in a page. Cinemas are dark for the same reason posters are not.

   --surface-cut at :root is --black. .band-black's override to sand is scoped to that class
   and does not reach here, so this stays black on light and on dark alike. */
.band-tour { background:var(--screen); color:var(--text-cut); --focus:var(--led); }
.band-tour h2 { color:var(--text-cut-strong); }
.band-tour .lede { color:var(--text-cut); }
/* On DARK the page is already near-black, so the band would merge into the board beneath it.
   One hairline of the ceiling's own white, bottom only — the top edge has the hero's lighter
   surface above it and needs nothing. */
/* ON DARK THE BAND GOES THE OTHER WAY. #1E2126 is the page's own surface there, so a screen
   the same colour is no screen at all — it drops to --black instead, darker than its
   surroundings, which is the same relationship inverted. Plus a hairline at the bottom, or it
   merges into the board beneath it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .band-tour {
    --screen: var(--black); border-bottom:1px solid var(--rule-cut);
  }
}
:root[data-theme="dark"] .band-tour {
  --screen: var(--black); border-bottom:1px solid var(--rule-cut);
}
/* NOTE FOR WHOEVER ADDS COPY HERE: --text-cut-muted is 4.40:1 on this ground and fails AA.
   The band carries an h2 and a lede and nothing else for that reason. Fine print does not
   belong on a screen surround; if it ever does, it needs a colour that clears the bar. */

/* NO FRAME ON THE VIDEO ANY MORE. It carried a 1px surface-toned border when it sat on paper,
   which was correct there and is wrong here: on black the surround IS the frame, and a line
   around it only draws the box the dark ground exists to dissolve. */
.tour-video { display:block; width:100%; height:auto; aspect-ratio:16/9; background:var(--black);
              border:0; border-radius:var(--radius); }

/* THE CLICK-ANYWHERE TARGET, 2026-08-22. See public/js/tour.js for why it exists at all.
   The frame only needs to be a positioning context; the button is added by script, so nothing
   here renders unless the script ran. */
.tour-frame { position:relative; }
/* THE SCRIM, 2026-08-22. Mark, looking at it at rest: "Seems like there should be a dimming
   overlay between the clip and the play button, to make it pop."

   The badge alone was not enough and his screenshot showed why mine had not: the poster's
   centre is the building's dark doorway, so a 74%-opaque near-black lozenge sat on near-black
   and disappeared, leaving a white triangle floating on a photograph. A badge whose legibility
   depends on what happens to be behind it is not a badge, and the frame under it changes the
   day anybody re-cuts the film.

   TWO LAYERS, AND THE SECOND IS THE POINT. A flat wash would dim the whole picture evenly and
   the poster is doing the selling. The radial sits under the badge and falls off to nothing by
   about a third of the way out, so the contrast is bought exactly where it is spent; the linear
   is a much lighter overall veil that also says "this is a still, not a paused film".

   IT FADES OUT BEFORE THE BOTTOM. The button stops at 85% so the native control bar stays
   clickable, and a wash that stopped dead at that line would draw a horizontal edge across the
   picture. It reaches zero at 72% instead, above where the button ends, so there is no seam. */
.tour-play  { position:absolute; inset:0 0 15% 0; width:100%; border:0; padding:0;
              cursor:pointer; border-radius:var(--radius);
              background:
                radial-gradient(circle at 50% 50%, rgba(14,15,18,.34) 0, rgba(14,15,18,0) 40%),
                linear-gradient(rgba(14,15,18,.20) 0, rgba(14,15,18,.20) 55%, rgba(14,15,18,0) 72%);
              transition:opacity var(--dur) var(--ease); }
/* On hover the whole scrim lightens rather than deepening: the badge is about to go amber and
   carry the contrast itself, so the picture gets to come back up as the cursor arrives. */
.tour-play:hover, .tour-play:focus-visible { opacity:.82; }

/* THE BADGE — Mark: "a YouTube-esque play button overlay so that it's obvious that it's video."
   He is right that it was not obvious. Chrome draws no centre glyph on a paused <video>; it
   puts a control bar at the bottom and leaves the picture bare, so the poster read as a
   photograph of a building rather than a film of one. Safari does draw one, which is exactly
   the kind of difference you cannot design around by trusting the browser.

   TWO PSEUDO-ELEMENTS, NO NEW MARKUP AND NO innerHTML. ::before is the lozenge, ::after is
   the triangle, both on the button that already exists — so the badge is created and destroyed
   with it, and there is no second element to remember to remove when the film starts.

   NOT YOUTUBE'S RED. The shape is the borrowed part, because that is what makes it read
   instantly as "video"; the colour is the site's. It sits on a dark screen band, so it starts
   as the ground's own near-black at 74% and turns amber under the cursor — --accent-cut, which
   on this band is the badge amber, since the theme-following override is scoped to .band-black
   and does not reach here. */
.tour-play::before,
.tour-play::after { content:''; position:absolute; left:50%; top:50%; pointer-events:none; }
/* THE BADGE IS AMBER AT REST, and it took two goes to get here. It was a near-black lozenge
   first, which is what YouTube does — and YouTube gets away with it because its thumbnails are
   mostly mid-tone. This poster is the building's dark navy doorway dead centre, so a near-black
   badge on near-black vanished and left a white triangle floating on a photograph.
   ADDING THE SCRIM DID NOT FIX IT AND COULD NOT HAVE. A dimming layer lowers the badge and its
   surroundings together, so where the frame is already dark the relative contrast barely moves.
   That was the wrong lever, pulled because it was the one being asked for.
   Amber solves it for any frame, which matters because the poster changes the day the film is
   re-cut. It is also the site's own accent doing a job rather than a borrowed grey.
   AMBER BY NAME, NOT THROUGH --accent-cut (2026-09-07). The first cut used the cut-surface token,
   which is amber inside the tour band and the EMBER everywhere else, so the day the sheet reached
   the reels on the paper ground the badge came out dark brown (#9B4C22, measured). The badge
   sits on a photograph, never on the page ground, so the ground's token is the wrong question. */
.tour-play::before { width:76px; height:54px; margin:-27px 0 0 -38px; border-radius:14px;
                     background:var(--amber);
                     transition:background-color var(--dur) var(--ease),
                                transform var(--dur) var(--ease); }
/* The triangle is nudged right of centre on purpose. Its visual weight sits about a third of
   the way from the flat edge, so a mathematically centred one looks left-heavy — the oldest
   correction in a play button and the reason a badly drawn one feels wrong without saying why. */
.tour-play::after  { margin:-13px 0 0 -8px; border-style:solid;
                     border-width:13px 0 13px 21px;
                     border-color:transparent transparent transparent var(--black);
                     transition:border-left-color var(--dur) var(--ease); }
/* Hover deepens rather than switches colour. The badge already carries the contrast at rest,
   so the cursor state only has to acknowledge the pointer — a hue change here would read as a
   different control rather than the same one under the mouse. */
.tour-play:hover::before,
.tour-play:focus-visible::before { background:var(--amber-hover); }
/* Smaller on a phone, where the video is a third of the width and a 76px lozenge covers a
   meaningful part of the picture it is advertising. */
@media (max-width:640px){
  .tour-play::before { width:60px; height:42px; margin:-21px 0 0 -30px; border-radius:11px; }
  .tour-play::after  { margin:-10px 0 0 -6px; border-width:10px 0 10px 16px; }
}
/* INVISIBLE BUT NOT UNFINDABLE. There is no hover styling on purpose — a tint appearing over
   the poster would read as a broken image or a lightbox, and the poster is doing the selling.
   The focus ring is the exception and is deliberately loud: for a keyboard user this is the
   only evidence the control is here at all. --focus on .band-tour is the ceiling's own white. */
.tour-play:focus-visible { outline:3px solid var(--focus); outline-offset:-6px; }
/* While tour.js holds the controls back (see the script), there is no control bar to keep clear
   of, so the sheet covers the whole frame and no strip of the poster is a dead tap. */
.tour-frame.no-controls .tour-play { inset:0; }
/* iOS SAFARI PAINTS ITS OWN START BUTTON in the middle of any paused <video controls>, so on a
   phone the amber badge sat over a grey one (owner, 2026-09-07). Desktop browsers draw none,
   which is why it never showed on a Mac. WebKit exposes that control as a pseudo-element; off
   on our videos only, and only where the sheet is doing the job. The native control BAR is
   untouched, and a tap on the poster still starts the film through the sheet. !important is
   the one way past the user-agent stylesheet here, and it is scoped to these two classes. */
.tour-video::-webkit-media-controls-start-playback-button,
.reel-video::-webkit-media-controls-start-playback-button { display:none !important; -webkit-appearance:none; }


/* ── THE ANSWER, AND THEN THE EVIDENCE ─────────────────────────────────────────────────────
   The traffic panel's lead sentence and the two tallies under it, 2026-08-22.

   .lead-stat is set at reading size, not at figure size. It is a SENTENCE — the temptation is
   to make the numbers inside it big, which turns it back into a row of figures with words
   stuck between them and undoes the point. The bold numerals are the only emphasis, and they
   are emphasis inside prose. */
.lead-stat { margin:0 0 var(--s5); max-width:56ch;
             font-size:var(--t-lede); line-height:1.5; color:var(--text); }
.lead-stat strong { font-weight:700; color:var(--text-strong);
                    font-variant-numeric:tabular-nums lining-nums; }

/* Two short tallies side by side, each a name and a count. This replaced two run-on lines of
   comma-separated pairs — the same information, and the hardest thing on the panel to read,
   because a list set as a sentence has to be parsed rather than scanned. */
.evidence { display:grid; gap:var(--s5) var(--s7); margin:0 0 var(--s5); }
@media (min-width:640px){ .evidence { grid-template-columns:repeat(2, minmax(0,1fr)); } }
.evidence h3 { margin:0 0 var(--s2); font:600 var(--t-label)/1.3 var(--font-text);
               letter-spacing:.1em; text-transform:uppercase; color:var(--text-muted); }
.tally { list-style:none; margin:0; padding:0; }
/* A hairline per row rather than zebra fill: this sits on the page surface, and the system
   holds a row with a line. The last one is dropped so the list ends on content, not on a rule
   that reads as the start of something else. */
.tally li { display:flex; align-items:baseline; justify-content:space-between; gap:var(--s4);
            padding:var(--s2) 0; border-bottom:1px solid var(--rule-surface); }
.tally li:last-child { border-bottom:0; }
.tally span { min-width:0; overflow-wrap:anywhere; }
.tally b { font-weight:700; font-variant-numeric:tabular-nums lining-nums; color:var(--text-strong); }

/* ── THE LIST WORKBENCH ────────────────────────────────────────────────────────────────────
   Added 2026-08-22 with the list's CRUD. Everything here is built from tokens that already
   exist; no new colour enters the system.

   NOTE ON .linkish: it is coloured --accent-cut, which is correct where it was born (the admin
   bar and the footer, both dark grounds) and wrong here. The dashboard is a SURFACE — warm
   paper in light mode — and --accent-cut is amber, which is the one thing the palette says
   outright fails AA on warm paper. So the table's own controls take --accent (ember on light,
   amber on dark), which is the surface-side accent and exists for exactly this. */
.list-tools { display:flex; align-items:center; gap:var(--s4); flex-wrap:wrap; margin:0 0 var(--s4); }
.btn-small { padding:var(--s2) var(--s4); min-height:0; font-size:var(--t-small); }

.list-table .linkish { color:var(--accent); font-size:var(--t-small); }
.list-table .linkish:hover { color:var(--text-strong); }
/* Destructive, and it has to LOOK different from the reversible action beside it — this is
   the only control on the page with no undo. Error ink, not a red box: the system holds
   things with a line and a colour, never a filled alarm on a table row. */
.list-table .linkish.danger { color:var(--error-ink); }
.list-table .linkish.danger:hover { color:var(--error-ink); text-decoration:underline; }

/* An unsubscribed row stays legible — it is a record, not a tombstone — but it should never
   be the thing the eye lands on when the question is "who can I write to". */
.list-table .row-off td { color:var(--text-muted); }

.pill { display:inline-block; padding:2px var(--s2); font-size:var(--t-label); font-weight:600;
        letter-spacing:.04em; border:1px solid var(--rule-surface-strong); border-radius:999px;
        color:var(--text-muted); white-space:nowrap; }
.pill-on { border-color:var(--accent); color:var(--accent); }

.note-form { display:flex; gap:var(--s2); align-items:center; }
.note-form input { flex:1 1 auto; min-width:0; padding:var(--s2) var(--s3); min-height:36px;
                   background:var(--surface-field); border:1px solid var(--rule-surface);
                   border-radius:var(--radius-part); font-size:var(--t-small); }
.note-form input:focus-visible { background:var(--surface-field-focus); }

.row-actions { display:flex; gap:var(--s4); align-items:center; white-space:nowrap; }
/* Stacked-table mode puts every cell on its own full-width row, so the actions need to sit
   apart from the note field above them rather than butting against it. */
@media (max-width:900px){
  .row-actions { padding-top:var(--s2); }
  .list-table .note-form { max-width:none; }
}

.linkish { background: none; border: 0; padding: 0; color: var(--accent-cut);
           text-decoration: underline; cursor: pointer; font-size: var(--t-small); }
.linkish:hover { color: var(--text-cut-strong); }

.req { font-weight: 400; color: var(--text-muted); }
.vh { position: absolute; width: 1px; height: 1px; clip-path: inset(50%); overflow: hidden; }
.kpi .mini { margin-top: var(--s2); }

/* ── THE PLAN PAGE'S FOLDED SHAPE, 2026-08-15 ──────────────────────────────────
   Mark's call after revision 13: nineteen same-looking sections and a mile of scroll had
   made the page hard to track even for its authors. The current state stays open at the top;
   everything else folds into six numbered <details> groups behind a contents card. Native
   details — no framework, keyboard- and reader-correct for free. /js/plan.js adds the two
   things CSS cannot: hash-targets inside closed groups, and open-everything for print. */

.contents { border:1px solid var(--oak); border-radius:10px; padding:var(--s4) var(--s5);
  margin:var(--s5) 0; }
.contents-head { margin:0 0 var(--s3); letter-spacing:.18em; font-size:.8rem;
  text-transform:uppercase; color:var(--text-muted); }
.contents ol { list-style:none; margin:0; padding:0; }
.contents li + li { border-top:1px solid color-mix(in srgb, var(--oak) 35%, transparent); }
.contents a { display:flex; gap:var(--s3); align-items:baseline; padding:var(--s2) 0;
  text-decoration:none; color:var(--text); }
.contents a:hover .clabel, .contents a:focus-visible .clabel { text-decoration:underline; }

.cnum { font-size:.8rem; letter-spacing:.08em; color:var(--oak-dark); font-weight:700;
  min-width:2ch; }
.clabel { display:block; font-weight:600; color:var(--text-strong); }
.csub { display:block; font-weight:400; font-size:.85rem; color:var(--text-muted); }

details.acc { border:1px solid color-mix(in srgb, var(--oak) 45%, transparent);
  border-radius:10px; margin:var(--s4) 0; max-width:var(--w-doc); }
details.acc > summary { display:flex; gap:var(--s3); align-items:baseline; cursor:pointer;
  padding:var(--s4) var(--s5); list-style:none; }
details.acc > summary::-webkit-details-marker { display:none; }
details.acc > summary::after { content:""; margin-left:auto; align-self:center;
  width:.55em; height:.55em; border-right:2px solid var(--oak-dark);
  border-bottom:2px solid var(--oak-dark); transform:rotate(45deg); transition:transform .15s;
  flex-shrink:0; }
details.acc[open] > summary::after { transform:rotate(225deg); }
details.acc[open] > summary { border-bottom:1px solid color-mix(in srgb, var(--oak) 35%, transparent); }
details.acc > :not(summary) { margin-inline:var(--s5); }
details.acc > :not(summary):first-of-type { margin-top:var(--s4); }
details.acc > :not(summary):last-child { margin-bottom:var(--s5); }
/* Group content keeps the page's measure even though the card is wider. */
details.acc h2 { margin-top:1.4em; }

/* The ledger's revisions, each its own fold. Newest ships open (the template says so). */
details.rev { border-top:2px solid var(--oak); padding-top:var(--s3); }
details.rev > summary { cursor:pointer; list-style:none; }
details.rev > summary::-webkit-details-marker { display:none; }
details.rev > summary .revtitle { display:block; margin-top:2px; }
details.rev > summary::before { content:"+"; float:right; color:var(--oak-dark);
  font-weight:700; font-size:1.1em; }
details.rev[open] > summary::before { content:"–"; }

/* was/now as a stacked pair: two columns only where two columns fit. Replaces the
   three-column .revtable, which forced a horizontal scroll at phone width. */
.chg { margin:var(--s4) 0; }
.chg-what { margin:0 0 var(--s2); }
.chg-pair { display:grid; gap:var(--s3); }
@media (min-width: 640px) { .chg-pair { grid-template-columns:1fr 1fr; } }
.chg-label { display:block; letter-spacing:.14em; font-size:.72rem; text-transform:uppercase;
  color:var(--text-muted); margin-bottom:2px; }
.chg-was { border-left:3px solid color-mix(in srgb, var(--oak) 45%, transparent);
  padding-left:var(--s3); color:var(--text-muted); }
.chg-now { border-left:3px solid var(--oak-dark); padding-left:var(--s3); }
.chg p { margin:0; }
.chg .revwhy { margin-top:var(--s2); font-size:.9rem; }

/* Print is the archive: /js/plan.js opens every fold on beforeprint; these keep the
   chrome quiet on paper. */
@media print {
  details.acc, details.rev { border:none; }
  details.acc > summary::after, details.rev > summary::before { display:none; }
  .contents { display:none; }
}

/* The plan page's state card — replaces three stacked banners, 2026-08-15. */
.state { border:1px solid var(--oak); border-radius:10px; padding:var(--s4) var(--s5);
  margin:var(--s5) 0; max-width:var(--w-doc); }
.state-head { display:flex; gap:var(--s3); align-items:center; flex-wrap:wrap; margin:0 0 var(--s3); }
.state .chip { font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; font-weight:700;
  color:var(--oak-dark); border:1px solid var(--oak); border-radius:999px; padding:2px 10px; }
.state-line { margin:0 0 var(--s3); }
.board-facts { list-style:none; margin:0 0 var(--s4); padding:0; display:grid; gap:var(--s2); }
.board-facts li { display:flex; gap:var(--s3); align-items:baseline;
  padding-bottom:var(--s2); border-bottom:1px solid color-mix(in srgb, var(--oak) 30%, transparent); }
.board-facts li:last-child { border-bottom:none; padding-bottom:0; }
.bf-n { font-weight:700; color:var(--text-strong); min-width:4.5ch; text-align:right;
  font-variant-numeric:tabular-nums; }
.bf-l { color:var(--text); }
.bf-s { display:block; font-size:.85rem; color:var(--text-muted); }
.bf-total .bf-n, .bf-total .bf-l { color:var(--oak-dark); font-weight:700; }
.state-note > summary { cursor:pointer; font-size:.9rem; color:var(--text-muted);
  list-style:none; }
.state-note > summary::-webkit-details-marker { display:none; }
.state-note > summary::before { content:"+ "; color:var(--oak-dark); font-weight:700; }
.state-note[open] > summary::before { content:"– "; }
.state-note p { font-size:.95rem; }

/* The brand sheet, 2026-08-15. Marks on their own grounds, swatches you can read a hex off. */
.marks, .swatches, ul.plain { list-style:none; margin:0 0 var(--s6); padding:0; }
ul.plain li { margin-bottom:var(--s2); }
.marks li { display:grid; gap:var(--s4); margin-bottom:var(--s5);
  padding-bottom:var(--s5); border-bottom:1px solid var(--rule); }
@media (min-width: 620px) { .marks li { grid-template-columns:220px 1fr; align-items:start; } }
.mark-stage { display:grid; place-items:center; padding:var(--s4); background:var(--sand);
  min-height:120px; }
.mark-stage.on-dark { background:var(--black); }
.mark-stage img { max-width:100%; max-height:150px; height:auto; }
.mark-name { display:flex; gap:var(--s3); align-items:center; flex-wrap:wrap; margin:0 0 var(--s2); }
.mark-meta p { margin:0 0 var(--s2); }
.mark-meta .chip { font-size:.68rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--oak-dark); border:1px solid var(--oak); border-radius:999px; padding:1px 8px; }

.swatches li { display:flex; gap:var(--s4); align-items:flex-start; margin-bottom:var(--s3); }
.chipcolor { width:44px; height:44px; flex-shrink:0; border:1px solid var(--rule-strong); }
.sw-meta code { margin-left:var(--s2); font-size:.85rem; color:var(--text-muted); }
.sw-meta strong { display:inline; }


/* Library entry lists — domains, pages, ownership. */
.entries { list-style:none; margin:0 0 var(--s6); padding:0; }
.entries > li { border-top:1px solid var(--rule); padding:var(--s4) 0; }
.entry-head { display:flex; gap:var(--s3); align-items:center; flex-wrap:wrap; margin:0 0 var(--s2); }
.entries p { margin:0 0 var(--s2); }
.entry-head .chip { font-size:.68rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--oak-dark); border:1px solid var(--oak); border-radius:999px; padding:1px 8px;
  white-space:nowrap; }
.entry-facts { display:grid; gap:2px var(--s4); margin:var(--s3) 0 0; }
@media (min-width: 560px) { .entry-facts { grid-template-columns:max-content 1fr; } }
.entry-facts dt { font-size:.72rem; letter-spacing:.12em; text-transform:uppercase;
  color:var(--text-muted); padding-top:3px; }
.entry-facts dd { margin:0 0 var(--s2); }
.mark-meta .dl a { margin-right:var(--s4); }
.mark-meta .dl { display:flex; gap:var(--s4); flex-wrap:wrap; align-items:baseline; }
.mark-meta .dl a { margin:0; }
.mark-meta .dl-main { font-weight:600; }
.mark-meta .dl-alt { font-size:.85rem; color:var(--text-muted); }

/* The library's word list. Stacks on a phone, pairs up where there is room. */
.words { margin:0 0 var(--s6); }
.words dt { font-weight:700; color:var(--text-strong); }
.words dd { margin:0 0 var(--s4); }
@media (min-width: 620px) {
  .words { display:grid; grid-template-columns:minmax(8rem, max-content) 1fr;
    gap:var(--s3) var(--s5); align-items:baseline; }
  .words dt { grid-column:1; }
  .words dd { grid-column:2; margin:0; }
}
@media (max-width: 619px) {
  .words dt { margin-bottom:2px; }
  .words dd { padding-bottom:var(--s3);
    border-bottom:1px solid color-mix(in srgb, var(--oak) 30%, transparent); }
}

/* ── PHONE LEGIBILITY, 2026-08-15 ──────────────────────────────────────────────
   Mark reads these pages on an iPhone and Mike does too, so 393px is the size that
   matters, not the desktop. Measured there, the secondary type was landing between
   10.9px and 13.6px: hex codes you are meant to copy, the DNS and registrar labels,
   the sub-lines under every product and mark, and the chips. Small type is a fine
   hierarchy signal on a 27-inch display and a wall on a phone. These raise the floor
   to about 15px for anything carrying content and 12.5px for pure labels, and give
   the download links a real tap target. Desktop is untouched. */
@media (max-width: 640px) {
  .bf-s, .csub, .dl-alt, .sw-meta code, .entry-facts dd, .mark-meta .muted { font-size:.95rem; }
  .entry-facts dt { font-size:.82rem; }
  .chip, .mark-name .chip, .entry-head .chip { font-size:.78rem; padding:2px 9px; }
  .eyebrow, .contents-head, .chg-label { font-size:.85rem; }
  .mark-meta .dl a { display:inline-block; padding:7px 0; }
  .contents a { padding:var(--s3) 0; }
  .swatches li { gap:var(--s3); }
  .chipcolor { width:52px; height:52px; }
}

/* Scroll affordance for the plan page's comparison tables (see public/js/plan.js). */
.scroll-hint { margin:0 0 var(--s2); font-size:.85rem; color:var(--text-muted); }
@media (min-width: 760px) { .scroll-hint { display:none; } }
.scroll.is-scrollable { position:relative; }
.scroll.is-scrollable::after { content:""; position:absolute; top:0; right:0; bottom:var(--s4);
  width:38px; pointer-events:none; transition:opacity .15s;
  background:linear-gradient(to right, transparent, var(--surface)); }
.scroll.is-scrollable.at-end::after { opacity:0; }
@media (max-width: 640px) { .revnum, .revtag { font-size:.82rem; } }
@media print { .scroll-hint { display:none; } }

/* The theme control. Text, not an icon: a sun or moon says "toggle" but never says which way
   you are going, and this button also has to report the state it is in. */
.theme-toggle { background:none; border:1px solid currentColor; color:inherit;
  border-radius:999px; width:44px; height:44px; display:inline-flex; align-items:center;
  justify-content:center; padding:0; cursor:pointer; opacity:.7; flex-shrink:0; }
.theme-toggle:hover { opacity:1; }
.theme-toggle:focus-visible { outline:2px solid var(--focus); outline-offset:2px; }
/* THE MENU BUTTON: the theme toggle's own outline, with a word. display:none at rest so the
   desktop never sees it even once the script unhides it; the phone tier turns it on. Bars are
   drawn in currentColor so the button follows whichever ground and theme it sits on. */
.menu-toggle { display:none; align-items:center; gap:10px; height:44px; padding:0 14px 0 12px;
               background:none; border:1px solid currentColor; border-radius:999px; color:inherit;
               font:600 var(--t-small)/1 var(--font-text); cursor:pointer; opacity:.8; flex-shrink:0; }
.menu-toggle:hover { opacity:1; }
.menu-toggle:focus-visible { outline:2px solid var(--focus); outline-offset:2px; }
.menu-toggle .bars { display:inline-block; width:18px; height:2px; background:currentColor;
                     box-shadow:0 -6px 0 currentColor, 0 6px 0 currentColor; }
.nav.is-open .menu-toggle .bars { background:transparent; box-shadow:none; position:relative; width:18px; }
.nav.is-open .menu-toggle .bars::before, .nav.is-open .menu-toggle .bars::after {
  content:''; position:absolute; left:0; top:0; width:18px; height:2px; background:currentColor; }
.nav.is-open .menu-toggle .bars::before { transform:rotate(45deg); }
.nav.is-open .menu-toggle .bars::after { transform:rotate(-45deg); }
/* The header is a brand on the left and the control on the right; it was a bare block until
   the control arrived. */
.nav { display:flex; align-items:center; justify-content:space-between; gap:var(--s4); }
/* In the signed-in bar the control sits with the other chrome, quieter than the nav links. */
.dock .theme-toggle { width:30px; height:30px; opacity:.6; }
.dock .theme-toggle:hover { opacity:1; }
/* Announced to a screen reader, never shown — the icon alone cannot say which way it goes. */
.vh { position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap; }
.theme-toggle:focus-visible { outline:2px solid var(--focus); outline-offset:2px; }

/* The house credit. Same lockup on every Rendered-built site. */
.site-foot .by { margin-top:var(--s3); }
.rendered-lockup { display:inline-flex; align-items:center; gap:.4em; text-decoration:none;
  color:var(--text-muted); }
.rendered-lockup:hover { color:var(--accent); }
.rendered-prism { width:1.1em; height:1.1em; flex-shrink:0; }
.rendered-verb { font-weight:400; }
.rendered-wordmark { font-weight:700; letter-spacing:.15em; text-transform:uppercase; font-size:.85em; }

/* Dashboard traffic figures — the five numbers that answer "how is it going". */
.figures { display:grid; grid-template-columns:repeat(auto-fit,minmax(8.5rem,1fr));
  gap:var(--s4); margin:0 0 var(--s5); }
.figures dl { margin:0; }
.figures dt { font-size:.78rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--text-muted); margin-bottom:2px; }
.figures dd { margin:0; font:700 1.6rem/1.1 var(--font-display); color:var(--text-strong); }
.sub-head { margin-top:var(--s6); }
.row-muted td { opacity:.45; }
.row-muted code { text-decoration:line-through; }

/* ── DASHBOARD TABLES ON A PHONE, 2026-08-15 ───────────────────────────────────
   Mark reads this on an iPhone and the tables were the worst thing on it: five and six
   columns behind a 32rem minimum, so every row had to be dragged sideways to finish, and
   "Aug 15" and "11:18 AM" both wrapped onto two lines inside their cells.

   Only .stacked tables do this. The plan page's comparison tables keep their columns on
   purpose — there the reader is scanning ACROSS, price against price, and stacking would
   destroy the comparison. Here each row is one record and reads perfectly as a block.

   The labels come from data-label on each cell rather than from the header row, because a
   stacked cell has no header above it any more. */
@media (max-width: 640px) {
  .stacked, .stacked tbody, .stacked tr, .stacked td { display:block; width:100%; }
  /* BOTH SELECTORS, AND THE SECOND ONE IS THE WHOLE BUG. `.stacked` alone is (0,1,0) and
     `.doc .data-table { min-width:32rem }` above is (0,2,0). A media query adds NO specificity,
     so inside a .doc page the reset lost and the table kept a 512px floor while every other
     stacked rule applied — it went to display:block, dropped its header row, and then sat 512px
     wide inside a 292px column. The page scrolled sideways by 171px at 390px wide.

     It hid for as long as it did because the only stacked tables lived on .dash pages, where
     the 32rem rule never applied. Moving the traffic panel from /list into /library/traffic on
     2026-08-24 carried identical markup into a .doc page and the latent conflict became real.
     Nothing about the table changed. Its container did.

     bin/smoke.js now compares the two specificities numerically rather than trusting this
     comment, because the comment above this media block already claimed the problem was solved. */
  .stacked, .doc .stacked { min-width:0; }
  .stacked thead { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  .stacked tr { padding:var(--s4) 0; border-bottom:1px solid var(--rule-surface); }
  .stacked tr:last-child { border-bottom:none; }
  .stacked td { display:flex; gap:var(--s4); align-items:baseline; justify-content:space-between;
    padding:2px 0; border:none; text-align:right; }
  .stacked td::before { content:attr(data-label); text-align:left; flex:0 0 auto;
    font-size:.78rem; letter-spacing:.08em; text-transform:uppercase; color:var(--text-muted); }
  /* A cell with no label — the action button — gets the whole width to itself. */
  .stacked td[data-label=""] { justify-content:flex-start; padding-top:var(--s3); }
  .stacked td[data-label=""]::before { content:none; }
  /* The first cell of each record is its title, not a labelled field. */
  .stacked td:first-child { font-weight:600; font-size:1.05rem; padding-bottom:var(--s2); }
  .stacked td:first-child::before { align-self:center; }
  .scroll:has(.stacked) { overflow-x:visible; padding-bottom:0; }
}
/* .nowrap HAD NEVER WORKED INSIDE A TABLE, and it is used on nothing else.
   `.data-table td { white-space:normal }` (above) is specificity 0,1,1 against this rule's
   0,1,0, so every `class="nowrap"` in every view has been decorative since the day it was
   written — the cells wrapped, and the class sat there looking like it was doing something.
   Found 2026-08-22 by a date wrapping mid-timestamp in the list, then by enumerating every
   declaration that sets white-space rather than trusting the one that was supposed to win. */
.nowrap { white-space:nowrap; }
.data-table td.nowrap { white-space:nowrap; }

/* The dashboard's folded sections use the same card as the plan page's, minus the numbering.
   Without a .cnum the summary's flex row needs the label to take the width so the chevron
   still lands on the right. */
.dash .acc > summary .clabel, .table-block .acc > summary .clabel { flex:1 1 auto; }
.table-block details.acc { margin-top:var(--s5); }


/* MESSAGES — the contact log, 2026-08-22. It was a table for four hours; see the note in
   views/messages.ejs. A table sets up parallel fields for comparing rows, and the field that
   matters here is a paragraph meant to be read.

   NO CARD, NO BORDER, NO SHADOW. A rule between entries and generous space — this system
   frames with bands and rules, and a stack of bordered boxes is the thing it refuses
   everywhere else. */
.msg-log { list-style:none; margin:var(--s6) 0 0; padding:0; max-width:var(--w-text); }
.msg-log > li { border-top:1px solid var(--rule-surface); }
.msg-log > li:last-child { border-bottom:1px solid var(--rule-surface); }

/* THE CLOSED ROW. Three columns: who, a peek at what they said, when. The peek is the reason
   the row can stay one line — a name and a date alone tell you nothing about whether this is
   the message you are looking for. */
.msg-sum { display:grid; grid-template-columns:auto 1fr auto; gap:var(--s3);
           align-items:baseline; padding:var(--s4) 0; cursor:pointer;
           list-style:none; }
/* Both spellings: ::-webkit-details-marker for Safari, ::marker for everyone else. The default
   triangle is replaced below with one that sits on the baseline of the name. */
.msg-sum::-webkit-details-marker { display:none; }
.msg-sum::marker { content:''; }
.msg-sum:hover .msg-sum-who { color:var(--accent); }
.msg-sum:focus-visible { outline:2px solid var(--focus); outline-offset:-2px; }
.msg-sum-who  { font-weight:600; }
/* min-width:0 lets the peek actually shrink inside the grid track — without it the text
   refuses to ellipsis and pushes the date off the row instead. */
.msg-sum-peek { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
                color:var(--text-muted); font-size:var(--t-small); }
.msg-sum-when { color:var(--text-muted); font-size:var(--t-small); white-space:nowrap; }
/* The peek is the first thing to go when there is no room for three columns — a name and a
   date still identify a row; half a sentence does not. */
@media (max-width:640px){
  .msg-sum { grid-template-columns:1fr auto; }
  .msg-sum-peek { display:none; }
}
/* The marker: a caret drawn in CSS so it can rotate, rather than a glyph that depends on a
   font having it. */
.msg-sum-who::before { content:''; display:inline-block; width:.42em; height:.42em;
                       margin-right:var(--s3); vertical-align:.06em;
                       border-right:2px solid var(--text-muted);
                       border-bottom:2px solid var(--text-muted);
                       transform:rotate(-45deg); transition:transform var(--dur) var(--ease); }
details[open] > .msg-sum .msg-sum-who::before { transform:rotate(45deg); }

/* THE OPEN ROW. */
.msg-open { padding:0 0 var(--s5); }
.msg-from { margin:0; display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4);
            align-items:baseline; font-size:var(--t-small); }
.msg-from a { color:var(--text-muted); text-decoration:underline; text-underline-offset:3px;
              text-decoration-color:var(--rule); }
.msg-from a:hover { color:var(--accent); }
.msg-when { color:var(--text-muted); }
/* pre-wrap because somebody's paragraph breaks are theirs; 46ch because this is reading copy
   and it is the measure the rest of the site uses for it. */
.msg-text { margin:var(--s3) 0 0; white-space:pre-wrap; max-width:46ch; }
.msg-act  { margin:var(--s4) 0 0; }
/* The delivery flag is the only loud thing on this page, and only a machine sets it. Its row
   is forced open in the template for the same reason. */
.msg-flag { margin-right:var(--s3); color:var(--accent); font-weight:600; }
/* Junk stays legible — it is hidden by default, and anyone looking at it is checking whether
   the call was right. Dimming it past reading would defeat the only reason to show it. */
.msg--junk { opacity:.6; }

/* ── the claim page, 2026-08-26 ───────────────────────────────────────────────
   The last step of the sale: photograph, blurb, then the three charter promises as a short
   ruled list above the form. Same rules as .entries so it reads as one system. */
.claim .promises { list-style:none; margin:0 0 var(--s6); padding:0; }
.claim .promises li { padding:var(--s3) 0; border-top:1px solid var(--rule); }
.claim .promises li:last-child { border-bottom:1px solid var(--rule); }
.claim .small { font-size:var(--t-small); margin-top:var(--s3); }


/* ── /thanks v2, 2026-08-28 — the page after the card is charged ─────────────────────────
   Mock: docs/mocks/thanks-v2-2026-08-28.html. Receipt as a two-column definition list on the
   page's rules, the place in line in display type, the next steps as a numbered list with the
   counter drawn as a coin. No card, no box — rules and space, like everything else here. */
.tk-receipt { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule);
              padding:var(--s4) 0; margin:var(--s5) 0; display:grid;
              grid-template-columns:1fr auto; gap:var(--s2) var(--s5); align-items:baseline; }
.tk-receipt dt { color:var(--text-muted); font-size:var(--t-small); }
.tk-receipt dd { margin:0; text-align:right; font-weight:600; }
.tk-line { font-family:var(--font-display); font-size:1.6rem; line-height:1.2; margin:var(--s5) 0 var(--s2); }
.tk-line strong { color:var(--accent-hover); }
.tk-next { list-style:none; padding:0; margin:0 0 var(--s5); counter-reset:n; }
.tk-next li { position:relative; padding:var(--s2) 0 var(--s2) 2.4rem; border-top:1px solid var(--rule); }
.tk-next li::before { counter-increment:n; content:counter(n); position:absolute; left:0; top:var(--s2);
                      width:1.6rem; height:1.6rem; border-radius:50%; background:var(--text-strong);
                      color:var(--surface); font:600 .85rem/1.6rem var(--font-text); text-align:center; }
.tk-promises { color:var(--text-muted); font-size:var(--t-small); margin:var(--s4) 0 var(--s5); }
.tk-promises span { white-space:nowrap; }
/* The dot trails each phrase but the last, so a line that wraps never starts with one (390px). */
.tk-promises span:not(:last-child)::after { content:"·"; margin:0 .6em; color:var(--rule); }
.tk-actions { display:flex; gap:var(--s3); flex-wrap:wrap; align-items:center; }
.tk-actions .quiet { color:var(--text-muted); text-decoration:underline; text-underline-offset:3px; }

/* ── /dashboard/open-house — the position box beside each row ─────────────────────────────
   The same inline form as the list's note field, shorter: a position is five characters. */
.pos-form { display:flex; gap:var(--s2); align-items:center; }
.pos-form input { width:6.5rem; min-width:0; padding:var(--s2) var(--s3); min-height:36px;
                  background:var(--surface-field); border:1px solid var(--rule-surface);
                  border-radius:var(--radius-part); font-size:var(--t-small);
                  text-transform:uppercase; font-variant-numeric:tabular-nums; }
.pos-form input:focus-visible { background:var(--surface-field-focus); }
/* Phone: the receipt stacks label-over-value; a right-aligned value beside a label has no
   room at 390px. Last in the sheet on purpose — phone tiers go at the end. */
@media (max-width:640px){
  .tk-receipt { grid-template-columns:1fr; gap:var(--s1); }
  .tk-receipt dd { text-align:left; margin-bottom:var(--s2); }
}

/* #memberships LANDS ON ITS OWN GROUND (Mark, launch day 4:51 PM: "tighten up this anchor
   point so it lands on cream and not black"). The generic :target scroll-margin above keeps
   FAQ and legal anchors from hugging the top edge, but on the home page it stops the scroll
   ~40px short of the band, so the viewport opens on the hero's black bottom edge and then a
   full band padding of cream before the heading. This section is the selling surface and is
   linked from four places; it starts at its own edge. Placed at the END of the sheet on
   purpose: same specificity as `:target` above, and source order decides. */
#memberships:target { scroll-margin-top: 0; }

/* PULL-TO-REFRESH INDICATOR (public/js/ptr.js). Only ever visible in the installed app — the
   script returns before building it anywhere else — so a tab reader pays the bytes and nothing
   more. Fixed, so it rides above the page while the body stays exactly where it was. z-index 60
   sits above the dock (40), its sheet (41) and the demo flag (50). Tokens are the presale's own
   (--paper / --rule / --accent), which the theme swaps, so it reads in both. Last in the sheet on
   purpose: nothing above sets these properties, and source order decides. */
.ptr{position:fixed;top:-44px;left:50%;transform:translate(-50%,0);z-index:60;opacity:0;
width:36px;height:36px;border-radius:50%;background:var(--paper);border:1px solid var(--rule);
display:grid;place-items:center;pointer-events:none;box-shadow:0 2px 10px rgba(0,0,0,.14)}
.ptr-ring{width:17px;height:17px;border-radius:50%;border:2px solid var(--rule);
border-top-color:var(--accent)}
/* Armed reads as a colour change, not a size change: the ring is 17px and nobody perceives a
   17px thing growing. */
.ptr.is-armed .ptr-ring{border-color:var(--accent);border-top-color:var(--accent)}
.ptr.is-settling{transition:transform .22s ease,opacity .22s ease}
.ptr.is-loading .ptr-ring{animation:ptrspin .7s linear infinite}
@keyframes ptrspin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.ptr.is-loading .ptr-ring{animation:none}
.ptr.is-settling{transition:none}}

/* LONG TOKENS WRAP. A release note carries a URL, a sha, a file path — one unbreakable word
   that is wider than a phone. Without this it pokes out of the column and the page starts
   rubber-banding sideways (Mark, iPhone, /library/releases, 8/28). `anywhere` rather than
   `break-word` so the word also stops propping the column's min-content open. Prose never
   notices: a word only breaks when it alone overflows its line. Inherited, so <code> and <a>
   get it too. End of sheet: source order, not specificity. */
.doc { overflow-wrap:anywhere; }

/* SWIPE STEPS THE LIGHTBOX (lightbox.js). Horizontal pans belong to the script — there is
   nothing to pan sideways anyway — while vertical panning and pinch-zoom stay the browser's,
   so a two-finger zoom on the photograph never turns into a step. Passive listeners on the
   script side; this declaration is what keeps the axis honest. */
.lightbox.is-open { touch-action:pan-y pinch-zoom; }

/* The second sentence under the traffic headline: the requests that never became people.
   Quieter than the answer, on its own line, so the number that matters is read first. */
.lead-stat .sub-stat { display:block; margin-top:var(--s2); font-size:.92em; color:var(--text-muted); }


/* The books' contact cell: email over phone, each its own line, and a number that breaks
   instead of pushing the row past the phone's edge (Mark's screenshot, 8/29). Right-aligned
   under the stacked layout, where the value sits opposite its label. */
.data-table .contact { display:inline-grid; gap:2px; overflow-wrap:anywhere; min-width:0; }
.stacked td .contact { text-align:right; justify-items:end; }

/* A QUOTATION ON THE PRESS PAGE. Set apart from the site's own voice on purpose — the rule is
   the seam: everything above it is the site talking, this is a person talking, and a reporter
   should be able to see which is which at a glance. Bitter at reading size, the attribution
   quiet underneath. */
.pull-quote { margin:var(--s5) 0; padding-left:var(--s4); border-left:3px solid var(--accent); }
.pull-quote blockquote { margin:0; }
.pull-quote blockquote p { font:400 1.18rem/1.55 var(--font-display); color:var(--text); margin:0; }
.pull-quote figcaption { margin-top:var(--s3); font-size:var(--t-small); color:var(--text-muted); }

/* THE VISIT LIST IS NOT A LABELLED TABLE, so the label column's floor does not apply to it.
   `.doc .data-table td:first-child { min-width:12rem }` exists for the plan's tables, where the
   first cell is a sentence; here it is a six-character code, and 12rem of it pushed the table
   past its container on a 1289px window — which squeezed the action cell to 41px, wrapped
   "that's us" into a stack, and stood every row 217px tall. Two rules, both narrow: the code
   gets only the room it needs, and the action never wraps. */
.doc .traffic-visits td:first-child { min-width:0; }
.traffic-visits td:last-child, .traffic-visits .linkish { white-space:nowrap; }
/* The code is an identifier, not prose: broken across two lines ("fx000" / "0") it stops being
   a thing you can compare against the line above it. It costs about 80px, which the table has
   now that the 12rem floor is gone. The From and Pages cells still wrap on purpose — a host or
   a list of paths is allowed to take two lines. */
.traffic-visits td:first-child code { white-space:nowrap; }

/* THE LAST OF THE VISIT TABLE'S WIDTH, measured on the live page rather than guessed
   (2026-09-01). Desktop only — inside the phone breakpoint the table is stacked blocks and a
   max-width would clip a host for no reason. Three levers, smallest first: tighter horizontal
   padding across seven columns; nowrap on the two narrow columns so they sit at their header
   width; and a clamp on From, because one long referrer (thebeachoff111.com) was setting the
   column width for forty rows. The clamped value keeps its full text in the title attribute. */
@media (min-width: 641px) {
  .traffic-visits th, .traffic-visits td { padding-left:var(--s3); padding-right:var(--s3); }
  .traffic-visits td[data-label="Views"],
  .traffic-visits td[data-label="Person"] { white-space:nowrap; }
  .traffic-visits td[data-label="From"] {
    min-width:0; max-width:9.5rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
}

/* ── Ready to post (v1.0.26) ── the plan is a numbered list, a caption is a quiet quoted block
   that keeps its line breaks (the scripts are one line per take), and the links sit on one
   line with the file's facts. At the END of the sheet, by the source-order rule. */
.plan-steps { padding-left:1.25rem; margin:0 0 var(--s5); }
.plan-steps li { margin:0 0 var(--s2); }
.caption { margin:var(--s2) 0 var(--s3); padding:var(--s2) var(--s3); border-left:3px solid var(--rule); white-space:pre-line; overflow-wrap:anywhere; }
.file-links { display:flex; flex-wrap:wrap; gap:var(--s2) var(--s3); align-items:baseline; }
.file-links a { font-weight:700; }

/* ── The marketing site (v1.1.0, SOW §2b) ─────────────────────────────────────────────────
   AT THE END OF THE SHEET ON PURPOSE. Same specificity beats intent in source order, and this
   file has been bitten by that three times in one afternoon before. Nothing below overrides a
   presale rule — these are classes only views/marketing.ejs uses — but the position is the
   convention and the phone tiers must come last regardless.

   THE PHONE RULE THIS PAGE OBEYS: headings keep the gutter, cards break out to 8px from the
   edge. .band-inner already sets the gutter; the tier rows below break out under 640px and the
   headings inside them do not. */
.hero-nap { margin:var(--s4) 0 var(--s3); font-size:1.05rem; }
.hero-nap strong { display:block; }
.hero-nap a { color:inherit; }
.hero-do { display:flex; flex-wrap:wrap; gap:var(--s4); align-items:center; margin:var(--s5) 0 0; }

.tier-rows { display:grid; gap:var(--s6); margin:var(--s6) 0; }
@media (min-width: 900px) { .tier-rows { grid-template-columns:repeat(3, 1fr); } }
.tier-row { border-top:1px solid var(--rule-dark); padding-top:var(--s4); }
.tier-row--conference { margin-top:var(--s6); }
.tier-head { display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4); align-items:baseline;
             justify-content:space-between; }
.tier-head h3 { margin:0; }
/* The price is the answer to the question this section's heading asks, so it is allowed to be
   loud — and unlike the presale board it IS actable, because the doors are open. */
.tier-price { margin:0; font-size:1.15rem; white-space:nowrap; }
.tier-price strong { font-size:1.5rem; color:var(--amber); }
.tier-lede { margin:var(--s3) 0 var(--s2); }

.hospitality { margin-top:var(--s7); border-top:1px solid var(--rule-dark); padding-top:var(--s5); }
.hospitality-shots { display:grid; gap:var(--s4); margin-top:var(--s4); }
@media (min-width: 700px) { .hospitality-shots { grid-template-columns:repeat(3, 1fr); } }

.find-us .nap { display:grid; gap:var(--s2) var(--s5); margin:var(--s5) 0; }
@media (min-width: 560px) { .find-us .nap { grid-template-columns:max-content 1fr; } }
.find-us .nap dt { font-weight:700; color:var(--text-strong); }
.find-us .nap dd { margin:0 0 var(--s3); }

/* Cards to the edge, headings in the gutter — measured on a 390px screen. */
@media (max-width: 640px) {
  .tier-rows { gap:var(--s5); }
  .tier-row { margin-inline:calc(var(--s5) * -1 + 8px); padding-inline:calc(var(--s5) - 8px); }
  .tier-price strong { font-size:1.35rem; }
  .hero-do .btn { width:100%; text-align:center; }
}
/* The hero's second button. Directions is a real destination and deserves a control, but it must
   not compete with "Come see it" — outline, not fill. Ghost buttons are new here, so the rule is
   written against the tokens rather than a colour: the ink border reads on the paper ground in
   both themes, and the label takes the body colour rather than the accent, which the accent memo
   reserves for one earned button per screen. */
.btn-ghost { background:transparent; color:var(--text-strong); border:1px solid var(--rule-strong);
             box-shadow:none; }
/* THE HOVER MUST RESTATE THE COLOUR (2026-09-08, opening morning, owner's phone). .btn:hover sets
   the label to --on-accent, paper, for the ember fill; this rule only swapped the ground to sand,
   so a hovered ghost button read paper on sand: invisible. iOS keeps :hover on the last tapped
   control until the next touch, so on a phone it is the button's resting state after the tap.
   --surface-panel is sand in the light theme and the charcoal panel in the dark one, where --sand
   would have been a light slab under light ink. :active is the same state on a touch screen. */
.btn-ghost:hover, .btn-ghost:active { background:var(--surface-panel); color:var(--text-strong); }

/* The floor board's inline setter: a select and a button on one line, inside a table cell, on a
   phone. Wraps rather than overflows — the stacked table already gives each cell the full width
   at 640px and below, so the control has room without a horizontal scroll. */
.inline-set { display:flex; flex-wrap:wrap; gap:var(--s2); align-items:center; margin:0 0 var(--s2); }
.inline-set:last-child { margin-bottom:0; }
.inline-set select { min-width:9rem; max-width:100%; }

/* ── The conference room, by request (v1.0.44) ────────────────────────────────────────────
   AT THE END OF THE SHEET, by the source-order rule. .form styled inputs and nothing else, so
   the contact page's textarea and the room form's selects were browser defaults beside styled
   text fields; the three take the same rule the inputs have. The day/start/length row lived
   here as .field-row until 2026-09-07; the /room block at the end of the sheet owns that row
   now (.room-form .when), and nothing else used the class. */
.form select, .form textarea { padding:var(--s3) var(--s4); min-height:48px; background:var(--surface-field);
                               border:1px solid var(--line-input); border-radius:var(--radius-part); max-width:100%; }
/* THE CHEVRON IS OURS, NOT THE BROWSER'S (owner, 2026-09-07: "missing padding on these chevrons").
   WebKit draws its own arrow hard against the border while the text sits 16px in, so the two
   edges disagree. appearance:none removes the browser's; a .select wrapper draws a chevron from
   two borders of a rotated square at the same 16px inset the text has, in the bone-dim tone
   that reads on the light field and the dark one alike, and the right padding keeps the text
   clear of it. Not a background image: the stylesheet loads no image, and smoke says so. */
.form select { -webkit-appearance:none; appearance:none; padding-right:44px; }
.select { position:relative; display:block; }
.select select { width:100%; }
.select::after { content:''; position:absolute; right:calc(var(--s4) + 2px); top:50%; width:9px; height:9px;
                 margin-top:-7px; border-right:1.8px solid var(--bone-dim); border-bottom:1.8px solid var(--bone-dim);
                 transform:rotate(45deg); pointer-events:none; }
.form textarea { min-height:6rem; resize:vertical; }
.form select:focus-visible, .form textarea:focus-visible { background:var(--surface-field-focus); }
/* What is taken, as a plain list: no names, no purposes, one line per booking. */
/* What is taken, as a plain list: no names, no purposes, one row per booking. Styled where it
   lives, in the room panel (.room-ask .booked, further down); there is no other consumer. */
.booked { list-style:none; margin:0; padding:0; }

/* ── The reels (v1.0.48) ──────────────────────────────────────────────────────────────────
   Two short vertical videos from the 14 August shoot: the conference room (on /room in both
   states and in the open home page's conference block) and the coffee (with the open home
   page's hospitality row). AT THE END OF THE SHEET, by the source-order rule, and the
   phone tier last of all.

   A 9:16 FRAME IS THE WHOLE PROBLEM THIS BLOCK SOLVES. Left to the column it sits in, a
   portrait video is taller than a phone's viewport and the reader scrolls through a black
   frame. So every reel is capped at a width (320px on /room, 300px in the conference block and
   the hospitality row), centred when it stands alone on a phone and flush left on a tablet
   (the band rule near the end), and its height follows from the ratio.
   The markup is the tour's: poster, controls, playsinline, preload none, no autoplay. Since
   2026-09-07 tour.js selects .reel-video as well, so each reel gets the same click-anywhere
   sheet and play badge as the tour (owner's ask). The caption takes the small size in the body
   colour of whichever ground it stands on: --text-muted on paper, --text-cut on the black
   board, where --text-cut-muted fails AA (see the note above .tour-video). */
.reel { margin:0; width:100%; max-width:320px; }
.reel-video { display:block; width:100%; height:auto; aspect-ratio:9/16; background:var(--black);
              border:0; border-radius:var(--radius); }
.reel figcaption { margin:var(--s2) 0 0; font-size:var(--t-small); line-height:1.4; color:var(--text-muted); }
.band-black .reel figcaption { color:var(--text-cut); }

/* THE CONFERENCE BLOCK'S MEDIA COLUMN, open state only (.rate-media exists in no other arm).
   Below 1024px the reel stands first, centred, and the photograph stack follows exactly as it
   did; from 1024px the two sit side by side inside the column, the reel at its cap and the
   photographs taking what is left. .rate-shots wraps the shot partial so the hero and its
   strip stay one stack inside the flex row: the partial renders them as siblings, and
   unwrapped they would become two flex items. The .rate-strip .shot rules above still apply
   inside the wrapper; nothing here re-states them. */
.rate-media .reel { max-width:300px; margin-inline:auto; margin-bottom:var(--s5); }
/* FROM 640px, NOT 1024 (owner, 2026-09-07, a phone held sideways at 844px): between those widths the
   reel stood alone on the left with the photographs stacked under it at phone size. The row fits
   from 640 (300 + 24 + at least 268), so the two-up composition starts there. */
@media (min-width:640px){
  /* AN EQUAL SPLIT, CAPPED. The page is 960px and the media column about 500px; a reel held at
     its 300px cap left the photograph stack 176px wide with 75px thumbnails under it
     (measured 2026-09-07). Both halves grow from zero, so they share the column evenly, about
     240px each, and the cap on the reel only ever binds on a wider column. */
  .rate-media { display:flex; gap:var(--s5); align-items:flex-start; }
  .rate-media .reel { flex:1 1 0; margin:0; }
  .rate-shots { flex:1 1 0; min-width:0; }
}

/* THE WHY BAND, the first thing under the hero on the open site (2026-09-08). It wears
   .band-tour's ground rather than its own: the screen is where a film lives on this site, and
   that class already carries the dark-theme handling and the focus ring for a video. Two things
   it adds. The eyebrow takes --text-cut, because --text-cut-muted is 4.40:1 on this ground and
   fails AA (the note by .band-tour says so). And a hairline at the bottom in every theme, since
   the board directly beneath it is #0E0F12 against this #1E2126: a small step that reads as one
   long dark run without a line, the same trap the room band fell into on sand.
   Stacked below 880px with the film first, because the film is the argument and the words are
   its caption; two-up above it. minmax(0,1fr) on the text column: a long word shrinks the
   column, not the page. */
.band-why { padding-block:var(--s6); border-bottom:1px solid var(--rule-cut); }
.band-why .eyebrow { color:var(--text-cut); }
/* The fly-through at the foot of the page now follows the black band, and the same small step
   applies at that seam. */
.band-black + .band-tour--foot { border-top:1px solid var(--rule-cut); }
.why-media { display:grid; gap:var(--s5); }
/* 240, NOT THE 300 THE OTHER TWO TAKE (owner, 2026-09-08). A portrait frame at 300 made this
   band 871px on a phone, a whole screen to scroll past before the prices. At 240 the band is
   about 620 and still plainly a film. */
.why-media .reel { max-width:240px; margin-inline:auto; }
.why-words h2 { margin:var(--s2) 0 var(--s3); }
.why-words .lede { margin:0; }
@media (min-width:880px){
  .why-media { grid-template-columns:240px minmax(0,1fr); gap:var(--s6); align-items:center; }
  .why-media .reel { margin:0; }
}

/* THE COFFEE REEL STANDS WITH THE HOSPITALITY ROW, NOT IN IT (open state; .extras-media exists
   in no other arm). It was a tile in the strip first, and two things were wrong with that. A
   9:16 tile beside 4:3 photographs makes the row as tall as the reel whatever the width: at
   390px the row measured 503px against 200px photographs, with 151px of empty ground above and
   below each, which is the "one image's worth of height on a phone" rule above broken by the
   row's own tile, and align-items:center only split the hole in two. And the tile's
   margin-inline:auto moved the strip's mandatory snap target off the column's left edge, so
   from about 434px to 1023px the strip loaded pre-scrolled (107px at 768px) and could never
   rest at zero. So the reel takes the conference block's grammar: .extras-media holds the reel
   and the strip, the reel stands first at its 300px cap, and the strip beneath it stays
   photographs only, one photograph tall, with its snap targets where the columns are. From
   1024px the two sit side by side, the reel at 300px and the two photographs stacked in a
   column beside it whose width is chosen so that two 4:3 frames and the gap between them
   (2 x 255 + 24 = 534px) match the reel's height (300 x 16/9 = 533px): the height is filled,
   not centred around. The strip's own 1024px rule (auto-fit, minmax 240px) already gives a
   340px column one track, so the stack needs no template of its own. */
.extras-media { margin-top:var(--s6); }
.extras-media .reel { max-width:300px; margin-inline:auto; }
.extras-media .extras-shots { margin-top:var(--s5); }
/* FROM 640px, NOT 1024, same reason as .rate-media above. Inside .extras-media the strip gives
   up its phone scroll-snap and stacks the two photographs in one column beside the reel, which
   is what the 1024 rule did; the presale's three-photo strip keeps its own tiers untouched. */
@media (min-width:640px){
  .extras-media { display:flex; gap:var(--s5); align-items:flex-start; }
  .extras-media .reel { flex:0 0 300px; margin:0; }
  .extras-media .extras-shots { flex:0 1 340px; margin-top:0; grid-auto-flow:row; grid-auto-columns:auto;
                                grid-template-columns:1fr; overflow-x:visible; gap:var(--s4); }
}

/* TABLETS, 640px up to the two-column tiers. Standing alone above left-aligned photographs
   (the conference block, the hospitality row), a centred reel sits on a different axis from
   everything around it: at 768px the conference reel's centre line measured 384px against the
   photographs' 304px. On a phone the cap and the gutters leave about 21px a side and centring
   is invisible; from the two-column tiers the reel has a column of its own; in between it
   keeps the left edge its neighbours keep. /room's own tablet rule is in its block below. */
@media (min-width:640px) and (max-width:1023px){
  .rate-media .reel, .extras-media .reel { margin-inline:0; }
}

/* Phones. Wherever a reel stands on a phone it is centred and it takes the gutter-to-gutter
   width up to its cap, never more: the cap is what keeps a 9:16 frame under the height of the
   viewport, and a stray width rule from a parent must not lift it. The /room block below is
   the only thing after this tier, and its phone tier never names .reel (bin/smoke.js §reels
   holds both). */
@media (max-width:640px){
  .reel { width:100%; margin-inline:auto; }
}

/* ── /room, the request page (2026-09-07 spec) ─────────────────────────────────────────────
   Source order is the phone order: offer, hero, ask, proof, good to know, return. One column
   below 880px. From 880px a two-column grid: the ask in column 2 spanning the offer, the hero,
   and the good-to-know block; the proof sheet and the return link spanning both columns. The
   row gap owns every seam, so direct children keep no bottom margin. The .doc > * 720px cap is
   lifted for the grid at 880px only, the same release .room-intro had. */
.room { display:grid; gap:var(--s6); align-items:start; }
.room > * { min-width:0; }
.room h2 { margin-top:0; }                                   /* after .doc h2: same specificity, later wins */
.room-offer > * { margin:0; max-width:none; }
.room-offer > * + * { margin-top:var(--s3); }
.room-offer .lede { max-width:46ch; }
.room-offer .room-do { margin-top:var(--s5); }
.room-rate { font:700 var(--t-h2)/1.15 var(--font-display); letter-spacing:-.02em;
             color:var(--text-strong); font-variant-numeric:lining-nums; }
.room-hero { margin:0; }
.room-hero img { display:block; width:100%; height:auto; }
.room-hero figcaption, .proof-cap { margin:var(--s2) 0 0; font-size:var(--t-small);
                                    line-height:1.4; color:var(--text-muted); }

/* THE ASK: the one form on the site's one panel fill. Fieldsets reset to nothing (a fieldset's
   default min-width is min-content, the classic 390px overflow), the grid on a child div,
   never on the fieldset; controls take the cell width, since .form select has a max-width
   and no width and would otherwise shrink to its longest option inside a grid cell. */
.room-ask-col h2 { margin-bottom:var(--s2); }
.room-ask-lede { margin:0 0 var(--s4); color:var(--text-muted); }
.room-ask { background:var(--surface-panel); padding:var(--s4); }
/* THE DATE FIELD ON iOS (owner, 2026-09-07, the field ran past the panel and centred its text). iOS
   Safari sizes a native date input by its own rules and ignores a plain width until its appearance
   is switched off; with it off the field takes the 100% every other field takes, and the value
   sits left where the other values sit. The min-height keeps the 48px tap target the others have. */
.room-form input[type="date"] { -webkit-appearance:none; appearance:none; display:block; width:100%; min-width:0;
                                box-sizing:border-box; text-align:left; min-height:48px; }
.room-form input[type="date"]::-webkit-date-and-time-value { text-align:left; }
.room-ask .notice { margin-bottom:var(--s4); }
.room-form legend { margin:0 0 var(--s3); padding:0;
  font:600 var(--t-label)/1.3 var(--font-text); letter-spacing:.12em; text-transform:uppercase;
  color:var(--text-muted); }
/* WHAT IS TAKEN IS A THING, NOT A STEP (owner, 2026-09-08, his phone, the list circled: "could
   look more like a booked item and less like it's part of the overall booking module"). It had
   the legend's uppercase rule for a heading and the form's oak hairlines around it, so it read
   as step one. Now each booking is a row led by a 3px oak bar, the way an event sits on a
   calendar: the day strong at body size, the hours under it in small text, and one quiet word
   at the right. "Taken" is the board's own word for a sold desk. Nothing here is the form's:
   no hairline, no uppercase tracking, no 600/--t-small (that is .form label), no field fill,
   no --radius-part, no ember. Oak is a line here, never a field. A taken hour is a fact, not a
   warning, so no alarm ink. No heading over it; the ul's aria-label carries the name. */
.room-ask .booked { display:grid; gap:var(--s3); margin:0 0 var(--s6); padding:0; }
.room-ask .booked li { display:grid; grid-template-columns:minmax(0,1fr) auto;
  grid-template-areas:"day state" "span state"; column-gap:var(--s3); align-items:start;
  margin:0; padding:var(--s1) 0 var(--s1) var(--s3); border:0; border-left:3px solid var(--oak); }
.room-ask .booked-day   { grid-area:day;   font-weight:600; line-height:1.3; color:var(--text-strong); }
.room-ask .booked-span  { grid-area:span;  font-size:var(--t-small); line-height:1.5; color:var(--text);
  font-variant-numeric:tabular-nums lining-nums; }
/* 15px at 1.5 sits on the 17px day's first line; small and muted, no tracking, so it is a fact
   beside the date and not a label over a control. nowrap: it is one word, and the day column is
   the one that gives (minmax(0,1fr)), so nothing can push past 390. */
.room-ask .booked-state { grid-area:state; font-size:var(--t-small); line-height:1.5;
  color:var(--text-muted); white-space:nowrap; }
.room-ask .form { max-width:none; }
.room-form fieldset { border:0; padding:0; margin:0; min-width:0; }
.room-form .fields { display:grid; gap:var(--s4); }
.room-form .when { display:grid; gap:var(--s4); grid-template-columns:repeat(2, minmax(0,1fr)); }
.room-form .field--day { grid-column:1 / -1; }
@media (min-width:640px){
  .room-form .when { grid-template-columns:1.4fr 1fr 1fr; }
  .room-form .field--day { grid-column:auto; }
}
.room-form .hint { margin:var(--s2) 0 0; font-size:var(--t-small); color:var(--text-muted); }
.room-form input, .room-form select, .room-form textarea { width:100%; min-width:0; }
/* THE HONEYPOT KEEPS ITS PIXEL. The rule above is (0,1,1) and .hp is (0,1,0), so without this
   the hidden field is 100% of the viewport from its static position at the gutter: measured
   2026-09-07 in Chrome at 390, left 24, right 414, scrollWidth 414, for a field nobody can
   see. With it the box is the 1px plus its padding, and the page is 390. */
.room-form .hp { width:1px; }
.room-form .btn { width:100%; }
.room-form .muted { margin:0; }

/* THE PROOF SHEET: an oak hairline, a heading across both tracks, the reel as the tall tile
   of one contact sheet beside a 2x2 grid of 4:3 photographs with one-line captions. */
.room-proof { padding-top:var(--s6); border-top:1px solid var(--oak); }
.room-proof .reel { margin-inline:auto; }
.proof-grid { list-style:none; margin:var(--s5) 0 0; padding:0; display:grid; gap:var(--s5); }
.proof-grid li { min-width:0; }
.proof-grid .shot { margin:0; max-width:none; }
.room-know > p { margin:0; }
.room-know > p + p { margin-top:var(--s4); }
.room-return { margin:0; }

@media (min-width:640px) and (max-width:879px){
  .room-proof .reel { margin-inline:0; }                   /* tablets: the reel keeps the left edge */
  /* THE PROOF SHEET TWO-UP FROM 640 (owner, 2026-09-07, a phone held sideways): the reel at its
     cap on the left and the four photographs two across beside it, which is the 880 composition
     at the tablet's own proportions. Below 640 the phone stack stands. */
  .room-proof { display:grid; grid-template-columns:300px minmax(0,1fr); gap:var(--s4) var(--s5); align-items:start; }
  .room-proof h2 { grid-column:1 / -1; margin:0; }
  .room-proof .reel { max-width:none; margin:0; }
  /* Two across only once each photograph can be 150px wide (316px of column); at 640 the column
     is 300px and the photographs stack in one, at 720 and up they pair. */
  .proof-grid { margin:0; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:var(--s4); }
  /* ONE COLUMN HERE TOO, so a 400 needs the reorder the phone tier has. Without it a refusal
     at 768x1024 measured the alert at y=1296, under the offer and the hero, off screen and
     silent; with it, y=470. From 880px the column is in the first screen already. */
  .room--error .room-ask-col { order:-1; }
}

@media (min-width:880px){
  .doc > .room { max-width:none; grid-template-columns:minmax(0,1fr) 400px;
                 grid-template-rows:auto auto 1fr; column-gap:var(--s7); row-gap:var(--s6); }
  /* auto auto 1fr, NOT three implicit auto rows. The ask column spans rows 1 to 3 and is
     taller than the offer, the hero, and the facts together; with implicit rows Grid splits
     that surplus across all three, and the offer-to-hero and hero-to-know seams measured 98px
     at 1280 instead of the 32px row gap (the hero started at y=605, with 195px of it in the
     800px first screen). Row 3 takes the surplus; align-items:start pins .room-know to its top. */
  .room-offer   { grid-column:1; grid-row:1; }
  .room-hero    { grid-column:1; grid-row:2; }
  .room-know    { grid-column:1; grid-row:3; }
  .room-ask-col { grid-column:2; grid-row:1 / span 3; }
  .room-proof, .room-return { grid-column:1 / -1; }
  .room-offer .room-do { display:none; }                   /* the form is on screen; its submit is the one ember */
  /* THE 400px PANEL CANNOT HOLD THREE CONTROLS. 1.4fr 1fr 1fr inside the panel's 368px gives
     each select 99px, and "12:30 PM" beside the native chevron needs about 125px: measured
     2026-09-07 with the placeholder rendered as "choos". So in the column the day takes the
     row and the two selects share the next, exactly as on a phone; three across is for the
     tablet, where the panel is the page. The design's own first screen at 1280 reads "Day,
     then Start and How long side by side", which is this. */
  .room-form .when { grid-template-columns:repeat(2, minmax(0,1fr)); }
  .room-form .field--day { grid-column:1 / -1; }
  /* 272fr 608fr: at the 912px document the reel is 272px (484px tall, 512 with its caption)
     beside two rows of 296x222 tiles with one-line captions (518px). Measure it rendered. */
  .room-proof { display:grid; grid-template-columns:272fr 608fr; gap:var(--s4) var(--s6);
                align-items:start; margin-top:var(--s5); }
  .room-proof h2 { grid-column:1 / -1; margin:0; }
  .room-proof .reel { max-width:none; margin:0; }
  .proof-grid { margin:0; grid-template-columns:repeat(2, minmax(0,1fr)); gap:var(--s4); }
}

/* Phones, last. Headings keep the gutter; the hero, the tiles, and the panel break out to 8px
   from the edge; captions and the panel's padding bring the text back to the 24px gutter. A
   400 orders the ask to the top of the grid so the alert lands on screen, as the tablet tier
   above does for 640 to 879px. */
@media (max-width:640px){
  .room-offer .room-do .btn, .room-return .btn { width:100%; }
  .room-hero, .proof-grid, .room-ask { margin-inline:calc(var(--s5) * -1 + 8px); }
  .room-hero figcaption, .proof-cap { padding-inline:calc(var(--s5) - 8px); }
  .room-ask { padding-inline:calc(var(--s5) - 8px); }
  .room--error .room-ask-col { order:-1; }
}

/* ── THE ROOM CALENDAR AS A LOG (2026-09-08, the owner's phone, both orientations) ──────────
   AT THE END OF THE SHEET, by the source-order rule. Mark reads /dashboard/room on an iPhone in
   both orientations and a five-column table was wrong in each: at 390 the generic .stacked rule
   set every value ragged against the right edge and split "2 hours" into a column beside the
   date; at 844 the Who column starved to 76px and the address broke letter by letter.

   The four calendar tables carry room-log INSTEAD of stacked, so the generic 640px block and
   `.stacked td .contact { text-align:right }` (which sits outside its media query) never reach
   them. Below 1024px each row is a record, the way /messages reads: the date as its title, the
   fields under it, the controls as a rail at the bottom; no card, no border, a rule between
   records. From 1024px the real table returns with its columns cut by hand so Who can hold an
   address. room-log--full is the five-column shape (Waiting, Coming up); room-log--brief the
   three-column one (Past, Junk).

   table.room-log, not .room-log: `.data-table td` is (0,1,1) and `.room-log td` would tie it and
   leave the result to source order, which this sheet has lost three times. The type selector
   makes every rule here (0,1,2) or better, at every width. */
table.room-log td { overflow-wrap:anywhere; white-space:normal; }
/* The person: name, email, phone each on its own line, flush left at every width. .contact is
   the books' inline-grid; Past and Junk print the email as a muted span after the name, and it
   takes its own line the same way, so an address always starts at the left edge with the whole
   width before it is ever asked to break. */
table.room-log td .contact { text-align:left; justify-items:start; }
table.room-log td[data-label="Who"] .muted { display:block; }
/* The date's companions (the hours, the rate, when it was asked) are their own quiet lines under
   it at every width, never a column beside it. The <br> that separated two of them in the old
   table would now be an empty line between two blocks, so it is off. */
table.room-log td:first-child .muted { display:block; font-weight:400; }
table.room-log td:first-child br { display:none; }
/* 44px controls in every tier. .btn-small zeroes min-height for the list workbench's dense rows;
   a booking is answered with a thumb. Confirm is the one ember button; Decline and Cancel are
   ghosts; Junk and Not junk are links, given the same 44px target, with side padding as the tap
   room and a negative start margin so the text keeps the same edge as the buttons. */
table.room-log .btn { min-height:44px; white-space:nowrap; }
table.room-log .linkish { display:inline-flex; align-items:center; min-height:44px;
                          padding:0 var(--s2); margin-inline-start:calc(var(--s2) * -1); }

/* THE TABLE, from 1024px, where the dashboard column is at its 912px cap. Fixed layout so the
   columns are decided before the content is read: in auto layout an overflow-wrap:anywhere
   address has a one-letter minimum width and the browser starves that column first (the 844
   screenshot). Who takes 30% (242px of content), so a 30-character address sits on one line and
   the 55-character test address breaks once, at a word, never letter by letter. The two narrow
   columns are rem so Confirm (92px) always fits and "included" never breaks inside itself; What
   for is the auto column and takes the rest (197px). Buttons fill their cell so the stack is
   even, and Member's note drops under the tier name. 912 = 201 + 274 + 197 + 112 + 128. */
@media (min-width:1024px){
  table.room-log { table-layout:fixed; }
  table.room-log--full th:nth-child(1) { width:22%; }     /* When */
  table.room-log--full th:nth-child(2) { width:30%; }     /* Who */
  table.room-log--full th:nth-child(4) { width:7rem; }    /* Member */
  table.room-log--full th:nth-child(5) { width:8rem; }    /* Answer / Change it */
  table.room-log--brief th:nth-child(1) { width:32%; }    /* When: 292 */
  table.room-log--brief th:nth-child(3) { width:9rem; }   /* Status / Change it: 144; Who takes 476 */
  table.room-log td[data-label="Member"] .muted { display:block; }
  table.room-log td:is([data-label="Answer"],[data-label="Change it"]) .btn { width:100%; }
}

/* THE LOG, below 1024px. No card, no box: a rule between records, as .msg-log does, and the
   record capped at --w-text so a landscape phone reads at a measure. The first cell is the title
   and carries no label; every other cell draws its label from data-label. The action cell is a
   rail: label off, controls in a row that wraps if it must. */
@media (max-width:1023px){
  .scroll:has(table.room-log) { overflow-x:visible; padding-bottom:0; }
  table.room-log, table.room-log tbody, table.room-log tr, table.room-log td { display:block; width:100%; }
  table.room-log { max-width:var(--w-text); min-width:0; }
  table.room-log thead { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  table.room-log tr { padding-block:var(--s5); padding-inline:0; border-top:1px solid var(--rule-surface); }
  table.room-log tr:last-child { border-bottom:1px solid var(--rule-surface); }
  table.room-log td { padding:0; border:0; text-align:left; }
  /* AIR BETWEEN THE FIELDS (owner, 2026-09-08, his phone): 8px ran the labelled rows together
     into one grey column. Each field gets a line of its own space above it, and the controls a
     wider one, so the record reads as title, then facts, then what you can do. */
  table.room-log td + td { margin-top:var(--s4); }
  table.room-log td::before { content:attr(data-label); display:block; margin-bottom:2px;
    font:600 var(--t-label)/1.3 var(--font-text); letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-muted); }
  table.room-log td:first-child { font-weight:600; font-size:var(--t-body); color:var(--text-strong); }
  table.room-log td:first-child::before { content:none; }
  table.room-log td:is([data-label="Answer"],[data-label="Change it"]) { display:flex; flex-wrap:wrap;
    gap:var(--s3); align-items:center; margin-top:var(--s5); }
  table.room-log td:is([data-label="Answer"],[data-label="Change it"])::before { content:none; }
  table.room-log td:is([data-label="Answer"],[data-label="Change it"]) .inline-set { margin:0; }
}

/* A PHONE HELD SIDEWAYS, 641 to 1023px: the label in a fixed 8rem column on the left, the value
   with the rest (496px at 844, inside the 640px cap); the title and the rail span the record.
   The label is positioned, not floated or gridded: a cell's children are loose inline runs (a
   <strong>, a text node, a .muted span) and a flex or grid parent would seat each in its own
   track, which is exactly the "2 hours in a column beside the date" bug. 13px at 1.85 is a 24px
   line box, the same as 15px body text at 1.6, so label and value share a baseline. */
@media (min-width:641px) and (max-width:1023px){
  table.room-log td:not(:first-child) { position:relative; padding-left:calc(8rem + var(--s4)); }
  table.room-log td::before { position:absolute; left:0; top:0; width:8rem; margin:0; line-height:1.85; }
  table.room-log td:is([data-label="Answer"],[data-label="Change it"]) { padding-left:0; }
}

/* THE PAYMENT CHIP ON A BOOKING (2026-09-08). The row's stamps in words, after the rate, under
   the date at every width: the same pill grammar as .pill (999px means nowrap in the same rule,
   so a label is never a lozenge), muted by default; a paid booking takes the accent, a lapsed link
   the error ink, as do a disputed payment and a lost chargeback, so the states somebody has to
   act on are the ones that read differently (a partial refund stays muted: the booking stands). On
   the books' months table the two notes under a figure ("of which room", "fees pending") are
   their own quiet line. Above the phone tier, by the source-order rule. */
.pay { display:inline-block; padding:2px var(--s2); font-size:var(--t-label); font-weight:600;
       letter-spacing:.04em; border:1px solid var(--rule-surface-strong); border-radius:999px;
       color:var(--text-muted); white-space:nowrap; line-height:1.4; }
.pay--paid { border-color:var(--accent); color:var(--accent); }
.pay--expired, .pay--disputed, .pay--chargeback { border-color:var(--error-line); color:var(--error-ink); }
table.room-log td:first-child .muted .pay { margin-top:2px; }
.months-note { display:block; }

/* Phones, last. Headings keep the 24px gutter; the record's rules break out to 8px from the edge
   and the row's padding brings the text back to the gutter, the same move the tier rows and the
   room page's ask block make. Nothing here sets a width past 100%, so the column closes at 342px with no sideways
   scroll. */
@media (max-width:640px){
  .scroll:has(table.room-log) { margin-inline:calc(var(--s5) * -1 + 8px); }
  table.room-log tr { padding-inline:calc(var(--s5) - 8px); }
}

/* The local fake checkout's summary (2026-09-08). Labelled pairs, the amount first, the shape the
   dashboard uses for a fact and its name. Dev-only page; no new colour, no new radius. */
/* ALIGN-ITEMS, NOT ALIGN-SELF ON THE LABEL (2026-09-08): baseline alignment needs BOTH items in
   the row to opt in. With it on the dt alone the dd stretched, so a 13px label and a 17px value
   sat on two different baselines and every row read crooked. The label's line-height matches the
   value's line box so the two share one line. */
.fake-sum { display:grid; grid-template-columns:max-content minmax(0,1fr); gap:var(--s3) var(--s5);
            align-items:baseline;
            margin:var(--s6) 0; padding:var(--s5) 0; border-block:1px solid var(--rule-surface); }
.fake-sum dt { font:600 var(--t-label)/1.6 var(--font-text); letter-spacing:.08em;
               text-transform:uppercase; color:var(--text-muted); }
.fake-sum dd { margin:0; overflow-wrap:anywhere; }
.fake-do { display:flex; flex-wrap:wrap; gap:var(--s4); align-items:center; }
.fake-do form { margin:0; }

/* The footer lockup is a link home (2026-09-08). Inline-block so the 70px image carries its own
   tap target, no underline under a picture, and the same focus ring every control has. */
.foot-brand-home { display:inline-block; text-decoration:none; }
.foot-brand-home:focus-visible { outline:2px solid var(--focus); outline-offset:4px; }

