/* BlueWorkz design system
 * ============================================================================
 * Built from the existing brand pack, not invented: Blue #0E52C9, dark-UI blue
 * #7B92FF, Ink #0F1626, Paper #FFFFFF. The mark is "the Hub" and its usage
 * rules are enforced here where CSS can enforce them (clear space, minimum
 * size, the light/dark variant swap) and stated on /styleguide/ where it
 * cannot.
 *
 * Conventions
 * -----------
 *   - Every class is prefixed `bwz-`. Block, `__element`, `--modifier`.
 *   - Light and dark are both first class. Nothing is "the dark override".
 *   - Brand hexes appear exactly once, in §1, as --bwz-brand-*. Everything
 *     else refers to a *role* token, so a component never names a colour.
 *   - Every foreground/background pair in §2 is checked against WCAG AA by
 *     core/design.py, and core/tests.py fails the build if one drops below.
 *
 * Targets: 320px minimum width, desktop and mobile parity, current and
 * previous major of Chrome, Safari, Firefox and Edge. No CSS nesting, no
 * :has(), no container queries — all four browsers support them at current,
 * not all at previous-major.
 * ============================================================================ */

/* ---------------------------------------------------------------------------
 * 1. Brand constants — the same in every theme, because they are the brand
 * ------------------------------------------------------------------------- */

:root {
  --bwz-brand-blue: #0E52C9;
  --bwz-brand-blue-dark-ui: #7B92FF;
  --bwz-brand-ink: #0F1626;
  --bwz-brand-paper: #FFFFFF;

  /* [CRITICAL] Windows PowerShell's own chrome, for the shell buttons in §24
   * (869f03448, Dan 2026-09-14: "I'd like these ssh links to be buttons, with
   * the buttons looking like tiny powershell windows").
   *
   * They live HERE, as named constants, rather than as hexes inside the
   * component — CLAUDE.md §6 — and they are deliberately theme-independent,
   * which the rest of this file's colours are not. Two reasons. A terminal
   * window that goes white in light mode is a picture of nothing, and on this
   * dashboard colour means "this check is failing", so the one thing a
   * declared alias must never wear is a palette that can be read as a status.
   * A button that looks the same in both themes cannot be mistaken for one.
   *
   * Not BlueWorkz's palette, so core/design.py does not document them and the
   * contrast sweep there does not cover them: it is a sweep of ROLE tokens,
   * and these are a reproduction. The two pairs that matter are measured
   * anyway — screen-fg on screen is 16.5:1 and chrome-fg on chrome is 11.5:1,
   * both well past AA — and core/tests.py asserts it so a "tidy-up" of these
   * five values cannot quietly drop below it. */
  --bwz-pwsh-chrome: #2B2B2B;
  --bwz-pwsh-chrome-fg: #E8E8E8;
  --bwz-pwsh-icon: #1E4FA1;
  --bwz-pwsh-screen: #000000;
  --bwz-pwsh-screen-fg: #F2E5A8;
}

/* ---------------------------------------------------------------------------
 * 2. Role tokens — light is the default, and is defined on bare :root so that
 *    no colour has its only definition inside a media query.
 * ------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Surfaces, back to front */
  --bwz-bg: #FFFFFF;
  --bwz-surface: #FFFFFF;
  --bwz-surface-2: #F4F6FB;
  --bwz-surface-3: #E9EEF7;

  /* Text */
  --bwz-fg: #0F1626;
  --bwz-fg-muted: #4A5568;
  --bwz-fg-subtle: #5C6880;

  /* Lines */
  --bwz-rule: #DDE3ED;
  --bwz-rule-strong: #B9C3D4;
  --bwz-field-border: #767F94;   /* 4.0:1 on paper — SC 1.4.11 wants 3:1 */

  /* Accent */
  --bwz-accent: #0E52C9;
  --bwz-accent-hover: #0B41A1;
  --bwz-accent-active: #083079;
  --bwz-accent-fg: #FFFFFF;
  --bwz-accent-surface: #EAF0FD;

  /* Illustration hues — the hero drawings in section 10c, and NOTHING else.
   * Dan, 2026-09-11 (869ezr12u): "Blueprint is the closest to being right, but
   * I would like something MUCH more complex, more colourful."
   *
   * [CRITICAL] These are read only inside section 10c, and a test fails the
   * build if one is read anywhere else. Three of them are a green, an amber
   * and a rose, and on /dashboard/ those three colours mean "this check is
   * failing" — a drawing hue that leaked onto a tile would be a monitoring
   * defect wearing an illustration. They are hue names on purpose, never
   * status names, so nothing here can be mistaken for --bwz-ok.
   *
   * They invert like everything else, and they have to: the hero gradient runs
   * Blue -> Ink under white in light and dark-UI-blue -> Paper-ish under
   * near-black in dark, so the ground a drawing sits on is DARK in the light
   * theme and LIGHT in the dark one. Light values are therefore bright and
   * dark values are deep. Every one clears 3:1 against BOTH ends of the
   * gradient in its own theme, and core/design.py grades all ten pairs. */
  --bwz-illus-cyan: #67E8F9;
  --bwz-illus-green: #86EFAC;
  --bwz-illus-amber: #FCD34D;
  --bwz-illus-rose: #FDA4AF;
  --bwz-illus-violet: #C4B5FD;

  /* The trend series (869f58teh, Dan 2026-09-22: "make them look nicer! use
   * colours"). ONE INK PER RESOURCE, and [CRITICAL] never a status: a trend
   * line says WHICH resource it is, never how that resource is doing.
   * ops_resourcesample stores no status, so a graded line would be today's
   * thresholds applied to an old reading -- see 26. Kept well clear of the
   * green/amber/red family for exactly that reason, and graded in
   * core/design.py against both grounds a chart is ever drawn on. */
  --bwz-series-cpu: #1D4ED8;
  --bwz-series-memory: #6D28D9;
  --bwz-series-disk: #0E7490;
  --bwz-series-swap: #A21CAF;

  /* Status — the dashboard is a traffic light, so these are load-bearing */
  --bwz-ok: #0F7040;
  --bwz-ok-surface: #E6F4EC;
  --bwz-warn: #8A5300;
  --bwz-warn-surface: #FCF1DE;
  --bwz-danger: #B3251C;
  --bwz-danger-surface: #FCEBE9;

  /* A tile for somebody else's logo. Paper in BOTH themes, on purpose: a
   * partner mark is supplied in one colourway and may not be recoloured,
   * inverted or filtered, so the ground moves to it rather than the other way
   * round. The precedent is the brand pack's own instruction for putting a
   * lockup on a dark ground — it sits on its own Paper tile. */
  --bwz-logo-tile: #FFFFFF;
  --bwz-logo-tile-rule: #DDE3ED;

  /* Elevation. Deliberately restrained: the brand is flat. */
  --bwz-shadow-sm: 0 1px 2px rgba(15, 22, 38, 0.06);
  --bwz-shadow-md: 0 2px 8px rgba(15, 22, 38, 0.08);
  --bwz-shadow-lg: 0 12px 32px rgba(15, 22, 38, 0.12);
  --bwz-overlay: rgba(15, 22, 38, 0.55);
}

/* Dark, by system preference — unless the reader has explicitly chosen light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-bwz-theme="light"]) {
    color-scheme: dark;

    --bwz-bg: #0F1626;
    --bwz-surface: #161F33;
    --bwz-surface-2: #1C2740;
    --bwz-surface-3: #24314E;

    --bwz-fg: #EEF1F7;
    --bwz-fg-muted: #A9B4C8;
    --bwz-fg-subtle: #8F9BB1;

    --bwz-rule: #2A3853;
    --bwz-rule-strong: #3D4E70;
    --bwz-field-border: #68799F;

    --bwz-accent: #7B92FF;
    --bwz-accent-hover: #9EAFFF;
    --bwz-accent-active: #B9C5FF;
    --bwz-accent-fg: #0B1020;
    --bwz-accent-surface: #1A2444;

    /* Deep, because in the dark theme the hero gradient is the LIGHT ground.
     * See the note beside the light values. */
    --bwz-illus-cyan: #0B3A47;
    --bwz-illus-green: #0E4429;
    --bwz-illus-amber: #6B3005;
    --bwz-illus-rose: #7A0F2E;
    --bwz-illus-violet: #4C1D95;

    /* The trend series. Light on the dark ground, for the same reason every
     * other pair here inverts: a line is only a line if it can be seen. */
    --bwz-series-cpu: #93C5FD;
    --bwz-series-memory: #C4B5FD;
    --bwz-series-disk: #67E8F9;
    --bwz-series-swap: #F0ABFC;

    --bwz-ok: #4FC98C;
    --bwz-ok-surface: #12301F;
    --bwz-warn: #E9B159;
    --bwz-warn-surface: #33260F;
    --bwz-danger: #FF8B82;
    --bwz-danger-surface: #3A1B18;

    /* Unchanged in the dark theme. See the note beside the light value. */
    --bwz-logo-tile: #FFFFFF;
    --bwz-logo-tile-rule: #DDE3ED;

    --bwz-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --bwz-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.45);
    --bwz-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
    --bwz-overlay: rgba(4, 8, 16, 0.7);
  }
}

/* Dark, chosen explicitly. Repeated rather than shared so the toggle wins in
 * both directions: a reader on a light system can still pick dark. */
:root[data-bwz-theme="dark"] {
  color-scheme: dark;

  --bwz-bg: #0F1626;
  --bwz-surface: #161F33;
  --bwz-surface-2: #1C2740;
  --bwz-surface-3: #24314E;

  --bwz-fg: #EEF1F7;
  --bwz-fg-muted: #A9B4C8;
  --bwz-fg-subtle: #8F9BB1;

  --bwz-rule: #2A3853;
  --bwz-rule-strong: #3D4E70;
  --bwz-field-border: #68799F;

  --bwz-accent: #7B92FF;
  --bwz-accent-hover: #9EAFFF;
  --bwz-accent-active: #B9C5FF;
  --bwz-accent-fg: #0B1020;
  --bwz-accent-surface: #1A2444;

  /* Deep, because in the dark theme the hero gradient is the LIGHT ground.
   * See the note beside the light values. */
  --bwz-illus-cyan: #0B3A47;
  --bwz-illus-green: #0E4429;
  --bwz-illus-amber: #6B3005;
  --bwz-illus-rose: #7A0F2E;
  --bwz-illus-violet: #4C1D95;

  /* The trend series. Light on the dark ground, for the same reason every
   * other pair here inverts: a line is only a line if it can be seen. */
  --bwz-series-cpu: #93C5FD;
  --bwz-series-memory: #C4B5FD;
  --bwz-series-disk: #67E8F9;
  --bwz-series-swap: #F0ABFC;

  --bwz-ok: #4FC98C;
  --bwz-ok-surface: #12301F;
  --bwz-warn: #E9B159;
  --bwz-warn-surface: #33260F;
  --bwz-danger: #FF8B82;
  --bwz-danger-surface: #3A1B18;

  /* Unchanged in the dark theme. See the note beside the light value. */
  --bwz-logo-tile: #FFFFFF;
  --bwz-logo-tile-rule: #DDE3ED;

  --bwz-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --bwz-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.45);
  --bwz-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
  --bwz-overlay: rgba(4, 8, 16, 0.7);
}

/* ---------------------------------------------------------------------------
 * 3. Type, space and shape — theme-independent
 * ------------------------------------------------------------------------- */

:root {
  --bwz-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --bwz-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  /* Type scale. Body is fixed; headings are fluid so that a 320px phone and a
   * 1440px desktop both get a sensible measure without a breakpoint each. */
  --bwz-text-xs: 0.75rem;      /* 12px — meta, table captions            */
  --bwz-text-sm: 0.875rem;     /* 14px — helper text, badges             */
  --bwz-text-base: 1rem;       /* 16px — body. Never smaller on mobile.  */
  --bwz-text-md: 1.125rem;     /* 18px — lede                            */
  --bwz-text-lg: clamp(1.25rem, 0.5vw + 1.15rem, 1.375rem);
  --bwz-text-xl: clamp(1.5rem, 1vw + 1.3rem, 1.75rem);
  --bwz-text-2xl: clamp(1.75rem, 2vw + 1.4rem, 2.25rem);
  --bwz-text-3xl: clamp(2rem, 4vw + 1.25rem, 3rem);

  --bwz-leading-tight: 1.15;
  --bwz-leading-snug: 1.35;
  --bwz-leading-normal: 1.6;

  --bwz-weight-normal: 400;
  --bwz-weight-medium: 500;
  --bwz-weight-semibold: 600;
  --bwz-weight-bold: 700;

  --bwz-tracking-tight: -0.02em;
  --bwz-tracking-normal: 0;
  --bwz-tracking-wide: 0.08em;

  /* Spacing — a 4px base, named by step, not by intent. */
  --bwz-space-1: 0.25rem;
  --bwz-space-2: 0.5rem;
  --bwz-space-3: 0.75rem;
  --bwz-space-4: 1rem;
  --bwz-space-5: 1.5rem;
  --bwz-space-6: 2rem;
  --bwz-space-7: 3rem;
  --bwz-space-8: 4rem;
  --bwz-space-9: 6rem;

  --bwz-radius-sm: 4px;
  --bwz-radius-md: 8px;
  --bwz-radius-lg: 14px;
  --bwz-radius-full: 999px;

  --bwz-border: 1px;
  --bwz-border-thick: 2px;

  /* Layout.
   *
   * Widened on Dan's instruction, ticket 869evwwzn, 2026-09-07: "All the
   * content is constrained to a width which is too narrow — on wider screens,
   * make it flow wider if the window is wider."
   *
   * The container went 68rem -> 96rem, so a card grid, a picture and the
   * header now use a 1600px screen instead of leaving a third of it empty.
   * The reading measure did NOT double with it, and that is deliberate: line
   * length is the one width where more is worse. 34rem is about 68 characters
   * and 40rem is about 80, which is the top of the readable band; twice 34rem
   * would be roughly 135 characters a line and a reader loses their place
   * returning to the left edge. So the page got wider and the paragraph got a
   * little wider — the empty space a wide window used to show is now filled by
   * the things that can use it. */
  --bwz-container: 96rem;
  --bwz-container-narrow: 52rem;
  --bwz-container-wide: 112rem;
  --bwz-measure: 40rem;      /* ~80 characters — the top of the readable band */
  /* [DAN'S DECISION, and it is his second time asking] 2026-09-09, 869evwx03:
   * "make the content area twice as wide, if the browser window is wide enough
   * to support it". The last session widened the container and deliberately did
   * NOT widen the reading measure, on the argument recorded above; he has now
   * repeated the instruction having seen that result, so the measure doubles.
   *
   * It doubles only where the clause in his sentence is satisfied — at 90em and
   * up, which is a window of about 1440px. Below that nothing moves at all, so
   * every phone, tablet and laptop keeps the 40rem measure. And it applies to
   * the marketing pages only (section 10c), never to a form, the dashboard or
   * the questionnaire, where a 1280px-wide text field is a defect. */
  --bwz-measure-wide: 80rem;
  --bwz-gutter: 1rem;

  /* The Hub mark: the centre circle is r=8 in a 48 viewBox, so its width is
   * exactly one third of the mark. That is the brand pack's clear space. */
  --bwz-mark-size: 2rem;
  --bwz-mark-clearspace: calc(var(--bwz-mark-size) / 3);
  --bwz-mark-min: 20px;      /* below this, use the favicon variant */

  --bwz-duration: 140ms;
  --bwz-ease: cubic-bezier(0.2, 0, 0.2, 1);

  --bwz-z-header: 100;
  --bwz-z-overlay: 200;
}

@media (min-width: 40em) {
  :root { --bwz-gutter: 1.5rem; }
}

/* A 96rem container on a 1600px screen has almost no margin left. The gutter
 * grows once there is room for it, which is the one place a breakpoint earns
 * its keep: it changes a spacing value, not a layout. */
@media (min-width: 90em) {
  :root { --bwz-gutter: 2.5rem; }
}

/* ---------------------------------------------------------------------------
 * 4. Reset and element defaults
 * ------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd, ul, ol { margin: 0; }

ul[class], ol[class] { padding: 0; list-style: none; }

img, svg, video, canvas { display: block; max-width: 100%; }
img, svg { height: auto; }

input, button, textarea, select { font: inherit; color: inherit; }

.bwz-body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bwz-bg);
  color: var(--bwz-fg);
  font-family: var(--bwz-font-sans);
  font-size: var(--bwz-text-base);
  line-height: var(--bwz-leading-normal);
  -webkit-font-smoothing: antialiased;
  /* An overflowing table or code block must scroll inside its own box, never
   * push the page sideways. At 320px that is the difference between usable
   * and not. */
  overflow-x: hidden;
}

:focus-visible {
  outline: var(--bwz-border-thick) solid var(--bwz-accent);
  outline-offset: 2px;
  border-radius: var(--bwz-radius-sm);
}

::selection {
  background: var(--bwz-accent);
  color: var(--bwz-accent-fg);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------------------------
 * 5. Layout primitives
 * ------------------------------------------------------------------------- */

.bwz-container {
  width: 100%;
  max-width: var(--bwz-container);
  margin-inline: auto;
  padding-inline: var(--bwz-gutter);
}
.bwz-container--narrow { max-width: var(--bwz-container-narrow); }
.bwz-container--wide { max-width: var(--bwz-container-wide); }

.bwz-main { flex: 1 0 auto; }

.bwz-section { padding-block: var(--bwz-space-7); }
.bwz-section--tight { padding-block: var(--bwz-space-5); }
.bwz-section--loose { padding-block: clamp(2.5rem, 8vw, 5.5rem); }
.bwz-section--tinted { background: var(--bwz-surface-2); }

/* A framed, separate document -- today the one thing that is one: the clickable
 * prototype captured off demo.blueworkz.com (869f0ce5k). An <iframe> and not an
 * inline include, because that capture carries its own :root custom properties,
 * its own `body` rule and its own prefers-color-scheme block; inlined it would
 * redefine this stylesheet's tokens on a page that also serves the dashboard,
 * where colour means "this check is failing".
 *
 * Height is viewport-relative rather than aspect-ratio: the frame holds a wide
 * application interface, not a video, so what matters is how much of it is
 * visible at once. 70vh with a floor keeps it usable at 320px, where the
 * prototype's own max-width:780px rules take over inside the frame.
 */
.bwz-embed {
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-md);
  background: var(--bwz-surface);
  overflow: hidden;
}
.bwz-embed iframe {
  display: block;
  width: 100%;
  height: max(28rem, 70vh);
  border: 0;
}

/* Vertical rhythm without margin arithmetic in every component. */
.bwz-stack > * + * { margin-top: var(--bwz-space-4); }
.bwz-stack--tight > * + * { margin-top: var(--bwz-space-2); }
.bwz-stack--loose > * + * { margin-top: var(--bwz-space-6); }

.bwz-cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-3);
}
.bwz-cluster--between { justify-content: space-between; }

/* auto-fit, so it collapses to one column at 320px with no breakpoint. */
.bwz-grid {
  display: grid;
  gap: var(--bwz-space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.bwz-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); }

.bwz-measure { max-width: var(--bwz-measure); }
.bwz-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * 6. Typography
 * ------------------------------------------------------------------------- */

.bwz-h1, .bwz-h2, .bwz-h3, .bwz-h4 {
  font-weight: var(--bwz-weight-bold);
  letter-spacing: var(--bwz-tracking-tight);
  line-height: var(--bwz-leading-tight);
  text-wrap: balance;
}
.bwz-h1 { font-size: var(--bwz-text-3xl); max-width: 22ch; }
.bwz-h2 { font-size: var(--bwz-text-2xl); }
.bwz-h3 { font-size: var(--bwz-text-xl); line-height: var(--bwz-leading-snug); }
.bwz-h4 {
  font-size: var(--bwz-text-lg);
  font-weight: var(--bwz-weight-semibold);
  line-height: var(--bwz-leading-snug);
}

.bwz-eyebrow {
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-semibold);
  letter-spacing: var(--bwz-tracking-wide);
  text-transform: uppercase;
  color: var(--bwz-accent);
}

.bwz-lede {
  font-size: var(--bwz-text-md);
  max-width: var(--bwz-measure);
}
.bwz-prose { max-width: var(--bwz-measure); }
.bwz-prose > * + * { margin-top: var(--bwz-space-4); }
.bwz-muted { color: var(--bwz-fg-muted); }
.bwz-subtle { color: var(--bwz-fg-subtle); font-size: var(--bwz-text-sm); }
.bwz-meta {
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-subtle);
  font-variant-numeric: tabular-nums;
}

.bwz-code {
  font-family: var(--bwz-font-mono);
  font-size: 0.9375em;
  background: var(--bwz-surface-3);
  border-radius: var(--bwz-radius-sm);
  padding: 0.1em 0.35em;
}

/* [CRITICAL] The type selector is load-bearing, and it is a fix rather than a
 * flourish. The reset in section 4 says `ul[class], ol[class] { padding: 0;
 * list-style: none; }` -- which is the right default, because most lists on
 * this site are navigation. But `ol[class]` is (0,1,1) and a bare `.bwz-list`
 * is (0,1,0), so the reset **won**, and every bullet and every step number on
 * this site had silently rendered as an unmarked line since the stylesheet was
 * written. Nobody saw it because nothing had used a bulleted list in anger
 * until Dan asked for one on 2026-09-09 (869evwx03) -- at which point three
 * cards on /what-we-do/ came out as run-on lines with no markers.
 *
 * `ul.bwz-list` ties the reset on specificity and wins on source order.
 *
 * ⚠️ `--plain` has to be raised WITH it, and the first attempt got this wrong
 * on the reasoning that `--plain` is used on its own. It is not: every one of
 * its four uses is `class="bwz-list bwz-list--plain"`, so raising only the base
 * put bullets on the site-wide footer navigation. Caught by looking at a
 * screenshot of the footer, one deploy later. Both are (0,1,1) and `--plain`
 * comes second, which is what makes the modifier win.
 * `core/tests.py::test_a_bulleted_list_actually_gets_its_bullets` pins both. */
ul.bwz-list,
ol.bwz-list {
  padding-left: 1.25em;
  list-style: disc;
}
.bwz-list > li + li { margin-top: var(--bwz-space-2); }
ol.bwz-list--ordered { list-style: decimal; }
ul.bwz-list--plain,
ol.bwz-list--plain { padding-left: 0; list-style: none; }

/* ---------------------------------------------------------------------------
 * 7. Links
 * ------------------------------------------------------------------------- */

a { color: var(--bwz-accent); }

.bwz-link {
  color: var(--bwz-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
  border-radius: var(--bwz-radius-sm);
}
.bwz-link:hover { color: var(--bwz-accent-hover); text-decoration-thickness: 2px; }
.bwz-link:active { color: var(--bwz-accent-active); }

/* For links inside a nav or a card, where the underline is noise. It still
 * underlines on hover and focus, so it is never colour-alone. */
.bwz-link--quiet { text-decoration: none; }
.bwz-link--quiet:hover,
.bwz-link--quiet:focus-visible { text-decoration: underline; }

.bwz-link--plain { color: inherit; text-decoration: none; }
.bwz-link--plain:hover { color: var(--bwz-accent); text-decoration: underline; }

/* Keyboard users reach the content without tabbing the whole header. */
.bwz-skip {
  position: absolute;
  left: -9999px;
  top: var(--bwz-space-2);
  z-index: var(--bwz-z-overlay);
  background: var(--bwz-accent);
  color: var(--bwz-accent-fg);
  padding: var(--bwz-space-2) var(--bwz-space-4);
  border-radius: var(--bwz-radius-md);
  text-decoration: none;
}
.bwz-skip:focus { left: var(--bwz-space-2); }

/* ---------------------------------------------------------------------------
 * 8. The mark and the lockups
 *
 * The brand pack forbids recolouring, gradients, stretching, rotation, an
 * added outline and a drop shadow. There is therefore no modifier here that
 * does any of those, and nothing sets `filter` or `transform` on a mark.
 * ------------------------------------------------------------------------- */

.bwz-mark {
  display: block;
  width: var(--bwz-mark-size);
  height: var(--bwz-mark-size);
  min-width: var(--bwz-mark-min);
  min-height: var(--bwz-mark-min);
  flex: none;
}
.bwz-mark--sm { --bwz-mark-size: 1.5rem; }
.bwz-mark--lg { --bwz-mark-size: 3rem; }
.bwz-mark--xl { --bwz-mark-size: 5rem; }

/* Clear space: at least the width of the centre circle, all the way round. */
.bwz-mark--clearspace {
  box-sizing: content-box;
  padding: var(--bwz-mark-clearspace);
}

.bwz-lockup { display: block; height: auto; }
.bwz-lockup--h { width: min(16.25rem, 100%); }
.bwz-lockup--stacked { width: min(12.5rem, 100%); }

/* The colour mark is solid #0E52C9 and goes muddy on the dark ground, so the
 * white variant is swapped in by CSS rather than by script. All three theme
 * states are handled: system-light, system-dark, and an explicit choice. */
.bwz-mark--on-dark { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-bwz-theme="light"]) .bwz-mark--on-light { display: none; }
  :root:not([data-bwz-theme="light"]) .bwz-mark--on-dark { display: block; }
}
:root[data-bwz-theme="dark"] .bwz-mark--on-light { display: none; }
:root[data-bwz-theme="dark"] .bwz-mark--on-dark { display: block; }

.bwz-logo {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-3);
  text-decoration: none;
  color: inherit;
  border-radius: var(--bwz-radius-md);
}
.bwz-logo__word {
  font-weight: var(--bwz-weight-bold);
  font-size: var(--bwz-text-md);
  letter-spacing: var(--bwz-tracking-tight);
  white-space: nowrap;
}
.bwz-logo__word b { color: var(--bwz-accent); font-weight: inherit; }

/* ---------------------------------------------------------------------------
 * 9. Buttons
 * ------------------------------------------------------------------------- */

.bwz-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--bwz-space-2);
  min-height: 44px;                 /* a real touch target at 320px */
  padding: var(--bwz-space-2) var(--bwz-space-5);
  border: var(--bwz-border-thick) solid transparent;
  border-radius: var(--bwz-radius-md);
  background: transparent;
  color: var(--bwz-fg);
  font-size: var(--bwz-text-base);
  font-weight: var(--bwz-weight-semibold);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--bwz-duration) var(--bwz-ease),
              border-color var(--bwz-duration) var(--bwz-ease),
              color var(--bwz-duration) var(--bwz-ease);
}

.bwz-btn--primary {
  background: var(--bwz-accent);
  border-color: var(--bwz-accent);
  color: var(--bwz-accent-fg);
}
.bwz-btn--primary:hover { background: var(--bwz-accent-hover); border-color: var(--bwz-accent-hover); }
.bwz-btn--primary:active { background: var(--bwz-accent-active); border-color: var(--bwz-accent-active); }

.bwz-btn--secondary {
  border-color: var(--bwz-accent);
  color: var(--bwz-accent);
}
.bwz-btn--secondary:hover { background: var(--bwz-accent-surface); color: var(--bwz-accent-hover); border-color: var(--bwz-accent-hover); }

.bwz-btn--ghost { color: var(--bwz-fg-muted); }
.bwz-btn--ghost:hover { background: var(--bwz-surface-2); color: var(--bwz-fg); }

.bwz-btn--danger {
  background: var(--bwz-danger);
  border-color: var(--bwz-danger);
  color: var(--bwz-brand-paper);
}
:root[data-bwz-theme="dark"] .bwz-btn--danger { color: var(--bwz-brand-ink); }
@media (prefers-color-scheme: dark) {
  :root:not([data-bwz-theme="light"]) .bwz-btn--danger { color: var(--bwz-brand-ink); }
}

.bwz-btn--sm { min-height: 36px; padding: var(--bwz-space-1) var(--bwz-space-3); font-size: var(--bwz-text-sm); }
.bwz-btn--lg { min-height: 52px; padding: var(--bwz-space-3) var(--bwz-space-6); font-size: var(--bwz-text-md); }
.bwz-btn--block { display: flex; width: 100%; }

.bwz-btn[disabled],
.bwz-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

.bwz-btn-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bwz-space-3);
}
/* At 320px a row of buttons must stack rather than shrink below the target. */
@media (max-width: 22.5em) {
  .bwz-btn-group > .bwz-btn { flex: 1 1 100%; }
}

/* ---------------------------------------------------------------------------
 * 10. Cards
 * ------------------------------------------------------------------------- */

.bwz-card {
  display: flex;
  flex-direction: column;
  background: var(--bwz-surface);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  box-shadow: var(--bwz-shadow-sm);
  overflow: hidden;
}
.bwz-card__header,
.bwz-card__body,
.bwz-card__footer { padding: var(--bwz-space-5); }
.bwz-card__header { border-bottom: var(--bwz-border) solid var(--bwz-rule); }
.bwz-card__footer {
  border-top: var(--bwz-border) solid var(--bwz-rule);
  background: var(--bwz-surface-2);
  margin-top: auto;
}
.bwz-card__title {
  font-size: var(--bwz-text-lg);
  font-weight: var(--bwz-weight-semibold);
  line-height: var(--bwz-leading-snug);
}
.bwz-card--flush { box-shadow: none; }
.bwz-card--tinted { background: var(--bwz-surface-2); }

.bwz-card--interactive { transition: border-color var(--bwz-duration) var(--bwz-ease), box-shadow var(--bwz-duration) var(--bwz-ease); }
.bwz-card--interactive:hover { border-color: var(--bwz-accent); box-shadow: var(--bwz-shadow-md); }
.bwz-card--interactive:focus-within { border-color: var(--bwz-accent); }

/* A stat tile for the dashboard. */
.bwz-stat__label {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-weight: var(--bwz-weight-medium);
}
.bwz-stat__value {
  font-size: var(--bwz-text-2xl);
  font-weight: var(--bwz-weight-bold);
  line-height: var(--bwz-leading-tight);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--bwz-tracking-tight);
}

/* ---------------------------------------------------------------------------
 * 10a. Logos and pictures
 *
 * Added for ticket 869evwwzn, 2026-09-07, on two instructions: "Use logos for
 * Asset Giant and Just Granted", and "use the mockups as graphics ... plus a
 * screenshot".
 *
 * Two rules govern everything here.
 *
 * A supplied mark is not recoloured. Asset Giant's logo carries a gradient and
 * a red; Just Granted's is a fixed teal. Neither survives an invert or a CSS
 * filter, and both would fail against the dark ground. So they sit on a Paper
 * tile in both themes -- the brand pack's own answer for a lockup on dark.
 *
 * Every picture reserves its space before it loads. width and height attributes
 * are on the markup and aspect-ratio is here, so nothing below an image jumps
 * down the page when it arrives.
 * ------------------------------------------------------------------------- */

.bwz-logo-tile {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  background: var(--bwz-logo-tile);
  border: var(--bwz-border) solid var(--bwz-logo-tile-rule);
  border-radius: var(--bwz-radius-md);
  padding: var(--bwz-space-4);
  min-height: 4.5rem;
}
.bwz-logo-tile img {
  width: auto;
  max-height: 2.5rem;
  max-width: 100%;
  object-fit: contain;
}
.bwz-logo-tile--centred { justify-content: center; }

/* A row of logos that wraps at 320px with no breakpoint. */
.bwz-logo-strip {
  display: grid;
  gap: var(--bwz-space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
}

/* A picture with an optional caption. <figure> is the element; this is only
 * the frame round it. */
.bwz-figure { display: flex; flex-direction: column; gap: var(--bwz-space-3); }
.bwz-figure img {
  width: 100%;
  border-radius: var(--bwz-radius-lg);
  border: var(--bwz-border) solid var(--bwz-rule);
  background: var(--bwz-surface-2);
}
.bwz-figure figcaption {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-subtle);
}

/* The gallery on a case study. auto-fit again, so one picture fills the width
 * and three share it. */
.bwz-gallery {
  display: grid;
  gap: var(--bwz-space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
}

/* A picture at the top of a card, above the title. It is cropped to a fixed
 * shape so a row of cards keeps its titles on one line. */
.bwz-card__media {
  aspect-ratio: 16 / 9;
  background: var(--bwz-surface-2);
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
}
.bwz-card__media img { width: 100%; height: 100%; object-fit: cover; }
.bwz-card__media--logo {
  aspect-ratio: auto;
  background: var(--bwz-logo-tile);
  padding: var(--bwz-space-5);
  display: flex;
  align-items: center;
}
.bwz-card__media--logo img {
  height: 2.25rem;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}

/* ---------------------------------------------------------------------------
 * 10b. Design concepts — /design-concepts/ only, staff only
 *
 * Dan, ticket 869evwwzn, 2026-09-07: "I'd like the site to be a lot more
 * visually arresting - include graphics and more colours etc ... professional,
 * modern, slight hints at AI capabilities but really very subtle on that part,
 * simple (just not quite this simple as we currently have). Draft various
 * designs / graphics etc for my approval."
 *
 * So these are DRAFTS. Nothing here is applied to a page a visitor can reach:
 * every rule is scoped under .bwz-dc, the page it renders on is signed-in-staff
 * only, and picking one is a separate piece of work.
 *
 * Three constraints held throughout, because they are not negotiable:
 *   - no component names a hex; everything is built from the role tokens in
 *     section 2, so all three directions follow the light and dark themes;
 *   - the mark is never recoloured, gradient-filled, rotated or outlined;
 *   - nothing here relies on CSS nesting, :has() or container queries, so the
 *     previous major of all four target browsers still renders it.
 *
 * The "AI hint" is a field of faint dots behind a heading and nothing else. No
 * circuitry, no glowing orb, no robot.
 * ------------------------------------------------------------------------- */

.bwz-dc { border-radius: var(--bwz-radius-lg); overflow: hidden; }
.bwz-dc__frame {
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  overflow: hidden;
}
.bwz-dc__pad { padding: clamp(1.5rem, 4vw, 3.5rem); }
.bwz-dc__cards {
  display: grid;
  gap: var(--bwz-space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.bwz-dc__card {
  background: var(--bwz-surface);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-md);
  padding: var(--bwz-space-5);
}
.bwz-dc__card p { color: var(--bwz-fg-muted); font-size: var(--bwz-text-sm); }
.bwz-dc__title {
  font-size: var(--bwz-text-2xl);
  font-weight: var(--bwz-weight-bold);
  letter-spacing: var(--bwz-tracking-tight);
  line-height: var(--bwz-leading-tight);
  /* 26ch, not the 22ch .bwz-h1 uses: at 20ch this wrapped to five lines on a
   * 1280px screen and left the right two-thirds of the hero empty, which read
   * as a layout fault rather than as a design. Measured in a screenshot. */
  max-width: 26ch;
}
.bwz-dc__lede {
  font-size: var(--bwz-text-md);
  color: var(--bwz-fg-muted);
  max-width: var(--bwz-measure);
  margin-top: var(--bwz-space-4);
}

/* -- A. Quiet ---------------------------------------------------------------
 * What the site is now, given room to breathe: one accent, a lot of white
 * space, and the pictures doing the work instead of the colour. The safe
 * choice, and the one that ages best. */
.bwz-dc--quiet { background: var(--bwz-bg); }
.bwz-dc--quiet .bwz-dc__title { color: var(--bwz-fg); }
.bwz-dc--quiet .bwz-dc__rule {
  width: 3rem;
  height: var(--bwz-border-thick);
  background: var(--bwz-accent);
  margin-bottom: var(--bwz-space-5);
}

/* -- B. Structured ----------------------------------------------------------
 * Colour arrives through surfaces rather than through hue: a tinted ground,
 * white cards floating on it, an accent rule and a numbered spine down the
 * left. Reads as an operations document, which is close to what is being
 * sold. */
.bwz-dc--structured { background: var(--bwz-surface-2); }
.bwz-dc--structured .bwz-dc__card {
  box-shadow: var(--bwz-shadow-md);
  border-color: transparent;
  position: relative;
  padding-left: var(--bwz-space-7);
}
.bwz-dc--structured .bwz-dc__num {
  position: absolute;
  left: var(--bwz-space-5);
  top: var(--bwz-space-5);
  font-variant-numeric: tabular-nums;
  font-weight: var(--bwz-weight-bold);
  color: var(--bwz-accent);
}
.bwz-dc--structured .bwz-dc__title {
  border-left: 4px solid var(--bwz-accent);
  padding-left: var(--bwz-space-5);
}

/* -- C. Signal --------------------------------------------------------------
 * The loud one. A gradient hero built from the two ends of the palette, a
 * field of faint dots behind it, and the section below it on the accent tint.
 * Both gradient stops are role tokens, so it inverts correctly in the dark
 * theme instead of turning to mud.
 *
 * The dot field is a repeating radial-gradient rather than an image: it is
 * about 90 bytes, it scales to any viewport, and there is no file to keep. */
.bwz-dc--signal .bwz-dc__hero {
  position: relative;
  background-image: linear-gradient(140deg, var(--bwz-accent) 0%, var(--bwz-fg) 100%);
  color: var(--bwz-accent-fg);
  isolation: isolate;
}
.bwz-dc--signal .bwz-dc__hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: radial-gradient(currentColor 1px, transparent 1px);
  background-size: 18px 18px;
  opacity: 0.12;
}
.bwz-dc--signal .bwz-dc__title,
.bwz-dc--signal .bwz-dc__lede { color: var(--bwz-accent-fg); }
.bwz-dc--signal .bwz-dc__band { background: var(--bwz-accent-surface); }
.bwz-dc--signal .bwz-dc__card { border-color: var(--bwz-accent); }

/* ---------------------------------------------------------------------------
 * 10c. Signal — the chosen direction, on the real site
 *
 * Dan picked C, "Signal", on 2026-09-08 (ticket 869evwwzn, read back off the
 * tick boxes on 869eyf9jx). ⚠️ He did NOT pick the recommended option — the
 * sweep starred B, Structured. Anybody revisiting this works from the tick.
 *
 * This is 10b's third draft made real. The draft stays where it is, so the
 * three directions can still be compared, and this section is what a visitor
 * actually gets. Three devices and no more:
 *
 *   1. a gradient hero, built from the two ends of the palette;
 *   2. a field of faint dots behind it — the only nod to AI anywhere on the
 *      site, and deliberately not a circuit board, an orb or a robot;
 *   3. the accent tint on a banded section, and an accent hairline on cards.
 *
 * [CRITICAL] Scoped to the marketing site, not to the stylesheet. The
 * dashboard, the client portal and the discovery questionnaire all extend the
 * same base.html and share this one stylesheet, so every rule below is behind
 * `.bwz-body--signal`, a body class that only content/page.html and
 * content/case_study.html set. Restyling `.bwz-card` or `.bwz-section--tinted`
 * outright would have repainted the operations dashboard, where colour means
 * "this check is failing" and nothing else.
 *
 * No hex appears here. Both gradient stops are role tokens, so the hero
 * inverts with the theme instead of turning to mud: light runs Blue -> Ink
 * under white text, dark runs dark-UI-blue -> Paper-ish under near-black text.
 * ------------------------------------------------------------------------- */

/* The hero. `isolation: isolate` gives the dot field a stacking context of its
 * own, so `z-index: -1` puts it behind the text and NOT behind the page. */
.bwz-hero {
  position: relative;
  isolation: isolate;
  background-image: linear-gradient(140deg, var(--bwz-accent) 0%, var(--bwz-fg) 100%);
  color: var(--bwz-accent-fg);
}

/* The AI hint: a repeating radial-gradient rather than an image. About ninety
 * bytes, scales to any viewport, and there is no file to keep in step with a
 * release. `currentColor` means it follows the hero's own text colour, so it
 * needs no token of its own in either theme. */
.bwz-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: radial-gradient(currentColor 1px, transparent 1px);
  background-size: 18px 18px;
  opacity: 0.12;
}

/* Everything inside the hero is on the gradient, so it takes the hero's
 * foreground rather than the page's. The eyebrow and the meta line are named
 * explicitly because both carry a colour of their own in section 6. */
.bwz-hero .bwz-h1,
.bwz-hero .bwz-lede,
.bwz-hero .bwz-prose,
.bwz-hero .bwz-eyebrow,
.bwz-hero .bwz-meta,
.bwz-hero .bwz-muted,
.bwz-hero .bwz-subtle { color: var(--bwz-accent-fg); }

/* A link on the gradient. The accent is the darkest thing in the light theme's
 * gradient and would vanish into it, so the link takes the hero's foreground
 * and keeps its underline — which is what carries the affordance once colour
 * is no longer doing it. */
.bwz-hero .bwz-link,
.bwz-hero a { color: var(--bwz-accent-fg); }
.bwz-hero .bwz-link:hover,
.bwz-hero a:hover { color: var(--bwz-accent-fg); text-decoration-thickness: 2px; }

/* [CRITICAL] The focus ring is `--bwz-accent` everywhere else, and the accent
 * is one END of this gradient: a keyboard user tabbing across the hero would
 * get an outline the same colour as the thing behind it. On the gradient the
 * ring is the hero's foreground instead, which is the highest-contrast colour
 * available against every point of it. */
.bwz-hero :focus-visible { outline-color: var(--bwz-accent-fg); }

/* The band under the hero: the accent tint, where `--tinted` is the neutral
 * one. A separate modifier rather than a redefinition of `--tinted`, because
 * `--tinted` is also the dashboard's table header and the case study's body. */
.bwz-section--band { background: var(--bwz-accent-surface); }

/* An accent hairline on the cards, so the colour carries past the hero rather
 * than stopping at it. Border only: the card keeps its own surface, because a
 * tinted card on a tinted band is two surfaces and no edge. */
.bwz-body--signal .bwz-card { border-color: var(--bwz-accent); }
.bwz-body--signal .bwz-card__header,
.bwz-body--signal .bwz-card__footer { border-color: var(--bwz-rule); }

/* The wide measure. Dan's instruction of 2026-09-09; the reasoning and the
 * limits are on --bwz-measure-wide in section 3. Scoped to the marketing body
 * because .bwz-prose is also the dashboard's and the questionnaire's, and
 * neither of those asked for a longer line.
 *
 * To put it back: delete this block. To go halfway, change the token to 60rem.
 * Nothing else reads --bwz-measure-wide. */
@media (min-width: 90em) {
  .bwz-body--signal .bwz-prose,
  .bwz-body--signal .bwz-lede,
  .bwz-body--signal .bwz-measure { max-width: var(--bwz-measure-wide); }
  /* The hero is the exception, and it is not a compromise: a lede set across
   * 1280px directly under a heading capped at 22ch reads as two unrelated
   * blocks. The hero keeps the readable measure. */
  .bwz-body--signal .bwz-hero .bwz-lede,
  .bwz-body--signal .bwz-hero .bwz-prose,
  .bwz-body--signal .bwz-hero .bwz-measure { max-width: var(--bwz-measure); }
}

/* The step drawings on /how-we-work/ (869evwx03, 2026-09-09; redrawn and re-set
 * 869ezr12u, 2026-09-11). The icon is `currentColor` throughout, so it takes
 * the accent here and inverts with the theme like everything else — there is
 * no second colour to keep in step, and the illustration hues are deliberately
 * NOT available to it: they are graded against the hero gradient and this is
 * not on the hero.
 *
 * Dan: "these icons look really amateur, design something much more
 * professional please." Half of that was the drawings and is in
 * content/icons.py. This is the other half: a 2rem drawing floating in
 * whitespace above a heading looks unfinished however well it is drawn, so it
 * now sits in a tinted tile on the heading's own row.
 *
 * `--bwz-accent` on `--bwz-accent-surface` is an existing documented contrast
 * pair, so the tile introduced no new pair for core/tests.py to grade. */
.bwz-body--signal .bwz-step__head {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-3);
}
.bwz-body--signal .bwz-step__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--bwz-radius-lg);
  border: var(--bwz-border) solid var(--bwz-accent);
  background: var(--bwz-accent-surface);
  color: var(--bwz-accent);
}
.bwz-body--signal .bwz-step__icon {
  width: 1.5rem;
  height: 1.5rem;
}

/* The tile is the first thing on a 320px line and the heading has to wrap
 * beside it rather than under it, so the heading gives up its balanced wrap —
 * `text-wrap: balance` on a narrow flex item produces two very short lines and
 * a ragged tile. */
@media (max-width: 30em) {
  .bwz-body--signal .bwz-step__mark {
    width: 2.25rem;
    height: 2.25rem;
  }
  .bwz-body--signal .bwz-step__icon {
    width: 1.25rem;
    height: 1.25rem;
  }
  .bwz-body--signal .bwz-step__head .bwz-h4 {
    text-wrap: initial;
  }
}

/* The hero drawing (869ezr12u, 2026-09-10). Dan asked for illustrations and
 * asked to compare three styles on three pages first, so three pages carry one
 * and everything else carries none. The drawing is inline SVG from
 * content/illustrations.py; the reasoning for that is in that module and is the
 * step icons' reasoning at a larger size.
 *
 * `--inner` is applied to EVERY marketing hero and `--split` only to the three,
 * so a page with no figure is a one-item grid: identical to the plain block
 * that was here before, which is what makes the trial reversible by deleting
 * three lines of Python and touching no CSS.
 *
 * No colour appears anywhere below, and none is wanted: the drawing is
 * `currentColor` throughout, and inside .bwz-hero that resolves to
 * --bwz-accent-fg — the one colour section 10c has already guaranteed against
 * every point of the gradient, in both themes. There is no second value to
 * keep in step and no new pair for core/tests.py to grade. */
.bwz-hero__inner {
  display: grid;
  gap: var(--bwz-space-7);
  align-items: center;
}

/* Under the text on a phone, never beside it: the first screen has to carry
 * the point on its own (content-drafting-rules section 4), and a decorative
 * drawing above the heading is scroll spent before the reader is told
 * anything. Source order puts the copy first, so this needs no rule — but the
 * width cap does, or a 400-unit-wide vector fills a 320px screen top to bottom. */
.bwz-hero__figure {
  justify-self: center;
  width: 100%;
  max-width: 26rem;
}

/* `height: auto` with the viewBox is what keeps the aspect ratio without a
 * hardcoded height per drawing, so a fourth style of a different shape needs
 * no rule of its own. */
.bwz-hero__art {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0.92;
}

/* The five illustration hues (869ezr12u, 2026-09-11). Dan: "Blueprint is the
 * closest to being right, but I would like something MUCH more complex, more
 * colourful."
 *
 * [CRITICAL] `color`, not `fill` and not `stroke`. The drawings are
 * `currentColor` throughout — exactly as they were when there was only one
 * colour — so setting `color` on a group re-points every shape inside it and
 * content/illustrations.py still names no colour at all. Painting `fill` here
 * would half-work: a stroked shape in the group would keep the hero's white
 * and nothing would say which of the two had been meant.
 *
 * A class here with no drawing using it is harmless. A drawing using a class
 * with no rule here is silent — the group simply inherits the hero's
 * foreground and the drawing quietly loses a colour — so a test walks the
 * drawings and fails the build on one.
 *
 * These tokens are read here and nowhere else in the stylesheet, and a test
 * holds that too: three of the five are a green, an amber and a rose, and on
 * /dashboard/ those three colours mean "this check is failing". */
.bwz-hero__art .bwz-art--cyan { color: var(--bwz-illus-cyan); }
.bwz-hero__art .bwz-art--green { color: var(--bwz-illus-green); }
.bwz-hero__art .bwz-art--amber { color: var(--bwz-illus-amber); }
.bwz-hero__art .bwz-art--rose { color: var(--bwz-illus-rose); }
.bwz-hero__art .bwz-art--violet { color: var(--bwz-illus-violet); }

/* Two columns only once there is genuinely room for two. Below this the grid
 * stays single-column and the figure sits under the lede.
 *
 * `minmax(0, ...)` on the text column, not `1fr` alone: a grid item's automatic
 * minimum size is its content, so a long unbroken word in a heading would push
 * the column wider than the track and take the figure off the right edge. */
@media (min-width: 60em) {
  .bwz-hero__inner--split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 24rem);
  }
  .bwz-hero__inner--split .bwz-hero__figure {
    max-width: none;
  }
}

/* ---------------------------------------------------------------------------
 * 11. Header, nav and footer
 * ------------------------------------------------------------------------- */

.bwz-header {
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
  background: var(--bwz-bg);
  padding-block: var(--bwz-space-3);
}
.bwz-header__inner {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-3);
}
/* The logo takes the free space so the nav and the controls stay together on
 * the right, whether or not there is a nav to show. An auto margin does that
 * for any number of children; `justify-content` did not, once there were
 * three of them. */
.bwz-header__inner > .bwz-logo { margin-right: auto; }

/* The nav wraps at every width the reader has JavaScript turned off at, and
 * collapses behind a disclosure below 48em when they have it. Ticket
 * 869evwwz8, on Dan's instruction: "Create a hamburger nav."
 *
 * [CRITICAL] The collapsed state is scoped to `:root[data-bwz-nav="js"]`, an
 * attribute set by bwz-nav.js and by nothing else. With no JavaScript there is
 * no attribute, so the nav is exactly the wrapping row it has always been and
 * the button that would hide it never appears. A menu whose only way open is a
 * script that did not load is a site with no navigation at all. */
.bwz-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-1) var(--bwz-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.bwz-nav__link {
  display: inline-block;
  padding: var(--bwz-space-2) 0;
  color: var(--bwz-fg-muted);
  font-weight: var(--bwz-weight-medium);
  text-decoration: none;
  border-bottom: var(--bwz-border-thick) solid transparent;
}
.bwz-nav__link:hover { color: var(--bwz-fg); border-bottom-color: var(--bwz-rule-strong); }
/* Current page is marked by weight and a rule, not by colour alone. */
.bwz-nav__link[aria-current="page"] {
  color: var(--bwz-fg);
  font-weight: var(--bwz-weight-semibold);
  border-bottom-color: var(--bwz-accent);
}

/* The disclosure itself. It ships `hidden` in the markup and bwz-nav.js is
 * what reveals it, for the same reason the theme toggle does: a control that
 * cannot do anything must not be on the page. */
.bwz-nav-toggle {
  display: none;
  align-items: center;
  gap: var(--bwz-space-2);
  min-height: 2.75rem;
  padding: var(--bwz-space-2) var(--bwz-space-3);
  border: var(--bwz-border) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-md);
  background: var(--bwz-surface);
  color: var(--bwz-fg);
  font: inherit;
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-medium);
  cursor: pointer;
}
.bwz-nav-toggle:hover { border-color: var(--bwz-accent); }

/* Three bars, drawn in `currentColor` so they follow the button's text in both
 * themes and there is no colour to keep in step. */
.bwz-nav-toggle__bars {
  display: block;
  position: relative;
  width: 1.125rem;
  height: 0.75rem;
  flex: none;
}
.bwz-nav-toggle__bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 1px;
  background: currentColor;
  transition: transform var(--bwz-duration) var(--bwz-ease),
    opacity var(--bwz-duration) var(--bwz-ease);
}
.bwz-nav-toggle__bars span:nth-child(1) { top: 0; }
.bwz-nav-toggle__bars span:nth-child(2) { top: calc(50% - 1px); }
.bwz-nav-toggle__bars span:nth-child(3) { bottom: 0; }

/* Open: the bars become a cross. The middle one fades rather than moving, so
 * nothing has to travel behind anything else. */
.bwz-header--nav-open .bwz-nav-toggle__bars span:nth-child(1) {
  transform: translateY(calc(0.375rem - 1px)) rotate(45deg);
}
.bwz-header--nav-open .bwz-nav-toggle__bars span:nth-child(2) { opacity: 0; }
.bwz-header--nav-open .bwz-nav-toggle__bars span:nth-child(3) {
  transform: translateY(calc(1px - 0.375rem)) rotate(-45deg);
}

/* Collapsed navigation: below 48em, and only where the script ran.
 *
 * The panel is in normal flow on its own row rather than floating over the
 * page, so opening it pushes the page down instead of covering it. That is
 * what makes it safe with no trap, no overlay and no scroll lock -- three
 * things that go wrong on real phones and cannot be tested from here. */
@media (max-width: 47.99em) {
  :root[data-bwz-nav="js"] .bwz-nav-toggle { display: inline-flex; }

  :root[data-bwz-nav="js"] .bwz-nav {
    order: 3;
    flex-basis: 100%;
    display: none;
  }
  :root[data-bwz-nav="js"] .bwz-header--nav-open .bwz-nav { display: block; }

  :root[data-bwz-nav="js"] .bwz-nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-top: var(--bwz-space-2);
  }
  /* Each link is its own row, at least 44px tall, with the rule under it
   * rather than under the text -- a whole-row target is what a thumb hits. */
  :root[data-bwz-nav="js"] .bwz-nav__link {
    display: block;
    padding: var(--bwz-space-3) 0;
    border-bottom: var(--bwz-border) solid var(--bwz-rule);
  }
  :root[data-bwz-nav="js"] .bwz-nav__link[aria-current="page"] {
    border-bottom-color: var(--bwz-accent);
  }
}

/* The header's sign-in / sign-out control. Ticket 869f06k66.
 *
 * [CRITICAL] <details> and <summary>, never a script -- the same rule the
 * runner cards follow in section 21. The header navigation is the one place on
 * this site where JavaScript may hide something, and it may only because
 * bwz-nav.js also CREATES the collapsed state it operates. Nothing here is
 * created by a script, so nothing here may be hidden by one: with JavaScript
 * off the panel is a native disclosure that opens on a click, and the two doors
 * are reachable.
 *
 * The panel floats rather than pushing the header taller, which is the opposite
 * call to the collapsed navigation two rules up. The reason they differ is
 * size: the nav panel is a full menu and covering the page with it needs a
 * focus trap and a scroll lock -- three things that go wrong on real phones --
 * while this is one short form, right-anchored, and clamped to the viewport so
 * it cannot reach an edge at 320px.
 *
 * It held two LINKS until 2026-09-12, when Dan's Q1(b) on 869f06k66 replaced
 * them with a real username and password box posting to /signin/. The two
 * `.bwz-account__door*` rules that styled the links went with them: a rule for a
 * component the project no longer renders is worse than no rule, because the
 * next person reads it as current.
 *
 * Layout and role tokens only. No hex, and no colour that means anything: this
 * renders on the operations dashboard too, where a colour is a check's status.
 */
.bwz-account {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-2);
}

/* Signed in: who, then the way out. The name is clamped rather than allowed to
 * push the header controls apart -- a username has no maximum length. */
.bwz-account__who {
  max-width: 10rem;
  overflow: hidden;
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Signed out: the disclosure. Sized like .bwz-nav-toggle on purpose, so the
 * three header controls are one row of one shape.
 *
 * .bwz-admin__summary (869f0kbp2) is the same shape and shares the rule rather
 * than copying it: two disclosures in one header row that drifted apart would
 * read as two different kinds of control. */
.bwz-account__summary,
.bwz-admin__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-2);
  min-height: 2.75rem;
  padding: var(--bwz-space-2) var(--bwz-space-3);
  border: var(--bwz-border) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-md);
  background: var(--bwz-surface);
  color: var(--bwz-fg);
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-medium);
  cursor: pointer;
  list-style: none;
}
.bwz-account__summary:hover,
.bwz-admin__summary:hover { border-color: var(--bwz-accent); }
/* The default marker is a different glyph in every engine and cannot be
 * rotated. Ours is one span drawn with a border, exactly as .bwz-runner__arrow. */
.bwz-account__summary::-webkit-details-marker,
.bwz-admin__summary::-webkit-details-marker { display: none; }
.bwz-account__arrow {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--bwz-duration) var(--bwz-ease);
}
[open] > .bwz-account__summary .bwz-account__arrow { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .bwz-account__arrow { transition: none; }
}

/* [CRITICAL] Anchored to the HEADER ROW, not to the control that opens it.
 *
 * `.bwz-account` was the containing block until the browser suite measured it:
 * below 48em the header wraps, the controls land on their own row at the LEFT,
 * and `right: 0` on a 90px summary put a 288px panel at x=-174 -- most of it
 * off the left edge of a 320px screen, with the whole control unreachable. The
 * bug the control exists to prevent, reproduced by the control. It was two doors
 * then and it is a login box now; the geometry is the same and so is the lesson.
 *
 * `.bwz-header__inner` is `.bwz-container`, so anchoring here means the panel
 * hangs from the bottom of the header at the reading gutter, at every width and
 * whatever the row above it did.
 * `test_the_open_sign_in_panel_stays_inside_a_320px_screen` in the browser suite
 * is what found it and what holds it. */
.bwz-account__panel {
  position: absolute;
  top: calc(100% + var(--bwz-space-3));
  right: var(--bwz-gutter);
  z-index: var(--bwz-z-overlay);
  width: 18rem;
  /* [CRITICAL] `100vw`, NOT `100%`, and it cost a browser-suite failure to learn.
   *
   * This clamp was `min(20rem, calc(100% - 2 * var(--bwz-gutter)))` until
   * 2026-09-12. A CLOSED <details> puts its content in a `content-visibility:
   * hidden` subtree, and Chrome resolves a percentage in there against a
   * ZERO-width containing block -- so `calc(100% - 48px)` went negative, clamped
   * to 0, and the panel laid out 34px wide (its padding) with the children
   * sticking out to x=1348 on a 1280px screen. The CONTENTS then failed
   * `test_the_page_does_not_scroll_sideways` on every page, at every width, in
   * both themes, while the panel itself measured fine and the document's
   * scrollWidth never moved.
   *
   * It did not bite the doors version because every child of that panel was
   * wrappable text, which simply reflowed inside 34px. The box's children are a
   * `white-space: nowrap` lockup and an `inline-flex` button, and neither can.
   *
   * `100vw` is the viewport and has nothing to collapse. It keeps the whole point
   * of the clamp -- the panel cannot reach an edge at 320px -- and it is the same
   * number at every width this site supports, because the panel is only 18rem
   * wide and the clamp bites below 336px of viewport. It ignores a desktop
   * scrollbar, which is harmless here for exactly that reason: at a width where
   * the clamp is doing anything there is no persistent scrollbar.
   *
   * The fix is NOT `overflow: hidden` on this panel. That would satisfy the
   * sweep's own "inside a box that scrolls on purpose" exemption while leaving
   * the layout wrong -- which is the masking that test's docstring warns about. */
  max-width: calc(100vw - 2 * var(--bwz-gutter));
  padding: var(--bwz-space-4);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
  box-shadow: var(--bwz-shadow-lg);
}

/* "show the Blueworkz logo" -- the lockup, above the fields. `.bwz-mark` and
 * `.bwz-logo__word` are the header's own, reused rather than copied: the mark has
 * a light and a dark file and the swap lives with those classes. */
.bwz-account__brand {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-2);
  margin: 0 0 var(--bwz-space-2);
}

/* `.bwz-field`, `.bwz-label`, `.bwz-input` and `.bwz-btn` are the site's own form
 * components and this panel uses them unchanged, so a box in the chrome cannot
 * drift from the forms on the pages. The only thing the panel adds is the stack
 * rhythm, and `.bwz-stack` already owns that -- so there is nothing here for the
 * form itself. */

/* The header's admin menu. Ticket 869f0kbp2.
 *
 * Dan: "Put this in the header, left aligned, next to the blueworkz logo."
 *
 * [CRITICAL] It is a <details>, never a script -- the same rule as the sign-in
 * control above it and the runner cards in section 21. Nothing here is created
 * by JavaScript, so nothing here may be hidden by it.
 *
 * The arrow is section 23's generic `.bwz-arrow`, which is what that component
 * was drawn for: `details[open] > summary .bwz-arrow` matches any disclosure,
 * so this one needed no rotation selector of its own. The three older copies
 * are deliberately left where they are.
 *
 * WHERE THE AUTO MARGIN LIVES. `.bwz-header__inner > .bwz-logo` takes the free
 * space, which is what holds the nav and the controls together on the right.
 * "Next to the logo" means the menu has to sit on the logo's right with the
 * free space AFTER it, so the auto margin moves one element along -- and only
 * when there is a menu, because `show_admin_menu` is false for everybody but a
 * signed-in superuser and the header must be unchanged for them. The template
 * sets the modifier on the same flag that renders the menu, so the two cannot
 * disagree; `:has()` would have done it without a class and is not used,
 * because the previous major of Firefox in the supported matrix predates it.
 *
 * Layout and role tokens only. No hex, and no colour that means anything: this
 * renders on the operations dashboard too, where a colour is a check's status.
 */
.bwz-admin {
  display: flex;
  align-items: center;
}
.bwz-header__inner--admin > .bwz-logo { margin-right: 0; }
.bwz-header__inner--admin > .bwz-admin { margin-right: auto; }

/* [CRITICAL] Anchored to the HEADER ROW, not to the summary that opens it --
 * the lesson `.bwz-account__panel` paid for in a 320px browser test. Below 48em
 * the header wraps and the controls move, and a panel anchored to a 90px
 * summary lands wherever that summary ended up. `.bwz-header__inner` is
 * `.bwz-container`, so this hangs from the bottom of the header at the reading
 * gutter at every width.
 *
 * `left`, where the sign-in panel uses `right`: this control is on the left of
 * the row and a panel that opened away from its own handle reads as somebody
 * else's. */
/* ⚠️ This still clamps on `100%`, where `.bwz-account__panel` had to move to
 * `100vw`: a closed <details> resolves a percentage against a zero-width box in
 * Chrome. It is not broken here because every child of this panel is wrappable
 * text, which reflows inside the collapsed width instead of overflowing it. Put
 * a `nowrap` lockup or an `inline-flex` button in here and it will be -- read the
 * note on `.bwz-account__panel` before adding one. */
.bwz-admin__panel {
  position: absolute;
  top: calc(100% + var(--bwz-space-3));
  left: var(--bwz-gutter);
  z-index: var(--bwz-z-overlay);
  width: max-content;
  max-width: min(20rem, calc(100% - 2 * var(--bwz-gutter)));
  padding: var(--bwz-space-4);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
  box-shadow: var(--bwz-shadow-lg);
}

/* A whole-block target, at least 44px tall, for the same reason the two doors
 * are: a thumb has to be able to hit it at 320px. */
.bwz-admin__item {
  display: block;
  padding: var(--bwz-space-3);
  border-radius: var(--bwz-radius-md);
  color: inherit;
  text-decoration: none;
}
.bwz-admin__item + .bwz-admin__item { margin-top: var(--bwz-space-2); }
.bwz-admin__item:hover { background: var(--bwz-surface-2); }
.bwz-admin__item:hover .bwz-admin__item-label,
.bwz-admin__item:focus-visible .bwz-admin__item-label { text-decoration: underline; }
.bwz-admin__item-label {
  display: block;
  font-weight: var(--bwz-weight-semibold);
}
.bwz-admin__item-note {
  display: block;
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-sm);
}
/* Where you already are, marked by a rule and the background rather than by
 * colour alone -- the same call `.bwz-nav__link[aria-current]` makes. */
.bwz-admin__item[aria-current="page"] {
  background: var(--bwz-surface-2);
  box-shadow: inset 2px 0 0 0 var(--bwz-accent);
}

/* A group of items under a label (869f2a694, Dan 2026-09-16: "Create a new
 * admin area menu item, between Runners and Django admin, 'Clients'").
 *
 * A LABEL and an indent, not a nested <details>: core/tests.py reads this panel
 * by splitting on the first `</details>`, so a submenu here would truncate the
 * list of links three tests assert on and they would go on passing on the
 * shorter list. The template says the same thing at more length. */
.bwz-admin__group {
  margin-top: var(--bwz-space-4);
  padding: 0 var(--bwz-space-3);
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-xs);
  font-weight: var(--bwz-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* The indent is what says "these two belong to the label above". Padding
 * rather than a margin, so the hover background still spans the item. */
.bwz-admin__item--grouped { padding-inline-start: var(--bwz-space-5); }
/* The label is not an item, so the item-to-item margin rule above does not
 * reach the first link under it. */
.bwz-admin__group + .bwz-admin__item { margin-top: var(--bwz-space-1); }

.bwz-footer {
  flex-shrink: 0;
  margin-top: var(--bwz-space-7);
  border-top: var(--bwz-border) solid var(--bwz-rule);
  background: var(--bwz-surface-2);
  padding-block: var(--bwz-space-6);
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
}
.bwz-footer__grid {
  display: grid;
  gap: var(--bwz-space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.bwz-footer__heading {
  font-size: var(--bwz-text-xs);
  font-weight: var(--bwz-weight-semibold);
  letter-spacing: var(--bwz-tracking-wide);
  text-transform: uppercase;
  color: var(--bwz-fg-subtle);
  margin-bottom: var(--bwz-space-2);
}
.bwz-footer__legal {
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-subtle);
}
/* The rule above the legal line separates it from the rest of the footer, so
 * it must not appear when the legal line IS the rest of the footer -- which is
 * the case on every page until there is something to link to. */
.bwz-footer__legal:not(:first-child) {
  margin-top: var(--bwz-space-5);
  padding-top: var(--bwz-space-4);
  border-top: var(--bwz-border) solid var(--bwz-rule);
}
.bwz-footer__version { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
 * 12. Forms
 * ------------------------------------------------------------------------- */

.bwz-form { max-width: var(--bwz-measure); }
.bwz-form > * + * { margin-top: var(--bwz-space-5); }

.bwz-fieldset { border: 0; padding: 0; margin: 0; }
.bwz-fieldset__legend {
  padding: 0;
  font-size: var(--bwz-text-lg);
  font-weight: var(--bwz-weight-semibold);
  margin-bottom: var(--bwz-space-3);
}

.bwz-field { display: block; }
.bwz-field > * + * { margin-top: var(--bwz-space-2); }

.bwz-label {
  display: block;
  font-weight: var(--bwz-weight-medium);
  font-size: var(--bwz-text-sm);
}
.bwz-label__optional { color: var(--bwz-fg-subtle); font-weight: var(--bwz-weight-normal); }
.bwz-label__required { color: var(--bwz-danger); }

.bwz-input,
.bwz-textarea,
.bwz-select {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: var(--bwz-space-2) var(--bwz-space-3);
  background: var(--bwz-surface);
  color: var(--bwz-fg);
  border: var(--bwz-border) solid var(--bwz-field-border);
  border-radius: var(--bwz-radius-md);
  /* 16px minimum, or iOS Safari zooms the whole page on focus. */
  font-size: var(--bwz-text-base);
  transition: border-color var(--bwz-duration) var(--bwz-ease);
}
.bwz-textarea { min-height: 7rem; resize: vertical; line-height: var(--bwz-leading-normal); }

.bwz-select {
  appearance: none;
  padding-right: var(--bwz-space-7);
  /* Chevron drawn in the border colour so it flips with the theme. */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 55%, calc(100% - 0.8rem) 55%;
  background-size: 0.36rem 0.36rem, 0.36rem 0.36rem;
  background-repeat: no-repeat;
}

.bwz-input:hover,
.bwz-textarea:hover,
.bwz-select:hover { border-color: var(--bwz-rule-strong); }

.bwz-input::placeholder,
.bwz-textarea::placeholder { color: var(--bwz-fg-subtle); opacity: 1; }

.bwz-input[disabled],
.bwz-textarea[disabled],
.bwz-select[disabled] {
  background: var(--bwz-surface-2);
  color: var(--bwz-fg-subtle);
  cursor: not-allowed;
}

/* Error state is driven by aria-invalid, so the styling and the accessibility
 * information cannot get out of step. */
.bwz-input[aria-invalid="true"],
.bwz-textarea[aria-invalid="true"],
.bwz-select[aria-invalid="true"] {
  border-color: var(--bwz-danger);
  border-width: var(--bwz-border-thick);
}

.bwz-help { font-size: var(--bwz-text-sm); color: var(--bwz-fg-muted); }
.bwz-error {
  display: flex;
  gap: var(--bwz-space-2);
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-medium);
  color: var(--bwz-danger);
}
/* A glyph as well as the colour: never colour alone. */
.bwz-error::before { content: "\26A0"; }

.bwz-check {
  display: flex;
  align-items: flex-start;
  gap: var(--bwz-space-3);
  cursor: pointer;
}
.bwz-check + .bwz-check { margin-top: var(--bwz-space-3); }
.bwz-check__input {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0.2rem 0 0;
  accent-color: var(--bwz-accent);
}
.bwz-check__label { font-size: var(--bwz-text-base); }

/* The honeypot wrapper. Off-canvas rather than display:none, because a bot that
 * skips hidden fields is exactly the bot this is meant to catch. It is also
 * aria-hidden and tabindex=-1 in the markup: three separate ways to keep a
 * person out of a field whose only correct value is empty. */
.bwz-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------------------------------------------------------------------------
 * 13. Tables
 * ------------------------------------------------------------------------- */

/* A table wider than 320px scrolls inside this, not the page. tabindex="0" on
 * the wrapper in the markup makes that scroll reachable from the keyboard. */
.bwz-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
}
.bwz-table-wrap:focus-visible { outline-offset: 0; }

.bwz-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--bwz-text-sm);
  text-align: left;
}
.bwz-table caption {
  padding: var(--bwz-space-3) var(--bwz-space-4);
  text-align: left;
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
}
.bwz-table th,
.bwz-table td {
  padding: var(--bwz-space-3) var(--bwz-space-4);
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
  vertical-align: top;
}
.bwz-table thead th {
  background: var(--bwz-surface-2);
  font-weight: var(--bwz-weight-semibold);
  font-size: var(--bwz-text-xs);
  letter-spacing: var(--bwz-tracking-wide);
  text-transform: uppercase;
  color: var(--bwz-fg-muted);
  white-space: nowrap;
}
.bwz-table tbody tr:last-child td { border-bottom: 0; }
.bwz-table--striped tbody tr:nth-child(even) { background: var(--bwz-surface-2); }
.bwz-table--hover tbody tr:hover { background: var(--bwz-accent-surface); }
.bwz-table--compact th,
.bwz-table--compact td { padding: var(--bwz-space-2) var(--bwz-space-3); }
.bwz-table__num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
 * 14. Badges, status dots and callouts
 * ------------------------------------------------------------------------- */

.bwz-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-2);
  padding: 0.15rem var(--bwz-space-3);
  border-radius: var(--bwz-radius-full);
  background: var(--bwz-surface-3);
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-xs);
  font-weight: var(--bwz-weight-semibold);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.bwz-badge--accent { background: var(--bwz-accent-surface); color: var(--bwz-accent); }
.bwz-badge--ok { background: var(--bwz-ok-surface); color: var(--bwz-ok); }
.bwz-badge--warn { background: var(--bwz-warn-surface); color: var(--bwz-warn); }
.bwz-badge--danger { background: var(--bwz-danger-surface); color: var(--bwz-danger); }

/* The traffic light. The dot is decorative; the word next to it carries the
 * meaning, because colour alone is not an accessible signal. */
.bwz-dot {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--bwz-radius-full);
  background: currentColor;
  flex: none;
}

.bwz-callout {
  display: flex;
  gap: var(--bwz-space-3);
  padding: var(--bwz-space-4) var(--bwz-space-5);
  border-radius: var(--bwz-radius-md);
  border-left: var(--bwz-space-1) solid var(--bwz-rule-strong);
  background: var(--bwz-surface-2);
  color: var(--bwz-fg);
}
.bwz-callout__body > * + * { margin-top: var(--bwz-space-2); }
.bwz-callout__title { font-weight: var(--bwz-weight-semibold); }
.bwz-callout--accent { border-left-color: var(--bwz-accent); background: var(--bwz-accent-surface); }
.bwz-callout--ok { border-left-color: var(--bwz-ok); background: var(--bwz-ok-surface); }
.bwz-callout--warn { border-left-color: var(--bwz-warn); background: var(--bwz-warn-surface); }
.bwz-callout--danger { border-left-color: var(--bwz-danger); background: var(--bwz-danger-surface); }

.bwz-divider {
  height: var(--bwz-border);
  border: 0;
  background: var(--bwz-rule);
  margin-block: var(--bwz-space-6);
}

/* ---------------------------------------------------------------------------
 * 15. Theme toggle
 * ------------------------------------------------------------------------- */

.bwz-theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-2);
  min-height: 36px;
  padding: var(--bwz-space-1) var(--bwz-space-3);
  border: var(--bwz-border) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-full);
  background: var(--bwz-surface);
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-medium);
  cursor: pointer;
}
.bwz-theme-toggle:hover { color: var(--bwz-fg); border-color: var(--bwz-accent); }
/* Hidden until the script that drives it has run — a toggle that does nothing
 * is worse than no toggle. */
.bwz-theme-toggle[hidden] { display: none; }

/* ---------------------------------------------------------------------------
 * 16. Styleguide-only furniture. Prefixed bwz-sg- so it is obvious that none
 *     of this is part of the design system the site and dashboard consume.
 * ------------------------------------------------------------------------- */

.bwz-sg-banner {
  background: var(--bwz-warn-surface);
  color: var(--bwz-warn);
  border-bottom: var(--bwz-border) solid var(--bwz-warn);
  padding-block: var(--bwz-space-2);
  font-size: var(--bwz-text-sm);
  font-weight: var(--bwz-weight-semibold);
}

.bwz-sg-toc {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bwz-space-2) var(--bwz-space-4);
  padding: 0;
  margin: 0;
  list-style: none;
  font-size: var(--bwz-text-sm);
}

.bwz-sg-section { scroll-margin-top: var(--bwz-space-5); }
.bwz-sg-section + .bwz-sg-section { margin-top: var(--bwz-space-8); }
.bwz-sg-section__head {
  border-bottom: var(--bwz-border-thick) solid var(--bwz-rule);
  padding-bottom: var(--bwz-space-2);
  margin-bottom: var(--bwz-space-5);
}

.bwz-sg-demo {
  padding: var(--bwz-space-5);
  border: var(--bwz-border) dashed var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
}
.bwz-sg-demo + .bwz-sg-demo { margin-top: var(--bwz-space-4); }
.bwz-sg-demo__label {
  font-size: var(--bwz-text-xs);
  letter-spacing: var(--bwz-tracking-wide);
  text-transform: uppercase;
  color: var(--bwz-fg-subtle);
  margin-bottom: var(--bwz-space-3);
}

.bwz-sg-swatch {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-3);
  padding: var(--bwz-space-2);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-md);
  background: var(--bwz-surface);
}
.bwz-sg-swatch__chip {
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  border-radius: var(--bwz-radius-md);
  border: var(--bwz-border) solid var(--bwz-rule-strong);
}
.bwz-sg-swatch__name { font-weight: var(--bwz-weight-semibold); font-size: var(--bwz-text-sm); }
.bwz-sg-swatch__hex {
  font-family: var(--bwz-font-mono);
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-muted);
  text-transform: uppercase;
}

.bwz-sg-scale { display: flex; align-items: baseline; gap: var(--bwz-space-4); flex-wrap: wrap; }
.bwz-sg-scale__token {
  font-family: var(--bwz-font-mono);
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-subtle);
  min-width: 9rem;
}
.bwz-sg-space__bar {
  height: 1rem;
  background: var(--bwz-accent);
  border-radius: var(--bwz-radius-sm);
}
.bwz-sg-dont { color: var(--bwz-danger); font-weight: var(--bwz-weight-semibold); }
.bwz-sg-do { color: var(--bwz-ok); font-weight: var(--bwz-weight-semibold); }

/* A darkened photograph strip, to show the one place the white mark is used
 * over an image. No real photograph is shipped for this. */
.bwz-sg-photo {
  display: grid;
  place-items: center;
  min-height: 9rem;
  border-radius: var(--bwz-radius-lg);
  background:
    linear-gradient(rgba(15, 22, 38, 0.62), rgba(15, 22, 38, 0.62)),
    repeating-linear-gradient(45deg, #5b6b86 0 14px, #74849e 14px 28px);
}

/* ---------------------------------------------------------------------------
 * 17. Dashboard
 *
 * Layout only. Every colour here is a role token that section 2 already
 * defines -- there is no new hex in this section and there must never be one,
 * because the traffic light on the dashboard has to be the same green as the
 * badge on the styleguide or the two stop meaning the same thing.
 *
 * The status itself is painted by .bwz-badge--ok / --warn / --danger and
 * .bwz-dot, which existed before this page did. What is added is the tile's
 * status edge and two small typographic helpers, and nothing else: a dashboard
 * that grows its own component set is a second design system.
 * ------------------------------------------------------------------------- */

/* Tighter than .bwz-grid: a status tile carries a label, a badge and a
 * sentence, so it wants to be narrower than a marketing card. auto-fit with a
 * min() floor, so 320px falls out of it rather than needing a breakpoint. */
.bwz-dash-grid {
  display: grid;
  gap: var(--bwz-space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

/* The traffic light again, as an edge on the card. Redundant with the badge on
 * purpose: the badge carries the word for anyone who cannot rely on the
 * colour, and the edge is what makes a grid of twenty tiles scannable. */
.bwz-tile { border-left: var(--bwz-space-1) solid var(--bwz-rule-strong); }
.bwz-tile--ok { border-left-color: var(--bwz-ok); }
.bwz-tile--warn { border-left-color: var(--bwz-warn); }
.bwz-tile--danger { border-left-color: var(--bwz-danger); }
.bwz-tile--neutral { border-left-color: var(--bwz-rule-strong); }

/* Ages, intervals and counts. Tabular figures so a column of them lines up. */
.bwz-dash-meta {
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-subtle);
  font-variant-numeric: tabular-nums;
}

/* Name/value pairs in a check's detail. The first column has a min() floor so
 * it collapses rather than pushing the page sideways at 320px, and the second
 * is minmax(0, 1fr) so a long URL wraps inside its cell instead of widening
 * the grid. No media query: the same rule as .bwz-grid. */
.bwz-dl {
  display: grid;
  grid-template-columns: minmax(min(9rem, 45%), auto) minmax(0, 1fr);
  gap: var(--bwz-space-1) var(--bwz-space-4);
  margin: 0;
}
.bwz-dl dt {
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-sm);
}
.bwz-dl dd {
  margin: 0;
  font-size: var(--bwz-text-sm);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
 * 17a. Dashboard graphics (869f03448)
 *
 * Dan's brief: "Across the whole management site, strongly prefer graphics over
 * text so for example a CPU or disc percentage should be displayed with graphs
 * or illustrations and not text, although text should usually be overlay, for
 * example a status bar showing 34% used."
 *
 * [CRITICAL] Section 17 says a dashboard that grows its own component set is a
 * second design system, and that is still true. What is added here is three
 * components and no fourth, every colour is a role token section 2 already
 * defines, and there is no new hex — because on this dashboard a colour MEANS
 * "this check is failing", and a graphic that invented a green would be a
 * monitoring defect rather than a styling choice.
 *
 * [CRITICAL] None of these rules decides a colour or a length. Both come from
 * the template, which gets them from ops/models.py:CheckState.meter() and
 * ops/rollup.py:tally_bar(). There is deliberately no `.bwz-meter--over-80`
 * here: ops/registry.py owns what counts as too much disk, and a bar that
 * graded itself would be a second authority disagreeing with the first one on
 * the same tile.
 *
 * Everything holds at 320px with no media query, which is the same bar the rest
 * of this file is held to.
 * ------------------------------------------------------------------------- */

/* A measured percentage, drawn, with its own number on top of it. The track has
 * a fixed height rather than a line-height so the bar is the same size whether
 * or not the overlaid figure wraps — which it cannot, hence the nowrap. */
.bwz-meter { width: 100%; }
.bwz-meter__track {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 1.5rem;
  border-radius: var(--bwz-radius-sm);
  background: var(--bwz-surface-3);
  overflow: hidden;
}
.bwz-meter__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  /* Neutral is the floor, not a colour of its own: a fill with no status is a
   * fill that should not have been drawn, and ops/models.py:CheckState.meter
   * returns None for every such reading. This is here so a template bug shows
   * up as a grey bar rather than an invisible one. */
  background: var(--bwz-rule-strong);
}
.bwz-meter__fill--ok { background: var(--bwz-ok); }
.bwz-meter__fill--warn { background: var(--bwz-warn); }
.bwz-meter__fill--danger { background: var(--bwz-danger); }
.bwz-meter__fill--neutral { background: var(--bwz-rule-strong); }

/* The figure, over the bar. It sits above the fill in the stacking order and
 * takes the page's own foreground colour, never a colour chosen to contrast
 * with the fill: the fill is a status colour and the pairs in core/design.py are
 * what guarantee a readable combination in both themes. Inset from the start
 * edge so a bar at 0% still has its number beside it rather than off it. */
.bwz-meter__value {
  position: relative;
  z-index: 1;
  padding-inline: var(--bwz-space-2);
  font-size: var(--bwz-text-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--bwz-fg);
}

/* The mix of statuses in one card, as one strip. flex-grow carries the
 * proportion, so the segments always fill the row exactly and no width has to
 * be rounded — which is what stops a 91-check strip ending 0.3px short. */
.bwz-tallybar {
  display: flex;
  gap: 1px;
  height: var(--bwz-space-2);
  border-radius: var(--bwz-radius-sm);
  overflow: hidden;
}
.bwz-tallybar__seg { flex-basis: 0; min-width: 2px; background: var(--bwz-rule-strong); }
.bwz-tallybar__seg--ok { background: var(--bwz-ok); }
.bwz-tallybar__seg--warn { background: var(--bwz-warn); }
.bwz-tallybar__seg--danger { background: var(--bwz-danger); }
.bwz-tallybar__seg--neutral { background: var(--bwz-rule-strong); }

/* The few key readings on a product card. auto-fit with a min() floor for the
 * same reason .bwz-dash-grid uses one: 320px falls out of it rather than
 * needing a breakpoint, and a card with two metrics and a card with four lay
 * out on the same rule. */
.bwz-metrics {
  display: grid;
  gap: var(--bwz-space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(7rem, 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.bwz-metric {
  padding-inline-start: var(--bwz-space-3);
  border-inline-start: var(--bwz-border) solid var(--bwz-rule-strong);
}
.bwz-metric--ok { border-inline-start-color: var(--bwz-ok); }
.bwz-metric--warn { border-inline-start-color: var(--bwz-warn); }
.bwz-metric--danger { border-inline-start-color: var(--bwz-danger); }
.bwz-metric--neutral { border-inline-start-color: var(--bwz-rule-strong); }
.bwz-metric > * + * { margin-top: var(--bwz-space-1); }
.bwz-metric__label {
  display: block;
  font-size: var(--bwz-text-xs);
  color: var(--bwz-fg-muted);
}
.bwz-metric__figure {
  margin: 0;
  font-size: var(--bwz-text-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
/* A metric with no reading. Same size as a figure so the row does not jump when
 * a credential arrives, and muted so it does not read as a number. */
.bwz-metric__figure--none {
  font-size: var(--bwz-text-sm);
  font-weight: 500;
  color: var(--bwz-fg-muted);
}

/* An expandable section. <details>, not a script: a blocked script must never
 * be able to hide something on this page, which is the same rule the header
 * navigation follows in reverse (section 15). */
.bwz-disclosure {
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
}
.bwz-disclosure__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--bwz-space-2) var(--bwz-space-4);
  padding: var(--bwz-space-4) var(--bwz-space-5);
  cursor: pointer;
}
/* No :focus-visible rule here: section 6's global one already rings every
 * focusable element with the accent, and a second ring on one element is a
 * second thing to keep in step with the contrast pairs in core/design.py. */
.bwz-disclosure__body {
  padding: 0 var(--bwz-space-5) var(--bwz-space-5);
}

/* ---------------------------------------------------------------------------
 * 17b. The product card's grey footer, lined up across a row (869f2a694)
 *
 * Dan, 2026-09-15: "Make the bottom section of the product cards uniform
 * height so the grey parts line up across all cards."
 *
 * MEASURED, not guessed. Six product cards at 1680px are one row of cards of
 * EQUAL height whose footers were 75px, 111px, 115px and 151px tall. The band
 * is bottom-anchored (margin-top: auto, section 10), so a taller footer starts
 * HIGHER up its card, and four grey bands began at four different heights on
 * one row. The height varies because the contents vary: a link, sometimes an
 * Admin button, sometimes one or two shell windows, each wrapping onto a line
 * of its own in a card this narrow.
 *
 * [CRITICAL] The fix is SUBGRID, and deliberately NOT a height somebody typed.
 * A card is two rows of the products grid -- body, then footer -- so the footer
 * track is sized by the tallest footer on that row and every card's band is
 * exactly that. Nothing here is declared in pixels, so a seventh product, a
 * second shell button or a longer label re-aligns the row instead of
 * overflowing a constant, and the rule cannot go stale the way a number would.
 *
 * ⚠️ It cannot line cards up across TWO rows of the grid: each row sizes its
 * own tracks, so a card left alone on a second row still sizes its own band.
 * That is the honest limit of doing this in CSS, and the alternative -- a fixed
 * height -- buys the second row by making every band wrong the first time the
 * contents change.
 *
 * Scoped to .bwz-dash-grid--aligned. .bwz-dash-grid is also the systems grid
 * and the checks grid, whose tiles have no footer at all: a shared component
 * quietly changing shape under every other page is what CLAUDE.md section 6
 * calls a monitoring defect rather than a design change.
 *
 * @supports, because the fallback must be the OLD layout rather than half of
 * the new one -- display: grid with grid-row: span 2 and no subgrid would put a
 * card's body and its footer in two different implicit rows.
 * ------------------------------------------------------------------------- */

@supports (grid-template-rows: subgrid) {
  .bwz-dash-grid--aligned {
    /* The gap between the two tracks of ONE card is the same row-gap as the
     * gap between two rows of cards -- grid has one value for both -- and a
     * strip of card background between a card's body and its grey footer is
     * worse than what this rule is fixing. So the row gap moves onto the card
     * as a margin, where it separates cards and nothing else. */
    row-gap: 0;
  }
  .bwz-dash-grid--aligned > .bwz-card {
    grid-row: span 2;
    display: grid;
    grid-template-rows: subgrid;
    margin-bottom: var(--bwz-space-4);
  }
  /* The footer track is already the bottom of the card and a grid item
   * stretches into its track, which is what makes the GREY fill the band
   * rather than the card's own background showing through under a short one.
   * margin-top: auto belongs to the flex layout this replaces. */
  .bwz-dash-grid--aligned > .bwz-card > .bwz-card__footer { margin-top: 0; }
}

/* ---------------------------------------------------------------------------
 * 17c. The product card's action band, drill-down link and site icon
 *      (869f2a694, Dan 2026-09-16)
 *
 * [CRITICAL] A SEPARATE SECTION FROM 17b ON PURPOSE. ops/tests.py's
 * test_the_product_cards_line_their_grey_footers_up_and_do_it_with_subgrid
 * reads §17b as a block and asserts two things about every line in it: that no
 * rule declares a height, and that every selector is scoped to
 * .bwz-dash-grid--aligned. Both are right for the subgrid and wrong for what is
 * below, which is a 2rem favicon and a flex row that has to behave the same on
 * a product's own page where there is no aligned grid at all. Merging the two
 * sections makes that test either fail or have to be softened, and softening it
 * is how the typed band height comes back.
 *
 * Three rules, from one comment of Dan's:
 *
 *   "please move the ''Asset giant in detail' link to the bottom of the white
 *    section above the grey section. Also, reduce the width of the PowerShell
 *    buttons, so that free buttons, including the admin link button, should fit
 *    in a single (grey), one-row box."
 *
 *   "Duplicate the favicon site images to the top of the cards, this time not
 *    in buttons, literally just the favicons, on the same row as the card title
 *    but aligned right. Make them approx 2x higher than the favicons appear in
 *    the admin buttons (to which: make no change). Link the favicons to the
 *    front user-facing pages for each"
 *
 * The one-row band needed BOTH halves: moving the link out, and narrowing the
 * buttons (§24). Either alone leaves two rows.
 * ------------------------------------------------------------------------- */

/* The band, as ONE wrapping row of buttons (869f2a694, Dan 2026-09-16: "reduce
 * the width of the PowerShell buttons, so that free buttons, including the
 * admin link button, should fit in a single (grey), one-row box").
 *
 * Two things had to change together and neither works alone. The drill-down
 * link moved into the white half above (.bwz-card__drilldown), which is the
 * other half of the same instruction; and the band became a flex row, because
 * the shell buttons arrive inside .bwz-pwsh-group -- a block-level flex
 * container -- which took a line of its own under the Admin button at every
 * width. Narrowing the buttons on their own would have produced a narrower
 * second row, not one row.
 *
 * NOT scoped to --aligned: the same band is drawn on a product's own page,
 * where the cards are not a subgrid, and a row of buttons that behaved
 * differently in two places would be the defect this include exists to stop.
 * It is scoped to the MODIFIER, so .bwz-card__footer everywhere else in the
 * project is untouched.
 *
 * align-items: center rather than baseline: an Admin button and a picture of a
 * terminal have no shared baseline worth aligning to.
 */
.bwz-card__footer--actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2);
}
/* The last four pixels, and they were MEASURED before they were taken. At
 * 820px the products grid is three columns of 247px, which leaves a 194px
 * band, and the Admin button plus two shell windows plus their gaps came to
 * 194.4px -- so the band wrapped on a fraction of a pixel in Chromium and
 * Firefox while WebKit rounded the other way and did not. Halving the gap
 * between the button's favicon and its word gives 4px of headroom.
 *
 * ⚠️ SCOPED TO THE BAND. .bwz-btn--faviconned is also the Admin button on a
 * product's own page, where there is room and no reason to tighten it, and the
 * FAVICON ITSELF is untouched -- Dan's instruction of 2026-09-16 about the
 * admin buttons' icons was "to which: make no change". */
.bwz-card__footer--actions .bwz-btn--faviconned { gap: var(--bwz-space-1); }

/* The drill-down link, last in the white half. margin-top: auto pushes it to
 * the bottom of the body, so a card with three metrics and a card with six put
 * it in the same place relative to their grey band. .bwz-stack's own rule sets
 * the top margin; this only says which way the slack goes. */
.bwz-card__drilldown {
  margin-top: auto;
  padding-top: var(--bwz-space-2);
}
/* .bwz-stack sets margin-top on every child after the first, which would win
 * over the auto above. Restated at the same specificity the stack uses. */
.bwz-stack > .bwz-card__drilldown { margin-top: auto; }

/* [CRITICAL] …and NEITHER of those two rules did anything at all until this one
 * was written (869f2a694, Dan 2026-09-18: "the 'asset giant in detail' links
 * should be vertically aligned to the bottom of the white box which they now
 * correctly occupy, so that all of them line up just above the grey boxes").
 *
 * MEASURED BEFORE IT WAS WRITTEN, and measured again after. At 1680px the six
 * links ended at six different y values in Chromium, Firefox and WebKit alike;
 * the drill reports it per row of the grid.
 *
 * The reason is one line of CSS that was never there: `.bwz-card__body` is a
 * BLOCK box and `.bwz-stack` spaces its children with margins rather than with
 * flex, and `margin-top: auto` in block layout computes to ZERO. Nothing was
 * pushing anything anywhere. The body has to be a flex column before "give me
 * the slack above" is a sentence the engine can act on -- and the body already
 * stretches to the full height of its row, because §17b's subgrid makes it a
 * grid item in a track sized by the tallest body on the row. That is what turns
 * "bottom of my own card" into "the same line across the row", which is the
 * half of the ask a card on its own could never satisfy.
 *
 * :has(), so the rule pairs itself with the element it exists for instead of
 * with a class somebody has to remember to add to the next card. An engine
 * without :has() drops the whole rule and gets exactly the 14.12.0 layout --
 * the same shape of fallback §17b takes with @supports, and for the same
 * reason: half of this is worse than none of it.
 *
 * NOT scoped to --aligned. The white half of a card is the white half of a card
 * wherever it is drawn, and a link that sat at the bottom on the roll-up and
 * half way up on a product's own page would be the drift this file keeps
 * being fixed for. */
.bwz-card__body:has(> .bwz-card__drilldown) {
  display: flex;
  flex-direction: column;
}

/* The product's own favicon, top-right of the card's title row (869f2a694,
 * Dan 2026-09-16: "on the same row as the card title but aligned right. Make
 * them approx 2x higher than the favicons appear in the admin buttons (to
 * which: make no change)").
 *
 * 2rem against .bwz-btn__favicon's 1rem, which is the "approx 2x" literally and
 * is left as a separate rule so that changing one cannot change the other --
 * "to which: make no change" is an instruction about the button.
 *
 * [CRITICAL] NO STATUS COLOUR, no tile class and no border that could read as
 * one. Everything else on this card is a measurement and colour here means a
 * check is failing; a declared URL is not a reading. */
.bwz-card__titlerow {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--bwz-space-3);
}
.bwz-card__titlerow > :first-child { min-width: 0; }
.bwz-card__siteicon {
  flex: none;
  display: inline-flex;
  line-height: 0;
  border-radius: var(--bwz-radius-sm);
  text-decoration: none;
  transition: opacity var(--bwz-duration) var(--bwz-ease);
}
.bwz-card__siteicon:hover,
.bwz-card__siteicon:focus-visible { opacity: 0.75; }
.bwz-card__siteicon img {
  width: 2rem;
  height: 2rem;
  border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .bwz-card__siteicon { transition: none; }
}

/* ---------------------------------------------------------------------------
 * 17d. What a COLLAPSED section is holding, on its own closed row
 *      (869f2a694, Dan 2026-09-18)
 *
 * "I do appreciate this rule, however I do want to see that the sections are
 *  all collapsed when I come to look at the page. I think the way to resolve
 *  this is to put on the collapsed card state, aligned to the right, the pills
 *  which show on the hidden cards."
 *
 * [CRITICAL] THIS IS THE PRICE OF THE REMEMBERED COLLAPSE, PAID RATHER THAN
 * ARGUED AWAY. bwz-disclosure.js may remember that a reader closed a section,
 * and the rule it was bought under is that a blocked script must never be able
 * to hide anything. A closed section that says nothing about its contents makes
 * that rule technically true and practically false. So the closed row now
 * carries the colours, and collapsing the page costs the detail rather than the
 * reading.
 *
 * Aligned right with an auto inline-start margin rather than with
 * space-between: the summary is a WRAPPING flex row of three other things, and
 * space-between would spread those three apart at every width instead of
 * pushing one group to the end.
 *
 * ⚠️ display: none here is on the OPEN state only, and it hides nothing: what
 * it drops is a DUPLICATE of pills that are on screen a few pixels below it.
 * With no stylesheet at all both are shown, which is the right way round. No
 * rule in this file may ever hide the contents of a section.
 * ------------------------------------------------------------------------- */

.bwz-disclosure__pills {
  margin-inline-start: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2);
}
.bwz-disclosure[open] > .bwz-disclosure__summary > .bwz-disclosure__pills { display: none; }

/* ---------------------------------------------------------------------------
 * 20. The discovery questionnaire
 *
 * A private, link-only page (content/discovery_views.py), so none of this is
 * reachable from the marketing site and none of it is reused by it. It is here
 * rather than in a second stylesheet because there is one stylesheet, and
 * because every rule below is built out of the tokens in section 2 — a page
 * somebody answers at night has to work in both themes like everything else.
 *
 * Everything holds at 320px with no media query. The section tabs scroll
 * inside themselves rather than widening the page, which is the same rule
 * .bwz-table-wrap follows.
 * ------------------------------------------------------------------------- */

.bwz-quest { max-width: 52rem; }
.bwz-quest > * + * { margin-top: var(--bwz-space-4); }

/* The section tabs. Submit buttons, not links: clicking one saves the section
 * being left rather than discarding it, so they have to be part of the form. */
.bwz-quest__steps {
  display: flex;
  gap: var(--bwz-space-1);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
  scrollbar-width: thin;
  /* [CRITICAL] `overflow-x: auto` alone is not enough here, and the reason is
   * worth writing down because it looks like a bug in the test rather than in
   * the page. `.bwz-body` sets `overflow-x: hidden`, and because <html> is
   * `visible` that value is *propagated to the viewport* -- which leaves the
   * body itself computing as `visible`. The fourteen tabs then contribute their
   * full 2.7 metres to the document's scrollable overflow, so
   * `documentElement.scrollWidth` reads 2570 in a 768px viewport even though
   * `window.scrollX` cannot move off zero. `test_the_page_does_not_scroll_
   * sideways` measures exactly that number, correctly, and would fail.
   * `contain: paint` ends the propagation at this box: nothing changes visually
   * and the tabs still scroll. Measured in Chromium on 2026-09-04; `max-width`
   * and `width: 100%` both make no difference, and clipping an ancestor does
   * not help either. */
  contain: paint;
}
.bwz-quest__step {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-2);
  min-height: 44px;
  padding: var(--bwz-space-2) var(--bwz-space-3);
  border: 0;
  border-bottom: var(--bwz-border-thick) solid transparent;
  background: none;
  color: var(--bwz-fg-muted);
  font: inherit;
  font-size: var(--bwz-text-sm);
  white-space: nowrap;
  cursor: pointer;
}
.bwz-quest__step:hover { color: var(--bwz-fg); }
/* aria-current, not a class: the state a screen reader is told and the state a
 * sighted reader sees are then the same fact. */
.bwz-quest__step[aria-current="step"] {
  color: var(--bwz-accent);
  border-bottom-color: var(--bwz-accent);
  font-weight: var(--bwz-weight-semibold);
}

/* The ring carries a shape as well as a colour: never colour alone. */
.bwz-quest__ring {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.1rem;
  height: 1.1rem;
  border: var(--bwz-border-thick) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-full);
  font-size: 0.6rem;
  line-height: 1;
  color: transparent;
}
.bwz-quest__ring[data-done="part"] {
  border-color: var(--bwz-warn);
  background: var(--bwz-warn-surface);
}
.bwz-quest__ring[data-done="all"] {
  border-color: var(--bwz-ok);
  background: var(--bwz-ok);
  color: var(--bwz-bg);
}

.bwz-quest__bar {
  height: 4px;
  border-radius: var(--bwz-radius-full);
  background: var(--bwz-surface-3);
  overflow: hidden;
}
.bwz-quest__bar > span {
  display: block;
  height: 100%;
  background: var(--bwz-accent);
  transition: width var(--bwz-duration) var(--bwz-ease);
}

.bwz-quest__meta {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-3);
  margin: 0;
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-variant-numeric: tabular-nums;
}
.bwz-quest__saved {
  padding: 0.1rem var(--bwz-space-3);
  border-radius: var(--bwz-radius-full);
  background: var(--bwz-ok-surface);
  color: var(--bwz-ok);
  font-size: var(--bwz-text-xs);
  font-weight: var(--bwz-weight-semibold);
}

.bwz-quest__head { margin-bottom: var(--bwz-space-5); }
.bwz-quest__eyebrow {
  margin: 0;
  font-family: var(--bwz-font-mono);
  font-size: var(--bwz-text-xs);
  letter-spacing: var(--bwz-tracking-wide);
  text-transform: uppercase;
  color: var(--bwz-fg-subtle);
}
.bwz-quest__title {
  margin: var(--bwz-space-1) 0 0;
  font-size: var(--bwz-text-xl);
  letter-spacing: var(--bwz-tracking-tight);
}
.bwz-quest__blurb {
  margin: var(--bwz-space-2) 0 0;
  max-width: var(--bwz-measure);
  color: var(--bwz-fg-muted);
}

.bwz-quest__form > * + * { margin-top: var(--bwz-space-4); }

/* One question. A fieldset, because a choice question is a group of inputs and
 * its legend is the question — which is what a screen reader announces. */
.bwz-quest__q {
  margin: 0;
  padding: var(--bwz-space-4);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
}
.bwz-quest__q > * + * { margin-top: var(--bwz-space-3); }
.bwz-quest__label {
  padding: 0;
  font-weight: var(--bwz-weight-semibold);
  font-size: var(--bwz-text-md);
  line-height: var(--bwz-leading-snug);
}
.bwz-quest__opts > .bwz-check + .bwz-check { margin-top: var(--bwz-space-2); }

.bwz-quest__other > * + * { margin-top: var(--bwz-space-2); }

.bwz-quest__files {
  margin: 0;
  padding: 0;
  list-style: none;
}
.bwz-quest__files li {
  display: flex;
  justify-content: space-between;
  gap: var(--bwz-space-3);
  padding: var(--bwz-space-2) 0;
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
  font-size: var(--bwz-text-sm);
  overflow-wrap: anywhere;
}
.bwz-quest__size {
  flex: none;
  color: var(--bwz-fg-subtle);
  font-size: var(--bwz-text-sm);
  font-variant-numeric: tabular-nums;
}
.bwz-quest__upload > * + * { margin-top: var(--bwz-space-2); }

.bwz-quest__nav {
  padding-top: var(--bwz-space-4);
  border-top: var(--bwz-border) solid var(--bwz-rule);
}
.bwz-quest__nav > * + * { margin-top: var(--bwz-space-3); }
.bwz-quest__count {
  margin: 0;
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-variant-numeric: tabular-nums;
}
.bwz-quest__foot { max-width: var(--bwz-measure); }

.bwz-quest__summary li {
  display: flex;
  justify-content: space-between;
  gap: var(--bwz-space-3);
  padding: var(--bwz-space-2) 0;
  border-bottom: var(--bwz-border) solid var(--bwz-rule);
}

/* ---------------------------------------------------------------------------
 * 21. The headless runners page (869f03368)
 *
 * One full-width row per runner, about two lines tall closed: a disclosure
 * triangle, the label, the status pill, ten words of state, and the one-line
 * "new ticket" box. Everything else is behind the triangle.
 *
 * <details> and <summary>, never a script. The header navigation is the one
 * place on this site where JavaScript is allowed to hide something, and it is
 * allowed there because it also *creates* the thing it hides (section 15). Here
 * a blocked script must leave every runner readable, and with native
 * disclosure it does — the rows simply all open.
 *
 * Layout only, as section 17: every colour below is a role token from section 2
 * and the status edge is .bwz-tile--*, which the dashboard already owns. A page
 * that grows its own greens is a page whose green means something else.
 * ------------------------------------------------------------------------- */

.bwz-runner {
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-lg);
  background: var(--bwz-surface);
  /* [CRITICAL] ONE rule for the whole card, because the card is full of strings
   * this repo did not write and cannot shorten: ticket titles from the board,
   * env key names, absolute paths, pause reasons. `overflow-wrap` inherits, so
   * every descendant gets it from here.
   *
   * `anywhere`, never `break-word`. Only `anywhere` reduces a box's MIN-CONTENT,
   * and min-content is what a flex item uses as its automatic minimum — with
   * `break-word` the text wraps and the row still refuses to be narrower than
   * `BWZ_RUNNER_AGENT_SECRET_VENUE_VIEW`, so the page scrolls sideways instead.
   *
   * Measured on the live board at 320px on 2026-09-11, three engines, with
   * every disclosure open: 56px of horizontal scroll from three independent
   * offenders — a ticket title, the dead-control reason, and the env key in the
   * "declared, not enrolled" callout. All three predate 12.2.0; no test had
   * reached this page at 320 until then. */
  overflow-wrap: anywhere;
}
.bwz-runner + .bwz-runner { margin-top: var(--bwz-space-3); }

/* Row one. `align-items: center` rather than baseline: the pill and the
 * triangle are boxes, not text, and baseline puts them half a line high. */
.bwz-runner__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2) var(--bwz-space-3);
  padding: var(--bwz-space-3) var(--bwz-space-4);
  cursor: pointer;
}
/* The default marker is a different glyph in every engine and cannot be
 * rotated. Ours is one span, drawn with a border, so it is identical in all
 * four browsers and takes currentColor in both themes. */
.bwz-runner__summary::-webkit-details-marker { display: none; }
.bwz-runner__summary { list-style: none; }
.bwz-runner__arrow {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 150ms ease;
}
[open] > .bwz-runner__summary .bwz-runner__arrow { transform: rotate(45deg); }

/* The ClickUp button on the title row (869f29wfz, Dan 2026-09-15). `margin-left:
 * auto` rather than relying on the headline to push it: the headline is
 * `flex: 1 1 12rem` and normally eats the slack, but a card whose headline is
 * short and whose row has wrapped would otherwise leave the button mid-row.
 * `flex: none` so a two-word label never shrinks to "Click…".
 *
 * It is the one control inside a <summary> on this page. Nothing here stops the
 * card toggling -- the DOM does, by choosing the LINK as the activation target
 * -- and no CSS could, which is why the reasoning is in the template beside the
 * markup and not in this comment. */
.bwz-runner__clickup { flex: none; }

/* The title row's right-hand cluster (869f29wfz, Dan 2026-09-18: "Move the
 * shell button icon to the top row so it is next to the click up button").
 *
 * `margin-left: auto` moved OFF the ClickUp button and onto the cluster, for
 * the reason the button carried it in the first place: the headline is
 * `flex: 1 1 12rem` and normally eats the slack, but a card with a short
 * headline, or a row that has wrapped, would leave the cluster mid-row. On the
 * cluster it also survives a card that declares no queue — Venue View — where
 * the shell buttons would otherwise be the ones needing it.
 *
 * It wraps, because at 320px "ClickUp" plus two 40px windows does not fit
 * beside a runner's name, and dropping to the next line is the correct failure. */
.bwz-runner__titletools {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2);
  margin-left: auto;
}
/* A plain wrapper round the shared include, so the group is a flex item of the
 * cluster and nothing about ops/_shell_buttons.html changes between the two
 * pages that use it. */
.bwz-runner__shell { display: inline-flex; align-items: center; }

/* The note beside them. Dan asked for the words to go — "so it shows only the
 * 'i' in a circle" — so the padding tightens to a circle rather than a pill,
 * and the accessible name is an aria-label in the markup. It is a DIRECT child
 * of the summary, which is what lets .bwz-info[open]'s `flex: 1 0 100%` give
 * the open panel the whole row instead of the right-hand cluster's width.
 *
 * [CRITICAL] It is __sshnote and NOT __shellnote. Three tests count
 * `bwz-runner__shell` with `str.count` to prove there is exactly ONE shell row
 * per card, and a class that CONTAINS that string counts as a second one — the
 * same substring trap the runner's own RUNNER-CLAIM / RUNNER-HANDOVER markers
 * are written against. Do not "tidy" the name. */
.bwz-runner__sshnote > summary { padding: 0.1rem; border-radius: 50%; }

/* The reason a card has NO shell button, on the second row where a sentence
 * fits. Right-aligned with the model line it sits above. */
.bwz-runner__noshell { margin: 0; text-align: right; }
@media (prefers-reduced-motion: reduce) {
  .bwz-runner__arrow { transition: none; }
}

.bwz-runner__label {
  font-weight: 600;
  font-size: var(--bwz-text-md);
}
/* Ten words, one line where there is room. `min-width: 0` is what lets it
 * shrink inside the flex row instead of pushing the pill off at 320px. */
.bwz-runner__headline {
  flex: 1 1 12rem;
  min-width: 0;
  margin: 0;
  color: var(--bwz-fg-muted);
  font-size: var(--bwz-text-sm);
}

/* Row two: the title box and its button, side by side above 30rem and stacked
 * below it. No media query — the wrap does it. */
.bwz-runner__new {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--bwz-space-2) var(--bwz-space-3);
  padding: 0 var(--bwz-space-4) var(--bwz-space-3);
}
.bwz-runner__new .bwz-field {
  flex: 1 1 16rem;
  min-width: 0;
  margin: 0;
}
/* ...except the complexity select, which is a word and not a sentence
 * (869f29wfz). Without this it inherits `flex: 1 1 16rem` from the rule above
 * and takes half the row from the title box, which is the control the row is
 * for. It still SHRINKS -- `1` as the flex-shrink -- so the two share what is
 * left at 320px rather than one of them forcing a horizontal scroll. */
.bwz-runner__new .bwz-runner__newcomplexity { flex: 0 1 9rem; }
/* The expanded quick-add's second row: the description on the left, the two
 * selects on the right, the submit buttons below (869f1qw3v -- Dan: "put it on
 * the same row as the description field, on the right, just before the create
 * button"). No media query: the wrap does it, exactly as .bwz-runner__new does,
 * so below ~34rem the selects drop underneath and nothing is clipped. */
.bwz-runner__addrow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--bwz-space-2) var(--bwz-space-3);
}
.bwz-runner__addrow > .bwz-field {
  flex: 1 1 18rem;
  min-width: 0;
  margin: 0;
}
.bwz-runner__addcol {
  flex: 0 1 14rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--bwz-space-2);
}
.bwz-runner__addcol > .bwz-field { margin: 0; }
.bwz-runner__body {
  padding: 0 var(--bwz-space-4) var(--bwz-space-4);
  border-top: var(--bwz-border) solid var(--bwz-rule);
}
.bwz-runner__body > * { margin-top: var(--bwz-space-4); }

/* A nested disclosure — "the other fourteen", "recent runs", "recent ticks".
 * Quieter than the row itself so that two levels of triangle do not read as
 * two levels of importance. */
.bwz-runner__more {
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-md);
}
.bwz-runner__more > summary {
  display: flex;
  align-items: center;
  gap: var(--bwz-space-2);
  padding: var(--bwz-space-2) var(--bwz-space-3);
  cursor: pointer;
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  list-style: none;
}
.bwz-runner__more > summary::-webkit-details-marker { display: none; }
.bwz-runner__more > div {
  padding: 0 var(--bwz-space-3) var(--bwz-space-3);
}

/* The ticket list. Tabular so a column of ids lines up, and the id is allowed
 * to wrap rather than widening the page at 320px. */
.bwz-runner__ticket {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bwz-space-1) var(--bwz-space-2);
  align-items: baseline;
}
/* `min-width` does NOT inherit, so every flex row inside a card that holds a
 * string from outside needs it spelled out. The wrapping half comes from
 * .bwz-runner above. */
.bwz-runner__ticket > *,
.bwz-runner__controls > * {
  min-width: 0;
}
.bwz-runner__ticket-when {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-variant-numeric: tabular-nums;
}

/* The one region of a card that may name an environment key. It is a wrapper
 * with a job rather than a look: 869f061h5 went red because an assertion about
 * env keys was scoped to a whole runner card, whose queue listing carries
 * ticket titles this repo did not write. */
.bwz-runner__requires > * + * { margin-top: var(--bwz-space-3); }

/* Row two of the closed card: the controls and the new-ticket box, side by
 * side. Dan's ask of 2026-09-11 — "move the stop / go buttons in the collapsed
 * view (so I see them all on page load)" — so STOP and GO are OUTSIDE the
 * <details>, not duplicated inside it. Wraps rather than scrolls at 320px. */
.bwz-runner__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--bwz-space-2) var(--bwz-space-3);
  padding: 0 var(--bwz-space-4) var(--bwz-space-3);
}
.bwz-runner__row .bwz-runner__new {
  flex: 1 1 18rem;
  padding: 0;
}
.bwz-runner__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2);
  /* It is itself a flex item. Without this its automatic minimum is its own
   * max-content and it cannot shrink inside .bwz-runner__row. */
  min-width: 0;
}
/* No `white-space: nowrap`: this carries "Asked to: STOPPED — you pressed this
 * a moment ago", and an unwrappable phrase of that length is a 320px floor
 * broken by a sentence that only appears after somebody presses a button. */
.bwz-runner__asked {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
}

/* The model line, above the new-ticket box on the closed row (869f0mvrp).
 *
 * A column so that "Default: …" sits above the Create button rather than
 * beside it, which is where Dan asked for it. It is a LABEL and not a control,
 * so it takes the muted treatment the rest of the row's helper text takes.
 *
 * `overflow-wrap: anywhere`, never `break-word`: this line carries a model id
 * the account chose, and an unbreakable token of unknown length is the 320px
 * floor broken by a string this repo did not write. Section 12.12.17.3.1 of the
 * encyclopaedia is three defects deep on exactly that. */
.bwz-runner__newcol {
  display: flex;
  flex-direction: column;
  gap: var(--bwz-space-1);
  min-width: 0;
}
.bwz-runner__row .bwz-runner__newcol { flex: 1 1 18rem; }
.bwz-runner__row .bwz-runner__newcol .bwz-runner__new { flex: 0 0 auto; padding: 0; }
/* The shell buttons were on this row from 14.10.0 until 2026-09-18, when Dan
 * moved them up to the title row (869f29wfz). What is left here is the line a
 * card with NO alias declared shows instead — see .bwz-runner__noshell above. */

.bwz-runner__default {
  margin: 0;
  text-align: right;
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
 * 21b. The Claude usage panel (869f0mvrp, 2026-09-12)
 *
 * One tile, one row per limit window the ACCOUNT has reported. There is no
 * colour of its own anywhere in here: the tile takes .bwz-tile--<light> and
 * each window takes a status badge, both graded by ops/registry.py and
 * ops/probes/runner.py. A panel about spending is exactly the place somebody
 * would reach for a "nearly full" amber of its own, and that would be a second
 * grading authority disagreeing with the first one on the same page.
 *
 * The bar is ops/templates/ops/_meter.html, unchanged and unextended — there is
 * no .bwz-meter--over-80 and there must never be one.
 * ------------------------------------------------------------------------ */
.bwz-usage { padding: var(--bwz-space-4); }
.bwz-usage > * + * { margin-top: var(--bwz-space-3); }
.bwz-usage__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2) var(--bwz-space-3);
  min-width: 0;
}
.bwz-usage__summary {
  margin: 0;
  flex: 1 1 16rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* THE COLLAPSE (869f0mvrp, Dan 2026-09-13). The closed row carries the
 * measurement -- a bar per window, the figure inside it and the countdown -- so
 * every window is a flex ROW inside the <summary> rather than a stacked list
 * item. The <details> itself takes no border of its own: it is inside
 * .bwz-tile, and a second box round the same content would read as a second
 * card.
 *
 * The arrow is section 23's generic `.bwz-arrow`, inherited whole from
 * `details[open] > summary .bwz-arrow`, which is why there is no rotation rule
 * here and must not be one.
 *
 * ⚠️ `.bwz-usage__window` sets `flex: 1 1 18rem`, so two windows sit side by
 * side on a wide screen and stack at 320 without a media query. Do not give the
 * bar a fixed width: .bwz-meter is `width: 100%` by design and takes whatever
 * the row has left. */
.bwz-usage__disclosure > .bwz-disclosure__summary {
  padding: 0;
  align-items: center;
}
.bwz-usage__windows { margin: 0; }
.bwz-usage__window {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bwz-space-2);
  min-width: 0;
  flex: 1 1 18rem;
}
.bwz-usage__window .bwz-meter { flex: 1 1 7rem; min-width: 7rem; }
.bwz-usage__body {
  padding-top: var(--bwz-space-3);
  margin-top: var(--bwz-space-3);
  border-top: var(--bwz-border) solid var(--bwz-rule);
}
.bwz-usage__body > * + * { margin-top: var(--bwz-space-2); }
.bwz-usage__sources { margin: 0; }
.bwz-usage__source {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  overflow-wrap: anywhere;
}
.bwz-usage__source + .bwz-usage__source { margin-top: var(--bwz-space-1); }
.bwz-usage__label { font-weight: 600; overflow-wrap: anywhere; }
.bwz-usage__resets {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-variant-numeric: tabular-nums;
}
.bwz-usage__phrase { margin: 0; overflow-wrap: anywhere; }
.bwz-usage__foot { overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
 * 22. The clickable note (869f03368, 2026-09-11)
 *
 * Dan: "move all the information in to clickable tooltips, so it is all still
 * there at my fingertips, but does not take up space on the page unless
 * clicked."
 *
 * [CRITICAL] It is <details>, not a script, and not a :hover tooltip.
 *   - a script: section 21's rule. A blocked script must leave every runner
 *     readable, and with native disclosure it does — every note simply opens.
 *   - :hover: there is no hover on a phone, and "at my fingertips" is literal.
 *     A hover tooltip is a control that does not exist on half the devices the
 *     design system is required to hold at (CLAUDE.md section 6).
 *
 * [CRITICAL] The panel is IN FLOW. It pushes what is below it down when it is
 * open and occupies nothing when it is closed, which is exactly what Dan asked
 * for — and it is deliberately not the overlay this shipped as for one hour on
 * 2026-09-11.
 *
 * The overlay was measured and rejected, in three engines, by
 * `test_an_open_clickable_note_never_pushes_the_runners_page_sideways`: an
 * absolutely positioned panel covers the control it explains, and Playwright
 * reported it exactly — *"<div class=bwz-info__panel> subtree intercepts
 * pointer events"*. Both ways out of that are worse than pushing content down
 * on a page carrying STOP buttons:
 *   - leave it intercepting, and the form a note describes cannot be used while
 *     the note explaining it is open;
 *   - add `pointer-events: none`, and a click on the visible panel lands on
 *     whatever is underneath it — which on this page can be STOP.
 * A note is prose. It never covers a control.
 *
 * It cannot push the page sideways either: the panel's max-width is a measure,
 * a flex item's automatic minimum is its min-content (one word here), and every
 * row it sits in wraps. There is no width to tune.
 * ------------------------------------------------------------------------- */

/* A heading or a line of text with its note alongside it. A wrapper rather
 * than nesting the <details> inside the <p> or the <h3>: `details` is flow
 * content and both of those take phrasing content only, so a parser closes the
 * paragraph before it and the markup a person reads stops matching the DOM the
 * browser built. */
.bwz-noteline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--bwz-space-1) var(--bwz-space-2);
}
.bwz-noteline > p { margin: 0; }

.bwz-info {
  display: inline-block;
  vertical-align: baseline;
}
.bwz-info > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-1);
  padding: 0.1rem var(--bwz-space-2);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-full);
  cursor: pointer;
  font-size: var(--bwz-text-sm);
  line-height: 1.4;
  color: var(--bwz-fg-muted);
  list-style: none;
}
.bwz-info > summary::-webkit-details-marker { display: none; }
.bwz-info > summary:hover,
.bwz-info > summary:focus-visible { color: var(--bwz-fg); }
.bwz-info[open] > summary { color: var(--bwz-fg); }

/* The mark itself. A circled "i" drawn in CSS rather than an icon file: it has
 * to take currentColor in both themes, and core/design.py owns the palette. */
.bwz-info__mark {
  flex: none;
  width: 1.05em;
  height: 1.05em;
  border: var(--bwz-border) solid currentColor;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85em;
  font-style: italic;
  font-weight: 700;
}

.bwz-info__panel {
  margin-top: var(--bwz-space-2);
  max-width: var(--bwz-measure);
  /* A note outside a runner card -- the page footer -- does not inherit the
   * card's wrapping rule, and its prose names env keys and absolute paths. */
  overflow-wrap: anywhere;
  padding: var(--bwz-space-3);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-md);
  background: var(--bwz-surface-2);
  box-shadow: var(--bwz-shadow-md);
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
}
.bwz-info__panel > * + * { margin-top: var(--bwz-space-2); }

/* Open, it takes the row it is on: a paragraph beside a status pill reads as a
 * caption of the pill. Closed, it is a marker the size of a word. */
.bwz-info[open] {
  display: block;
  flex: 1 0 100%;
  min-width: 0;
}

/* A note whose HANDLE is a button rather than a marker (869f7b3b9, 21.2.0).
 *
 * Dan asked for a *button* labelled "Answer sheet - auto login" that sits in
 * the row of buttons on an answer-sheet card. The mechanics wanted are exactly
 * section 22's -- a native <details>, in flow, never a popover, taking the whole
 * row when it is open -- and only the handle's appearance differs, so this is a
 * modifier rather than a second component.
 *
 * `.bwz-info > summary` is (0,2,0) and would beat `.bwz-btn` (0,1,0) on every
 * property the two disagree about, so putting both class lists on one element
 * does not work and is not what this is. The summary carries `.bwz-info__mark`
 * nowhere: a button says what it does in words.
 *
 * The numbers are .bwz-btn--sm's own, read from section 6 rather than guessed,
 * so the handle lines up with the real buttons either side of it. */
.bwz-info--btn > summary {
  min-height: 36px;
  padding: var(--bwz-space-1) var(--bwz-space-3);
  border-width: var(--bwz-border-thick);
  border-radius: var(--bwz-radius-md);
  color: var(--bwz-fg-muted);
  font-weight: var(--bwz-weight-semibold);
}
.bwz-info--btn > summary:hover { background: var(--bwz-surface-2); color: var(--bwz-fg); }

/* The link itself, and the one line that says what happened to it. Both live
 * inside the panel, so a blocked script leaves the link on screen to be
 * selected -- which is the whole fallback. `anywhere` because a token has no
 * spaces in it and a URL that overflows its card is a URL nobody can read. */
.bwz-copy__value {
  display: block;
  padding: var(--bwz-space-2);
  border: var(--bwz-border) solid var(--bwz-rule);
  border-radius: var(--bwz-radius-sm);
  background: var(--bwz-surface);
  font-family: var(--bwz-font-mono);
  font-size: var(--bwz-text-sm);
  overflow-wrap: anywhere;
}
.bwz-copy__status:empty { display: none; }

/* ---------------------------------------------------------------------------
 * 23. Arrows, and the pill that explains itself (869f03448, 2026-09-11)
 *
 * Two of Dan's asks on the dashboard, and they share a component:
 *
 *   "Show arrows on the collapsible cards (to indicate they are collapsible /
 *    expandable)"
 *
 *   "If any pill on the page is not 'Ok', i.e. it is 'Failing' or 'Warning'
 *    then, on hover or click, pop out a small box with more info and a link to
 *    see more detailed info."
 *
 * [CRITICAL] Click, not hover. There is no hover on a phone and 320px is a hard
 * target (CLAUDE.md section 6), so "pop out on hover" is a control that does not
 * exist on half the devices this design system is required to hold at. Every
 * disclosure here is <details>: section 21's rule, because a blocked script must
 * never be able to hide something on this page.
 *
 * [CRITICAL] The panel is IN FLOW and inherits section 22's .bwz-info__panel
 * wholesale, which is where the measurement lives: an absolutely positioned
 * panel covers the control it explains, and on these pages that control can be
 * STOP. A note is prose. Prose never covers a control.
 *
 * ONE arrow, drawn once. There were already three copies of these eight lines
 * -- .bwz-account__arrow, .bwz-runner__arrow and the theme toggle's -- each with
 * its own `[open] > .x__summary` rotation selector because each was written for
 * one component. `.bwz-arrow` is the generic one: the rotation selector matches
 * any <details> whose summary holds it, so the next disclosure needs no CSS at
 * all. The two existing copies are deliberately left alone -- they work, and
 * restyling the runners page is not this ticket.
 * ------------------------------------------------------------------------- */
.bwz-arrow {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--bwz-duration) var(--bwz-ease);
}
details[open] > summary .bwz-arrow { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .bwz-arrow { transition: none; }
}

/* The expandable sections on the roll-up. The arrow is the only change: the
 * summary was already a flex row, so it takes its place without a reflow. */
.bwz-disclosure__summary::marker,
.bwz-disclosure__summary::-webkit-details-marker { display: none; }
.bwz-disclosure__summary { list-style: none; }

/* ---- the pill that explains itself -------------------------------------- */

/* [CRITICAL] The BADGE is the pill, and the summary adds no second outline.
 * Section 22's .bwz-info > summary draws its own bordered capsule, which is
 * right for a note hanging off a heading and wrong here: a capsule around a
 * capsule reads as two controls, and the inner one is the traffic light — the
 * one element on this page whose shape must not become decorative. */
.bwz-info--pill > summary {
  border: 0;
  padding: 0;
  border-radius: 0;
  gap: var(--bwz-space-1);
  /* The badge carries its own colour from ops/models.py:BADGE_CLASS. Section
   * 22's summary sets `color: var(--bwz-fg-muted)`, which would repaint the
   * status word and make a failing pill argue with its own dot. */
  color: inherit;
}
.bwz-info--pill > summary:hover .bwz-info__arrow,
.bwz-info--pill > summary:focus-visible .bwz-info__arrow { color: var(--bwz-fg); }
.bwz-info__arrow {
  flex: none;
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--bwz-duration) var(--bwz-ease);
  color: var(--bwz-fg-muted);
}
details[open] > summary .bwz-info__arrow { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .bwz-info__arrow { transition: none; }
}

/* What the box lists: every not-OK check behind this pill, each one its own
 * link. The left edge carries the check's OWN status colour — ops/rollup.py
 * hands it over as a role token, and there is no threshold anywhere in this
 * stylesheet that could disagree with ops/registry.py about it. */
.bwz-pillnote {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--bwz-space-2);
}
.bwz-pillnote__item {
  padding-inline-start: var(--bwz-space-3);
  border-inline-start: 3px solid var(--bwz-rule-strong);
}
.bwz-pillnote__item--ok { border-inline-start-color: var(--bwz-ok); }
.bwz-pillnote__item--warn { border-inline-start-color: var(--bwz-warn); }
.bwz-pillnote__item--danger { border-inline-start-color: var(--bwz-danger); }
.bwz-pillnote__item--neutral { border-inline-start-color: var(--bwz-rule-strong); }
/* The reading goes under the name rather than beside it. At 320px a check label
 * and its summary on one line is the one place this card has ever overflowed. */
.bwz-pillnote__item > .bwz-subtle { display: block; }

/* ---------------------------------------------------------------------------
 * 24. The shell button — a tiny PowerShell window (869f03448, Dan 2026-09-14)
 *
 * "I'd like these ssh links to be buttons, with the buttons looking like tiny
 * powershell windows … and just 'SSH' for blueworkz /kestrel. They should
 * render approx the same width as a 'Warning' pill currently does."
 *
 * So it is a picture of a window: a title bar with PowerShell's blue mark, and
 * a black screen with one word on it in monospace. The word is Dan's ("Prod",
 * "Dev", "SSH") and is aria-hidden — ops/_shell_buttons.html carries the
 * accessible name, because "Prod" names no machine and two of these sit side
 * by side on Asset Giant.
 *
 * [CRITICAL] It carries NO STATUS COLOUR and it must never acquire one. Every
 * other coloured thing on this dashboard is a measurement. A declared alias is
 * not one, and the vendor chrome above is the one palette here that cannot be
 * read as a traffic light. Do not add a .bwz-pwsh--ok, and do not make it
 * follow the theme.
 *
 * The width WAS matched to a "Warning" pill -- 6rem, Dan's own 2026-09-14 ask
 * that they "render approx the same width as a 'Warning' pill currently does".
 *
 * [CRITICAL] THAT INSTRUCTION WAS SUPERSEDED ON 2026-09-16 (869f2a694): "reduce
 * the width of the PowerShell buttons, so that free buttons, including the
 * admin link button, should fit in a single (grey), one-row box." The two asks
 * are in direct conflict and the later one wins; decision log 2026-09-16. Do
 * not "restore" 6rem from the older comment on the older ticket.
 *
 * 2.5rem is 40px, and every number in the sum below was MEASURED in Chromium
 * on the real page rather than chosen (drills/product_card_footer_drill.py,
 * 2026-09-16). A product card at 1680px is 253px wide and its grey band has
 * 1.5rem of padding each side, leaving 200px. The widest band on the page is
 * Asset Giant's, which holds the Admin button (102px, a 16px favicon and the
 * word) and TWO of these:
 *
 *     102 + 8 (band gap) + 40 + 4 (group gap) + 40  =  194  <=  200
 *
 * At 6rem that sum was 102 + 8 + 96 + 8 + 96 = 310 and the band was two rows;
 * at 3.25rem it was 222 and still two. Six pixels of headroom is not much, and
 * it is stated rather than hidden: the grid's own floor is a 15rem column, so a
 * viewport that lands a column at exactly 240px leaves 192px and the group
 * wraps. That is the graceful failure and it is the right one -- a band that
 * wraps is readable, and the alternative is a button that clips its own word.
 *
 * It is still a WIDTH rather than a min-width, so "Prod" and "Dev" are the same
 * size next to each other, and the title bar keeps its ellipsis -- "PowerShell"
 * does not fit at this size and is not meant to: the window is a picture, and
 * the accessible name is the sentence in the markup.
 * ------------------------------------------------------------------------- */

.bwz-pwsh-group {
  display: flex;
  flex-wrap: wrap;
  /* --bwz-space-1 since 869f2a694: Asset Giant is the only card with two of
   * these, and 4px rather than 8px between them is 4px of the 6px that made
   * its band one row. */
  gap: var(--bwz-space-1);
  /* A row of bookmarks, not a row of controls: it sits under the label that
   * introduces it, and wrapping is how two buttons behave at 320px. */
}

.bwz-pwsh {
  display: inline-flex;
  flex-direction: column;
  flex: none;
  width: 2.5rem;
  border-radius: var(--bwz-radius-sm);
  border: var(--bwz-border) solid var(--bwz-pwsh-chrome);
  overflow: hidden;
  background: var(--bwz-pwsh-screen);
  text-decoration: none;
  /* A real touch target at 320px is 44px; this is 42px of window plus the
   * border, and it is a bookmark rather than STOP, so the shortfall is
   * deliberate and bounded. Widening it would stop it looking like a window. */
  transition: border-color var(--bwz-duration) var(--bwz-ease),
              box-shadow var(--bwz-duration) var(--bwz-ease);
}

.bwz-pwsh:hover,
.bwz-pwsh:focus-visible {
  border-color: var(--bwz-pwsh-icon);
  box-shadow: 0 0 0 2px var(--bwz-accent-surface);
}

/* The title bar. Windows Terminal's tab strip, at a twelfth of the size. */
.bwz-pwsh__bar {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.15rem 0.3rem;
  background: var(--bwz-pwsh-chrome);
  color: var(--bwz-pwsh-chrome-fg);
  font-size: 0.5rem;
  line-height: 1;
  letter-spacing: 0.01em;
}

/* PowerShell's mark: a blue square with a chevron and an underscore in it.
 * Drawn rather than shipped as an image — it is two characters. */
.bwz-pwsh__icon {
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 1px;
  background: var(--bwz-pwsh-icon);
  position: relative;
}
.bwz-pwsh__icon::before {
  content: ">_";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bwz-pwsh-chrome-fg);
  font-family: var(--bwz-font-mono);
  font-size: 0.4rem;
  line-height: 1;
}

.bwz-pwsh__title {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  /* Smaller since the window narrowed (869f2a694). It ellipsises at this width
   * whatever the size, and this is the size at which the ellipsis looks like a
   * tab strip rather than like a truncation. */
  font-size: 0.42rem;
}

/* The screen. One word, PowerShell's own pale yellow, and no cursor — Dan
 * asked for the reference images "without the cursor showing". */
.bwz-pwsh__screen {
  display: flex;
  align-items: center;
  /* 0.25rem of inline padding rather than 0.4rem since the window narrowed:
   * "Prod" is ~29px of monospace at --bwz-text-xs and the screen is 32px wide
   * inside its border. */
  padding: 0.25rem 0.25rem 0.3rem;
  background: var(--bwz-pwsh-screen);
  color: var(--bwz-pwsh-screen-fg);
  font-family: var(--bwz-font-mono);
  /* --bwz-text-xs since the window narrowed to 3.25rem (869f2a694). "Prod" at
   * --bwz-text-sm is 34px of glyph in 33px of screen, and a clipped word on a
   * button that is a PICTURE of a terminal reads as a rendering bug. */
  font-size: var(--bwz-text-xs);
  font-weight: var(--bwz-weight-semibold);
  line-height: 1.1;
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .bwz-pwsh { transition: none; }
}

/* The Admin button's favicon (869f03448, Dan 2026-09-14: "Buttons should be
 * normal buttons but include their targets' favicons as well as 'Admin'").
 *
 * A plain button plus an image, which is why there is no new button variant
 * here: it is .bwz-btn--secondary --sm with one modifier that stops the icon
 * being stretched by the flex row it sits in. The bytes are inlined by
 * ops/favicons.py; the size is fixed here so that a favicon somebody replaces
 * with a 512px one cannot resize the button. */
.bwz-btn--faviconned { gap: var(--bwz-space-2); }
.bwz-btn__favicon {
  flex: none;
  width: 1rem;
  height: 1rem;
  border-radius: 2px;
  object-fit: contain;
  /* Always on paper, in BOTH themes, and this one is measured rather than
   * tidy: SR-CRM's favicon is a dark navy square and in dark mode it very
   * nearly vanishes into the button it is sitting in. These are four other
   * organisations' marks, drawn for a browser tab strip, so none of them can
   * be assumed to work on a dark ground — and the brand rule that applies to
   * our own mark ("never recolour it") applies harder to somebody else's.
   * Invisible in light mode, where the surface is already paper. */
  background: var(--bwz-brand-paper);
}

/* ===========================================================================
 * §25. The Snapshot control (869f34k2p, Dan 2026-09-16)
 * ===========================================================================
 *
 * The first CONTROL ever to sit on a product card — everything else in that
 * band is navigation — and the only one on the dashboard that spends money at a
 * vendor. Two rules shape every line below and both are already written down.
 *
 * [CRITICAL] IT IS A <details> AND NEVER A POPOVER. "A tooltip Dan asks for is
 * a <details> note — in flow, never :hover, never a popover"
 * (.claude/rules/ops-dashboard.md), and "prose never covers a control"
 * (.claude/rules/runner.md §12.12.17). Dan's word on the ticket was "pop up";
 * an overlaying panel on this page either blocks the control under it or passes
 * the click onto it, and what is under a product card is the rest of a
 * monitoring page. So there is NO position: absolute, NO position: fixed, no
 * transform and no z-index anywhere in this section, deliberately.
 *
 * [CRITICAL] NO STATUS COLOUR. Colour on this dashboard means a check is
 * failing. A snapshot is not a reading, so this wears the same neutral chrome
 * .bwz-btn--secondary does and is never given a tile class, a pill or a meter.
 * The one exception is the "running" flag, which is drawn in the SUBTLE ink and
 * not in an amber — it is a fact about an action somebody took, not a warning.
 */
.bwz-snapshot {
  /* Closed, it is one more button in the wrapping row. OPEN, it takes a whole
   * row of the band — which is what lets the confirmation open IN FLOW without
   * the panel sitting beside the Admin button in a 90px column. The band is a
   * flex container (§17b), so this is flex-basis and not a width. */
  flex: 0 1 auto;
  min-width: 0;
}
.bwz-snapshot[open] { flex: 1 0 100%; }

.bwz-snapshot__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-1);
  padding: var(--bwz-space-1) var(--bwz-space-3);
  border: 1px solid var(--bwz-border);
  border-radius: var(--bwz-radius-sm);
  background: var(--bwz-surface);
  color: var(--bwz-ink);
  font-size: var(--bwz-text-sm);
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
  /* The native marker is removed in both spellings: WebKit still wants the
   * pseudo-element, everything else honours list-style. Removing it is what
   * makes the summary read as the button Dan asked for; the open state is
   * carried by the panel appearing, which is the thing a reader is looking at
   * anyway. */
  list-style: none;
}
.bwz-snapshot__summary::-webkit-details-marker { display: none; }
.bwz-snapshot__summary:hover { background: var(--bwz-surface-2); }
.bwz-snapshot__summary:focus-visible {
  outline: 2px solid var(--bwz-focus);
  outline-offset: 2px;
}

/* "running" beside the word, when DigitalOcean has not yet said the last
 * snapshot finished. Subtle ink rather than a warning colour, for the reason in
 * the section header: an action in progress is not a check that is failing. */
.bwz-snapshot__flag {
  font-weight: 400;
  font-size: var(--bwz-text-xs);
  color: var(--bwz-ink-subtle);
}

.bwz-snapshot__panel {
  /* flex-basis: 100% above makes the DETAILS full width; this keeps the panel
   * itself readable rather than letting a sentence run the width of three
   * product cards on a wide screen. */
  max-width: var(--bwz-measure);
  margin-top: var(--bwz-space-2);
  font-size: var(--bwz-text-sm);
}

/* ---------------------------------------------------------------------------
 * §26. The trend charts (869f58teh, Dan 2026-09-21)
 *
 * [CRITICAL] There is no status colour in this section and that is the rule,
 * not an omission. `ops_resourcesample` stores no status, so a graded line
 * would be the page applying today's thresholds to an old reading. Colour on
 * this dashboard means "this check is failing"; a trend line is history, and
 * history is drawn in ink.
 *
 * The capacity line is distinguished by a DASH as well as by being a second
 * colour, for the same reason every tile carries the status word as well as the
 * dot: colour is never the only signal.
 *
 * Every coordinate arrives projected from ops/trends.py. Nothing here sizes the
 * plot: the viewBox does, so the chart is responsive down to 320px without a
 * media query and without a script.
 * ------------------------------------------------------------------------ */

.bwz-chart {
  max-width: 100%;
  overflow: hidden;
}

.bwz-chart__svg {
  display: block;
  width: 100%;
  height: auto;
}

/* [CRITICAL] --bwz-rule, never --bwz-border. `--bwz-border` is a WIDTH (1px);
 * it shipped here as a stroke on 2026-09-21 and `stroke: 1px` is invalid at
 * computed-value time, so every labelled rule on every trend chart was drawn
 * with no stroke at all -- an axis with numbers beside it and nothing to read
 * them against. Dan spotted the symptom, not the cause, on 2026-09-22 when he
 * asked for "subtle horizontal lines to make them more readable": they were
 * already asked for, and they were already invisible.
 * core/tests.py::test_no_colour_property_in_the_stylesheet_names_a_length_token
 * is what stops the next one. */
.bwz-chart__grid {
  stroke: var(--bwz-rule);
  stroke-width: 1;
}

/* The unlabelled rules halfway between the labelled ones. Dan, 2026-09-22:
 * "subtle horizontal lines to make them more readable". Subtle is the whole
 * specification -- a minor rule as strong as a major one is a second axis, and
 * an axis nobody can read past is worse than no help at all. */
.bwz-chart__grid--minor {
  stroke: var(--bwz-rule);
  opacity: 0.45;
  stroke-dasharray: 2 6;
}

.bwz-chart__axis {
  fill: var(--bwz-fg-muted);
  font-size: 15px;
}

/* The min-max band. Deliberately faint: it is context for the mean line, and a
 * band as strong as the line it sits behind reads as two measurements. */
.bwz-chart__band {
  fill: var(--bwz-fg-muted);
  opacity: 0.16;
  stroke: none;
}

.bwz-chart__line {
  stroke: var(--bwz-fg);
  stroke-width: 2.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* ONE INK PER RESOURCE (869f58teh, Dan 2026-09-22). The band takes the same
 * hue as its line at a low opacity, because the band IS that line's spread --
 * the question Dan asked, "some graphs show a black line and grey shading
 * behind, why is this", is easier to answer once the two are visibly the same
 * measurement. The default above still stands for a kind nobody has declared:
 * a new resource draws in ink rather than not drawing at all.
 *
 * [CRITICAL] These say WHICH resource. They never say how it is doing, and
 * nothing in this block may ever be var(--bwz-ok/-warn/-danger). */
.bwz-chart__line--cpu { stroke: var(--bwz-series-cpu); }
.bwz-chart__line--memory { stroke: var(--bwz-series-memory); }
.bwz-chart__line--disk { stroke: var(--bwz-series-disk); }
.bwz-chart__line--swap { stroke: var(--bwz-series-swap); }

.bwz-chart__band--cpu { fill: var(--bwz-series-cpu); opacity: 0.18; }
.bwz-chart__band--memory { fill: var(--bwz-series-memory); opacity: 0.18; }
.bwz-chart__band--disk { fill: var(--bwz-series-disk); opacity: 0.18; }
.bwz-chart__band--swap { fill: var(--bwz-series-swap); opacity: 0.18; }

/* The mouseover targets. Transparent rather than `fill: none`, because an
 * element with no fill is not a pointer target in any engine and the tooltip
 * would never appear. `pointer-events: all` is what makes a fully transparent
 * fill catch the pointer; it is the whole mechanism and there is no script
 * behind it. */
.bwz-chart__hotspot {
  fill: transparent;
  pointer-events: all;
}

.bwz-chart__capacity {
  stroke: var(--bwz-fg-muted);
  stroke-width: 2;
  stroke-dasharray: 8 6;
  stroke-linecap: round;
}

/* The key beside the chart. An inline swatch rather than a legend inside the
 * SVG: the words belong in the page's text layer, where they are selectable,
 * translatable and readable by a screen reader that skips the graphic. */
.bwz-chart__key {
  display: inline-block;
  width: 1.5rem;
  height: 0;
  vertical-align: middle;
  border-top-width: 3px;
  border-top-style: solid;
}
.bwz-chart__key--line { border-top-color: var(--bwz-fg); }
.bwz-chart__key--capacity {
  border-top-style: dashed;
  border-top-color: var(--bwz-fg-muted);
}

/* The band's swatch is a filled block rather than a rule, because the band is
 * an area. Height and the border together, so the two swatches in one sentence
 * sit on the same baseline. */
.bwz-chart__key--band {
  height: 0.7rem;
  border-top: 0;
  background: var(--bwz-fg-muted);
  opacity: 0.3;
}

.bwz-chart__key--line.bwz-chart__key--cpu { border-top-color: var(--bwz-series-cpu); }
.bwz-chart__key--line.bwz-chart__key--memory { border-top-color: var(--bwz-series-memory); }
.bwz-chart__key--line.bwz-chart__key--disk { border-top-color: var(--bwz-series-disk); }
.bwz-chart__key--line.bwz-chart__key--swap { border-top-color: var(--bwz-series-swap); }

.bwz-chart__key--band.bwz-chart__key--cpu { background: var(--bwz-series-cpu); }
.bwz-chart__key--band.bwz-chart__key--memory { background: var(--bwz-series-memory); }
.bwz-chart__key--band.bwz-chart__key--disk { background: var(--bwz-series-disk); }
.bwz-chart__key--band.bwz-chart__key--swap { background: var(--bwz-series-swap); }

/* One series on the stacked droplet page. A hairline above each block after
 * the first, so three plots read as three sections rather than as one very
 * tall chart. Nothing here sizes a plot: the viewBox still does. */
.bwz-trend-stack__item + .bwz-trend-stack__item {
  border-top: 1px solid var(--bwz-rule);
  padding-top: var(--bwz-space-5);
}

/* ===========================================================================
 * 25. The questionnaire overview and the answer sheet (869f5cv7w, 16.2.0)
 *
 * Two small components, both on `/dashboard/questionnaires/`. Everything else
 * that area draws is borrowed: the tab strip and the overall bar are §20's
 * (`.bwz-quest__steps`, `.bwz-quest__bar`), the section strip is §17a's
 * (`.bwz-tallybar`), and the cards, tables, forms and buttons are the base
 * system's. That reuse is the point rather than economy -- the tabs on the
 * answer sheet are the same tabs, with the same completion rings, as the ones
 * the client is looking at on the questionnaire itself.
 *
 * NO HEX IS NAMED HERE. Every colour is a role token from §2, so both themes
 * and the WCAG AA sweep in core/design.py apply to it unchanged.
 * ======================================================================== */

/* `.bwz-qgraph` -- one small bar per section, the graph that answers "where
 * have they stopped?". A three-column grid rather than a flex row per section:
 * the labels then line up down the page and the bars all start at the same x,
 * which is the entire reason a reader can compare them at a glance.
 *
 * It collapses to a single column below 40em. A 14-section set at 320px would
 * otherwise give each bar about 60px, and a bar that narrow is a decoration
 * rather than a measurement. */
.bwz-qgraph {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) 1fr auto;
  align-items: center;
  gap: var(--bwz-space-2) var(--bwz-space-3);
  margin: 0;
}
.bwz-qgraph > p { margin: 0; }
.bwz-qgraph__label { font-size: var(--bwz-text-sm); }
.bwz-qgraph__count {
  font-size: var(--bwz-text-sm);
  color: var(--bwz-fg-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.bwz-qgraph__track {
  height: 8px;
  border-radius: var(--bwz-radius-full);
  background: var(--bwz-surface-3);
  overflow: hidden;
}
.bwz-qgraph__fill { display: block; height: 100%; background: var(--bwz-rule-strong); }
/* The three states are the ones content/discovery.py:progress already names,
 * so the bar, the tab's ring and the strip above cannot disagree. A zero-width
 * fill is still drawn -- a "not started" section keeps its empty track, which
 * is what makes the column of tracks readable as a column. */
.bwz-qgraph__fill--none { background: var(--bwz-rule-strong); }
.bwz-qgraph__fill--part { background: var(--bwz-warn); }
.bwz-qgraph__fill--all { background: var(--bwz-ok); }

@media (max-width: 40em) {
  .bwz-qgraph { grid-template-columns: 1fr; gap: var(--bwz-space-1); }
  .bwz-qgraph__count { margin-bottom: var(--bwz-space-3); }
}

/* `.bwz-qa` -- one question and its answer, as prose.
 *
 * Deliberately NOT the questionnaire's own question card: a disabled form
 * control reads as "you may not tick this", not as "they did not tick this",
 * and this page is a transcript rather than a form somebody is being stopped
 * from using. The rule on the left edge is what separates one question from
 * the next without drawing fourteen boxes down a long page. */
.bwz-qa {
  padding-left: var(--bwz-space-4);
  border-left: var(--bwz-border-thick) solid var(--bwz-rule);
}
.bwz-qa + .bwz-qa { margin-top: var(--bwz-space-4); }
.bwz-qa__question {
  margin: 0;
  font-weight: var(--bwz-weight-semibold);
}
.bwz-qa__answer { margin: var(--bwz-space-2) 0 0; }
/* A blank answer is muted and italic rather than hidden: "nobody has answered
 * this" is one of the two facts this page exists to carry, and a page that
 * dropped unanswered questions would read as a finished questionnaire. */
.bwz-qa--blank { border-left-color: var(--bwz-rule-strong); }
.bwz-qa__answer--blank { color: var(--bwz-fg-muted); font-style: italic; }

/* ---------------------------------------------------------------------------
 * The portal-visibility switch (869f5cv7w, 17.4.0)
 *
 * [CRITICAL] It is a SUBMIT BUTTON drawn as a tick box, and the reason is not
 * style. Dan asked for the separate Save button to go -- "ticking (or
 * unticking) the box IS the save" -- and an <input type="checkbox"> cannot
 * save itself: only a script could submit its form on change, and the rule on
 * this dashboard is that a blocked script never costs anybody a control. So
 * the thing that looks like a box is the thing that is pressed, once.
 *
 * `role="switch"` and `aria-checked` are in the markup, so the state a screen
 * reader is told and the state a sighted reader sees are the same fact -- the
 * same rule `aria-current="step"` follows on the tab strip.
 *
 * NO HEX IS NAMED HERE. Both colours are role tokens from §2, so the theme
 * sweep and the WCAG AA pairs in core/design.py apply to it unchanged.
 * ------------------------------------------------------------------------ */
.bwz-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--bwz-space-3);
  padding: var(--bwz-space-2) var(--bwz-space-3);
  margin: 0;
  border: var(--bwz-border) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-sm);
  background: var(--bwz-surface);
  color: var(--bwz-fg);
  font: inherit;
  font-size: var(--bwz-text-base);
  text-align: left;
  cursor: pointer;
}
.bwz-switch:hover { border-color: var(--bwz-accent); }
/* The box itself. A fixed square so that "on" and "off" do not move the label
 * sideways -- a control that shifts under the pointer is one somebody presses
 * twice. */
.bwz-switch__box {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  border: var(--bwz-border-thick) solid var(--bwz-rule-strong);
  border-radius: var(--bwz-radius-sm);
  font-size: var(--bwz-text-sm);
  line-height: 1;
}
/* On: the tick AND the colour, never the colour alone -- the same rule
 * .bwz-error::before follows. */
.bwz-switch--on .bwz-switch__box {
  border-color: var(--bwz-ok);
  color: var(--bwz-ok);
}
.bwz-switch__label { font-weight: var(--bwz-weight-medium); }

/* The SLIDER form of the same control (869f4mvv1, 18.1.0 — Dan 2026-09-22)
 *
 * Dan asked for "an on / off slider" beside the dashboard heading. It is the
 * same submit button as the tick-box above, with a track and a knob instead of
 * a box, for the same reason and with the same accessibility markup: a blocked
 * script must never cost anybody a control here, so the thing that looks like a
 * slider is the thing that is pressed.
 *
 * [CRITICAL] THE KNOB MOVES. The colour is the second signal and never the
 * only one — the same rule .bwz-switch--on .bwz-switch__box follows, and the
 * reason is that on this dashboard colour means "this check is failing", so a
 * control whose only difference is green would be reading in the page's own
 * vocabulary. NO HEX IS NAMED: both colours are role tokens from §2.
 */
.bwz-switch__track {
  flex: none;
  position: relative;
  display: inline-block;
  width: 2.25rem;
  height: 1.25rem;
  border: var(--bwz-border-thick) solid var(--bwz-rule-strong);
  border-radius: 999px;
  background: var(--bwz-surface-2);
  transition: background-color 120ms ease-out, border-color 120ms ease-out;
}
.bwz-switch__knob {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  background: var(--bwz-fg-muted);
  transition: left 120ms ease-out, background-color 120ms ease-out;
}
.bwz-switch--on .bwz-switch__track {
  border-color: var(--bwz-ok);
  background: var(--bwz-ok-surface);
}
.bwz-switch--on .bwz-switch__knob {
  left: 1.125rem;
  background: var(--bwz-ok);
}
/* Somebody who has asked for less motion still gets the position change; they
 * simply get it immediately. */
@media (prefers-reduced-motion: reduce) {
  .bwz-switch__track,
  .bwz-switch__knob { transition: none; }
}
