/* ═══════════════════════════════════════════════════════════════════════════
   PORTAL NAVIGATION SHELL — DIRECTION 6 ("Planner desk")            2026-09-15
   ═══════════════════════════════════════════════════════════════════════════

   The bar, the sidebar, the status strip and the content frame that
   app/scripts/portal-nav.js builds. Spec: docs/execution/PORTAL_DIRECTION6_SPEC.md
   §1–§4; decisions: the orchestrator's D6_DECISIONS (2026-09-15).

   ⚠ EVERY RULE IS SCOPED TO `body.d6`, AND THAT IS LOAD-BEARING, NOT TIDINESS.
   `body.d6` is written by portal-nav.js only when it has actually found the
   header and built the sidebar. Three consumers render the portal's own CSS
   and JS WITHOUT this file — test/portal-touch-floor-rendered.test.mjs and
   test/portal-rail-target-size.test.mjs build a stub page from portal.css +
   portal-rooms.js alone, and scripts/portal-a11y-audit.mjs measures the real
   page — so the legacy `.rooms` rail must keep rendering at its full size
   wherever this file is absent. Unscoped rules would hide a rail those guards
   measure, and the 44px floor would be asserted against nothing.

   ⚠ NO RAW HEX ANYWHERE IN THIS FILE. scripts/portal-palette-census.mjs
   discovers every stylesheet client-portal.html links and ratchets the count of
   off-palette colours (test/ains-portal-palette.test.mjs case 1). Chapter 08
   token names only — `var(--ch8-*)` — plus `transparent`, `currentColor` and
   `color-mix()`. A fallback hex is a new off-palette colour to the census even
   when it is byte-identical to the token it mirrors.

   ⚠ RADII ARE THE HOUSE'S, NOT DIRECTION 6's. tokens.css:265-277 allows 0, 2px
   (inputs only) and 50%; Direction 6 draws 6/7/8/14px. The orchestrator recorded
   the fork as an owner question and the house wins here: 0 everywhere, 2px on
   the search control because it is input-shaped, 50% on the one dot.

   ⚠ CONTRAST. The sidebar ground is Alabaster (#E7E3DD). `--ch8-onyx-62` is
   4.94:1 on Linen but only 4.33:1 on Alabaster — under AA — so every muted ink
   ON THE SIDEBAR is `--ch8-onyx-78` (7.50:1 on Alabaster). The status strip and
   the bar sit on Linen, where onyx-62 passes, and use it.

   ⚠ THE FACES ARE THE PORTAL'S, NOT THE SITE'S — `var(--sans)` AND `var(--serif)`,
   NEVER A FAMILY NAMED HERE (2026-09-18, closeout-max pkgJ). §8.6 gives the Portal
   two faces, Crimson Pro and Inter, and keeps Montserrat for the wordmark alone;
   Libre Baskerville is retired outright. This file shipped four literal families —
   `'Montserrat'` on the search control, the Help button and the view strip, and
   `'Libre Baskerville'` on the status strip's event name. portal-v15.css's §8.6
   sweep could not reach any of them: it hangs off `body[data-active-tab] .tab-panel`
   and this chassis sits OUTSIDE the panel, in the header and the rail. Measured by
   scripts/portal-typeface-census.mjs on 2026-09-18 against the A11YDLV fixture:
   352 rendered elements in Montserrat off the wordmark where the 2026-09-09 run had
   0, and 258 of them — `#pnFind` 28, `#pnHelp` 28, `#roomViews button` 202 — came
   from this file. The other 94 are portal.css's Direction-6 room blocks, fixed in
   the same change. A face written as a name here cannot be swept from anywhere;
   the variable is the fix, and it is why `var(--sans)` is what the rest of this
   file already uses.
   ═══════════════════════════════════════════════════════════════════════════ */

body.d6 {
  /* One number drives the whole frame: the sidebar's width, the content's left
     gutter and the drawer's breakpoint all read it, so the two can never disagree
     (the `--bar` posture of the prototype, c06:4809). */
  --pn-w: 240px;
  --pn-gutter: 18px;
  /* ⚠ THE BAR'S GAP IS A VARIABLE BECAUSE TWO RULES HAVE TO AGREE ON IT (2026-09-15,
     d6-topbar). portal-v10.css §9m joins the event chip and the account chip into one
     segmented control with `#hdrEventSwitcher{margin-right:-10px}`, and its own comment says
     the number exists to "cancel .hdr-meta's 10px flex gap" — but §9k had already made that
     gap 8px, so the event chip was drawn 2px ON TOP of the account chip at every width
     (measured at 1440/1280/1024/768/390, and the same 2px at 4898b708c, so it predates
     Direction 6). A second hard-coded number cannot be kept in step by remembering; the gap
     and the margin that cancels it now read the same value. */
  --pn-bar-gap: 8px;
  padding-left: var(--pn-w);
}

/* ── THE BAR ─────────────────────────────────────────────────────────────────
   `.hdr-top` keeps its element, its id-bearing children and its place inside
   the fixed `.portal-hdr`; only its layout changes. Rewriting the markup would
   move `#hdrAccountSwitcher`, `#inboxBell`, `#portalCommsStop` and
   `#hdrEventSwitcher` out from under the guards and the tutorial anchors that
   address them. */
/* ⚠ THE RESERVE IS LEFT ALONE — `.portal-hdr` KEEPS ITS min-height.
   portal.css holds the header at `min-height: var(--hdr-reserve)` so the page
   below it does not jump while syncHeaderPad() measures, and every band is a
   MEASUREMENT of the settled header at that width. The first cut of this file
   released it (`min-height: 0`), reasoning that a 52px bar does not need 137px
   reserved — and that was wrong twice over: the D6 header is the bar PLUS the
   status strip PLUS #roomViews, which settles at 136-160px on a signed-in client
   (measured on all four fixtures, 1440/1024/390), i.e. about what the bands
   already say; and releasing it re-opened the shift the variable exists to
   prevent (test/portal-header-reserve cases 2 and 3 went red at 280px and 768px).
   If a band does need retuning it is retuned in portal.css, with that test as the
   instrument, never worked around from here. */

/* ⚠ THE THREE NEW CONTROLS ARE INERT UNTIL THE SHELL IS UP, AND THIS RULE IS
   UNSCOPED ON PURPOSE. They are static markup in client-portal.html, so a page
   that loads this sheet but never gets `body.d6` — portal-nav.js absent, or
   still waiting for the rail — would otherwise render three unstyled buttons
   inside `.hdr-meta` and push the bell, the per-event stop, the event band and
   the account menu past the right edge of a phone (measured:
   test/portal-phone-layout-reachable case 1 at 320px). They select nothing but
   this file's own classes, so nothing else can be hidden by them. */
.pn-burger, .pn-find, .pn-ib, .pn-tools { display: none; }

/* ⚠ THE D6 BAR IS A ≥701px LAYOUT, AND THE NUMBER IS PORTAL-V10's, NOT A CHOICE.
   portal-v10.css §9p (owner decision EVD-166, check 02-03) makes `.hdr-top` a
   TWO-ROW grid below 700px — logo · stop · bell, then event · account — because
   measured on production at 320-430px the event switcher and the whole account
   menu sat past the right edge in all three engines while nothing scrolled. That
   rule and this one have identical specificity, so a single flex row here would
   win on load order alone and put the same four controls back off the screen.
   Below 700px the two-row header stands untouched; the burger takes a column of
   its own (see the phone block), and Search and Help move into the sidebar,
   where there is room for their words. */
@media (min-width: 701px) {
  body.d6 .hdr-top {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 52px;
    padding: 0 var(--pn-gutter);
    background: var(--ch8-linen);
  }
}

body.d6 .hdr-logo { margin-right: 0; padding: 8px 0; }

/* ⚠ AND THE WORDMARK KEEPS ITS 44px TARGET WHERE THERE IS A FINGER (2026-09-15, d6-topbar).
   The rule above is a `padding` shorthand on a selector one class more specific than
   portal.css's own touch-floor block, so it silently replaced the 14.5px that lifts this link
   to 44px on a phone and left the matching negative margins behind: measured 44px tall at 390
   before Direction 6 (4898b708c) and 31px after. The conditions are portal.css's, unchanged —
   the padding and the margin that gives its growth back to the layout are restated together,
   because either one alone moves a --hdr-reserve band. `.hdr-top` is 122px at 390 with both
   values, so no band moves. */
@media (max-width: 640px), (max-height: 500px) and (orientation: landscape), (hover: none) and (pointer: coarse) {
  body.d6 .hdr-logo { padding: 14.5px 0; margin-top: -14.5px; margin-bottom: -14.5px; }
}

/* ⚠ THE SUPPORT PROMISE IS SHRUNK, NEVER HIDDEN. It is the line that tells a
   paying client how to reach the house, and portal.css already hides it under
   900px — hiding it further up as well left test/portal-header-reserve case 3a
   ("the support line is a 24px target wherever it is shown") measuring nothing at
   1000px, which is a guard going quiet rather than a promise being kept. In the
   flex bar it shrinks and ellipsizes instead, and the search control shrinks with
   it, so the two give way before any control does. */
body.d6 .hdr-support { justify-self: auto; flex: 0 1 auto; min-width: 0; }

/* ⚠ SCOPED TO THE SAME ≥701px BAND AS `.hdr-top`, AND FOR A SHARPER REASON.
   portal-v10.css §9p makes `.hdr-meta` `display:contents` below 700px so its four
   children — the per-event stop, the bell, the event band and the account menu —
   place themselves on the HEADER's grid, two to a row. This rule has identical
   specificity and loads later, so an unscoped `display:flex` took all four back
   out of that grid and into one row that ran off a 320px phone: measured
   72→604px inside a 320px viewport, with the wordmark overlapping the stop
   control. Above 700px there is no grid to belong to and flex is the bar. */
@media (min-width: 701px) {
  body.d6 .hdr-meta {
    display: flex;
    align-items: center;
    gap: var(--pn-bar-gap);
    margin-left: auto;
    min-width: 0;
    /* ⚠ AND IT MUST BE ALLOWED TO SHRINK (2026-09-15, d6-polish). `.hdr-meta` computes to
       `flex:0 0 auto` in the row above it, so at 1024px the whole group of controls — Search,
       Help, Stop messages, the bell, the event chip, the account chip — ran 32px past the
       right edge and the client's own name was sliced in half. `min-width:0` alone cannot fix
       that: a flex item with shrink 0 never gives way whatever its floor. With shrink on, the
       search control gives first (it is `0 1 280px` down to 110px) and the two chips ellipsize
       after it, which is the order the comment above intends. */
    flex: 0 1 auto;
  }
}

/* ── The bar's three Direction 6 controls ─────────────────────────────────── */
body.d6 .pn-burger {
  display: none;               /* phone only — see the ≤640 block */
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 1px solid var(--ch8-onyx-20);
  border-radius: 0;
  background: transparent;
  color: var(--ch8-onyx);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

body.d6 #pnFind, body.d6 .pn-tools .pn-find {
  display: inline-flex;
  align-items: center;
  flex: 0 1 280px;
  min-width: 110px;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--ch8-onyx-20);
  border-radius: 2px;          /* the house's one softening — inputs only */
  background: color-mix(in srgb, var(--ch8-linen) 60%, transparent);
  color: var(--ch8-onyx-62);
  font-family: var(--sans);      /* §8.6 — the product face; see the header note */
  font-size: 13px;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
body.d6 .pn-find:hover { border-color: var(--ch8-onyx-62); color: var(--ch8-onyx-78); }

body.d6 #pnHelp, body.d6 .pn-tools .pn-ib {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  color: var(--ch8-onyx-78);
  font-family: var(--sans);      /* §8.6 — the product face; see the header note */
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
}
body.d6 .pn-ib:hover { background: var(--ch8-onyx-12); color: var(--ch8-onyx); }

body.d6 .pn-find:focus-visible,
body.d6 .pn-ib:focus-visible,
body.d6 .pn-burger:focus-visible {
  outline: 2px solid var(--ch8-crimson);
  outline-offset: 2px;
}

/* ── THE BAR IS ONE 52px ROW, AND THREE EXISTING CONTROLS HAVE TO FIT IT ─────
   Every one of them stays — the owner's rule for this direction is that the
   header keeps everything Direction 6 drops — so they are made to sit on one
   line rather than removed.
   · #portalCommsStop carries an `i` hint button that portal-hint.js appends
     after it (its own title is over 40 characters). In a block container the
     hint wrapped onto a second line and took the bar to 76px; a flex row keeps
     it beside the control it explains.
   · The event band and the account chip each draw a two-line stack — an
     uppercase eyebrow over a name. The eyebrow's words ("WEDDING · THE WHOLE
     EVENT", "CLIENT") are the first two things the status strip below already
     says, so in the D6 bar the chips keep the NAME, which is the half a client
     uses to tell one event from another. Nothing is removed from the DOM:
     the strip reads #hdrEventLabel's text, and textContent is readable on a
     display:none node. */
@media (min-width: 701px) {
  body.d6 #portalCommsStop { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; }
  body.d6 .hdr-event-label,
  body.d6 .hdr-client-label { display: none; }
  body.d6 .hdr-event-chip,
  body.d6 .hdr-client-chip { min-height: 36px; align-items: center; }
  /* ⚠ THE LAST CONTROL IN THE BAR WAS BEING CUT IN HALF AT 1024 (2026-09-15, d6-polish).
     A flex row only gives way where something is allowed to: `#pnFind` shrinks and the support
     line ellipsizes, but the event chip and the account chip had `min-width:auto` from their
     own content, so once those two had shrunk to their floors the row simply ran past the right
     edge and "Zofia Kowalczyk & Min Chen" was sliced at the viewport (measured at 1024). The
     names now ellipsize instead — the client still sees which event and which account they are
     in, which is the half of each chip the D6 bar keeps, and nothing is pushed off-screen. */
  body.d6 .hdr-event-switcher,
  body.d6 .hdr-account-switcher { min-width: 0; }
  /* `nowrap` is what makes the name shrink: a WRAPPING flex line never compresses an item
     below its content, it overflows the line instead — which is why the client's name was
     drawn 21px outside its own chip and over the event chip beside it. */
  body.d6 .hdr-event-chip,
  body.d6 .hdr-client-chip { min-width: 0; flex-wrap: nowrap; justify-content: flex-start; }
  /* ⚠ `flex:1 1 0`, NOT a shrink on an `auto` basis. With `flex-basis:auto` the name's base size
     is its own text — measured 152px inside a 101px chip — and the browser drew it 19px outside
     the chip and over the event chip beside it rather than compressing it. A zero basis makes
     the name grow INTO whatever the chip has and never past it, which is what lets the ellipsis
     below actually fire. */
  body.d6 .hdr-event-name,
  body.d6 .hdr-client-name {
    flex: 1 1 0;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Both menus hang from the RIGHT edge of their chip, because the chips are now narrower than
     the menus' own min-widths (180px and 220px) and a left-anchored menu hung 23px past the
     viewport at 1024. portal-v10.css already does this below 700px for the same reason. */
  body.d6 .hdr-account-dropdown,
  body.d6 #hdrEventSwitcher .hdr-event-dropdown { left: auto; right: 0; }
  /* ⚠ THE TWO CHIPS WERE 2px ON TOP OF EACH OTHER, AND IT WAS A STALE NUMBER, NOT A SHRINK
     PROBLEM (2026-09-15, d6-topbar). §9m's -10px was written to cancel a 10px gap; the gap is
     8px, so `#hdrEventSwitcher`'s border box ended 2px PAST `#hdrAccountSwitcher`'s left edge —
     the account chip painted over the last 2px of the event chip, at every width, and the two
     controls' rects intersected wherever they were measured. The owner's join stays exactly as
     §9m designed it (attached edges, the client chip's left border as the divider, event chip
     `border-right:0`): the margin now cancels the gap that is actually there, so the two boxes
     MEET instead of overlapping. Same variable as `.hdr-meta`'s gap above, so the pair cannot
     drift apart again. */
  body.d6 #hdrEventSwitcher { margin-right: calc(-1 * var(--pn-bar-gap)); }
}

/* ⚠ BELOW 701px THE SAME -10px IS SIMPLY WRONG, AND IT IS VISIBLE THERE (2026-09-15,
   d6-topbar). portal-v10.css §9p puts the event band and the account menu in two different
   grid areas of the two-row header, 8px apart, and gives the event chip its RIGHT BORDER BACK —
   there is no segmented join on a phone. §9m's negative margin still applied, so the two
   fully-bordered boxes overlapped by 2px at 390 (measured). Nothing else about §9p changes. */
@media (max-width: 700px) {
  body.d6 #hdrEventSwitcher { margin-right: 0; }
}


/* ── SEARCH AND HELP WHERE THE BAR CANNOT HOLD THEM ──────────────────────────
   Below 701px the header is portal-v10's two-row grid and has no room for two
   more controls, so the same two buttons are drawn at the top of the sidebar —
   the drawer on a phone, the always-open rail at 641-700. Nothing is lost and
   nothing is duplicated on screen: the bar's pair and this pair are never both
   visible, and both press the same handlers (#roomBtnFind's command palette and
   EvorrahTutorial.openHelp('portal')). */
body.d6 .pn-tools {
  display: none;
  gap: 6px;
  padding: 8px 14px 4px;
}
@media (max-width: 700px) {
  body.d6 .pn-tools { display: flex; }
  body.d6 .pn-tools .pn-find { flex: 1 1 auto; min-height: 44px; }
  body.d6 .pn-tools .pn-ib { min-height: 44px; }
  body.d6 #pnFind, body.d6 #pnHelp { display: none; }
}

/* ── THE STATUS STRIP ────────────────────────────────────────────────────────
   A child of `.portal-hdr`, so syncHeaderPad() sums it into `--hdr-h` and every
   consumer of that number — body's padding-top, scroll-padding-top and
   hdrOffset() — reserves for it without being told twice. */
body.d6 .pn-strip {
  display: block;
  padding: 7px var(--pn-gutter);
  background: var(--ch8-linen);
  border-top: 1px solid var(--ch8-onyx-12);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ch8-onyx-62);
  /* never clips a FACT: the prototype's own `overflow:hidden` ate the plan label at
     tablet widths (spec §1.4, "a real defect to fix in implementation"). The only thing
     clipped below is the leading hairline of each wrapped line, inside .pn-s-row. */
  overflow: visible;
  white-space: normal;
}
body.d6 .pn-strip:empty { display: none; }
/* ⚠ THE STRIP RESERVES ITS OWN BAND, BECAUSE THE PINNED HEADER'S HEIGHT MUST NOT DEPEND ON
   THE EVENT'S FACTS (2026-09-15, d6-polish). `--hdr-reserve` is a MEASUREMENT of the settled
   header and its guard allows 24px of slack; Direction 6 put a wrapping band of text inside
   that header, and the band is 35px for a client with three facts and 113px for one with ten
   on a 320px phone. Measured on all four fixtures on the rig: 35-36px at 641px and above,
   94px at 360-390px upright, 113px at 280-320px upright. A floor at those numbers does not
   add a pixel to any client who has them — the reserve was already holding the header at that
   height and the space was appearing as a gap BELOW the header instead — and it makes the
   header the same height with the facts, without them, and in a fallback font.
   ⚠ A FLOOR, NOT A CAP: `min-height` never clips a fact (spec §1.4), it only stops the box
   collapsing. And `:empty` still wins over it, so a portal with no event reserves nothing. */
body.d6 .pn-strip { min-height: 36px; }
/* ⚠ UPRIGHT ONLY, AND IN TWO STEPS. The bands are the widths the strip's own wrapping steps
   at, measured; `(orientation:portrait)` keeps a 640x520 landscape screen — which has no rows
   to spare — out of the 94px floor, and it is the same query pair `--hdr-reserve` and
   placeViews() already split the phone on. */
@media (max-width: 640px) and (orientation: portrait) { body.d6 .pn-strip { min-height: 94px; } }
@media (max-width: 320px) and (orientation: portrait) { body.d6 .pn-strip { min-height: 113px; } }
/* ⚠ NO SEPARATOR CAN DANGLE, AT ANY WIDTH (2026-09-15, d6-polish). A "·" written between
   items sat at the END of a line whenever the strip wrapped ("Kowalczyk Chen Wedding ·" on a
   390px phone). CSS cannot remove a separator at a line start, so the separator is not text:
   every item draws a 1px hairline on its LEFT, the inner row is pulled 11px left, and the
   outer row clips that 11px — so the hairline of whichever item begins a line, first or
   wrapped, falls into the clipped strip and only the hairlines BETWEEN two items on the same
   line are ever seen. Vertical growth is untouched: overflow clips 11px sideways, never a row. */
body.d6 .pn-strip .pn-s-row { overflow: hidden; }
body.d6 .pn-strip .pn-s-in {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  row-gap: 2px;
  margin-left: -11px;
}
body.d6 .pn-strip .pn-s-i {
  display: inline-flex;
  align-items: baseline;
  padding-left: 10px;
  margin-right: 10px;
  border-left: 1px solid var(--ch8-onyx-20);
  line-height: 1.35;
}
body.d6 .pn-strip .pn-s-name {
  /* §8.6 retires Libre Baskerville outright; the Portal's document face is Crimson
     Pro. The census read 0 Libre Baskerville on 2026-09-18 only because this
     fixture's strip rendered without an event name — the declaration is a residue
     waiting for the first client whose event is named. */
  font-family: var(--serif);
  font-size: 15px;
  color: var(--ch8-onyx);
}
body.d6 .pn-strip .pn-s-fig { color: var(--ch8-onyx); font-weight: 500; }
body.d6 .pn-strip .pn-s-g { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; }

/* ── THE SIDEBAR ─────────────────────────────────────────────────────────────
   Fixed rather than a flex column, because `.portal-hdr` is already
   position:fixed and the page below it is 15,000 lines of existing layout. A
   shell rewrite would have had to re-parent `main#portalMain`; a fixed rail plus
   one `padding-left` on body does the same job and moves nothing. */
/* ⚠ THE SIDEBAR STARTS UNDER THE STATUS STRIP, NOT UNDER THE HEADER (2026-09-15, d6-polish).
   Direction 6 draws the sidebar directly below the strip and #roomViews at the top of the
   CONTENT column only. #roomViews cannot leave `.portal-hdr` on a desktop — syncHeaderPad sums
   the header's children into --hdr-h, the header-reserve bands are measurements of that sum,
   and test/portal-view-strip-scrolls-on-upright-phone case 2 holds it after the rail — so the
   node stays where it is and only the GEOMETRY moves: #roomViews is inset by the sidebar's
   width (below), and the sidebar rises to the strip's bottom edge and sits one step above the
   header (201 over 200) so it covers the empty left end of the header's views row.
   `--pn-side-top` is that edge measured inside the header by portal-nav.js#syncSideTop; before
   it is measured the old reserve stands. Nothing in the header's left 240px can open over it:
   the account menu, the event switcher, the bell and their pop-overs are all right-aligned. */
body.d6 .pn-side {
  position: fixed;
  top: calc(var(--beta-h, 0px) + var(--pn-side-top, var(--hdr-h, var(--hdr-reserve))));
  left: 0;
  bottom: 0;
  width: var(--pn-w);
  z-index: 201;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--ch8-alabaster);
  border-right: 1px solid var(--ch8-onyx-20);
  padding: 4px 0 14px;
  /* ⚠ A ROW IS NEVER LEFT SLICED AT THE TOP EDGE (2026-09-16, d6-walkfix). The rail's top edge
     is the status strip's bottom edge, so a scrolled rail cut whatever row happened to be there
     through the middle — "Set-up ● 8" halved under the strip on the `guests` walk at 1440.
     `scroll-padding-block` makes every scroll the rail performs for itself (focus moving with
     Tab, scrollIntoView) stop a whole row clear of both edges; the fade below says there is
     more when the rail still has to scroll. */
  scroll-padding-block: 44px 44px;
}

body.d6 .pn-side ul { list-style: none; margin: 0; padding: 0; }
body.d6 .pn-side li { margin: 0; }

/* ⚠ A DISPLAY RULE ON `.pn-i` OUTRANKS THE BROWSER'S OWN `[hidden]`, so the
   attribute has to be honoured explicitly. The After room sets `hidden` on its
   button before the event has happened (§22A.3: a room that opens onto nothing
   must not be announced), and without this it would have stayed a 0px-high but
   present flex box in the list. */
body.d6 .pn-i[hidden] { display: none; }

/* The sentence a badge or a lock carries, for a screen reader.
   ⚠ IT IS NOT A `title`, AND THAT IS THE WHOLE POINT. app/scripts/portal-hint.js
   turns every visible `title` of 40 characters or more into a little "i" button
   placed AFTER the control — the house's answer to `title` being invisible on
   touch — and eleven of those in a 240px rail is eleven extra tab stops and a
   column of italic i's down the sidebar (measured on the `guests` fixture, one
   under "What we handle"). Carrying the sentence as visually-hidden text inside
   the button gives every client the same words, on touch as well as hover,
   without a second control: the accessible name becomes "What we handle,
   Evorrah is handling 8 of 12 parts of your event", which still opens with the
   visible label, so WCAG 2.5.3 (Label in Name) holds. */
body.d6 .pn-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Section headings — a <div> on desktop, an accordion <button> on the phone.
   portal-nav.js rebuilds them on a breakpoint crossing rather than trying to
   media-query an attribute. */
/* ⚠ THE HEADING IS THE STRONGEST TEXT IN THE SIDEBAR, AND IT IS UPPERCASE ON PURPOSE
   (owner brief, 2026-09-15: "make the section headings visually prominent and bold" and
   "clearly distinguish section headings from clickable navigation items").
   Full Onyx at 700 against items at Onyx-78 400 — the heading out-weighs and out-inks every
   item under it. UPPERCASE with .1em tracking rather than sentence case at a larger size,
   because the difference that separates a heading from a button has to be one a glance reads
   without comparing sizes: every item is a sentence-case word ("Guests", "Seating"), and a
   bold sentence-case "Guests" heading directly above a regular sentence-case "Guests" item
   reads as two buttons, one pressed. A tracked capital label is the register the house already
   uses for "this names a group" (the eyebrow), so it cannot be read as a place to press.
   0.78rem on the 18px root is 14px — the items' own size — and capitals at 700 set 14px
   noticeably larger than lower-case at 400. Non-interactive on the desktop: a <div>, the
   default cursor, no hover, nothing selectable. Contrast: Onyx on Alabaster 14.60:1. */
body.d6 .pn-grp {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  margin: 0;
  /* ⚠ 12/6, NOT 16/8 (2026-09-16, d6-walkfix). All fifteen items and six headings have to fit
     a 1440x900 desktop with the strip present — measured 957px of rail in an 812px box, with
     Plan & billing, Events and People below the fold. The heading keeps every mark that makes
     it a heading (700, uppercase, .1em, full Onyx, 14px — see the note above); only the air
     around it comes down, here and in the four rules below. */
  padding: 12px 14px 6px 17px;
  border: 0;
  background: transparent;
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ch8-onyx);
  text-align: left;
  cursor: default;
  user-select: none;
}
/* Air and a hairline above every section after the first: 6px + the heading's 12px = 18px
   between the last item of one section and the next heading, with the hairline between.
   Inset to the text column so the rule groups, not boxes. (8px + 16px until 2026-09-16 —
   the rail did not fit its own box; see the .pn-grp note.) */
body.d6 .pn-sec + .pn-sec::before {
  content: '';
  display: block;
  height: 1px;
  margin: 6px 14px 0;
  background: var(--ch8-onyx-20);
}
/* "The day" is its own clearly separated section (owner brief): a FULL-bleed hairline above
   AND below, and twice the air on both sides, so it reads as a band of its own directly above
   What we handle rather than a fifth list in the column. The section after it drops its own
   inset rule — the band's lower edge is already that rule. */
body.d6 .pn-sec--day {
  /* 10/6/12 rather than 16/10/18 since 2026-09-16 (d6-walkfix). The BAND is kept whole — both
     full-bleed hairlines, more air than any other section boundary has — it is simply no
     longer paying 44px of a rail that has to hold fifteen items in 812px. */
  margin: 10px 0;
  padding-bottom: 6px;
  border-top: 1px solid var(--ch8-onyx-20);
  border-bottom: 1px solid var(--ch8-onyx-20);
}
body.d6 .pn-sec--day::before,
body.d6 .pn-sec--day + .pn-sec::before { display: none; }
body.d6 .pn-sec--day > .pn-grp { padding-top: 12px; }

body.d6 .pn-i {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  /* ⚠ 34px IS THE MOUSE FLOOR ONLY (2026-09-16, d6-walkfix; was 40px). A finger never sees it:
     the `(hover:none) and (pointer:coarse)` arm below puts every item back on the 44px house
     floor, and the ≤640 phone block draws 48px. 34px x 15 items is 90 of the 145px the rail
     had to give back to fit 1440x900 with the strip present, and it is still ten above the
     24px minimum-target floor test/portal-target-size-24px measures. */
  min-height: 34px;
  /* 24px + the 3px rule: the label sits 10px in from its heading, so the hierarchy shows
     at a glance without a tree glyph. */
  padding: 0 14px 0 24px;
  border: 0;
  border-left: 3px solid transparent;
  border-radius: 0;
  background: transparent;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ch8-onyx-78);
  text-align: left;
  cursor: pointer;
}
body.d6 .pn-i .pn-l {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
body.d6 .pn-i:hover { background: var(--ch8-onyx-12); color: var(--ch8-onyx); }
/* The current page is four things at once — a 3px Bordeaux rule, a Linen fill that reads as a
   tab pulled out of the Alabaster rail, full Onyx ink and weight 600 — and aria-current="page"
   (portal-nav.js#setCurrent). Hovering it does not grey it back into the list. */
body.d6 .pn-i[aria-current="page"],
body.d6 .pn-i[aria-current="page"]:hover {
  background: var(--ch8-linen);
  border-left-color: var(--ch8-bordeaux);
  color: var(--ch8-onyx);
  font-weight: 600;
}
/* Inset, so the ring is never clipped by the sidebar's own edge or its overflow. */
body.d6 .pn-i:focus-visible { outline: 2px solid var(--ch8-crimson); outline-offset: -2px; }

/* A locked room stays focusable and says why (§35: aria-disabled, never
   `disabled`, so a keyboard client can read what is shut instead of tabbing
   past it). Dimming is done with ink, not opacity — portal.css already carries
   that ruling for `.rooms button.is-locked`. */
/* ⚠ A LOCKED ITEM KEEPS onyx-78, IT IS NOT DIMMED TO onyx-62 — THE GROUND CHANGED.
   The rail dims a locked room with `--ch8-onyx-62` (portal-rooms.js CSS) and that
   is correct THERE: the rail sits on Linen, where onyx-62 measures 4.94:1, "the
   last AA text step". The sidebar sits on Alabaster, where the same token is
   4.33:1 — under AA — so a locked area would be the one thing in the rail a
   client with low vision cannot read, which §35's ruling ("a locked view stays
   legible and says it is locked") forbids. The state is carried by `aria-disabled`,
   by the cursor, by the absence of a hover, and by the lock sentence the button
   holds as visually-hidden text. See the report's owner question: a locked area is
   now distinguishable by cursor and screen reader but not by ink alone. */
body.d6 .pn-i[aria-disabled="true"] {
  cursor: not-allowed;
  /* Recessed rather than dimmed: the ink stays at full strength and the ROW
     carries the state, so "you cannot open this yet" is visible without spending
     the contrast the sentence needs. It cannot be mistaken for the current item,
     which is the opposite move — a LIGHTER Linen fill plus a Bordeaux left rule.
     ⚠ 3%, NOT 6% (2026-09-16, d6-walkfix). At 6% the shut Files row was a solid grey band
     across the whole rail, HEAVIER than the current item's Linen tab + Bordeaux rule, so on
     the `new` fixture the locked row read as the page you were on and Home did not. The
     current item has to be the strongest row in the column; a locked one is now the faintest
     mark in it. Onyx-78 on the result (~#E0DCD7) is 7.9:1, well over AA, and the reason the
     row is shut is no longer ink's job at all — pressing it writes `.pn-why` below. */
  background: color-mix(in srgb, var(--ch8-onyx) 3%, transparent);
}
body.d6 .pn-i[aria-disabled="true"]:hover {
  background: color-mix(in srgb, var(--ch8-onyx) 3%, transparent);
  color: var(--ch8-onyx-78);
}

/* ── THE REASON A SHUT ROW IS SHUT, SAID OUT LOUD (2026-09-16, d6-walkfix) ───────────────
   Until today a locked item carried its reason ONLY as `.pn-sr` visually-hidden text: a
   screen-reader client heard "Approve your designs to unlock the complete suite" and a mouse
   client got nothing at all — pressed Files, nothing happened, no sentence, no title (the
   title is deliberately absent, see the .pn-sr note). portal-nav.js#showWhy writes this line
   into the item's own <li>, directly under the row that was pressed, and clears it on the
   next navigation. It is `aria-live="polite"`, so the same words are announced to a client
   who never sees it, and the item stays focusable and `aria-disabled` exactly as §35 asks.
   Ink is Onyx-78 (8.55:1 on Alabaster) — Onyx-62's 4.33:1 fails AA for text this size. */
body.d6 .pn-why {
  margin: 0;
  padding: 2px 14px 8px 24px;    /* +3px of rule = the label's own left edge */
  font-family: var(--sans);
  font-size: 12.5px;
  /* 400, stated: <body> sets Montserrat 300 and a 12.5px hairline weight is not a sentence a
     client reads at a glance — measured on the `review` fixture, where this line IS the only
     visible explanation the portal gives for a shut room. */
  font-weight: 400;
  line-height: 1.45;
  color: var(--ch8-onyx-78);
  border-left: 3px solid var(--ch8-onyx-20);
}
/* ⚠ `is-quiet` IS NOT `is-locked`, AND THE SIDEBAR MUST NOT SAY IT IS.
   applyEmphasis() (portal-rooms.js) puts `.is-quiet` on rooms that are merely
   not the client's next move — Guests, Messages and Files before the vision is
   in — and in the old rail it was a small weight change on a horizontal tab. In
   a vertical list beside genuinely locked items the same dimming reads as "you
   cannot open this", which is false: on the `guests` fixture Files is quiet and
   open and has four files ready to download. Dimming in this rail means locked
   and nothing else, so the class is deliberately given no colour here. */
body.d6 .pn-i.is-quiet { color: inherit; }

/* The badge: a bare tabular number, no pill (spec §2.5) — but in ONE SLOT and of TWO KINDS
   (owner brief, 2026-09-15: "keep all existing count badges and make them easier to understand").
   · The slot: right-aligned in a fixed 3.2em box at the item's right edge, tabular figures, so
     every number in the column lines up on its last digit whatever the label's length.
   · "Needs you": Crimson, weight 700, with a 6px Crimson dot before it — the same dot the Home
     queue's "What we need from you" eyebrow wears while anything is open (see the foot of this
     file), so the colour is learned from the page itself and needs no legend.
   · A plain count: Onyx-78 at 500, no dot.
   ⚠ PLAIN IS Onyx-78, NOT Onyx-62, AND THAT IS A MEASUREMENT: Onyx-62 is 4.33:1 on Alabaster,
   under AA. The two kinds stay unmistakable by hue, weight and the dot together.
   The sentence each one means ("2 guests have not replied") is visually-hidden text inside
   the button (.pn-sr, portal-nav.js#paintItem) and the bare figure is aria-hidden beside it. */
body.d6 .pn-i .pn-c {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
  min-width: 3.2em;
  margin-left: auto;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  text-align: right;
  color: var(--ch8-onyx-78);
}
body.d6 .pn-i .pn-c.is-hot { color: var(--ch8-crimson); font-weight: 700; }
body.d6 .pn-i .pn-c.is-hot::before {
  content: '';
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ch8-crimson);
}
/* The rail's ONE badge before this direction was a dot on Today, and it is kept
   as the fallback for as long as a number is not available — dropping it would
   remove the only interrupt the portal has. */
body.d6 .pn-i .pn-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ch8-crimson);
}

/* ── THE LEGACY RAIL AND TAB BAR ─────────────────────────────────────────────
   Hidden, never removed: applyTabLockStates(), paintRail(), announceRoomLock()
   and the gates all read them, and portal-rooms.js's own rule 2 is "the legacy
   rail is hidden, never removed". portal-nav.js hands the `roomBtn-*` ids over
   to the sidebar's own room items before this hides anything, so exactly one
   element carries each id and the walk clicks something a client can see. */
body.d6 #roomRail { display: none !important; }

/* ── #roomViews — the in-page sub-nav, restyled as Direction 6 chips ───────── */
body.d6 #roomViews {
  display: flex;
  flex-wrap: wrap;
  gap: 0 4px;
  padding: 0 var(--pn-gutter);
  background: var(--ch8-linen);
  border-top: 1px solid var(--ch8-onyx-12);
}
body.d6 #roomViews button {
  min-height: 44px;
  padding: 0 10px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: transparent;
  font-family: var(--sans);      /* §8.6 — the product face; see the header note */
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ch8-onyx-62);
  cursor: pointer;
}
body.d6 #roomViews button:hover { color: var(--ch8-onyx); }
body.d6 #roomViews button.on { color: var(--ch8-onyx); border-bottom-color: var(--ch8-bordeaux); }
body.d6 #roomViews button.is-locked { color: var(--ch8-onyx-62); }
body.d6 #roomViews button:focus-visible { outline: 2px solid var(--ch8-crimson); outline-offset: -3px; }
/* the phone moves the strip into the page (placeViews); it keeps the same skin */
body.d6 #roomViews.roomviews--page { border-top: 0; border-bottom: 1px solid var(--ch8-onyx-12); }
/* In the header (every width but an upright phone) the strip is inset by the sidebar's width,
   so it heads the CONTENT column and the sidebar rises past it to the status strip — see the
   note on `.pn-side`. --pn-w is 0 at ≤640, so a phone on its side keeps the full width. */
body.d6 #roomViews:not(.roomviews--page) { margin-left: var(--pn-w); }
/* ⚠ FOUR TODAY LENSES MUST FIT ONE ROW ON A 360px PHONE (2026-09-16, integration of origin/main).
   Main's day lens added a fourth chip, "The day", which appears only once the event's date is
   known — after first paint. At 360px (Galaxy S24 upright) the four chips wrapped to a second
   44px row at that moment and pushed the page down (test/portal-header-reserve-signed-in.test.mjs
   cases 2 and 6, EVD-181's no-layout-shift rule). Tighter chip padding keeps them on one row;
   the 44px target height is untouched. */
@media (max-width: 400px) {
  body.d6 #roomViews.roomviews--page { gap: 0 2px; }
  body.d6 #roomViews.roomviews--page button { padding: 0 6px; letter-spacing: .06em; }
}

/* ── TABLET · 641–1023 ───────────────────────────────────────────────────────
   Direction 6 has no tablet state at all — spec §1.4 measured 1024 as "the
   desktop layout in a narrower box", with the strip clipped mid-word. A
   narrower rail and a wrapping strip are the two fixes. */
@media (min-width: 641px) and (max-width: 1023px) {
  body.d6 { --pn-w: 200px; --pn-gutter: 16px; }
  body.d6 .pn-i { padding: 0 12px 0 20px; font-size: 13.5px; }
  /* "TALKING TO GUESTS" at the desktop's 14px/.1em needs ~186px; the 200px rail leaves 171. */
  body.d6 .pn-grp { padding: 14px 12px 8px 15px; font-size: 0.7rem; letter-spacing: .06em; }

  /* ⚠ THE SHELL MEANS VIEWPORT WIDTH IS NO LONGER CONTENT WIDTH, AND ONE RULE IN
     portal.css ASSUMED THEY WERE THE SAME. `.doc-card` is forced square
     (`aspect-ratio:1!important`, portal.css:4783) and the tile grid drops to two
     columns at ≤1024px — sized for a 768px page, which is 568px once 200px of
     rail is taken out. Measured at 768: the tile asked for a 236px square and its
     own content needed 279px, so "Open ↗" was clipped away — the exact defect
     test/portal-phone-layout-reachable case 7 was written for, arriving at a width
     that used to be safe. The tile keeps its square wherever the column can hold
     it and gives it up rather than clip a control, which is the same answer
     portal.css:6872 already gives in its own narrow-column block. */
  body.d6 .doc-card { aspect-ratio: auto !important; min-height: 0 !important; }
}

/* A tablet is a touch device that the 640px arm never sees; the floor is the
   pointer's, not the viewport's. */
@media (hover: none) and (pointer: coarse) {
  body.d6 .pn-i { min-height: 44px; }
  body.d6 .pn-ib, body.d6 .pn-find { min-height: 44px; }
}

/* ── PHONE · ≤640 — the drawer ───────────────────────────────────────────── */
@media (max-width: 640px) {
  body.d6 { --pn-w: 0px; --pn-gutter: 14px; padding-left: 0; }
  /* ⚠ THE BURGER TAKES A COLUMN OF THE §9p GRID; IT IS NEVER AUTO-PLACED.
     portal-v10.css §9p names four areas — logo · stop · bell / event · account —
     and an unnamed extra child would be auto-placed into the first empty cell,
     which is how a fifth control silently re-broke the two-row header this rule
     exists to keep. The template is restated with a leading `burger` column and
     the event band spans the whole second row, so both dropdowns still open
     inside a 320px screen. */
  body.d6 .portal-hdr .hdr-top {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "burger logo stop bell" "event event account account";
  }
  body.d6 .pn-burger { display: inline-flex; grid-area: burger; }
  body.d6 .pn-strip { gap: 2px 8px; }

  body.d6 .pn-side {
    /* The drawer opens under the WHOLE pinned header: on an upright phone #roomViews is out
       of the header already, and the header's reserve below the strip is still header. */
    top: calc(var(--hdr-h, var(--hdr-reserve)) + var(--beta-h, 0px));
    left: 0;
    right: 0;
    width: auto;
    z-index: 190;              /* over the page, under .portal-hdr so ✕ stays reachable */
    border-right: 0;
    padding: 4px 0 40px;
  }
  body.d6 .pn-side[data-open="false"] { display: none; }
  /* The Bordeaux rule is KEPT on the phone (the prototype drops it): the current page is
     unmistakable at every width, not only where there is room for a fill to read. */
  body.d6 .pn-i { min-height: 48px; font-size: 15px; padding: 0 16px 0 26px; }
  /* Accordion headings: the desktop's bold capital label, 48px tall, a hairline under it and
     the +/− at the right. Here — and only here — a heading IS a control, so it gets a hover. */
  body.d6 .pn-grp {
    min-height: 48px;
    padding: 0 16px 0 19px;
    border-bottom: 1px solid var(--ch8-onyx-20);
    cursor: pointer;
  }
  body.d6 .pn-grp:hover { background: var(--ch8-onyx-12); }
  body.d6 .pn-grp .pn-grp-mk {
    min-width: 20px;
    font-size: 20px;
    font-weight: 400;
    line-height: 1;
    text-align: center;
  }
  body.d6 .pn-grp:focus-visible { outline: 2px solid var(--ch8-crimson); outline-offset: -2px; }
  /* The heading's own hairline divides the sections, so the inset rule goes; "The day" keeps
     its band — full hairlines and air above and below. */
  body.d6 .pn-sec + .pn-sec::before { display: none; }
  body.d6 .pn-sec--day { margin: 14px 0; padding-bottom: 0; }
  body.d6 .pn-sec--day > .pn-grp { padding-top: 0; }
  body.d6 .pn-sec[data-open="false"] > ul { display: none; }
  /* body scroll lock while the drawer is up */
  body.d6.pn-locked { overflow: hidden; }

  /* ⚠ THE SEATING RAIL WRAPS ON A PHONE, IT DOES NOT SCROLL SIDEWAYS (2026-09-16, d6-walkfix).
     `.guests-steps` is `display:flex; overflow-x:auto` with `.guests-step{flex:1}`, so at 390
     the five steps were 59-95px wide: "RSVP TRACKER" broke across two lines, every `.pl-count`
     sat hard against the label it belongs to, and "ready for 2" — `white-space:nowrap` inline,
     in a 59px box — ran to the right edge of the screen and was cut. Three even columns give
     each step ~117px and the last two row-wrap under them; the same answer `.rm-chain` already
     takes inside a panel ("a step whose label is severed is not a step a client can read").
     ⚠ IT WRAPS, IT DOES NOT BECOME A GRID, AND THAT IS NOT A PREFERENCE.
     updateGuestsStepBar() (portal-guests-seating.js) writes `bar.style.display` INLINE and
     uses it as the show/hide switch too — `hasGuests ? 'flex' : 'none'` — so a
     `display:grid` here is ignored (an inline declaration wins), and a `display:grid
     !important` would win against the 'none' as well and draw the rail for a client with no
     guests at all. `flex-wrap` is not written inline, so it needs no weight and cannot reach
     the hidden state. `0 1 33.333%` — grow 0, so the second row's two steps keep the first
     row's width instead of stretching to half the rail each; box-sizing is border-box
     globally (portal.css:94), so three of them are exactly the 350px the strip has. */
  body.d6 .guests-steps {
    flex-wrap: wrap;
    overflow-x: visible;
  }
  body.d6 .guests-steps > .guests-step { flex: 0 1 33.333%; padding: 10px 8px 9px; min-width: 0; }
  /* The count is a second line under the label, so it needs the air a second line needs — and
     it must wrap rather than run out of its own column. Both are inline styles on the node
     portal-print-loop.js#_setCount creates, so both need the weight to beat them. */
  body.d6 .guests-steps .pl-count {
    white-space: normal !important;
    margin-top: 6px !important;
    line-height: 1.35;
  }
  /* The label's desktop tracking costs ~14px on "RSVP TRACKER" alone; at a phone's column
     width that is the difference between one line and two. */
  body.d6 .guests-steps .guests-step-lbl { letter-spacing: .06em; }
}

/* The house draws depth with hairlines and forbids decorative motion; the one
   transition here is a colour, and it goes when the client asks for less. */
@media (prefers-reduced-motion: reduce) {
  body.d6 .pn-i, body.d6 .pn-ib, body.d6 .pn-find, body.d6 #roomViews button { transition: none; }
}

/* ── THE DECORATIVE LAYERS STAY BEHIND THE SHELL ─────────────────────────────
   The celebration confetti, the cursor sparkle and the star burst are
   `position:fixed` over the whole viewport at z-index 99990-99998 (portal.css
   ~7726, ~7752, ~7763), which was right when the whole viewport was the page.
   With a fixed rail and a fixed bar it meant confetti falling ACROSS the
   navigation. They keep every other property — the fall, the spin, the
   pointer-events:none — and simply pass behind the two surfaces that are now
   always on screen. Nothing is disabled: over the content column, which is where
   the thing being celebrated is, they look exactly as they did. */
body.d6 .cf-piece,
body.d6 .px-spark,
body.d6 .px-star-petal { z-index: 140 !important; }

/* The footer is `position:fixed; left:0; right:0` inline (client-portal.html), so
   its centred line was centred on the viewport rather than on the page the client
   is reading, and its left end ran under the rail. */
body.d6 #portalFooter { left: var(--pn-w) !important; }

/* ⚠ AND THE PAGE RESERVES WHAT THE FOOTER ACTUALLY IS, NOT WHAT IT IS ON ONE LINE
   (2026-09-16, d6-walkfix). portal.css:463 reserves `body{padding-bottom:48px}` for a footer
   whose own markup makes it 61px (test/portal-footer-geometry derives exactly that), so 13px
   of every page was already under the opaque bar at 1440. Direction 6 made it worse rather
   than caused it: the rail takes 240px out of the footer's width too, so at 1024 the five
   legal links wrap to TWO rows and the bar measures 105px — 57px of the page, which on the
   `guests` fixture at 1024 was the queue's last row and its OPEN GUESTS button, sliced.
   A stylesheet cannot know how many rows the row wrapped to, so portal-nav.js#syncFoot
   MEASURES the settled footer into `--pn-foot` (0 where the footer is in flow, ≤700px and in
   landscape) and this reserves that plus a 10px gap. `max()` keeps the old 48px as the floor,
   so nothing loses air it had, and the var's own fallback is the same number for a tree where
   the script never runs. */
body.d6 { padding-bottom: max(48px, calc(var(--pn-foot, 48px) + 10px)); }

/* ⚠ THE HELP BUBBLE IS NOT PART OF THE DRAWER, SO IT MUST NOT PAINT OVER IT (2026-09-16,
   d6-walkfix). `.evo-t-launcher` is `position:fixed` at z-index 9300 (assets/tutorial.css) —
   far above the drawer's 190 — and it sits OUTSIDE the drawer's focus trap, so on a 390 phone
   it floated over the open navigation as a live-looking control a keyboard client could never
   reach. Hidden while the drawer is up and back the moment it closes, which is exactly the
   move the tutorial's own sheet already makes for a running tour two lines below its rule
   (`body.evo-t-running .evo-t-launcher{display:none}`) and for the same reason. */
body.d6.pn-locked .evo-t-launcher { display: none; }

/* ⚠ THE SCROLL LOCK HAS TO REACH <html>, AND ONLY <html> CAN CARRY IT (2026-09-16,
   d6-walkfix). `body.d6.pn-locked{overflow:hidden}` (the ≤640 block above) does nothing to the
   viewport here: overflow propagates from <body> only while <html>'s own overflow is
   `visible`, and portal.css:416 sets `html{overflow-x:hidden}`, so the used value on <html> is
   not visible and body's is simply body's. Measured with the drawer open at 390 — a wheel over
   the drawer scrolled the drawer (correct), and `document.scrollingElement.scrollTop = 763`
   still moved the page behind it. `<html>` is the scrollingElement, so the lock goes there.
   This is the one rule in this file that cannot be scoped to `body.d6` — no selector reaches
   an ancestor — and it does not need to be: `pn-locked` is written on <html> by
   portal-nav.js#openDrawer only, and removed by closeDrawer(), and this module is the only
   thing in the tree that uses the name. */
html.pn-locked { overflow: hidden; }

/* ── THE RAIL SAYS WHEN THERE IS MORE (2026-09-16, d6-walkfix) ───────────────────────────
   Tightening the rhythm above fits all fifteen items and six headings at 1440x900, but a
   1024x768 desktop gives the rail 661px for 797 and no honest tightening closes that. So
   where it must still scroll it says so, rather than ending in a row that simply stops:
   `data-pn-more` / `data-pn-up` are written by portal-nav.js#syncMore from the rail's own
   scroll geometry, and each draws a short fade to the rail's own Alabaster plus a hairline at
   the edge it is hiding content behind. `position:fixed` on the pseudo-elements and not
   `sticky`: `.pn-side` is the scroll container, so anything in its flow scrolls with the list;
   pinned to the viewport they stay on the edge they describe. pointer-events:none, so the
   fade can never eat a press on the row underneath. */
body.d6 .pn-side[data-pn-more="true"]::after,
body.d6 .pn-side[data-pn-up="true"]::before {
  content: '';
  position: fixed;
  left: 0;
  width: var(--pn-w);
  height: 28px;
  pointer-events: none;
  z-index: 1;
}
body.d6 .pn-side[data-pn-up="true"]::before {
  top: calc(var(--beta-h, 0px) + var(--pn-side-top, var(--hdr-h, var(--hdr-reserve))));
  border-top: 1px solid var(--ch8-onyx-20);
  background: linear-gradient(
    to bottom,
    var(--ch8-alabaster),
    color-mix(in srgb, var(--ch8-alabaster) 0%, transparent));
}
body.d6 .pn-side[data-pn-more="true"]::after {
  bottom: 0;
  border-bottom: 1px solid var(--ch8-onyx-20);
  background: linear-gradient(
    to top,
    var(--ch8-alabaster),
    color-mix(in srgb, var(--ch8-alabaster) 0%, transparent));
}
/* On a phone the rail is the full-width drawer and the page behind it is locked, so a fade
   pinned to the viewport's left 0px would be a band across the drawer. The drawer scrolls as
   a drawer; it needs no cue. */
@media (max-width: 640px) {
  body.d6 .pn-side::before, body.d6 .pn-side::after { display: none; }
}

/* ⚠ BELOW 1100px THE SUPPORT LINE IS THE ADDRESS, AND IT MUST NOT BE SHRUNK (2026-09-16,
   d6-walkfix). `flex: 0 1 auto` above lets the line give way before any control does, which is
   right while the line is prose — but at 1024 the bar had squeezed it to 107px against the
   120px "hello@evorrah.com" needs and a paying client read "hello@evorrah…", which is not a
   route to anybody. portal-init-helpers.js#applyEventLeadSupportLine now drops the prose by
   WIDTH at this same 1100px rather than by measurement, so what is left here is the address
   alone and it is allowed its own 120px; `.hdr-meta` is the flex child that gives them up. */
   ⚠ A FLOOR, NOT A PIN. `flex:0 0 auto` would have done it and is the wrong tool: this element
   holds the FULL 449px sentence until portal-init-helpers.js has a client record to fit it
   against — a second or two on a cold load, and forever in the signed-out tree the header
   guards render — and a pinned box would have put all 449px into a 1024px bar and pushed a
   control off the end. `min-width` lets it keep shrinking with the bar right up to the width
   the address itself needs and no further, which is the same answer in the settled state and a
   merely-ellipsised one in the transient. 10.5em on this element's own 12px is 126px against
   the 120px "hello@evorrah.com" measures. */
@media (min-width: 901px) and (max-width: 1100px) {
  body.d6 .hdr-support { min-width: 10.5em; }
}
/* ⚠ AND IT COMES BACK BETWEEN 701 AND 900, WHERE portal.css HID IT OUTRIGHT (2026-09-16,
   d6-walkfix). `@media(max-width:900px){.hdr-support{display:none}}` (portal.css:592) was
   written for the pre-Direction-6 header, where the line was the full prose sentence and there
   was no room for it. What is drawn there now is `hello@evorrah.com` — 120px — and Direction 6
   made the bar a flex row with space for it. Measured on the `guests` fixture at 701, 768 and
   900: `.portal-hdr` settles at 156px with the line and 156px without it, `--hdr-reserve` is
   156px at all three, and neither `.hdr-top` nor the document gains a pixel of overflow. So no
   header-reserve band moves and test/portal-header-reserve case 3a now measures a target that
   is drawn at three more widths instead of none.
   ⚠ 761 AND NOT 641, FOR TWO SEPARATE REASONS. Below 701, portal-v10.css §9p makes `.hdr-top`
   a two-row grid with four NAMED areas, and this file's own ≤640 note records what a fifth,
   unnamed child does there — auto-placed into the first empty cell, re-breaking the two-row
   header. And between 701 and 740 the flex bar has the room for neither: measured on `guests`
   (whose two chips are already at their 210px/170px caps, so this is close to the worst case),
   `.hdr-top` scrollWidth ran 713 against 701 and 722 against 720 with the line in, and came
   back level at 740 — 12px and 2px of a control pushed past the bar's own edge. 761 keeps a
   20px margin over the width where it measured level. The address is not restored below it. */
@media (min-width: 761px) and (max-width: 900px) {
  body.d6 .hdr-support { display: block; min-width: 10.5em; }
}
/* And above 1100px, for the band that rule cannot name: the width at which the SHORT prose
   stops fitting depends on the event lead's name and on the promise's own wording, so it is
   measured rather than guessed. portal-init-helpers.js#pinIfStillCut writes this class only
   when the address alone — the last tier there is — is still being ellipsised, and takes it
   off again before every re-fit. Measured at 1101 on `guests`: 120px needed, 109px given.
   The same floor, for the same reason as the band above — never a pin, so the un-fitted
   sentence can still give way while it is the thing in the box. */
body.d6 .hdr-support.hdr-support--pin { min-width: 10.5em; }

/* ── HOME · ONE COLUMN MEASURE FOR THE GRID AND THE CARDS UNDER IT (2026-09-15, d6-polish) ──
   The panel grid sat inside `.td-wrap{max-width:900px}` (portal-rooms.js's injected sheet)
   while the standing cards appended after it — Your plan, Who's handling what, Your guests —
   ran the full column, so at 1440 the grid stopped 172px short of the card beneath it.
   Direction 6 has no max-width; 1320px keeps a line readable on a wide screen. The grid and the
   cards share it, so their right edges agree at every width, and both inherit the panel's own
   gutters. Scoped to #tab-today so the other rooms' `.td-wrap` keep their 900px letter measure. */
body.d6 #tab-today > .td-wrap { max-width: 1320px; }
/* ⚠ THE CONCIERGE MOUNTS SIT IN THE SAME COLUMN AS THE PANEL THEY PRECEDE (2026-09-16, integration
   of origin/main). Main authored #conciergeWelcome, #hostBriefMount, #milestonesMount, #questionsMount,
   #eventBriefMount (Today) and #approvalHistoryMount (Design) as siblings ABOVE every .tab-panel and
   gates each to its room in portal.css. They are not inside a panel, so they took none of
   `.tab-panel.active`'s gutter and ran flush against the sidebar at every width. Same gutter as the
   active panel (portal-v10.css), so their left edges agree with the room below them. */
body.d6 #portalMain > #conciergeWelcome,
body.d6 #portalMain > #hostChangesMount,
body.d6 #portalMain > #hostBriefMount,
body.d6 #portalMain > #milestonesMount,
body.d6 #portalMain > #questionsMount,
body.d6 #portalMain > #eventBriefMount,
body.d6 #portalMain > #approvalHistoryMount {
  box-sizing: border-box;
  padding-left: var(--v10-gutter);
  padding-right: var(--v10-gutter);
}
body.d6 #tab-today > #clientPlanCard,
body.d6 #tab-today > #ownershipCard,
body.d6 #tab-today > #headcountCard {
  box-sizing: border-box;
  max-width: 1320px;
  margin-left: 0;
  margin-right: 0;
}
/* The legend-free cue: while anything needs the client, the Home queue's eyebrow wears the
   sidebar's "needs you" DOT (`.pn-c.is-hot`) and darkens its words one step.
   `td-grid--hot` is written by renderToday only when the count is above zero, so a quiet
   Home never shows the alarm cue.

   ⚠ THE COLOUR IS THE DOT, NOT THE WORDS (2026-09-18, closeout-max pkgJ). This rule read
   `color: var(--ch8-crimson)` and the contrast was never the problem — crimson on Linen is
   8.83:1. Chapter 8.5 is: "a functional colour is a filled chip carrying Linen type", with a
   text form reserved for "where a table needs one". An eyebrow is not a table row, and at
   --text-xs it renders 11.52px on the phone's 16px root — under the 12px threshold
   scripts/portal-brand-conformance.mjs holds (2 findings, "What we need from you", phone
   only, desktop's 18px root put the same declaration at 12.96px and hid it).
   Raising the size instead was not available: §8.7 gives eyebrows --text-micro, which is
   SMALLER, and calls it "the smallest type permitted". The 6px crimson dot below is already
   the fill form of this signal, so the cue survives; the words take Onyx (16.43:1), which
   still reads a step louder than a quiet panel's Onyx-62 eyebrow. */
body.d6 .td-grid--hot .td-panel--needs .td-eyebrow { color: var(--ch8-onyx); }
body.d6 .td-grid--hot .td-panel--needs .td-eyebrow::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  margin: 0 8px 0 0;
  border-radius: 50%;
  background: var(--ch8-crimson);
  vertical-align: middle;
}
