/* ############################################################################
   #                                                                          #
   #   W A N D L E T   —   D E S I G N   S Y S T E M                          #
   #   Split Rail, round 2. The left rail is gone; a top bar replaces it.     #
   #                                                                          #
   #   Ported from design/prototypes/wandlet-redesign.html, which is the       #
   #   approved record and is not edited. This file is that stylesheet plus    #
   #   the parts production needs and a prototype never has: the markup the    #
   #   scripts write at runtime, and the light/dark contract as theme.js       #
   #   actually implements it.                                                 #
   #                                                                          #
   #   All seven pages come out of what is written below: the shared system    #
   #   in sections 0–12, then one scoped section per page in 13–15 holding     #
   #   only the geometry that page needed. Do not invent new colours, new      #
   #   font sizes, new radii, or a second button system. If something you      #
   #   need genuinely is not here, add it to this comment when you add it.     #
   #                                                                          #
   ##########################################################################

   ----------------------------------------------------------------------------
   0. THE SHELL, AND WHAT CARRIES CONTINUITY
   ----------------------------------------------------------------------------
   The direction's argument is "one shell, both sides of the product". The left
   rail used to carry it. Now these five things carry it, together, and they are
   the reason nothing here may be page-specific:

     1. .topbar   — ONE bar, same markup, same links, same order, on all seven
                    pages, copied verbatim from work/shell.html and compared
                    byte-for-byte by landing/nav-consistency.test.js. Marketing
                    destinations and account destinations are peers in the SAME
                    nav row, divided only by a hairline (.topnav .sep). Signing
                    in does not swap the furniture; it moves the accent
                    underline along a row that never changes. The only thing
                    that varies is the account slot's label (Sign in → the
                    user's name), written by nav.js into #navAuth and nowhere
                    else. The CTA is the same button everywhere.
     2. .pagehead — every page opens with the identical block: eyebrow, h1,
                    lead. The landing hero is that block with a stage under it.
     3. .col      — one measure, one centred column, on every page.
     4. .foot     — one footer, identical everywhere. A page must NOT ship its
                    own footer or its own nav.
     5. The type scale, the spacing scale and the single accent below.

   Structure of every page:

     body.page-NAME > header.nav.topbar#nav     verbatim, see work/shell.html
                    > main#main                 the page, and only the page
                    > footer.foot               verbatim
                    > .modal                    dialogs live at body level

   ----------------------------------------------------------------------------
   0a. HOW THIS FILE MEETS THE JAVASCRIPT   (the part the prototype never had)
   ----------------------------------------------------------------------------
   Roughly forty class names in this file appear in NO html file. The scripts
   write them into innerHTML at runtime — the whole dashboard, the whole Action
   Library, the cookie banner, the account menu. If a rule for one goes missing
   the page renders as unstyled text and NO test catches it. They are marked
   "runtime · <script>:<line>" where they are styled, so the pairing stays
   discoverable. Rules of engagement:

     · The JS hook wins, the CSS adapts. Where the prototype's name and the
       script's name mean the same thing, ONE rule carries both selectors —
       .chip, .macro-status { … } — rather than two rules for one idea, and
       rather than 25 edits across two scripts.
     · .btn-primary and .btn-ghost are ALIASES of .btn-accent and .btn-quiet.
       dashboard.js and repo.js emit the old names ~25 times and
       nav-consistency.test.js pins them in the static nav. They are not dead
       code; do not "clean them up".
     · Hook-only classes carry no rules at all, because a page author can put
       them on an element that already has a design class: .price-card,
       .price-amt, .price-quota, .price-annual (app.js:49–60) ride alongside
       .plan, .amt, .quota, .annual. Classes the scripts EMIT get real rules
       here, because nobody can add a class to markup that does not exist yet.
     · Ids appear as selectors only where the id IS the contract: #repoGrid,
       #macrosBody, #loginNote — the scripts require those exact ids, so
       styling the container by id is the honest way to reach it.

   ----------------------------------------------------------------------------
   0b. LIGHT / DARK, AS THE LIVE SITE IMPLEMENTS IT
   ----------------------------------------------------------------------------
   theme.js:35–36 SETS html[data-theme="dark"] for dark and REMOVES the
   attribute entirely for light. There is no data-theme="light" at runtime, on
   any page, ever. So:

       dark   →  html[data-theme="dark"] …
       light  →  the base rule, or html:not([data-theme="dark"]) … when a rule
                 genuinely needs to name it (only the wordmark swap does)

   A rule written html[data-theme="light"] … — as the prototype's wordmark rule
   was — never matches on the live site, and both wordmarks render stacked.

   ----------------------------------------------------------------------------
   1. COLOUR TOKENS   (both themes; html[data-theme="dark"] flips them)
   ----------------------------------------------------------------------------
     --paper      page background
     --bar        top-bar background — one step off --paper, never a slab of brand
     --surface    cards, panels, inputs sitting on --paper
     --line       borders you are meant to see (cards, bar, table)
     --line-soft  hairlines inside a component (row separators)
     --ink        body text                     (≥4.5:1 on --paper in both themes)
     --heading    headings; a touch darker/lighter than --ink
     --muted      secondary text                (≥4.5:1 on --paper in both themes)
     --accent     THE accent. One. Used for: primary button, active nav
                  underline, focus ring, list bullets, step numbers, chip-accent,
                  the usage bar. Never as a large filled area.
     --accent-soft  ~9% accent wash, for chip and notice backgrounds only
     --danger     destructive text/border        --ok  positive chip
   Anything needing a colour must resolve to one of these. No new hex values.
   Nothing reads a token from JavaScript any more: the one place that did — a
   plans-editor label built with an inline color: var(--muted) — now emits a
   .field instead, so every colour on the page comes out of this block.

   ----------------------------------------------------------------------------
   2. LAYOUT TOKENS AND THE CONTAINER / MEASURE RULE
   ----------------------------------------------------------------------------
     --col       880px   the reading column
     --col-wide 1010px   the shell + hero container
     --pad-x     clamp(22px, 4.2vw, 60px)   gutter, identical for both
     --r         12px    card / panel radius   (buttons 10px, small 8px)
     --bar-h     60px    top bar height

   THE RULE:
     .col        max-width:var(--col); margin-inline:auto; padding-inline:var(--pad-x)
                 → everything you read sits here. Default choice.
     .col-wide   same, but max-width:var(--col-wide)
                 → the top bar, the footer, the landing hero — and the Action
                   Library, which is the one PAGE that takes it. It is a
                   symmetric 65px-a-side inset of the reading column inside the
                   shell, and it is deliberate: furniture is wider than prose. A
                   catalogue of cards is furniture; at --col its grid can never
                   be more than two columns. Any page that takes it owes the
                   prose inside it a measure of its own — see §13's caps on
                   .notice and .card > p. Prose alone never takes it.
     Measure: running prose never exceeds ~34em (.lead) / ~40em (.prose). If a
     block of text is wider than that, it is the wrong block.
     Nothing may cause horizontal body scroll. Wide things (code, tables,
     prompts) scroll inside their own container: .table-wrap, .cmd-row code,
     .prompt.

   ----------------------------------------------------------------------------
   3. TYPE SCALE   (system stack --sans; --mono for prompts, code, textareas)
   ----------------------------------------------------------------------------
     body       16.5px / 1.65        (16px below 620px)
     h1         clamp(2.05rem, 5.1vw, 3.15rem)   one per page, in .pagehead
     h2         clamp(1.5rem, 3.1vw, 2rem)       section headings
     .quote     clamp(1.25rem, 2.6vw, 1.6rem)    a stated conviction, not a heading level
     h3         1.03rem                          card / step headings
     .lead      clamp(1.02rem, 1.6vw, 1.16rem), --muted, max 34em
     .prose p   15.4px --muted                   legal + long-form body
     body copy in components   14.5–15.2px
     .note      13.5px --muted                   fine print  (= .acct-who, runtime)
     .eyebrow   11px, 0.15em tracking, uppercase, --muted — above every h1/h2
     .amt/.big  1.55–1.7rem, 640 weight          a number that matters
     Headings: weight 660, line-height 1.18, letter-spacing -0.018em. Never bold
     body text for emphasis at more than 600.

   ----------------------------------------------------------------------------
   4. SPACING SCALE   (use the token, not a number)
   ----------------------------------------------------------------------------
     --s1  6px   --s2 10px   --s3 14px   --s4 20px
     --s5 28px   --s6 40px   --s7 56px
     --band  clamp(56px, 8vw, 104px)   vertical padding of a .band section
     Utilities: .mt-1 …6  =  s2 s3 s4 s5 s6 s7   (top margin only; there is no
     margin-bottom utility — space is pushed down the page, never up).
     Rhythm: inside a card, gaps are s2–s4; between cards, s4; between sections,
     --band with a hairline rule.

   ----------------------------------------------------------------------------
   5. REUSABLE CLASSES   (what each is for — one line each)
   ----------------------------------------------------------------------------
   SHELL
     .topbar / .bar-in / .topnav / .menupanel  the one navigation bar; do not copy it into a page
     .foot / .foot-links                       the one footer; do not copy it into a page
     .skip                                     skip-to-content link
     .user-menu / .user-trigger / .user-dropdown   runtime · nav.js:121 — the
                    signed-in account menu inside #navAuth; opens on hover and
                    on focus-within, which is what makes it work on touch
   PAGE FRAME
     .band          one vertical section with --band padding and a top hairline rule
     .col .col-wide .col-library
                    the three containers — see the container rule above.
                    .col-library is /repo's grid only; see §13.
     .pagehead      the identical opening block of every page: eyebrow + h1 + lead
     .subnav        optional in-page jump links under a .pagehead (page content, not shell)
     .eyebrow       small uppercase kicker above a heading
     .lead .note    the two secondary text sizes
     .prose         long-form text block for the legal pages (h2/p/ul rhythm, 40em)
     .reveal        opt-in scroll reveal; app.js:32–36 adds .in when it scrolls
                    into view. Landing only — put it on .band sections.
   BLOCKS
     .card          the standard bordered surface panel; .card-head for its title row
     .grid          responsive auto-fit card grid, min 280px, gap s4 (.grid-3 → min 220px)
     .duo           two-column prose split with a wide gutter, for a stated contrast
     .list          bordered vertical stack container for rows
     .row           ONE row inside it: .ic icon · .nm (b + small) · .chip · .ops buttons
                    (.list > li gets .row's styling automatically; = .macro-item, runtime)
     .table-wrap + .table   tabular data; the wrap is what scrolls sideways
     .deflist       label/value pairs (dl) — profile facts, Impressum fields
     .prompt        a full action prompt in mono  (= .review-prompt, runtime).
                    §13 cuts it to --pp-lines whole lines inside .repo-prompt
                    and adds .is-open to expand it in place.
     .notice        one quiet accent-edged paragraph: a caveat, a short-form note
                    (= .dl-foot, whose exact class attribute download-links.test.js pins)
     .facts         bulleted list of plain claims, accent dots
     .acts          wrapping row of pill labels (built-in actions); .soft = dashed pill
     .flow          chips joined by → arrows, for a status progression
     .steps         numbered how-it-works list
     .plans/.plan   the pricing table  (a .plan also carries app.js's .price-card hook)
     .cmd/.cmd-row  a copyable one-line install command
     .bar-meter     thin accent progress bar (usage)  (= .acct-bar, runtime)
     .waitlist      the quiet e-mail block in the download area (#wlForm, app.js:71)
     .modal         centred dialog at body level; .modal-box, .modal-foot.
                    dashboard.js drives #deleteModal with inline style.display,
                    so the base rule is display:none WITHOUT !important.
     .cc-*          the cookie banner and dialog — runtime · consent.js:120–178.
                    .cc-btn's min-width and .cc-btn-accent's colour-only
                    difference are asserted by cookie-consent.test.js:116–126.
   CONTROLS
     .btn           the only button. Variants: .btn-accent (one per view),
                    .btn-quiet, .btn-bare, .btn-danger; sizes .btn-sm, .btn-block.
                    ALIASES: .btn-primary = .btn-accent, .btn-ghost = .btn-quiet.
     .chip          status pill; .chip-accent, .chip-ok  (= .macro-status[.approved], runtime)
     .field         label + input/textarea/select stack; .field-2 for icon+name
                    (= .macro-input / .macro-textarea, runtime)
     .seg           segmented control (aria-pressed on the buttons)
     .toolbar       filter/sort row above a listing — search input + .seg
     .tog           the 30px square icon button (theme toggle)
     .menubtn       the mobile menu control (#navBurger)
     .link-accent   an inline forward link in the accent, "Do the thing →"
     kbd            a key cap

   PER-PAGE ADDITIONS — sections 13–15 at the foot of this file. Each is scoped
   to a body.page-* class or to a class only that page can have, so nothing in
   them can reach another page: the Library's .repo-* listing split, the sign-in
   card, and the legal pages' closing cross-links and cookie table.

   ----------------------------------------------------------------------------
   6. THE PAGE CONTRACT   (read this before writing a page)
   ----------------------------------------------------------------------------
   - Copy the head, nav, footer and tail scripts from work/shell.html verbatim.
     Never write a second bar, a second footer, or a second <h1>.
   - Open with:  <section class="band"><div class="col"><div class="pagehead">
                   <span class="eyebrow">…</span><h1>…</h1><p class="lead">…</p>
                 </div> … </div></section>
   - Every page needs <span id="year"> (it is in the shared footer) and
     data-cc-open (also in the shared footer). Both are load-bearing.
   - Dialogs go at body level with the other .modal blocks.
   - Both themes must work. Test with the toggle. Never hard-code a colour.
   - Real, finished content only. No "Loading…", no empty states, no lorem.
   ========================================================================== */

/* ============================================================================
   0 — POPOVER REPLICA — pasted verbatim from POPOVER.html. Do not restyle.
   It is the product's own UI, used once, large, as the landing hero's
   evidence. Its tokens are self-contained (--wl-*) and it must not be made to
   read from the site's tokens.
   ========================================================================= */
.wlpop {
  --wl-bg: rgba(26, 26, 32, 0.99);
  --wl-fg: #f2f2f5;
  --wl-muted: #9a9aa6;
  --wl-accent: #7c5cff;
  --wl-sel: rgba(124, 92, 255, 0.24);
  --wl-border: rgba(255, 255, 255, 0.08);
  --wl-chip: rgba(124, 92, 255, 0.18);
  --wl-ok: #3ddc84;
  --wl-diff-del: #ff9d94;
  --wl-diff-del-bg: rgba(255, 110, 100, 0.14);
  --wl-diff-ins: #6fdfa2;
  --wl-shadow: rgba(0, 0, 0, 0.45);

  font-size: 13px;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--wl-fg);
  line-height: 1.4;
  text-align: left;
  user-select: none;
  -webkit-user-select: none;
  display: flex;
  flex-direction: column;
  width: 25.4em;
  max-width: 100%;
}
.wlpop.light {
  --wl-bg: rgba(249, 249, 252, 0.99);
  --wl-fg: #1c1c24;
  --wl-muted: #6b6b78;
  --wl-accent: #6a44ff;
  --wl-sel: rgba(106, 68, 255, 0.16);
  --wl-border: rgba(0, 0, 0, 0.12);
  --wl-chip: rgba(106, 68, 255, 0.1);
  --wl-ok: #1a9c5b;
  --wl-diff-del: #b3261e;
  --wl-diff-del-bg: rgba(179, 38, 30, 0.1);
  --wl-diff-ins: #17743f;
  --wl-shadow: rgba(0, 0, 0, 0.18);
}
.wlpop * { box-sizing: border-box; margin: 0; padding: 0; }

.wlpop .wl-panel {
  background: var(--wl-bg);
  border: 1px solid var(--wl-border);
  border-radius: 0.77em;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.wlpop.float .wl-panel { box-shadow: 0 1.5em 3.5em -0.8em var(--wl-shadow); }

.wlpop .wl-query { padding: 0.69em 0.92em 0.54em; border-bottom: 1px solid var(--wl-border); }
.wlpop .wl-qrow { display: flex; align-items: center; gap: 0.54em; min-height: 1.46em; }
.wlpop .wl-filter { font-size: 1.04em; color: var(--wl-muted); }
.wlpop .wl-filter.typed { color: var(--wl-fg); }
.wlpop .wl-qtitle,
.wlpop .wl-label { font-size: 1.04em; font-weight: 600; color: var(--wl-fg); }
.wlpop .wl-label { font-size: 0.92em; color: var(--wl-muted); }
.wlpop .wl-qsub { font-size: 0.81em; color: var(--wl-muted); margin-top: 0.15em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wlpop .wl-back { color: var(--wl-muted); font-size: 1.31em; line-height: 1; padding: 0 0.23em 0.15em; margin-left: -0.23em; }
.wlpop .wl-bar-right { margin-left: auto; display: flex; align-items: center; gap: 0.46em; }

.wlpop .wl-list { list-style: none; padding: 0.31em; }
.wlpop .wl-item { display: flex; align-items: center; gap: 0.62em; padding: 0.46em 0.62em; border-radius: 0.54em; white-space: nowrap; }
.wlpop .wl-item.sel { background: var(--wl-sel); box-shadow: inset 0 0 0 1px rgba(124, 92, 255, 0.5); }
.wlpop .wl-item .wl-icon { width: 1.38em; text-align: center; flex: none; }
.wlpop .wl-item .wl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.wlpop .wl-item .wl-flags { flex: none; font-size: 0.77em; color: var(--wl-muted); letter-spacing: 1px; }
.wlpop .wl-item .wl-num { flex: none; font-size: 0.77em; color: var(--wl-muted); border: 1px solid var(--wl-border); border-radius: 0.31em; padding: 0 0.38em; line-height: 1.6; }
.wlpop .wl-item.pin-end { position: relative; margin-bottom: 0.54em; }
.wlpop .wl-item.pin-end::after { content: ""; position: absolute; left: 0.46em; right: 0.46em; bottom: -0.31em; border-top: 1px dashed var(--wl-border); }

.wlpop .wl-preview { padding: 0.69em 0.85em; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; user-select: text; -webkit-user-select: text; }
.wlpop .wl-caret { display: inline-block; width: 0.46em; height: 1.08em; background: var(--wl-accent); margin-left: 1px; vertical-align: text-bottom; animation: wl-blink 1s steps(2, start) infinite; }
@keyframes wl-blink { to { visibility: hidden; } }
@media (prefers-reduced-motion: reduce) { .wlpop .wl-caret { animation: none; } }
.wlpop .wl-preview del { color: var(--wl-diff-del); background: var(--wl-diff-del-bg); text-decoration: line-through; border-radius: 3px; padding: 0 1px; }
.wlpop .wl-preview ins { color: var(--wl-diff-ins); text-decoration: none; font-weight: 600; }

.wlpop .wl-actions { display: flex; align-items: center; gap: 0.46em; padding: 0.54em 0.77em 0.54em 0.92em; border-top: 1px solid var(--wl-border); }
.wlpop .wl-btn { flex: none; border: 1px solid var(--wl-border); background: transparent; color: var(--wl-fg); font: inherit; font-size: 0.92em; font-weight: 600; padding: 0.31em 0.85em; border-radius: 0.46em; }
.wlpop .wl-btn.primary { border-color: var(--wl-accent); background: var(--wl-chip); }
.wlpop .wl-btn.retry { padding: 0.31em 0.62em; }
.wlpop .wl-refine { flex: 1 1 auto; min-width: 0; border: 1px dashed var(--wl-border); border-radius: 0.46em; color: var(--wl-muted); font-size: 0.88em; padding: 0.31em 0.69em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.wlpop .wl-status { display: flex; align-items: center; gap: 0.62em; padding: 0.38em 0.69em 0.38em 0.85em; border-top: 1px solid var(--wl-border); font-size: 0.81em; color: var(--wl-muted); min-height: 2em; }
.wlpop .wl-status .wl-keys { margin-left: auto; font-size: 0.95em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wlpop .wl-acct { flex: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wlpop .wl-acct.in { color: var(--wl-fg); }
.wlpop .wl-acct .wl-dot { color: var(--wl-ok); margin-right: 0.3em; }
.wlpop .wl-iconbtn { border: 1px solid var(--wl-border); background: transparent; color: var(--wl-muted); font-size: 1.15em; line-height: 1; width: 2em; height: 1.85em; border-radius: 0.46em; display: flex; align-items: center; justify-content: center; flex: none; }
.wlpop .wl-chip { border: 1px solid var(--wl-border); background: transparent; color: var(--wl-muted); font-size: 0.81em; padding: 0.15em 0.62em; border-radius: 999px; white-space: nowrap; flex: none; }
.wlpop .wl-acctbtn { border: 1px solid var(--wl-border); background: transparent; color: var(--wl-accent); font-size: 1em; font-weight: 600; padding: 0.15em 0.62em; border-radius: 0.38em; white-space: nowrap; flex: none; }

/* ============================================================================
   1 — TOKENS
   ========================================================================= */
:root {
  color-scheme: light;
  --paper:    #f4f5f7;
  --bar:      #ecedf1;
  --surface:  #ffffff;
  --line:     #dfe1e9;
  --line-soft:#e9eaf0;
  --ink:      #1b2030;
  --heading:  #0f1e2b;
  --muted:    #586074;
  --accent:   #6c3afd;
  --accent-soft: rgba(108, 58, 253, 0.09);
  --danger:   #b3261e;
  --ok:       #17743f;

  --col: 880px;
  --col-wide: 1010px;
  /* wider than --col-wide, and only the Action Library uses it — see §13.
     --col-wide itself may not move: the top bar and the footer share it. */
  --col-library: 1440px;
  --pad-x: clamp(22px, 4.2vw, 60px);
  --bar-h: 60px;
  --r: 12px;

  --s1: 6px; --s2: 10px; --s3: 14px; --s4: 20px; --s5: 28px; --s6: 40px; --s7: 56px;
  --band: clamp(56px, 8vw, 104px);

  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;

  --pop: 0 18px 44px -24px rgba(15, 20, 35, 0.38);
}
/* Dark is the ONLY attributed state — theme.js removes data-theme for light.
   Never write html[data-theme="light"]; it matches nothing on the live site. */
html[data-theme="dark"] {
  color-scheme: dark;
  --paper:    #0a0c11;
  --bar:      #0e1016;
  --surface:  #13161e;
  --line:     #242836;
  --line-soft:#1b1f2a;
  --ink:      #e7eaf2;
  --heading:  #f7f8fc;
  --muted:    #a3abbd;
  --accent:   #a78bfa;
  --accent-soft: rgba(167, 139, 250, 0.13);
  --danger:   #ff9d94;
  --ok:       #6fdfa2;

  --pop: 0 18px 44px -22px rgba(0, 0, 0, 0.75);
}

/* ============================================================================
   2 — BASE
   ========================================================================= */
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16.5px;
  line-height: 1.65;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
h1, h2, h3, h4 { color: var(--heading); line-height: 1.18; letter-spacing: -0.018em; margin: 0; font-weight: 660; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.skip {
  position: absolute; left: 10px; top: -60px; z-index: 90;
  background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  padding: 10px 16px; border-radius: 10px; text-decoration: none; font-weight: 600;
}
.skip:focus { top: 10px; }

/* ============================================================================
   3 — THE TOP BAR. Identical on every page. This is the shell.

   The markup is <header class="nav topbar" id="nav">: `topbar` is the design,
   `nav` and `#nav` are the hooks (nav.js:35,44 and nav-consistency.test.js's
   extractor). Likewise <nav class="nav-links topnav">: `topnav` is the design,
   `nav-links` is what nav.js:58 queries to mark the current page — it carries
   no styling of its own, on purpose, so it can sit on the mobile panel too.
   ========================================================================= */
.topbar {
  position: sticky; top: 0; z-index: 50;
  background: var(--bar);
  border-bottom: 1px solid var(--line);
}
/* nav.js:44 toggles .scrolled past 8px — one step of separation, no reflow */
.topbar.scrolled { box-shadow: 0 1px 0 0 var(--line), 0 10px 24px -22px rgba(15, 20, 35, 0.5); }
.bar-in {
  max-width: var(--col-wide); margin: 0 auto; padding: 0 var(--pad-x);
  min-height: var(--bar-h);
  display: flex; align-items: center; gap: var(--s4);
}
.brand { display: inline-flex; text-decoration: none; padding: 2px; border-radius: 6px; flex: none; }
/* --wm-optical: the wordmark's artwork is top-light — the wand and the two
   sparkles occupy the upper third, so the ink's alpha centroid sits 26 of 132
   rows BELOW the image's geometric centre, and the lettering band (rows 73–129)
   sits 35 rows below it. Centring the img BOX therefore drops the letters ~6.6px
   under the bar's centre line, which is why the logo read as uncentred next to
   nav text that is centred. The nudge lifts the lettering onto that line; it is
   a transform, so the layout box is untouched and nothing reflows. Expressed as
   a percentage of the rendered height, so it survives any logo size. */
.brand img { height: 25px; width: auto; transform: translateY(var(--wm-optical, -26.5%)); }
/* The artwork ships as two rasters because its navy ink is unreadable on the
   dark surface. Written as :not([data-theme="dark"]) because light is the
   ABSENCE of the attribute — see §0b. */
html:not([data-theme="dark"]) .wm-dark,
html[data-theme="dark"] .wm-light { display: none; }

/* one nav row. Product links and account links are peers in it, parted by a
   hairline — that hairline is the whole "one shell" argument, made visible.
   The row stretches the full bar height so the active underline sits exactly
   on the bar's bottom border. */
.topnav { display: flex; align-self: stretch; align-items: stretch; gap: 2px; margin-left: var(--s2); }
.topnav a {
  position: relative; display: flex; align-items: center; text-decoration: none;
  color: var(--muted); font-size: 14.5px; line-height: 1.4; white-space: nowrap;
  padding: 0 10px; border-radius: 8px 8px 0 0;
  transition: color .15s ease;
}
.topnav a:hover { color: var(--ink); }
/* nav.js:63 adds .active + aria-current to the link whose path is this page */
.topnav a.active { color: var(--heading); font-weight: 600; }
.topnav a.active::after {
  content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px;
  height: 2px; border-radius: 2px 2px 0 0; background: var(--accent);
}
.topnav .sep { width: 1px; align-self: stretch; margin: 16px var(--s2); background: var(--line); flex: none; }

.bar-right { margin-left: auto; display: flex; align-items: center; gap: var(--s2); flex: none; align-self: stretch; }

/* the account slot. #navAuth is the ONLY session-dependent element in the bar:
   signed out it holds the .acct link below, signed in nav.js:121 replaces its
   contents with .user-menu. It stretches so the dropdown falls under the bar. */
.nav-auth { display: flex; align-items: center; align-self: stretch; min-width: 0; }
.acct, .user-trigger {
  font-size: 13.5px; color: var(--muted); text-decoration: none; font-weight: 600;
  white-space: nowrap; max-width: 190px; overflow: hidden; text-overflow: ellipsis;
}
.acct:hover, .user-trigger:hover { color: var(--ink); }

/* runtime · nav.js:121–130 — the signed-in account menu. Hover opens it on a
   pointer; :focus-within is what opens it from the keyboard and on touch. */
.user-menu { position: relative; display: flex; align-items: center; align-self: stretch; min-width: 0; }
.user-trigger { font-family: inherit; background: transparent; border: 0; padding: 0; cursor: pointer; }
.user-dropdown {
  position: absolute; right: 0; top: 100%; min-width: 190px; z-index: 60;
  display: none; padding: 5px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--pop);
}
.user-menu:hover .user-dropdown, .user-menu:focus-within .user-dropdown { display: block; }
.user-dropdown a, .user-dropdown button {
  display: block; width: 100%; text-align: left; cursor: pointer;
  font: inherit; font-size: 14px; color: var(--ink); text-decoration: none;
  background: none; border: 0; padding: 8px 10px; border-radius: 7px;
}
.user-dropdown a:hover, .user-dropdown button:hover { background: var(--paper); }
.user-dropdown form { margin: 0; }

/* the theme toggle. theme.js:42 writes its aria-label and title; the icon swap
   is pure CSS so it is right at first paint, before any script runs. */
.tog {
  flex: none; width: 30px; height: 30px; border-radius: 8px;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  display: grid; place-items: center; cursor: pointer; font-size: 13px; line-height: 1;
}
.tog:hover { color: var(--ink); border-color: var(--muted); }
.tog svg { display: block; }
.tog .tt-sun { display: none; }
html[data-theme="dark"] .tog .tt-sun { display: block; }
html[data-theme="dark"] .tog .tt-moon { display: none; }

/* the mobile control (#navBurger) and the panel it opens (#navMenu).
   nav.js:73–75 toggles .open on both. */
.menubtn { display: none; gap: 7px; align-items: center; font: inherit; font-size: 13.5px; font-weight: 600;
  color: var(--ink); background: transparent; border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 11px; cursor: pointer; }
.menubtn:hover { border-color: var(--muted); }
.menupanel { display: none; border-top: 1px solid var(--line); background: var(--bar); }
.menupanel .inner { max-width: var(--col-wide); margin: 0 auto; padding: var(--s3) var(--pad-x) var(--s4); }
.menupanel a {
  display: block; text-decoration: none; color: var(--ink); font-size: 15px;
  padding: 10px 0; border-bottom: 1px solid var(--line-soft);
}
.menupanel a.active { font-weight: 660; color: var(--accent); }
/* nav.js swaps the panel's "Sign in" row for this once you are signed in — a
   POST is the only honest way to sign out, so the row becomes a form. It has to
   look exactly like the anchor it replaced. */
.menupanel .nav-signout button {
  display: block; width: 100%; text-align: left; appearance: none; border: 0;
  border-bottom: 1px solid var(--line-soft); background: none; cursor: pointer;
  color: var(--ink); font: inherit; font-size: 15px; padding: 10px 0;
}
/* the CTA is a .btn, not a menu row — win back the button's own box */
.menupanel a.btn { display: flex; width: 100%; padding: 12px 16px; border-bottom: 0; margin-top: var(--s3); }
.menupanel a.btn-accent, .menupanel a.btn-primary { color: #fff; }
html[data-theme="dark"] .menupanel a.btn-accent,
html[data-theme="dark"] .menupanel a.btn-primary { color: #14101f; }

/* ============================================================================
   4 — BUTTONS, CHIPS, CONTROLS

   .btn-primary and .btn-ghost are aliases, not duplicates: dashboard.js and
   repo.js emit them ~25 times inside innerHTML and nav-consistency.test.js
   pins them in the static nav. One rule, both names.
   ========================================================================= */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font: inherit; font-size: 14.5px; font-weight: 600; line-height: 1;
  padding: 11px 16px; border-radius: 10px; border: 1px solid transparent;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.btn-accent, .btn-primary { background: var(--accent); color: #fff; }
html[data-theme="dark"] .btn-accent,
html[data-theme="dark"] .btn-primary { color: #14101f; }
.btn-accent:hover, .btn-primary:hover { filter: brightness(1.07); }
.btn-quiet, .btn-ghost { background: var(--surface); color: var(--ink); border-color: var(--line); }
.btn-quiet:hover, .btn-ghost:hover { border-color: var(--muted); }
.btn-bare { background: transparent; color: var(--muted); border-color: var(--line); }
.btn-bare:hover { color: var(--ink); border-color: var(--muted); }
.btn-danger { background: transparent; color: var(--danger); border-color: var(--line); }
.btn-danger:hover { border-color: var(--danger); }
.btn-sm { font-size: 13px; padding: 7px 11px; border-radius: 8px; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn-block { width: 100%; }
/* the bar's CTA is small by position, not by an extra class — the nav markup is
   byte-compared and nav-consistency.test.js pins its class attribute exactly */
.bar-right .btn { font-size: 13px; padding: 7px 12px; border-radius: 8px; }

.chip, .macro-status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 620; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); white-space: nowrap; }
.chip-accent { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
/* runtime — the positive state, under both names: .chip-ok at dashboard.js:200
   (a listed action) and :415 (an installed default), .macro-status.approved at
   :601,602 (the admin users card) and repo.js:124. The bare .chip /
   .macro-status above is the neutral one (dashboard.js:512). */
.chip-ok, .macro-status.approved { color: var(--ok); border-color: currentColor; }

/* segmented control. /repo's Newest / Most installed pair is the only user.
   repo.js:186–187 sets aria-pressed on those two buttons; it used to assign
   element.className wholesale, which wiped whatever the markup had put on them
   the first time setSort() ran — and it runs at page load. Keep it an
   attribute: the state has to survive a re-sort, and aria-pressed is what a
   segmented control owes assistive tech anyway. */
.seg { display: inline-flex; gap: 4px; border: 1px solid var(--line); border-radius: 9px; padding: 3px; background: var(--surface); }
.seg button {
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  border: 1px solid transparent; background: transparent; color: var(--muted);
  padding: 4px 10px; border-radius: 7px;
}
.seg button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }

.toolbar { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; margin-top: var(--s5); }
.toolbar input[type="search"], .toolbar input[type="text"] {
  flex: 1 1 220px; min-width: 0; font: inherit; font-size: 14.5px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 9px; padding: 9px 12px;
}

kbd {
  font-family: var(--sans); font-size: 0.82em; font-weight: 600;
  border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px;
  padding: 1px 6px; background: var(--surface); color: var(--ink); white-space: nowrap;
}

/* ============================================================================
   5 — PAGE FRAME
   ========================================================================= */
main { min-width: 0; }
.col { max-width: var(--col); margin: 0 auto; padding: 0 var(--pad-x); }
.col-wide { max-width: var(--col-wide); }
/* The Action Library's grid container, and nothing else on the site. A
   reflowing catalogue of cards has no reading measure to respect, and capping
   it at --col-wide is what made the cards get NARROWER as the window got
   wider (1010px minus a growing --pad-x). Its page keeps its prose — the
   pagehead, the publish card — in .col-wide, so only the grid runs this wide. */
.col-library { max-width: var(--col-library); }

section.band { padding: var(--band) 0; border-top: 1px solid var(--line); scroll-margin-top: calc(var(--bar-h) + 8px); }
main > section.band:first-child, main > .band:first-child { border-top: 0; }

.pagehead { padding-top: 4px; }
.pagehead h1 { font-size: clamp(2.05rem, 5.1vw, 3.15rem); letter-spacing: -0.03em; }
.pagehead .lead { margin-top: var(--s4); }

.eyebrow {
  font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--muted); font-weight: 620; margin-bottom: 18px; display: block;
}
h1 { font-size: clamp(2.05rem, 5.1vw, 3.15rem); letter-spacing: -0.03em; }
h2 { font-size: clamp(1.5rem, 3.1vw, 2rem); }
h3 { font-size: 1.03rem; letter-spacing: -0.01em; }
.lead { font-size: clamp(1.02rem, 1.6vw, 1.16rem); color: var(--muted); max-width: 34em; margin-top: var(--s4); }
/* .note is the fine-print size; .acct-who is the same idea written by
   dashboard.js (×17) and repo.js (×5), and .oneline-note is the same idea with
   the exact class attribute download-links.test.js:40 pins. */
.note, .acct-who, .oneline-note { font-size: 13.5px; color: var(--muted); }
.acct-who a, .oneline-note a { color: var(--ink); text-underline-offset: 3px; }
.link-accent { color: var(--accent); font-weight: 600; text-decoration: none; font-size: 14px; }
.link-accent:hover { text-decoration: underline; text-underline-offset: 3px; }

.subnav { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-top: var(--s5); padding-bottom: var(--s3); border-bottom: 1px solid var(--line); }
.subnav a { font-size: 13.5px; color: var(--muted); text-decoration: none; }
.subnav a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }

.mt-1 { margin-top: var(--s2); }
.mt-2 { margin-top: var(--s3); }
.mt-3 { margin-top: var(--s4); }
.mt-4 { margin-top: var(--s5); }
.mt-5 { margin-top: var(--s6); }
.mt-6 { margin-top: var(--s7); }

/* scroll reveal — app.js:32–36 adds .in as a section comes into view. Landing
   only; it is opt-in, so a page without .reveal elements costs nothing. */
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .6s ease, transform .6s ease; }
.reveal.in { opacity: 1; transform: none; }

/* ============================================================================
   6 — BLOCKS
   ========================================================================= */
.card { border: 1px solid var(--line); background: var(--surface); border-radius: var(--r); padding: clamp(20px, 3vw, 26px); }
.card-head { display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; justify-content: space-between; margin-bottom: 18px; }
.card-head h2 { font-size: 1.06rem; letter-spacing: -0.01em; }
.card-head .hint { font-size: 13px; color: var(--muted); }
.card > p { color: var(--muted); font-size: 15.2px; }
.card > p + p { margin-top: var(--s3); }

.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--s4); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.duo { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: clamp(28px, 4vw, 56px); margin-top: var(--s5); }
.duo h3 { margin-bottom: var(--s2); }
.duo p { color: var(--muted); font-size: 15.2px; }
.quote { font-size: clamp(1.25rem, 2.6vw, 1.6rem); color: var(--heading); letter-spacing: -0.02em; line-height: 1.35; max-width: 20em; font-weight: 600; }

/* one row. .list > li is what the custom-actions and installed-actions cards
   emit (dashboard.js:197, 412); .macro-item is the same row in the three cards
   that still build it by hand — reports (dashboard.js:479), feedback (:518) and
   users (:599) — where the content is wrapped in two <span>s rather than the
   .ic/.nm/.chip/.ops parts, which the flex row handles as-is either way. */
.list { list-style: none; border-top: 1px solid var(--line-soft); }
.list > li, .row, .macro-item { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; justify-content: space-between; padding: 13px 0; border-bottom: 1px solid var(--line-soft); }
.macro-item { font-size: 14.8px; }
.macro-item > span:first-child { flex: 1 1 240px; min-width: 0; }
.macro-item > span:last-child { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; flex: none; }
.macro-item b { font-weight: 600; color: var(--ink); }
.ic { width: 26px; text-align: center; flex: none; font-size: 15px; }
.nm { flex: 1 1 180px; min-width: 0; }
.nm b { font-weight: 600; font-size: 14.8px; color: var(--ink); display: block; }
.nm small { color: var(--muted); font-size: 12.8px; }
.ops { display: flex; gap: 7px; flex: none; flex-wrap: wrap; }

.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); margin-top: var(--s4); }
.table { width: 100%; border-collapse: collapse; font-size: 14.6px; }
.table th, .table td { text-align: left; padding: 12px 16px; border-top: 1px solid var(--line-soft); vertical-align: top; color: var(--ink); }
.table thead th { border-top: 0; font-size: 11px; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); font-weight: 620; }
.table td.muted { color: var(--muted); }

/* label/value pairs. .acct-row is the same idea written by dashboard.js:44,
   144,145 — a flex row with .k and .v inside. Scope the dashboard's .k to
   .acct-row: .pair .k below is a different thing (an uppercase kicker). */
.deflist { display: grid; gap: 0; }
.deflist div { display: grid; grid-template-columns: 190px minmax(0,1fr); gap: 8px 18px; padding: 11px 0; border-top: 1px solid var(--line-soft); font-size: 14.8px; }
.deflist div:first-child { border-top: 0; padding-top: 0; }
.deflist dt { color: var(--muted); font-size: 13.5px; }
.deflist dd { margin: 0; color: var(--ink); }
.acct-row { display: flex; flex-wrap: wrap; gap: 4px 18px; justify-content: space-between; padding: 11px 0; border-top: 1px solid var(--line-soft); font-size: 14.8px; }
.acct-row:first-child { border-top: 0; padding-top: 0; }
.acct-row .k { color: var(--muted); font-size: 13.5px; }
.acct-row .v { color: var(--ink); text-align: right; }

/* a full action prompt, set as an object so the eye skips it as easily as a
   code block. .review-prompt is the same block on a <pre> (dashboard.js:453,
   :516), so it also has to reset the UA's <pre> margin and cap its
   height — a runtime prompt can be any length. */
.prompt, .review-prompt {
  font-family: var(--mono); font-size: 12.8px; line-height: 1.6; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line); border-radius: 9px;
  padding: 12px 14px; white-space: pre-wrap; overflow-wrap: anywhere; margin-top: var(--s3);
}
.review-prompt { margin-bottom: 0; max-height: 20em; overflow-y: auto; }
.prompt-head { font-size: 11px; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); margin-top: var(--s3); }

/* the typed settings a listing exposes, as pills. runtime · dashboard.js's
   review panel, and the Library's fieldsHtml() */
.review-fields { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: var(--s3); }
.review-fields code {
  font-family: var(--mono); font-size: 11.6px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 9px;
}

/* .dl-foot is .notice with the exact class attribute download-links.test.js:46
   pins — it may not carry a second class, so it is named in the rule instead */
.notice, .dl-foot {
  border-left: 2px solid var(--accent); background: var(--accent-soft);
  border-radius: 0 8px 8px 0; padding: 12px 16px; font-size: 14px; color: var(--muted);
}
.notice strong, .dl-foot strong { color: var(--ink); font-weight: 600; }
/* .dl-foot cannot take a .mt-* utility either — same pinned class attribute —
   so the one place it is used (the landing's download band) gets its gap here */
.dl-foot { margin-top: var(--s4); }

.prose { max-width: 40em; }
.prose h2 { font-size: 1.12rem; margin-top: var(--s6); }
.prose > *:first-child { margin-top: 0; }
.prose p { color: var(--muted); font-size: 15.4px; margin-top: var(--s3); }
.prose ul { list-style: none; margin-top: var(--s3); display: grid; gap: 8px; }
.prose li { position: relative; padding-left: 22px; color: var(--muted); font-size: 15.4px; }
.prose li::before { content: ""; position: absolute; left: 0; top: 11px; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
.prose a { color: var(--ink); text-underline-offset: 3px; }

.facts { margin-top: var(--s5); list-style: none; display: grid; gap: var(--s3); }
.facts li { position: relative; padding-left: 26px; font-size: 15.2px; color: var(--muted); }
.facts li::before { content: ""; position: absolute; left: 0; top: 11px; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
.facts a { color: var(--ink); text-underline-offset: 3px; }

.acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--s5); }
.acts span {
  border: 1px solid var(--line); background: var(--surface); border-radius: 999px;
  padding: 6px 13px; font-size: 14px; color: var(--ink);
}
.acts span.soft { color: var(--muted); border-style: dashed; background: transparent; }

.flow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: var(--s3); }
.flow .arw { color: var(--muted); font-size: 13px; }

/* form controls. .macro-input / .macro-textarea are the same controls emitted
   by dashboard.js (×20: the action editor, the typed-field rows, the admin
   billing form, the plans editor, the user filter). */
.field { display: grid; gap: 6px; margin-top: var(--s3); }
.field label { font-size: 13px; color: var(--muted); font-weight: 600; }
.field input, .field textarea, .field select, .macro-input {
  font: inherit; font-size: 14.5px; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line); border-radius: 9px;
  padding: 10px 12px; width: 100%;
}
.field input::placeholder, .field textarea::placeholder, .macro-input::placeholder { color: var(--muted); opacity: .85; }
.field textarea, .macro-textarea { min-height: 96px; resize: vertical; line-height: 1.55; font-family: var(--mono); font-size: 13.2px; }
.field-2 { display: grid; grid-template-columns: 92px minmax(0,1fr); gap: 12px; margin-top: var(--s3); }
.field-2 > .field, .settings-grid > .field { margin-top: 0; }
.settings-grid, .mac-field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--s2); }

.cmd { margin-top: var(--s3); }
.cmd .lbl { font-size: 12px; color: var(--muted); margin-bottom: 7px; }
.cmd-row { display: flex; gap: 8px; align-items: stretch; }
/* #installCmd carries an id and nothing else — download-links.test.js:34 allows
   the <code> no other attribute — so the styling has to be a descendant rule */
.cmd-row code {
  flex: 1 1 auto; min-width: 0; overflow-x: auto; white-space: nowrap;
  font-family: var(--mono); font-size: 12.5px; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 11px; line-height: 1.5;
}
/* the label flips Copy → Copied → Copy (app.js:105); a fixed floor stops the
   code block resizing under it */
.cmd-row .btn { flex: none; min-width: 5.5rem; }
.cmd .oneline-note { margin-top: var(--s2); }

/* ============================================================================
   7 — LANDING-SPECIFIC
   ========================================================================= */
.band.hero { padding-top: clamp(40px, 6vw, 76px); padding-bottom: var(--band); }
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s5); }
.hero-meta { margin-top: var(--s3); font-size: 13.5px; color: var(--muted); }

.stage {
  margin-top: clamp(36px, 4.5vw, 58px);
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start;
}
.sheet {
  grid-column: 1;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  padding: 26px 28px 30px; max-width: 560px;
  font-size: 15px; line-height: 1.7; color: var(--muted);
}
.sheet .from { font-size: 11px; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); margin-bottom: var(--s3); }
.sheet p + p { margin-top: 12px; }
.sheet mark { background: rgba(108, 58, 253, .15); color: var(--ink); border-radius: 3px; padding: 1px 0; }
html[data-theme="dark"] .sheet mark { background: rgba(167, 139, 250, .2); }

.popwrap { grid-column: 2; position: relative; z-index: 2; margin: 74px 0 0 -92px; display: flex; justify-content: flex-end; }
.stage .wlpop { font-size: clamp(12.5px, 1.15vw, 15.5px); }
.stage-cap { grid-column: 1 / -1; margin-top: 22px; font-size: 13px; color: var(--muted); max-width: 42em; }

.steps { margin-top: var(--s5); list-style: none; }
.steps li { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 18px; padding: 18px 0; border-top: 1px solid var(--line-soft); }
.steps li:first-child { border-top: 0; padding-top: 4px; }
.steps .n { font-size: 13px; font-weight: 620; color: var(--accent); font-variant-numeric: tabular-nums; padding-top: 2px; }
.steps p { color: var(--muted); font-size: 15.2px; margin-top: 5px; }

.aside-note { margin-top: var(--s5); border-left: 2px solid var(--line); padding: 2px 0 2px 18px; font-size: 14.5px; color: var(--muted); }

.pair { margin-top: var(--s4); border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); overflow: hidden; }
.pair div { padding: 14px 18px; font-size: 15px; }
.pair div + div { border-top: 1px solid var(--line-soft); }
.pair .k { display: block; font-size: 11px; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }

/* pricing. Each .plan also carries app.js's hooks — class="plan price-card"
   data-plan="free|pro", with .amt.price-amt, .quota.price-quota and
   .annual.price-annual inside — so /api/plans fills the real numbers at
   runtime and the markup's numbers are only the no-JS fallback. */
.plans { margin-top: var(--s5); border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); overflow: hidden; }
.plan { padding: 26px clamp(20px, 3vw, 30px); display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 20px 26px; align-items: start; }
.plan + .plan { border-top: 1px solid var(--line); }
.plan .amt { font-size: 1.55rem; color: var(--heading); font-weight: 640; letter-spacing: -0.02em; }
/* the plan's NAME cannot live inside .amt: app.js:51–57 replaces .price-amt's
   innerHTML wholesale ('€0' or '€9 <span>/ month</span>'), so anything else in
   there is destroyed the moment /api/plans answers. It sits beside it instead,
   on the same baseline, and reads as one line: "€0 Free" · "€9 / month · Pro". */
.plan .amt-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
/* app.js:55 writes `€9 <span>/ month</span>` into .price-amt — the bare span is
   the styling hook, and it is the same size as .amt small */
.plan .amt small, .plan .amt span, .plan .amt-row > small { font-size: 0.92rem; font-weight: 500; color: var(--muted); letter-spacing: 0; }
.plan .quota { font-size: 14px; color: var(--accent); font-weight: 600; margin-top: 4px; }
.plan .annual { font-size: 13.5px; color: var(--muted); margin-top: 2px; }
.plan ul { list-style: none; margin-top: var(--s3); display: grid; gap: 7px; }
.plan ul li { font-size: 14.6px; color: var(--muted); padding-left: 18px; position: relative; }
.plan ul li::before { content: "—"; position: absolute; left: 0; color: var(--muted); opacity: .5; }

.dl-head { display: flex; align-items: center; gap: var(--s3); }
.dl-head img { width: 46px; height: 46px; border-radius: 11px; border: 1px solid var(--line); flex: none; }
.dl-head .eyebrow { margin-bottom: 6px; }
.dl-card h3 { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.dl-card h3 span { font-size: 12.5px; font-weight: 500; color: var(--muted); letter-spacing: 0; }
.dl-card ul { list-style: none; margin: var(--s3) 0 18px; display: grid; gap: 6px; font-size: 14px; color: var(--muted); }

/* the waitlist. A live feature with a rate-limited endpoint behind it, so it
   is carried into the new design as a small, quiet block in the download area
   — never the loudest thing on the page. #wlForm/#wlEmail/#wlMsg are app.js's
   (app.js:71–76); app.js rewrites #wlMsg's className wholesale on submit, so
   every state it can be in must be styleable from .wl-msg alone. */
.waitlist { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--line-soft); }
.waitlist h3 { font-size: .95rem; }
.waitlist p { font-size: 13.5px; color: var(--muted); margin-top: 4px; }
/* the privacy link is the only thing to click in that sentence — same treatment
   as a link inside .note or .notice */
.waitlist p a { color: var(--ink); text-underline-offset: 3px; }
.wl-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--s3); }
.wl-form input {
  flex: 1 1 240px; min-width: 0; font: inherit; font-size: 14.5px; color: var(--ink);
  background: var(--paper); border: 1px solid var(--line); border-radius: 9px; padding: 10px 12px;
}
.wl-msg { font-size: 13.5px; color: var(--muted); margin-top: var(--s2); min-height: 1.3em; }
.wl-msg.ok { color: var(--ok); }
.wl-msg.err { color: var(--danger); }

/* ============================================================================
   8 — FOOTER. One, identical on every page.
   ========================================================================= */
.foot { border-top: 1px solid var(--line); padding: var(--s6) 0 var(--s7); }
.foot-links { display: flex; flex-wrap: wrap; gap: 8px 20px; list-style: none; }
.foot-links a, .foot-links button {
  font: inherit; font-size: 13.5px; color: var(--muted); text-decoration: none;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.foot-links a:hover, .foot-links button:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.foot-bot { margin-top: var(--s4); display: flex; flex-wrap: wrap; gap: 6px 20px; justify-content: space-between; font-size: 12.5px; color: var(--muted); }

/* ============================================================================
   9 — DASHBOARD

   Everything below the .dash-* geometry is markup dashboard.js writes into a
   container id at runtime. None of these class names appear in dashboard.html.
   ========================================================================= */
.dash-top { padding-top: clamp(34px, 5vw, 56px); display: flex; flex-wrap: wrap; gap: var(--s4); align-items: flex-end; justify-content: space-between; }
.dash-top .pagehead h1 { font-size: clamp(1.7rem, 3.6vw, 2.2rem); }
.dash-top .pagehead p.who { color: var(--muted); font-size: 14.5px; margin-top: var(--s2); }

/* padding-top/bottom only — a `padding` shorthand here would wipe .col's gutter */
.dash-sections { padding-top: var(--s5); padding-bottom: var(--s6); }
.dash-sections > section, .dash-sections > details { scroll-margin-top: calc(var(--bar-h) + 16px); }
.dash-sections > section + section, .dash-sections > details + section, .dash-sections > section + details { margin-top: var(--s4); }

.usage { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2); }
.usage .big { font-size: 1.7rem; font-weight: 640; color: var(--heading); letter-spacing: -0.02em; }
.usage .sub { font-size: 14px; color: var(--muted); }
/* runtime · dashboard.js:141 — <div class="acct-bar"><i style="width:N%"></i></div>,
   sandwiched between the .usage figure and the first .acct-row, so it needs air
   on both sides */
.bar-meter, .acct-bar { margin-top: var(--s3); height: 6px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.bar-meter i, .acct-bar i { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.acct-bar { margin-bottom: var(--s3); }
.row-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: var(--s4); }

/* runtime · dashboard.js:185–187 — the non-Pro state of the custom-actions card */
#macrosBody > .acct-who + .btn { margin-top: var(--s3); }

/* the action editor. runtime · dashboard.js:214–226, and the same form shape is
   reused by the admin billing card (:545) and the plans editor (:653). */
.macro-form { display: grid; gap: var(--s2); margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--line-soft); }
.macro-form > b { font-size: 14.8px; color: var(--ink); }
.macro-form-foot { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; justify-content: space-between; }
.macro-form-foot > span:last-child { display: flex; gap: 7px; flex-wrap: wrap; }

/* the disclosure that holds the typed-settings rows, and the one the feedback
   card wraps a diagnostics dump in. runtime · dashboard.js:220, :514 */
.macro-fields { margin-top: var(--s1); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
.macro-fields > summary { cursor: pointer; font-size: 13.5px; font-weight: 600; color: var(--muted); list-style: none; }
.macro-fields > summary::-webkit-details-marker { display: none; }
.macro-fields > summary::before { content: "▸ "; color: var(--accent); }
.macro-fields[open] > summary::before { content: "▾ "; }
.macro-fields > *:not(summary) { margin-top: 12px; }

/* one typed field being defined. runtime · dashboard.js:315–325 */
.mac-field { border: 1px solid var(--line); border-radius: 10px; padding: 12px; margin-bottom: var(--s2); display: grid; gap: var(--s2); background: var(--paper); }
.mac-field .mf-del { justify-self: start; }

/* the admin review queue. runtime · dashboard.js:447–453 */
.review-item { border-top: 1px solid var(--line-soft); padding: var(--s3) 0; }
.review-item:first-child { border-top: 0; }
.review-head { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; justify-content: space-between; font-size: 14.8px; }
.review-head > span:last-child { display: flex; gap: 7px; flex-wrap: wrap; }

/* the six admin cards are ordinary .cards hidden with an INLINE
   style="display:none" that dashboard.js clears with style.display = ''. Never
   hide them with a class, and never with !important — that cannot be undone
   from script and admins lose all six. This wrapper is the only thing that
   marks them as a different room of the page. */
.admin-wrap { margin-top: var(--s5); border: 1px dashed var(--line); border-radius: var(--r); padding: clamp(18px, 3vw, 24px); background: transparent; }
.admin-wrap > summary { cursor: pointer; list-style: none; display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.admin-wrap > summary::-webkit-details-marker { display: none; }
.admin-wrap > summary h2 { font-size: 1.06rem; }
.admin-wrap > summary .caret { margin-left: auto; color: var(--muted); font-size: 13px; }
.admin-wrap > summary .caret::after { content: "Expand"; }
.admin-wrap[open] > summary .caret::after { content: "Collapse"; }
.admin-wrap > .card { margin-top: var(--s4); }
/* Nothing in dashboard.js touches the WRAPPER — it only clears the inline
   display on the cards — so without this a normal account would be shown an
   empty "Administration" box. The test reads the cards' own inline attribute:
   while every card still carries style="display:none" (written exactly that
   way in dashboard.html, no space) the wrapper is hidden; the moment
   dashboard.js sets style.display = '' on one, that attribute no longer
   contains the string and the wrapper appears. Written as :not(:has(…)) so a
   browser without :has() drops the rule and simply shows the wrapper — the
   safe direction: an admin never loses the cards to an unsupported selector. */
.admin-wrap:not(:has(> .card:not([style*="display:none"]))) { display: none; }

/* ============================================================================
   10 — MODALS

   #deleteModal is driven by dashboard.js:88,92 with inline style.display =
   'flex' / 'none', and its backdrop click compares e.target === deleteModal.
   So: display:none WITHOUT !important, and .modal-box is a direct child.

   #deleteModal is the ONLY .modal on the site. There is deliberately no
   .modal.open rule: nothing sets that class, and the cookie dialog — the other
   candidate for one — is .cc-modal, built and shown by consent.js. Add the
   class rule back the day a dialog is actually opened from markup.
   ========================================================================= */
.modal { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 20px; background: rgba(10, 12, 17, .55); }
.modal-box { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 24px; max-width: 460px; width: 100%; max-height: 90vh; overflow-y: auto; }
.modal-box h2 { font-size: 1.1rem; }
.modal-box > p { color: var(--muted); font-size: 14.5px; margin-top: var(--s2); }
.modal-foot { display: flex; flex-wrap: wrap; gap: 9px; justify-content: flex-end; margin-top: var(--s4); }

/* ============================================================================
   11 — COOKIE CONSENT.  runtime · consent.js:120–178 — none of these class
   names appear in any HTML file.

   The bar is a notice, not a wall: it never covers the page's own controls and
   the site stays usable behind it. "Accept" and "Decline optional" are the
   same size, weight and footprint — only the fill colour differs — so neither
   reads as the path of least resistance (EDPB Guidelines 03/2022 on deceptive
   design). cookie-consent.test.js:116–126 reads THIS FILE to check that:
   .cc-btn must carry a min-width, and .cc-btn-accent may not mention
   font-size, padding, min-width or font-weight. Keep both rules single-block.
   ========================================================================= */
.cc-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 200;
  background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: var(--pop);
  transform: translateY(100%); transition: transform .28s ease;
}
.cc-bar.in { transform: none; }
.cc-inner {
  max-width: var(--col-wide); margin: 0 auto; padding: var(--s4) var(--pad-x);
  display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap;
}
.cc-copy { flex: 1 1 420px; min-width: 0; }
.cc-copy h2 { font-size: 1.03rem; }
.cc-copy p { margin-top: 5px; font-size: 14px; line-height: 1.55; color: var(--muted); }
.cc-copy a, .cc-fine a, .cc-list a { color: var(--ink); text-underline-offset: 3px; }
.cc-actions { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.cc-btn {
  font: inherit; font-size: 14.5px; font-weight: 600; line-height: 1;
  padding: 11px 16px; min-width: 148px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  cursor: pointer; transition: border-color .15s ease, background .15s ease;
}
.cc-btn:hover { border-color: var(--muted); }
.cc-btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
html[data-theme="dark"] .cc-btn-accent { color: #14101f; }
.cc-btn-accent:hover { filter: brightness(1.07); border-color: var(--accent); }
/* "Manage" is the only secondary action — it opens more choices, it is not one */
.cc-btn-quiet { min-width: 0; background: none; border-color: transparent; color: var(--muted); font-weight: 600; }
.cc-btn-quiet:hover { color: var(--ink); border-color: var(--line); }

.cc-modal {
  position: fixed; inset: 0; z-index: 210;
  background: rgba(10, 12, 17, .55);
  display: flex; align-items: center; justify-content: center;
  padding: 20px; overflow-y: auto;
}
.cc-panel {
  width: 100%; max-width: 560px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: var(--pop); padding: 24px;
  max-height: calc(100vh - 40px); overflow-y: auto;
}
.cc-panel h2 { font-size: 1.1rem; }
.cc-lead { margin-top: var(--s2); font-size: 14.5px; color: var(--muted); }
.cc-cat { border-top: 1px solid var(--line-soft); margin-top: var(--s4); padding-top: var(--s4); }
.cc-cat-head { display: flex; align-items: center; gap: 11px; cursor: pointer; font-size: 14.4px; color: var(--ink); }
.cc-cat-head input { width: 16px; height: 16px; flex: none; accent-color: var(--accent); cursor: pointer; }
.cc-cat-head input:disabled { cursor: default; }
.cc-cat-head b { font-weight: 600; }
.cc-cat-head em {
  font-style: normal; font-size: 11px; font-weight: 620; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px; margin-left: 6px;
}
.cc-cat p { margin: 7px 0 0 27px; font-size: 13.8px; color: var(--muted); }
.cc-list { margin: var(--s2) 0 0 27px; list-style: none; display: grid; gap: 5px; }
.cc-list li { font-size: 13px; color: var(--muted); }
.cc-list code { font-family: var(--mono); font-size: 12.2px; color: var(--ink); }
.cc-fine { border-top: 1px solid var(--line-soft); margin-top: var(--s4); padding-top: var(--s4); font-size: 13.5px; color: var(--muted); }
.cc-panel .cc-actions { justify-content: flex-end; margin-top: var(--s4); }
.cc-locked { overflow: hidden; }

/* ============================================================================
   12 — RESPONSIVE. The bar collapses to a plain menu below 880px.
   ========================================================================= */
@media (max-width: 900px) {
  .stage { grid-template-columns: minmax(0, 1fr); }
  .popwrap { grid-column: 1; min-width: 0; margin: -28px auto 0; justify-content: center; padding: 0 4px; }
  .stage .wlpop { font-size: 13.5px; }
  .sheet { max-width: none; position: relative; max-height: 220px; overflow: hidden; }
  .sheet::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 70px;
    background: linear-gradient(to top, var(--surface), rgba(255, 255, 255, 0));
  }
  html[data-theme="dark"] .sheet::after { background: linear-gradient(to top, var(--surface), rgba(19, 22, 30, 0)); }
}
/* Between the container cap and the menu breakpoint the bar has to hold every
   link at less than its comfortable width, so it tightens: smaller nav padding,
   tighter gaps, and the account label — never a link — is what gives. */
@media (max-width: 1080px) {
  .bar-in { gap: var(--s3); }
  .topnav { margin-left: 2px; flex: none; }   /* the nav row never compresses */
  .topnav a { padding: 0 7px; font-size: 14px; }
  .topnav a.active::after { left: 7px; right: 7px; }
  .topnav .sep { margin: 16px 6px; }
  .bar-right { flex: 0 1 auto; min-width: 0; }
  .nav-auth { flex: 0 1 auto; }
}
@media (max-width: 880px) {
  .topnav { display: none; }
  .bar-right .btn { display: none; }   /* the CTA moves into the panel */
  .menubtn { display: inline-flex; }
  .menupanel.open { display: block; }
  .bar-in { gap: var(--s2); }
  /* The account slot STAYS in the bar, unlike the prototype's: it is the only
     way to reach Sign out, which nav.js renders inside #navAuth and nowhere
     else. It shrinks instead of leaving. */
  .acct, .user-trigger { max-width: 9em; }
  .user-dropdown { right: 0; left: auto; }
}
@media (max-width: 620px) {
  body { font-size: 16px; }
  .stage .wlpop { font-size: 12.5px; }
  .plan { grid-template-columns: minmax(0,1fr); }
  .deflist div { grid-template-columns: minmax(0,1fr); gap: 2px; }
  .field-2 { grid-template-columns: minmax(0,1fr); }
  .dash-top { align-items: flex-start; }
  .modal-foot .btn { flex: 1 1 auto; }
  .macro-item { align-items: flex-start; }
  .cc-inner { gap: var(--s3); }
  .cc-actions { width: 100%; }
  .cc-bar .cc-btn { flex: 1 1 140px; min-width: 0; }
  .cc-bar .cc-btn-quiet { flex-basis: 100%; order: 3; }
  .cc-panel .cc-actions { justify-content: stretch; }
  .cc-panel .cc-btn { flex: 1 1 100%; }
}
@media (max-width: 400px) {
  :root { --pad-x: 16px; }
  .brand img { height: 21px; }
  .menubtn { font-size: 12.5px; padding: 6px 9px; }
  .acct, .user-trigger { max-width: 5.5em; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .reveal { opacity: 1; transform: none; }
}

/* ============================================================================
   13 — PAGE: ACTION LIBRARY  (/repo · body.page-library)

   THE PROMISE. The h1 is "Read the prompt. Then install it.", so the prompt is
   ON the card, at rest, on every listing, never behind a disclosure. The page
   is a reflowing grid of cards rather than one full-width listing per row, and
   that is what puts the promise under pressure: a dozen blocks of monospace
   side by side would be a wall of text. The card answers it by RANKING, not by
   hiding. Reading order down a card is: what it is called → what it does →
   the prompt, as quiet evidence in a mono well → how many people use it → the
   install control, at the same place on every card. Seven whole lines of
   prompt are readable without a click, which is most prompts entire; the rest
   opens in place and never on another page.

   THE MEASURE. The grid sits in .col-library (§2), not in .col-wide. Capping
   it at --col-wide is what made the cards get NARROWER as the window got
   wider — 1010px minus a --pad-x that grows with the viewport — so a 1600px
   screen showed three 283px cards in an 890px strip. .col-library is 1440px,
   which is four ~315px columns at 1440px and up, three at 1280, two at 1024,
   one below 700. The page's PROSE stays in .col-wide, so the pagehead and the
   publish card still line up with the top bar and the footer; only the
   catalogue runs wider, which is what a catalogue is for.

   EXPANDING IS LOCAL, AND NOTHING OVERLAPS. The prompt grows the card it is
   in — it is not a floating panel. It was one, absolutely positioned out of a
   fixed-height well, and the panel grew DOWN over the card's own install
   button and ate its clicks: the page's primary control, dead, on every card
   with an open prompt. Nothing here is layered over anything now. What keeps
   the growth local instead is `align-items: start` on the grid: without it a
   taller card stretches every sibling in its row, so one click moves three
   cards. With it, one click changes exactly one card, and any number of
   prompts can be open at once.

   THE CLOSED STATE IS A WHOLE NUMBER OF LINES. -webkit-line-clamp, not a
   fixed height with overflow:hidden — a height cut the (n+1)th line through
   the middle of its letters, which reads as a clipping bug rather than as
   "there is more". The clamp also makes the well CONTENT-SIZED: a three-line
   prompt gets a three-line box instead of six lines of empty bordered panel.

   repo.js emits every class below; none of them appear in repo.html.
   .repo-install / .repo-uninstall / .repo-report / .repo-default are
   event-delegation targets that render() re-binds on every pass — they are
   behaviour, never looks. The looks of the same elements come from .repo-cta
   (the full-width control in the foot) and .repo-link (the quiet text ones),
   so a rename of a behaviour hook cannot silently restyle the page.
   ========================================================================= */
/* the two places prose would otherwise run the full width of the wide column */
.page-library .notice { max-width: 48em; }
.page-library .card > p { max-width: 46em; }

/* the toolbar's own heading is a card-sized h2, not a page-sized one: it names
   the listing it sits on top of, and it must not compete with the .pagehead h1
   two blocks above it. flex:none keeps it off the search input's flex line. */
.page-library .lib-bar h2 { font-size: 1.06rem; flex: none; margin-right: var(--s2); }
/* the search input is the toolbar's only elastic part, and at four columns the
   toolbar is 1320px wide — an input that eats all of it looks like a mistake */
.page-library .lib-bar input[type="search"] { max-width: 26em; }

/* runtime · repo.js — the count / filter-result / outcome line under the
   toolbar. It reserves its own height so writing into it never nudges the grid. */
.lib-status { min-height: 1.45em; margin-top: var(--s2); display: flex; flex-wrap: wrap; gap: 2px 12px; align-items: baseline; }
.lib-status.is-err { color: var(--danger); }
.lib-clear {
  font: inherit; font-size: 13.5px; color: var(--muted); background: none;
  border: 0; padding: 0; cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
}
.lib-clear:hover { color: var(--ink); }

/* the container id is the contract — repo.js writes into it. auto-FILL, not
   auto-fit: filtering down to one match must leave one card the width of a
   card, not one card the width of the page. align-items:start is load-bearing,
   not a nicety — see EXPANDING IS LOCAL above. */
#repoGrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(310px, 100%), 1fr));
  gap: var(--s4); margin-top: var(--s3); align-items: start;
}

.repo-card {
  display: flex; flex-direction: column; min-width: 0;
  padding: clamp(16px, 2.2vw, 20px);
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* :focus-within, not only :hover — a keyboard walking the grid has to see the
   same card come forward that a pointer does */
.repo-card:hover, .repo-card:focus-within { border-color: var(--muted); box-shadow: var(--pop); }

.repo-head { display: flex; gap: 11px; align-items: flex-start; }
/* The icon is author-supplied TEXT, not an image, and it is the one thing on
   the card with no minimum mass: "•" rendered as a 3px dot in an empty space
   and read as a failed image load. A bordered tile gives every glyph the same
   footprint, and overflow:hidden is what stops a listing whose "icon" is a
   paragraph from shoving the name out of the card. */
.repo-icon {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--paper);
  font-size: 17px; line-height: 1; overflow: hidden;
}
.repo-id { min-width: 0; }
.repo-card h3 { font-size: 1rem; overflow-wrap: anywhere; }
.repo-by { font-size: 12.6px; color: var(--muted); margin-top: 3px; overflow-wrap: anywhere; }
/* "Included by default" used to be .macro-status.approved — a saturated green
   pill borrowed from the moderation UI, where green means a reviewer approved
   something. Here it out-shouted the action's own name, introduced a second
   accent with no system behind it, and cost a whole row between the title and
   the description, so one card's description started 30px below its
   neighbour's. It is a fact about the listing, so it sits on the byline with
   the other facts, in the same muted voice. */
.repo-flag { white-space: nowrap; }
/* .repo-chips survives for "Your action", which is rare and is about YOU */
.repo-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: var(--s2); }

/* The description is the fastest discriminator on a card — twelve cards share
   one silhouette, and this is the line that decides whether the prompt gets
   read at all — so it is set in --ink, not in the muted body voice it had.
   min-height reserves two lines so a row of cards does not look like rubble;
   it is NOT a clamp. It was one, and a three-line description was then cut
   mid-sentence with no expander, no title and no other route to the rest of
   it — author text with nowhere to read it, on the page that promises nothing
   is withheld. Descriptions are short; letting a long one wrap costs a card
   one line and costs the page nothing. */
.repo-card > .repo-desc {
  font-size: 14.6px; line-height: 1.5; color: var(--ink);
  margin-top: var(--s2); min-height: 3em;
}

/* the well's fill. In light, --paper against a white card is so close in value
   that the prompt read as an outline rather than as an inset object, while in
   dark it was clearly recessed — the page's whole promise should be equally
   findable in both themes. The border is the SOFT hairline: the object has to
   read, but a dozen heavy boxes were the first thing the eye landed on, ahead
   of the names. */
.page-library { --well: #eceef4; }
html[data-theme="dark"] .page-library { --well: var(--paper); }

.repo-prompt { --pp-lines: 7; margin-top: var(--s3); }
/* the label and the toggle share one line above the well, so the toggle is at
   a fixed spot on every card and never moves when the prompt opens */
.repo-prompt .prompt-head { margin-top: 0; display: flex; align-items: center; justify-content: space-between; gap: var(--s2); }
.lib-more { flex: none; text-transform: none; letter-spacing: 0; font-size: 12px; padding: 3px 9px; }
.repo-prompt .prompt {
  margin-top: 7px; background: var(--well); border-color: var(--line-soft);
  /* the closed state: whole lines only, and the box is the size of what is in
     it. -webkit-line-clamp adds its own ellipsis, which says "there is more"
     without a fade that has to be masked on and off. */
  display: -webkit-box; -webkit-line-clamp: var(--pp-lines);
  -webkit-box-orient: vertical; overflow: hidden;
}
/* open: in place, in the flow, growing its own card and nothing else. The cap
   is for the pathological prompt — 400 lines of monospace should scroll inside
   the card, not turn one listing into a page. */
.repo-prompt .prompt.is-open {
  display: block; -webkit-line-clamp: unset;
  max-height: min(34em, 62vh); overflow-y: auto;
  border-color: var(--line);
}
.repo-prompt .review-fields { margin-top: var(--s2); gap: 6px 7px; }
/* the fields were `${label} · ${type}` straight off the manifest — "Length ·
   select" is a data dump, not a sentence. The row says what it means now, and
   the type is on the pill's title where it costs no width. */
.rf-label { font-size: 11.6px; color: var(--muted); }

/* margin-top:auto is what pins the foot to the bottom of a card that has spare
   height; with align-items:start on the grid most cards have none, and it
   costs nothing when they do not. */
.repo-foot { margin-top: auto; padding-top: var(--s4); display: flex; flex-direction: column; gap: var(--s2); }
/* the numbers, and the report control that belongs beside them rather than in
   a row of its own under the card's primary action */
.repo-metrics { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px var(--s2); }
.repo-stat { font-size: 12.6px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* the install count is the one genuinely differentiating number on a card and
   it was the smallest, dimmest text on it */
.repo-stat b { color: var(--ink); font-weight: 620; }

/* THE INSTALL CONTROL, and the reason it is full width. Its label changes when
   you install ("Install" → "✓ Installed — remove"), and on one line beside the
   stats a longer label wrapped the foot onto a second line: every card in the
   row jumped ~29px at the moment of the click. A control that owns its own
   line has the same geometry whatever it says. It is also the page's stated
   primary action — half of the h1 — and it used to be the quietest button on
   the page while a purple "Sign in to write one" sat in the grid beside it. */
.repo-cta { width: 100%; }
.repo-cta:hover, .repo-cta:focus-visible { border-color: var(--accent); color: var(--accent); }
.repo-cta.is-installed { color: var(--ok); border-color: var(--line); }
.repo-cta.is-installed:hover, .repo-cta.is-installed:focus-visible { color: var(--danger); border-color: var(--danger); }

/* the quiet text controls. Buttons, not href="#" anchors: neither of them
   navigates, and a dead hash on a failed handler jumped the page to the top. */
.repo-link {
  font: inherit; font-size: 12.6px; color: var(--muted); background: none;
  border: 0; padding: 0; cursor: pointer; text-decoration: none;
}
.repo-link:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
/* "Make default for all users" changes what EVERY user of the product sees.
   It was styled identically to "Report this action" — same size, same grey,
   same row — with a gap as the only thing between them. A mutation with that
   blast radius gets a border and the accent, and its own line. */
.repo-admin {
  font: inherit; font-size: 12.4px; font-weight: 600; color: var(--accent);
  background: none; border: 1px dashed var(--accent); border-radius: 8px;
  padding: 6px 10px; cursor: pointer; width: 100%;
}
.repo-admin:hover { background: var(--accent-soft); }

/* the empty and the error state are the same paragraph, and it owns the row */
.repo-empty { grid-column: 1 / -1; padding: var(--s5) 0; }
.repo-empty .lib-clear { margin-left: 6px; }

@media (max-width: 700px) {
  /* one column, so the card is wide and a prompt needs fewer lines to be whole */
  .repo-prompt { --pp-lines: 6; }
  /* no neighbour left to line the cards up with, so the reserved second
     description line is only a gap */
  .repo-card > .repo-desc { min-height: 0; }
}

/* ============================================================================
   14 — PAGE: SIGN IN  (/login · body.page-signin)
   The smallest page in the site: one card, centred in the band, nothing else.
   ========================================================================= */
.page-signin .signin {
  display: grid; align-content: center;
  /* centre the card instead of stacking it under a --band gap, and stop short
     enough that the shared footer still lands on a normal screen */
  padding-top: var(--s7); padding-bottom: var(--s7);
  min-height: calc(100vh - var(--bar-h) - 240px);
}
.page-signin .signin-card { max-width: 430px; margin: 0 auto; }
/* same step-down .dash-top makes: a display h1 is wrong inside a 430px card */
.page-signin .pagehead h1 { font-size: clamp(1.5rem, 4vw, 1.85rem); letter-spacing: -0.022em; }
.page-signin .pagehead .lead { margin-top: var(--s2); font-size: 15.2px; }
/* the arriving-from-a-redirect note: a quiet .notice, not an alarm. The inline
   script fills #loginNote for ?login=required and adds .err for a failure, so
   the element is empty on a normal visit and must show nothing at all then. */
#loginNote {
  margin-top: var(--s4); font-size: 13.8px; color: var(--muted);
  border-left: 2px solid var(--accent); background: var(--accent-soft);
  border-radius: 0 8px 8px 0; padding: 12px 16px;
}
#loginNote:empty { display: none; }
#loginNote.err { border-left-color: var(--danger); color: var(--danger); background: transparent; }
.page-signin .choices { display: grid; gap: var(--s2); margin-top: var(--s5); }
.page-signin .choices .btn { padding-top: 13px; padding-bottom: 13px; }
.page-signin .mark { width: 18px; height: 18px; flex: none; }
.page-signin .fine { margin-top: var(--s4); line-height: 1.55; }

/* ============================================================================
   15 — PAGES: PRIVACY · TERMS · IMPRESSUM
   The on-screen use of .prose and .table-wrap/.table, plus .deflist off the
   dashboard. Nothing new but the few pieces of geometry below.
   ========================================================================= */
/* the closing row of cross-links at the foot of each legal page wears the
   footer's own link styling (.foot-links); this only places the row */
.lgl-links { align-items: center; margin-top: var(--s6); padding-top: var(--s4); border-top: 1px solid var(--line); }
/* links inside a .notice earn the ink colour — they are the only thing to click */
.notice a { color: var(--ink); text-underline-offset: 3px; }
/* .facts follows a .card-head here, which already carries the gap */
.page-privacy .card .facts { margin-top: 0; }
/* the cookie table is part of the reading column, so it holds the .prose
   measure instead of running out to the full .col width */
.page-privacy .table-wrap, .page-privacy .cookie-note { max-width: 40em; }
/* cookie names read as identifiers, not prose */
.page-privacy .table td.cname { font-family: var(--mono); font-size: 12.6px; white-space: nowrap; }
/* consent.js:126,170 deep-links to /privacy#cookies from the cookie bar and the
   cookie dialog. Without this the sticky bar lands on top of the heading it
   just jumped to — .band's own scroll-margin cannot reach an id inside it. */
.page-privacy #cookies { scroll-margin-top: calc(var(--bar-h) + 16px); }
/* a "Cookie settings" button sitting inside a sentence, drawn as .link-accent —
   this only strips the button chrome; the colour and size come from that class */
.lgl-inline { font-family: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
/* the Impressum's field labels carry a German term and an English gloss */
.page-impressum .deflist > div { grid-template-columns: 232px minmax(0, 1fr); align-items: start; }
.page-impressum .deflist dt { line-height: 1.4; }
/* the gloss is real text, not decoration — it holds the full --muted colour so
   it clears 4.5:1 on --paper in both themes (an earlier opacity took it to 3.2) */
.page-impressum .deflist dt span { display: block; color: var(--muted); font-size: 12.5px; }
@media (max-width: 620px) {
  .page-impressum .deflist > div { grid-template-columns: minmax(0, 1fr); }
}
