/* =====================================================================
   Bespoke Woodcraft Studio — project-wide theme system
   TWO themes: ONE light (`linen-ink`) and ONE dark (`midnight-oak`).
   P606 (2026-08-12) collapsed ten themes to two and the founder picked both
   by name: "We need to have one light theme and one dark theme, and then we
   have it so that the person can choose either on the page or they can do
   system." LATER THE SAME NIGHT he changed the light pick: "I changed my
   mind, I want the linen theme instead of the current blue theme" — so the
   ten-theme era's Linen palette returns as THE light theme, replacing
   porcelain-blue. The ids are internal; the account menu says Light / Dark
   / System and never shows an id.

   NO BROWN, ANYWHERE (founder, P606 session): "I hate brown. We never,
   ever use brown. It looks old and dingy. We are a modern, AI-driven, hip,
   and cool website." Linen's SURFACES are warm paper neutrals — the
   founder's own named pick, not a drift back toward brown — while its
   accent, header and semantic families stay on the cool slate/steel axis,
   and the one mustard token the old Linen carried (--warn #b5862f) stays
   retuned to the P606 amber. Any OTHER warm token added here has to
   justify itself.

   Each theme sets a CANONICAL palette; the alias layer maps that palette
   onto BOTH token vocabularies used across the suite (cream/walnut/gold
   AND surface/accent/text), so one theme recolours every page.
   `:root[data-theme]` (0,2,0) outranks each page's own `:root` (0,1,0),
   so themes win without touching per-page CSS.
   Soft/fill tokens (accent-soft, ok-soft, danger-soft, warn-soft) stay LIGHT
   in both themes — they're light chips/notices paired with dark text.

   SYSTEM MODE lives in the RESOLVED attribute, not in the CSS: the pre-paint
   bootstrap in every page's <head> reads `bws:theme` and, for "system" (or an
   absent/retired value), resolves prefers-color-scheme to ONE OF THE TWO IDS
   and stamps that. So page CSS only ever targets [data-theme="midnight-oak"]
   for dark — one hook, no per-page media-query duplication.
   ===================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600&display=swap');

/* ONE font, site-wide (founder mandate 2026-06-26; grand-master pick = Inter). Codified here so
   every page that loads theme.css inherits it — no per-page font edits. `:root body` (0,1,1)
   outranks each page's own `body{}` (0,0,1) regardless of source order or whether a theme is
   active, and `:root .serif` (0,2,0) repoints every old Playfair display heading to Inter at a
   heavy weight, so hierarchy comes from WEIGHT within one family (Refactoring UI), not a 2nd font.
   Inter is drawn for screens (tall x-height, open apertures, tabular figures for the dimension
   tools). The Playfair <link> is removed from the few pages that still load it. */
:root{
  --font:"Inter","Inter var",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,"Helvetica Neue",Arial,sans-serif;
  --font-num:"Inter",system-ui,sans-serif;
}
:root body{ font-family:var(--font); }
:root .serif{ font-family:var(--font); font-weight:800; letter-spacing:-.015em; }
:root .num,:root [data-num]{ font-variant-numeric:tabular-nums lining-nums; }

/* ---------- LIGHT — "Linen" (`linen-ink`), the founder's pick ----------
   Warm linen-paper surfaces under cool slate ink, with a calm steel-blue accent.
   This is the ten-theme era's Linen, restored on the founder's 2026-08-12 order
   ("I want the linen theme instead of the current blue theme"), replacing
   porcelain-blue — with ONE correction kept from P606:
   --warn was #b5862f, a mustard-gold that read as the old brown suite; it stays
   the P606-retuned amber. It is a FILL/BORDER token — warning TEXT goes through
   --warn-text/--warn-ink, which are unchanged and still AA-pinned. */
:root[data-theme="linen-ink"]{
  --bg:#f5f3ee; --surface:#ffffff; --surface-2:#ebe8e0; --surface-hi:#f0ede4;
  --text:#20242b; --text-muted:#565d66; --text-faint:#616871;
  --line:#e3e0d8; --border:#d2cfc5;
  --accent:#5c7c93; --accent-deep:#3f5a6b; --accent-soft:#cfe0eb;
  --header-bg:#20242b; --header-bg-2:#2c323b; --header-bg-3:#171a20; --header-line:#3a414b; --header-text:#f5f3ee;
  --ok:#5a8a6a; --ok-soft:#e4efe8; --warn:#c0720f; --warn-soft:#f7f0da; --danger:#b1503f; --danger-soft:#f3e1dc;
  /* P85-3: dashboard identity hues (Dev Dashboard "who/model" chips) get their OWN
     tokens instead of reusing --danger (Fable) / --accent-deep (Opus), so retuning
     --danger for an error state can never shift an identity colour, and vice versa. */
  --identity-fable:#b1503f; --identity-opus:#3f5a6b;
  --shadow-c:0 1px 2px rgba(32,36,43,.06),0 8px 24px rgba(32,36,43,.08);
  --shadow-lg-c:0 2px 6px rgba(32,36,43,.10),0 18px 44px rgba(32,36,43,.15);
}

/* ---------- DARK — "Midnight Oak" (`midnight-oak`), the founder's pick ----------
   Surfaces and text flip; the soft/fill tokens stay light (they are light chips
   paired with dark ink). Despite the name this palette is cool blue-slate with a
   cyan accent — no wood tone in it, which is why it survived the collapse. */
:root[data-theme="midnight-oak"]{
  --bg:#11161c; --surface:#1a212a; --surface-2:#232c37; --surface-hi:#2c3744;
  --text:#e6edf3; --text-muted:#a3b0bd; --text-faint:#8994a1;
  --line:#283340; --border:#374453;
  --accent:#4aa3c0; --accent-deep:#74c4dd; --accent-soft:#bfe0ec;
  --header-bg:#0b1016; --header-bg-2:#161e27; --header-bg-3:#070b0f; --header-line:#283340; --header-text:#e6edf3;
  /* --warn was #d8a657 — one hex step off the retired walnut/studio-cream gold
     (#d4a056) and the most brown-reading token left in the suite. Retuned to a
     brighter amber on the orange side. Fill/border only; warning TEXT on dark goes
     through the --warn-text override below, which is unchanged and AA-pinned. */
  --ok:#5fb08a; --ok-soft:#e2efe8; --warn:#e8a03c; --warn-soft:#f7eed5; --danger:#dd7a6a; --danger-soft:#f3e0db;
  --identity-fable:#dd7a6a; --identity-opus:#74c4dd;
  --shadow-c:0 1px 2px rgba(0,0,0,.38),0 8px 24px rgba(0,0,0,.5);
  --shadow-lg-c:0 2px 6px rgba(0,0,0,.5),0 18px 44px rgba(0,0,0,.62);
}

/* ---------- NO-ATTRIBUTE FALLBACK (both light AND dark) ----------
   The pre-paint bootstrap normally stamps a resolved id before anything paints, so
   this block should never be the live palette. It is the safety net for the cases
   where it isn't: a page whose <head> snippet has not run (or was stripped), the
   snippet's own catch branch, and any embed that loads theme.css alone. Without it
   the :root[data-theme] palette AND the alias layer below are inert and the page
   renders with transparent fills, missing borders and invisible selected states.

   TWO halves, because "no attribute" is not the same as "light":
     • the bare block mirrors linen-ink exactly;
     • the prefers-color-scheme:dark override below mirrors midnight-oak exactly,
   so a dark-OS visitor whose snippet never ran still gets a dark page instead of a
   white flash. That is the same resolution the bootstrap performs, expressed in CSS.

   Specificity (0,2,0) equals :root[data-theme]; once an id IS stamped the :not()
   stops matching, so the stamped theme always wins and the media override cannot
   drag a deliberately-chosen Light page dark. A page's OWN :root:not([data-theme])
   block is later in source order and still wins over this. The alias layer below
   lists :root:not([data-theme]) too, so the full vocabulary resolves either way. */
:root:not([data-theme]){
  --bg:#f5f3ee; --surface:#ffffff; --surface-2:#ebe8e0; --surface-hi:#f0ede4;
  --text:#20242b; --text-muted:#565d66; --text-faint:#616871;
  --line:#e3e0d8; --border:#d2cfc5;
  --accent:#5c7c93; --accent-deep:#3f5a6b; --accent-soft:#cfe0eb;
  --header-bg:#20242b; --header-bg-2:#2c323b; --header-bg-3:#171a20; --header-line:#3a414b; --header-text:#f5f3ee;
  --ok:#5a8a6a; --ok-soft:#e4efe8; --warn:#c0720f; --warn-soft:#f7f0da; --danger:#b1503f; --danger-soft:#f3e1dc;
  --identity-fable:#b1503f; --identity-opus:#3f5a6b;
  --shadow-c:0 1px 2px rgba(32,36,43,.06),0 8px 24px rgba(32,36,43,.08);
  --shadow-lg-c:0 2px 6px rgba(32,36,43,.10),0 18px 44px rgba(32,36,43,.15);
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme]){
    --bg:#11161c; --surface:#1a212a; --surface-2:#232c37; --surface-hi:#2c3744;
    --text:#e6edf3; --text-muted:#a3b0bd; --text-faint:#8994a1;
    --line:#283340; --border:#374453;
    --accent:#4aa3c0; --accent-deep:#74c4dd; --accent-soft:#bfe0ec;
    --header-bg:#0b1016; --header-bg-2:#161e27; --header-bg-3:#070b0f; --header-line:#283340; --header-text:#e6edf3;
    --ok:#5fb08a; --ok-soft:#e2efe8; --warn:#e8a03c; --warn-soft:#f7eed5; --danger:#dd7a6a; --danger-soft:#f3e0db;
    --identity-fable:#dd7a6a; --identity-opus:#74c4dd;
    --shadow-c:0 1px 2px rgba(0,0,0,.38),0 8px 24px rgba(0,0,0,.5);
    --shadow-lg-c:0 2px 6px rgba(0,0,0,.5),0 18px 44px rgba(0,0,0,.62);
    /* NB: the dark SEMANTIC overrides (--ok-text/--warn-text/--danger-text/--on-danger-bg)
       cannot live here — the alias layer below re-declares all four at the same (0,2,0)
       specificity and later source order, so it would win. They repeat in a second
       prefers-color-scheme block placed AFTER the alias layer instead. */
  }
}

/* ---------- ALIAS LAYER ----------
   Maps the canonical palette onto every legacy token name used across the
   suite. Applies to ANY themed page AND the un-themed first-visit fallback above;
   resolves to the active theme's values (or, with no attribute stamped, to the light
   fallback — or the dark one when the OS asks for dark). */
:root[data-theme], :root:not([data-theme]){
  /* surfaces / backgrounds */
  --cream:var(--bg); --paper:var(--bg); --cream-card:var(--surface);
  --cream-2:var(--surface-2); --surface2:var(--surface-2); --cream-hi:var(--surface-hi); --highlight:var(--surface-hi);
  /* text */
  --ink:var(--text); --ink-soft:var(--text-muted); --muted:var(--text-muted);
  --ink-faint:var(--text-faint); --faint:var(--text-faint);
  /* lines / borders */
  --cream-line:var(--line); --cream-line-2:var(--border);
  /* accent (gold / electric) */
  --gold:var(--accent); --gold-deep:var(--accent-deep); --gold-soft:var(--accent-soft);
  /* grouping-chip hue — visually distinct violet for .tag.cab (distinct from the accent/blue family) */
  --cab:#6f5bd0;
  /* SPRINT HUES (FD72, founder 2026-08-05: "I want you to color-code the Sprints to make it
     easier for me to catch them and follow them").

     Twelve categorical hues, all at ONE fixed perceptual lightness (OKLCH L 0.615, chroma capped
     at 0.155 and clipped to the sRGB gamut), with the twelve hue angles chosen by maximising the
     smallest pairwise OKLab distance rather than by eye. One lightness for all twelve means no
     sprint reads louder than its neighbours, and it is what lets ONE set serve both themes
     without per-theme overrides: every hue clears 3.4:1 against the light theme's surface
     (#ffffff) and 4.5:1 against the dark theme's page (#11161c) — the same two anchors the
     set was originally tuned against, so the 10→2 collapse did not move them.

     LEFT ALONE BY THE BROWN PURGE (P606), deliberately: --sprint-16 (#8a7f65) and --sprint-18
     (#a3622e) do read brown/taupe. They are Dev-Dashboard sprint chips, not product surfaces,
     and their hue angles come from the maximised-distance derivation above — repicking two of
     them by eye would break the property that makes one set work for both themes. Flagged for
     the dashboard owner rather than changed here.

     THEY ARE NEVER A TEXT COLOUR. tests/site-lint/theme-contrast.cjs checks a FIXED pair list, so
     a token added here is not automatically contrast-checked and a bad text pair would ship
     silently. These drive borders, dots and 12%-tint fills only; the label stays var(--text) and
     always spells the sprint out in words, so colour is never the only signal. */
  --sprint-1:#d25853;  --sprint-2:#c86701;  --sprint-3:#a87d01;  --sprint-4:#818e02;
  --sprint-5:#3e9c42;  --sprint-6:#019b7c;  --sprint-7:#0096a7;  --sprint-8:#048dcf;
  --sprint-9:#5f7de1;  --sprint-10:#906dd5; --sprint-11:#b260b7; --sprint-12:#c95789; --sprint-13:#d9694f; --sprint-14:#4f9d8a; --sprint-15:#7a8f3d; --sprint-16:#8a7f65; --sprint-17:#5b8fa8; --sprint-18:#a3622e;
  /* on-accent text/fill pair for solid accent-filled pills (selected segment, M/L/XXL
     on-state). The dark theme's accent is light enough for near-black text to clear
     WCAG AA; the light theme overrides below (its mid-tone accent fails dark text, so
     it uses the deep accent + white).
     The ink was #1a1410 — a WARM near-black left over from the walnut/gold suite, and
     the last brown token on a high-traffic accent surface. Now #0f1419, the same
     lightness on the cool slate axis both surviving palettes live on. */
  --on-accent-bg:var(--accent); --on-accent-fg:#0f1419;
  /* Canonical on-accent pair for ANY solid accent-FILLED control (buttons, brand marks, badges,
     tags, step numbers, selected segments). --on-accent = the text, --on-accent-fill = the fill.
     Resolves per theme via the bg/fg pair above (the light theme overrides to deep-accent +
     white). Verified WCAG AA >=4.5:1 in both themes and both no-attribute states by
     tests/site-lint/theme-contrast.cjs.
     Route every "dark/white text on --gold/--accent" element here instead of hardcoding. */
  --on-accent:var(--on-accent-fg); --on-accent-fill:var(--on-accent-bg);
  --electric:var(--accent); --electric-deep:var(--accent-deep); --electric-tint:var(--accent-soft);
  --accent-dim:var(--accent-deep);
  /* accent used as SMALL-TEXT fill on the light page surfaces (--surface, --cream, the #bank
     gradient). Resolves to --accent-deep, which clears WCAG AA 4.5:1 on those surfaces in BOTH
     themes unaided (the light theme's #3f5a6b is 7.3:1 on white). The per-theme --accent-text
     overrides this used to need existed only for the retired mustard-gold light themes, whose
     warm --accent-deep managed ~3.2-3.9:1; they left with those themes. Mirrors --ok-text. */
  --accent-text:var(--accent-deep);
  /* brand header / walnut family */
  --walnut:var(--header-bg); --walnut-2:var(--header-bg-2); --walnut-3:var(--header-bg-3);
  --walnut-deep:var(--header-bg); --walnut-line:var(--header-line); --walnut-tint:var(--header-bg-2);
  --wood-deep:var(--header-bg-3);
  /* semantic */
  --green:var(--ok); --green-bg:var(--ok-soft); --green-dim:var(--ok); --ok-bg:var(--ok-soft);
  --amber:var(--warn); --amber-bg:var(--warn-soft); --no:var(--danger); --red:var(--danger); --red-bg:var(--danger-soft);
  /* on-danger pair for SOLID danger-filled buttons (the destructive confirm). Light
     themes' --danger is a deep brick red, so white text on it already clears WCAG AA
     (~5:1). The dark themes tune --danger as a BRIGHT terracotta accent (so over-wall
     markers read on dark surfaces), where white text would be only ~3:1 — they override
     --on-danger-bg with a deeper red below. Keep --danger as the button BORDER so its
     boundary stays >=3:1 against the (dark) modal card. Mirrors the --on-accent pair. */
  --on-danger-bg:var(--danger); --on-danger-fg:#ffffff;
  /* dark "ink" text for use ON the light semantic soft-fills (the *-soft tints stay
     light in every theme), so chips/badges/readouts clear WCAG AA in all themes.
     ACCENT text on a light --accent-soft fill: use --accent-ink (fixed deep slate), NOT
     --accent-text (which resolves bright in dark themes and fails on the light soft fill). */
  --ok-ink:#2f5a3f; --warn-ink:#6e5114; --danger-ink:#7a3326; --accent-ink:#3f5a6b;
  /* theme-adaptive semantic TEXT — for semantic-coloured text drawn on a surface that
     FLIPS dark (page canvas --bg/--cream, --surface/--cream-card, --surface-2, menus).
     Resolves to the deep -ink in light themes and a bright AA tint in dark (override
     below). Mirrors --on-accent / --on-danger. For text on the *-soft fills (which stay
     light in every theme) keep -ink; for text on the hardcoded light --drop-bg keep -ink. */
  --ok-text:var(--ok-ink); --warn-text:var(--warn-ink); --danger-text:var(--danger-ink);
  /* semantic text on a PERMANENTLY-DARK surface (the inventory rail / header family,
     dark in EVERY theme): always the bright tint so light themes clear AA on it too. */
  --ok-on-dark:#9ed39a; --warn-on-dark:#e8c067; --danger-on-dark:#f0a98f;
  /* =====================================================================
     P164 / P166 / P167 — the PLAN CLASS-COLOR language. ONE hue per
     cabinet class drives THREE derived tokens that the floor-plan box
     FILL, the on-box NAME label, the dimension LADDER (lines + numbers +
     tag) and the legend all read, so the fill, the label, the ladder and
     the key can never disagree — one colour language, one source.
       --cc-<class>-hue    the class's base hue (a theme token by default;
                           P167 overrides just THIS at the #plan-panel scope
                           and the fill + ink re-derive automatically).
       --cc-<class>-fill   the light box tint = the P108/P120 formula
                           (hue mixed into --surface), byte-identical to
                           the old inline .cabfill-* mixes, per class.
       --cc-<class>-ink    the DEEP variant for label text + ladder line/
                           number: the hue driven toward --text so it stays
                           AA on --surface AND on its own fill in EVERY theme
                           (light themes -> darker, dark themes -> lighter,
                           by construction). "Inkier than the fill," derived
                           — never the raw fill value. The P167 runtime floor
                           deepens a user pick further only when it must. */
  --cc-base-hue:var(--accent);
  --cc-upper-hue:var(--ok);
  --cc-tall-hue:var(--danger);
  --cc-island-hue:var(--cab);
  --cc-appliance-hue:var(--text-muted);
  --cc-base-fill:color-mix(in srgb, var(--cc-base-hue) 24%, var(--surface));
  --cc-upper-fill:color-mix(in srgb, var(--cc-upper-hue) 48%, var(--surface));
  --cc-tall-fill:color-mix(in srgb, var(--cc-tall-hue) 24%, var(--surface));
  --cc-island-fill:color-mix(in srgb, var(--cc-island-hue) 34%, var(--surface));
  --cc-appliance-fill:color-mix(in srgb, var(--cc-appliance-hue) 20%, var(--surface));
  --cc-base-ink:color-mix(in srgb, var(--cc-base-hue) 52%, var(--text));
  --cc-upper-ink:color-mix(in srgb, var(--cc-upper-hue) 52%, var(--text));
  --cc-tall-ink:color-mix(in srgb, var(--cc-tall-hue) 55%, var(--text));
  --cc-island-ink:color-mix(in srgb, var(--cc-island-hue) 55%, var(--text));
  --cc-appliance-ink:color-mix(in srgb, var(--cc-appliance-hue) 40%, var(--text));
  /* effects */
  --shadow:var(--shadow-c); --shadow-lg:var(--shadow-lg-c);
  /* P58 — tokenized ELEVATION SCALE (a depth vocabulary; adding tokens is not
     palette work, and it does not touch any theme's colours). Apply consistently:
       --elev-1  resting cards / tiles          (= the existing card shadow)
       --elev-2  popovers, dropdowns, toasts    (= the existing large shadow)
       --elev-3  modals (they sit over a scrim) — a monochrome ambient so it reads
                 as the highest layer on ANY theme without per-theme tuning.
     A DOCKED panel / inspector divider stays a BORDER, never a shadow. */
  --elev-1:var(--shadow-c); --elev-2:var(--shadow-lg-c);
  --elev-3:0 2px 8px rgba(0,0,0,.12),0 28px 64px rgba(0,0,0,.26);
}
/* The LIGHT theme's mid-tone accent (#5c7c93) fails dark text on a solid fill, so the
   on-accent pill uses the deep accent + white (~7.3:1). The no-attribute LIGHT fallback
   rides the same rule, since it mirrors linen-ink token for token. Placed after
   the alias layer so equal-specificity source order lets it win.
   (The retired gold/warm light themes each carried an --accent-text override here to
   drag their mustard --accent-deep up to AA; linen-ink's #3f5a6b is 7.3:1 on white
   unaided, so the whole override family went with them.) */
:root[data-theme="linen-ink"], :root:not([data-theme]){
  --on-accent-bg:var(--accent-deep); --on-accent-fg:#ffffff;
}
/* The DARK theme tunes --danger as a bright terracotta ACCENT (over-wall bars/markers on
   dark surfaces), so white text on it is only ~3:1. Solid danger BUTTONS therefore use
   a deeper red fill (white text ~5:1) while keeping the bright --danger as the border.
   Placed after the alias layer so equal-specificity source order lets it win. */
:root[data-theme="midnight-oak"]{
  --on-danger-bg:#b35140;
  /* semantic TEXT goes bright on the dark surfaces (the fixed dark -ink tints drop to
     ~1.5-2.1:1 there). Verified 7.0-10.7:1 on every dark surface. */
  --ok-text:#9ed39a; --warn-text:#e8c067; --danger-text:#f0a98f;
}
/* …and the same two overrides for the no-attribute DARK state (snippet never ran, OS is
   dark). Must sit AFTER the alias layer for the identical source-order reason; the base
   midnight-oak palette for this state is up in the first prefers-color-scheme block. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme]){
    /* the light fallback's on-accent override (deep accent + white) is wrong here: on the
       dark theme the accent is light enough that near-black ink is the AA-clearing pair,
       so revert to the alias layer's defaults for this state. */
    --on-accent-bg:var(--accent); --on-accent-fg:#0f1419;
    --on-danger-bg:#b35140;
    --ok-text:#9ed39a; --warn-text:#e8c067; --danger-text:#f0a98f;
  }
}
/* Smooth the flip; respect reduced-motion. Targets :root body (0,1,1) so the FIRST
   theme application (un-themed fallback → chosen theme) animates too, not just
   theme-to-theme switches. */
@media (prefers-reduced-motion: no-preference){
  :root body{ transition: background-color .25s ease, color .25s ease; }
}

/* =====================================================================
   P58 — the suite MOTION STANDARD. State changes animate in 150-250ms,
   transform/opacity ONLY (never width/height/top/left — those thrash
   layout), and are SILENCED under prefers-reduced-motion. Shared as tokens
   + opt-in helper classes so nothing animates unless it asks for it.
     --bws-motion / --bws-motion-slow  standard duration+easing for
       per-component transitions, e.g. `transition: opacity var(--bws-motion)`.
     .bws-fade-in / .bws-pop-in  enter animations for panels/menus/toasts.
   ===================================================================== */
:root{
  --bws-ease:cubic-bezier(.4,0,.2,1);
  --bws-motion:.18s var(--bws-ease);
  --bws-motion-slow:.24s var(--bws-ease);
}
@media (prefers-reduced-motion: no-preference){
  .bws-fade-in{ animation:bws-fade-in .18s var(--bws-ease) both; }
  .bws-pop-in{ animation:bws-pop-in .2s var(--bws-ease) both; }
  @keyframes bws-fade-in{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:none; } }
  @keyframes bws-pop-in{ from{ opacity:0; transform:scale(.96); } to{ opacity:1; transform:none; } }
}
@media (prefers-reduced-motion: reduce){
  .bws-fade-in, .bws-pop-in{ animation:none !important; }
}

/* P58 — alignment for the shared line-icon hosts (shared/bws-icons.js fills any
   [data-bws-icon] span with an inline SVG). Centres the glyph inside
   buttons/tabs (flex ignores vertical-align) AND inline labels/headings
   (vertical-align:middle), without per-page CSS. */
[data-bws-icon]{ display:inline-flex; align-items:center; flex:0 0 auto; vertical-align:middle; }
