/* ============================================================================
   shell.css — the topbar + sidebar chrome worn by every page on
   gushwork-design.vercel.app.

   EVERY number here was read off Figma node properties in
   `Q9L6q38dEj3Qu1JkjiT13y` → page `Gushwork Design` → frame `overview`
   (478:14788). Nothing was eyeballed from a screenshot. Where a value could
   not be measured, the rule says so and names it a RULING, not a measurement.

   Sources:
     topbar   478:14789
     sidebar  478:14805
     content  478:14832
     dark     dashboard-component-audit.md §5 (measured off frame 236:33407)

   Load order on a page:
     <link rel="stylesheet" href="/foundation/tokens.css">
     <link rel="stylesheet" href="/shell.css">
     <script src="/shell.js" defer></script>
   ========================================================================= */

/* ---------------------------------------------------------------------------
   0. Box model

   Every measured number here — topbar 60, sidebar 240, nav row 32, search
   400x36 — is an OUTER size in Figma, padding and stroke included. Under the
   browser default (content-box) the topbar's 60 plus its 14/14 padding and
   1.5 border renders 89.5 instead. All four generated sheets already set this
   same reset for themselves, so this is consistent with them, not a change.
   ------------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

/* ---------------------------------------------------------------------------
   1. Theme alias layer

   The Brand variable collection has ONE mode ("Gushwork") — there is no
   light/dark mode in Figma. Dark is built the way the dashboard builds it:
   each layer is re-pointed at a different token in the same single-mode
   collection. Every mapping below is tagged with where it came from.
   ------------------------------------------------------------------------ */
:root,
:root[data-theme="light"] {
  --s-page-bg:       var(--gw-color-neutral-25);   /* measured: screen 478:14788 fill */
  --s-chrome-bg:     var(--gw-color-neutral-50);   /* measured: topbar + sidebar fill */
  --s-chrome-border: var(--gw-color-neutral-100);  /* measured: 1.5px stroke */

  --s-heading:       var(--gw-color-black);        /* measured: wordmark, h6 */
  --s-body:          var(--gw-color-neutral-600);  /* measured: body copy */

  --s-nav-label:     var(--gw-color-neutral-900);  /* measured: nav row label + icon */
  --s-nav-sel:       var(--gw-color-neutral-100);  /* measured: selected row fill */
  --s-nav-hover:     var(--gw-color-neutral-25);   /* measured: audit §12 row 2 */
  --s-group-label:   var(--gw-color-neutral-400);  /* measured: list label fill */

  --s-field-bg:      var(--gw-color-neutral-25);   /* measured: search field fill */
  --s-field-border:  var(--gw-color-neutral-200);  /* measured: search + toggle stroke */
  --s-placeholder:   var(--gw-color-neutral-400);  /* measured: placeholder fill */
  --s-field-icon:    var(--gw-color-neutral-600);  /* measured: MagnifyingGlass vector */

  --s-lock-bg:       var(--gw-color-neutral-100);  /* measured: lock badge fill */
  --s-lock-icon:     var(--gw-color-neutral-400);  /* measured: LockSimple vector */

  --s-thumb-bg:      var(--gw-color-black);        /* measured: active toggle cell fill */
  --s-thumb-icon:    var(--gw-color-white);        /* measured: SunDim vector */
  --s-thumb-idle:    var(--gw-color-neutral-900);  /* measured: Moon vector */

  --s-card-bg:       var(--gw-color-white);        /* measured: audit §5 card fill */
  --s-card-border:   var(--gw-color-neutral-100);  /* measured: audit §5 card border */
  --s-danger:        var(--gw-color-red-400);      /* measured: audit §5 Danger */
}

/* System dark. The artifact's theme system has FOUR blocks, not two: bare
   :root is light, this follows the OS, and the two [data-theme] blocks are
   the user's explicit override in either direction. Without this block the
   page can only be dark once JS has written an attribute, so an OS-dark user
   gets a white flash on every load and never follows a live OS change. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* measured — audit §5, read off frame 236:33407 */
    --s-chrome-bg:     var(--gw-color-neutral-900);  /* topbar / sidebar → Neutral/900 */
    --s-chrome-border: var(--gw-color-neutral-800);  /* border → Neutral/800 */
    --s-heading:       var(--gw-color-white);        /* all Vert Grotesk headings → white */
    --s-body:          var(--gw-color-neutral-300);  /* muted icon/text → Neutral/300 */
    --s-card-bg:       var(--gw-color-neutral-900);  /* card fill → Neutral/900 */
    --s-card-border:   var(--gw-color-neutral-800);  /* card border → Neutral/800 */
    --s-page-bg:       var(--gw-color-black);        /* table body row → Neutral/black */

    /* measured — audit §12 row "the selected stat in dark mode is neutral-600
       and hover should be 700", confirmed by Utsav against the dark frame */
    --s-nav-sel:       var(--gw-color-neutral-600);
    --s-nav-hover:     var(--gw-color-neutral-700);

    /* measured — audit line 358: the nav label tracks to white in dark */
    --s-nav-label:     var(--gw-color-white);

    /* measured — audit §9: dark primary button is a white fill with Neutral/900
       text, which is the same inversion the dark tab-active uses */
    --s-thumb-bg:      var(--gw-color-white);
    --s-thumb-icon:    var(--gw-color-neutral-900);
    --s-thumb-idle:    var(--gw-color-neutral-300);
    --s-danger:        var(--gw-color-red-300);      /* measured: audit §5 Danger → Red/300 */

    /* RULINGS — not measured anywhere. The dark frame has no search field and
       no lock badge, so these follow the mappings above rather than a source.
       Flagged to Utsav; replace with measured values when the dark screen
       grows a search field. */
    --s-field-bg:      var(--gw-color-black);
    --s-field-border:  var(--gw-color-neutral-800);
    --s-lock-bg:       var(--gw-color-neutral-800);
    --s-group-label:   var(--gw-color-neutral-400);  /* unchanged; legible on /900 */
    --s-placeholder:   var(--gw-color-neutral-400);
    --s-field-icon:    var(--gw-color-neutral-300);
  }
}

:root[data-theme="dark"] {
  /* measured — audit §5, read off frame 236:33407 */
  --s-chrome-bg:     var(--gw-color-neutral-900);  /* topbar / sidebar → Neutral/900 */
  --s-chrome-border: var(--gw-color-neutral-800);  /* border → Neutral/800 */
  --s-heading:       var(--gw-color-white);        /* all Vert Grotesk headings → white */
  --s-body:          var(--gw-color-neutral-300);  /* muted icon/text → Neutral/300 */
  --s-card-bg:       var(--gw-color-neutral-900);  /* card fill → Neutral/900 */
  --s-card-border:   var(--gw-color-neutral-800);  /* card border → Neutral/800 */
  --s-page-bg:       var(--gw-color-black);        /* table body row → Neutral/black */

  /* measured — audit §12 row "the selected stat in dark mode is neutral-600
     and hover should be 700", confirmed by Utsav against the dark frame */
  --s-nav-sel:       var(--gw-color-neutral-600);
  --s-nav-hover:     var(--gw-color-neutral-700);

  /* measured — audit line 358: the nav label tracks to white in dark */
  --s-nav-label:     var(--gw-color-white);

  /* measured — audit §9: dark primary button is a white fill with Neutral/900
     text, which is the same inversion the dark tab-active uses */
  --s-thumb-bg:      var(--gw-color-white);
  --s-thumb-icon:    var(--gw-color-neutral-900);
  --s-thumb-idle:    var(--gw-color-neutral-300);
  --s-danger:        var(--gw-color-red-300);      /* measured: audit §5 Danger → Red/300 */

  /* RULINGS — not measured anywhere. The dark frame has no search field and
     no lock badge, so these follow the mappings above rather than a source.
     Flagged to Utsav; replace with measured values when the dark screen
     grows a search field. */
  --s-field-bg:      var(--gw-color-black);
  --s-field-border:  var(--gw-color-neutral-800);
  --s-lock-bg:       var(--gw-color-neutral-800);
  --s-group-label:   var(--gw-color-neutral-400);  /* unchanged; legible on /900 */
  --s-placeholder:   var(--gw-color-neutral-400);
  --s-field-icon:    var(--gw-color-neutral-300);
}

/* ---------------------------------------------------------------------------
   1b. THE SITE TYPE SCALE — the rule every page on this site follows

   Five roles, and nothing between them. A page that needs a size not on this
   list is either inventing a role or using the wrong one.

     page title       600 44px/1.2   Vert Grotesk Display   .sg__title / h1
     section heading  600 28px/1.2   Vert Grotesk Display   .sg-sec > h2
     sub-heading      --gw-text-body-18-sem   Inter         h3, entry titles
     body and lede    --gw-text-body-18-med   Inter         paragraphs
     meta, small print --gw-text-body-14-med  Inter         dates, notes

   Chips, pills, rail links and micro-labels sit outside it — they are controls
   and labels, not reading matter, and stay on the 12/14 button tokens.

   WHY THIS IS WRITTEN DOWN. The changelog sheet drifted off it and nobody
   noticed for weeks, because each page defines its own type and the sheets are
   usually read one at a time. It shipped its body at 14/reg and its lede at
   16/reg while the styleguide and the install page read at 18/med — two steps
   smaller and a weight lighter for the text people actually read. Only the
   title and the meta line matched, which is exactly why it looked almost right.

   Pages set these from the tokens rather than inheriting them, because each
   generated sheet has to stand up on its own when opened straight out of
   preview/ with no shell. So this block is the contract, not the mechanism:
   change it here, then change it in the pages that implement it —
   web/style-guide.html, preview/install.html, scripts/_changelog_sheet.py.
   ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
   2. Page frame
   ------------------------------------------------------------------------ */
.gw-shell-ready body { background: var(--s-page-bg); }

/* The generated sheets set their own body background. Only pages that opt in
   with data-gw-page get the shell's canvas colour. */
.gw-shell-ready body[data-gw-page] {
  margin: 0;
  background: var(--s-page-bg);
  color: var(--s-body);
  font: var(--gw-text-body-18-med);
  letter-spacing: var(--gw-text-body-18-med-tracking);
  -webkit-font-smoothing: antialiased;
}

/* shell.js moves the page's original children in here. */
.gw-main {
  margin-top: 60px;    /* measured: topbar height */
  margin-left: 240px;  /* measured: sidebar width */
  min-height: calc(100vh - 60px);
}

/* Content column — measured 478:14832: 1200 wide, pad 60/40, gap 60.
   1200 − 80 = 1120 inner, which is what the hero and sections are. */
.gw-col {
  max-width: 1200px;
  padding: var(--gw-space-60) var(--gw-space-40);
  display: flex;
  flex-direction: column;
  gap: var(--gw-space-60);
}
/* Body sections sit at pad 0/40 inside the 1120 column, and their text runs
   to 820. Measured off 478:14832 → Frame 2147260245 / 2147260246. */
.gw-section { padding: 0 var(--gw-space-40); }
.gw-prose   { max-width: 820px; }
.gw-prose p {
  margin: 0;
  font: var(--gw-text-body-18-med);
  letter-spacing: var(--gw-text-body-18-med-tracking);
  color: var(--s-body);
}
/* No margin — the gap comes from the .gw-stack-* flex container, which is how
   Figma spaces it (Frame 2147260246, gap 20). Carrying both would double it. */
.gw-prose h2 {
  margin: 0;
  font: var(--gw-text-h6);              /* measured: Vert Grotesk Semibold 26/120% */
  letter-spacing: var(--gw-text-h6-tracking);
  color: var(--s-heading);
}
.gw-stack-20 { display: flex; flex-direction: column; gap: var(--gw-space-20); }
.gw-stack-40 { display: flex; flex-direction: column; gap: var(--gw-space-40); }

/* ---------------------------------------------------------------------------
   3. Topbar — measured 478:14789
      1440x60 · AL HORIZONTAL · pad 14/40 · SPACE_BETWEEN|CENTER
      fill Neutral/50 · strokeBottomWeight 1.5 Neutral/100
   ------------------------------------------------------------------------ */
.gw-topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px var(--gw-space-40);
  background: var(--s-chrome-bg);
  border-bottom: 1.5px solid var(--s-chrome-border);
}

/* dashboard-title 478:14790 — HUG, gap 8 */
.gw-brand {
  display: flex;
  align-items: center;
  gap: var(--gw-space-8);
  text-decoration: none;
}
/* logo chip 478:14791 — 32x32, pad 8, r8, fill Primary/500-main */
.gw-brand__chip {
  width: 32px; height: 32px;
  flex: none;
  display: grid; place-items: center;
  border-radius: var(--gw-radius-8);
  background: var(--gw-color-primary-500);
}
.gw-brand__chip svg { width: 16px; height: 16px; display: block; }
/* wordmark 478:14792 — Vert Grotesk Display Semibold 20/100%, ls 0, black.
   No text style is bound in Figma, so this is spelled out rather than
   pointed at a --gw-text-* token. */
.gw-brand__name {
  font: 600 20px/1 var(--gw-font-display);
  letter-spacing: 0;
  color: var(--s-heading);
}

/* Frame 2147260244 — HUG, gap 16, MAX|CENTER */
.gw-topbar__right { display: flex; align-items: center; gap: var(--gw-space-16); }

/* search 478:14794 — 400x36, pad 8/12, gap 8, r12,
   fill Neutral/25, 1px Neutral/200 */
.gw-search {
  width: 400px; height: 36px;
  display: flex; align-items: center;
  gap: var(--gw-space-8);
  padding: var(--gw-space-8) var(--gw-space-12);
  border-radius: var(--gw-radius-12);
  background: var(--s-field-bg);
  border: 1px solid var(--s-field-border);
}
.gw-search svg { width: 16px; height: 16px; flex: none; color: var(--s-field-icon); }
.gw-search input {
  flex: 1; min-width: 0;
  border: 0; background: none; padding: 0;
  font: var(--gw-text-button-14);          /* measured: Button/button-14-med */
  letter-spacing: 0;
  color: var(--s-nav-label);
}
.gw-search input::placeholder { color: var(--s-placeholder); }
.gw-search input:focus { outline: none; }
.gw-search:focus-within { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }
/* The field opens the ⌘K palette rather than accepting typing, so it behaves as a button:
   pointer stays a text caret because that is what the shape promises, and the input inside
   ignores the pointer so every click lands on the field itself. */
.gw-search { cursor: text; }
.gw-search input { pointer-events: none; }

/* The shortcut, shown rather than hidden in a title attribute — a shortcut nobody can see is
   a shortcut nobody uses. Filled by shell.js so it reads ⌘K on a Mac and Ctrl K elsewhere. */
.gw-search__key {
  flex: none; display: inline-flex; align-items: center; gap: 1px;
  padding: 0 var(--gw-space-4); height: 20px;
  border: 1px solid var(--s-field-border); border-radius: var(--gw-radius-4);
  background: var(--s-lock-bg);
  font: var(--gw-text-body-12-med); letter-spacing: 0;
  color: var(--s-field-icon); white-space: nowrap;
}

/* theme toggle 478:14798 — controls/tab 66x36, pad 4, gap 2, r12,
   1px Neutral/200, no fill. Cells 28x28, pad 8, r8. */
.gw-theme {
  display: flex; align-items: center;
  gap: var(--gw-space-2);
  padding: var(--gw-space-4);
  border-radius: var(--gw-radius-12);
  /* The control has no declared size — 4 + 28 + 2 + 28 + 4 hugs to exactly
     the measured 66x36. Figma centre-aligns strokes, so its 1px does not grow
     the frame; a CSS border would, giving 68x38. An inset shadow draws the
     same line without touching layout. */
  box-shadow: inset 0 0 0 1px var(--s-field-border);
  border: 0;
  background: none;
}
.gw-theme__btn {
  width: 28px; height: 28px;
  flex: none;
  display: grid; place-items: center;
  padding: var(--gw-space-8);
  border: 0;
  border-radius: var(--gw-radius-8);
  background: none;
  color: var(--s-thumb-idle);
  cursor: pointer;
  transition: background var(--gw-motion-fast), color var(--gw-motion-fast);
}
.gw-theme__btn svg { width: 12px; height: 12px; display: block; }
.gw-theme__btn[aria-pressed="true"] {
  background: var(--s-thumb-bg);
  color: var(--s-thumb-icon);
}
.gw-theme__btn:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }

/* ---------------------------------------------------------------------------
   4. Sidebar — measured 478:14805
      240 wide · fill Neutral/50 · strokeRightWeight 1.5 Neutral/100
      SPACE_BETWEEN over a top block and an end block — the same pattern the
      dashboard uses to keep admin and the user card at the bottom.
   ------------------------------------------------------------------------ */
.gw-sidebar {
  position: fixed;
  top: 60px; left: 0; bottom: 0;
  z-index: 30;
  width: 240px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--s-chrome-bg);
  border-right: 1.5px solid var(--s-chrome-border);
  overflow-y: auto;
}

/* list-groups 478:14808 — measured pad 24/20, gap 24.
   The vertical padding is 40, not the measured 24: Utsav's call, so the space
   above GETTING STARTED and the space below the last admin row match. */
.gw-navgroups {
  display: flex;
  flex-direction: column;
  gap: var(--gw-space-24);
  padding: var(--gw-space-40) var(--gw-space-20) var(--gw-space-24);
}
/* list-group 478:14809 — vertical, gap 0 */
.gw-navgroup { display: flex; flex-direction: column; gap: 0; }

/* group label 478:14810 — 200x23, pad 4/8, Inter Semi Bold 10/15px, ls 0,
   Neutral/400, textCase UPPER. Matches Body/body-10-sem exactly. */
.gw-navlabel {
  padding: var(--gw-space-4) var(--gw-space-8);
  font: var(--gw-text-body-10-sem);
  letter-spacing: var(--gw-text-body-10-sem-tracking);
  text-transform: uppercase;
  color: var(--s-group-label);
}

/* nav row — 200x32, pad 8, gap 8, r8.
   Icon and label are both Neutral/900, so the icon inherits the row colour
   rather than carrying its own (audit line 357). */
.gw-navitem {
  display: flex;
  align-items: center;
  gap: var(--gw-space-8);
  height: 32px;
  padding: var(--gw-space-8);
  border-radius: var(--gw-radius-8);
  text-decoration: none;
  color: var(--s-nav-label);
  font: var(--gw-text-button-14);
  letter-spacing: 0;
  background: none;
  border: 0;
  width: 100%;
  cursor: pointer;
  transition: background var(--gw-motion-fast);
}
.gw-navitem svg { width: 16px; height: 16px; flex: none; display: block; }
/* line-height MUST be re-set here. --gw-text-button-14 is `500 14px/1`, which
   gives a 14px font a 14px line box — and `overflow:hidden` (needed for the
   ellipsis) then shears the descenders off g, y and p. Figma's 100% line
   height has the same number but lets glyphs paint outside the box, so the
   design does not show the problem. 20px is the DS line height for 14px Inter
   (--gw-text-body-14-*). The row stays 32 tall: it is a fixed height with
   align-items:center, so a taller line box only eats into the 8px padding. */
.gw-navitem__text {
  flex: 1; min-width: 0; text-align: left;
  line-height: 20px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-navitem:hover { background: var(--s-nav-hover); }
.gw-navitem[aria-current="page"] { background: var(--s-nav-sel); }
.gw-navitem:focus-visible { outline: var(--gw-focus-ring); outline-offset: -2px; }

/* lock badge 478:14814 — 20x20, pad 4, r4, fill Neutral/100,
   LockSimple Bold 12 in Neutral/400 */
.gw-lock {
  width: 20px; height: 20px;
  flex: none;
  display: grid; place-items: center;
  border-radius: var(--gw-radius-4);
  background: var(--s-lock-bg);
  color: var(--s-lock-icon);
}
.gw-lock svg { width: 12px; height: 12px; display: block; }

/* End block — admin group + user card, pinned to the bottom by the
   sidebar's space-between. Footer padding 20/20/32 is the dashboard's
   measured footer (audit line 349). */
.gw-navend { display: flex; flex-direction: column; }
/* 40 below the last admin row, matching the 40 above GETTING STARTED so the
   rail is symmetrically inset top and bottom. */
.gw-navend .gw-navgroups {
  padding-top: var(--gw-space-24);
  padding-bottom: var(--gw-space-40);
}

.gw-user {
  display: flex;
  align-items: center;
  gap: var(--gw-space-8);
  padding: var(--gw-space-20) var(--gw-space-20) var(--gw-space-32);
  border-top: 1.5px solid var(--s-chrome-border);
}
.gw-user__av {
  width: 32px; height: 32px; flex: none;
  border-radius: var(--gw-radius-full);
  background: var(--gw-color-primary-500);
  color: var(--gw-color-white);
  display: grid; place-items: center;
  font: var(--gw-text-button-12);
  overflow: hidden;
}
.gw-user__av img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gw-user__txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* name Button/button-12-med, role Button/button-10-med Neutral/400 — the
   dashboard's measured user card (audit §4.15) */
/* Same descender clipping as the nav label — see the note there. */
.gw-user__name {
  font: var(--gw-text-button-12);
  line-height: 16px;
  color: var(--s-nav-label);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-user__role {
  font: var(--gw-text-button-10);
  line-height: 15px;
  color: var(--s-group-label);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-user__out {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  border: 0; border-radius: var(--gw-radius-8);
  background: none; color: var(--s-body); cursor: pointer;
  transition: background var(--gw-motion-fast);
}
.gw-user__out svg { width: 16px; height: 16px; display: block; }
.gw-user__out:hover { background: var(--s-nav-hover); }
.gw-user__out:focus-visible { outline: var(--gw-focus-ring); outline-offset: -2px; }

/* Signed-out footer: a plain sign-in row in the same slot as the user card. */
.gw-signin {
  display: flex; align-items: center; justify-content: center;
  gap: var(--gw-space-8);
  margin: var(--gw-space-20);
  height: 36px;
  border-radius: var(--gw-radius-10);
  border: 1px solid var(--s-field-border);
  background: var(--s-card-bg);
  color: var(--s-nav-label);
  font: var(--gw-text-button-14);
  cursor: pointer;
  transition: background var(--gw-motion-fast);
}
.gw-signin:hover { background: var(--s-nav-hover); }
.gw-signin svg { width: 16px; height: 16px; display: block; }
.gw-signin:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }

/* ---------------------------------------------------------------------------
   5. Hero — measured 478:14833 `the-playbook-cover/default`
      1120x200 · pad 40 · gap 20 · r20 · clip · fill Primary/500-main
      · 2px Primary/600 stroke
      Grid layer: 224 cells of 40x40, each a 1px Primary/600 stroke.
      Drawn as a repeating-linear-gradient rather than 224 nodes.
   ------------------------------------------------------------------------ */
.gw-hero {
  position: relative;
  overflow: hidden;
  /* FIXED 200 in Figma, not a minimum. The measured content stacks to 188.8
     (40 + 60 + 20 + 28.8 + 40), so it fits — and holding the height is what
     clips the third filled grid cell at y=200, exactly as the design does. */
  box-sizing: border-box;
  height: 200px;
  padding: var(--gw-space-40);
  display: flex;
  flex-direction: column;
  justify-content: center;   /* measured: primaryAxisAlignItems CENTER */
  gap: var(--gw-space-20);
  border-radius: var(--gw-radius-20);
  background: var(--gw-color-primary-500);
  border: 2px solid var(--gw-color-primary-600);
}
.gw-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, var(--gw-color-primary-600) 1px, transparent 1px),
    linear-gradient(to bottom, var(--gw-color-primary-600) 1px, transparent 1px);
  background-size: 40px 40px;
  pointer-events: none;
}
/* The two filled cells in the measured grid (Rectangle 42844, 43908) — the
   design has exactly two, offset from each other. */
.gw-hero__cell {
  position: absolute;
  width: 40px; height: 40px;
  background: var(--gw-color-primary-600);
  pointer-events: none;
}
/* Lift the text above the ::before grid. Must exclude the cells — they are
   absolutely positioned, and a blanket `.gw-hero > *` turns them back into
   flex items that push the title down the column. */
.gw-hero > :not(.gw-hero__cell) { position: relative; }
/* title — Vert Grotesk Display Semibold 60/100%, ls 0, white. No text style
   bound in Figma; --gw-text-h1 is 700/1.2 so it does NOT match. */
.gw-hero__title {
  margin: 0;
  font: 600 60px/1 var(--gw-font-display);
  letter-spacing: 0;
  color: var(--gw-color-white);
}
/* meta — Body/body-18-med, Neutral/Alpha/60-white */
.gw-hero__meta {
  margin: 0;
  font: var(--gw-text-body-18-med);
  letter-spacing: var(--gw-text-body-18-med-tracking);
  color: var(--gw-color-neutral-alpha-60-white);
}

/* ---------------------------------------------------------------------------
   6. Login modal
      No design exists in Figma yet — Utsav is sending one. Built from tokens
      so restyling it is a matter of swapping values, not rebuilding markup.
   ------------------------------------------------------------------------ */
.gw-modal[hidden] { display: none; }
.gw-modal {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center;
  padding: var(--gw-space-20);
  background: var(--gw-color-neutral-alpha-50-black);
}
.gw-modal__box {
  width: 100%; max-width: 420px;
  padding: var(--gw-space-32);
  border-radius: var(--gw-radius-16);
  background: var(--s-card-bg);
  border: 1px solid var(--s-card-border);
  box-shadow: var(--gw-shadow-s4);
  text-align: center;
  position: relative;
}
.gw-modal__x {
  position: absolute; top: var(--gw-space-12); right: var(--gw-space-12);
  width: 28px; height: 28px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--gw-radius-8);
  background: none; color: var(--s-body); cursor: pointer;
}
.gw-modal__x svg { width: 14px; height: 14px; display: block; }
.gw-modal__x:hover { background: var(--s-nav-hover); }
.gw-modal__chip {
  width: 48px; height: 48px; margin: 0 auto var(--gw-space-20);
  display: grid; place-items: center;
  border-radius: var(--gw-radius-12);
  background: var(--gw-color-primary-500);
}
.gw-modal__chip svg { width: 24px; height: 24px; display: block; }
.gw-modal__title {
  margin: 0 0 var(--gw-space-8);
  font: var(--gw-text-h7);
  letter-spacing: var(--gw-text-h7-tracking);
  color: var(--s-heading);
}
.gw-modal__sub {
  margin: 0 0 var(--gw-space-24);
  font: var(--gw-text-body-14-reg);
  letter-spacing: var(--gw-text-body-14-reg-tracking);
  color: var(--s-body);
}
.gw-modal__btn {
  width: 100%; height: 44px;
  display: flex; align-items: center; justify-content: center;
  gap: var(--gw-space-12);
  border-radius: var(--gw-radius-10);
  border: 1px solid var(--s-field-border);
  background: var(--gw-color-white);
  color: var(--gw-color-black);
  font: var(--gw-text-button-14);
  cursor: pointer;
  text-decoration: none;
  transition: box-shadow var(--gw-motion-fast);
}
.gw-modal__btn:hover { box-shadow: var(--gw-shadow-s2); }
/* Primary treatment for the password submit — the Google button has to stay
   white with its own mark, so the two cannot share one look. */
.gw-modal__btn--primary {
  background: var(--gw-color-primary-500);
  border-color: var(--gw-color-primary-600);
  color: var(--gw-color-white);
}
.gw-modal__btn--primary:hover { background: var(--gw-color-primary-600); }
.gw-modal__btn--primary:disabled { opacity: .6; cursor: progress; }

.gw-modal__or {
  display: flex; align-items: center; gap: var(--gw-space-12);
  margin: var(--gw-space-16) 0;
  font: var(--gw-text-body-12-reg); color: var(--s-group-label);
}
.gw-modal__or::before, .gw-modal__or::after {
  content: ""; flex: 1; height: 1px; background: var(--s-card-border);
}

.gw-modal__form { display: flex; flex-direction: column; gap: var(--gw-space-8); text-align: left; }
.gw-modal__label {
  font: var(--gw-text-body-12-sem);
  letter-spacing: var(--gw-text-body-12-sem-tracking);
  color: var(--s-nav-label);
}
/* Same field geometry as the topbar search: 36 tall, r12, 1px border. */
.gw-modal__input {
  height: 40px;
  padding: var(--gw-space-8) var(--gw-space-12);
  border-radius: var(--gw-radius-12);
  border: 1px solid var(--s-field-border);
  background: var(--s-field-bg);
  color: var(--s-nav-label);
  font: var(--gw-text-body-14-reg);
}
.gw-modal__input::placeholder { color: var(--s-placeholder); }
.gw-modal__input:focus-visible {
  outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset);
}
.gw-modal__err {
  margin: 0;
  font: var(--gw-text-body-12-reg);
  color: var(--s-danger);
}
.gw-modal__err[hidden] { display: none; }
.gw-modal__form .gw-modal__btn { margin-top: var(--gw-space-4); }
.gw-modal__btn:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }
.gw-modal__btn svg { width: 18px; height: 18px; display: block; }
.gw-modal__note {
  margin: var(--gw-space-16) 0 0;
  font: var(--gw-text-body-12-reg);
  color: var(--s-group-label);
}

/* ---------------------------------------------------------------------------
   6b. "On this page" rail

   Measured off the styleguide TOC (478:15070 → Frame 2147260252): 260 wide,
   stack gap 16; a 22x22 r8 caret button filled Neutral/100 with a 1px
   Neutral/200 stroke, a 16px List glyph, the label at Button/button-14-med
   Neutral/900; links indented 32, gap 16, Button/button-14-med Neutral/700,
   active Primary/500-main.

   NOTE: the changelog carries an identical copy of these rules inside its own
   generated <style>, because that sheet has to stand up on its own when it is
   opened straight out of preview/ with no shell. Both were written from the
   same measurement — if you change one, change scripts/_changelog_sheet.py too.
   ------------------------------------------------------------------------ */
.idx {
  position: sticky;
  top: calc(60px + var(--gw-space-40));
  align-self: start;
  max-height: calc(100vh - 60px - 80px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  display: flex; flex-direction: column;
  gap: var(--gw-space-16);
}
.idx__t {
  display: flex; align-items: center;
  gap: var(--gw-space-8);
  font: var(--gw-text-button-14);
  line-height: 20px;
  color: var(--s-nav-label);
  padding-bottom: var(--gw-space-8);
  position: sticky; top: 0; z-index: 1;
  background: var(--s-page-bg);
}
.idx__ico { width: 16px; height: 16px; flex: none; display: block; }
.idx__col {
  width: 22px; height: 22px; flex: none;
  display: grid; place-items: center;
  padding: var(--gw-space-4);
  border-radius: var(--gw-radius-8);
  background: var(--s-lock-bg);
  border: 1px solid var(--s-field-border);
  color: var(--s-body);
  cursor: pointer;
}
.idx__col svg { width: 12px; height: 12px; display: block; transition: transform var(--gw-motion-fast); }
.idx__col:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }
.idx__list {
  display: flex; flex-direction: column;
  gap: var(--gw-space-16);
  padding-left: var(--gw-space-32);
}
.idx a {
  font: var(--gw-text-button-14);
  line-height: 20px;
  font-variant-numeric: tabular-nums;
  color: var(--gw-color-neutral-700);
  text-decoration: none;
}
:root[data-theme="dark"] .idx a { color: var(--s-body); }
.idx a:hover { color: var(--s-nav-label); }
.idx a:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }
/* Active entry is marked with colour alone — no tint, no weight change. */
.idx a.now { color: var(--gw-color-primary-500); }
.idx[data-collapsed="true"] .idx__list { display: none; }
.idx[data-collapsed="true"] .idx__col svg { transform: rotate(90deg); }

/* ---------------------------------------------------------------------------
   Collapsed TOC — measured against frame 478:15652, which is this same page
   with the rail collapsed. It is not just "hide the links":

                     expanded (478:15070)   collapsed (478:15652)
     main column     800                    1012
     divider         800                    1012
     rail            260                    48
     caret pill      22x22, caret only      48x26, caret AND the List glyph
     label + links   visible                hidden

   The column rule is `minmax(0,1fr) auto`, so the RAIL'S OWN WIDTH drives the
   split and both measured states fall out of one declaration:
     1120 inner - 60 gutter - 260 = 800    1120 - 60 - 48 = 1012
   ------------------------------------------------------------------------ */
.idx{width:260px}
.idx[data-collapsed="true"]{width:48px}

/* Collapsed, the pill IS the header row: the List glyph moves inside it and
   the button loses its own chrome, so the two read as one control. */
.idx[data-collapsed="true"] .idx__t{
  width:48px;height:26px;padding:var(--gw-space-4);
  /* Figma measures this gap at 10, but the audit (§6) lists gap:10 as a
     Figma default leftover rather than a decision, and there is no 10 in
     the spacing scale. Snapped to the token either side. */
  gap:var(--gw-space-8);justify-content:center;
  border-radius:var(--gw-radius-8);background:var(--s-lock-bg);
  padding-bottom:var(--gw-space-4)}
.idx[data-collapsed="true"] .idx__col{
  width:12px;height:12px;padding:0;background:none;border:0}
.idx[data-collapsed="true"] .idx__t > span{display:none}


/* ---------------------------------------------------------------------------
   6c. Search — the ⌘K palette

   The topbar field is the affordance; it does not accept typing. Focusing or
   clicking it opens this, which is where the typing happens. One input to
   manage, one place results can be, and the field keeps its measured 400x36
   without having to grow a results list underneath it.

   Every result is an anchor a reader can land on — a heading, or a release on
   the changelog. Built from the deploy at publish time: scripts/_search_index.py.
   ------------------------------------------------------------------------ */
.gw-pal { position: fixed; inset: 0; z-index: 90; display: none; }
.gw-pal[data-open="true"] { display: block; }
.gw-pal__scrim { position: absolute; inset: 0; background: rgba(13,13,13,.44); }

.gw-pal__box {
  position: relative; margin: 96px auto 0;
  /* 100% of .gw-pal (fixed, inset 0) = the content box, gutter excluded. */
  width: min(640px, calc(100% - 32px));
  max-height: calc(100vh - 160px); display: flex; flex-direction: column;
  background: var(--s-card-bg); border: 1px solid var(--s-card-border);
  border-radius: var(--gw-radius-16); box-shadow: var(--gw-shadow-s3);
  overflow: hidden;
}
.gw-pal__top {
  display: flex; align-items: center; gap: var(--gw-space-12);
  padding: var(--gw-space-16) var(--gw-space-20);
  border-bottom: 1px solid var(--s-card-border); flex: none;
}
.gw-pal__top svg { width: 20px; height: 20px; flex: none; color: var(--s-field-icon); }
.gw-pal__top input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none; padding: 0;
  font: var(--gw-text-body-18-med); letter-spacing: var(--gw-text-body-18-med-tracking);
  color: var(--s-heading);
}
.gw-pal__top input::placeholder { color: var(--s-placeholder); }
/* Desktop closes with esc, and the footer legend says so — so this is phone only. */
.gw-pal__x {
  display: none;
  width: 36px; height: 36px; flex: none; place-items: center;
  border: 0; background: none; padding: 0; border-radius: var(--gw-radius-8);
  color: var(--s-field-icon); cursor: pointer;
}
.gw-pal__x svg { width: 16px; height: 16px; display: block; }
.gw-pal__x:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }

.gw-pal__list { overflow-y: auto; padding: var(--gw-space-8); flex: 1 1 auto; }
.gw-pal__row {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  cursor: pointer; padding: var(--gw-space-12); border-radius: var(--gw-radius-12);
  display: grid; grid-template-columns: 20px minmax(0,1fr); gap: var(--gw-space-12);
  align-items: start; text-decoration: none;
}
/* Selection is a surface, not a colour change — the row still has to read as text. */
.gw-pal__row[aria-selected="true"] { background: var(--s-nav-hover); }
.gw-pal__hash {
  font: var(--gw-text-body-18-med); color: var(--s-placeholder);
  line-height: 24px; text-align: center;
}
/* BLOCK, both of them. As inline spans they ran onto one line — the breadcrumb butted
   straight against the heading — and `text-overflow: ellipsis` silently does nothing on an
   inline box, so neither truncated either. */
.gw-pal__t {
  display: block;
  font: var(--gw-text-body-18-sem); letter-spacing: var(--gw-text-body-18-sem-tracking);
  color: var(--s-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-pal__crumb {
  display: block;
  margin-top: 2px; font: var(--gw-text-body-14-med);
  letter-spacing: var(--gw-text-body-14-med-tracking); color: var(--s-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-pal__row mark { background: none; color: var(--gw-color-primary-500); font-weight: 600; }

.gw-pal__empty {
  padding: var(--gw-space-32) var(--gw-space-20); text-align: center;
  font: var(--gw-text-body-18-med); color: var(--s-body);
}
.gw-pal__foot {
  display: flex; align-items: center; gap: var(--gw-space-20); flex: none;
  padding: var(--gw-space-12) var(--gw-space-20);
  border-top: 1px solid var(--s-card-border); background: var(--s-chrome-bg);
  font: var(--gw-text-body-14-med); color: var(--s-body);
}
.gw-pal__foot b {
  display: inline-block; min-width: 20px; padding: 0 var(--gw-space-4);
  margin-right: var(--gw-space-8); text-align: center;
  border: 1px solid var(--s-field-border); border-radius: var(--gw-radius-4);
  background: var(--s-lock-bg); font-weight: 500; color: var(--s-heading);
}

@media (max-width: 767px) {
  .gw-pal__box { margin-top: var(--gw-space-40); max-height: calc(100vh - 80px); }
  /* The footer is a keyboard legend, and a phone has no arrow keys, no return and no esc to
     legend. Keeping it would spend a fifth of a 812-tall screen telling a thumb about keys it
     does not have — and push results below the fold to do it. Tapping a result and tapping
     outside already do what it describes. */
  .gw-pal__foot { display: none; }
  /* ...which leaves the scrim as the only way out, and with a full result list that is a
     40px strip top and bottom. An explicit, labelled control instead. */
  .gw-pal__x { display: grid; }
}

/* ---------------------------------------------------------------------------
   6b. Login page — measured 488:21730 `login`

   The modal above is the in-place affordance: you are on a page, you click a
   locked row, it opens over the top. This is the other door — where the
   middleware sends you when you asked for a gated URL and have no session, so
   there is no page underneath to overlay. Both start the same Google flow.
   ------------------------------------------------------------------------ */
.gw-login {
  position: relative;
  overflow: hidden;
  /* Fills the content region exactly: .gw-main already offsets the 60 topbar
     and the 240 sidebar, so this is the drawn 1200x828 at 1440x888. */
  min-height: calc(100vh - 60px);
  display: grid;
  place-items: center;
  padding: var(--gw-space-60) var(--gw-space-40);
}

/* The brand grid, from the same two-gradient recipe as .gw-hero — one source
   for the pattern. This is the neutral colourway, so instead of a second theme
   block the lines take --s-chrome-border and the cells --s-chrome-bg: both
   already step correctly in BOTH dark paths, the attribute and OS-dark. */
.gw-login__grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--s-chrome-border) 1px, transparent 1px),
    linear-gradient(to bottom, var(--s-chrome-border) 1px, transparent 1px);
  background-size: 40px 40px;
}
/* 30% in dark. Ruled by Utsav 3 Sep 2026: the same neutral grid reads heavier
   against black than it does against Neutral/25, so it is stepped back rather
   than re-toned — the opacity carries the filled cells with it, which keeps the
   pattern one piece. Two selectors because the theme has two dark paths: the
   explicit attribute, and OS-dark with no attribute set. */
:root[data-theme="dark"] .gw-login__grid { opacity: .3; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .gw-login__grid { opacity: .3; }
}

/* A few filled cells break the regularity, the same job they do on the hero.
   Placed to suit this frame rather than measured at these coordinates. */
.gw-login__cell {
  position: absolute;
  width: 40px;
  height: 40px;
  background: var(--s-chrome-bg);
}

/* Card 480 wide, pad 40/32, radius 24, S4. Measured off 621:5092. */
.gw-login__card {
  position: relative;   /* lifts it off the grid */
  width: 480px;
  max-width: 100%;
  padding: var(--gw-space-40) var(--gw-space-32);
  background: var(--s-card-bg);
  border: 1px solid var(--s-card-border);
  border-radius: var(--gw-radius-24);
  box-shadow: var(--gw-shadow-s4);
  display: flex;
  flex-direction: column;
  gap: var(--gw-space-40);   /* 621:5094 container gap */
}

.gw-login__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gw-space-20);   /* 621:5095 */
}
/* 60x60 at radius 15 holding a 30 mark. Those are not tokens by accident: the
   tile is the topbar's Spacing/8 + Radius/8 logo chip placed at 1.875x, so the
   drawn values land between steps. Reproduced as measured rather than rounded
   to Radius/16, which is the nearer token. */
.gw-login__chip {
  flex: none;
  width: 60px;
  height: 60px;
  display: grid;
  place-items: center;
  border-radius: 15px;
  background: var(--gw-color-primary-500);
  color: var(--gw-color-white);
}
.gw-login__chip svg { width: 30px; height: 30px; display: block; }

.gw-login__copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;                 /* 621:5097 — drawn 10, not a spacing step */
  text-align: center;
}
.gw-login__copy h1 {
  margin: 0;
  font: var(--gw-text-h5);
  letter-spacing: var(--gw-text-h5-tracking);
  color: var(--s-heading);
}
.gw-login__copy p {
  margin: 0;
  font: var(--gw-text-body-16-med);
  letter-spacing: var(--gw-text-body-16-med-tracking);
  color: var(--s-body);
}

/* Button 628:5272. Google's own button, so it is deliberately NOT themed: hard
   white with a black label in BOTH modes, exactly as .gw-modal__btn is and for
   the same reason — Google's mark keeps its own presentation and the surface it
   sits on has to stay neutral. The border is hard-coded for the same reason: a
   themed one would go Neutral/800 in dark and ring a white button.

   Drawn at a 0.81px border, which is a scaled instance rather than an intent;
   built at 1, the value the component holds unscaled. */
.gw-login__google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gw-space-8);
  width: 100%;
  height: 64px;
  padding: var(--gw-space-16) var(--gw-space-20) var(--gw-space-16) var(--gw-space-24);
  border: 1px solid var(--gw-color-neutral-200);
  border-radius: var(--gw-radius-16);
  background: var(--gw-color-white);
  color: var(--gw-color-black);
  font: var(--gw-text-button-16);
  text-decoration: none;
  box-shadow: var(--gw-shadow-s3);
  transition: box-shadow var(--gw-motion-fast);
}
.gw-login__google:hover { box-shadow: var(--gw-shadow-s4); }
.gw-login__google:focus-visible {
  outline: var(--gw-focus-ring);
  outline-offset: var(--gw-focus-offset);
}
/* Both marks are drawn 18. Sized separately rather than by one descendant rule:
   the G is four-colour brand artwork and the arrow inherits currentColor, so
   they are unlike assets that happen to share a dimension. */
.gw-login__g     { width: 18px; height: 18px; flex: none; display: block; }
.gw-login__arrow { width: 18px; height: 18px; flex: none; display: block; }

/* -- the password stopgap on this page -----------------------------------
   488:21730 draws the finished Google-only card, so none of this is measured
   off it. The field and the blue submit are lifted from the SAME node's
   previous revision — a Neutral/50 well at 56/r8 and a Primary/500 button at
   r12 — rather than inventing a second idiom for one temporary door. It all
   disappears the moment SITE_PASSWORD is blanked, which README-auth.md §4
   says to do once Google works. */
.gw-login__or {
  display: flex;
  align-items: center;
  gap: var(--gw-space-12);
  font: var(--gw-text-body-12-reg);
  color: var(--s-group-label);
}
.gw-login__or::before, .gw-login__or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--s-card-border);
}
.gw-login__or[hidden], .gw-login__pw[hidden], .gw-login__google[hidden] { display: none; }

.gw-login__pw {
  display: flex;
  flex-direction: column;
  gap: var(--gw-space-8);
  text-align: left;
}
.gw-login__label {
  font: var(--gw-text-body-12-sem);
  letter-spacing: var(--gw-text-body-12-sem-tracking);
  color: var(--s-nav-label);
}
/* --s-field-bg, not the drawn Neutral/50: in dark that token and the card both
   land on Neutral/900 and the well vanishes into the card. This one is
   Neutral/25 in light and black in dark, so it reads as an inset either way —
   the same reason .gw-search uses it. */
.gw-login__field {
  display: flex;
  align-items: center;
  height: 56px;
  padding: 0 var(--gw-space-16);
  border-radius: var(--gw-radius-8);
  background: var(--s-field-bg);
  overflow: hidden;
}
.gw-login__field:focus-within {
  outline: var(--gw-focus-ring);
  outline-offset: var(--gw-focus-offset);
}
.gw-login__input {
  flex: 1 0 0;
  min-width: 0;
  border: 0;
  background: none;
  padding: 0;
  font: var(--gw-text-button-14);
  color: var(--s-heading);
}
.gw-login__input:focus { outline: none; }
.gw-login__input::placeholder { color: var(--s-placeholder); }

.gw-login__err {
  margin: 0;
  font: var(--gw-text-body-12-reg);
  color: var(--s-danger);
}
.gw-login__err[hidden] { display: none; }

.gw-login__submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gw-space-8);
  width: 100%;
  margin-top: var(--gw-space-4);
  padding: var(--gw-space-16) var(--gw-space-20) var(--gw-space-16) var(--gw-space-24);
  border: 0;
  border-radius: var(--gw-radius-12);
  background: var(--gw-color-primary-500);
  color: var(--gw-color-white);
  font: var(--gw-text-button-16);
  cursor: pointer;
  box-shadow: var(--gw-shadow-s3);
}
.gw-login__submit:hover { background: var(--gw-color-primary-600); }
.gw-login__submit:disabled { opacity: .6; cursor: progress; }
.gw-login__submit:focus-visible {
  outline: var(--gw-focus-ring);
  outline-offset: var(--gw-focus-offset);
}

.gw-login__note {
  margin: 0;
  text-align: center;
  font: var(--gw-text-body-14-med);
  letter-spacing: var(--gw-text-body-14-med-tracking);
  color: var(--s-body);
}

/* ---------------------------------------------------------------------------
   7. Holding state for routes that have no design yet
   ------------------------------------------------------------------------ */
.gw-soon {
  padding: var(--gw-space-40);
  border-radius: var(--gw-radius-16);
  background: var(--s-card-bg);
  border: 1px solid var(--s-card-border);
  max-width: 820px;
}
.gw-soon h2 {
  margin: 0 0 var(--gw-space-8);
  font: var(--gw-text-h7);
  letter-spacing: var(--gw-text-h7-tracking);
  color: var(--s-heading);
}
.gw-soon p { margin: 0; font: var(--gw-text-body-14-reg); color: var(--s-body); }
.gw-soon ul { margin: var(--gw-space-16) 0 0; padding-left: var(--gw-space-20); }
.gw-soon li { font: var(--gw-text-body-14-reg); color: var(--s-body); margin-bottom: var(--gw-space-4); }
.gw-soon a { color: var(--gw-color-primary-500); }

/* ---------------------------------------------------------------------------
   8. Fitting the generated sheets under the topbar

   changelog-sheet, review-sheet, catalogue and install were all written
   before this chrome existed. They are not restyled — they keep their own
   layout — but two things break when a 60px fixed topbar appears above them.

   a) Sticky rails. `position: sticky` resolves `top` against the viewport,
      not against .gw-main's margin, so a rail written as top:20px now hides
      under the topbar. Each known offset is pushed down by 60.
   b) Anchor jumps. The changelog's 46-release index scrolls to #v1-39-0 and
      friends; without scroll-padding the heading lands behind the topbar.
   ------------------------------------------------------------------------ */
:root.gw-shell-ready { scroll-padding-top: calc(60px + var(--gw-space-16)); }

.gw-main .idx { top: calc(60px + var(--gw-space-40)); }  /* changelog index rail */
.gw-main .nav { top: calc(60px + 20px); }                /* review sheet nav rail */
.gw-main thead th { top: 60px; }                         /* sticky table headers */

/* ---------------------------------------------------------------------------
   9. Below 1440 — SCALE the canvas. Do not reflow. RULED.

   exports/dashboard/build-rules.md, ruled by Utsav 7 Aug 2026 after three
   attempts got it wrong: "1440 is the minimum width. Below it, SCALE the
   canvas." Reflow is named there as a rejected attempt — it produces a layout
   matching none of the drawn variants.

   This page is a rail + content shell like `dashboard-build`, so the same
   ruling governs: hold the 1440 layout at every viewport and scale it down to
   fit. Every measured value survives — the 240 rail, the 1200 column, the 800
   main and 260 TOC — and nothing is clipped or rearranged.

   Unlike `dashboard-build` this page SCROLLS, so the factor comes from width
   only and the height is left to grow. `--gw-fit` is set by shell.js as
   min(1, innerWidth / 1440); it never scales up.

   The trade-off, per the rule: below 1440 type paints smaller than the ramp.
   That is the accepted price of a fixed-width canvas.
   ------------------------------------------------------------------------ */
:root { --gw-fit: 1; }

.gw-shell {
  zoom: var(--gw-fit, 1);
  /* The 1440 canvas has to fill the real viewport once scaled.

     `100%`, NOT `100vw`. Two different reasons, and they happen to agree:

     a) `100vw` counts the classic-scrollbar gutter and the content box does
        not. Measured at 1512x950 with a 15px always-on scrollbar:
        100vw = 1512 but documentElement.clientWidth = 1497, so the shell laid
        out 15px wider than the space it had — the topbar ran under the
        scrollbar and the page grew a phantom horizontal scrollbar of exactly
        that 15px. A percentage resolves against the content box, so it never
        counts the gutter. Under macOS overlay scrollbars the gutter is 0,
        which is why this only showed up with "Show scroll bars: Always".

     b) `zoom` already divides the containing block back out when it resolves
        a percentage, so `100%` needs NO `/ var(--gw-fit)`. Dividing as well
        double-compensates: measured at 1200 wide / fit .8333, `100% / fit`
        computed 1706.4px and rendered 1422 against a 1185 content box —
        237px of overflow. Plain `100%` renders exactly 1185.

     Viewport units are absolute and are NOT divided back out, so anything
     still written in vh below keeps its `/ var(--gw-fit)`. */
  width: 100%;
  min-height: calc(100vh / var(--gw-fit, 1));
}

/* The chrome is fixed to the viewport. Inside a zoomed ancestor its own
   offsets scale with everything else, which is what we want — but it still
   needs an explicit width, or it stops short of the right edge below fit 1.

   The base rule is `inset: 0 0 auto 0`, which already stops at the content
   edge; an explicit width overrides that `right: 0` (over-constrained in LTR),
   and the old `100vw` width is what pushed the bar under the scrollbar. `100%`
   on a fixed element resolves against the viewport's content box — gutter
   excluded — and, as above, zoom needs no division on a percentage. */
.gw-shell .gw-topbar { width: 100%; }

/* ---------------------------------------------------------------------------
   10. Phone — reflow, not scale

   The scale-to-fit ruling above is scoped to `dashboard-build`, and says so:
   its numbers "are mutually dependent and hold ONLY at 1084". This site has no
   such composition — prose, cards and a nav list all reflow without deforming.

   So below the Breakpoint collection's own Phone threshold (767, already the
   width at which tokens.css swaps the whole --gw-bp-* set) the page reflows to
   the phone token set instead of scaling. At 375 the scaled canvas paints 14px
   type at under 4px, which is a thumbnail rather than a phone view.

   The nav follows the navbar component's Phone variants: `Collapsed` is
   logo + hamburger at 60 tall, `Menu` is a full-screen overlay. Bar height
   stays 60 — page-shell.md measures 60 at BOTH breakpoints, which contradicts
   --gw-bp-navbar-h (56 on phone); that token is transcribed from the master
   spec rather than read off the component, so the measured 60 wins.
   ------------------------------------------------------------------------ */
.gw-burger {
  display: none;                     /* desktop: the rail is always visible */
  width: 36px; height: 36px; flex: none;
  place-items: center;
  border: 0; border-radius: var(--gw-radius-8);
  background: none; color: var(--s-nav-label); cursor: pointer;
}
.gw-burger svg { width: 16px; height: 16px; display: block; }

/* The phone's search affordance. BARE — no box, no fill: on a phone bar the icon is the
   control, and boxing it makes a header of competing chrome out of what should read as a row
   of actions. It keeps the full 36x36 as a tap target; only the paint is gone.

   The burger beside it keeps its box because it is measured — it follows the navbar
   component's Phone `Collapsed` variant — and dropping that would be a deviation from the
   source rather than a choice about a control this repo invented. */
.gw-searchbtn {
  display: none;
  width: 36px; height: 36px; flex: none;
  place-items: center;
  border: 0; background: none; padding: 0;
  border-radius: var(--gw-radius-12);
  /* --s-field-icon, not --s-nav-label: it is documented as "measured: MagnifyingGlass
     vector", so it is the token for this exact glyph, and it is the lighter of the two in
     both themes (neutral-600 / neutral-300 against neutral-900 / white). */
  color: var(--s-field-icon); cursor: pointer;
}
/* 16, matching the burger. The burger's size is the measured one, so search moves to it
   rather than the other way round — the two sit next to each other and should read as one
   row of actions, not two sizes of icon. */
.gw-searchbtn svg { width: 16px; height: 16px; display: block; }
.gw-searchbtn:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }
.gw-burger:focus-visible { outline: var(--gw-focus-ring); outline-offset: var(--gw-focus-offset); }

@media (max-width: 767px) {
  /* The zoom is neutralised by shell.js setting --gw-fit to 1 here, but pin
     the box too so a stale inline value cannot shrink a reflowed layout. */
  .gw-shell { zoom: 1; width: 100%; min-height: 100vh; }
  .gw-shell .gw-topbar { width: 100%; }

  .gw-topbar { padding: 0 var(--gw-bp-pad-page-h); }
  .gw-burger { display: grid; }
  /* The 400px field cannot fit at 375, so it gives way to the icon button — which is the
     same control, not a removal: search is reachable on a phone. */
  .gw-search { display: none; }
  .gw-searchbtn { display: grid; }

  /* The measured gap 16 (Frame 2147260244) separates the FIELD from the theme toggle. At
     this width neither is there: it is a bare icon beside the burger, and a bare icon has no
     box edge, so 16 reads as a bigger hole than it measures. 8 here is a decision about a
     composition that only exists on phone — the desktop frame keeps its measured 16. */
  .gw-topbar__right { gap: var(--gw-space-8); }

  /* `Menu` — the rail becomes a full-screen overlay under the bar. */
  .gw-sidebar {
    width: 100%;
    border-right: 0;
    border-bottom: 1.5px solid var(--s-chrome-border);
    transform: translateX(-100%);
    transition: transform var(--gw-motion-fast);
    z-index: 50;
  }
  :root.gw-nav-open .gw-sidebar { transform: translateX(0); }
  /* The rail is the whole screen on phone, so its bottom block does not need
     to be pushed away from the nav. */
  .gw-sidebar { justify-content: flex-start; }
  .gw-navend { margin-top: auto; }

  .gw-main { margin-left: 0; }

  /* The login card is a fixed 480 on desktop, which overflows a 375 phone.
     max-width already reins the card in; these bring its padding onto the
     phone token set so the copy is not pressed against the edges. */
  .gw-login { padding: var(--gw-bp-pad-section-v) var(--gw-bp-pad-page-h); }
  .gw-login__card { padding: var(--gw-space-32) var(--gw-space-20); }

  /* Page padding from the phone tokens: 16 sides, 60 vertical. */
  .gw-col {
    max-width: none;
    padding: var(--gw-bp-pad-section-v) var(--gw-bp-pad-page-h);
    gap: var(--gw-bp-section-gap);
  }
  .gw-section { padding: 0; }
  .gw-prose { max-width: none; }

  /* The hero is a fixed 200 at desktop; on phone the title wraps, so height
     gives way and the type steps to --gw-bp-type-hero (44 in Phone mode). */
  .gw-hero {
    height: auto;
    min-height: 0;
    padding: var(--gw-bp-pad-card-v) var(--gw-bp-pad-card-h);
    border-radius: var(--gw-bp-radius-large);
  }
  .gw-hero__title { font-size: var(--gw-bp-type-hero); }
  /* The three grid cells are placed at desktop coordinates; off-canvas here. */
  .gw-hero__cell { display: none; }

  /* The "On this page" rail is a desktop affordance — there is no phone design
     for it, and the sections are reachable by scrolling. */
  .idx { display: none; }
}

/* The artifact carries this; motion tokens are the only place it can apply. */
@media (prefers-reduced-motion: reduce) { :root { --gw-motion-fast: 0ms; } }

/* ---------------------------------------------------------------------------
   11. Phone dock — the theme toggle at phone width

   From the postmortem artifact. Below the phone breakpoint the topbar cannot
   hold the wordmark, the toggle AND the drawer trigger at 375, so the toggle
   moves to a floating cluster fixed 20 from the left and bottom. It is a
   duplicate node, not a moved one — CSS cannot reparent, and the theme JS
   keeps every [data-theme-set] in sync.

   Unlike the topbar copy, which has no fill, the docked one carries a fill,
   a border and Shadows/S3 so it reads against scrolling content. Surface
   aliases, so it still inverts in dark.
   ------------------------------------------------------------------------ */
.gw-phone-dock { display: none; }

@media (max-width: 599px) {
  .gw-phone-dock {
    display: flex; align-items: center; gap: var(--gw-space-8);
    position: fixed; left: 20px; bottom: 20px; z-index: 50;
  }
  .gw-phone-dock .gw-theme {
    /* --s-card-bg, not the artifact's --s-card: same role, different name in
       this codebase's alias layer. Copied verbatim it resolved to nothing and
       the dock painted transparent. */
    background: var(--s-card-bg);
    box-shadow: inset 0 0 0 1px var(--s-field-border), var(--gw-shadow-s3);
  }
  /* The topbar copy stands down so only one is ever visible. */
  .gw-topbar__right > .gw-theme { display: none; }

  /* Topbar at phone: the logo tile is a scaled instance — 20x20 rendering the
     32x32 tile, which is why its corner takes radius/4 rather than the /8 the
     component is defined at. The wordmark steps to 16. */
  .gw-brand__chip { width: 20px; height: 20px; border-radius: var(--gw-radius-4); }
  .gw-brand__chip svg { width: 10px; height: 10px; }
  .gw-brand__name { font-size: 16px; }
  .gw-topbar { padding: 0 var(--gw-bp-pad-page-h); }
}

/* The drawer trigger is `controls/tab` 36x36, radius/8, 1px neutral/200 with a
   16px List glyph — it had no border here before. */
.gw-burger { border: 1px solid var(--s-field-border); }
