/* ═══════════════════════════════════════════════════════════════════════════
   PORTAL · TODAY — CHAPTER 08 SKIN
   brand/EVORRAH_BRAND_SYSTEM_V15.html §8 applied to /client-portal#today.
   Built 2026-09-02.

   WHAT THIS IS
   Chapter 08 of the brand book is the client portal's specification, and it
   says so in its own words: "It is a specification, not a description: as of
   31 August 2026 none of it is built." This file builds it, for one screen —
   the Today room, which is the screen a client lands on.

   WHAT WAS ACTUALLY WRONG, MEASURED IN THE RUNNING PORTAL (1440px, staging
   fixture a11y-review@example.invalid) BEFORE A LINE OF THIS WAS WRITTEN:

     font-family   Montserrat 208 elements · Crimson Pro 16 · Libre Bask. 1
                   Inter — the face chapter 05 sets the product in — was not
                   loaded at all. §8.6 names this by name: "Montserrat
                   currently sets body, so every paragraph a client reads is a
                   geometric signage face at 15px, where Inter was drawn for
                   exactly that."
     ink           rgb(30,11,14) on 190 elements — the RETIRED onyx. §8.2's
                   Onyx is #141414.
     action        rgb(176,42,69) — bloodstone. The site ships #9D0617.
     structure     rgb(38,84,80) — the sage teal §8.2 exists to delete. It was
                   carrying the whole readiness card: the figure, the unit, the
                   sub-line, the bar, the track and the footer links.
     done          rgb(49,114,84) — forest. §8.3's Done is Moss Fern #3B6B24.
     ground        #F8F1E7 on 29 elements and #FBF7EF on 3 — two grounds.
     radius        2px on 8 elements, including buttons. §8.8: 2px is INPUTS
                   ONLY; every button, card, chip and pill is 0.

   ── THE THREE HEXES WHERE THE BRAND BOOK IS STALE, AND WHY THIS FILE DOES
      NOT FOLLOW IT ────────────────────────────────────────────────────────
   Chapter 08 says to take colour "from this chapter or from app/v15/style.css,
   and nothing else". Those two now disagree, and where they do, the newer
   owner decision wins. All three are recorded in brand/EVORRAH_PORTAL_DECISIONS.html
   ("Two Portal Decisions", measured 31 August 2026), which post-dates the
   chapter and supersedes it:

     next step   §8.3 says Mahogany #C81420. The owner replaced the deep stop
                 with #9D0617 on 2026-08-31 (app/v15/style.css step 4) — a wine
                 red at 353°, the same hue as Black Cherry, measured 7.58:1 on
                 Linen and 6.65:1 on Alabaster where #C81420 was 5.24 and 4.59.
                 It BUYS contrast rather than spending it. --ch8-crimson.
     send back   §8.3 says Black Cherry #6B0410. Superseded 31 Aug: at the new
                 action red, Black Cherry fell to ΔE 21.0 from it — the exact
                 collision that page exists to prevent. It is Night Bordeaux
                 #4C061D, ΔE 41.9 away, Linen on it at 13.86:1. --ch8-bordeaux.
     approve     §8.5's lifted Moss is #4E8D30; the decisions page carries
                 #82C95E (9.18:1) for dark grounds. Moot on this screen — the
                 Today room has no dark plate — but the token is bound to the
                 decisions page's value so a future dark band is already right.

   ── WHAT THIS FILE DELIBERATELY DOES NOT DO ─────────────────────────────
   1. IT DOES NOT SWEEP portal.css. tokens.css says it twice, in two separate
      blocks, and it is right: "ADOPT AS RULES ARE TOUCHED. Do NOT sweep the
      stylesheets in one pass" — 2,775 spacing and 524 tracking declarations
      moved at once is a redesign wearing a tidy-up's clothes and no one could
      review it. This is one screen, one file, one link line. Delete the link
      and the portal is byte-identical to what it was.
   2. IT DOES NOT MOVE THE ROOT FONT SIZE. §8.7 wants the portal's shorter
      scale on a 16px root; tokens.css sets html{font-size:112.5%} (18px) and
      says why — 2026-08-03, owner, "make the portal match the site". A root
      change is global by construction: it cannot be scoped to one room, and
      it shrinks every word in all five rooms by a ninth. That is an owner
      decision, not a side effect of a skin. The STEPS in §8.7 are already in
      tokens.css at the chapter's own values and are used here as named.
   3. IT DOES NOT RETIRE LIBRE BASKERVILLE PORTAL-WIDE. §8.6 measures 297
      declarations of it across four files. This file stops it on the Today
      screen (.td-p and .ap-p, the two that render here); the other rooms
      still load and use it, so the <link> stays.
   4. IT DOES NOT CHANGE COPY. §8.11 ("The words") has real findings on this
      screen — the decision pair should read Approve · Send it back — but the
      buttons here open flows with live handlers and tests behind them, and
      copy is a product change, not a paint job.

   ── HOW IT WINS THE CASCADE ─────────────────────────────────────────────
   ⚠ Most of the Today room's CSS is NOT in a stylesheet. portal-rooms.js
   carries it as a JS string (var CSS, ~line 171) and appends it to <head> as
   <style id="roomsCss"> at boot — i.e. AFTER every <link>, including this one.
   So source order cannot be relied on and specificity must do the work. Every
   rule below is prefixed body[data-active-tab] (0,2,x), which beats
   both #roomsCss's bare classes (0,1,x) and portal-v10.css's `body ` prefixes
   (0,1,x+1). That prefix is also the SCOPE: showTab() stamps the attribute
   (client-portal.html:9344, PX-005) and it reads "today" only in this room —
   verified live by clicking through to Design, where it becomes "concept" and
   every rule here stops applying. The other four rooms are untouched.

   ⚠ Load AFTER portal-v10.css. See the <link> in app/client-portal.html.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   §8.12 — THE CONTRACT, REBOUND ON THIS SCREEN ONLY

   Chapter 08's tokens-to-paste, bound to the --ch8-* names tokens.css already
   defines (AINS §24, row 24-a) rather than to hexes. Two reasons this is a
   rebind of the EXISTING names rather than a new vocabulary: every rule in
   portal.css, portal-v10.css and #roomsCss that already reads var(--onyx),
   var(--bloodstone) or var(--sage) is repainted by this block with no selector
   of its own, and a rule that later adopts a --ch8-* name keeps working when
   this file is deleted. Nothing here escapes the room — the attribute changes
   the moment the client leaves it.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab]{
  /* ground and ink — §8.1 */
  --ivory:          var(--ch8-linen);        /* the retired ground, repointed  */
  --pearl:          var(--ch8-linen);
  --mat:            var(--ch8-linen);
  --linen:          var(--ch8-linen);
  --alabaster:      var(--ch8-alabaster);
  --onyx:           var(--ch8-onyx);         /* #1E0B0E -> #141414             */

  /* the one next step — §8.1, at the owner's 2026-08-31 stop */
  --action:         var(--ch8-crimson);
  --action-lift:    var(--ch8-crimson-lift);
  --bloodstone:     var(--ch8-crimson);
  --bloodstone-dk:  var(--ch8-bordeaux);     /* hover darkens toward the ground colour */

  /* §8.2 — the quiet layer costs no colour. The structural teal is gone: both
     sage names now resolve to the ink step that does the job the teal was
     doing, so the ~394 rules that read them stop being a second palette.

     ⚠ ONYX 78%, NOT 62% (owner, 2026-09-04, ledger row 24-a residue (3)). The
     62% step is the last AA text step ON LINEN and nowhere else: 4.94 on Linen,
     4.34 on Alabaster, and under 4.5 over every tint the Portal paints. The
     sage names are read by chips and badges that sit on tinted panels, so the
     62% binding was shipping ten measured AA failures on one screen alone —
     `.rel-chip.rel-friend` 4.02 on the Onyx-10% tint, `.ag-badge-pending` 4.15
     on the 8.5% tint, `.ag-footer-count` / `.ag-footer-link` 4.33 on Alabaster,
     all reported by scripts/portal-a11y-audit.mjs as axe color-contrast.
     78% clears every one of them: 8.55 Linen · 7.50 Alabaster · 7.19 / 6.97 on
     those two tints. Swept across all 373 declarations that read these three
     names: 366 rule/ground pairs improve, none regresses on a light ground.
     What 78% CANNOT survive is a dark plate (1.92 on Onyx) — which is why the
     scoped block below exists and why this binding may not be read as the whole
     answer. */
  --sage:           var(--ch8-onyx-78);
  --sage-dk:        var(--ch8-onyx-78);
  --ash:            var(--ch8-onyx-78);
  --ink-2:          var(--ch8-onyx-78);
  --ink-meta:       var(--ch8-onyx-62);
  --ink-pending:    var(--ch8-onyx-48);
  --ink-off:        var(--ch8-onyx-34);

  /* §8.8 — rules and edges. 1px hairline is Onyx 20% on Linen. The portal's
     three border names all resolve onto that scale rather than onto three
     unrelated alphas of the retired ink. */
  --hair:           var(--ch8-onyx-20);
  --hair-soft:      var(--ch8-onyx-12);
  --border:         var(--ch8-onyx-12);
  --border-d:       var(--ch8-onyx-20);
  --border-s:       var(--ch8-onyx-34);

  /* §8.3 — the functional layer. Five states, five colours, every pair ≥ΔE 31.
     bad/success/warn are portal.css's spelling of the same three roles. */
  --done:           var(--ch8-moss);
  --forest:         var(--ch8-moss);
  --success:        var(--ch8-moss);
  --sendback:       var(--ch8-bordeaux);
  --late:           var(--ch8-amber);
  --ochre:          var(--ch8-amber);
  --warn:           var(--ch8-amber);
  --gold-ink:       var(--ch8-amber);
  --broken:         var(--ch8-terracotta);
  --brick:          var(--ch8-terracotta);
  --bad:            var(--ch8-terracotta);

  /* ⚠ AN ALIAS DECLARED AT :root DOES NOT FOLLOW A REBIND ON body, AND THIS
     COST AN HOUR. portal.css:65-66 sets `--onyx-dk: var(--onyx,#1E0B0E)` and
     `--sage-lt: var(--sage,#265450)` INSIDE ITS OWN :root block. Custom
     property substitution happens where the property is DECLARED, so those
     var() lookups resolve against :root's --onyx and --sage — the retired
     values in tokens.css — no matter what this block rebinds further down the
     tree. The aliases had to be rebound by name, and `--onyx-dk` alone is read
     77 times by portal.css. The symptom was specific and misleading: rules
     using it looked like they were losing the cascade when they were winning
     it and painting the old colour. Check `--x: var(--y)` at :root before
     concluding a rebind failed. */
  --onyx-dk:        var(--ch8-onyx);
  /* ⚠ NOT --ch8-onyx-48. That is chapter 8.2's PENDING step and its own row
     says "large text only — never an 11px label"; --sage-lt paints .item-num,
     which renders at 6.9px. Bound there first and measured at 3.18:1, which is
     WORSE than the sage it replaced (5.3:1) — a rebind can regress a contrast
     the old palette happened to pass. Onyx 62% is the last AA text step. */
  --sage-lt:        var(--ch8-onyx-62);   /* the lighter structural teal: ordinals, quiet marks */
  --white:          var(--ch8-linen);     /* chapter 04: the ground is never pure white */
  --greige:         var(--ch8-alabaster);
  --gold:           var(--ch8-amber);
  --overlay-bg:     color-mix(in srgb, var(--ch8-onyx) 70%, transparent);
  /* §8.8 — depth is a hairline, never a shadow. --shl is portal.css's raised
     elevation and is the only one of the three that still drew one. */
  --sh:             none;
  --sh-hover:       none;
  --shl:            none;

  /* portal-v10.css keeps a SECOND token set of its own, and four of its six
     values are the retired palette and the retired faces. Rebinding them here
     is far higher leverage than chasing the rules that read them: --v10-rule
     is read 5 times, --v10-rule-edge 4, --v10-ui 3, --v10-title 2. Found from
     the other end — a sage border survived on .bb-slot after its colour and
     face were already correct, and the rule painting it named no hex at all. */
  --v10-rule:       var(--ch8-onyx-12);
  --v10-rule-edge:  var(--ch8-onyx-20);
  --v10-rule-ink:   color-mix(in srgb, var(--ch8-linen) 26%, transparent);
  --v10-title:      var(--serif);
  --v10-ui:         var(--sans);
  --v10-display:    var(--serif);

  /* §8.6 — the typefaces. Crimson Pro and Inter, chapter 05 unchanged.
     The wordmark keeps Montserrat and has its own rule below. */
  --serif:          "Crimson Pro", Georgia, "Times New Roman", serif;
  --sans:           "Inter", -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* §8.8 — layout. 1240px container, 660px measure. */
  --container:      1240px;
  --measure:        660px;

  background: var(--ch8-linen);
  color: var(--ch8-onyx);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.5 RULE 2 — THE DARK PLATES, AND THE ONLY PLACE THE LIFTED STOPS PAINT
   (AINS §24 / §50A, ledger row 24-a residues (2) and (3); owner, 2026-09-04.)

   Chapter 8.5 rule 2: "on light grounds use the value above; on Onyx and
   Bordeaux use the lifted stop". The block above binds the retired names FLAT,
   which is right on Linen and Alabaster and wrong on a dark plate — measured,
   with `--ch8-onyx` #141414 as the ground:

       --gold  → Amber      3.39      Amber lifted      4.52
       --forest→ Moss       2.91      Moss lifted       4.54
       --bloodstone→Crimson 2.17      Crimson lifted    4.55
       --brick → Terracotta 3.39      Terracotta lifted 4.51
       --sage  → Onyx 78%   1.92      Onyx 34%          7.60

   ⚠ THE OWNER'S RULING IS THE REASON THIS IS A SCOPE AND NOT A REBINDING. Asked
   whether the sage names should take Onyx 78% (which every tinted panel needs)
   or keep a value a dark plate can carry, the answer was BOTH — 78% flat, and a
   scoped dark-plate token so no plate regresses. The same shape for the three
   functional names: the flat binding stays for light grounds, a lighter variant
   applies only here. One rule may not be right on two grounds, so the ground
   gets its own scope.

   ⚠ THE SCOPE IS THE MEASURED SET, NOT A HAND-TYPED LIST. Every selector below
   is a rule whose background RESOLVES — through this file's own rebind and
   tokens.css — to chapter 08's Onyx #141414 or Bordeaux #4C061D. There is no
   class the dark grounds already share and no attribute stamped on them, so the
   set is enumerated, and `test/ains-portal-dark-plate.test.mjs` re-derives it
   from the stylesheets and fails when a plate appears that this list does not
   name. `.plate-dark` leads the list so a new dark surface can opt in by class
   without touching it. A `::after` plate (`.wheel-chip::after`) is deliberately
   absent: `:is()` does not take a pseudo-element, and a pseudo-element has no
   descendants for a token to inherit into — its own ink is declared inline.

   ⚠ WHAT THIS BLOCK DOES NOT CLAIM. On BORDEAUX the lifted stops read 3.80–3.84,
   not the 4.5 the brand book states for them — the book's "each at 4.5:1" holds
   on Onyx and not on Bordeaux, and that is measurement, not opinion. Every
   Bordeaux plate in the Portal today is a button `:hover` carrying Linen type
   (13.86), so nothing reads a lifted stop there; a functional colour as a WORD
   on Bordeaux would have to be a filled chip under rule 8.5-1 instead. The lift
   is still bound here because it is strictly better than the flat value on that
   ground (3.84 against 1.83) and because leaving one of the book's two dark
   grounds out would make the scope mean something narrower than its name.

   ⚠ AN ELEMENT THAT PAINTS ITS OWN GROUND INLINE IS NOT ON ITS CLASS'S PLATE, AND THE RIG
   PROVED IT. A class is a claim about a ground, and an inline `background` overrules it: the
   first run of this scope put `#portalSeatingPlansBtn` — a `.tng-generate-btn` whose markup says
   `background:none` — at 2.16:1, because it inherited the dark-plate quiet ink onto Linen. axe
   reported it on desktop/guests/rsvp and nothing else did. Measured across `client-portal.html`
   and every `portal-*.js`, exactly three elements carrying a scoped class override their
   background inline: two `.tng-generate-btn` (to `none`, a light ground) and one
   `.share-copy-btn` (to Moss — which would have taken the LIFTED moss under Linen type, 3.62,
   a regression the walk never reaches). `:not([style*="background"])` states the rule the leak
   taught: the scope follows the ground, and an element that declares its own leaves it.

   ⚠ AND WHAT IT DELIBERATELY LEAVES ALONE. `--sendback` / `--bloodstone-dk`
   have NO text form (8.5 rule 3 — a button, never a status), so they are not
   lifted. `--onyx` / `--ink-*` and the hairline steps are not rebound either:
   measured, no rule paints them as text on an Onyx or Bordeaux plate today (the
   28 ink/plate pairs that exist are Linen, at 13.39–16.43), and a binding with
   no reader is a claim nothing checks. Nested light grounds were measured too —
   exactly one exists, `.bb-preview iframe`, and custom properties do not cross
   a document boundary, so nothing inherits a lifted stop onto a pale panel.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] :is(.plate-dark,
  /* §9q, 2026-09-05: the sub-nav strip is a Bordeaux plate and belongs in this scope — a
     functional colour used as a word there would otherwise keep its light-ground value. */
  .roomviews,
  .addr-add-btn:hover, .ag-search-btn, .ann-push-btn:hover, .ann-tip-btn:hover,
  .approval-send-btn:hover, .atl-reveal-btn, .bb-preview, .bb-thumb, .btn-sendback,
  .bub-cta:hover, .cdl-revision-submit:hover, .cpp-sidebar, .dhb-btn-primary, .dir-buy-btn,
  .dir-download-btn, .doc-btn-download:hover, .doc-btn:hover, .dsc-btn:hover,
  .dwb-cta:hover, .es-btn:hover, .es-prog-fill, .fsc-view-suite-btn:hover, .gen-btn:hover,
  .gs-cal-day, .gs-cal-month, .guest-edit-save:hover, .int-admin-save:hover,
  .intentions-reveal-btn:hover, .keepsake-btn-dark:hover, .lg-no, .login-btn:hover,
  .login-screen, .menu-dc.on, .ndl-review-btn:hover, .nsc-btn:hover, .pal-mode-btn.active,
  .pbr-begin:hover, .portal-loading-overlay, .portal-toast, .qr-dl-btn:hover,
  .qr-gen-btn:hover, .rlm-btn, .ros-add-btn:hover, .rsb-btn:hover, .rsb-export-btn,
  .rsvp-add-btn:hover, .rsvp-export-cta, .rvm-btn-primary, .sc-next-btn,
  .seating-apply-btn:hover, .seating-toggle-btn.active, .share-copy-btn:hover,
  .social-dl-btn:hover, .submit-btn:hover, .suc-primary-btn:hover, .tab-panel .dna-band,
  .testi-submit-btn, .thread-send:hover, .tng-apply-btn:hover, .tng-gen-btn:hover,
  .tng-generate-btn, .tng-mode-tab.active, .upgrade-btn:hover, .upgrade-card-btn:hover,
  .vendor-print-btn:hover, .wm-btn:hover, [data-signal="sendback"]):not([style*="background"]){
  /* the quiet layer, inverted: on a dark plate the DIMMER step is the lighter
     one. Onyx 34% is 7.60 here and 6.41 on Bordeaux — the quietest chapter-08
     step that clears AA on BOTH dark grounds (48% is 5.16 on Onyx but 4.36 on
     Bordeaux, so it would be right on one plate and wrong on the other). Its
     row in tokens.css reads "disabled controls — never text a client must
     read"; that is its role at 2.16:1 on LINEN, and a step's role inverts with
     its ground, which is the whole reason this scope exists. */
  --sage:           var(--ch8-onyx-34);
  --sage-dk:        var(--ch8-onyx-34);
  --sage-lt:        var(--ch8-onyx-34);
  --ash:            var(--ch8-onyx-34);
  /* the functional layer in its text form — 8.5 rule 2. Every alias the block
     above points at a functional colour moves with it, or a rule reading
     `--warn` would keep the flat value while its sibling reading `--gold`
     took the lift. */
  --gold:           var(--ch8-amber-lift);
  --gold-ink:       var(--ch8-amber-lift);
  --ochre:          var(--ch8-amber-lift);
  --late:           var(--ch8-amber-lift);
  --warn:           var(--ch8-amber-lift);
  --forest:         var(--ch8-moss-lift);
  --done:           var(--ch8-moss-lift);
  --success:        var(--ch8-moss-lift);
  --bloodstone:     var(--ch8-crimson-lift);
  --action:         var(--ch8-crimson-lift);
  --brick:          var(--ch8-terracotta-lift);
  --bad:            var(--ch8-terracotta-lift);
  --broken:         var(--ch8-terracotta-lift);
}

/* A LIGHT CARD INSIDE A DARK PLATE IS A LIGHT GROUND AGAIN. `.login-screen` is
   Onyx and sits in the scope above, but `.login-box` paints Linen on top of it,
   and custom properties inherit by the DOM, not by the painted ground — so the
   labels, hint, legal line and both policy links read Onyx 34% on Linen (2.16:1)
   and the sign-in button ran crimson-lift (3.61:1). Measured 2026-09-04 by
   test/ains-portal-firstpaint-a11y.test.mjs case 1 (11 axe nodes), introduced
   by the rebind at a139e95d1. The card takes the flat light-ground bindings
   back, by name — the same 17 the scope moves, so a step added to one must be
   added to the other (the guard reads both lists). */
.login-screen .login-box{
  --sage:           var(--ch8-onyx-78);
  --sage-dk:        var(--ch8-onyx-78);
  --sage-lt:        var(--ch8-onyx-62);
  --ash:            var(--ch8-onyx-78);
  --gold:           var(--ch8-amber);
  --gold-ink:       var(--ch8-amber);
  --ochre:          var(--ch8-amber);
  --late:           var(--ch8-amber);
  --warn:           var(--ch8-amber);
  --forest:         var(--ch8-moss);
  --done:           var(--ch8-moss);
  --success:        var(--ch8-moss);
  --bloodstone:     var(--ch8-crimson);
  --action:         var(--ch8-crimson);
  --brick:          var(--ch8-terracotta);
  --bad:            var(--ch8-terracotta);
  --broken:         var(--ch8-terracotta);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.6 / §8.7 — TYPE

   One sweep, then the exceptions. Everything on the screen takes Inter; the
   serif steps are re-declared per element below because #roomsCss names
   "Crimson Pro" directly and this has to agree with it, not fight it.

   ⚠ THE WORDMARK IS THE ONE MONTSERRAT THAT STAYS (§8.6: "three families
   either way, because the wordmark keeps Montserrat regardless"). It is
   excluded by selector, not by hoping the sweep misses it.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab],
body[data-active-tab] .portal-hdr,
body[data-active-tab] .portal-hdr *:not(.hdr-logo):not(.hdr-logo *),
body[data-active-tab] #nextStepCard *,
body[data-active-tab] #hostReadinessMount *,
body[data-active-tab] #hostChangesMount *,
body[data-active-tab] .tab-panel,
body[data-active-tab] .tab-panel *,
body[data-active-tab] #eventSetupCard *,
body[data-active-tab] #ownershipCard *,
body[data-active-tab] #headcountCard *,
body[data-active-tab] #thankYouCard *{
  font-family: var(--sans);
}

/* THE THREE SURFACES THE SWEEP ABOVE COULD NOT REACH, and why naming them is the
   fix rather than widening the selector. `body[data-active-tab]` sets the face on
   the BODY, so it only reaches an element that inherits; portal.css gives each of
   these its own `font-family:'Montserrat'` (536 declarations in that file), and a
   declaration beats inheritance whatever the specificity of the ancestor's rule.
   Measured on the running page: 11 Montserrat nodes on every logged-in screen and
   21 more before the door — §05 permits Montserrat for the wordmark and for nothing
   else ("not available for headings, labels, buttons"), and both wordmarks are
   excluded below by selector.

     · .skip-link      — the first control a keyboard client meets
     · #portalFooter   — Terms, Privacy, Contact: 9 links on all 95 room views
     · .login-screen   — the whole sign-in form, i.e. the first Evorrah screen a
                         client sees after paying. Its colour was already chapter 08;
                         only its lettering said a different company.

   ⚠ THE DOOR IS THREE PANELS, NOT ONE, AND NAMING ONE OF THEM BY ID LEFT TWO BEHIND
   (EVR-085, measured 2026-09-09). This block read `#loginScreen` until today, and
   `client-portal.html` carries three `.login-screen` panels — `#loginScreen` (882),
   `#activationScreen` (1002) and `#forgotScreen` (1040). Only the first was swept, so
   the screen a client reaches when their access code did not arrive — the one moment
   they are already unsure the company is real — was the only Evorrah surface still set
   in a retired face. scripts/portal-typeface-census.mjs, walking the door before login,
   found six: `p.login-sub`, `label`, `button#forgotBtn.login-btn`, `p.login-recover-lead`,
   `a.login-recover-btn` (the mailto that reaches a human) and `button.login-text-btn`,
   at both widths. The class is what the three panels share, and portal.css's own rules
   for them top out at `a.login-recover-btn` (0,2,1) — below this selector's (0,4,1) —
   so the swap costs no specificity it needed.

   Not a bulk edit of portal.css: that file is edited by several sessions at once and
   the repo's own rule forbids sweeping a face across it in one pass. */
body[data-active-tab] .skip-link,
body[data-active-tab] #portalFooter,
body[data-active-tab] #portalFooter *,
body[data-active-tab] .login-screen,
body[data-active-tab] .login-screen *:not(.login-logo):not(.login-logo *){
  font-family: var(--sans);
}

/* §05 admits Inter 300/400/500 and no other weight; .skip-link and the login labels
   were set 600, which is the weight Montserrat needed to hold its own and Inter does
   not. 500 is the button/label weight the book names. */
body[data-active-tab] .skip-link,
body[data-active-tab] .login-screen label,
body[data-active-tab] .login-screen .login-btn{
  font-weight: 500;
}

/* The door's own wordmark — same locked setting as .hdr-logo, on all three panels. */
body[data-active-tab] .login-screen .login-logo,
body[data-active-tab] .login-screen .login-logo *{
  font-family: "Montserrat", -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-weight: 500;
}

/* The eye toggle inside the access-code field carried NO family at all, so it fell to
   the UA's Arial — a fifth family on a client-facing surface, which §05 calls drift
   outright. It is a <button>, and a bare <button> does not inherit font. */
body[data-active-tab] .login-screen button{
  font-family: var(--sans);
}

/* §8.7's floor is 0.62rem and "the smallest type permitted"; the footer's legal row
   rendered at 9px — the row that carries Terms of Use, Privacy Policy and Contact,
   i.e. the one place a client may actually need to read the small print. 0.68rem is
   12.2px at this root, which also clears the 12px floor
   scripts/portal-brand-conformance.mjs holds. Line-height opens with it so the links
   keep their 44px tap target rather than gaining one. */
body[data-active-tab] #portalFooter,
body[data-active-tab] #portalFooter *{
  font-size: 0.68rem;
  line-height: 1.9;
}

/* The wordmark — Montserrat 500, uppercase, and its tracking is the brand's,
   not a UI label's. Restated rather than merely exempted, so the sweep above
   cannot leave it inheriting something else. */
body[data-active-tab] .hdr-logo,
body[data-active-tab] .hdr-logo *{
  font-family: "Montserrat", -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-weight: 500;
}

/* Serif — §8.7: titles, card values, piece names and figures. Crimson Pro 300,
   which is where the face was drawn to sit; the shipped rules ask for 400.

   ⚠ THE READINESS ARMS CARRY #hostReadinessMount AND MUST KEEP IT — THEY WERE DEAD
   RULES WITHOUT IT (measured on :4611, 2026-09-08; owner: "formatting issue here").
   The sweep above names that card's container by ID — `#hostReadinessMount *` is
   (1,1,1) — while a bare `body[data-active-tab] .hr-h` is (0,2,1). Two classes do not
   beat an id, so §8.7's serif never landed: "Event readiness" rendered Inter 300 while
   the sub-line under it rendered Crimson Pro 400, and the card read with its heading
   and its subtitle the wrong way round. The other arms need no such qualification —
   `.tab-panel .td-h` is (0,3,1) against a (0,2,1) sweep, and the #eventSetupCard arms
   already name their own id — which is why this one selector family was the only
   casualty and why it went unseen. `.nsc-title` is in the same trap and is NOT fixed
   here on purpose: portal-v10.css pins it to var(--v10-display) with !important, so it
   renders Crimson Pro anyway and adding an id would change nothing on the screen. */
body[data-active-tab] .nsc-title,
body[data-active-tab] #hostReadinessMount .hr-h,
body[data-active-tab] #hostReadinessMount .hr-num,
body[data-active-tab] .tab-panel .td-h,
body[data-active-tab] .tab-panel .ap-h,
body[data-active-tab] .tab-panel .rm-wall-h,
body[data-active-tab] .tab-panel .td-next .t,
body[data-active-tab] .tab-panel .td-letter .nm,
body[data-active-tab] .tab-panel .td-stats b,
body[data-active-tab] .tab-panel .dna-nm,
body[data-active-tab] #eventSetupCard .es-title,
body[data-active-tab] #eventSetupCard .es-mod-title,
body[data-active-tab] .tab-panel .s1__headline,
body[data-active-tab] .tab-panel .s1__item > .s1__label,
body[data-active-tab] .tab-panel .s1__clear{
  font-family: var(--serif);
  font-weight: 300;
  letter-spacing: -.008em;
}
/* S1 · YOUR EVENT — the queue's number and each row's sentence. portal-rooms.js's S1
   sheet asks for Crimson Pro on both (its `.s1__headline` / `.s1__label` rules), and
   §8.7 pairs 1.05rem with Crimson Pro 300 — but a bare class is (0,1,0) and the sans
   sweep above is (0,2,1), so the serif never landed and the queue read as Inter at a
   size no sans step owns (measured :4620, 2026-09-08). The child combinator keeps the
   withheld band's `.s1__label` (a quiet Montserrat-turned-Inter meta row) out of it.
   The cost line was 0.72rem lowercase and untracked, which is the LABEL step's size
   without its setting; --text-sm 400 is the book's quiet sub-line. */
body[data-active-tab] .tab-panel .s1__cost{
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ch8-onyx-62);
}
/* The row sentences run 400, not the serif list's 300 (owner, 2026-09-08: "S1 rows at
   400"). Same reading as app/v15/style.css's `.srf-sm`: Crimson Pro Light at text size
   (16.8px here) goes grey on Linen, and a row that asks for a decision must not. The
   headline above keeps 300 — it is 20.8px, above the site's 20px line. */
body[data-active-tab] .tab-panel .s1__item > .s1__label{
  font-weight: 400;
}

/* §8.7's two unstated rules. Uppercase always takes letter-spacing — never set
   capitals at 0 — and anything that is a column of digits is tabular, or the
   rows stop aligning as the numbers change. */
body[data-active-tab] .hr-num,
body[data-active-tab] .hr-unit,
body[data-active-tab] .es-prog,
body[data-active-tab] .tab-panel .td-stats b,
body[data-active-tab] .tab-panel .ap-v,
body[data-active-tab] .tab-panel .td-letter .dt,
body[data-active-tab] .tab-panel .td-next .t{
  font-variant-numeric: tabular-nums;
}

/* Body — §8.7: --text-base, Inter 300. Running prose stops being signage.
   ⚠ .td-p and .ap-p are the two Libre Baskerville declarations that render on
   this screen; §8.6 retires the face and this is where it stops here. */
body[data-active-tab] .tab-panel .td-p,
body[data-active-tab] .tab-panel .ap-p,
body[data-active-tab] .nsc-action,
body[data-active-tab] .tab-panel .rm-sec-b{
  font-family: var(--sans);
  font-weight: 300;
  font-size: var(--text-base);
  line-height: 1.7;
  color: var(--ch8-onyx-78);
  max-width: var(--measure);
}
body[data-active-tab] .tab-panel .td-p strong,
body[data-active-tab] .tab-panel .ap-p b{
  color: var(--ch8-onyx);
  font-weight: 500;
}

/* Field labels and eyebrows — §8.10: --text-xs Inter 500, uppercase 0.18em,
   Onyx 62%. The tracked-label family across the four blocks on this screen. */
body[data-active-tab] .nsc-eyebrow,
body[data-active-tab] .hr-label,
body[data-active-tab] .hr-sub,
body[data-active-tab] .tab-panel .td-lbl,
body[data-active-tab] .tab-panel .td-reg,
body[data-active-tab] .tab-panel .rm-sec-h,
body[data-active-tab] .tab-panel .rm-wall-k,
body[data-active-tab] .tab-panel .td-letter .dt,
body[data-active-tab] #eventSetupCard .es-mod-num{
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ch8-onyx-62);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.1 / §8.2 — GROUND AND THE QUIET LAYER

   One ground, not two. The screen had #F8F1E7 on 29 elements and the retired
   #FBF7EF on 3, which is the kind of difference nobody sees and everybody
   half-notices.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .portal-hdr,
body[data-active-tab] .hdr-top,
body[data-active-tab] .rooms,
body[data-active-tab] .roomviews,
body[data-active-tab] main,
body[data-active-tab] #nextStepCard,
body[data-active-tab] #hostReadinessMount,
body[data-active-tab] .tab-panel,
body[data-active-tab] .tab-panel .td-wrap{
  background: var(--ch8-linen);
}

/* Panels — §8.1: "Alabaster is the panel a card sits on." */
body[data-active-tab] .tab-panel .td-stats,
body[data-active-tab] .tab-panel .ir-wrap,
body[data-active-tab] .tab-panel .rm-pin,
body[data-active-tab] .tab-panel .fstates,
body[data-active-tab] .tab-panel .rm-sum div{
  background: var(--ch8-alabaster);
}

/* Ink — the three steps §8.2 actually uses for reading. Everything the shipped
   rules set at rgba(30,11,14,.62 … .75) collapses onto two named stops. */
body[data-active-tab] .nsc-inner,
body[data-active-tab] .hr-card,
body[data-active-tab] .tab-panel .td-row-t,
body[data-active-tab] .tab-panel .ir-t,
body[data-active-tab] .tab-panel .td-letter .nm,
body[data-active-tab] .tab-panel .td-next .t,
body[data-active-tab] .tab-panel .td-h,
body[data-active-tab] .tab-panel .ap-h,
body[data-active-tab] .nsc-title,
body[data-active-tab] .hr-h{
  color: var(--ch8-onyx);
}
body[data-active-tab] .tab-panel .td-row-s,
body[data-active-tab] .tab-panel .td-next .s,
body[data-active-tab] .tab-panel .ir-m,
body[data-active-tab] .tab-panel .ir-key,
body[data-active-tab] .tab-panel .td-cost,
body[data-active-tab] .tab-panel .ap-v,
body[data-active-tab] .tab-panel .ap-q,
body[data-active-tab] .tab-panel .td-stats span,
body[data-active-tab] .hr-foot,
body[data-active-tab] .nsc-meanwhile{
  color: var(--ch8-onyx-62);
}

/* §8.2 — pending is quiet information, not an alarm, and it is LARGE TEXT
   ONLY: Onyx 48% is 3.18:1, which clears AA for large text and nothing else.
   Never let this class land on an 11px label. */
body[data-active-tab] .tab-panel .td-stats div:not(.hot) b{
  color: var(--ch8-onyx-78);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.3 / §8.4 — THE FUNCTIONAL LAYER

   The readiness card was the single worst offender: the figure, the unit, the
   sub-line, the bar fill AND the track were all one structural teal, so a card
   whose whole job is to say how ready an event is said it in a colour that
   means nothing. Under §8.2 the reading matter goes quiet and the STATE goes
   to the bar — which is non-text and so may carry a functional colour at the
   3:1 floor rather than 4.5:1.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .hr-num,
body[data-active-tab] .hr-unit,
body[data-active-tab] .hr-pct{
  color: var(--ch8-onyx);
}
body[data-active-tab] .hr-bar{
  background: var(--ch8-onyx-20);
  border-radius: 0;
}
body[data-active-tab] .hr-fill{
  background: var(--ch8-moss);
  border-radius: 0;
}

/* Status pills — §8.10: filled chip in the role colour, Linen type, 10×16px,
   --text-sm uppercase 0.12em, weight 500, radius 0. One per row.
   ⚠ "Waiting on you" is NEXT STEP, not an alarm. "With us" and "nothing
   needed" are the quiet layer by §8.5 — once the ball is with us the event is
   with us — so they take an Onyx step, which as a FILL still carries Linen
   type at 4.94:1. */
body[data-active-tab] .nsc-ball{
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-radius: 0;
  line-height: 1;
}
body[data-active-tab] .nsc-ball-you{
  background: var(--ch8-crimson);
  color: var(--ch8-linen);
}
body[data-active-tab] .nsc-ball-maison,
body[data-active-tab] .nsc-ball-none{
  background: var(--ch8-onyx-62);
  color: var(--ch8-linen);
}

/* Lane rules — the three owners a Today row can have. §8.5's fill rule does not
   apply to a 2px rule, which carries no type; the 3:1 non-text floor does, and
   all three clear it on Linen. */
body[data-active-tab] .tab-panel .td-lane{
  border-left-color: var(--ch8-onyx-20);
}
body[data-active-tab] .tab-panel .td-lane.you{ border-left-color: var(--ch8-crimson); }
body[data-active-tab] .tab-panel .td-lane.us{ border-left-color: var(--ch8-amber); }
body[data-active-tab] .tab-panel .td-lane.auto{ border-left-color: var(--ch8-onyx-62); }
body[data-active-tab] .tab-panel .td-lane.you .td-lbl{ color: var(--ch8-crimson); }
body[data-active-tab] .tab-panel .td-lane.us .td-lbl{ color: var(--ch8-amber); }
body[data-active-tab] .tab-panel .td-lane.auto .td-lbl{ color: var(--ch8-onyx-62); }
/* §24 (row 50/50A/50B, 2026-09-04): a functional colour is never small text on a pale panel. The
   lane eyebrows keep their lane colour and take the 12px floor; the setup card's links and its
   required mark likewise. Measured, not assumed: scripts/portal-brand-conformance.mjs. */
body[data-active-tab] .tab-panel .td-lane.us .td-lbl,
body[data-active-tab] .tab-panel .td-lane.you .td-lbl{ font-size: .75rem; }
body[data-active-tab] #eventSetupCard .es-link,
body[data-active-tab="today"] .tab-panel .es-req{ font-size: max(.75rem, 1em); }

/* ── EVENT SET-UP CARD — §8.7 steps on the sentence-case form ─────────────────
   Measured on :4611, 2026-09-08 (owner: "fix the set-up card type"): the field labels
   ran 0.95rem, the booking line 0.95rem, the "Change" link resolved the floor above to
   1em = 16px, and the progress row, the save state and the section status chips carried
   NO size rule at all — 12px and 9.92px, inherited. None of those is a §8.7 step.
   Direction A (owner EVD-022, 2026-09-05, portal.css ~8140) is FORM — sentence-case
   labels and buttons, one filled primary — and is kept whole: nothing below changes
   case; each rule only moves a size onto the scale, or gives a label the weight its
   step carries. Every selector carries the card's id because portal.css sets these by
   class and Direction A's own block sits 8,000 lines in.
   .es-btn was first left at Direction A's 0.92rem !important as that ruling's own choice;
   the owner then said "buttons too" (2026-09-08), so it takes --text-sm below — the one
   !important in this block, because nothing weaker beats portal.css's. Case stays. */
body[data-active-tab] #eventSetupCard .fld-lbl{
  font-size: var(--text-base);
  font-weight: 500;
}
body[data-active-tab] #eventSetupCard .fld-hint{
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ch8-onyx-62);
}
body[data-active-tab] #eventSetupCard .es-booking-who{
  font-size: var(--text-base);
  font-weight: 300;
}
body[data-active-tab] #eventSetupCard .es-booking-who strong{ font-weight: 500; }
body[data-active-tab] #eventSetupCard .es-booking-lbl{
  font-size: var(--text-xs);
  font-weight: 500;
}
body[data-active-tab] #eventSetupCard .es-prog,
body[data-active-tab] #eventSetupCard .es-prog span,
body[data-active-tab] #eventSetupCard .es-save,
body[data-active-tab] #eventSetupCard .es-mod-status{
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ch8-onyx-62);
}
body[data-active-tab] #eventSetupCard .es-howto,
body[data-active-tab] #eventSetupCard .es-mod-title em{ font-weight: 500; }
/* Re-measured after the block above: three quiet meta lines on the Now view still ran
   300 at the 0.72 / 0.78rem steps, where §8.7 has no 300 — 400 is the meta weight. */
body[data-active-tab] .tab-panel .td-mile,
body[data-active-tab] .tab-panel .td-next .s{ font-weight: 400; }
body[data-active-tab] #eventSetupCard .es-link,
body[data-active-tab] #eventSetupCard .es-change{
  font-size: var(--text-sm);
  font-weight: 500;
}
body[data-active-tab] #eventSetupCard .es-btn,
/* and the Now view's next-step button, which shares Direction A's rule
   (owner, 2026-09-08: "next-step button too") */
body[data-active-tab] #nextStepCard .nsc-btn{
  font-size: var(--text-sm) !important;
}
/* Two Today-room siblings found in the same census, both below or beside the scale:
   the readiness summary's bold labels ran 0.58rem — under §8.7's 0.62rem floor, "the
   smallest type permitted" — and The Road's step titles ran 0.86rem with 0.58rem dates. */
body[data-active-tab] .tab-panel .rm-sum b,
body[data-active-tab] .tab-panel .rd-dt{
  font-size: var(--text-micro);
  font-weight: 500;
  letter-spacing: .22em;
}
body[data-active-tab] .tab-panel .rd-t{ font-size: var(--text-base); }

/* Status dots — §8.10: 8px circle in the role colour, radius 50%, and ONLY
   where a word is already present. Every one of these sits beside its label. */
body[data-active-tab] .tab-panel .ir-k{
  width: 8px;
  height: 8px;
  border-radius: var(--radius-round);
}
body[data-active-tab] .tab-panel .ir-k.you{ background: var(--ch8-crimson); }
body[data-active-tab] .tab-panel .ir-k.us{ background: var(--ch8-amber); }
body[data-active-tab] .tab-panel .ir-k.auto{ background: var(--ch8-onyx-62); }

/* The room rail's attention pip and count, and the hot figures. */
body[data-active-tab] .rooms .rm-dot,
body[data-active-tab] .ptab-attn-pip{ background: var(--ch8-crimson); }
body[data-active-tab] .rooms button.on{ border-bottom-color: var(--ch8-crimson); }
body[data-active-tab] .rooms button.on .rm-cnt{
  background: color-mix(in srgb, var(--ch8-crimson) 12%, transparent);
  color: var(--ch8-crimson);
}
body[data-active-tab] .tab-panel .td-stats div.hot b,
body[data-active-tab] .tab-panel .fst.hot b{ color: var(--ch8-crimson); }

/* The announcement strip — "running late" is attention, not alarm (§8.3). */
body[data-active-tab] .tab-panel .rm-ann{
  border-left-color: var(--ch8-amber);
  background: color-mix(in srgb, var(--ch8-amber) 7%, transparent);
  color: var(--ch8-onyx-78);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.10 — COMPONENTS
   Sizes are the shipping ones; only the colours, faces and corners move.
   ─────────────────────────────────────────────────────────────────────────── */

/* Primary button — Mahogany fill, Linen type, NO BORDER. 12×24, --text-sm
   uppercase 0.16em, weight 500. Hover darkens 8%. */
body[data-active-tab] .nsc-btn,
body[data-active-tab] .tab-panel .td-btn.pri,
body[data-active-tab] #eventSetupCard .es-btn{
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  padding: 12px 24px;
  border: none;
  border-radius: 0;
  background: var(--ch8-crimson);
  color: var(--ch8-linen);
  box-shadow: none;
  transition: background var(--dur-quick) var(--ease);
}
body[data-active-tab] .nsc-btn:hover,
body[data-active-tab] .tab-panel .td-btn.pri:hover,
body[data-active-tab] #eventSetupCard .es-btn:hover{
  background: color-mix(in srgb, var(--ch8-crimson) 92%, #000);
}
/* Disabled — §8.10: Onyx 34% fill, Onyx 62% type. Not opacity: a control at
   0.4 opacity multiplies against whatever it already was, which is the
   compound-dimming trap portal.css records having been caught by twice. */
body[data-active-tab] .nsc-btn:disabled,
body[data-active-tab] .tab-panel .td-btn.pri:disabled,
body[data-active-tab] #eventSetupCard .es-btn:disabled{
  background: var(--ch8-onyx-34);
  color: var(--ch8-onyx-62);
  opacity: 1;
}

/* Ghost button — transparent, 1px Onyx 35% border, Onyx type, same box.
   "For Open, Download, Cancel — anything that is not a decision." */
body[data-active-tab] .tab-panel .td-btn:not(.pri),
body[data-active-tab] .roomviews button:not(.on){
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  border: 1px solid var(--ch8-onyx-34);
  border-radius: 0;
  background: transparent;
  color: var(--ch8-onyx);
  box-shadow: none;
  transition: border-color var(--dur-quick) var(--ease);
}
body[data-active-tab] .tab-panel .td-btn:not(.pri){ padding: 12px 24px; }
body[data-active-tab] .tab-panel .td-btn:not(.pri):hover,
body[data-active-tab] .roomviews button:not(.on):hover{
  border-color: var(--ch8-onyx);
}

/* The two lights — §8.4. Approve is Moss Fern, Send it back is Night Bordeaux,
   same box so the pair reads as one control, and neither is ever an outline:
   "the positive action is always filled." Bound here so the decision panels
   these buttons open are already correct when they mount into this room. */
body[data-active-tab] .btn-approve,
body[data-active-tab] .lg-ok,
body[data-active-tab] [data-signal="approve"]{
  background: var(--ch8-signal-approve);
  border: none;
  border-radius: 0;
  color: var(--ch8-linen);
  padding: 18px 28px;
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
}
body[data-active-tab] .btn-sendback,
body[data-active-tab] .lg-no,
body[data-active-tab] [data-signal="sendback"]{
  background: var(--ch8-bordeaux);
  border: none;
  border-radius: 0;
  color: var(--ch8-linen);
  padding: 18px 28px;
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
}

/* ══ THE HOUSE 44px TOUCH FLOOR, THE CONTROLS THAT WERE STILL UNDER IT ══════════
   Owner, 2026-09-07. scripts/portal-a11y-audit.mjs applies this floor on a phone, and these
   are what it still reported there. Every box below is MEASURED on an iPhone 13, in the room
   named beside it, before anything was changed:

     .bb-vtab            256x41   Files/Design · the version tabs
     .bb-file-btn        272x37   Files/Design · OPEN ↗
     .btn-ghost           84x30   Experience   · "Set these up"
     #rehearsalToggle    254x33   Experience   · "Read what your guests will read"
     #bcKind/#bcAudience/#bcChannel  x38  Experience · the broadcast selects
     #bcSubject          260x38   Experience   · the broadcast subject
     #bcProofBtn         129x32   Experience   · "Send me a proof"
     .gen-btn            215x35   Experience   · "Create story images →"
     .dhb-btn-primary    350x43   Files        · "Leave a Review →" — ONE pixel under
     .testi-submit-btn   188x40   Files        · "Submit Review →"
     .bc-act              — x32   Experience   · "See who this reaches" · "Save as a draft"
     .er-open           156x34   After        · "Share this story"
     .ty-act             73x28   After        · "Copy all" and the five "Restore the suggested
                                    wording for …" — bare <button style="…"> in
                                    portal-thankyou.js, so they carry a class now for the same
                                    reason .bc-act and .sp-act do. ⚠ THESE WERE INVISIBLE UNTIL
                                    THE WALKER WAS FIXED: the After room is gated behind an
                                    event that has happened, the audit runs the delivered
                                    fixture, and "the rail hides this screen" was all it could
                                    say. Measured on --stage after, which is what that warning
                                    now names.
     .sp-act              — x<44  Experience   · "▸ Send in batches", from send-plan.js. ⚠ THAT
                                    MODULE IS SHARED WITH THE CRM — it renders in both and
                                    branches on mode(el)==='crm'. This floor is scoped to
                                    `body[data-active-tab]`, which the CRM's body does not
                                    carry, so the studio's own chrome is untouched. Its sibling
                                    "New batched send" carries the class too: it only renders
                                    once the section is expanded, so the audit never saw it,
                                    and a target that is hidden at audit time is still a target.
     .dhb-btn-referral    — x<44  Files        · the satisfaction chips ("1 of 5" … "5 of 5",
                                    "Yes, with pleasure", "Rather not"). ⚠ The class is shared
                                    with a 350x45 button that already clears the floor, so the
                                    floor is a no-op on that one and lands only where it is
                                    needed — a min-height never shrinks anything.

   ⚠ IT IS HERE RATHER THAN IN portal.css's TOUCH BLOCKS BECAUSE THE CASCADE DECIDES IT, NOT
   TIDINESS. These rules are declared across portal.css, portal-v10.css and this file, and a
   floor written into portal.css's block at ~5096 is beaten by any same-specificity rule later
   in that file — the trap that cost a round on `.es-howto`/`.hr-howto`. This sheet loads last
   AND `body[data-active-tab]` is (0,2,0), so it wins on both counts for every selector below.
   ⚠ min-height, NEVER height or padding. The broadcast controls carry INLINE padding from
   portal-broadcast.js; a stylesheet cannot beat that without !important, and it does not need
   to — none of them declares min-height inline, so the floor lands cleanly on top.
   ⚠ THREE OF THEM HAD NO SELECTOR AT ALL. "See who this reaches", "Send me a proof" and "Save
   as a draft" are built in portal-broadcast.js as bare <button style="…"> inside an unnamed
   flex div — nothing to aim a rule at. They carry `class="bc-act"` now: one word in the module
   that renders them, rather than an !important or a structural selector that would break the
   next time that div gains a sibling.
   ⚠ HEIGHT ONLY. Every control here is already wider than 44px; the two that are not
   (`.tstar` at 32x32 and `.testi-public-check` at 24x24) are deliberately NOT in this list —
   they need width as well, which changes how they look, and that is the owner's call. */
@media (max-width:640px), (max-height:500px) and (orientation:landscape), (hover:none) and (pointer:coarse){
  body[data-active-tab] .bb-vtab,
  body[data-active-tab] .bb-file-btn,
  body[data-active-tab] .btn-ghost,
  body[data-active-tab] #rehearsalToggle,
  body[data-active-tab] #bcKind,
  body[data-active-tab] #bcAudience,
  body[data-active-tab] #bcChannel,
  body[data-active-tab] #bcSubject,
  body[data-active-tab] #bcProofBtn,
  body[data-active-tab] .gen-btn,
  body[data-active-tab] .dhb-btn-primary,
  body[data-active-tab] .dhb-btn-referral,
  body[data-active-tab] .bc-act,
  body[data-active-tab] .sp-act,
  body[data-active-tab] .er-open,
  /* ⚠ .ty-act TAKES THE FLOOR THROUGH A VARIABLE, NOT THROUGH min-height. Its buttons are
     built inline in portal-thankyou.js WITH a min-height of their own (24px, the criterion),
     and an inline declaration beats any rule here — measured, the plain min-height below did
     nothing to them. They read `min-height:var(--ty-floor,24px)`, so this sets the variable
     instead and the cascade wins after all. "Copy all" has no inline min-height and takes the
     plain rule; both are listed so neither depends on the other's mechanism. */
  body[data-active-tab] .ty-act,
  body[data-active-tab] .testi-submit-btn{
    min-height: 44px;
  }
  body[data-active-tab] .ty-act{ --ty-floor: 44px; }

  /* ⚠ THE TWO THAT NEED WIDTH, AND THEY ARE A SEPARATE RULE BECAUSE THEY ARE A SEPARATE
     DECISION (owner, 2026-09-07, asked and answered). Everything above is already wider than
     44 and only needed a height; these two are square and small — `.tstar` 32x32 and the
     testimonial consent box 24x24 — so the floor CHANGES HOW THEY LOOK: five stars become a
     visibly larger rating row, and the checkbox a larger box. The checkbox already met WCAG
     2.5.8's 24px; it is here for the house's stricter floor, on the owner's answer.
     ⚠ width/height, not min-*, on the checkbox: portal.css:7891 sets `width:24px;height:24px`
     outright, and a min-width cannot beat an explicit width. `.tstar` sets neither, so a
     min-* is enough there — and it keeps the glyph's own size out of it.
     The flex centring is not decoration: a 1.75rem star in a 44px box sits top-left without
     it, which is the same lesson the skip link's 4.5px taught. */
  body[data-active-tab] .tstar{
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  body[data-active-tab] .testi-public-check{
    width: 44px;
    height: 44px;
  }
}

/* The next-step card's link is a standalone control, not a word in a sentence: measured
   118x24 on a phone, over SC 2.5.8's 24 and under the house's 44px touch floor (owner,
   2026-09-07). It is already inline-flex, so it only needs the height and the centring.
   Scoped to a finger, like every other 44 in this repo; a mouse keeps the 24px criterion. */
@media (max-width:640px), (max-height:500px) and (orientation:landscape), (hover:none) and (pointer:coarse){
  body[data-active-tab] .nsc-link{min-height:44px;align-items:center}
}

/* Link — §8.1: "Mahogany is still the one next step — the primary button, the
   link, the italic turn." */
body[data-active-tab] .nsc-link,
body[data-active-tab] .hr-foot a,
body[data-active-tab] .tab-panel .td-u,
body[data-active-tab] .tab-panel .rm-wall-in a,
body[data-active-tab] #eventSetupCard .es-link{
  color: var(--ch8-crimson);
  border-bottom-color: var(--ch8-crimson);
}
body[data-active-tab] .hdr-support strong,
body[data-active-tab] .hdr-support a{ color: var(--ch8-crimson); }

/* Card — §8.10: Linen or Alabaster ground, 1px hairline, radius 0, 24–32px
   padding, no shadow and no hover lift.
   ⚠ .ap shipped a bloodstone-tinted edge. A card is a hairline; the decision
   it holds is carried by its eyebrow and its buttons, which are the accent. */
body[data-active-tab] .tab-panel .ap,
body[data-active-tab] #eventSetupCard .es-card,
body[data-active-tab] .tab-panel .ir-wrap,
body[data-active-tab] .tab-panel .td-stats,
body[data-active-tab] .tab-panel .rm-pin{
  border: 1px solid var(--ch8-onyx-20);
  border-radius: 0;
  box-shadow: none;
}
/* ⚠ .hr-card IS NOT IN THAT FAMILY, AND PUTTING IT BACK IS THE REGRESSION (2026-09-08,
   owner: "formatting issue here", measured on :4611 at 1280px). It is one of the TWO
   FULL-BLEED BANDS — portal-v10.css:305 and its long note name them: .nsc-inner and
   .hr-card sit in parents that compute padding-left:0, so v10 gives them a top hairline
   and a 64px gutter of their own instead of a box. `border: 1px` above put all four
   sides back on an element 1280px wide inside a 1280px viewport: the left edge landed
   ON the window edge, the right edge landed off it, and the readiness card was the only
   boxed thing on a screen of bands. Its sibling .nsc-inner was never in this list, which
   is why the two disagreed. §8.10 asks for radius 0, no shadow and an Onyx-20 hairline —
   the band already has the first two, and the line below gives it the third. */
body[data-active-tab] .hr-card{
  border: 0;
  border-top: 1px solid var(--ch8-onyx-20);
  border-radius: 0;
  box-shadow: none;
}
body[data-active-tab] .tab-panel .ap{
  background: var(--ch8-alabaster);
  padding: 24px;
}
body[data-active-tab] .tab-panel .ap-k{
  color: var(--ch8-crimson);
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: .18em;
}
/* The locked note inside it — quiet layer, hairline rule, no tint of its own. */
body[data-active-tab] .tab-panel .ap-lock,
body[data-active-tab] .tab-panel .g360-note{
  background: transparent;
  border-left: 1px solid var(--ch8-onyx-20);
  color: var(--ch8-onyx-78);
  font-size: var(--text-xs);
}

/* Input — §8.10 and §8.8: radius 2px, and it is the ONLY thing that gets 2px.
   Linen ground, 1px Onyx 20% border, 12×14px, --text-base Inter 300.
   ⚠ font-size stays at or above 16px on phones or Safari zooms the page on
   focus — the iOS rule portal.css and the a11y audit both enforce. */
body[data-active-tab] .tab-panel input[type="text"],
body[data-active-tab] .tab-panel input[type="email"],
body[data-active-tab] .tab-panel input[type="tel"],
body[data-active-tab] .tab-panel input[type="date"],
body[data-active-tab] .tab-panel input[type="number"],
body[data-active-tab] .tab-panel textarea,
body[data-active-tab] .tab-panel select,
body[data-active-tab] #eventSetupCard .es-input{
  font-family: var(--sans);
  font-weight: 300;
  font-size: var(--text-base);
  padding: 12px 14px;
  border: 1px solid var(--ch8-onyx-20);
  border-radius: var(--radius-hair);
  background: var(--ch8-linen);
  color: var(--ch8-onyx);
}
/* Focus. §8.9 permits swapping the outline for an inset shadow on an inset
   field — but only as "an equally visible replacement", and that is a claim
   about PIXELS, not about intent.

   ⚠ IT WAS NOT EQUALLY VISIBLE, AND ONLY THE PIXEL GATE COULD SAY SO. With
   `outline:none` and the inset ring alone, scripts/portal-a11y-audit.mjs
   reported "0px changed" on 34 controls across Guests, Experience and Files —
   every <select>, and the inputs and textareas of the message composer and the
   guest paste box. Two reasons, both invisible in a stylesheet: a native
   <select> does not paint an inset shadow over its own control layer at all,
   and a 26% tint of the accent is under the detector's threshold on a small
   clip. This rule read as careful and shipped controls with no focus at all.

   So the outline STAYS and the inset is an addition, not a swap. The Today
   room passed with the old rule only because it renders no form control. */
body[data-active-tab] .tab-panel input:focus-visible,
body[data-active-tab] .tab-panel textarea:focus-visible,
body[data-active-tab] .tab-panel select:focus-visible,
body[data-active-tab] #eventSetupCard .es-input:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 3px;
  border-color: var(--ch8-crimson);
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--ch8-crimson) 26%, transparent);
}
/* Error — Terracotta border and a message, never colour alone. */
body[data-active-tab] .tab-panel .es-err,
body[data-active-tab] .tab-panel [aria-invalid="true"]{
  border-color: var(--ch8-terracotta);
}
body[data-active-tab] #eventSetupCard .es-err{
  color: var(--ch8-terracotta);
  font-size: var(--text-xs);
  margin-top: 4px;
}

/* Table row — 1px Onyx 12% bottom rule, hover tints Onyx 4%, no zebra. */
body[data-active-tab] .tab-panel .td-row,
body[data-active-tab] .tab-panel .ir{
  border-bottom: 1px solid var(--ch8-onyx-12);
}
body[data-active-tab] .tab-panel .ir:hover{
  background: color-mix(in srgb, var(--ch8-onyx) 4%, transparent);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.8 — STRUCTURE
   Radius: 0 for everything, 2px for inputs (above), 50% for dots and avatars.
   Depth is a hairline, never a shadow — measured: this screen carried 2px
   corners on eight elements, buttons among them.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .portal-hdr [class*="btn"],
body[data-active-tab] .portal-hdr [class*="chip"],
body[data-active-tab] .roomviews button,
body[data-active-tab] .tab-panel button,
body[data-active-tab] .tab-panel .rm-cnt,
body[data-active-tab] #eventSetupCard button,
body[data-active-tab] #eventSetupCard .es-pill,
body[data-active-tab] #eventSetupCard .es-mod{
  border-radius: 0;
}
body[data-active-tab] .tab-panel .ir-k,
body[data-active-tab] .rooms .rm-dot,
body[data-active-tab] .hdr-bell{
  border-radius: var(--radius-round);
}
/* No card shadow, no hover lift, anywhere on the screen.
   ⚠ `:not(:focus-visible)` IS LOAD-BEARING, NOT TIDINESS. These selectors carry
   an id, so they outrank the focus rules further down whatever the source order
   — and a focus ring drawn as an inset box-shadow (§8.9's permitted replacement
   for an inset field, and the second ring on a filled button) is a box-shadow.
   Without this the blanket rule silently deletes the ring, which is precisely
   the failure the pixel audit exists to catch and cannot be seen in a diff. */
body[data-active-tab] .tab-panel [class]:not(:focus-visible),
body[data-active-tab] #nextStepCard [class]:not(:focus-visible),
body[data-active-tab] #hostReadinessMount [class]:not(:focus-visible),
body[data-active-tab] #eventSetupCard [class]:not(:focus-visible){
  box-shadow: none;
}
/* …except the two places a shadow is doing real work: a field's focus
   replacement ring, and a modal that must separate from the page behind it.
   Re-stated after the blanket rule so source order restores them. */
body[data-active-tab] .tab-panel input:focus-visible,
body[data-active-tab] .tab-panel textarea:focus-visible,
body[data-active-tab] .tab-panel select:focus-visible,
body[data-active-tab] #eventSetupCard .es-input:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 3px;
  box-shadow: inset 0 0 0 3px color-mix(in srgb, var(--ch8-crimson) 26%, transparent);
}
body[data-active-tab] .tab-panel .g360-card{
  box-shadow: 0 20px 60px color-mix(in srgb, var(--ch8-onyx) 30%, transparent);
}

/* Hairlines — one weight, one colour. The portal's soft 8% ink rules were a
   quarter the weight of the site's, which tokens.css names as the reason "the
   site looks drawn, the portal looks soft". */
body[data-active-tab] .portal-hdr,
body[data-active-tab] .rooms,
body[data-active-tab] .roomviews,
body[data-active-tab] #nextStepCard,
body[data-active-tab] #hostReadinessMount{
  border-bottom-color: var(--ch8-onyx-20);
}
body[data-active-tab] .tab-panel .td-next{
  border-top-color: var(--ch8-onyx-20);
  border-bottom-color: var(--ch8-onyx-20);
}
body[data-active-tab] .tab-panel .td-reg{
  border-bottom-color: var(--ch8-onyx-20);
}

/* Measure — §8.8: prose never runs wider than 660px. The room's own wrapper
   is 900px, which is the container for rows and panels; only the running
   paragraphs are capped, which is what the rule is actually about. */
body[data-active-tab] .tab-panel .rm-wall-p,
body[data-active-tab] .nsc-action{
  max-width: var(--measure);
}

/* The dark strip — §8.5 rule 2: on a dark plate every accent takes its lifted
   stop, and the rule reads the token, never the stop. */
body[data-active-tab] .tab-panel .dna-band,
body[data-active-tab] .tab-panel .rm-wall-in{
  --action: var(--ch8-crimson-lift);
  --done: var(--ch8-moss-lift);
  --late: var(--ch8-amber-lift);
  --broken: var(--ch8-terracotta-lift);
  --hair: color-mix(in srgb, var(--ch8-linen) 16%, transparent);
}
body[data-active-tab] .tab-panel .dna-band{
  background: var(--ch8-onyx);
  color: color-mix(in srgb, var(--ch8-linen) 78%, transparent);
}
body[data-active-tab] .tab-panel .dna-use b{ color: var(--ch8-amber-lift); }
body[data-active-tab] .tab-panel .rm-wall-in{
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--ch8-onyx) 0%, transparent),
    color-mix(in srgb, var(--ch8-onyx) 86%, transparent) 62%,
    color-mix(in srgb, var(--ch8-onyx) 95%, transparent));
  color: var(--ch8-linen);
}
body[data-active-tab] .tab-panel .rm-wall-k{
  color: color-mix(in srgb, var(--ch8-linen) 78%, transparent);
}
body[data-active-tab] .tab-panel .rm-wall-p{
  color: color-mix(in srgb, var(--ch8-linen) 82%, transparent);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.9 — FOCUS

   2px solid the action colour, offset 3px, square. On a dark or Mahogany
   ground it switches to Linen, because red on red is invisible.

   ⚠ THE RAIL IS THE EXCEPTION AND IT IS NOT A STYLE PREFERENCE. .rooms is
   overflow-x:auto and a room button fills its height, so a ring drawn OUTSIDE
   the box is clipped to two thin vertical lines — measured in pixels by
   scripts/portal-a11y-audit.mjs, not read off the stylesheet. Inside the box
   it is whole at every width. The existing rule already knew this; it is
   restated here only to move it onto the new action colour.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .portal-hdr *:focus-visible,
body[data-active-tab] #nextStepCard *:focus-visible,
body[data-active-tab] #hostReadinessMount *:focus-visible,
body[data-active-tab] .tab-panel *:focus-visible,
body[data-active-tab] #eventSetupCard *:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 3px;
  border-radius: 0;
}
body[data-active-tab] .rooms button:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: -3px;
}
/* ⚠ A FILLED CONTROL TAKES **TWO** RINGS, AND THE FIRST VERSION OF THIS FILE
   GOT IT WRONG IN A WAY ONLY PIXELS COULD CATCH.

   §8.9 reads "on a dark or Mahogany ground it switches to Linen, because red on
   red is invisible", and that was applied here literally: a Linen outline at
   outline-offset:3px on every filled button. But outline-offset:3px draws the
   ring OUTSIDE the control — so its ground is not the Mahogany, it is the LINEN
   PAGE BEHIND IT. Linen on Linen. The ring was rendering and could not be seen.

   Not a judgement call — scripts/portal-a11y-audit.mjs focuses each control,
   screenshots before and after and counts changed pixels. It reported
   "0px changed" on the register tab and both event set-up buttons, 18 blockers
   across phone and desktop. The same run against this page with the <link>
   removed reported 0, so all 18 were introduced here and none were inherited.

   The fix keeps both halves of the rule true: the OUTER ring is the action
   colour, because the ground it is actually drawn on is Linen (7.58:1 there),
   and a LINEN INSET sits between it and the fill, so red never touches red.
   That is what §8.9's sentence is protecting against, and it is the same
   inset-plus-outline pattern the file already uses for inputs. */
body[data-active-tab] .nsc-btn:focus-visible,
body[data-active-tab] .tab-panel .td-btn.pri:focus-visible,
body[data-active-tab] .roomviews button.on:focus-visible,
body[data-active-tab] #eventSetupCard .es-btn:focus-visible,
body[data-active-tab] .btn-approve:focus-visible,
body[data-active-tab] .btn-sendback:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 3px;
  box-shadow: inset 0 0 0 2px var(--ch8-linen);
}
/* On a genuinely dark plate the page ground IS dark, so there the ring is Linen
   and needs no inset — this is the case §8.9 is actually describing. */
body[data-active-tab] .tab-panel .dna-band *:focus-visible,
body[data-active-tab] .tab-panel .rm-wall-in *:focus-visible{
  outline: 2px solid var(--ch8-linen);
  outline-offset: 3px;
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.8 — HAND (640px)
   Single column, touch-first. Nothing here is new geometry; it keeps the two
   button families at the 44px house floor once their padding changed, and
   holds inputs at 16px so iOS does not zoom the page on focus.
   ─────────────────────────────────────────────────────────────────────────── */
@media (max-width: 640px){
  /* ⚠ EVERY button this file re-boxes must be listed here, not just the ones in
     the room. §8.10's primary button is 12x24px, which is SHORTER than the
     house's 44px touch floor — so re-boxing a control and forgetting it here
     silently shrinks a tap target. .wm-btn and .inbox-mark-all were added after
     scripts/portal-a11y-audit.mjs caught exactly that: "Start my vision" on the
     first-run modal came back one warning over the baseline. */
  body[data-active-tab] .nsc-btn,
  body[data-active-tab] .tab-panel .td-btn,
  body[data-active-tab] #eventSetupCard .es-btn,
  body[data-active-tab] #welcomeModal .wm-btn,
  body[data-active-tab] #welcomeModal .wm-dismiss,
  body[data-active-tab] #inboxDrawer .inbox-mark-all{
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  body[data-active-tab] .tab-panel .ap-a{
    display: grid;
    grid-template-columns: 1fr;
    gap: 8px;
  }
  body[data-active-tab] .tab-panel .ap-a .td-btn{ width: 100%; }
  body[data-active-tab] .tab-panel input,
  body[data-active-tab] .tab-panel textarea,
  body[data-active-tab] .tab-panel select,
  body[data-active-tab] #eventSetupCard .es-input{
    font-size: 16px;
  }
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.9 — THE FLOOR, AND MOTION
   One curve, and under prefers-reduced-motion everything collapses to opacity
   at 120ms. tokens.css already carries the blanket rule for the whole portal;
   this only pins the transitions this file introduced onto the house curve so
   a second easing cannot enter through it.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .nsc-btn,
body[data-active-tab] .tab-panel .td-btn,
body[data-active-tab] .roomviews button,
body[data-active-tab] #eventSetupCard .es-btn{
  transition-timing-function: var(--ease);
  transition-duration: var(--dur-quick);
}


/* ───────────────────────────────────────────────────────────────────────────
   RESIDUE — the rules the families above did not reach

   Every entry here was found by re-measuring the running screen after the
   block above landed, not by reading the stylesheets: a walk of every visible
   element under the header, the two cards and the room, looking for a retired
   ink alpha, a sage tint, or a radius that is not 0 / 2px-on-an-input / 50%.
   Seventeen elements came back. They are grouped by cause, because each cause
   is a different reason a token rebind cannot reach a value.
   ─────────────────────────────────────────────────────────────────────────── */

/* CAUSE 1 — hardcoded rgba() of the RETIRED ink, so no token points at them.
   The room rail is the portal's primary navigation and four of its five
   labels were among them.
   ⚠ THE VALUE MATTERS HERE, NOT JUST THE HEX FAMILY. portal-rooms.js records
   that .58 measured 4.48:1 once the ground moved from Ivory to Linen — under
   AA by two hundredths, on navigation. These were .60/.66/.68, i.e. the same
   near-miss band. Onyx 62% is the last step chapter 8.2 still calls AA text:
   4.94:1 on Linen, and it still reads as dimmed beside the solid-onyx active
   room, so the active/inactive distinction survives the change. */
body[data-active-tab] .rooms button,
body[data-active-tab] .rooms button .rm-l,
body[data-active-tab] .rooms button .rm-s,
body[data-active-tab] .rooms button.is-quiet:not(.on):not(:hover),
body[data-active-tab] .rooms button.is-quiet:not(.on):not(:hover) .rm-l,
body[data-active-tab] .rooms button.rm-find,
body[data-active-tab] .rooms button.rm-find span,
body[data-active-tab] .rooms button.is-locked{
  color: var(--ch8-onyx-62);
}
body[data-active-tab] .rooms button.on,
body[data-active-tab] .rooms button.on .rm-l,
body[data-active-tab] .rooms button:hover:not(.is-locked),
body[data-active-tab] .rooms button:hover:not(.is-locked) .rm-l{
  color: var(--ch8-onyx);
}
body[data-active-tab] .tab-panel .ap-cost,
body[data-active-tab] .tab-panel .ap-cost b{
  color: var(--ch8-onyx-62);
}

/* CAUSE 2 — the structural teal survived as a TINT rather than as text, which
   is why the colour census caught none of these: an alpha of sage on Linen is
   a border or a panel, not a word. §8.2 has one answer for all three — the
   quiet layer is Onyx, and a rule is a hairline. */
body[data-active-tab] .nsc-inner,
body[data-active-tab] .hr-foot,
body[data-active-tab] .tab-panel .rm-wall{
  border-color: var(--ch8-onyx-20);
}
body[data-active-tab] #eventSetupCard .es-rec,
body[data-active-tab] #eventSetupCard .es-callout{
  background: color-mix(in srgb, var(--ch8-onyx) 4%, transparent);
  color: var(--ch8-onyx-78);
}

/* CAUSE 3 — a runtime-injected <style> beats a <link> on source order however
   late the link is, so the pill that opens the message controls kept its
   999px. §8.8 permits three radii and a stadium is not one of them.
   The same is true of any other chip the header mounts after boot, so this is
   written as the family rather than as the one element that was measured. */
body[data-active-tab] .portal-hdr button,
body[data-active-tab] .portal-hdr .pcs-btn,
body[data-active-tab] #portalCommsStop button,
body[data-active-tab] .hdr-client-chip,
body[data-active-tab] .hdr-event-chip{
  border-radius: 0;
}
/* …and the bell stays a circle: §8.10's radius 50% is for "avatars and status
   dots", and it is both. Restated after the rule above, which would flatten it. */
body[data-active-tab] .portal-hdr .hdr-bell{
  border-radius: var(--radius-round);
}


/* ───────────────────────────────────────────────────────────────────────────
   THE GROUND CHANGES THE STEP — §8.2's table is measured ON LINEN

   Found by running the §8.9 floor over all 165 text nodes on the screen after
   the residue pass: three failures, one cause. Chapter 8.2 calls Onyx 62% the
   last step that is still AA text and gives it as 4.94:1 — and that number is
   Onyx 62% ON LINEN. The approval card is Alabaster, because §8.1 says
   Alabaster is the panel a card sits on, and on #E7E3DD the same step measures
   4.34:1. Under AA, on the card carrying the one decision this screen exists
   to put to the client.

   Neither the token nor the card was wrong; the pairing was. On Alabaster the
   last AA step is Onyx 78% — measured 7.41:1 there, against 8.55:1 on Linen —
   so the meta layer inside an Alabaster panel takes the step above.

   ⚠ Written as the family, not as the three elements that failed. Only .ap
   renders on the fixture this was measured against; .td-stats, .ir-wrap,
   .rm-pin, .fstates and .rm-sum are the same Alabaster-panel-with-meta-inside
   shape and would fail identically the moment an event reaches a state that
   renders them. A fix that only covers what happened to be on screen is how
   this comes back.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel .ap .ap-v,
body[data-active-tab] .tab-panel .ap .ap-q,
body[data-active-tab] .tab-panel .ap .ap-cost,
body[data-active-tab] .tab-panel .ap .ap-cost b,
body[data-active-tab] .tab-panel .ap .ap-lock,
body[data-active-tab] .tab-panel .td-stats span,
body[data-active-tab] .tab-panel .ir-m,
body[data-active-tab] .tab-panel .ir-key,
body[data-active-tab] .tab-panel .rm-pin .rm-pin-k,
body[data-active-tab] .tab-panel .rm-pin-next b,
body[data-active-tab] .tab-panel .fst span,
body[data-active-tab] .tab-panel .rm-sum span{
  color: var(--ch8-onyx-78);
}


/* ───────────────────────────────────────────────────────────────────────────
   THE OVERLAYS THIS SCREEN OPENS

   Two of them mount on <body>, not inside .tab-panel, so none of the container
   selectors above reached them — and half-reaching is worse than not reaching
   at all. Measured on the phone viewport: #welcomeModal reported Inter on the
   overlay itself, because the sweep sets font-family on <body> and it inherits,
   while every element portal.css names by class stayed Montserrat. The first
   screen a new client ever sees was two typefaces at once.

     · #welcomeModal   the first-run walkthrough — Today is step 1, and this is
                       what opens ON this room before anything else is read.
     · #inboxDrawer    opens from the bell in the header, which this file has
                       already repainted, so leaving the drawer behind would
                       put a legacy panel over an adapted page.

   Both are scoped the same way as everything else and both close back onto
   Today, so nothing here can be seen from another room.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] #welcomeModal,
body[data-active-tab] #welcomeModal *,
body[data-active-tab] #inboxDrawer,
body[data-active-tab] #inboxDrawer *,
body[data-active-tab] #inboxOverlay *{
  font-family: var(--sans);
}
body[data-active-tab] #welcomeModal .wm-title,
body[data-active-tab] #welcomeModal .wm-step-title,
body[data-active-tab] #inboxDrawer .inbox-hdr-title{
  font-family: var(--serif);
  font-weight: 300;
}

/* §8.10 — the same four components, in the same colours, inside the overlay. */
body[data-active-tab] #welcomeModal .welcome-modal,
body[data-active-tab] #inboxDrawer{
  background: var(--ch8-linen);
  border-radius: 0;
  box-shadow: none;
  border: 1px solid var(--ch8-onyx-20);
}
body[data-active-tab] #welcomeModal .wm-steps,
body[data-active-tab] #welcomeModal .wm-step,
body[data-active-tab] #inboxDrawer .inbox-notif{
  border-color: var(--ch8-onyx-12);
  border-radius: 0;
}
body[data-active-tab] #welcomeModal .wm-title,
body[data-active-tab] #welcomeModal .wm-step-title,
body[data-active-tab] #inboxDrawer .inbox-notif-title{
  color: var(--ch8-onyx);
}
body[data-active-tab] #welcomeModal .wm-sub,
body[data-active-tab] #welcomeModal .wm-step-desc,
body[data-active-tab] #inboxDrawer .inbox-notif-body{
  color: var(--ch8-onyx-78);
}
body[data-active-tab] #welcomeModal .wm-dismiss,
body[data-active-tab] #inboxDrawer .inbox-notif-time,
body[data-active-tab] #inboxDrawer .inbox-notif-meta,
body[data-active-tab] #inboxDrawer .inbox-empty-text{
  color: var(--ch8-onyx-62);
}
/* The step numerals: the room a client is being sent to is the next step; the
   rest are the quiet layer. A filled chip carries its own contrast (§8.5). */
body[data-active-tab] #welcomeModal .wm-step-num{
  background: var(--ch8-onyx-20);
  color: var(--ch8-onyx);
  border-radius: var(--radius-round);
}
body[data-active-tab] #welcomeModal .wm-step.is-current .wm-step-num,
body[data-active-tab] #welcomeModal .wm-step:first-child .wm-step-num{
  background: var(--ch8-crimson);
  color: var(--ch8-linen);
}
/* Primary and ghost, exactly as §8.10 sets them everywhere else on the screen. */
body[data-active-tab] #welcomeModal .wm-btn,
body[data-active-tab] #inboxDrawer .inbox-mark-all{
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  padding: 12px 24px;
  border: none;
  border-radius: 0;
  background: var(--ch8-crimson);
  color: var(--ch8-linen);
  box-shadow: none;
}
body[data-active-tab] #welcomeModal *:focus-visible,
body[data-active-tab] #inboxDrawer *:focus-visible{
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 3px;
}
body[data-active-tab] #welcomeModal .wm-btn:focus-visible,
body[data-active-tab] #inboxDrawer .inbox-mark-all:focus-visible{
  box-shadow: inset 0 0 0 2px var(--ch8-linen);
}


/* ───────────────────────────────────────────────────────────────────────────
   THE SIX MORE THAT MOUNT ON <body>, AND THE TWO MODALS UNDER #portalMain
   EVR-085, censused 2026-09-09 with scripts/portal-typeface-census.mjs.

   The two overlays above were found the same way and named one at a time; these
   are the rest of the same population, and they are the surfaces a client meets at
   the loudest moments of the job — the screen that confirms their vision was
   received, the sheet where they buy more pieces, the modal that shares their
   designs. Measured with every one of them open: 31 elements in Montserrat and 11
   in Libre Baskerville, none of them a wordmark.

     · #sucScreen      the vision-submitted takeover. `.suc-title` and every
                       `.suc-card-val` were Libre Baskerville — including the em in
                       "Your Vision, /In Our Hands/", the largest words on it.
     · #addonOverlay   the add-on sheet: `.addon-sheet-title` ×2, Libre.
     · #socialModal    `.social-box-title` Libre; the three `.social-tab` buttons and
                       both download controls Montserrat.
     · #shareModal     the three `.share-note` paragraphs and the mailto beside them.
     · #csvWarnModal   · #teamMsgModal   siblings of the share modal, same rules.
     · #editGuestModal · #csvPreviewModal  the two that sit under #portalMain rather
                       than <body>, so `.tab-panel *` misses them as well.

   Same shape as the block above and for the same reason: the sweep sets the face on
   <body> and these elements carry their own `font-family` in portal.css, which beats
   inheritance at any ancestor specificity. Naming the eight containers rebinds them
   without touching a line of portal.css — the file's own standing rule. The face is
   switched by VALUE, not by element (the pattern the inline block below already
   uses), so `.suc-title`'s serif STAYS a serif and only stops being the retired one.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] #sucScreen,
body[data-active-tab] #sucScreen *,
body[data-active-tab] #addonOverlay,
body[data-active-tab] #addonOverlay *,
body[data-active-tab] #socialModal,
body[data-active-tab] #socialModal *,
body[data-active-tab] #shareModal,
body[data-active-tab] #shareModal *,
body[data-active-tab] #csvWarnModal,
body[data-active-tab] #csvWarnModal *,
body[data-active-tab] #teamMsgModal,
body[data-active-tab] #teamMsgModal *,
body[data-active-tab] #editGuestModal,
body[data-active-tab] #editGuestModal *,
body[data-active-tab] #csvPreviewModal,
body[data-active-tab] #csvPreviewModal *{
  font-family: var(--sans);
}
/* The serif stays a serif — §8.7 gives the document voice to titles, card values and
   figures, and every one of these was already set in a serif face. Only the face is
   retired, so these are re-declared rather than swept into Inter with the rest.
   `.suc-card-val` is the client's own name, event, venue and date read back to them;
   `.suc-title` and `.addon-sheet-title` carry their inner `<em>`, which the `*` sweep
   above would otherwise steal (the same trap documented for headings further down). */
body[data-active-tab] #sucScreen .suc-title,
body[data-active-tab] #sucScreen .suc-title :is(em,i,span),
body[data-active-tab] #sucScreen .suc-card-val,
body[data-active-tab] #addonOverlay .addon-sheet-title,
body[data-active-tab] #addonOverlay .addon-sheet-title :is(em,i,span),
body[data-active-tab] #socialModal .social-box-title{
  font-family: var(--serif);
}

/* ⚠ AND FOUR THAT NO CONTAINER RULE CAN REACH, BECAUSE THE FACE IS ON THE ELEMENT.
   `app/client-portal.html` writes `style="font-family:'Montserrat',…"` on the CSV
   preview's lead paragraph (5146), on `#egChannelLabel` / `#egChannelToggle` (5187–8)
   and on both "+ Add family member" buttons (4598, 5222). An inline declaration beats
   every stylesheet rule at any specificity, so the eight containers above leave
   exactly these four on screen — measured, not assumed. Same VALUE switch the
   `.tab-panel` block below already uses, for the same reason and with the same
   safety: it matches the retired face, never the element, so an inline serif or one
   of the script-face specimens is not touched. */
body[data-active-tab] #sucScreen [style*="Montserrat"],
body[data-active-tab] #addonOverlay [style*="Montserrat"],
body[data-active-tab] #socialModal [style*="Montserrat"],
body[data-active-tab] #shareModal [style*="Montserrat"],
body[data-active-tab] #csvWarnModal [style*="Montserrat"],
body[data-active-tab] #teamMsgModal [style*="Montserrat"],
body[data-active-tab] #editGuestModal [style*="Montserrat"],
body[data-active-tab] #csvPreviewModal [style*="Montserrat"]{
  font-family: var(--sans) !important;
}
body[data-active-tab] #sucScreen [style*="Libre Baskerville"],
body[data-active-tab] #addonOverlay [style*="Libre Baskerville"],
body[data-active-tab] #socialModal [style*="Libre Baskerville"],
body[data-active-tab] #shareModal [style*="Libre Baskerville"],
body[data-active-tab] #csvWarnModal [style*="Libre Baskerville"],
body[data-active-tab] #teamMsgModal [style*="Libre Baskerville"],
body[data-active-tab] #editGuestModal [style*="Libre Baskerville"],
body[data-active-tab] #csvPreviewModal [style*="Libre Baskerville"]{
  font-family: var(--serif) !important;
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE OTHER FOUR ROOMS — Design · Guests · Experience · Files
   Added 2026-09-02, extending this file from one room to all five.

   WHAT THE GENERALISATION ALONE ACHIEVED, measured room by room before any of
   the rules below were written. Changing the scope from
   `[data-active-tab="today"]` to `[data-active-tab]` — identical specificity,
   so nothing this file had already tuned moved — and `#tab-today` to
   `.tab-panel`, which is the class all five room panels share:

     room        legacy elements   Montserrat   Libre Baskerville
     Design         36 ->  2         28 -> 0        5 -> 0
     Guests         90 -> 40         82 -> 13       5 -> 2
     Experience     85 -> 32         79 -> 27       2 -> 0
     Files         252 -> 77        214 ->  4      29 -> 9

   464 -> 152 with no new selector, because the §8.12 token rebind repaints
   every rule that reads var(--onyx) / var(--sage) / var(--bloodstone) without
   naming it. What is left below is what hardcodes a hex.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   THE FACES, WHERE THEY ARE WRITTEN INLINE

   Measured: of the 27 Montserrat elements left in Experience, 23 carry their
   own `style="font-family:…"`, written by this page's inline JS. An inline
   declaration beats every stylesheet rule, so the `.tab-panel *` sweep above
   cannot reach them and no amount of specificity will.

   ⚠ A BLANKET `font-family: var(--sans) !important` ON THE PANEL WOULD BE
   WRONG IN TWO WAYS, and both were checked before writing this rather than
   found afterwards:
     · 8 inline declarations already ask for Crimson Pro and 10 more for a
       serif — flattening those to Inter would strip the serif off headings
       this file spent its first half putting there.
     · portal.css:5079/5091/5117 set Great Vibes and Alex Brush on #ts7, #ts17
       and #tsf11 — the typography specimens that show a client their own
       chosen lettering. They are the one place a display face is the content.

   So the switch is on the VALUE, not on the element: an inline Montserrat
   becomes Inter, an inline Libre Baskerville or Cormorant becomes Crimson Pro,
   and anything else — the script faces, the already-correct Crimson Pro, the
   one monospace — is not matched at all. Matching on the substring also
   survives the three spellings in the file ('Montserrat', Montserrat, and the
   backslash-escaped \'Montserrat\' inside JS strings).
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel [style*="Montserrat"]{
  font-family: var(--sans) !important;
}
body[data-active-tab] .tab-panel [style*="Libre Baskerville"],
body[data-active-tab] .tab-panel [style*="Cormorant"]{
  font-family: var(--serif) !important;
  font-weight: 300;
}
/* The rule-based stragglers the `*` sweep loses to on specificity. */
body[data-active-tab] .tab-panel h1,
body[data-active-tab] .tab-panel h2,
body[data-active-tab] .tab-panel h3,
body[data-active-tab] .tab-panel h4{
  font-family: var(--serif);
  font-weight: 300;
}

/* ⚠ AND THE INLINE PARTS OF A HEADING, WHICH THE `*` SWEEP ABOVE STEALS BACK.
   The sweep sets `.tab-panel *` to var(--sans); the rule directly above
   re-serifs only the heading ELEMENT. An `<em>` or `<span>` INSIDE a heading is
   therefore caught by the sweep and missed by the fix, and renders Inter under
   a Crimson Pro roman.

   That is not a corner case. Censused in the running portal 2026-09-05:
   **28 `<em>` and one `<span>` inside `.tab-panel` headings** — among them
   `#briefHeroTitle`'s `<em>`, which carries THE EVENT NAME on every event
   ("Tell us everything about / *Margaret Okafor's 70th Birthday Dinner.*"),
   and `.rhb-heading`'s span ("Your Suite,"). The owner reported it off the
   screen; `portal.css:702` (`.hero-title em`) had been dead since this file
   landed, so nothing in the source said the italic was wrong.

   `em`/`i`/`span` by name rather than `*`: BR and STRONG carry no face of their
   own, and naming the three leaves room to write a deliberate inline sans
   fragment inside a heading later without fighting a blanket rule. Specificity
   (0,2,3) beats the sweep's (0,2,1); font-weight is left to inherit the 300
   above, which the sweep never touched. */
body[data-active-tab] .tab-panel :is(h1,h2,h3,h4) :is(em,i,span){
  font-family: var(--serif);
}


/* ── §8.7 · THE DISPLAY TRACKING, TAKEN FROM THE BRAND SHOWCASE ─────────────
   Owner, 2026-09-05, choosing it against eleven alternatives in
   `brand/font-lab.html`: the showcase's headline treatment, adopted for the
   rooms. `brand/showcase.html` sets its display spec at **-0.022em**
   (`.ty-web .ty-spec`) and the rooms carried either nothing or half of it —
   measured live before the change: `.hero-title` and `.sec-title` (×14, the
   largest heading population at 30.4px) at `normal`; `.rsvp-hero-title`,
   `.bsp-hero-title`, `.ds-hero-h1`, `.rhb-heading`, `.billing-title` at
   -0.01em. Crimson Pro 300 is a light face and needs the correction at display
   size; without it the words sit loose and the line reads undersized.

   ⚠ SCOPED TO h1/h2 DELIBERATELY — tracking has to scale with size, and h3/h4
   in the rooms are NOT all display. `.dhb-step-hd` is 10.56px at **+0.22em**
   and the two bare `<h4>`s are 8.8px at **+0.16em**: uppercase micro-labels
   that a negative correction would damage. Do not widen this to h3/h4 without
   re-measuring those three. The smallest h2 is `.es-title` at 18.72px, where
   this resolves to -0.41px.

   ⚠ SIZE AND LEADING ARE NOT CHANGED, and that is the same call the 18px root
   gets above: the showcase's `clamp(2rem,5.6vw,4.3rem)` is about a quarter
   larger than the rooms' heading scale, so adopting it would re-flow five room
   headings — a redesign, not a tracking pass. Tracking is the half that is
   free. */
body[data-active-tab] .tab-panel h1,
body[data-active-tab] .tab-panel h2{
  letter-spacing: -0.022em;
}
/* ⚠ ONE EXCEPTION, AND IT IS THE PRICE OF FIXING A HEADING LEVEL. The Guests room's band
   titles were h3 under an h1 — a skipped level, reported by scripts/portal-a11y-audit.mjs on
   both widths — and correcting them to h2 brought them under the display tracking above. They
   are not display headings: they are .16em uppercase labels, and -0.022em flattens them.
   Restated here rather than in portal-rooms.js's string, because that string is (0,1,0) and
   this rule is (0,2,2); the class cannot win where it is declared. MEASURED after: 11.52px
   Crimson Pro 300, tracking 1.8432px — identical to what the h3 rendered. */
body[data-active-tab] .tab-panel .s4__band-title{
  letter-spacing: 0.16em;
}
body[data-active-tab] .tab-panel input,
body[data-active-tab] .tab-panel select,
body[data-active-tab] .tab-panel textarea,
body[data-active-tab] .tab-panel label,
body[data-active-tab] .tab-panel button{
  font-family: var(--sans);
}


/* ───────────────────────────────────────────────────────────────────────────
   DESIGN  ·  #tab-concept  ·  Direction · The Suite · Your DNA · Your Vision
   Two elements, both hardcoding the retired ink on what is a heading and a
   figure — §8.7 puts both in Crimson Pro.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel .ds-hero-h1,
body[data-active-tab] .tab-panel .ds-hero-stat-val{
  color: var(--ch8-onyx);
  font-family: var(--serif);
  font-weight: 300;
}
body[data-active-tab] .tab-panel .ds-hero-stat-val{ font-variant-numeric: tabular-nums; }


/* ───────────────────────────────────────────────────────────────────────────
   GUESTS  ·  #tab-rsvp  ·  Everyone · Replies · Coming · Seating · Needs attention
   ─────────────────────────────────────────────────────────────────────────── */
/* Headings and the empty state — §8.11: "No guests yet. Add the first…" is a
   sentence, and it is read, so it takes ink rather than a dimmed step. */
body[data-active-tab] .tab-panel .rsvp-hero-title,
body[data-active-tab] .tab-panel .guests-empty-title,
body[data-active-tab] .tab-panel .guests-phase-title,
body[data-active-tab] .tab-panel .ros-open,
body[data-active-tab] .tab-panel .ros-run{
  color: var(--ch8-onyx);
}
/* Secondary copy — the second line of a row (§8.2, Onyx 78%). */
body[data-active-tab] .tab-panel .ros-sub,
body[data-active-tab] .tab-panel .ros-note,
body[data-active-tab] .tab-panel .rsvp-security-tip,
body[data-active-tab] .tab-panel .dop-t{
  color: var(--ch8-onyx-78);
}
/* Labels, timestamps, meta — the last AA step on Linen. */
body[data-active-tab] .tab-panel .ros-openh,
body[data-active-tab] .tab-panel .guests-empty-sub,
body[data-active-tab] .tab-panel .rsvp-privacy-note,
body[data-active-tab] .tab-panel .dop-h,
body[data-active-tab] .tab-panel .dop-s,
body[data-active-tab] .tab-panel .dop-d{
  color: var(--ch8-onyx-62);
}
/* §8.2 again — the structural teal was tinting a panel and a sub-line here.
   A tint is not a signal; it is the quiet layer, and the quiet layer is Onyx. */
body[data-active-tab] .tab-panel .ps-sub{
  color: var(--ch8-onyx-62);
  background: transparent;
}
/* .pri is the primary control in the seating tools — the one next step. */
body[data-active-tab] .tab-panel .pri{
  background: var(--ch8-crimson);
  border-color: var(--ch8-crimson);
  color: var(--ch8-linen);
}


/* ───────────────────────────────────────────────────────────────────────────
   EXPERIENCE  ·  #tab-experience  ·  Website · Messages · Personal
   Almost entirely inline-styled, so the value-matched font rules above do the
   work. What is left is the guest-tools copy and one sage tint.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel .gt-t{ color: var(--ch8-onyx); }
body[data-active-tab] .tab-panel .gt-b{ color: var(--ch8-onyx-78); }
body[data-active-tab] .tab-panel .rh-toggle{
  background: color-mix(in srgb, var(--ch8-onyx) 4%, transparent);
  border-color: var(--ch8-onyx-20);
}


/* ───────────────────────────────────────────────────────────────────────────
   FILES  ·  #tab-suite
   The densest room — 252 legacy elements at the start, and the only one where
   the structural teal was carrying STATUS rather than structure. Each class
   below was read on screen before it was assigned a role, because "waiting"
   and "not available yet" are different things in chapter 08 and the shipped
   stylesheet painted them the same colour.
   ─────────────────────────────────────────────────────────────────────────── */
/* Names and figures — §8.7 puts piece names, card values and figures in
   Crimson Pro. "Invitation", "Menu", "Moodboards", "44". */
body[data-active-tab] .tab-panel .ndl-piece-name,
body[data-active-tab] .tab-panel .item-name,
body[data-active-tab] .tab-panel .doc-title,
body[data-active-tab] .tab-panel .doc-shelf-name,
body[data-active-tab] .tab-panel .ech-num,
body[data-active-tab] .tab-panel .ech-title,
body[data-active-tab] .tab-panel .dhb-step-hd{
  color: var(--ch8-onyx);
  font-family: var(--serif);
  font-weight: 300;
}
body[data-active-tab] .tab-panel .ech-num,
body[data-active-tab] .tab-panel .doc-row-num,
body[data-active-tab] .tab-panel .item-num{
  font-variant-numeric: tabular-nums;
}
/* The spec line — "PDF · CMYK · 5×7″ · 0.125″ bleed". §8.7 files specs under
   --text-xs, and §8.7's second unstated rule applies: these are columns of
   figures, so they are tabular or they stop lining up between pieces. */
body[data-active-tab] .tab-panel .ndl-fmt-spec{
  color: var(--ch8-onyx-78);
  font-variant-numeric: tabular-nums;
}
/* Explanatory sub-copy and ordinals — the quiet layer, and the teal goes with
   them. "Unlocks when you confirm the final", "01", "02". */
body[data-active-tab] .tab-panel .ndl-fmt-sub,
body[data-active-tab] .tab-panel .item-num,
body[data-active-tab] .tab-panel .doc-row-num,
body[data-active-tab] .tab-panel .doc-shelf-note,
body[data-active-tab] .tab-panel .dhb-cap,
body[data-active-tab] .tab-panel .dhb-note{
  color: var(--ch8-onyx-62);
  background: transparent;
}
body[data-active-tab] .tab-panel .dhb-path{ color: var(--ch8-onyx-78); }
body[data-active-tab] .tab-panel .items-sec-title{
  font-family: var(--sans);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ch8-onyx-62);
}

/* ⚠ THE TWO FORMAT BUTTONS MEAN DIFFERENT THINGS AND SHIPPED THE SAME COLOUR.
   Read on screen: `.na` says "Confirm to unlock →" — a control that is not
   available yet — and `.waiting` says "Being prepared", which is the event
   sitting WITH US. Both were the structural teal, so a client could not tell
   a thing they must do from a thing we are doing.

   §8.10 gives the first one exactly: disabled is Onyx 34% ground with Onyx 62%
   type. And §8.5 gives the second: once it is with us it is the quiet layer,
   never a functional colour — which is why "Being prepared" is NOT Amber.
   Amber is running late; being prepared on schedule is not late. */
/* ⚠ AND `.na` IS NOT §8.10's DISABLED CONTROL, WHICH IS WHERE I PUT IT FIRST.
   Onyx 34% fill with Onyx 62% type is the chapter's disabled spec and measures
   2.29:1 — legal only because a genuinely disabled control carries no
   information. This one carries an instruction, "Confirm to unlock →", and the
   element is a live <button> with `disabled === false` and no aria-disabled,
   so nothing exempts it and an inspector reads it as ordinary text. It is a
   ghost button: the client must be able to read what to do. */
body[data-active-tab] .tab-panel .ndl-fmt-btn.na{
  background: transparent;
  border: 1px solid var(--ch8-onyx-34);
  color: var(--ch8-onyx);
}
body[data-active-tab] .tab-panel .ndl-fmt-btn.waiting{
  background: transparent;
  border: 1px solid var(--ch8-onyx-20);
  color: var(--ch8-onyx-62);
}
/* "● Ready" — this one IS a status, and it is DONE. §8.3's Moss Fern, and the
   dot beside the word is the §8.10 status dot: colour never carries the
   meaning alone, the word is already there. */
body[data-active-tab] .tab-panel .ssl-badge{
  color: var(--ch8-moss);
}


/* ───────────────────────────────────────────────────────────────────────────
   THE SEVEN `!important` FACES

   ⚠ `!important` HERE IS NOT LAZINESS — IT IS THE ONLY THING THAT WORKS, and
   it is deliberately the narrowest rule in this file. portal.css declares the
   retired serif with `!important` on exactly seven classes, e.g.
   `.doc-title{font-family:'Libre Baskerville',serif!important}` (portal.css
   :4272, :4088, and five more). No specificity beats `!important`; only
   `!important` does.

   The diagnosis is worth keeping because the symptom lied. `.ndl-piece-name`
   took the new face while `.doc-title` and `.item-name` — in the SAME rule of
   this file, one line apart — did not. That looks like a broken selector and
   is not: `.ndl-piece-name` is the one of the three portal.css does not mark
   important. A cascade probe that reads `rule.style[prop]` cannot see this,
   because that returns the value without its priority; read
   `getPropertyPriority()` or the source.

   Their COLOURS need nothing, even though they are `!important` too: those
   declarations read `var(--onyx-dk)`, and the token block now rebinds that
   name. An important declaration still resolves its variables at use — which
   is why rebinding is the better tool wherever a value goes through one, and
   why this list is seven classes rather than the whole room.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel .doc-title,
body[data-active-tab] .tab-panel .doc-big-num,
body[data-active-tab] .tab-panel .item-name,
body[data-active-tab] .tab-panel .bb-action-disabled,
body[data-active-tab] .tab-panel .bb-action-norevisions,
body[data-active-tab] .tab-panel .bb-action-reassure{
  font-family: var(--serif) !important;
  font-weight: 300;
}
/* This one is a tracked label, not a title — §8.7 puts it in Inter. */
body[data-active-tab] .tab-panel .items-sec-title{
  font-family: var(--sans) !important;
}

/* ⚠ "● Ready" carries its status as an INLINE hex — #317254, the retired
   forest — written into the markup at client-portal.html:3823. Matched on that
   value rather than on the class, because `.ssl-badge` is also "Sample Only",
   which is not done and correctly stays amber. Colouring the whole class green
   would have turned a caveat into a completion. */
body[data-active-tab] .tab-panel .ssl-badge[style*="317254"]{
  color: var(--ch8-moss) !important;
  border-color: color-mix(in srgb, var(--ch8-moss) 45%, transparent) !important;
}


/* ───────────────────────────────────────────────────────────────────────────
   THE FOUR RUNTIME CARDS THAT SCOPE BY ID

   Four features mount their own <style> at runtime and scope every rule to
   their card's id — `#dayOfPrintPanel .dop-t`, `#guestToolsPanel .gt-t`,
   `#rosCard .ros-sub`, `#photoSpaceCard .ps-sub`. That is (1,1,0); the
   panel-scoped rules above are (0,3,1), so an id beats them however late this
   file loads. The same shape as the `#roomsCss` problem in the header, one
   level further in — and the same answer: match the id.

   Found by walking every visible element still carrying a retired value,
   skipping the ones with an inline style, and asking which stylesheet rule was
   actually winning. That is the list; it is not a guess at which cards exist.
   ─────────────────────────────────────────────────────────────────────────── */

/* Titles and headings — §8.7 puts a card's title in Crimson Pro. */
body[data-active-tab] #dayOfPrintPanel .dop-t,
body[data-active-tab] #guestToolsPanel .gt-t,
body[data-active-tab] #rosCard h3,
body[data-active-tab] #photoSpaceCard h3{
  font-family: var(--serif);
  font-weight: 300;
  color: var(--ch8-onyx);
}
/* Running copy and the second line of a row — Onyx 78%. */
body[data-active-tab] #dayOfPrintPanel .dop-d,
body[data-active-tab] #guestToolsPanel .gt-b,
body[data-active-tab] #rosCard .ros-sub,
body[data-active-tab] #rosCard .ros-note,
body[data-active-tab] #rosCard .ros-run,
body[data-active-tab] #photoSpaceCard .ps-sub{
  font-family: var(--sans);
  font-weight: 300;
  color: var(--ch8-onyx-78);
  background: transparent;
}
/* Eyebrows, labels and meta — the tracked label, §8.10. */
body[data-active-tab] #dayOfPrintPanel .dop-h,
body[data-active-tab] #dayOfPrintPanel .dop-s,
body[data-active-tab] #rosCard .ros-openh{
  font-family: var(--sans);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ch8-onyx-62);
}
/* Their buttons — the ghost box, and `.pri` is the one next step (§8.10). */
body[data-active-tab] #rosCard button,
body[data-active-tab] #photoSpaceCard button{
  font-family: var(--sans);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  border: 1px solid var(--ch8-onyx-34);
  border-radius: 0;
  background: transparent;
  color: var(--ch8-onyx);
  box-shadow: none;
}
body[data-active-tab] #photoSpaceCard button.pri,
body[data-active-tab] #rosCard button.pri{
  background: var(--ch8-crimson);
  border-color: var(--ch8-crimson);
  color: var(--ch8-linen);
}


/* ───────────────────────────────────────────────────────────────────────────
   THE LAST RULE-BASED RESIDUES

   Nine visible elements were still painting a retired value from a stylesheet
   rather than from an inline style, found by walking every room and asking
   which rule was winning on each. Three causes, all in the quiet layer, and
   each one an alpha of the retired ink that no token points at.
   ─────────────────────────────────────────────────────────────────────────── */
/* Today's milestone line — "Next — … with the atelier" (#roomsCss .td-mile). */
body[data-active-tab] .tab-panel .td-mile{ color: var(--ch8-onyx-62); }
/* The Files room's two questions — "May we show your suite…", "One last
   question…". Running prose a client is asked to answer, so Onyx 78%, not the
   meta step (portal.css:6604 .dhb-ask p). */
body[data-active-tab] .tab-panel .dhb-ask p{
  color: var(--ch8-onyx-78);
  font-family: var(--sans);
  font-weight: 300;
}
/* The emphasis inside the Files note — "print-ready", "screen version". These
   are the emphasised half of a sentence, so they take the step ABOVE their
   surrounding copy, not below it (#roomsCss .fst-note b). */
body[data-active-tab] .tab-panel .fst-note b{ color: var(--ch8-onyx); }

/* .fst-note sits inside an Alabaster panel, where Onyx 62% is 4.34:1 — the
   same ground-changes-the-step trap this file already records for the Today
   room's approval card. On Alabaster the last AA step is Onyx 78%. */
body[data-active-tab] .tab-panel .fst-note{ color: var(--ch8-onyx-78); }


/* ───────────────────────────────────────────────────────────────────────────
   COMPOUND DIMMING — the trap this repository has already been caught by, now
   reintroduced by the rebind itself

   Two axe color-contrast failures, six nodes, and the cause is one sentence
   portal.css already has written down in a different room: "DIM THE NUMERAL,
   NEVER THE LABEL — COMPOUND DIMMING IS THE TRAP", where a cell's opacity
   MULTIPLIED against text that was already a dimmed step.

   ⚠ WHAT MAKES THIS ONE WORTH RECORDING: the dimming was there all along and
   PASSED. The structural teal is a saturated dark colour, so at 80–85% opacity
   it still cleared AA. Onyx 62% is the last AA step ON ITS OWN — it has no
   headroom left to spend — so rebinding the teal onto it turned two rules that
   had always been legal into failures:

     .pd-piece-spec{color:var(--sage-dk);opacity:.8}        4.94 -> 3.24:1
     .doc-row.is-prepared .doc-row-body{opacity:.85}        4.94 -> 3.65:1
       (containing .doc-sub{color:var(--sage)!important})

   A palette migration can regress a contrast the OLD palette happened to pass.
   Both are fixed the way the house rule says rather than by picking a darker
   step: the opacity comes off, and the colour carries the quiet on its own —
   which is the entire point of chapter 8.2's fixed steps.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .tab-panel .pd-piece-spec{
  opacity: 1;
  color: var(--ch8-onyx-62);
}
body[data-active-tab] .tab-panel .doc-row.is-prepared .doc-row-body{
  opacity: 1;
}
/* "Prepared" still reads as a state — it is carried by the row's own quiet
   layer and its status word, not by making the whole row hard to read. */
body[data-active-tab] .tab-panel .doc-row.is-prepared .doc-sub{
  color: var(--ch8-onyx-62);
}


/* ───────────────────────────────────────────────────────────────────────────
   §8.7 — THE TYPE SCALE, ON THE STEPS THAT ARE OFF IT

   Added 2026-09-02 (owner: make /client-portal#today carry the brand system's
   identity). The faces and the palette were already right when this pass
   began — a census of every visible element in #tab-today at 1440px returned
   Inter ×209 and Crimson Pro ×16, no Montserrat, no Libre Baskerville, ink at
   #141414, action at #9D0617, Moss Fern on the readiness bar, Amber on the
   late line, no shadow anywhere, and radius 2px on exactly eight elements, all
   of them <input>, which is what §8.8 permits. What that census also found is
   the part §8.7 calls drift by name: "a size not on this list is drift."

   ⚠ MEASURED, NOT READ OFF THE STYLESHEETS. Two files own this copy and only
   one of them is a stylesheet: portal-event-setup.css owns the .es-* card, and
   portal-rooms.js carries .ap-*, .rm-* and .td-* as a JS string appended as
   <style id="roomsCss"> after every <link>. Reading either alone would have
   missed half of it, and source order cannot win against the second — so every
   rule here is prefixed body[data-active-tab="today"] .tab-panel (0,3,1),
   which outranks #roomsCss's bare classes (0,1,1).

   ── WHAT IS FIXED HERE ──────────────────────────────────────────────────
     .58rem  → --text-micro   .es-opt · .es-sens · .es-mod-title em
               This one is not a rounding difference. §8.7 says of 0.62rem:
               "The smallest type permitted." 0.58rem is BELOW the floor of
               the scale, on uppercase 0.12–0.16em labels, which is the least
               legible shape small type takes.
     .66rem  → --text-xs      .ap-v · .es-mod-status · .es-btn
     .68rem  → --text-xs      .es-prog
     .82rem  → --text-base    .es-intro — a running paragraph, and §8.7 gives
                              running paragraphs exactly one step.
     1.15rem → --h3           .es-mod-title
     600/700 → 500            25 elements at Inter 600 and 2 at Inter 700.
                              §8.7 admits Inter at 300, 400 and 500 only, and
                              §8.10 sets both the primary button and the status
                              pill at "weight 500" explicitly. 600 is not a
                              heavier version of a permitted step; it is a step
                              the scale does not have.

   ── WHAT IS DELIBERATELY NOT FIXED HERE, AND WHY EACH IS AN OWNER CALL ───
   1. THE ROOT IS 18px AND §8.7 WANTS 16px. Unchanged, for the reason this
      file already gives at the top: rem is resolved on <html>, so the change
      is global by construction and shrinks all five rooms by a ninth.
      Scoping it — restating the six steps in px inside this block — was
      considered and rejected: it would leave the Today room a ninth smaller
      than Design, Guests, Experience and Files, which is a worse answer than
      a uniform 18px. Every rendered size in this room is therefore 12.5%
      above its §8.7 value, in proportion.
   2. THE ROOM HEADINGS SIT BETWEEN TWO STEPS. .td-h 1.75rem, .rm-wall-h
      1.7rem, .ap-h 1.5rem, .es-title 1.35rem and .t 1.25rem all land above
      --h3 (1.05rem) and below --h2-sm, whose clamp resolves to ~40px at this
      width. Snapping them up is not a paint fix — it is every heading in the
      room getting a quarter larger, which the owner should see before it
      ships. Recorded here with the numbers so the decision has them.
   3. .rm-wall-in CARRIES A GRADIENT, which brand.json forbids outright. It is
      a legibility scrim behind Linen type over a client's own artwork, so
      removing it would cost contrast rather than buy it — the same reasoning
      the portal brand audit already applies to overlays. Left, and named.
   ─────────────────────────────────────────────────────────────────────── */

/* Below the floor of the scale — the only one of these that is a legibility
   defect rather than a tidiness one. */
body[data-active-tab="today"] .tab-panel .es-opt,
body[data-active-tab="today"] .tab-panel .es-sens,
body[data-active-tab="today"] .tab-panel .es-mod-title em{
  font-size: var(--text-micro);
}

/* Quiet meta and specs — §8.7's --text-xs is "Field labels, chips, specs". */
body[data-active-tab="today"] .tab-panel .ap-v,
body[data-active-tab="today"] .tab-panel .es-mod-status,
body[data-active-tab="today"] .tab-panel .es-prog{
  font-size: max(.75rem, var(--text-xs)); /* §24: functional colour is never small text — 12px floor */
}

/* A running paragraph takes the one step §8.7 gives running paragraphs. */
body[data-active-tab="today"] .tab-panel .es-intro{
  font-size: var(--text-base);
}

/* "Card values, piece names, figures" — a module title is that. */
body[data-active-tab="today"] .tab-panel .es-mod-title{
  font-size: var(--h3);
}

/* §8.10: the primary button is "--text-sm uppercase 0.16em, weight 500".
   .es-btn was .66rem/600 — off the scale on both axes at once. */
body[data-active-tab="today"] .tab-panel .es-btn{
  font-size: var(--text-sm);
  letter-spacing: .16em;
  font-weight: 500;
}

/* Inter has three permitted weights and 600 is not one of them. Listed rule by
   rule rather than swept with a `*` selector: a blanket weight rule would also
   overwrite Crimson Pro's 300, and §8.7 gives the two faces different steps. */
body[data-active-tab="today"] .tab-panel .rm-sec-h,
body[data-active-tab="today"] .tab-panel .td-lbl,
body[data-active-tab="today"] .tab-panel .ap-k,
body[data-active-tab="today"] .tab-panel .es-rec strong,
body[data-active-tab="today"] .tab-panel .es-linked-primary > strong,
body[data-active-tab="today"] .tab-panel .es-linked-crunch,
body[data-active-tab="today"] .tab-panel .fld-lbl,
body[data-active-tab="today"] .tab-panel .es-req,
body[data-active-tab="today"] .tab-panel .dna-use b{
  font-weight: 500;
}
/* The room tabs' active state, which portal-rooms.js sets to 600. It is a
   nav label, so §8.7's --text-xs row applies: "Inter 500". */
body[data-active-tab="today"] .rooms button.on{
  font-weight: 500;
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE SUB-VIEWS

   A room is not one screen. Design has four views behind its second rail —
   Direction · The Suite · Your DNA · Your Vision — Guests has five and
   Experience three, and each swaps a different legacy tab panel underneath
   (#tab-concept, #tab-designs, #tab-dna, #tab-briefing, …). The first pass
   censused the view each room OPENS on, so Direction was clean and the other
   three were never looked at. Measured by walking the second rail: The Suite
   21 · Your DNA 16 · Your Vision 38.

   ⚠ ONE THING HERE MUST NOT BE "FIXED": the design specimens. #ts7, #ts17 and
   #tsf11 are set in Great Vibes and Alex Brush, and the palette strips and
   #dp-dot swatches carry arbitrary hexes, because in those elements the
   lettering and the colour ARE the content — they are showing a client their
   own chosen type and palette. A sweep that drives every room to "zero
   off-palette elements" destroys them. Nothing below touches them.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE SUITE (#tab-designs) — the per-piece approval board ─────────────── */
/* §8.3: done is Moss Fern. The pill's TINT hardcodes the retired forest as an
   rgba, so the token rebind cannot reach it — `background:rgba(49,114,84,.15)`
   is a literal, not a var. */
body[data-active-tab] .tab-panel .bb-status-pill.ready,
body[data-active-tab] .tab-panel .bb-status-pill.approved{
  color: var(--ch8-moss);
  background: color-mix(in srgb, var(--ch8-moss) 15%, transparent);
  border-radius: 0;
}
/* Waiting on us is the quiet layer (§8.5), not a colour. */
body[data-active-tab] .tab-panel .bb-status-pill.pending,
body[data-active-tab] .tab-panel .bb-status-pill.in_progress,
body[data-active-tab] .tab-panel .bb-status-pill.in_review{
  color: var(--ch8-onyx-78);
  background: color-mix(in srgb, var(--ch8-onyx) 8%, transparent);
  border-radius: 0;
}
/* The two lights, §8.4 — approve and send back, side by side on every piece. */
body[data-active-tab] .tab-panel .bb-action-approve{ color: var(--ch8-signal-approve); }
body[data-active-tab] .tab-panel .bb-action-approve:hover{
  background: color-mix(in srgb, var(--ch8-signal-approve) 8%, transparent);
}
body[data-active-tab] .tab-panel .bb-action-approve.active{
  background: color-mix(in srgb, var(--ch8-signal-approve) 12%, transparent);
  color: var(--ch8-signal-approve);
}
body[data-active-tab] .tab-panel .bb-action-revise{ color: var(--ch8-signal-revise); }
body[data-active-tab] .tab-panel .bb-action-revise:hover{
  background: color-mix(in srgb, var(--ch8-signal-revise) 8%, transparent);
}
/* The structural teal was tinting three containers. §8.2 — a tint is not a
   signal, and the quiet layer costs no colour. */
body[data-active-tab] .tab-panel .bb-slot,
body[data-active-tab] .tab-panel .pd-piece-hdr,
body[data-active-tab] .tab-panel .pdp-panel{
  background: color-mix(in srgb, var(--ch8-onyx) 3%, transparent);
}
body[data-active-tab] .tab-panel .mts-open{ color: var(--ch8-onyx); }

/* ── YOUR DNA (#tab-dna) — the event's own style record ──────────────────── */
/* Injected by #eviStyles at runtime, all in Montserrat with ink alphas. */
body[data-active-tab] #tab-dna .evi-chip,
body[data-active-tab] #tab-dna .evi-btn{
  font-family: var(--sans);
  color: var(--ch8-onyx);
  background: color-mix(in srgb, var(--ch8-onyx) 5%, transparent);
  border-radius: 0;
}
body[data-active-tab] #tab-dna .evi-p,
body[data-active-tab] #tab-dna .evi-uses span{
  font-family: var(--sans);
  font-weight: 300;
  color: var(--ch8-onyx-78);
}
body[data-active-tab] #tab-dna .evi-lbl,
body[data-active-tab] #tab-dna .evi-lbl i,
body[data-active-tab] #tab-dna .evi .evi-sub{
  font-family: var(--sans);
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ch8-onyx-62);
}

/* ── YOUR VISION (#tab-briefing) — the brief, and its section badges ─────── */
/* A completed section is DONE, so Moss; the tint beside it is the quiet layer. */
body[data-active-tab] .tab-panel .bsp-sec-badge,
body[data-active-tab] .tab-panel .bsp-status-badge{
  color: var(--ch8-moss);
  background: color-mix(in srgb, var(--ch8-moss) 8%, transparent);
  border-color: color-mix(in srgb, var(--ch8-moss) 22%, transparent);
  border-radius: 0;
  font-family: var(--sans);
}

/* The approval pill's tint and edge are `!important` literals of the retired
   forest (portal.css `.bb-status-pill.ready{background:rgba(49,114,84,.05)
   !important}`), so the colour landed and the chip around it did not. */
body[data-active-tab] .tab-panel .bb-status-pill.ready,
body[data-active-tab] .tab-panel .bb-status-pill.approved{
  background: color-mix(in srgb, var(--ch8-moss) 12%, transparent) !important;
  border-color: color-mix(in srgb, var(--ch8-moss) 30%, transparent) !important;
}
body[data-active-tab] .tab-panel .bb-action-approve.active{
  border-color: color-mix(in srgb, var(--ch8-signal-approve) 28%, transparent);
}

/* .sec-num is an eyebrow — uppercase at 0.24em on --text-xs — so §8.7 puts it
   at Inter 500. It computes 700 in the running page, not the 600 that
   portal.css:692 declares, because a later rule in the same file raises it;
   caught by measuring the rendered weight rather than by reading the
   stylesheet, which is the only way this one was visible at all. The two child
   spans inside it carry no weight of their own and simply inherit. */
body[data-active-tab="today"] .tab-panel .sec-num,
body[data-active-tab="today"] .tab-panel .sec-num > span{
  /* ⚠ !important, which nothing else in this file needs. portal.css:4460 sets
     `font-weight:700!important` on .sec-num, and an !important declaration is
     not beaten by specificity — only by another !important from a later
     stylesheet, which this is. Left as the single exception rather than
     removing the !important at the source: portal.css:4460 is a block of nine
     forced rules serving all five rooms, and unpicking it is portal-wide work,
     not this screen's. */
  font-weight: 500 !important;
}

/* The per-piece approve control marks its fill AND its edge `!important`
   (portal.css `.bb-action-approve{border:1px solid rgba(49,114,84,.28)
   !important}` and `.active{background:rgba(49,114,84,.06)!important}`), so
   the word went Moss and the chip around it stayed forest — the two greens
   side by side on the same control. Matched in kind. */
body[data-active-tab] .tab-panel .bb-action-approve{
  border: 1px solid color-mix(in srgb, var(--ch8-signal-approve) 28%, transparent) !important;
}
body[data-active-tab] .tab-panel .bb-action-approve.active{
  background: color-mix(in srgb, var(--ch8-signal-approve) 10%, transparent) !important;
}
body[data-active-tab] .tab-panel .bb-action-revise{
  border-color: color-mix(in srgb, var(--ch8-signal-revise) 28%, transparent) !important;
}

/* The briefing timeline's 3x3 bullet marks carry `background:#265450` inline —
   the retired structural teal as a literal, on an element with no class. Same
   value-matched technique as the faces above, and safe for the same reason:
   #265450 is a RETIRED hex, so anywhere it still appears it is wrong by
   chapter 08. It is not a client's chosen colour, which is why the palette
   strips and swatches are matched on nothing and left alone. */
body[data-active-tab] .tab-panel [style*="background:#265450"],
body[data-active-tab] .tab-panel [style*="background: #265450"]{
  background: var(--ch8-onyx-34) !important;
}

/* The Road's date line (#roomsCss .rd-dt), and the photo-space card's own
   tint — the last two rule-painted residues on the reachable views. */
body[data-active-tab] .tab-panel .rd-dt{ color: var(--ch8-onyx-62); }
body[data-active-tab] #photoSpaceCard{
  background: color-mix(in srgb, var(--ch8-onyx) 3%, transparent);
  border-color: var(--ch8-onyx-20);
}


/* ───────────────────────────────────────────────────────────────────────────
   §9q — THE SUB-NAV IS A DARK PLATE  (owner pick, 2026-09-05)

   The row of views and lenses under the room rail was six ghost boxes on Linen:
   §8.7's outline button, applied to `.roomviews button:not(.on)` at line 677 so
   that navigation borrowed the chrome of a *control you press once*. It gave the
   guests room a row reading EVERYONE · ONE PERSON, ONE RECORD · EVERYONE ·
   REPLIES · COMING · NEEDS ATTENTION where the selected view was the only one
   WITHOUT a box — the boxes marked the things you had not chosen.

   This replaces the boxes with the site's own navigation idiom, unchanged from
   /v15 style.css:648: no chrome at all, and a 1px rule that DRAWS under the word
   from the left, 0 → 100% over 350ms. `.on` holds the finished rule, which is
   what [aria-current] does on the site nav, so selected and hovered are the same
   gesture at two points in time rather than two different pictures.

   ⚠ THE GROUND IS BORDEAUX, AND THAT IS WHAT MAKES THE RULE LINEN.
   §8.9: "on a dark or Mahogany ground it switches to Linen, because red on red is
   invisible." Crimson on Bordeaux measures 1.6:1 — the same measurement /v15
   records at style.css:230 for the site's own nav, which is why that file sets
   `--accent: var(--linen)` over a dark plate instead of drawing a rule nobody
   can see. Every ink here comes from the palette and is measured over #4C061D,
   not inherited from a Linen-ground rule that no longer applies:

     available  Onyx 20  → 9.05:1    no rule
     current    Linen    → 13.86:1   solid rule
     locked     Onyx 20  → 9.05:1    dotted rule, cursor, aria-disabled

   ⚠ THE STATE IS THE RULE, NOT A TINT (owner ruling EVR-044, 2026-09-06). The
   three states were three Linen alphas — 68% resting and 55% locked, written as
   the hexes they resolve to so the contrast instruments could read them. §24.1
   takes colour from chapter 08 and /v15 style.css and from nothing else, and no
   palette ink sits between 4.5:1 and 9:1 on Bordeaux, so those two hexes held the
   palette ratchet red on main and blocked every publish behind it. Locked is now
   as legible as available — §35 keeps it focusable so a keyboard client can READ
   what is locked — and is told apart by a dotted rule where the current view
   carries a solid one.
   ─────────────────────────────────────────────────────────────────────────── */
body[data-active-tab] .roomviews{
  background: var(--ch8-bordeaux);
  border-bottom: 0;
}
/* border:0 undoes §8.7's ghost outline at line 677; the box is gone, and with it
   the 2px bottom border `.on` used to carry — the rule below is the whole mark. */
body[data-active-tab] .roomviews button,
body[data-active-tab] .roomviews button:not(.on){
  position: relative;
  border: 0;
  background: transparent;
  /* +3px of padding replaces the 3px of border this rule just removed (1px of
     ghost outline top and bottom, and the 2px transparent bottom border `.on`
     used to reserve minus the 2px the box no longer has). The row measures 42px
     either way, which is the point: `--hdr-reserve` is a MEASUREMENT of this
     strip (portal.css:175), test/portal-header-reserve.test.mjs asserts the
     five bands against it, and the 304px wrap threshold is calibrated on this
     height. A restyle that moves it silently re-opens all of them. */
  padding: 13px 2px 12px;
  /* ⚠ THE INK COMES FROM THE PALETTE, AND THE RULE CARRIES THE STATE (owner ruling
     EVR-044, 2026-09-06: "use a different cue for the locked tab"). This declared
     Linen 68% and Linen 55% as the hexes they resolve to (#C1A6A6, #AA878C) so the
     contrast instruments could read them — §24.1 accepts colour from chapter 08 and
     /v15 only, and no palette ink sits between 4.5:1 and 9:1 on Bordeaux, so the
     ratchet was red on main and every publish with it. A token is not a color-mix:
     getComputedStyle resolves var() to `rgb()`, so the audits still read the ink.
     Three states, three RULES rather than three tints:
       available  Onyx 20 → 9.05:1, no rule
       current    Linen   → 13.86:1, solid rule (below)
       locked     Onyx 20 → 9.05:1, dotted rule (below), cursor + aria-disabled */
  color: var(--ch8-onyx-20);
  transition: color 350ms var(--ease);
}
body[data-active-tab] .roomviews button::after{
  content: "";
  position: absolute;
  left: 0;
  bottom: 4px;
  height: 1px;
  width: 0;
  background: var(--ch8-linen);
  transition: width 350ms cubic-bezier(.4, 0, .2, 1);
}
body[data-active-tab] .roomviews button:hover:not(.is-locked),
body[data-active-tab] .roomviews button:not(.on):hover{
  color: var(--ch8-linen);
  border: 0;
}
body[data-active-tab] .roomviews button:hover:not(.is-locked)::after,
body[data-active-tab] .roomviews button.on::after{
  width: 100%;
}
body[data-active-tab] .roomviews button.on{
  background: transparent;
  color: var(--ch8-linen);
}
body[data-active-tab] .roomviews button.is-locked{
  color: var(--ch8-onyx-20);   /* 9.05:1 on Bordeaux — legible, because a label nobody can read is missing, not disabled */
  border: 0;
  cursor: not-allowed;
}
/* The locked cue is the RULE, not a dimmer ink: a dotted line under the word, where the
   current view carries a solid one. Same height, same position, so the strip does not move
   (--hdr-reserve is measured from it; test/portal-header-reserve.test.mjs holds five bands). */
body[data-active-tab] .roomviews button.is-locked::after{
  width: 100%;
  height: 0;
  background: transparent;
  border-bottom: 1px dotted var(--ch8-onyx-20);
}
/* The ring is Linen for the reason §8.9 gives and the a11y audit measures in
   pixels: a Crimson ring on a Bordeaux plate is 1.6:1, i.e. a ring that renders
   and cannot be seen. Offset OUTWARD is safe here — unlike `.rooms`, this row is
   not overflow-x:auto, so nothing clips it. */
body[data-active-tab] .roomviews button:focus-visible,
body[data-active-tab] .roomviews button.on:focus-visible{
  outline: 2px solid var(--ch8-linen);
  outline-offset: 2px;
  box-shadow: none;
}
@media (prefers-reduced-motion: reduce){
  body[data-active-tab] .roomviews button,
  body[data-active-tab] .roomviews button::after{ transition: none; }
}

/* ── THE PLAN CARD'S SMALL BUTTONS KEEP A HIT BOX (EVR-120, WCAG 2.5.8, 2026-09-09) ──────────
   "Mark done" ([data-plan-done]) and "Share this out" / the assignee chip ([data-plan-assign])
   are drawn inline by portal-plan.js at .56rem with 3px of padding. After the owner's 2026-09-08
   --text-sm rulings they MEASURED 112×19 on the phone and 123×21 on desktop
   (scripts/portal-a11y-audit.mjs, Today and Design views) — under the 24px minimum, and under the
   44px house floor on the phone. The type size is the owner's; the hit box is the law's. Stated
   here rather than in the inline style so a future re-size of the font cannot take the box with it. */
/* Scoped to the body attribute only: #clientPlanCard is mounted OUTSIDE .tab-panel, so a
   .tab-panel-scoped rule MEASURED as never matching (Share this out still 112×19). */
body[data-active-tab] [data-plan-done],
body[data-active-tab] [data-plan-assign],
body[data-active-tab] [data-plan-view],
body[data-active-tab] [data-plan-ics]{
  min-height: 24px;
  display: inline-flex;
  align-items: center;
}
@media (max-width: 900px){
  body[data-active-tab] [data-plan-done],
  body[data-active-tab] [data-plan-assign],
  body[data-active-tab] [data-plan-view],
  body[data-active-tab] [data-plan-ics]{
    min-height: 44px;
  }
}

/* ══ THE DESIGN ROOM OF A *NEW* EVENT (EVR-136, WCAG 1.4.3 + 2.5.8, 2026-09-09) ═══════════════
   Every a11y run before this one measured the DELIVERED fixture, so none of what follows had
   ever been measured. On Sofia — an event at the new stage —
   `PORTAL_LOGIN_URL=… node scripts/portal-a11y-audit.mjs --only design` reported **38 blockers**
   across the six Design views at both widths: 16 axe colour-contrast nodes and 22 hit boxes
   under 24px. Each rule below carries the number it was written against.

   ⚠ THE FIXES ARE HERE RATHER THAN BESIDE THE RULES THEY CORRECT, and the reason is the cascade,
   the same one the 44px floor block above states. Four of these six declarations lose to their
   originals from portal.css unless they win on BOTH counts: `.doc-big-num`'s colour is
   `!important` at portal.css:4575, and `.bb-pr-specs em`'s dimming is an INLINE `opacity` written
   in the markup — neither can be beaten by a rule earlier in the cascade at (0,1,0). This sheet
   loads last and `body[data-active-tab]` is (0,2,0), which is exactly enough.

   ⚠ NEVER OPACITY ON TEXT. Three of the four contrast failures below are the same mistake wearing
   three faces — a sanctioned ink step multiplied by an opacity, which composites to a colour
   nobody chose and no token names. Each is replaced by the chapter-08 step that says what the
   text is: onyx 62% (#6B6864, 4.94:1 on Linen) is the lightest that clears 4.5:1 as TEXT.
   ─────────────────────────────────────────────────────────────────────────────────────────── */

/* 1.4.3 — `.doc-big-num` is the delivered document's ORDINAL, painted
   `color:var(--gold)!important;opacity:.14!important` at portal.css:4575. MEASURED #e8ddcf on
   Linen: **1.19:1** at 46.8px (desktop) and 60.8px (phone) — large text, so the threshold is 3:1
   and it misses by more than half. The house has already answered what an ordinal is painted
   with: `--sage-lt`, "the lighter structural teal: ordinals, quiet marks", is bound to onyx 62%
   two hundred lines above this for that exact reason. Stated as the step rather than through the
   alias so the reason survives a future rebind. */
body[data-active-tab] .doc-big-num{
  color: var(--ch8-onyx-62) !important;
  opacity: 1 !important;
}

/* 1.4.3 — the interactive-design badge is IVORY ON A LIGHT GROUND, and it is the rebind above
   that made it one. portal.css:2721 paints it `background:var(--ash, …62% onyx…)`, and the
   2026-08-25 note beside that rule reasons entirely about a DARK scrim: "ivory on the same
   ground is 17.71:1". True then. But `.bb-thumb` is in the dark-plate scope at the top of this
   sheet, where every quiet alias INVERTS — on a dark plate the dimmer step is the lighter one —
   so `--ash` resolves to onyx 34% (#AAA69F, opaque), the badge paints that as its GROUND, and
   ivory on it MEASURES 2.16:1, six nodes per Design/designs view. An alias flipped for text is
   wrong the moment a rule uses it as a ground. Rebound rather than re-declared so portal.css
   stays the one place the badge's background is written, and so the blur, border and fallback
   it carries are untouched. */
body[data-active-tab] .bb-thumb-html-badge{
  --ash: color-mix(in srgb, var(--ch8-onyx) 78%, transparent);
}

/* 1.4.3 — the pending piece's specification aside. `<em style="…opacity:.6">` in
   client-portal.html dims `.bb-pr-specs`'s own colour (`--sage`, rebound to onyx 78%) to
   #8d8a84 on Linen — **3.06:1** at 11.5px. `opacity` needs the `!important` because the losing
   declaration is inline; the colour does not, because nothing declares it on the element. */
body[data-active-tab] .bb-pr-specs em{
  opacity: 1 !important;
  color: var(--ch8-onyx-62);
}

/* 1.4.3 — "CHANGE HISTORY". Two rules in portal.css decide this label between them: 5585 wins
   the colour (`--sage` → onyx 78%) and 2775 still supplies `opacity:.45`, which composites to
   #a4a099 on the Alabaster panel — **2.18:1** at 12.5px. */
body[data-active-tab] .rev-hist-section-lbl{
  color: var(--ch8-onyx-62);
  opacity: 1;
}

/* ── 2.5.8 — THE DESIGN ROOM'S HIT BOXES ──────────────────────────────────────────────────────
   Three controls, 22 of the 38 blockers. MEASURED phone / desktop:
     button.menu-del-btn        18×14 / 19×16   Design · "×" beside every dish
     button.menu-add-dish       85×18 / 95×20   Design · "+ Add dish", once per course
     .bb-pr-specs-wrap summary 315×11 / 852×13  Design · "Intended printer settings"
   ⚠ min-height/min-width, never height/width — a floor must never SHRINK a control that already
   clears it, and "+ Add dish" and the summary are both already far wider than 44.
   ⚠ THE DELETE BUTTON IS THE ONLY ONE THAT NEEDS WIDTH: it is a bare "×" with 8px of left
   padding, so it is square-ish and small in both directions, and it takes the centring with it
   (a 24px box with `padding:0 0 0 8px` and no centring puts the glyph at the top-left, which is
   the lesson `.tstar` and the skip link each taught once already).
   ⚠ THE SUMMARY TAKES PADDING AS WELL AS THE FLOOR, and deliberately not `display:flex`: a
   <summary> is `display:list-item`, and flex removes its disclosure marker — the one thing that
   says the line opens. Padding keeps the marker and centres the text inside the taller box. */
body[data-active-tab] .menu-add-dish{
  min-height: 24px;
}
body[data-active-tab] .menu-del-btn{
  min-width: 24px;
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
body[data-active-tab] .bb-pr-specs-wrap > summary{
  min-height: 24px;
  padding: 6px 0;
}
@media (max-width: 900px){
  body[data-active-tab] .menu-add-dish{ min-height: 44px; }
  body[data-active-tab] .menu-del-btn{ min-width: 44px; min-height: 44px; }
  body[data-active-tab] .bb-pr-specs-wrap > summary{ min-height: 44px; padding: 16px 0; }
}

/* 1.4.3 — THE "IN DESIGN" PILL ON A PIECE THAT DOES NOT EXIST YET (EVR-136, second measure,
   2026-09-10). Found by the NEW-EVENT fixture and by nothing before it: every earlier a11y run
   logged in as a client whose pieces were finished, and `.bb-status-pill.in_progress` is the
   one word only an unfinished piece wears. On Sofia's four pending rows axe measures
   **4.35:1** — #846439 on #EDE5DA at 12.48px — four nodes per Design/designs view, at both
   widths.

   ⚠ THE INK IS NOT THE MISTAKE; THE TINT UNDER IT IS, AND THEY ARE THE SAME HUE. portal.css
   :4179 paints this pill `color:var(--gold-ink)` over `background:color-mix(--ch8-amber 4%)`,
   and tokens.css:126 is explicit that --gold-ink is "the same hue carried to a READABLE value"
   — 5.09:1, measured on Linen. Stack 4% of the fill hue behind the text hue and that headroom
   is spent: the ground moves from #F1EAE1 to #EDE5DA and 4.55 becomes 4.35. Dropping only the
   tint therefore lands at 4.55:1 — a pass by six hundredths, which is not a fix anyone can
   defend after the next token nudge.

   ⚠ SO IT IS RESTATED, NOT REDESIGNED. This sheet ALREADY publishes the answer for this state
   two hundred lines above — "Waiting on us is the quiet layer (§8.5), not a colour",
   `.bb-status-pill.in_progress{color:var(--ch8-onyx-78)}` — and that declaration has never
   reached a screen: it is (0,3,0) with no !important against portal.css:4179's (0,2,0) WITH
   one, and an !important is not beaten by specificity. Exactly the defeat the note at
   portal.css:4176 records for `.pending`, matched at the source that day; `.in_progress` was
   left behind. Restating chapter 08's own rule here, in kind, is what that note did for the
   pill beside this one. 4.35 → 8.55:1. */
body[data-active-tab] .tab-panel .bb-status-pill.in_progress{
  color: var(--ch8-onyx-78) !important;
  border-color: var(--ch8-onyx-20) !important;
  background: transparent !important;
}
