/* =============================================================================
 * ws.css — WideSky Apps Launcher & Onboard, shared design-system stylesheet.
 *
 * Served as a static asset at /assets/ws.css and linked from both
 * launcher/index.html and onboard/index.html. It carries every token, the
 * hero treatment and every shared primitive the two pages use — the large
 * majority of their visual surface. Neither page keeps a page-local inline
 * <style> block of its own; a page-only rule (see the PAGE-LOCAL section near
 * the bottom of this file) lives here too, scoped by the page's body class.
 *
 * SELECTORS TARGET THE CLASS NAMES THE TWO PAGES EMIT (`card`, `btn`, `main`,
 * `ico`, `steps`, `approw`, `ws-hero`, `ws-brand`, `ws-theme-toggle`, ...).
 * Both pages build most of their markup from JS template literals inside
 * `tile()`, `render()`, `extrasBlock()`, `unknownNote()`, `drawSteps()` and
 * `drawAccess()`, which are off-limits to logic changes — so a new,
 * unrelated class vocabulary would style nothing those functions produce.
 *
 * Colour, radius and glow token VALUES are copied verbatim from the WideSky
 * brand definition, `brands/wideSky.css.ts` in ds-canonical
 * (packages/tokens/src/brands/wideSky.css.ts) — do not round or "improve" a
 * colour here; change the brand file and re-copy instead.
 *
 * No build step. This file is hand-written CSS, no preprocessor, no bundler.
 * ========================================================================== */

/* -----------------------------------------------------------------------
 * Font — Inter, self-hosted, variable (weight axis 100-900). Latin subset
 * only: assets/inter-latin.woff2, already present alongside this file.
 * `unicode-range` keeps glyphs outside the latin set falling through to the
 * system stack instead of rendering as .notdef boxes.
 * -------------------------------------------------------------------- */
@font-face{
  font-family:'Inter';
  src:url('/assets/inter-latin.woff2') format('woff2');
  font-weight:100 900;
  font-style:normal;
  font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
    U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,
    U+FEFF,U+FFFD;
}

/* =============================================================================
 * TOKENS — three-block theme structure.
 *
 * Every token is declared once, in the bare `:root`, at its LIGHT value. The
 * two blocks below only ever redefine an already-declared token — nothing is
 * given its first definition inside a media query or a `[data-theme]` block,
 * so a stylesheet-only consumer (or a browser that matches neither) still
 * gets a complete, correct light theme.
 *
 * `assets/theme.js` sets `data-theme="light"` / `"dark"` on `<html>` for an
 * explicit user choice, and leaves the attribute OFF to mean "follow the
 * system". That is why the dark media query is guarded with
 * `:not([data-theme="light"])` — it must apply whenever the system is dark
 * AND the user has not explicitly pinned light — and the attribute block
 * repeats the same values so an explicit dark choice wins regardless of the
 * system setting.
 * ========================================================================== */
:root{
  /* surfaces */
  --ws-bg:#F3F6FB;
  --ws-surface:#FFFFFF;
  --ws-surface-raised:#FFFFFF;
  --ws-surface-overlay:#FFFFFF;
  --ws-well:#F7F9FC;
  --ws-line:rgba(2,16,36,.10);
  /* text */
  --ws-text:#002B4E;
  --ws-text-muted:#58595B;
  --ws-text-faint:#818284;
  --ws-text-on-accent:#FFFFFF;
  /* accent */
  --ws-accent:#0072B6;
  --ws-accent-strong:#015C95;
  --ws-accent-soft:rgba(0,114,182,.12);
  --ws-accent-2:#185FAD;
  /* interaction */
  --ws-hover:rgba(0,43,78,.05);
  --ws-pressed:rgba(0,43,78,.09);
  --ws-focus-ring:0 0 0 3px rgba(0,114,182,.22);
  /* status */
  --ws-success:#0D8459;
  --ws-success-soft:rgba(13,132,89,.12);
  --ws-warning:#FBAD18;
  --ws-warning-soft:rgba(251,173,24,.18);
  --ws-danger:#D73327;
  --ws-danger-soft:rgba(215,51,39,.12);
  /* secondary-button neutral fill */
  --ws-sec-neutral:#E7EDF6;
  --ws-sec-neutral-hover:#EEF3FA;

  /* ---- depth ramp: light is the evened four-step ramp, sh1-sh3 only used
     here (sh4 is the modal elevation; neither page has a modal, so it is
     not declared — add it if a modal ever arrives). ---- */
  --ws-sh1:0 0 3px rgba(2,16,36,.05),0 1px 2px rgba(2,16,36,.05),0 6px 14px -6px rgba(2,16,36,.14);
  --ws-sh2:0 0 4px rgba(2,16,36,.06),0 2px 4px rgba(2,16,36,.06),0 14px 28px -8px rgba(2,16,36,.22);
  --ws-sh3:0 0 5px rgba(2,16,36,.07),0 4px 8px rgba(2,16,36,.08),0 24px 48px -12px rgba(2,16,36,.28);
  --ws-well-inset:inset 0 1px 3px rgba(2,16,36,.10);

  /* ---- radius ramp ---- */
  --ws-r-xs:4px;
  --ws-r-sm:8px;
  --ws-r-md:12px;
  --ws-r-lg:16px;
  --ws-r-full:9999px;

  /* ---- motion ---- */
  --ws-ease:cubic-bezier(.2,.6,.2,1);
  --ws-motion-instant:100ms;
  --ws-motion-fast:180ms;
  --ws-motion-base:280ms;
  --ws-motion-slow:500ms;
}

/* System dark, no explicit theme choice on <html>. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --ws-bg:#070D1D;
    --ws-surface:#0E1730;
    --ws-surface-raised:#14203D;
    --ws-surface-overlay:#1A2848;
    --ws-well:rgba(0,0,0,.78);
    --ws-line:rgba(255,255,255,.10);
    --ws-text:#EDF1F8;
    --ws-text-muted:#AAB7D0;
    --ws-text-faint:#8296AB;
    --ws-text-on-accent:#0E141B;
    --ws-accent:#609ED2;
    --ws-accent-strong:#7CB1DC;
    --ws-accent-soft:rgba(96,158,210,.16);
    --ws-accent-2:#52A4D5;
    --ws-hover:rgba(255,255,255,.06);
    --ws-pressed:rgba(255,255,255,.10);
    --ws-focus-ring:0 0 0 3px rgba(96,158,210,.30);
    --ws-success:#4FB582;
    --ws-success-soft:rgba(79,181,130,.16);
    --ws-warning:#FBAD18;
    --ws-warning-soft:rgba(251,173,24,.15);
    --ws-danger:#F05068;
    --ws-danger-soft:rgba(240,80,104,.16);
    --ws-sec-neutral:#1B2742;
    --ws-sec-neutral-hover:#22325A;

    /* dark is the tonal ladder (surface climbs #0E1730 -> #1A2848 across the
       rungs) plus a growing accent-aura glow, not a plain black drop shadow
       — locked in the reviewed prototype and copied verbatim. */
    --ws-sh1:0 1px 2px rgba(0,0,0,.5),0 6px 14px -6px rgba(0,0,0,.55),0 0 18px rgba(96,158,210,.07);
    --ws-sh2:0 2px 4px rgba(0,0,0,.55),0 14px 28px -8px rgba(0,0,0,.6),0 0 24px rgba(96,158,210,.09);
    --ws-sh3:0 4px 8px rgba(0,0,0,.6),0 24px 48px -12px rgba(0,0,0,.65),0 0 32px rgba(96,158,210,.12);
    --ws-well-inset:none;
  }
}

/* Explicit dark choice, set by assets/theme.js. Repeats the block above so an
   explicit choice wins over the system setting either way. */
:root[data-theme="dark"]{
  --ws-bg:#070D1D;
  --ws-surface:#0E1730;
  --ws-surface-raised:#14203D;
  --ws-surface-overlay:#1A2848;
  --ws-well:rgba(0,0,0,.78);
  --ws-line:rgba(255,255,255,.10);
  --ws-text:#EDF1F8;
  --ws-text-muted:#AAB7D0;
  --ws-text-faint:#8296AB;
  --ws-text-on-accent:#0E141B;
  --ws-accent:#609ED2;
  --ws-accent-strong:#7CB1DC;
  --ws-accent-soft:rgba(96,158,210,.16);
  --ws-accent-2:#52A4D5;
  --ws-hover:rgba(255,255,255,.06);
  --ws-pressed:rgba(255,255,255,.10);
  --ws-focus-ring:0 0 0 3px rgba(96,158,210,.30);
  --ws-success:#4FB582;
  --ws-success-soft:rgba(79,181,130,.16);
  --ws-warning:#FBAD18;
  --ws-warning-soft:rgba(251,173,24,.15);
  --ws-danger:#F05068;
  --ws-danger-soft:rgba(240,80,104,.16);
  --ws-sec-neutral:#1B2742;
  --ws-sec-neutral-hover:#22325A;
  --ws-sh1:0 1px 2px rgba(0,0,0,.5),0 6px 14px -6px rgba(0,0,0,.55),0 0 18px rgba(96,158,210,.07);
  --ws-sh2:0 2px 4px rgba(0,0,0,.55),0 14px 28px -8px rgba(0,0,0,.6),0 0 24px rgba(96,158,210,.09);
  --ws-sh3:0 4px 8px rgba(0,0,0,.6),0 24px 48px -12px rgba(0,0,0,.65),0 0 32px rgba(96,158,210,.12);
  --ws-well-inset:none;
}

/* =============================================================================
 * BASE — document defaults. Body text defaults to the locked `body` role
 * (14/20, 400).
 * ========================================================================== */
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
  min-height:100dvh;
  background:var(--ws-bg);
  color:var(--ws-text);
  font:400 14px/20px 'Inter',Arial,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  -webkit-font-smoothing:antialiased;
}
code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:12px;
  background:var(--ws-well);
  box-shadow:var(--ws-well-inset);
  padding:1px 5px;
  border-radius:var(--ws-r-xs);
}
/* `h2` is two different things wearing one tag, so it is scoped by PAGE, via
 * `ws-page-onboard`/`ws-page-launcher` on `<body>` — one attribute on static
 * markup, no JavaScript. Onboard's three headings ("Something went wrong",
 * "Invite someone", "Invite sent" — fail(), renderForm(), the success branch
 * in submit()) get the real h2 role. The launcher's lone `<h2>Account</h2>`
 * was a section label rather than a heading, so it took the overline role;
 * the phase-2 re-cut removed it, and the rule stays as a guard.
 *
 * The page class is the discriminator, not a structural accident. A
 * `:first-child`/`:not(:first-child)` split would have worked when both pages
 * still emitted one (`show()` always writes onboard's heading first into
 * `#view`), but nothing would have FAILED if a later edit reordered the markup
 * and silently flipped the typography — the suite asserts on `data-k` and
 * `data-action`, never on type scale. Scoping by page means a markup reorder
 * cannot touch which role an `h2` gets. Phase 2 reordered exactly that markup,
 * which is the case this scoping was chosen for. */
.ws-page-onboard h2{font:600 21px/28px 'Inter',Arial,sans-serif;color:var(--ws-text);margin:0 0 4px}
/* The launcher emits NO h2 since the phase-2 re-cut: `<h2>Account</h2>` was
 * the only one, and the account card names itself with `role="group"` plus
 * `aria-label` instead, which keeps the accessible landmark without spending
 * a row of the card on a label. The rule is kept as a guard, so a future
 * launcher heading lands on the overline role rather than on the browser
 * default; `.overline` below is the same treatment for a plain div. */
.ws-page-launcher h2{
  font:600 11px/16px 'Inter',Arial,sans-serif;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ws-text-muted);margin:0 0 8px;
}
[hidden]{display:none!important}

/* =============================================================================
 * HERO, BRAND LOCKUP, LATTICE, THEME TOGGLE — elements both pages carry, with
 * no legacy class name to match: the hero, the real brand lockup SVG, the
 * lattice canvas and the floating theme toggle.
 * ========================================================================== */

/* .ws-hero — both pages. Fixed-dark gradient in BOTH themes: the lattice's
 * nodes are painted from the accent ramp, and the accent token INVERTS
 * between light and dark, so a token-driven gradient would swallow the
 * lattice into the background in one of the two themes. Carried over from
 * the editor's sign-in hero (widesky-editor/app/globals.css:186,
 * `.wse-hero`) — these four hex values are the one deliberate exception to
 * tokens-only in this file. */
.ws-hero{
  position:relative;
  isolation:isolate;
  min-height:100dvh;
  display:grid;
  place-items:center;
  /* `align-content`, not just `align-items`. The launcher has TWO grid items
   * now — the main card and the account card — and a grid's default
   * `align-content:normal` stretches its auto rows to fill the hero, which
   * pushed the account card to the bottom of the viewport with a screen of
   * empty gradient between the two. Centring the CONTENT keeps the pair
   * together; `row-gap` is then the actual space between them. Onboard has one
   * item, where this behaves exactly as `place-items:center` did. */
  align-content:center;
  row-gap:14px;
  padding:26px 16px;
  overflow:auto;
  background:linear-gradient(160deg,#0073bc 0%,#003052 100%);
}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .ws-hero{
    background:linear-gradient(160deg,#003052 0%,#070d1d 100%);
  }
}
:root[data-theme="dark"] .ws-hero{
  background:linear-gradient(160deg,#003052 0%,#070d1d 100%);
}

/* F5 fix (CORE-9439): the fixed toggle occupies a 38px square at
 * `right:16px;bottom:16px` — a 54px footprint. Below ~548px viewport width the
 * card's content runs edge-to-edge close enough that a full-width control's
 * right side (onboard's `select.lvl` chevron is the one this actually hits)
 * sits under it. Add bottom clearance so scrolling to the end never leaves a
 * control underneath; 74px clears the 54px footprint with margin to spare. */
@media (max-width:548px){
  .ws-hero{padding-bottom:74px}
}

/* .ws-lattice — the hero's canvas, drawn by assets/lattice.js. */
.ws-lattice{position:absolute;inset:0;z-index:-1;display:block}

/* .ws-brand — wraps the real BrandLockup SVG, centred above the card content
 * on both pages. */
.ws-brand{display:flex;justify-content:center;margin-bottom:20px}
.ws-brand svg{display:block;height:32px;width:auto;color:var(--ws-text)}

/* .ws-lpa — the LPA endorsement mark, bottom left, balancing the theme toggle
 * bottom right. Fixed for the same reason the toggle is: `.ws-hero` grows with
 * its content, so an absolutely-placed mark would sit at the bottom of the
 * DOCUMENT and off screen until you scrolled.
 *
 * `bottom:22px`, not 16. The toggle is a 38px square at `bottom:16px`, so its
 * optical centre sits 35px up; a 26px mark has to start at 22px to share that
 * line. Matching the inset instead would leave the two visibly out of level.
 *
 * The asset is a flat white PNG at 2x, 1.2 KB. White is a listed LPA primary
 * and the hero is dark in BOTH themes, so one file covers both and no filter
 * runs at paint time. A vector would be better if the brand team has one. */
.ws-lpa{
  position:fixed;left:16px;bottom:22px;z-index:3;
  width:30px;height:26px;
  opacity:.6;
  pointer-events:none;              /* decorative: never let it read as a button */
}

/* .ws-theme-toggle — floating ghost button, bottom-right of the viewport.
 * `position:fixed`, not `absolute`: `.ws-hero` has no capped height (it grows
 * with content — the organisation list, the access list, the four-step
 * progress on a long onboard state), so an absolutely-positioned toggle sits
 * at the bottom of the DOCUMENT, off-screen until the visitor scrolls all the
 * way down. Fixed keeps it anchored to the viewport instead, reachable at any
 * scroll position. Nothing else keys off `.ws-hero`'s `position:relative` to
 * place this element, so the switch needs no other change. */
.ws-theme-toggle{
  position:fixed;right:16px;bottom:16px;z-index:3;
  appearance:none;border:0;cursor:pointer;
  width:38px;height:38px;border-radius:var(--ws-r-full);
  display:grid;place-items:center;
  background:rgba(255,255,255,.10);color:rgba(255,255,255,.80);
  backdrop-filter:blur(6px);
  transition:background var(--ws-motion-fast) var(--ws-ease),color var(--ws-motion-fast) var(--ws-ease);
}
.ws-theme-toggle:hover{background:rgba(255,255,255,.18);color:#fff}
/* F2 fix (CORE-9439): the toggle sits on the fixed-dark hero in BOTH themes
 * (see the `.ws-hero` comment above), never on a token surface, so its outline
 * is a hardcoded near-white to match the hover/rest rules right above it —
 * the same deliberate tokens-only exception those already make. */
.ws-theme-toggle:focus-visible{outline:2px solid rgba(255,255,255,.9);outline-offset:2px;box-shadow:0 0 0 3px rgba(255,255,255,.35)}
.ws-theme-toggle svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}

/* =============================================================================
 * PRIMITIVES — every selector below targets a class or tag the two pages
 * emit, most of it from inside `tile()`, `render()`, `extrasBlock()`,
 * `unknownNote()`, `drawSteps()` and `drawAccess()`.
 * ========================================================================== */

/* ---- card (both): the panel inside `.ws-hero` that wraps the brand lockup,
 * `#sub`, `#view` and `#foot` — `#view` is one child among siblings here, on
 * both pages. No border, no inset "lip" — depth and tone carry the surface
 * instead, per the design-language rules. */
.card{
  position:relative;
  background:var(--ws-surface);
  color:var(--ws-text);
  border-radius:var(--ws-r-md);
  box-shadow:var(--ws-sh2);
  padding:22px;
  width:100%;
  max-width:440px;
  margin:0 auto;
}

/* onboard's subtitle under the brand mark, and "signed in as" line */
.sub{color:var(--ws-text-muted);font:400 14px/20px 'Inter',Arial,sans-serif;text-align:center;margin:2px 0 22px}
/* Step two puts the INSTANCE NAME here, where the question was, so the card is
 * not saying the same thing twice in two places. It is the card's title then,
 * not a muted aside, so it takes the title weight and the full text ink. */
.sub.name{color:var(--ws-text);font:600 15px/22px 'Inter',Arial,sans-serif}
/* `muted`, not `faint`: measured 3.85:1 in Chromium, and this names the
 * account the invite is being created FROM, which is the one fact on the
 * page an inviter needs to check before they send it. See `.hint`. */
.who{font:400 13px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin-bottom:16px} /* label role's 13/16 pairing; weight kept at 400, on-scale */

/* ---- buttons (both): `.btn` (launcher) and `.main` (onboard) are the same
 * role — a solid primary action — under two different legacy names.
 * `.ghost` is the secondary/neutral variant on both. `.dead` (launcher
 * only) is a non-interactive, greyed tile. Borderless by design-language
 * rule: weight and fill carry hierarchy, not a border. */
.btn,.main{
  display:flex;
  align-items:center;
  /* CENTRED. `text-align:left` and this gap date from when these were
   * icon-and-text tile rows, where the icon pinned the left edge and the label
   * sat beside it. Those rows are `.app` and `.acct` now, so every remaining
   * `.btn`/`.main` is a single run of text in a full-width button — "Send
   * invite", "Try again", "Back to apps" — and a lone left-aligned label in a
   * 400px button reads as a layout fault. `justify-content` does the work
   * because this is a flex container; `text-align` covers a label that wraps. */
  justify-content:center;
  gap:12px;
  width:100%;
  padding:12px 14px;
  margin:9px 0 0;
  border:0;
  border-radius:var(--ws-r-sm);
  background:var(--ws-accent);
  color:var(--ws-text-on-accent);
  font:600 14px/20px 'Inter',Arial,sans-serif; /* body role's 14/20 pairing; weight kept at 600 for CTA emphasis, on-scale */
  text-align:center;
  text-decoration:none;
  cursor:pointer;
  box-shadow:var(--ws-sh1);
  transition:background var(--ws-motion-fast) var(--ws-ease);
}
.btn:hover,.main:hover{background:var(--ws-accent-strong)}
/* F1/F2 fix (CORE-9439): `--ws-focus-ring` alone is a 22%-alpha glow, which
 * composites to well under the 3:1 WCAG 2.4.11 floor, and `.btn.ghost`'s
 * `box-shadow:none` below shares this rule's specificity and wins the
 * cascade, so a focused ghost button previously had no indicator at all
 * (`outline:0` here removed the one thing that would have shown). A real
 * `outline` is a separate property from `box-shadow`, so `.ghost` cannot
 * blank it out; the soft glow stays as an extra layer where it does survive.
 * `--ws-accent-2` is an existing token, not a new one. */
.btn:focus-visible,.main:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}
.btn.ghost,.main.ghost{background:var(--ws-sec-neutral);color:var(--ws-text);box-shadow:none}
.btn.ghost:hover,.main.ghost:hover{background:var(--ws-sec-neutral-hover)}
.btn:disabled,.main:disabled,.btn[disabled],.main[disabled]{opacity:.45;cursor:not-allowed}
.btn:disabled:hover,.main:disabled:hover{background:var(--ws-accent)}
.btn.ghost:disabled:hover,.main.ghost:disabled:hover{background:var(--ws-sec-neutral)}
/* `.btn.dead` used to style the greyed "not configured" tile. That branch was
 * unreachable — render() filtered to `known` before calling tile() — and the
 * phase-2 re-cut deleted it, so the rules went with it. The instance the
 * launcher has no address for is a disabled pill on the rail now; see
 * `.inst button:disabled` below. */

/* ---- small text-link button (launcher): the "Wait, show me my apps" /
 * "Not this one? Show all" cancel links in `render()`. Onboard has no
 * equivalent control, so this class is launcher-only, but it lives here
 * rather than in a page-local block since nothing else warrants one. */
.link{
  appearance:none;background:none;border:0;cursor:pointer;display:inline-block;
  padding:3px 5px;margin-top:10px;border-radius:var(--ws-r-xs);
  font:500 12px/16px 'Inter',Arial,sans-serif; /* caption role, exact match */
  color:var(--ws-accent-2);text-decoration:underline;text-underline-offset:2px;
}
.link:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}

/* ---- icon chip (both): the square icon tile inside a `.btn`/`.main` row */
.ico{
  flex:0 0 auto;width:34px;height:34px;border-radius:var(--ws-r-sm);
  display:grid;place-items:center;background:var(--ws-well);
}
.ico svg{
  width:19px;height:19px;fill:none;stroke:var(--c,var(--ws-accent));
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round;
}
.ico svg .solid{fill:var(--c,var(--ws-accent));stroke:none}

/* ---- status text (both): "Signing you in...", "Opening X..." -----------
 * `.txt`, the label-plus-subtext block that used to sit beside an icon chip,
 * went with the phase-2 re-cut: app tiles are `.app .t` now and the account
 * card carries no subtext at all. Neither page emits `.txt` any more. */
.msg{font:400 14px/20px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);text-align:center}
/* The instance name in the sticker host's Opening line. Kept on the type
 * scale rather than left to the browser's 700-weight default. */
.msg b{font-weight:600;color:var(--ws-text)}

/* ---- alerts (both): err / warn / ok status blocks, used as standalone
 * divs rather than a shared wrapper class in either page today. */
/* label role's 13/16 pairing (not 13/18 — 13 is the label size, whose line
 * height is 16); weight kept at 400, on-scale. */
/* THE WASH CARRIES THE STATUS; THE TEXT IS READ AT `--ws-text`.
 *
 * Measured in Chromium, not reasoned: `--ws-danger` (#D73327) on
 * `--ws-danger-soft` over the light card is 4.00:1, and this is 13px text, so
 * AA wants 4.5. `develop` had the same message as plain #c0392b on the white
 * card at 5.44:1, which means the soft fill this stylesheet introduced turned
 * a passing combination into a failing one. That is a regression we own, so it
 * is fixed here rather than escalated.
 *
 * Fixing it at the component level is the design system's OWN answer to this
 * class of problem — `warning-text` was removed from the palette on exactly
 * that reasoning. No token value is edited; the elements read a different
 * token. `--ws-text` on the danger wash measures 12.1:1 light, and the tinted
 * panel plus the wording still say which of the three states this is. */
.err{border-radius:var(--ws-r-sm);padding:11px 13px;font:400 13px/16px 'Inter',Arial,sans-serif;margin-top:12px;background:var(--ws-danger-soft);color:var(--ws-text)}
.warn{border-radius:var(--ws-r-sm);padding:11px 13px;font:400 13px/16px 'Inter',Arial,sans-serif;margin-top:12px;background:var(--ws-warning-soft);color:var(--ws-text)}
.ok{border-radius:var(--ws-r-sm);padding:11px 13px;font:400 13px/16px 'Inter',Arial,sans-serif;margin-top:12px;background:var(--ws-success-soft);color:var(--ws-text)}
.err b,.warn b,.ok b{font-weight:600}

/* ---- hint: muted helper text inside a card. ONBOARD ONLY now — field help,
 * the "grants" summary line and an app's single access level, all left-aligned
 * under the field they belong to. The launcher's one use, a centred "apps open
 * in a new tab" footnote, was removed. */
/* `--ws-text-muted`, NOT `--ws-text-faint`. Measured in Chromium: faint
 * (`#818284`) on the light card is 3.85:1, and this is 12px text carrying an
 * instruction and an address a person is meant to read and type. `muted`
 * measures 7.0:1 on the same ground. This changes WHICH token the element
 * uses, never a token's value — the faint token's own contrast is a design
 * system defect and is raised there. */
.hint{font:400 12px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin-top:10px}
.hint b{color:var(--ws-text);font-weight:600}

/* ---- footer text (both) ---------------------------------------------------
 * F6 fix (CORE-9439): `--ws-text-faint` at this 11px size is ~3.75:1 on the
 * white/dark card surface, below AA. `--ws-text-muted` is ~7:1 here and is
 * already the muted-body-text token elsewhere in this file, so this switches
 * which token the footer reads, not either token's own locked value. */
/* ---- back control (onboard): a way out of this page that is present in every
 * state, because it lives in the static markup outside `#view` rather than
 * being written into one screen.
 *
 * It replaces a footer line that read "&larr; Back to apps &middot;
 * <hostname>". Absolutely placed, NOT in flow: an inline control at the top of
 * the card would push the centred brand lockup off axis by its own width.
 *
 * The launcher has no equivalent and no footer at all now — it IS the place
 * "back" goes, and printing its own hostname under the card told a person
 * nothing they could not read in the address bar. */
/* NATIVE CHROME IS NEUTRALISED EXPLICITLY, because this class lands on BOTH an
 * `<a>` (onboard) and a `<button>` (the launcher's step back). A button carries
 * a grey fill, an outset border and its own padding; an anchor carries none of
 * it. Styling only what the anchor needed left the button drawn as a default
 * form control — reported from production, twice. */
.back{
  position:absolute;left:12px;top:12px;
  width:30px;height:30px;border-radius:var(--ws-r-full);
  display:grid;place-items:center;
  appearance:none;-webkit-appearance:none;
  border:0;background:transparent;padding:0;font:inherit;cursor:pointer;
  color:var(--ws-text-muted);text-decoration:none;
  transition:background var(--ws-motion-fast) var(--ws-ease),color var(--ws-motion-fast) var(--ws-ease);
}
/* The transparent-at-rest ladder, same as `.acct a`: the fill appears only
 * under the pointer, which stops a 30px control competing with the brand
 * lockup beside it. `.btn.ghost` carries a solid fill at rest instead, which is
 * right for a full-width action and wrong for an icon in a corner. */
.back:hover{background:var(--ws-hover);color:var(--ws-text)}
/* `--ws-pressed` is a declared token that nothing consumed until now. Without
 * it the control has no click feedback at all: press and release look the same
 * as hover, so a tap on a touch screen acknowledges nothing. */
.back:active{background:var(--ws-pressed);color:var(--ws-text)}
/* A real outline, never `outline:0` plus a glow. The prototype used the glow
 * alone; it composites to about 1.4:1, which is below the WCAG 2.4.11 floor of
 * 3:1, so this is a deliberate divergence from it and not an oversight. */
.back:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}
.back svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* ---- spinner (both) -------------------------------------------------------
 * `spin` (unprefixed) is asserted by the launcher test suite — keep this
 * exact class name, never rename or prefix it. */
.spin{
  width:30px;height:30px;border-radius:var(--ws-r-full);margin:16px auto;
  border:3px solid var(--ws-accent-soft);border-top-color:var(--ws-accent);
  animation:ws-spin .9s linear infinite;
}
@keyframes ws-spin{to{transform:rotate(360deg)}}

/* ---- form field, label, input/select (onboard) ----------------------------
 * A well belongs on an input (a recess to type into), never as a generic
 * grouping panel, per the design-language rules — so no border, a well
 * fill instead. */
label{display:block;font:500 13px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin:14px 0 6px}
input,select{
  width:100%;padding:11px 12px;border:0;border-radius:var(--ws-r-sm);
  background:var(--ws-well);box-shadow:var(--ws-well-inset);
  color:var(--ws-text);font:400 14px/20px 'Inter',Arial,sans-serif; /* body role, exact match */
}
/* `muted` at 7.0:1, not `faint` at 3.85:1. A placeholder is still text a
 * person reads. It stays clearly lighter than a real value, which is
 * `--ws-text`, so the two cannot be confused. */
input::placeholder{color:var(--ws-text-muted)}
input:focus,select:focus{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-well-inset),var(--ws-focus-ring)}

/* ---- checkbox row (onboard): one row per app in the access list, built by
 * `drawAccess()` as `.approw > label > (.appck + span), select.lvl` -------- */
/* `min-height` so every row is the same height whether its level is a popover
 * trigger (34px tall) or, for an app with a single level, plain text (20px).
 * Without it the checkboxes step up and down the column. */
.approw{display:flex;align-items:center;gap:10px;margin:6px 0;min-height:34px}
.approw label{display:flex;align-items:center;gap:8px;margin:0;flex:1;font-weight:400;color:var(--ws-text);cursor:pointer}
.appck{width:16px;height:16px;accent-color:var(--ws-accent);cursor:pointer;flex:0 0 auto;margin:0}
.appck:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px}
.lvl{width:auto;min-width:120px;padding:6px 8px} /* font-size now redundant with the base input,select 14px rule above */

/* =============================================================================
 * POPOVER SELECT (assets/ws-select.js) — the trigger and the floating list
 * that stand in for a native <select>'s operating-system dropdown.
 *
 * The native element is NOT removed. It keeps the value, the `disabled` state
 * and the `change` event, and it is hidden rather than deleted so that a page
 * whose script never loads still has a working control. `display:none` also
 * takes it out of the tab order without relying on the `tabindex="-1"` the
 * script sets.
 * ========================================================================== */
.wsel-native{display:none!important}

.wsel{
  display:flex;align-items:center;gap:8px;width:100%;
  padding:11px 12px;border:0;border-radius:var(--ws-r-sm);
  background:var(--ws-well);box-shadow:var(--ws-well-inset);
  color:var(--ws-text);font:400 14px/20px 'Inter',Arial,sans-serif; /* body role, matching `input,select` */
  text-align:left;cursor:pointer;
}
.wsel-v{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wsel-c{
  flex:0 0 auto;width:16px;height:16px;fill:none;stroke:var(--ws-text-muted);
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round;
  transition:transform var(--ws-motion-fast) var(--ws-ease);
}
.wsel[aria-expanded="true"] .wsel-c{transform:rotate(180deg)}
/* The same real outline every other control gets. `--ws-focus-ring` composites
 * to about 1.4:1 and is a glow, not an indicator; see the `.btn` focus rule. */
.wsel:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-well-inset),var(--ws-focus-ring)}
.wsel:disabled{opacity:.45;cursor:not-allowed}

/* The level select inside an access row: sized to its content, not the row. */
.approw .wsel{width:auto;min-width:120px;flex:0 0 auto;padding:6px 10px}

/* The floating list. A child of <body> and positioned `fixed` from the
 * trigger's viewport rect, so the access list's `overflow` cannot clip it.
 * `--ws-surface-overlay` and `--ws-sh3` are the design system's overlay
 * surface and its top elevation rung. */
.wsel-pop{
  position:fixed;z-index:40;
  margin:0;padding:4px;
  max-width:min(92vw,420px);
  overflow-y:auto;overscroll-behavior:contain;
  background:var(--ws-surface-overlay);
  border-radius:var(--ws-r-md);
  box-shadow:var(--ws-sh3);
  color:var(--ws-text);
}
.wsel-o{
  padding:9px 11px;border-radius:var(--ws-r-xs);cursor:pointer;
  font:400 14px/20px 'Inter',Arial,sans-serif; /* body role, exact match */
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* `.on` is the ACTIVE option -- where the arrow keys are -- and is painted by
 * the script, never by `:hover`. Hover and keyboard have to agree on one
 * highlight, or a mouse resting over the list contradicts the arrow keys. */
.wsel-o.on{background:var(--ws-hover)}
/* `aria-selected` is the COMMITTED value, which is a different fact from the
 * active option and is shown as weight and colour rather than as a second
 * highlight competing with it. */
.wsel-o[aria-selected="true"]{color:var(--ws-accent);font-weight:600}

/* ---- the access list, capped.
 * A GUARD, not a fix for anything visible today: `catalogue()` yields one row
 * per app an organisation's roles grant, which is two or three. If that list
 * ever grows, the cap keeps "Send invite" on screen instead of pushing it below
 * a long column. `overscroll-behavior` stops a flick inside the list from
 * scrolling the page once the list hits its end. */
.approws{max-height:min(38vh,320px);overflow-y:auto;overscroll-behavior:contain}

/* ---- four-step progress list (onboard), built by `drawSteps()` as
 * `.steps > .step(.run|.done|.fail) > .dot` --------------------------------- */
.steps{margin-top:18px;font-size:13px} /* label role size */
/* A PENDING step, at `muted` rather than `faint` (3.85:1). These four lines
 * tell an inviter what is about to happen to a live Keycloak realm, so they
 * have to be readable before they run, not only after. `.run` below stays
 * darker and the dot carries the done/failed state, so nothing is lost by
 * pending and done sharing one ink. */
.step{display:flex;gap:10px;align-items:center;margin:9px 0;font:400 13px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted)} /* label role's 13/16 pairing; weight kept at 400, on-scale */
/* A well belongs on an input or as the middle layer of a card sandwich, per
 * the design-language rules — a progress bullet is neither, so it gets a
 * flat tonal fill instead of `--ws-well`/`--ws-well-inset`. `--ws-sec-neutral`
 * is that fill elsewhere in this file (the ghost button variant), and its
 * light/dark values keep enough contrast against `--ws-text-faint` for the
 * numeral inside. */
.dot{
  flex:0 0 auto;width:20px;height:20px;border-radius:var(--ws-r-full);
  background:var(--ws-sec-neutral);
  color:var(--ws-text-muted);
  /* 20px line-height here is a layout mechanism, not a type role — it
     vertically centres the single numeral/glyph inside the 20px circle. */
  font:600 11px/20px 'Inter',Arial,sans-serif;text-align:center;
}
.step.run{color:var(--ws-text)}
.step.run .dot{background:var(--ws-accent);color:var(--ws-text-on-accent);box-shadow:none}
.step.done{color:var(--ws-text-muted)}
.step.done .dot{background:var(--ws-success);color:#fff;box-shadow:none}
.step.fail{color:var(--ws-danger)}
.step.fail .dot{background:var(--ws-danger);color:#fff;box-shadow:none}

/* =============================================================================
 * LAUNCHER RE-CUT (CORE-9439 phase 2) — the instance rail, the app grid, the
 * account card and the sticker host's Opening screen. These are the four
 * genuinely NEW elements in the design, so they carry new class names rather
 * than overloading `.btn`, whose role is a solid primary action.
 * ========================================================================== */

/* ---- section overline: a small uppercase label above a block. Used for the
 * instance name when there is only one instance (so no rail is drawn), and
 * for "Device" on the sticker host. Same metrics as the launcher h2 role. */
.overline{
  font:600 11px/16px 'Inter',Arial,sans-serif;
  letter-spacing:.06em;text-transform:uppercase;
  /* `muted`, not `faint`: this names the instance every tile below it leads
   * to, and faint measures 3.85:1 on the light card — below AA at any size,
   * and this is 11px. See the note on `.hint`. */
  color:var(--ws-text-muted);
  margin:0 0 8px;
}

/* ---- instance rail: a fully-rounded segmented control, one segment per
 * instance the person has access to. The rail sits in a well, which is a
 * sanctioned well use — it is a control, not a grouping panel. It wraps
 * rather than scrolls: a segment scrolled out of sight is an instance the
 * person cannot reach. */
/* ---- the picker region. Cancelling an auto-open hands focus here, and the
 * element is `tabindex="-1"` with no style of its own, so Chrome painted its
 * own `outline: auto 1px rgb(16,16,16)` — square-cornered, zero offset, cutting
 * across the tile corners, and the only indicator on the page that was not the
 * design-system ring. Styled rather than removed: see the no-`outline:0` rule. */
#picker:focus-visible{
  outline:2px solid var(--ws-accent-2);
  outline-offset:4px;
  border-radius:var(--ws-r-md);
}

/* ---- instance tiles (step one). App tiles are icon-led; an instance is a
 * PLACE, not a thing with a glyph, so these are name-led with the app count
 * beneath. The count says something useful before you commit, and keeps the
 * two steps distinguishable at a glance. The minimum track is wider than the
 * app grid's because instance codes are long words that must not hyphenate. */
.insts{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(136px,1fr))}
.inst-t{
  display:flex;flex-direction:column;align-items:flex-start;gap:1px;
  width:100%;min-width:0;overflow-wrap:anywhere;
  appearance:none;border:0;cursor:pointer;text-align:left;
  background:var(--ws-sec-neutral);color:var(--ws-text);
  border-radius:var(--ws-r-sm);padding:13px 14px;
  font:600 15px/22px 'Inter',Arial,sans-serif;
  transition:background var(--ws-motion-fast) var(--ws-ease);
}
.inst-t:hover{background:var(--ws-sec-neutral-hover)}
/* A real outline, never `outline:0` plus a glow — see the `.btn` focus rule
 * above for why the glow on its own is not an indicator. */
.inst-t:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}
.inst-t .c{font:400 12px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted)}

/* An instance the person holds access to that this launcher has no base URL
 * for. Drawn, never dropped: a missing INSTANCES entry has to look like a gap
 * in the launcher's configuration, not a gap in the person's access. `faint`
 * here and nowhere else — 3.85:1 on the light card, which WCAG 1.4.3 exempts
 * for an INACTIVE component; the `title` and the note below carry the reason. */
.inst-t:disabled{
  background:var(--ws-well);box-shadow:var(--ws-well-inset);
  color:var(--ws-text-faint);cursor:not-allowed;
}
.inst-t:disabled:hover{background:var(--ws-well)}
.inst-t:disabled .c{color:var(--ws-text-faint)}

/* ---- app grid: icon over label, three across at the card's width. `auto-fit`
 * rather than `repeat(3,1fr)` so one or two apps fill the row instead of
 * leaving a stranded third-width tile; at the 440px card the 104px minimum
 * still yields exactly three columns. */
.apps{
  display:grid;gap:8px;margin-top:14px;
  grid-template-columns:repeat(auto-fit,minmax(104px,1fr));
}
.app{
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;
  padding:14px 8px;min-width:0;
  border:0;border-radius:var(--ws-r-sm);
  background:var(--ws-sec-neutral);color:var(--ws-text);
  font:500 13px/16px 'Inter',Arial,sans-serif; /* label role, exact match */
  text-decoration:none;cursor:pointer;
  transition:background var(--ws-motion-fast) var(--ws-ease);
}
.app:hover{background:var(--ws-sec-neutral-hover)}
.app:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}
.app .t{display:block;min-width:0;overflow-wrap:anywhere}
/* The instance subtitle is drawn ONLY when no rail was drawn. With a rail the
 * pill has already answered which instance, and repeating it in every tile is
 * noise. `render()` owns that decision; this rule only styles the result. */
.app .t small{display:block;font:400 12px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin-top:2px}
/* Row form: used when a single app is offered, where one column tile in a
 * three-column grid reads as a rendering fault rather than as a choice. */
.apps.rows{grid-template-columns:1fr}
.apps.rows .app{flex-direction:row;text-align:left;gap:12px;padding:12px 13px;font:600 14px/20px 'Inter',Arial,sans-serif}

/* ---- account card: its own card below the main one, one row of icon-and-title
 * tiles, no subtext. The longer phrasing those tiles used to carry as subtext
 * moves to `aria-label` and `title`, so neither a screen reader nor a hover
 * loses it. Each visible label is a prefix of its accessible name, which is
 * what WCAG 2.5.3 (Label in Name) asks for. */
.card.tight{padding:14px}
/* Centred flex, NOT a grid of equal fractions. The tile count varies with what
 * a person may do — two for most people, four for a realm admin — and
 * `1fr` tracks split the whole card between however many there are, so two
 * tiles became two 200px slabs. A fixed basis keeps every tile the same size
 * whoever is looking, and centring keeps the row balanced when it is short.
 * Four at 96px plus gaps is 402px, inside the 412px the card leaves. */
/* `nowrap` plus a cap, NOT a wrapping row with a fixed basis. `flex-wrap` breaks
 * the row before `flex-shrink` can act, so four 96px bases overflowed a 375px
 * card's ~315px content box and stranded the fourth tile on its own line. The
 * items share the row and shrink; the cap stops two tiles becoming slabs. */
.acct{display:flex;flex-wrap:nowrap;justify-content:center;gap:6px}
.acct a{
  flex:1 1 0;max-width:104px;min-width:0;
  display:flex;flex-direction:column;align-items:center;gap:7px;
  padding:12px 6px;border-radius:var(--ws-r-sm);
  text-decoration:none;color:var(--ws-text-muted);
  font:500 12px/16px 'Inter',Arial,sans-serif; /* caption role, exact match */
  text-align:center;
  transition:background var(--ws-motion-fast) var(--ws-ease),color var(--ws-motion-fast) var(--ws-ease);
}
.acct a:hover{background:var(--ws-hover);color:var(--ws-text)}
.acct a:focus-visible{outline:2px solid var(--ws-accent-2);outline-offset:2px;box-shadow:var(--ws-focus-ring)}
.acct a.danger:hover{color:var(--ws-danger)}
.acct svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
/* Signing out leaves; the door glyph is drawn entering, so it is mirrored. */
/* `.flip svg`, not `svg.flip`: the class is on the wrapping span, because the
 * glyph strings in `ICON` are whole `<svg>` elements and the renderer cannot
 * add a class inside one without string surgery. The earlier `svg.flip` form
 * matched nothing and the arrow silently kept pointing the wrong way. */
.acct .flip svg{transform:scaleX(-1)}

/* ---- "no access": a statement, not an error.
 *
 * This used to be a `.msg` line plus a red `.err` block. Nothing has FAILED
 * when a person has no entitlement yet — the sign-in worked perfectly — so the
 * danger role was the wrong one, and it also put 13px text on the danger wash
 * at 4.00:1. Named lines on the plain card instead: what is true, who it is
 * true of, and what to do about it. Type roles are the locked h3, label and
 * body. */
.noacc{text-align:center;padding:4px 0 2px}
.na1{font:600 17px/24px 'Inter',Arial,sans-serif;color:var(--ws-text)}
.na2{font:500 13px/16px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin-top:4px}
.na3{font:400 14px/20px 'Inter',Arial,sans-serif;color:var(--ws-text-muted);margin-top:16px}

/* ---- sticker host Opening screen: the scanned asset id is the subject.
 * JetBrains Mono is the design system's mono face but this repo does not
 * self-host it, so the stack falls to the platform mono rather than pulling a
 * second web font onto a page an installer opens on cellular. */
.centre{text-align:center}
.asset{
  font:500 26px/32px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  color:var(--ws-text);letter-spacing:-.01em;margin:0 0 8px;overflow-wrap:anywhere;
}
/* Indeterminate progress. The hand-off to the commissioner has no measurable
 * progress, so this reports "working", not "how far". */
.bar{
  height:4px;border-radius:var(--ws-r-full);
  background:var(--ws-well);box-shadow:var(--ws-well-inset);
  overflow:hidden;margin:16px auto 0;max-width:220px;
}
.bar span{
  display:block;height:100%;width:40%;border-radius:var(--ws-r-full);
  background:var(--ws-accent);
  animation:ws-slide 1.4s cubic-bezier(.4,0,.6,1) infinite;
}
@keyframes ws-slide{0%{transform:translateX(-110%)}100%{transform:translateX(320%)}}

/* =============================================================================
 * PAGE-LOCAL — rules that apply to one page only, scoped by the body class
 * (`ws-page-launcher`/`ws-page-onboard`) rather than living in a page's own
 * inline <style> block. Neither page keeps one of those; this is where a
 * page-only rule belongs instead.
 *
 * EMPTY on purpose. The only entry was `.ws-page-launcher .hint`, which
 * centred the "apps open in a new tab" footnote; that copy is gone and the
 * launcher emits no `.hint` at all now, so the rule went with it. The section
 * stays because `h2` above is scoped the same way and a later page-only rule
 * belongs here rather than in a new inline block.
 * ========================================================================== */

/* =============================================================================
 * MOTION SAFETY — every transition in this file runs at zero duration under
 * reduced motion; nothing here uses `scroll-behavior`, so that rule is not
 * carried over.
 *
 * The spinner (`.spin`) is the one deliberate exception: it keeps animating,
 * only slower. Stopping it outright would remove the page's only "something
 * is happening" signal during the OIDC round trip — a static ring reads as a
 * stuck or broken page, not a page respecting a motion preference. Slowing
 * it is the reduction; it is not an oversight.
 * ========================================================================== */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{transition-duration:0s!important;transition-delay:0s!important}
  .spin{animation-duration:2.4s}
  /* The indeterminate bar stops outright and fills instead. It differs from
   * the spinner because the line of text beside it already says what is
   * happening, so a still bar cannot be read as a stalled page. */
  .bar span{animation:none;width:100%}
}
