﻿/* ==========================================================================
   SKlone design system  (sk-)
   --------------------------------------------------------------------------
   Purely additive. Nothing here overrides an old-design selector, and nothing
   outside this file references var(--sk-*), so linking it on a page that
   carries no sk- classnames cannot change how that page renders.

   The old .pref-* / .members-table rules in general.css stay exactly as they
   are and keep serving every page that has not migrated yet. This file is what
   they migrate TO, one page at a time.

   Three rules govern everything below:

   1. ASSUME HOSTILE BASE STYLES. These components land on pages already loading
      _table.css, _main.css, _typography.css and Bootstrap. Each component
      resets what those impose on it, scoped under its own class — never by
      editing the base stylesheet, which is the old design and off-limits.
      Resets use :where() so they carry ZERO specificity: they beat bare
      element selectors like _table.css's `td`, while still losing to any
      deliberate page-level override. No !important needed.
   2. NO BOOTSTRAP. No selector references row, col-*, card, container,
      form-group, form-control or any other Bootstrap classname. This matters
      concretely: Auth.Master loads BS4, AuthNew.Master loads BS5, and
      ModulesController.ascx renders under both.
   3. SOLID EDGES. Every border, divider and track is a solid colour. The old
      system tinted them at 0.08-0.20 alpha, which measured 1.1:1-1.3:1 against
      its own surface. Alpha also drifts, because the fixed planets.jpg body
      image scrolls under the cards' backdrop blur.

   Baseline notes (2026), i.e. why this file looks the way it does:
   - color-mix(), :has(), :where() and container queries are all Baseline and
     the first three are used below. Native nesting is Baseline too and is
     DELIBERATELY NOT USED — see the next note.
   - NESTING IS NOT USED ANYWHERE IN THIS FILE, and adding it back is a bug,
     not a style choice. A browser without nesting support does not skip just
     the nested selector: it drops the WHOLE nested block, because the parser
     hits `& > * {` inside a declaration list, treats it as a bad declaration
     and recovers by skipping to the matching brace. That made the Status page
     unusable on a mobile Safari below 16.5, which an iPhone 7 or older can
     never get. Write every rule flat and in full — which also means grepping
     for a classname finds its definition.
   - CASCADE LAYERS ARE DELIBERATELY NOT USED. Unlayered styles beat layered
     ones, and every old stylesheet here is unlayered — putting this system in
     a @layer would make it lose to the CSS it is replacing. Exactly backwards.
   - CONTAINER QUERIES ARE DELIBERATELY NOT USED on cards. container-type makes
     an element the containing block for fixed-position descendants, and this
     app renders a position:fixed modal (.modal-overlay) inside cards. It would
     trap the overlay inside the card.

   See SK_DESIGN_SYSTEM.md for the token reference, the old -> new name map,
   the "classnames only" policy, and the page-migration pattern.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. TOKENS
   --------------------------------------------------------------------------
   On :root deliberately. Custom properties are inert until referenced, and
   nothing outside this system references var(--sk-*) — so a global declaration
   is the safest placement, not the riskiest, and a component then works
   anywhere without needing a wrapper element.

   Ratios are measured against --sk-surface-card composited over .main-body's
   #171b27, i.e. rgb(19,26,40).
   -------------------------------------------------------------------------- */

:root {
  /* Palette ------------------------------------------------------------------
     Every hue the components use, as a raw value written once, here. The
     tokens below
     say what a colour MEANS (--sk-accent, --sk-res-pop, --sk-alert ...) and
     point at a palette entry, and components read those. A player theme
     overrides palette entries or meanings on :root[data-sk-palette="<name>"]
     - see "Player themes" in SK_DESIGN_SYSTEM.md.

     Named for the colour rather than a meaning, so two meanings can share one
     entry and be split later by pointing one of them elsewhere. Tailwind's
     names where the value is Tailwind's; the rest by the names the colour
     standard gives them (the accent blues keep "accent", the app's one action
     colour). Neutrals (surfaces, text, borders) are not
     here: they are already one token each and a theme overrides them
     directly. */
  --sk-pal-accent: #447cff;
  --sk-pal-accent-light: #5b8cff;
  --sk-pal-accent-mid: #3b6fe0;
  --sk-pal-blue-800: #1e40af;
  --sk-pal-blue-900: #1e3a8a;
  --sk-pal-amber-300: #fcd34d;
  --sk-pal-amber-400: #fbbf24;
  --sk-pal-amber-700: #b45309;
  --sk-pal-amber-900: #78350f;
  --sk-pal-yellow-500: #eab308;
  --sk-pal-yellow-600: #ca8a04;
  --sk-pal-cyan-300: #67e8f9;
  --sk-pal-cyan-400: #22d3ee;
  --sk-pal-cyan-700: #0e7490;
  --sk-pal-cyan-900: #164e63;
  --sk-pal-purple-300: #d8b4fe;
  --sk-pal-purple-500: #a855f7;
  --sk-pal-violet-700: #6d28d9;
  --sk-pal-violet-900: #4c1d95;
  --sk-pal-green-300: #86efac;
  --sk-pal-green-500: #22c55e;
  --sk-pal-green-600: #16a34a;
  --sk-pal-red-300: #fca5a5;
  --sk-pal-red-400: #f87171;
  --sk-pal-red-500: #ef4444;
  --sk-pal-red-600: #dc2626;
  --sk-pal-red-700: #b91c1c;
  --sk-pal-red-900: #7f1d1d;
  --sk-pal-mint: #71f4a8;
  --sk-pal-rose: #fb6a82;
  --sk-pal-scarlet: #ff5542;
  --sk-pal-magenta: #fb60dc;
  --sk-pal-orange: #fc9840;
  --sk-pal-gold: #fcca40;
  --sk-pal-purple: #c28dfc;
  --sk-pal-blue: #529dff;
  --sk-pal-lime: #b9e34a;
  --sk-pal-teal: #3fd9c8;
  --sk-pal-steel: #c3cfe2;
  --sk-pal-unit-red: #ff6257;
  --sk-pal-unit-yellow: #f4e04d;
  --sk-pal-peach: #ffd1ae;
  --sk-pal-azure: #3ba2ff;

  /* Base hues. Every tint below is derived from these with color-mix(), so an
     accent change propagates instead of needing 12 rgba() literals updated. */
  --sk-accent: var(--sk-pal-accent);          /* 4.62:1  safe for text and icons */
  --sk-accent-hover: var(--sk-pal-accent-light);      /* 5.51:1 */
  --sk-success: var(--sk-pal-green-500);          /* 7.64:1 */
  --sk-danger: var(--sk-pal-red-400);          /* 6.29:1  lightened from #ef4444 to clear AA as text */
  --sk-warning: var(--sk-pal-amber-400);          /* 10.43:1 */

  /* Faction hues -------------------------------------------------------------
     A kingdom-war palette, not decoration: each hue means something, so a
     player learns to read a panel's colour before its heading. Gold is taken
     from the live Mine Gold styling (probe-styles.css:368) rather than invented,
     so migrated pages keep the identity they already have.

        gold    economy, quick actions, newbie features
        ion     scanning, intelligence
        plasma  premium, magic
        toxic   growth, production, success
        alert   destruction, danger

     These say which KIND of panel something is. A reading about a subject -
     one of the eight buildings or its resource, land, missiles, networth,
     time, research, offense, defense, an alliance relation or a unit group -
     takes that subject's hue instead (see "Concept hues" and "Subject hues"
     below), never a faction hue.

     Every value below is a light tint chosen to clear 4.5:1 as text on the card
     surface - these are foreground colours, not fills. */
  --sk-gold: var(--sk-pal-amber-300);
  --sk-ion: var(--sk-pal-cyan-300);
  --sk-plasma: var(--sk-pal-purple-300);
  --sk-toxic: var(--sk-pal-green-300);
  --sk-alert: var(--sk-pal-red-300);

  /* The saturated end of each hue: stripes, gradients and glows only, never
     text. */
  --sk-gold-deep: var(--sk-pal-yellow-500);
  --sk-ion-deep: var(--sk-pal-cyan-400);
  --sk-plasma-deep: var(--sk-pal-purple-500);
  --sk-toxic-deep: var(--sk-pal-green-500);
  --sk-alert-deep: var(--sk-pal-red-500);

  /* Concept hues ------------------------------------------------------------
     One colour per building and the resource it produces, so population is
     the same green on every page it appears - a pill on Buildings, a card on
     Status, the tint behind the Residences artwork.

     Chosen, not derived. Population, Barracks and Income keep their building's
     artwork colour (sampled from the artwork once, when the tokens were chosen).
     The rest are picked to be told apart and to group by meaning: Barracks,
     ASB and ACH are one family of reds - rose, scarlet, magenta - because all
     three are military housing; Power is orange; Probes blue; Training the
     purple left over, since nothing reads less often.

     Foreground colours like the faction tints above: every one clears 4.5:1
     on the card surface. Applied with the sk-hue-<concept> classes. */
  --sk-res-pop: var(--sk-pal-mint);   /* Residences       12.6:1 */
  --sk-res-barracks: var(--sk-pal-rose);   /* Barracks          6.2:1 */
  --sk-res-asb: var(--sk-pal-scarlet);   /* ASB               5.5:1 */
  --sk-res-ach: var(--sk-pal-magenta);   /* ACH               6.5:1 */
  --sk-res-power: var(--sk-pal-orange);   /* Power plants      8.0:1 */
  --sk-res-income: var(--sk-pal-gold);   /* Star mines       11.3:1 */
  --sk-res-training: var(--sk-pal-purple);   /* Training camps    7.1:1 */
  --sk-res-probes: var(--sk-pal-blue);   /* Probe factories   6.3:1 */

  /* Subject hues --------------------------------------------------------------
     The same idea for everything else a reading can be about. Some share a
     colour on purpose, because they rarely meet on one page: research and
     modules take training's purple, time takes networth's steel, offense is
     war's red and defense NAP's cyan. Each still has its own meaning, so
     splitting two apart is one line here. */
  --sk-res-land: var(--sk-pal-lime);            /* 11.7:1 */
  --sk-res-missile: var(--sk-pal-teal);         /*  9.9:1 */
  --sk-res-networth: var(--sk-pal-steel);       /* 11.1:1  networth, rank, scores */
  --sk-subject-time: var(--sk-pal-steel);       /* ticks, timers, round end */
  --sk-subject-research: var(--sk-pal-purple);  /* research, scientists, modules */
  --sk-subject-offense: var(--sk-pal-red-300);
  --sk-subject-defense: var(--sk-pal-cyan-300);
  /* Military / training-room pages, and the nav's fist that leads there:
     one red for the page's header, its CTAs and the nav icon. */
  --sk-subject-military: var(--sk-pal-red-500);
  /* Modules, and the nav's atom that leads there (general.css's #206BDF, moved
     onto the nearest palette step). */
  --sk-subject-modules: var(--sk-pal-accent-mid);

  /* Alliance relations, one mapping for every page that shows them. */
  --sk-rel-war: var(--sk-pal-red-300);
  --sk-rel-barricade: var(--sk-pal-green-300);
  --sk-rel-nap: var(--sk-pal-cyan-300);
  --sk-rel-ally: var(--sk-pal-gold);

  /* Kingdom state icons (Growth / Mobilisation / Offensive / Defensive) - the
     game's own colours, kept EXACTLY as the pre-sk Sector View had them
     (general.css .growth/.mobilisation/.offensive/.defensive). Not palette
     steps on purpose: players read a state by these colours. */
  --sk-kd-state-growth: #006400;        /* darkgreen */
  --sk-kd-state-mobilisation: #8b0000;  /* darkred */
  --sk-kd-state-offensive: #98fb98;     /* palegreen */
  --sk-kd-state-defensive: #00ffff;     /* aqua */

  /* Unit groups. Soldiers and defensive units keep the colours their artwork
     was sampled at; offensive units are a stronger red and hybrid a yellow
     between them; scientists (growth) are research. The artwork itself is
     recoloured to these in the browser (sk-art-recolour.js). */
  --sk-unit-soldiers: var(--sk-pal-peach);      /* 12.4:1 */
  --sk-unit-defensive: var(--sk-pal-azure);     /*  6.5:1 */
  --sk-unit-hybrid: var(--sk-pal-unit-yellow);  /* 12.9:1 */
  --sk-unit-offensive: var(--sk-pal-unit-red);  /*  5.9:1 */
  --sk-unit-growth: var(--sk-subject-research);

  /* Gradients for primary calls to action. The gold one mirrors the live
     Mine Gold button (probe-styles.css:422) so the system absorbs the existing
     look instead of competing with it. */
  --sk-grad-accent: linear-gradient(135deg, var(--sk-pal-accent-mid) 0%, var(--sk-pal-blue-900) 100%); /* white 4.63:1 at the light end */
  --sk-grad-gold: linear-gradient(135deg, var(--sk-pal-amber-700) 0%, var(--sk-pal-amber-900) 100%); /* live CTA starts #ca8a04 = 2.94:1 with white; this is 5.02:1 */
  --sk-grad-ion: linear-gradient(135deg, var(--sk-pal-cyan-700) 0%, var(--sk-pal-cyan-900) 100%); /* 5.36:1 */
  --sk-grad-plasma: linear-gradient(135deg, var(--sk-pal-violet-700) 0%, var(--sk-pal-violet-900) 100%); /* 7.10:1 */
  --sk-grad-alert: linear-gradient(135deg, var(--sk-pal-red-700) 0%, var(--sk-pal-red-900) 100%); /* 6.47:1 */

  /* Coloured elevation. A CTA on a starfield needs to sit above it, and a hue
     glow does that without another border. */
  --sk-glow-accent: 0 4px 14px color-mix(in srgb, var(--sk-pal-accent) 35%, transparent);
  --sk-glow-gold: 0 4px 14px color-mix(in srgb, var(--sk-pal-yellow-600) 35%, transparent);
  --sk-glow-ion: 0 4px 14px color-mix(in srgb, var(--sk-pal-cyan-400) 30%, transparent);
  --sk-glow-plasma: 0 4px 14px color-mix(in srgb, var(--sk-pal-purple-500) 32%, transparent);
  --sk-glow-alert: 0 4px 14px color-mix(in srgb, var(--sk-pal-red-600) 35%, transparent);

  /* Surfaces. Still glassmorphic — the blur is the design's character; only the
     borders stopped being transparent. */
  /* 0.45, measured rather than chosen. The binding constraint is not text —
     over the brightest blurred region of planets.jpg the body copy still reads
     12.8:1 at this alpha, and would hold past 0.20. It is --sk-border needing
     3:1: at 0.72 it had headroom to spare, and the glass was doing far less
     work than the design intends. See --sk-border below, which moved one step
     to buy this. Because the card fill and .main-body are near-identical in
     luminance, dropping the alpha shifts the card over the DARK background by
     about one point of 8-bit value, so every ratio documented against
     rgb(19,26,40) still holds. */
  /* The page itself. It had no token - the value lived only as a literal in
     scripts/_main.css (.main-body) - which meant nothing in the system could
     refer to the ground its surfaces composite against. Bootstrap needs it too:
     its --bs-body-bg default is #fff. */
  --sk-surface-page: #171b27;
  /* The tooltip surface. Another value that only existed as a literal in
     general.css (rgba(16, 56, 148, 1)), with no token to refer to it. */
  --sk-surface-tooltip: rgba(16, 56, 148, 1);
  --sk-surface-card: rgba(18, 25, 41, 0.45);
  --sk-surface-inner: rgba(2, 6, 23, 0.55);
  --sk-surface-table: rgba(15, 23, 42, 0.55);
  /* Lighter and near-opaque so a table header or input reads as a distinct
     plane. At rgba(30,41,66,.9) it measured 1.22:1 against the table body -
     the "raised" surface was not raised by luminance at all, only by its
     border. */
  --sk-surface-raised: rgba(42, 58, 92, 0.96);
  /* Fields are RECESSED, not raised. Reusing --sk-surface-raised for inputs
     conflated two opposite affordances - a table header or resting tab sits
     above the card, a field you type into sits below it - and it did not pass:
     placeholder text landed at 4.42:1 and the field's own border at 2.66:1
     against it. This is darker than the card on purpose, and measures
     text 18.1:1, placeholder 7.4:1, border 4.4:1, focus ring 5.0:1. */
  --sk-surface-field: rgba(8, 13, 28, 0.90);
  --sk-surface-overlay: rgba(0, 0, 0, 0.65);

  /* For a control with NOTHING behind it - sitting on planets.jpg rather than on
     a card. Nearly opaque, because every wash in this system is measured against
     the card surface and a wash over a bright nebula is not a surface at all,
     it is a tint on a photograph. sk-pill has always used this value; the token
     exists so the next component that needs it copies a name instead of the
     number. Companion to sk-on-image, which does the same job for text. */
  --sk-surface-on-image: rgba(11, 16, 33, 0.92);

  /* Same colour as sk-surface-on-image, fully opaque. For a piece that must
     not let anything behind it show through at all - #0b1021 is the exact
     same rgb(11,16,33), just without the 8% that lets the starfield bleed
     through at the edges. */
  --sk-surface-on-image-solid: #0b1021;

  /* brightness() is doing real work, not styling. The card is 72% opaque over
     planets.jpg, whose brightness (after the page's dark gradient) runs 0-188.
     Over its bright regions the card composited to rgb(66,71,82) and every
     border fell under 2:1. Clamping the backdrop keeps the glass while making
     the surface predictable wherever a card lands on the image. */
  --sk-blur: blur(10px) brightness(0.35) saturate(1.15);

  /* Borders and non-text UI. WCAG 1.4.11 wants >= 3.0:1 for anything you must
     perceive to operate a control. */
  --sk-border: #6b7c93;              /* 4.07:1 on the dark card, 3.15:1 over the image's brightest
                                        region. Was #64748b, which held 3.66:1 dark but fell to
                                        2.82:1 over the bright regions once --sk-surface-card
                                        dropped to 0.45 - the one token that had to move to make
                                        the glass more transparent. */
  --sk-border-accent: var(--sk-pal-accent-light); /* 5.51:1  was #3863ca, which fell to 1.70:1 over the image's bright regions */
  --sk-border-strong: #94a3b8;       /* 6.79:1  hover, focus ring, emphasis */
  --sk-border-subtle: #3f4a5f;       /* 1.95:1  DECORATIVE ONLY - never a control edge */
  --sk-success-border: var(--sk-pal-green-600); /* 5.28:1 */
  --sk-danger-border: var(--sk-pal-red-600); /* 3.61:1 */

  /* Fills. Washes are never used as an edge — only as a background. */
  --sk-wash-accent: color-mix(in srgb, var(--sk-accent) 16%, transparent);
  --sk-wash-accent-strong: color-mix(in srgb, var(--sk-accent) 30%, transparent);
  --sk-wash-success: color-mix(in srgb, var(--sk-success) 14%, transparent);
  --sk-wash-success-strong: color-mix(in srgb, var(--sk-success) 28%, transparent);
  --sk-wash-danger: color-mix(in srgb, var(--sk-danger) 14%, transparent);
  --sk-wash-danger-strong: color-mix(in srgb, var(--sk-danger) 28%, transparent);

  /* Text */
  --sk-text: #f8fafc;                /* 16.64:1 */
  --sk-text-muted: #94a3b8;          /* 6.79:1  replaces #787f88 (4.30:1, below AA) */
  --sk-text-on-accent: #ffffff;

  /* ------------------------------------------------------------------
     Bootstrap colour handoff.

     Bootstrap 5.3 drives every colour through --bs-* custom properties, so
     pointing those at our tokens makes Bootstrap render OUR palette instead of
     its own - .text-muted, .text-primary, .border-secondary, button and alert
     colours, all of it. No !important duel, no markup to change.

     This file loads last, so these beat Bootstrap's own :root. Nothing in the
     app sets data-bs-theme, so :root is enough.

     The -rgb triplets exist because Bootstrap composes them as
     rgba(var(--bs-primary-rgb), var(--bs-text-opacity)), and CSS cannot split a
     hex into channels. KEEP THEM IN SYNC with the hex tokens above - changing
     --sk-accent alone will leave every Bootstrap utility on the old colour.
     ------------------------------------------------------------------ */
  --bs-primary: var(--sk-accent);
  --bs-primary-rgb: 68, 124, 255;            /* #447cff, --sk-accent */
  --bs-secondary: var(--sk-text-muted);
  --bs-secondary-rgb: 148, 163, 184;         /* #94a3b8, --sk-text-muted */
  --bs-secondary-color: var(--sk-text-muted);/* what .text-muted actually reads */
  --bs-success: var(--sk-success);
  --bs-success-rgb: 34, 197, 94;             /* #22c55e, --sk-success */
  --bs-danger: var(--sk-danger);
  --bs-danger-rgb: 248, 113, 113;            /* #f87171, --sk-danger */
  --bs-warning: var(--sk-warning);
  --bs-warning-rgb: 251, 191, 36;            /* #fbbf24, --sk-warning */
  --bs-info: var(--sk-ion);
  --bs-info-rgb: 103, 232, 249;              /* #67e8f9, --sk-ion */
  --bs-light: var(--sk-text);
  --bs-light-rgb: 248, 250, 252;             /* #f8fafc, --sk-text */
  --bs-link-color: var(--sk-link);
  --bs-link-hover-color: var(--sk-link-hover);

  /* Bootstrap 5.3 ships a LIGHT theme by default - body #fff, text #212529,
     borders #dee2e6, emphasis #000. Any component reading those renders
     light-on-dark here: dropdown menus, modals, tooltips, popovers, tables.
     These are the structural half of the handoff. */
  --bs-body-bg: var(--sk-surface-page);
  --bs-body-color: var(--sk-text);
  --bs-emphasis-color: var(--sk-text);
  --bs-secondary-bg: var(--sk-surface-inner);
  --bs-tertiary-bg: var(--sk-surface-card);
  --bs-tertiary-color: var(--sk-text-muted);
  --bs-border-color: var(--sk-border);
  --bs-border-color-translucent: var(--sk-border-subtle);
  --bs-heading-color: var(--sk-text);
  --bs-code-color: var(--sk-ion);
  --bs-highlight-bg: var(--sk-wash-accent);
  --bs-highlight-color: var(--sk-text);
  --bs-dark: var(--sk-surface-raised);
  --bs-form-valid-border-color: var(--sk-success-border);
  --bs-form-invalid-border-color: var(--sk-danger-border);
  /* --bs-white/-black deliberately left alone: they mean literal white and
     black, and overriding them would surprise anyone reading the markup. */

  /* Links are their own colour, not the accent.
     --sk-accent was doing three jobs at once - primary action, link, and the
     "info" hue - and when everything is blue nothing reads as the action.
     Blue now means DO THIS; links get their own lighter tone; --sk-ion carries
     "informational". */
  --sk-link: #93c5fd;                /* 8.94:1 on the card surface */
  --sk-link-hover: #bfdbfe;          /* 11.71:1 */

  /* Neutral ramp. Without one, every value between "muted" and "white" gets
     invented at the point of use - which is how #787f88, #b2bec3, #cbd5e1,
     #dfe6e9 and #e2e8f0 all ended up in general.css meaning roughly the same
     thing. Five steps, dark to light; use these instead of a new literal. */
  --sk-neutral-1: #1d2436;           /* raised fill on a dark surface */
  --sk-neutral-2: #3f4a5f;           /* decorative divider */
  --sk-neutral-3: #64748b;           /* structural border, 3.66:1 */
  --sk-neutral-4: #94a3b8;           /* muted text, 6.79:1 */
  --sk-neutral-5: #cbd5e1;           /* secondary text, 12.19:1 */

  /* Typography. Matches the app's Verdana base (scripts/_typography.css) so a
     migrated component does not change typeface mid-page.

     --sk-text-min is a FLOOR, not a step. The old system dropped labels to
     0.62rem and status text to 0.52rem under 768px (~10px and ~8px). The fluid
     sizes below clamp against that floor, which also removes the pile of
     font-size overrides the old system needed in its media query.

     --sk-text-dense is the deliberate exception, and it is opt-in. Some tables
     in this app carry more columns than 12px type can fit, and shortening the
     content does not always get there. Rather than lower the floor for
     everything -- --sk-text-min also sets sk-badge, sk-stat-label,
     sk-field-stack-label and sk-eyebrow, none of which are tables -- a table
     asks for it by name with sk-table--dense. */
  /* Segoe UI first: every Windows player already has it, it needs no webfont
     download, and it is ~10% narrower than Verdana - real density back on
     data-heavy pages. Verdana stays as the fallback so nothing shifts on a
     machine without it.

     Caveat for migration: until a page is fully migrated it will show Segoe UI
     inside sk- components and Verdana around them. Migrate a page's visible
     text in one pass rather than leaving it half-and-half. */
  /* A system stack: every entry is already installed on the platform, so there
     is no @font-face here and nothing to download. That is why only Orbitron
     has an @font-face below - it is a .ttf bundled in this repo, while these
     ship with the OS.

     Ordered by platform: Segoe UI (Windows), -apple-system (macOS/iOS),
     Roboto (Android), then Verdana as the universal fallback so a machine with
     none of them lands exactly where the old design already was. */
  --sk-font: "Segoe UI", -apple-system, BlinkMacSystemFont, "Helvetica Neue",
             Roboto, Verdana, Geneva, Tahoma, sans-serif;
  --sk-font-heading: Orbitron-Regular, "Segoe UI", -apple-system, Roboto, Verdana, sans-serif;

  /* PINNED IN PIXELS, deliberately. No root font-size is set anywhere, so rem
     resolves against 16px while the app's body text is 13px
     (scripts/_typography.css) - a rem scale silently ran ~15% large next to
     old-design text. Pixels make the scale independent of that mismatch.
     Still fluid: each step grows with the viewport and bottoms out at the
     12px floor. */
  --sk-text-xxs: 11px;    /* one step under --sk-text-min: a phone's cramped button row, opt-in */
  --sk-text-min: 12px;
  --sk-text-dense: 8px;   /* opt-in, tables only - see sk-table--dense */
  --sk-text-xs: clamp(12px, 11.5px + 0.15vw, 12.5px);
  --sk-text-sm: clamp(12px, 11.4px + 0.2vw, 13.5px);
  --sk-text-base: clamp(13px, 12px + 0.3vw, 15px);
  --sk-text-lg: clamp(15px, 14px + 0.35vw, 17.5px);
  --sk-text-xl: clamp(17px, 15px + 0.5vw, 21px);
  --sk-text-2xl: clamp(19px, 17px + 0.7vw, 26px);
  --sk-weight-normal: 400;
  --sk-weight-medium: 500;
  --sk-weight-semibold: 600;
  --sk-weight-bold: 700;

  /* Spacing */
  --sk-space-1: 4px;
  --sk-space-2: 8px;
  --sk-space-3: 12px;
  --sk-space-4: 16px;
  --sk-space-5: 20px;
  --sk-space-6: 24px;
  /* Clearance for something that sits OUTSIDE a component's own box and
     needs room made for it above - e.g. Modules' tab-actions flag, which
     sits on top of .sk-tabs via translateY, not inside its flow. Not on
     the same +4px rhythm as the rest of the scale; the next real use
     decides whether that gap ever gets filled in. */
  --sk-space-7: 50px;

  /* Gutter and rhythm. Phone values here; the block after :root raises them
     above 768px to what the desktop always had. A layout container reads
     these instead of restating its spacing in a max-width block, so every
     page gets the same screen gutter, card padding and gap between cards.
       --sk-gutter    screen edge to card edge (sk-page, legacy containers)
       --sk-card-pad  card edge to content
       --sk-stack     the gap between stacked cards */
  --sk-gutter: var(--sk-space-2);
  --sk-card-pad: var(--sk-space-3);
  --sk-stack: var(--sk-space-3);

  /* Radius */
  --sk-radius-sm: 6px;
  --sk-radius: 8px;
  --sk-radius-lg: 12px;

  /* The measure of a one-column action page: one card wide rather than the
     1400px page. A list of rows with the name on the left and the buttons on
     the right loses the row across a wide monitor. Shared by Missile.aspx
     (missile.css) and Modules (sk-page--compact). */
  --sk-compact-width: 900px;

  /* Elevation */
  --sk-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
  --sk-shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.6);
  --sk-shadow-inset: inset 0 1px 1px rgba(255, 255, 255, 0.05);

  /* Motion */
  --sk-transition: 0.2s ease;
  --sk-transition-slow: 0.3s ease;
}

/* Breakpoints. Spacing is mobile-first: the phone value is the rule, and a
   min-width: 768.02px block raises it (the .02 closes the gap a fractional
   zoom width would fall through between 768 and 769). max-width: 768px is
   for phone-only BEHAVIOUR - stacking, hiding, reflowing - never for
   restating a padding or a margin. */
@media (min-width: 768.02px) {
  :root {
    --sk-gutter: var(--sk-space-4);
    --sk-card-pad: var(--sk-space-6);
    --sk-stack: var(--sk-space-6);
  }
}


/* Tooltip and popover colours.

   These cannot live in :root. Bootstrap declares --bs-tooltip-* ON .tooltip
   itself, and a declaration on the element beats one inherited from :root, so
   a :root override is silently ignored. Bootstrap's own defaults then apply:

     --bs-tooltip-color: var(--bs-body-bg)      -> our dark page colour
     --bs-tooltip-bg:    var(--bs-emphasis-color) -> our near-white text colour

   which paints dark text on a light chip - inverted, and unreadable against the
   blue surface general.css gives .tooltip-inner. Declared here on the same
   element Bootstrap uses; this file loads last, so it wins the tie.

   --bs-tooltip-bg also colours the arrow: Bootstrap draws it with
   border-*-color: var(--bs-tooltip-bg). */
.tooltip {
  --bs-tooltip-color: var(--sk-text-on-accent);
  --bs-tooltip-bg: var(--sk-surface-tooltip);
  --bs-tooltip-opacity: 1;
  --bs-tooltip-max-width: 300px;
  /* Above every overlay this file stacks: sk-menu 8500, sk-drawer 9000,
     the toast stack 9500, sk-modal 10000. Bootstrap's own 1080 put a
     tooltip on anything inside a drawer (Probe stats' figures) BEHIND the
     drawer - it opened, but nobody could see it. A tooltip is transient
     and follows the pointer, so it is the one thing that may cover them. */
  z-index: 10100;
}

/* Legacy tables keep their own surface.

   Bootstrap paints every cell:
     background-color: var(--bs-table-bg)
   and --bs-table-bg defaults to var(--bs-body-bg), which our :root handoff
   points at --sk-surface-page. So each cell laid a solid #171b27 over the
   rgb(34, 34, 34) that scripts/_table.css gives the table, and the legacy look
   disappeared on the 31 elements carrying class="table".

   transparent lets the table's own background show through, which is what BS4
   effectively did - it never painted cell backgrounds at all.

   The inset 9999px box-shadow in the same rule is harmless once this is set:
   it draws --bs-table-accent-bg, which is already transparent unless a striped
   or hover variant is in play. Those are mapped to our washes below so they
   read on a dark surface instead of Bootstrap's light-theme alphas.

   Declared on .table for the same reason as .tooltip above: Bootstrap declares
   these on the component, and a :root override would never apply. */
.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--sk-text);
  --bs-table-border-color: var(--sk-border);
  --bs-table-striped-color: var(--sk-text);
  --bs-table-striped-bg: var(--sk-wash-accent);
  --bs-table-hover-color: var(--sk-text);
  --bs-table-hover-bg: var(--sk-wash-accent);
  --bs-table-active-color: var(--sk-text);
  --bs-table-active-bg: var(--sk-wash-accent-strong);
}

.popover {
  --bs-popover-bg: var(--sk-surface-raised);
  --bs-popover-header-bg: var(--sk-surface-inner);
  --bs-popover-body-color: var(--sk-text);
  --bs-popover-header-color: var(--sk-text);
  --bs-popover-border-color: var(--sk-border);
}
/* Orbitron is the app's heading face. These two @font-face rules are BYTE-FOR-BYTE
   the same faces general.css:35-45 already declares — same family name, same
   descriptors, same files — so on an app page this is a duplicate the browser
   dedupes, and it changes nothing. It exists so the standalone preview page,
   which does not load general.css, renders the real heading font.

   Do NOT add weights here that general.css does not declare (e.g. a 700 face
   pointing at Orbitron-Bold.ttf). That would change how existing pages resolve
   bold headings, which is exactly the kind of leak this system must not cause. */
@font-face {
  font-family: Orbitron-Regular;
  src: url(../fonts/Orbitron-Regular.ttf);
  font-display: swap;
}

@font-face {
  font-family: Orbitron-Regular;
  font-weight: 500;
  src: url(../fonts/Orbitron-Bold.ttf);
  font-display: swap;
}

/* Where backdrop-filter is unsupported, the translucent surfaces would sit
   directly on the planets.jpg image and lose the contrast every ratio above
   assumes. Opaque fallbacks keep the measurements honest. */
@supports not (backdrop-filter: blur(1px)) {
  :root {
    --sk-surface-card: #131a28;
    --sk-surface-inner: #0b1021;
    --sk-surface-table: #101828;
    --sk-surface-overlay: rgba(0, 0, 0, 0.85);
  }
}


/* --------------------------------------------------------------------------
   2. TYPOGRAPHY
   --------------------------------------------------------------------------
   Bare element selectors are IMPOSSIBLE here. This stylesheet is linked from
   both master pages, so it loads on all ~74 pages — a global `h1 {}` or `p {}`
   rule would restyle the entire old design. Element typography is therefore
   scoped to the components that opt in.

   Two opt-in surfaces:
     .sk-card   — anything inside a card, which is where most content lives.
     .sk-prose  — a plain wrapper for card-less regions that still want the
                  system's typography.

   :where() keeps these at zero specificity, so ANY page rule, old or new, still
   wins. They set a floor, not a law.

   @scope would express this more directly — `@scope (.sk-card) { h4 { ... } }`
   — and reached Baseline in Feb 2026 when Firefox 146 shipped it. It is not
   used because "newly available" is too new for this audience: a player on a
   Firefox older than 146 would lose all heading styling. Revisit in a year.

   Headings keep Orbitron (the app's display face) rather than the body Verdana;
   general.css:104 already does this for the old design, and the system now
   states it explicitly instead of inheriting it by accident. */

.sk-card :where(h1, h2, h3, h4, h5, h6),
.sk-prose :where(h1, h2, h3, h4, h5, h6) {
  margin: 0 0 var(--sk-space-3);
  font-family: var(--sk-font-heading);
  font-weight: var(--sk-weight-semibold);
  line-height: 1.25;
  letter-spacing: 0.5px;
  color: var(--sk-text);
  text-wrap: balance;
}

/* Every level is a distinct size. h3 and h4 were both --sk-text-lg, which made
   two heading levels indistinguishable in the style guide. Card headers are
   unaffected: .sk-card-header pins its own heading size regardless of level. */
.sk-card :where(h1), .sk-prose :where(h1) { font-size: var(--sk-text-2xl); }
.sk-card :where(h2), .sk-prose :where(h2) { font-size: var(--sk-text-xl); }
.sk-card :where(h3), .sk-prose :where(h3) { font-size: var(--sk-text-lg); }
.sk-card :where(h4), .sk-prose :where(h4) { font-size: var(--sk-text-base); }
.sk-card :where(h5, h6),
.sk-prose :where(h5, h6) {
  font-size: var(--sk-text-sm);
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--sk-text-muted);
}

.sk-card :where(p),
.sk-prose :where(p) {
  margin: 0 0 var(--sk-space-3);
  /* Body copy ran the full card width - 150+ characters per line on a wide
     monitor, which is roughly twice a comfortable measure. */
  max-width: 70ch;
  font-family: var(--sk-font);
  font-size: var(--sk-text-base);
  line-height: 1.6;
  color: var(--sk-text);
  text-wrap: pretty;
}

.sk-card :where(p, h1, h2, h3, h4, h5, h6):last-child,
.sk-prose :where(p, h1, h2, h3, h4, h5, h6):last-child {
  margin-bottom: 0;
}

.sk-card :where(small),
.sk-prose :where(small) {
  font-size: var(--sk-text-sm);
  color: var(--sk-text-muted);
}

.sk-card :where(strong, b),
.sk-prose :where(strong, b) {
  font-weight: var(--sk-weight-bold);
  color: var(--sk-text);
}

.sk-card :where(ul, ol),
.sk-prose :where(ul, ol) {
  margin: 0 0 var(--sk-space-3);
  padding-left: var(--sk-space-5);
  font-size: var(--sk-text-base);
  line-height: 1.6;
}

/* Links inside a component. _typography.css colours bare `a` #447CFF already;
   restated so a card is not dependent on that file staying as it is. */
.sk-card :where(a:not(.sk-btn)),
.sk-prose :where(a:not(.sk-btn)) {
  color: var(--sk-link);
  text-decoration: none;
}

.sk-card :where(a:not(.sk-btn)):hover,
.sk-prose :where(a:not(.sk-btn)):hover {
  color: var(--sk-link-hover);
  text-decoration: underline;
}

/* Icons pick up their colour from context by default; the header rule below is
   the one place the system tints them. */
.sk-card :where(code, kbd, samp),
.sk-prose :where(code, kbd, samp) {
  font-family: Consolas, Monaco, monospace;
  font-size: 0.9em;
  color: var(--sk-text);
  background: var(--sk-surface-raised);
  padding: 1px 5px;
  border-radius: var(--sk-radius-sm);
}

.sk-prose {
  font-family: var(--sk-font);
  font-size: var(--sk-text-base);
  color: var(--sk-text);
}

/* ---- Stored markup: HTML a stored procedure wrote ------------------------
   Some text reaches a page as markup the database concatenated - the attack
   report (Attacks_history.AttackerNews) is the first user: <br> between
   lines, <b> round the figures, Font Awesome <i>s beside them, one picture.
   It carries no sk- classes and never will, so this container styles those
   bare tags from outside. What reaches it has been through
   AttackReportHtml.ToSafeHtml, which keeps only those tags.

   The picture is sized here: the procs' own inline style on it is dropped by
   that helper, and "height: 184" (no unit) never applied anyway. :where()
   keeps every descendant rule at zero specificity, as sk-prose does. */
.sk-stored-markup {
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  line-height: 1.7;
  color: var(--sk-text);
  overflow-wrap: anywhere;
}

.sk-stored-markup :where(b, strong) {
  font-weight: var(--sk-weight-bold);
  color: var(--sk-text);
  font-variant-numeric: tabular-nums;
}

.sk-stored-markup :where(i[class*="fa-"]) {
  color: var(--sk-accent);
  margin-right: 2px;
}

/* A stored report opens with its picture and then a <br>: after a block
   image that <br> is an empty line, a gap between picture and text (and,
   where the picture is missing - the battle images are only on the live
   site - a gap at the top of the report). The picture's own margin is the
   spacing. */
.sk-stored-markup img:first-child + br {
  display: none;
}

/* A heading row for a report panel: what it is on the left, the outcome
   badge on the right (Attack.aspx's result card). */
.sk-stored-markup-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sk-space-2);
  margin-bottom: var(--sk-space-2);
}

.sk-stored-markup :where(a) {
  color: var(--sk-link);
}

.sk-stored-markup :where(img) {
  display: block;
  max-width: 100%;
  max-height: 184px;
  width: auto;
  height: auto;
  margin: 0 auto var(--sk-space-2);
  border-radius: var(--sk-radius-sm);
}

/* The report on a panel of its own, for when there is a picture or a clip
   behind it (Attack.aspx's result card plays one): thin text straight over
   explosions does not read. The backing is the surface for content over
   imagery; the art stays visible round the panel. The panel is centred and
   its lines capped at 70ch - a card-wide line of prose is hard to follow
   back to its start - while the text in it stays left-aligned. On a phone
   the cap is wider than the card, so the panel takes the card's width.
   Attack history's expanded report does not use it (nothing behind it). */
.sk-stored-markup--panel {
  max-width: 70ch;
  margin-inline: auto;
  padding: var(--sk-space-3) var(--sk-space-4);
  /* 75% opaque, not --sk-surface-on-image's 92%: the clip and its still
     show through behind the report (Attack's result card - the owner,
     2026-10-19). */
  background: color-mix(in srgb, var(--sk-surface-on-image-solid) 75%, transparent);
  border-radius: var(--sk-radius-lg);
}


/* --------------------------------------------------------------------------
   3. CARD
   -------------------------------------------------------------------------- */

.sk-card {
  background: var(--sk-surface-card);
  backdrop-filter: var(--sk-blur);
  -webkit-backdrop-filter: var(--sk-blur);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius-lg);
  padding: var(--sk-card-pad);
  margin-bottom: var(--sk-stack);
  box-shadow: var(--sk-shadow);
  color: var(--sk-text);
  font-family: var(--sk-font);
}

.sk-card-header {
  display: flex;
  align-items: center;
  /* Wraps rather than squeezing the heading: a badge on a narrow card was
     forcing "Mine Gold" onto two lines. */
  flex-wrap: wrap;
  gap: var(--sk-space-3);
  /* No rule under the header: the owner's call (2026-10-18). With no rule
     there is nothing for a padding-bottom to hold off, so the margin alone
     spaces the header from the content. */
  border-bottom: 0;
  margin-bottom: var(--sk-space-3);
}

@media (min-width: 768.02px) {
  .sk-card-header {
    margin-bottom: var(--sk-space-5);
  }
}

/* Zero-specificity reset: the base stylesheets set their own margins and
   colours on bare h1-h6. */
.sk-card-header :where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-semibold);
  letter-spacing: 0.5px;
  color: var(--sk-text);
  text-wrap: balance;
  /* The heading takes the slack, so everything after it packs against the far
     end — badge, help trigger, or both. The badge's own margin-left: auto
     below is not enough once a header carries two trailing elements, and it
     does nothing at all when the badge is conditional and absent: the trigger
     would drift back to the heading exactly when there is no kingdom
     selected. Two auto margins simply share the free space, so the pair still
     lands right. */
  margin-right: auto;
}

.sk-card-header :where(i) {
  color: var(--sk-accent);
  font-size: var(--sk-text-xl);
  line-height: 1;
}

/* sk-card-header--eyebrow-mobile: the card's full header on a desktop, an
   eyebrow on a phone - sk-eyebrow's small muted caps, the icon at that size in
   the card's hue - the way the action pages head their sections there. For a
   page that keeps dev's cards on a desktop (Mine Gold), so its phone screen
   does not open with a stack of large titles. Same (0,1,0) as the two rules
   above, so it is declared after them. */
@media (max-width: 768px) {
  .sk-card-header--eyebrow-mobile {
    gap: var(--sk-space-2);
    padding-bottom: 0;
  }

  .sk-card-header--eyebrow-mobile :where(h1, h2, h3, h4, h5, h6) {
    font-family: var(--sk-font);
    font-size: var(--sk-text-min);
    font-weight: var(--sk-weight-semibold);
    color: var(--sk-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.8px;
  }

  .sk-card-header--eyebrow-mobile :where(i) {
    color: var(--sk-hue, var(--sk-accent));
    font-size: var(--sk-text-min);
  }
}

/* A collapsed card ends at its header. The rule under the header and the gap
   after it separate the header from a body; with no body they drew a hairline
   and an empty strip at the bottom of the card. Two ways a body goes away:

     :last-child           a v-if / server condition left nothing after the
                           header (comment nodes do not count as children).
     .sk-card--collapsed   the body is still there but hidden - v-show, or
                           sk-is-hidden - so the page binds this from the same
                           flag that hides it (Attack's Intelligence card).

   Qualified with .sk-card / .sk-card-inner so it outranks the variants that
   set the header's own spacing (--compact, --header-fill, --host), which are
   (0,2,0) and declared later. No card in the markup has a header as its only
   child while open, so an open card is untouched. The width alone, not the
   border shorthand: the style stays solid, so the width can ease (below). */
.sk-card > .sk-card-header:last-child,
.sk-card-inner > .sk-card-header:last-child,
.sk-card.sk-card--collapsed > .sk-card-header {
  border-bottom-width: 0;
  padding-bottom: 0;
  margin-bottom: 0;
}

/* The same, on a phone only: for a card or panel whose body collapses on a
   phone alone and is always shown on a wider screen (the body hidden with
   sk-hide-mobile, this class bound to the same flag). Attack's Target
   analytics panel shuts to its header on a phone. */
@media (max-width: 768px) {
  .sk-card.sk-card--collapsed-mobile > .sk-card-header,
  .sk-card-inner.sk-card--collapsed-mobile > .sk-card-header {
    border-bottom-width: 0;
    padding-bottom: 0;
    margin-bottom: 0;
  }
}

/* A card that collapses in place: sk-card--collapsible always, sk-card--collapsed
   bound to the same flag as the body's <transition name="sk-slide">. The
   header's rule and gap ease out (and back) with the slide - same duration,
   same easing, starting on the same flip - so header and body close as one
   motion. Without it they went the instant the flag flipped and the body slid
   after: a jump, then the slide. */
.sk-card.sk-card--collapsible > .sk-card-header {
  transition:
    border-bottom-width var(--sk-transition-slow),
    padding-bottom var(--sk-transition-slow),
    margin-bottom var(--sk-transition-slow);
}

/* Shut, a collapsible card is one slim bar: less padding above and below its
   header, and the Show / Hide control without the margin it keeps between a
   body and what follows. At full padding a shut Attack history read heavier
   than the ATTACK button over it (the owner, 2026-10-19). Every shut
   collapsible card, Defender Intelligence on a phone too. */
.sk-card.sk-card--collapsible.sk-card--collapsed {
  padding-top: var(--sk-space-3);
  padding-bottom: var(--sk-space-3);
}

.sk-card-header button.sk-disclosure {
  margin: 0;
}

/* ---- Header with a title group and actions -------------------------------
   The header already packs anything after a bare heading against its far
   end. These three cover the two cases that did not work:

     .sk-card-title     a heading WITH a subtitle under it. A wrapper div is
                        not a heading, so it takes the slack itself - and
                        min-width: 0 lets a long name wrap inside it rather
                        than pushing the actions onto a line of their own.
     .sk-card-subtitle  the line under the title: coordinates, a date.
     .sk-card-actions   the buttons. Never wraps, never shrinks, so on a phone
                        they stay on the title's line and the TITLE gives way.

       <div class="sk-card-header">
         <div class="sk-card-title"><h2>Name</h2>
           <span class="sk-card-subtitle">(X:6;Y:4) Sector 13</span></div>
         <div class="sk-card-actions">
           <a class="sk-btn sk-btn--icon" ...><i class="fas fa-cog"></i></a>
         </div>
       </div>

   The heading inside a title group inherits its colour instead of forcing
   --sk-text, so a state class on the header (Profile's dead / sl / newbie)
   still colours the name. Declared after the header's own heading rule, so
   the (0,1,0) ties break this way. */
.sk-card-title {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: var(--sk-space-1);
  min-width: 0;
  margin-right: auto;
}

.sk-card-title > :where(h1, h2, h3, h4, h5, h6) {
  margin-right: 0;
  color: inherit;
  overflow-wrap: break-word;
}

.sk-card-subtitle {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.sk-card-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--sk-space-2);
}

/* A CTA among a header's actions, on a phone: sk-btn--cta's 28px sides are
   sized for a page's committing action, and beside a heading and a badge they
   push the actions onto a line of their own (Attack's Simulate, next to
   "Powerless ON", came out 5px too wide at 375px). Narrower sides keep the
   header on one line and the button its icon and word. (0,2,1), to beat
   button.sk-btn--cta's (0,1,1) wherever either is declared. */
@media (max-width: 768px) {
  .sk-card-actions > button.sk-btn--cta,
  .sk-card-actions > a.sk-btn--cta {
    padding-inline: var(--sk-space-3);
  }
}

/* A header that names the PAGE's subject - a profile, a kingdom - rather than
   one section of it: the title a step up from a section heading. */
.sk-card-header--lead :where(h1, h2, h3, h4, h5, h6) {
  font-size: var(--sk-text-2xl);
  letter-spacing: 1px;
}

/* Cards do not nest. A control that renders its own sk-card -- SOK does, and
   has to, because it also stands alone on Probe and ProbeHistory -- must not
   paint a second glass pane, a second border and a second set of padding when
   a page drops it inside a card of its own. Flattened rather than forbidden,
   so the control needs no knowledge of where it was placed.

   sk-card-inner is the deliberate way to recess something inside a card, and
   it is untouched by this. */
/* OPT-IN, via the host. The first version of this matched .sk-card .sk-card and
   flattened every nested card everywhere -- including the SOK inside
   ProbeHistory's entry cards, which is nested but is NOT being presented as one
   panel with its host and lost its surface entirely. A page that means to
   absorb a control's card says so; every other nesting keeps its own. */
.sk-card--host .sk-card {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin: 0;
  box-shadow: none;
}

/* The absorbed card's header rule goes too. The host has already drawn one
   under its own title, so two hairlines landed a few lines apart and read as a
   double border rather than as two sections. */
.sk-card--host .sk-card .sk-card-header {
  border-bottom: 0;
  padding-bottom: 0;
  margin-bottom: var(--sk-space-3);
}

/* A card that fills the height of its column instead of stopping at its
   content. For a two-column layout where one side is a single card and the
   other is a stack of several: the tall side sets the height and the short one
   ends halfway down, leaving the split looking unfinished.

   Desktop only. Below the split's own breakpoint the columns are stacked and
   there is no neighbour to match, so stretching would only add dead space.

   The height comes from the flex chain above -- sk-split stretches its
   children and sk-stack is a flex column -- so this only has to claim the
   slack. Nothing is asked of the card's contents, which is what lets a shared
   control keep its own markup. */
@media (min-width: 768.02px) {
  .sk-card--fill {
    flex: 1;
  }
}

/* A card that is a flex column, for a card with one child that should take
   the height the card has spare - sk-verdict on Attack's Summary, whose card
   the split stretches to the Deploy card's height. Pair with the child's own
   flex: 1.

   In a block card the header's margin-bottom and the next child's margin-top
   collapse into one; in a flex column they add up. Zeroing the second keeps
   the gap under the header what every other card shows. */
.sk-card--column {
  display: flex;
  flex-direction: column;
}

.sk-card--column > .sk-card-header + * {
  margin-top: 0;
}

.sk-card-inner {
  background: var(--sk-surface-inner);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  padding: var(--sk-space-3);
  box-shadow: var(--sk-shadow-inset);
  transition: border-color var(--sk-transition), box-shadow var(--sk-transition);
}

@media (min-width: 768.02px) {
  .sk-card-inner {
    padding: var(--sk-space-4) var(--sk-space-5);
  }
}


/* ---- Kingdom imagery: a planet behind the card, a race badge in its corner --
   One component, two pages: the Status page's Military Overview card (which is
   the own-kingdom SOK's host) and the Profile page's main card. Both get it the
   same way -- KingdomImagery.ImageryClass adds this class and
   KingdomImagery.ImageryVars declares the two urls -- so the look is defined
   once here and the pages only say WHICH kingdom.

   THREE BACKGROUND LAYERS, first is topmost:

     1. a scrim that fades from light at the top to nearly solid page colour
     2. the planet image, centred and covering
     3. (the element's own background-color, untouched)

   with the race badge above them as an element - .sk-kd-imagery-race below.

   The scrim is the only reason this is legible. backdrop-filter cannot help:
   it filters what is BEHIND an element, and these images are ON it, so the
   card's --sk-blur leaves them at full brightness. The gradient runs 40% to
   92% of --sk-surface-page down the card, which is chosen around what sits
   there: the identity band is at the top and gets the planet nearly clear,
   while the dense report below it lands on what is effectively the page
   colour. Ice Giant and Helium are the bright ones to check any change
   against.

   The race badge sits ABOVE the scrim on purpose. It lands in the darkest part
   of the gradient, and a badge faded into that is a smudge rather than a mark.

   The race badge HUGS THE BOTTOM EDGE of the padding box, so its feet sit on
   the card's inner border edge rather than being lifted off it by the card's
   own padding. The corner radius clips a sliver of it, which is what makes it
   read as standing ON the card rather than floating inside it.

   var(..., none) on the url is load-bearing. A custom property that is not
   set makes the whole background-image declaration invalid at computed-value
   time -- not just its own layer -- so without the fallback a card with the
   class but no declared planet would lose the scrim too. */
.sk-kd-imagery {
  /* The containing block for .sk-kd-imagery-race, and its own stacking
     context so that layer's z-index: -1 lands ABOVE this card's background
     and BELOW its content. backdrop-filter already makes one on both cards;
     isolation says so on purpose rather than by accident. */
  position: relative;
  isolation: isolate;
  background-image:
    linear-gradient(180deg,
      color-mix(in srgb, var(--sk-surface-page) 40%, transparent) 0%,
      color-mix(in srgb, var(--sk-surface-page) 78%, transparent) 42%,
      color-mix(in srgb, var(--sk-surface-page) 92%, transparent) 100%),
    var(--sk-planet-img, none);
  background-position: center, center;
  background-size: cover, cover;
  background-repeat: no-repeat;
}

/* ---- The race badge: an image, or a video that plays and becomes the image --
   It was the top background layer; it is an element now because it can be a
   video, and a background cannot. Every measurement below reproduces what
   that layer did, so the image lands on exactly the same pixels:

     the LAYER is the card's padding box (inset: 0 on a positioned card is the
     padding box, which is where background-origin put it) with the card's
     corner radius, clipping what spills - the corner still takes its sliver.
     The overflow is here, not on the card, so nothing else in the card starts
     clipping.

     the MEDIA is right: 0; bottom: 0; height 420px (240px on a phone), width
     from its own ratio - "auto 420px" as a background-size. max-width: none
     because a background was never capped at the card's width either.

   z-index: -1 inside the card's isolated stacking context: above the scrim
   and the planet, below every line of text - the order it had as a layer.
   pointer-events: none so it can never take a click meant for the report.

   The video and the image are the same size box, so the swap in
   kd-race-video.js moves nothing.

   --waiting: the video is IN the layout but not seen until it is actually
   playing. It has to occupy its box, because that box is what the
   IntersectionObserver in kd-race-video.js watches to know it has scrolled
   into view - sk-is-hidden (display: none) would give it no box at all. And
   it must not show the poster meanwhile, or the viewer sees the image, then a
   blink, then the video. opacity, not visibility, so the fade-in on reveal
   has something to animate. */
.sk-kd-imagery-race {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}

.sk-kd-imagery-race-media {
  position: absolute;
  right: 0;
  bottom: 0;
  display: block;
  width: auto;
  max-width: none;
  height: 420px;
  transition: opacity var(--sk-transition);
}

.sk-kd-imagery-race-media--waiting {
  opacity: 0;
}

/* Smaller on a phone, where the card is a third of the width it has on the
   desktop split and 420px of badge would be taller than most of the report. */
@media (max-width: 768px) {
  .sk-kd-imagery-race-media {
    height: 240px;
  }
}


/* --------------------------------------------------------------------------
   4. BUTTON
   -------------------------------------------------------------------------- */

.sk-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sk-space-1);
  box-sizing: border-box;
  /* Defensive: base CSS sets min-width on some buttons in this app. */
  min-width: 0;
  width: auto;
  padding: var(--sk-space-2) var(--sk-space-3);
  background: var(--sk-wash-accent);
  border: 1px solid var(--sk-border-accent);
  border-radius: var(--sk-radius-sm);
  color: var(--sk-text-on-accent);
  font-family: var(--sk-font);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-semibold);
  line-height: 1.4;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  /* filter and box-shadow are in the list on purpose: the CTA animates with
     those, and leaving them out is what made its hover snap instead of ease. */
  transition:
    background-color var(--sk-transition),
    border-color var(--sk-transition),
    color var(--sk-transition),
    box-shadow var(--sk-transition),
    filter var(--sk-transition),
    transform var(--sk-transition);
}

.sk-btn:hover {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, transparent);
  border-color: var(--sk-border-strong);
  color: var(--sk-text-on-accent);
  text-decoration: none;
  transform: translateY(-1px);
}

/* :focus-visible, not :focus — a mouse click should not paint a focus ring. */
.sk-btn:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-btn:disabled,
.sk-btn[disabled],
a.sk-btn.aspNetDisabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* The same idea for a control that is inert only at an EDGE of its range - a
   Move up on the first row of a list, a Move down on the last - rather than
   disabled outright.

   It carries aria-disabled and this class, never the `disabled` attribute, for
   the reason spelled out under "A <button> in this app must not carry
   `disabled`": general.css:693 paints a disabled button black-on-white with an
   !important nothing can beat. pointer-events also stops the hover lift from
   promising an action that is a no-op.

   :disabled stays for the genuinely form-significant case; this is the state
   version, matching what sk-stepper already does on its wrapper. */
.sk-btn.is-inert {
  opacity: 0.5;
  pointer-events: none;
  transform: none;
}

/* The tooltip host for a control that can go inert. pointer-events: none
   above means an inert button never sees the hover, so a tooltip on the button
   itself vanishes exactly when it has the most to say ("why can't I press
   this?"). The tooltip goes on this wrapper instead, which keeps its pointer
   events; Bootstrap's focusin trigger bubbles, so keyboard focus on the button
   inside still opens it. inline-flex, so the wrapper is the button's box and
   adds no line-height around it. Attack's Load latest probe is the first. */
.sk-tip-host {
  display: inline-flex;
}

/* Rule 1, assume hostile base styles — the case sk-btn missed.

   _typography.css:31 sets `a:link, a.nav-link { color: #447CFF }`. That is
   (0,1,1); `.sk-btn`'s own `color` declaration is (0,1,0) and loses to it. So
   every button built as an <a> rather than a <button> or <input type=submit>
   rendered its label in accent blue instead of --sk-text-on-accent: readable
   enough on the plain sk-btn wash, and near-invisible on sk-btn--cta, which
   fills with --sk-grad-accent and expects white on top of it.

   :link and :visited are both named because _typography.css:37 sets the same
   colour on :visited, and a bare `a.sk-btn` at (0,1,1) would only tie with it —
   losing on load order for any button the player has already clicked, which is
   how this survives a casual look at a fresh page.

   Not fixed by editing _typography.css: that is the old design's base layer and
   is off-limits, and every non-button link in the app still wants that colour. */
a.sk-btn,
a.sk-btn:link,
a.sk-btn:visited {
  color: var(--sk-text-on-accent);
}

a.sk-btn:hover {
  color: var(--sk-text-on-accent);
}

.sk-btn--danger {
  background: var(--sk-wash-danger);
  border-color: var(--sk-danger-border);
}

.sk-btn--danger:hover {
  background: var(--sk-wash-danger-strong);
  border-color: var(--sk-danger);
}

/* A danger button standing on the starfield.

   These two modifiers conflict: --danger paints --sk-wash-danger, --on-image
   paints --sk-surface-on-image, both at (0,1,0), and --on-image is declared
   later - so it wins and the button comes out grey. Combining them without a
   rule of their own reads as "the danger modifier silently did nothing".

   (0,2,0) so it beats either alone regardless of which is written first in the
   class attribute - source order decides in CSS, not markup order.

   Near-opaque like --on-image, because the whole point of that modifier is a
   button that survives a bright nebula behind it, but unmistakably red. */
.sk-btn--on-image.sk-btn--danger {
  background: color-mix(in srgb, var(--sk-alert-deep) 55%, var(--sk-surface-on-image));
  border-color: var(--sk-alert-deep);
  color: var(--sk-text);
}

.sk-btn--on-image.sk-btn--danger:hover {
  background: color-mix(in srgb, var(--sk-alert-deep) 75%, var(--sk-surface-on-image));
  border-color: var(--sk-alert-deep);
}

.sk-btn--success {
  background: var(--sk-wash-success);
  border-color: var(--sk-success-border);
}

.sk-btn--success:hover {
  background: var(--sk-wash-success-strong);
  border-color: var(--sk-success);
}

/* Smaller icon button, for one sitting inside a row of text rather than beside
   a heading: a 32px circle next to 13px copy is a control that outranks the
   line it belongs to. Same shape, two sizes, chosen per placement. */
.sk-btn--icon-sm {
  width: 26px;
  height: 26px;
}

.sk-btn--icon-sm :where(i) {
  font-size: var(--sk-text-min);
}

/* A button with nothing behind it — sitting on the starfield rather than on a
   card. sk-btn's fill is --sk-wash-accent, 0.16 alpha, which reads as a button
   against a card and as a faint blue smudge over a bright nebula. Same
   near-opaque surface sk-pill uses out there, for the same reason, plus a
   shadow so the button has an edge wherever the image is light.

   The text colour moves too: --sk-text-on-accent is white for a filled button,
   and this one is not filled. */
.sk-btn--on-image {
  background: var(--sk-surface-on-image);
  border-color: var(--sk-border-strong);
  color: var(--sk-text);
  box-shadow: var(--sk-shadow);
}

.sk-btn--on-image:hover {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, var(--sk-surface-on-image));
  border-color: var(--sk-hue, var(--sk-accent));
  color: var(--sk-text);
}

/* The a:link defence again, for the one variant that does not want
   --sk-text-on-accent. `a.sk-btn:link` up at the base component is (0,2,1) and
   would otherwise repaint this variant white; these selectors tie at (0,2,1)
   and win on order, which is why they live here rather than beside it. */
a.sk-btn--on-image,
a.sk-btn--on-image:link,
a.sk-btn--on-image:visited,
a.sk-btn--on-image:hover {
  color: var(--sk-text);
}

/* Icon-only, and a circle: an (i) is a marker you press, and a round target
   reads as one at a glance next to a square heading. The width was set but the
   height was left to the content, so the box also came out wider than it was
   tall.

   The icon is deliberately smaller than the glyph sizes around it. An icon-only
   button is a target you press, not a symbol to admire, and sk-card-header sets
   bare <i> at --sk-text-xl — a help button placed in a header inherited that and
   filled its own box. Same specificity (0,1,0), declared later, so this wins. */
.sk-btn--icon {
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: var(--sk-radius-sm);
  /* For the inline case - a copy button sitting at the end of a line of text.
     An inline-flex box aligns on its baseline by default, which drops a 32px
     circle below 13px text and pushes the line height out. Ignored when the
     button is a flex or grid item, so this costs the common case nothing. */
  vertical-align: middle;
  /* Never squeezed out of round. The defensive layer below gives it
     min-width: 0, so as a flex item it could shrink below 32px: next to a
     full-width sk-disclosure in a header's sk-card-actions (Attack's Defender
     Intelligence, with Load latest SOK) it came out narrower than tall - an
     oval. A circle is a fixed size; the title beside it gives way instead. */
  flex-shrink: 0;
}

/* A figure in an icon button's glyph slot - a count the button is waiting on
   (Modules' Reset on cooldown shows the ticks left instead of its arrow). Small
   and bold so three digits fit the 32px circle; tabular so it does not jitter
   as it counts down. */
.sk-btn-count {
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.sk-btn--icon :where(i) {
  margin: 0;
  font-size: var(--sk-text-xs);
}

/* Opt-in fixed width, for button rows that should line up. The old .pref-btn
   forced width:120px on every button, truncating longer labels. */
.sk-btn--fixed {
  width: 120px;
}


/* --------------------------------------------------------------------------
   4b. BUTTON — DEFENSIVE LAYER
   --------------------------------------------------------------------------
   <asp:Button> renders <input type="submit">, and both base stylesheets style
   that element at (0,2,1) — element + attribute + the class inside :not():

     general.css:621   input[type="submit"]:not(.danger-btn), button, .button
                       -> width:auto, min-width:160px, border-radius:0,
                          padding:6px 10px, background-color, box-shadow
     general.css:701   input[type="submit"]:hover, button:hover
                       -> background-color:#000
     _main.css:7       input[type="submit"]:not(.table-col-submit)

   A bare .sk-btn is (0,1,0) and .sk-btn:hover only (0,2,0), so every one of
   those won. The visible result on the first migrated page: sk-btn--full did
   nothing at all (width:auto held), the CTA carried a 160px floor and square
   corners, and it went black on hover.

   Qualifying element + attribute + class reaches (0,2,1), and Auth.Master loads
   this file after both base sheets, so the tie breaks our way.

   Note <input> cannot render ::after, so an <asp:Button> CTA has no sheen. That
   is decoration; everything structural is restated here.
   -------------------------------------------------------------------------- */

input[type="submit"].sk-btn,
input[type="button"].sk-btn,
button.sk-btn {
  width: auto;
  min-width: 0;
  padding: var(--sk-space-2) var(--sk-space-3);
  border: 1px solid var(--sk-border-accent);
  border-radius: var(--sk-radius-sm);
  background: var(--sk-wash-accent);
  box-shadow: none;
}

input[type="submit"].sk-btn:hover,
input[type="button"].sk-btn:hover,
button.sk-btn:hover {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, transparent);
  border-color: var(--sk-border-strong);
}

input[type="submit"].sk-btn--cta,
input[type="button"].sk-btn--cta,
button.sk-btn--cta {
  padding: 10px 28px;
  border: none;
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent));
}

input[type="submit"].sk-btn--cta:hover,
input[type="button"].sk-btn--cta:hover,
button.sk-btn--cta:hover {
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  filter: brightness(1.06);
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent)), 0 6px 20px rgba(0, 0, 0, 0.35);
}

/* The defensive layer has to defend against ITSELF here.
   `button.sk-btn` above is (0,1,1) and `.sk-btn--icon` is only (0,1,0), so the
   modifier lost its 35px width and its tight padding to the very rule that was
   written to protect it — a real <button> with class="sk-btn sk-btn--icon" came
   out as a wide button with an icon rattling around inside it.

   So the modifier is restated qualified, and it must sit AFTER the base block:
   at equal specificity the later rule wins, which is the whole mechanism this
   section relies on. */
input[type="submit"].sk-btn--icon,
input[type="button"].sk-btn--icon,
button.sk-btn--icon {
  width: 32px;
  height: 32px;
  min-width: 0;
  padding: 0;
  /* border-radius too: button.sk-btn sets --sk-radius-sm at (0,1,1), which would
     otherwise square the circle straight back off. */
  border-radius: 50%;
}

/* And the size modifier needs the same treatment again, plus a place AFTER the
   --icon block above: both selectors are (0,1,1), so this one wins on order
   alone. Move it up and an icon button silently goes back to 32px. */
input[type="submit"].sk-btn--icon-sm,
input[type="button"].sk-btn--icon-sm,
button.sk-btn--icon-sm {
  width: 26px;
  height: 26px;
}

/* The smallest text button: one that shares a table cell with an sk-input--cell
   (Attack's War leaders "Max", before its field). Everything in em off the
   cell's own font, the field's vertical padding and line-height, so the two are
   one height and scale together with the phone table's smaller type - the row
   keeps the height of the rows around it. Element-qualified and after the
   (0,1,1) restatement above, as sk-btn--icon-sm. */
.sk-btn--cell,
button.sk-btn--cell {
  padding: 0.35em 0.6em;
  font-size: inherit;
  line-height: normal;
}

/* The Settings trigger: the button that opens a page's settings drawer, the same on every
   page. Its resting purple tint is the border/background/text formula sk-badge uses for a
   hue-tinted chip, at every width - the word and the gear are one button, and it opens a
   premium feature. Pair with a hue class (sk-hue-plasma):

     <button type="button" class="sk-btn sk-btn--settings sk-hue-plasma"
             data-sk-drawer-open="buildingsColumns" title="Buildings settings">
       <i class="fas fa-cog" aria-hidden="true"></i> Settings
     </button>

   Two classes, (0,2,0), so it beats button.sk-btn's own (0,1,1) restatement without !important.
   Moved here from buildings-page.css (.buildings-page button.buildings-columns-trigger), where
   only Buildings had it - Military's own trigger looked like a different button for that reason. */
.sk-btn.sk-btn--settings {
  border-color: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 55%, transparent);
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 18%, transparent);
  color: var(--sk-hue, var(--sk-accent));
}

/* A button that wears its hue at rest, not only on hover, for a row of actions
   where the colour IS the meaning. Alliance Control's relation buttons are the
   case: War red, NAP cyan, Barricade green, Ally gold, the same hue as the
   relation's card stripe - on a phone that colour is the safeguard against
   tapping Declare War where Send NAP was meant. Resting tint is the same
   formula as sk-btn--settings above; hover deepens it, where the base
   sk-btn:hover would swap in a neutral border and label. Pair with a hue class:

     <a class="sk-btn sk-btn--hued sk-hue-alert" href="...">Declare War</a>

   The a:link / a:visited selectors exist because a LinkButton renders <a href>
   and a.sk-btn:link (0,2,1) would repaint the label --sk-text-on-accent. A
   disabled LinkButton (a relation on cooldown) has no href, keeps its hue and
   takes a.sk-btn.aspNetDisabled's shared fade - the countdown in its label
   says why. */
.sk-btn.sk-btn--hued,
a.sk-btn.sk-btn--hued:link,
a.sk-btn.sk-btn--hued:visited {
  border-color: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 55%, transparent);
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 18%, transparent);
  color: var(--sk-hue, var(--sk-accent));
}

.sk-btn.sk-btn--hued:hover,
a.sk-btn.sk-btn--hued:hover {
  border-color: var(--sk-hue-deep, var(--sk-accent));
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 30%, transparent);
  color: var(--sk-hue, var(--sk-accent));
}

/* A worded button that becomes an icon button under 768px - the button
   counterpart of sk-badge--icon-mobile. The word on a desktop, the glyph alone
   in a phone's crowded section head. The word stays in the accessibility tree
   at every width; still give the button a title for the glyph's hover.

     <button type="button" class="sk-btn sk-btn--icon-mobile" title="Table settings">
       <i class="fas fa-cog"></i><span class="sk-btn-text">Settings</span>
     </button>

   The phone rules are qualified with the element and come AFTER
   button.sk-btn--icon-sm above: button.sk-btn is restated at (0,1,1) in §4b,
   and a bare class would lose its padding and size to it. */
.sk-btn--icon-mobile > :where(i) {
  display: none;
}

@media (max-width: 768px) {
  .sk-btn--icon-mobile,
  button.sk-btn--icon-mobile,
  /* A CTA in a card header gets 12px sides on a phone at (0,2,1) - beat it
     here (declared later), or the glyph sits off-centre in the circle. */
  .sk-card-actions > button.sk-btn--icon-mobile,
  .sk-card-actions > a.sk-btn--icon-mobile {
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 50%;
  }

  .sk-btn--icon-mobile > :where(i) {
    display: inline;
  }

  .sk-btn--icon-mobile > .sk-btn-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* sk-btn--keep-icon: an sk-btn--icon-mobile button that keeps its glyph beside
   the word on a desktop too (Attack's Simulate) - only the word goes on a phone. */
.sk-btn--icon-mobile.sk-btn--keep-icon > :where(i) {
  display: inline;
}

/* The opposite of sk-btn--icon-mobile: an icon button on a desktop that shows
   its label as well on a phone, where a bare arrow beside two selects says too
   little. Markup: sk-btn sk-btn--icon sk-btn--label-mobile, the label in an
   sk-btn-text span before the icon. The kingdom chooser's "Change". */
@media (min-width: 769px) {
  .sk-btn--label-mobile > .sk-btn-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* (0,2,1): clears button.sk-btn--icon's 32px circle, (0,1,1), on a phone. */
@media (max-width: 768px) {
  button.sk-btn--icon.sk-btn--label-mobile {
    width: auto;
    padding: 0 var(--sk-space-3);
    border-radius: var(--sk-radius-sm);
  }
}

/* Same trap, same fix: button.sk-btn (0,1,1) sets the wash background and would
   beat .sk-btn--on-image (0,1,0), leaving the button translucent on the very
   backdrop the modifier exists for. */
input[type="submit"].sk-btn--on-image,
input[type="button"].sk-btn--on-image,
a.sk-btn--on-image,
button.sk-btn--on-image {
  background: var(--sk-surface-on-image);
  border-color: var(--sk-border-strong);
  color: var(--sk-text);
  box-shadow: var(--sk-shadow);
}

input[type="submit"].sk-btn--on-image:hover,
input[type="button"].sk-btn--on-image:hover,
a.sk-btn--on-image:hover,
button.sk-btn--on-image:hover {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, var(--sk-surface-on-image));
  border-color: var(--sk-hue, var(--sk-accent));
  color: var(--sk-text);
}

/* Same trap, the two status modifiers. button.sk-btn above is (0,1,1) and
   paints the accent wash; .sk-btn--success / --danger are (0,1,0), so on a real
   <button> both came out as a plain accent button. LinkButtons render <a> and
   never met it, which is why Research's danger button looked right.
   .sk-btn--on-image.sk-btn--danger is (0,2,0) and still wins over these. */
input[type="submit"].sk-btn--success,
input[type="button"].sk-btn--success,
button.sk-btn--success {
  background: var(--sk-wash-success);
  border-color: var(--sk-success-border);
}

input[type="submit"].sk-btn--success:hover,
input[type="button"].sk-btn--success:hover,
button.sk-btn--success:hover {
  background: var(--sk-wash-success-strong);
  border-color: var(--sk-success);
}

input[type="submit"].sk-btn--danger,
input[type="button"].sk-btn--danger,
button.sk-btn--danger {
  background: var(--sk-wash-danger);
  border-color: var(--sk-danger-border);
}

input[type="submit"].sk-btn--danger:hover,
input[type="button"].sk-btn--danger:hover,
button.sk-btn--danger:hover {
  background: var(--sk-wash-danger-strong);
  border-color: var(--sk-danger);
}

input[type="submit"].sk-btn--full,
input[type="button"].sk-btn--full,
button.sk-btn--full {
  width: 100%;
}


/* --------------------------------------------------------------------------
   5. TABS
   -------------------------------------------------------------------------- */

.sk-tabs {
  /* The containing block for .sk-tab-actions (Modules), which anchors to
     this element's own top-right corner. No offsets are set here, so this
     has no visual effect on any other page that shares .sk-tabs. */
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-2);
  /* Owns its vertical rhythm at both ends. It previously set only a bottom
     margin, so a tab row directly under a header or a row of pills sat flush
     against it. Adjacent margins collapse, so this cannot double up. */
  margin-top: var(--sk-space-3);
  /* Near-opaque, with an accent-tinted edge. The row sits on the starfield,
     where a 0.55-alpha surface reads as a smudge rather than as a bar - the
     same reason sk-pill and sk-btn--on-image use this surface. Ported from
     .pref-tabs-wrapper, which this now matches. */
  background: var(--sk-surface-on-image);
  border: 1px solid var(--sk-border-accent);
  border-radius: var(--sk-radius);
  padding: var(--sk-space-2);
  margin-bottom: var(--sk-space-3);
  scrollbar-width: none;
}

@media (min-width: 768.02px) {
  .sk-tabs {
    margin-block: var(--sk-space-5);
  }
}

.sk-tabs::-webkit-scrollbar {
  display: none;
}

/* The resting tab carries a visible border. In the old system only the active
   tab had one (resting measured 1.17:1), so the row read as loose text rather
   than a set of controls.

   DEFENSIVE LAYER. Every .sk-tab renders on a real <button> (see AFKBuilder,
   UniversalNews, ModulesController, Buildings, Research, AllianceLoadoutController),
   and _main.css:7 styles that element at (0,1,1):

     _main.css:7   input[type="submit"]:not(.table-col-submit), button:not(.table-col-submit)
                   -> background-color, color, border, padding, min-width:160px,
                      box-sizing, font-weight, outline-color, transition

   A bare .sk-tab is (0,1,0) — one tier short — so EVERY property above won
   outright, on every page that loads _main.css, on every tab this component
   has ever rendered. It went unnoticed because the two rules' box properties
   overlap almost completely (both set a dark-ish background, a border, similar
   padding), so the tab still looked roughly like a tab; min-width:160px was
   the one property with no visual sibling to hide behind, and forcing six
   tabs onto one row on AFK Builder's Building-heavy layout is what finally
   made it visible. This is the same fight §4b already fixed for .sk-btn
   against general.css — qualifying element + class reaches (0,1,1) and ties;
   Auth.Master loads this file after _main.css (line 35 vs line 49), so the
   tie breaks in this component's favour. */
button.sk-tab {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sk-space-2);
  box-sizing: border-box;
  padding: var(--sk-space-2) var(--sk-space-3);
  /* A dark recessed chip, not a raised button. Ported from .tab-btn on Modules,
     which reads better than the sk-btn geometry this used to borrow: against the
     near-opaque wrapper above, a RECESSED tab makes the one lit tab obviously
     current, where a row of raised buttons left the active state competing with
     five others of the same weight. */
  background: var(--sk-surface-table);
  /* One thing is deliberately NOT ported. .tab-btn rests on a
     rgba(255,255,255,0.05) border — measured at 1.17:1, and one of the specific
     non-text contrast failures this system exists to fix, which is why the
     component mapping records "Visible resting border" against it.
     --sk-border-subtle is the closest token by appearance and is stamped
     DECORATIVE ONLY — never a control edge — so this stays --sk-border at
     4.07:1. It is the one visible difference from Modules, and it is deliberate. */
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius-sm);
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  font-weight: var(--sk-weight-medium);
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  outline: none;
  transition:
    background-color var(--sk-transition),
    border-color var(--sk-transition),
    color var(--sk-transition),
    box-shadow var(--sk-transition),
    transform var(--sk-transition);
}

/* The icon shifts with the tab's state rather than sitting at one flat
   colour. This is what separates the row from a set of grey buttons — the
   old .tab-btn had no icon rule at all and read as text with a border. */
.sk-tab > :where(i) {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  transition: color var(--sk-transition);
}

/* No lift. A recessed chip that rises on hover fights the metaphor, and
   Modules does not do it either - it tints toward the accent instead. */
.sk-tab:hover {
  background: var(--sk-wash-accent);
  border-color: var(--sk-border-accent);
  color: var(--sk-text-on-accent);
}

.sk-tab:hover > :where(i) {
  color: var(--sk-accent);
}

.sk-tab:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

/* Two on a row on small screens, rather than four squeezed into 375px; a lone
   last tab grows to the full row. Qualified to match button.sk-tab above: as a
   bare .sk-tab this is (0,1,0) and the base rule's flex:1/min-width:0 beat it,
   cramming every tab onto one line. */
@media (max-width: 768px) {
  button.sk-tab {
    flex: 1 1 calc(50% - var(--sk-space-2));
    min-width: calc(50% - var(--sk-space-2));
    padding: var(--sk-space-2);
  }
}

.sk-tab.is-active {
  background: linear-gradient(135deg,
      color-mix(in srgb, var(--sk-accent) 45%, transparent) 0%,
      color-mix(in srgb, var(--sk-pal-blue-800) 55%, transparent) 100%);
  border-color: var(--sk-accent);
  color: var(--sk-text-on-accent);
  font-weight: var(--sk-weight-semibold);
  box-shadow: 0 0 10px color-mix(in srgb, var(--sk-accent) 30%, transparent);
}

.sk-tab.is-active > :where(i) {
  color: var(--sk-text-on-accent);
}

/* A destructive tab (Buildings' Raze): red at rest and red when current, so
   the half of a drawer that destroys things never looks like its neighbour.
   Same reservation as sk-emblem--armed - red means destructive, nothing else. */
.sk-tab--danger,
.sk-tab--danger > :where(i) {
  color: var(--sk-alert);
}

.sk-tab--danger {
  border-color: var(--sk-danger-border);
}

.sk-tab--danger:hover {
  background: var(--sk-wash-danger);
  border-color: var(--sk-danger);
  color: var(--sk-text-on-accent);
}

.sk-tab--danger:hover > :where(i) {
  color: var(--sk-alert);
}

.sk-tab--danger.is-active {
  background: linear-gradient(135deg,
      color-mix(in srgb, var(--sk-alert-deep) 55%, transparent) 0%,
      color-mix(in srgb, var(--sk-pal-red-900) 65%, transparent) 100%);
  border-color: var(--sk-alert-deep);
  color: var(--sk-text-on-accent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--sk-alert-deep) 40%, transparent);
}

.sk-tab--danger.is-active > :where(i) {
  color: var(--sk-text-on-accent);
}

/* A tab whose feature is switched off (AFK Builder: the Explore tab while AFK
   exploring is off). Still a working tab - its settings can be changed before
   the feature is switched on - so it only reads quieter: muted text, the glyph in
   the danger hue. On the active tab the active look wins, with the red glyph kept. */
.sk-tab.is-off {
  color: var(--sk-text-muted);
  border-style: dashed;
}

.sk-tab.is-off > :where(i) {
  color: var(--sk-danger);
}


/* Extra clearance above the tab row so the flag (translateY(-100%), i.e.
   fully above the row) has room to sit without colliding with whatever is
   above it on the page. A named modifier rather than :has(), applied only
   to the tab row that actually carries the flag (ModulesController.ascx);
   every other .sk-tabs keeps its normal corner and margin-top. */
.sk-tabs--with-actions {
  margin-top: var(--sk-space-7);
  border-top-right-radius: 0;
}

/* Holds two real sk-btn buttons (Reset, Help) rather than a custom link
   style - "make them look like buttons" - so border/radius/hover/disabled
   all come from the button component for free. right: 0 keeps it flush
   with .sk-tabs's own right edge, matching the flattened corner above.
   Gap/padding step down on a phone; align-items: stretch (the flex
   default, stated for clarity) is what lets Reset define the row's
   height and Help match it (see its own calc() below). */
.sk-tab-actions {
  position: absolute;
  top: 0;
  right: 0;
  transform: translateY(-100%);
  display: flex;
  align-items: stretch;
  gap: var(--sk-space-2);
  padding: var(--sk-space-2);
  background: var(--sk-surface-on-image-solid);
  border: 1px solid var(--sk-border-accent);
  border-bottom: 0;
  border-radius: var(--sk-radius) var(--sk-radius) 0 0;
}

@media (max-width: 768px) {
  .sk-tab-actions {
    gap: var(--sk-space-1);
    padding: var(--sk-space-1);
  }
}

/* aspect-ratio + flex stretch (width/height: auto) does not reliably derive
   the main size from a stretched cross size across browsers - it collapsed
   to the icon's own content width instead of Reset's height. Computed
   explicitly instead: this is .sk-btn's own auto-height formula (line-height
   x font-size, plus its vertical padding and border), spelled out in the
   same tokens Reset's height comes from, so both land on the same value by
   construction rather than one measured figure copied onto the other. */
.sk-tab-actions .sk-btn--icon-sm {
  width: calc(1.4 * var(--sk-text-xs) + 2 * var(--sk-space-2) + 2px);
  height: calc(1.4 * var(--sk-text-xs) + 2 * var(--sk-space-2) + 2px);
}

/* Reset takes whatever room Help's fixed square doesn't need, rather than
   both sizing to content and leaving the flag's own width undecided. */
.sk-tab-actions .sk-btn--danger {
  flex-grow: 1;
}


/* --------------------------------------------------------------------------
   6. FORM CONTROLS
   --------------------------------------------------------------------------
   Named sk-input / sk-select rather than reusing the old form-control-col
   family, whose names sat one hyphen from Bootstrap's live .form-control and
   .form-group (61 and 74 uses in this codebase). */

.sk-input,
.sk-select {
  box-sizing: border-box;
  width: 100%;
  max-width: 240px;
  /* Never let a flex row crush the field. `width: 100%` becomes the flex-basis,
     and flex-shrink then squeezes it to whatever its siblings leave — on Mine
     Gold a MAX button and a checkbox reduced a 6-digit input to ~100px, cutting
     the number off. A floor is the fix; it belongs on the component because
     every page that puts a field beside a button hits this. */
  min-width: 160px;
  /* general.css:446 sets a bare `select { height: 35px }`. That is only
     (0,0,1) so a class beats it, but only if the class actually declares
     height — otherwise the fixed 35px combines with our 8px vertical padding
     and squeezes the text box to ~17px, which drops the label toward the
     bottom of the control and clips its descenders. */
  height: auto;
  padding: var(--sk-space-2) var(--sk-space-3);
  background-color: var(--sk-surface-field);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius-sm);
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  transition: border-color var(--sk-transition), box-shadow var(--sk-transition);
}

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

.sk-input:focus,
.sk-select:focus {
  border-color: var(--sk-accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sk-accent) 35%, transparent);
  outline: none;
}

/* Browser autofill. Chrome paints an autofilled field with its own light
   background, which no background rule can beat, so a saved login turned the
   recessed dark field pale (the login pages, 2026-10-29). An inset shadow as
   big as the field paints over it in an opaque field colour, and the text
   fill keeps --sk-text. box-shadow is one property, so the focus ring is
   restated for an autofilled field that has focus. Firefox tints autofill
   with a filter instead; :autofill gets its own rule because a selector list
   with a pseudo-class the browser does not know is dropped whole. */
input.sk-input:-webkit-autofill,
input.sk-input:-webkit-autofill:hover {
  -webkit-text-fill-color: var(--sk-text);
  caret-color: var(--sk-text);
  box-shadow: 0 0 0 1000px var(--sk-surface-on-image-solid) inset;
}

input.sk-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--sk-text);
  box-shadow:
    0 0 0 1000px var(--sk-surface-on-image-solid) inset,
    0 0 0 2px color-mix(in srgb, var(--sk-accent) 35%, transparent);
}

input.sk-input:autofill {
  filter: none;
}

.sk-input::placeholder,
.sk-select::placeholder {
  color: var(--sk-text-muted);
  opacity: 1;
}

/* A field that holds a number (above 0): a slight accent border and tint, so
   the filled ones stand out in a long grid of zeros (Attack's Defender
   Intelligence and Deploy Personnel). Hover and focus still win.
   Two classes: input.sk-input (0,1,1, the base sheet's fight) out-ranked a
   bare .sk-input--filled, and the tint never showed. */
.sk-input.sk-input--filled {
  border-color: color-mix(in srgb, var(--sk-accent) 45%, var(--sk-border));
  background-color: color-mix(in srgb, var(--sk-accent) 7%, var(--sk-surface-field));
}

.sk-input.sk-input--filled:hover {
  border-color: var(--sk-border-strong);
}

.sk-input.sk-input--filled:focus {
  border-color: var(--sk-accent);
}

@media (max-width: 768px) {
  .sk-input,
  .sk-select {
    max-width: 100%;
  }
}

/* Defensive reset — and the one case where :where() is the WRONG tool.
   sk-table beats _table.css's bare `td` (0,0,1) with `.sk-table :where(th, td)`
   (0,1,0). That works because the hostile selector is a bare element. Here the
   hostile selectors are attribute selectors at (0,1,1):

     general.css:543  input[type=number|text|password], textarea
                      -> background: rgba(255,255,255,1); color: black
     general.css:603  input[type="number"]
                      -> width: 80px; text-align: center

   A bare `.sk-input` is (0,1,0) and loses to both, so the field rendered white
   with black text at 80px wide. Qualifying with the element name raises us to
   (0,1,1) — a tie — and Auth.Master loads this file after general.css, so the
   tie breaks our way. Do not "simplify" these selectors back to bare classes.

   The width also explains why a min-width floor alone was not the fix: the
   field was not being squeezed by flex, it was being pinned at 80px. */
input.sk-input,
textarea.sk-input,
select.sk-select {
  width: 100%;
  background-color: var(--sk-surface-field);
  border-color: var(--sk-border);
  color: var(--sk-text);
  text-align: left;
}

/* Sized for a small number - a count, a page size - rather than for content.
   sk-input's 160px floor is right for a figure a player types into a form; it
   is absurd on a two-digit count with a button beside it. Qualified for the
   same reason every field rule in this file is: input.sk-input sets width:100%
   at (0,1,1), and general.css sets input[type="number"] { width: 80px } at the
   same weight - this file loads after both. */
input.sk-input--narrow,
.sk-input--narrow {
  width: 88px;
  min-width: 0;
  max-width: 88px;
}

/* type="number"'s native up/down arrows eat width for nothing on a field
   this narrow - there's no room left for the figure once they're drawn.
   Same technique as .sk-stepper-input (section 15), minus the stepper:
   nothing replaces the removed control here, the field is just typed or
   pasted into directly. */
input[type="number"].sk-input--narrow {
  -moz-appearance: textfield;
}

input[type="number"].sk-input--narrow::-webkit-outer-spin-button,
input[type="number"].sk-input--narrow::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* The input for a table cell - a count typed into a row (Attack's Deploy
   column; the Military and Probe tables are the next users). sk-input--narrow
   is 88px, and a px box does not shrink with the phone table's smaller type,
   so the column held its desktop width at 375px. Everything here is em and the
   font is the cell's, so the field scales with the table it sits in.
   Element-qualified for the same reason as sk-input--narrow. */
input.sk-input--cell,
.sk-input--cell {
  /* flex: none - beside a button in a row (War leaders' Max) flex would
     shrink the field below the column's 5.5em, out of line with the rest. */
  flex: none;
  width: 5.5em;
  min-width: 0;
  max-width: 100%;
  padding: 0.35em 0.5em;
  font-size: inherit;
}

/* A field that reads as a figure in a labelled box rather than a blank to fill
   in: the value centred and coloured by the field's hue, the edge in that hue
   at low alpha, a faint wash of it behind. For a short run of percentages or
   modifiers that sit under a block of plain inputs and should read as the
   settings that bend them (Attack's Bonus / Research / Shields is the first
   user; any page's row of modifiers takes the same shape). The hue is an
   sk-hue-* class on
   the input itself - sk-hue-research, sk-hue-defense - and the accent when it
   has none.

   Still a real input: typing, focus and disabled all work, and a disabled one
   (Attack's Bonus, set by the server) keeps full strength, because it is the
   one figure there the player cannot change and still has to read. Safari
   fades a disabled field with opacity and -webkit-text-fill-color, hence both.
   Element-qualified for the same reason as sk-input--narrow: input.sk-input
   sets colour, border and text-align at (0,1,1) and this has to follow it. */
input.sk-input--readout,
.sk-input--readout {
  border-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  background-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 10%, var(--sk-surface-field));
  color: var(--sk-hue, var(--sk-accent));
  font-variant-numeric: tabular-nums;
  font-weight: var(--sk-weight-semibold);
  text-align: center;
}

input.sk-input--readout:hover {
  border-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 75%, transparent);
}

input.sk-input--readout:focus {
  border-color: var(--sk-hue, var(--sk-accent));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 35%, transparent);
}

input.sk-input--readout:disabled {
  border-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  opacity: 1;
  -webkit-text-fill-color: currentColor;
  cursor: default;
}

/* Selects size to their content rather than a fixed cap. */
.sk-select {
  max-width: 100%;
}

/* A select that opens a richer list than a native <select> can hold - Target
   Finder's Planet Type, a checklist in an sk-menu. A <button> carrying sk-select
   (so it is the same field) plus this: value left, chevron right.

       <button type="button" class="sk-select sk-select-trigger"
               data-sk-menu-open="planetMenu" aria-haspopup="true" aria-expanded="false">
         <span>3 selected</span><i class="fas fa-chevron-down" aria-hidden="true"></i>
       </button>

   Element-qualified against general.css's bare `button` (its background, border,
   shadow and 160px min-width); colour and weight on the CHILDREN, because that
   rule sets the button's own with !important (the sk-disclosure approach).
   Hover restated at (0,2,1) against general.css's button:hover. */
button.sk-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sk-space-2);
  background-color: var(--sk-surface-field);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius-sm);
  box-shadow: none;
  text-align: left;
  cursor: pointer;
}

button.sk-select-trigger:hover {
  background-color: var(--sk-surface-field);
  border-color: var(--sk-border-strong);
  box-shadow: none;
}

.sk-select-trigger > span {
  color: var(--sk-text);
  font-weight: 400;
}

.sk-select-trigger > i {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

/* A select that sits beside its label in a sk-row instead of wrapping under it.
   select.sk-select is width: 100%, which as a flex-basis forces the wrap. */
select.sk-select.sk-select--auto {
  width: auto;
}

.sk-input--full {
  max-width: 100%;
}

/* Native file input (Profile Edit's avatar upload). Opt-in; legacy upload
   controls are untouched. The picker button takes the accent look. */
.sk-file-upload {
  display: block;
  max-width: 100%;
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  line-height: 1.5;
}
.sk-file-upload::file-selector-button {
  margin-right: var(--sk-space-3);
  padding: var(--sk-space-2) var(--sk-space-4);
  border: 1px solid var(--sk-border-accent);
  border-radius: var(--sk-radius-sm);
  background: var(--sk-grad-accent);
  color: var(--sk-text-on-accent);
  font: inherit;
  font-weight: var(--sk-weight-semibold);
  cursor: pointer;
  transition: filter var(--sk-transition), border-color var(--sk-transition);
}
.sk-file-upload:hover::file-selector-button { filter: brightness(1.15); }
.sk-file-upload:focus-visible { outline: 2px solid var(--sk-border-strong); outline-offset: 3px; }
.sk-file-upload:disabled { opacity: 0.6; cursor: not-allowed; }
.sk-file-upload:disabled::file-selector-button { cursor: not-allowed; }
@media (max-width: 768px) {
  .sk-file-upload { width: 100%; overflow-wrap: anywhere; }
  .sk-file-upload::file-selector-button { margin-bottom: var(--sk-space-2); }
}


/* --------------------------------------------------------------------------
   7. FIELD ROW
   --------------------------------------------------------------------------
   Replaces form-group-row / form-label-col / form-status-col /
   form-control-col. The old proportional columns (45% status, 20% control)
   squeezed the label into a sliver, which is what drove the 0.62rem mobile
   type. These size to content instead. */

.sk-field-row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sk-space-3);
  width: 100%;
  padding: var(--sk-space-3) 0;
  border-bottom: 1px solid var(--sk-border-subtle);
}

.sk-field-row:last-of-type {
  border-bottom: none;
}

.sk-field-row + .sk-section {
  border-top: none;
}

/* Label above control, each full width — instead of compressing three
   columns into 375px and shrinking the type to compensate. */
@media (max-width: 768px) {
  .sk-field-row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--sk-space-2);
  }
}

/* These three often sit on <td>/<th>, which _table.css gives a black border and
   a grey background. Zeroed defensively. */
.sk-field-label,
.sk-field-status,
.sk-field-control {
  min-width: 0;
  border: none;
  background: transparent;
  padding: 0;
}

@media (max-width: 768px) {
  .sk-field-label,
  .sk-field-status,
  .sk-field-control {
    flex: 1 1 100%;
    width: 100%;
  }
}

/* Opt-in: keeps label and control sharing one row under 768px instead of
   the default full-width stack. Right for a field whose control is small
   and self-contained - a switch, in practice - where stacking spends a
   whole extra row's height for nothing; a select or text input still wants
   its own line below the label, which is why this isn't the default.
   Higher specificity than the plain mobile rules above (two classes via a
   child combinator vs. one), so it wins regardless of source order. */
/* nowrap at EVERY width: .sk-field-row wraps, and a label with a long sublabel is
   wider than the room beside the control, so the control dropped to its own line,
   at the left - on a phone, and on a desktop too (AFK Builder's Maximum Scientists,
   whose sublabel is a sentence and a half). The label takes the free width and its
   text wraps there; the control keeps its size at the right. */
.sk-field-row--inline {
  flex-wrap: nowrap;
}

.sk-field-row--inline > .sk-field-label {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-field-row--inline > .sk-field-control {
  flex: 0 0 auto;
  justify-content: flex-end;
}

@media (max-width: 768px) {
  .sk-field-row--inline {
    flex-direction: row;
    align-items: center;
  }

  .sk-field-row--inline > .sk-field-label,
  .sk-field-row--inline > .sk-field-control {
    width: auto;
  }
}

/* Two columns by default - label left, control right - which is what a data
   page wants and what Profile already does. The middle .sk-field-status column
   is opt-in: include the element and you get three columns, omit it and the
   label simply fills. The old system forced all three at fixed 45%/20% widths,
   which is what squeezed the label and drove the 0.62rem mobile type. */
.sk-field-label {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  text-align: left;
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-base);
  text-wrap: pretty;
}

.sk-field-sublabel {
  margin-top: var(--sk-space-1);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
  font-weight: var(--sk-weight-normal);
}

/* ---- Field validation message ---------------------------------------------
   The reason a control has rejected what is in it. Belongs directly under the
   field it describes, not at the end of the card - by the time a player reads
   a message somewhere else they have stopped looking at the input.

   --sk-danger, not --sk-alert: the lighter tint is a decorative hue for rails
   and icons, while this is body text and has to clear 4.5:1 on the card
   surface. --sk-danger is 6.29:1.

   Deliberately NOT display: flex. Every other component here that pairs an icon
   with a sentence is a flex container, and each one has to warn that raw inline
   content becomes one flex item per element - the sk-callout hazard. A block
   with an inline icon has no such trap, and this element is usually written by
   script setting textContent, which would drop a child icon anyway.

   Pair it with sk-is-hidden while there is nothing to say, and reveal by
   REMOVING that class. It is `display: none !important`, so an inline display
   can never bring it back. */
.sk-field-error {
  margin: var(--sk-space-2) 0 0;
  color: var(--sk-danger);
  font-family: var(--sk-font);
  font-size: var(--sk-text-xs);
  line-height: 1.5;
}

.sk-field-error :where(i) {
  margin-right: var(--sk-space-2);
  color: var(--sk-danger);
}

.sk-field-status {
  flex: 1 1 auto;
  display: flex;
  justify-content: center;
  align-items: center;
}

@media (max-width: 768px) {
  .sk-field-status {
    justify-content: flex-start;
  }
}

.sk-field-control {
  flex: 0 1 auto;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--sk-space-2);
}

@media (max-width: 768px) {
  .sk-field-control {
    justify-content: space-between;
  }
}

.sk-field-control--center {
  flex: 1 1 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  padding-top: var(--sk-space-3);
}


/* --------------------------------------------------------------------------
   8. SWITCH
   --------------------------------------------------------------------------
   Canonical markup — one pattern, not two:

       <span class="sk-switch">
         <input type="checkbox" id="x"><label for="x">Optional text</label>
       </span>

   The OFF track was rgba(148,163,184,.3) — 1.75:1, so an off switch was
   invisible. It is solid now. ON was already fine and keeps its green. */

.sk-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
  min-height: 24px;
  padding-left: 54px;
  margin: var(--sk-space-1) 0;
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-base);
  cursor: pointer;
  user-select: none;
  text-align: left;
}

.sk-switch > input[type="checkbox"] {
  position: absolute;
  left: 0;
  top: 1px;
  width: 44px;
  height: 22px;
  margin: 0;
  opacity: 0;
  z-index: 2;
  cursor: pointer;
}

/* Track */
.sk-switch > input[type="checkbox"] + label::before {
  content: '';
  position: absolute;
  left: 0;
  top: 1px;
  width: 44px;
  height: 22px;
  box-sizing: border-box;
  background-color: #646e7d;      /* 3.37:1 against the card — an OFF switch is visible */
  border: 1px solid var(--sk-border);
  border-radius: 100px;
  transition: background-color var(--sk-transition), border-color var(--sk-transition);
}

/* Knob */
.sk-switch > input[type="checkbox"] + label::after {
  content: '';
  position: absolute;
  top: 4px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background-color: #ffffff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  transition: transform var(--sk-transition);
}

.sk-switch > input[type="checkbox"]:checked + label::before {
  background-color: var(--sk-success);
  border-color: var(--sk-success-border);
}

.sk-switch > input[type="checkbox"]:checked + label::after {
  transform: translateX(22px);
}

.sk-switch > input[type="checkbox"]:focus-visible + label::before {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

/* Read-only. Without this a disabled switch looked exactly as live as its
   neighbours - Alliance Control's Vice Leader column, which a Vice Leader may
   see but not change. Same fade as a disabled sk-btn. An <asp:CheckBox> with
   Enabled=False renders its sk-switch span with aspNetDisabled added, around an
   input carrying `disabled`, so the input covers every shape and the span only
   needs its pointer cursor taken back. */
.sk-switch.aspNetDisabled,
.sk-switch > input[type="checkbox"]:disabled {
  cursor: not-allowed;
}

.sk-switch > input[type="checkbox"]:disabled + label {
  opacity: 0.5;
}


/* --------------------------------------------------------------------------
   9. TABLE
   --------------------------------------------------------------------------
   The heaviest defensive block, and the reason the pattern exists.
   _table.css:27-29 sets a global `td { border: 1px solid black }`. The old
   .members-table themed only border-bottom, so every cell kept black left,
   right and top borders (1.19:1) under a "glassmorphic" surface.

   border-collapse is `separate` with zero spacing, not `collapse`: browsers
   ignore border-radius on a collapsed table, so the old rounded corners never
   actually rendered. */

.sk-table {
  width: 100%;
  /* It had none, so a table sat flush against whatever preceded and followed
     it — visible as soon as one appeared mid-card rather than as the card's
     only content. Margins collapse, so this cannot double up. */
  margin: var(--sk-space-3) 0;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--sk-surface-table);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  overflow: hidden;
  font-family: var(--sk-font);
  color: var(--sk-text);

  /* Reset every side first. Zero specificity, so it beats _table.css's bare
     `td` without !important, and still loses to a deliberate page override. */
  /* Tabular figures. This is a game of numbers and there was not a single
     font-variant-numeric in the codebase, so every column of land, networth
     and rank was set in proportional digits - 1 narrower than 0, so nothing
     lines up vertically and magnitudes cannot be compared by eye. */
  font-variant-numeric: tabular-nums;
}

.sk-table :where(th, td) {
  border: 0;
  background: transparent;
  padding: var(--sk-space-2) var(--sk-space-3);
  /* Text reads from the left. Numbers get .sk-num. Centring was the old
     default and it is why a scores table is hard to scan: centred figures
     put the digit positions in a different place on every row. */
  text-align: left;
  font-size: var(--sk-text-sm);
  color: var(--sk-text);
}

/* Numeric column: right-aligned so units, tens and hundreds stack. */
.sk-table :where(th, td).sk-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Then draw back only the divider we want. Both shapes are spelled out
   because Web Forms emits tables with and without a <tbody>. */
.sk-table > tr:not(:last-child) > :where(th, td),
.sk-table > tbody > tr:not(:last-child) > :where(th, td) {
  border-bottom: 1px solid var(--sk-border-subtle);
}

.sk-table :where(thead) th {
  background: var(--sk-surface-raised);
  border-bottom: 1px solid var(--sk-border);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-bold);
  text-transform: uppercase;
  /* Was 0.5px. Uppercase already costs about 10% width over sentence case,
     and on a four-column table in a ~350px card that tracking was the
     difference between a header fitting and breaking mid-word — "NETWORTH"
     rendering as "NETWORT / H". Type size cannot be the lever here:
     --sk-text-xs is already at the 12px floor this system keeps. */
  letter-spacing: 0.3px;
}

/* Perceptible hover. The old rgba(68,124,255,.08) measured 1.10:1 against the
   resting row — the table advertised interactivity and delivered none. */
.sk-table > tr:hover > td,
.sk-table > tbody > tr:hover > td {
  background: var(--sk-wash-accent);
}

/* The row the viewing player IS, among many they are being compared against.

   A tinted ground rather than bold text or a marker column: the row has to be
   findable at a glance without its figures reading differently from the rows
   above and below it.

   --sk-hue, so the tint is whatever the card around the table already set and
   no colour is invented for it. The fallback matters: a table outside a
   hue-carrying card has no --sk-hue at all, and without it the whole
   color-mix() would be invalid and the row would simply not be marked.

   Specificity, the same fight .sk-table-group documents further down: the hover
   rule just above is (0,1,3), so a bare `.sk-is-current > td` at (0,1,1) would
   lose the moment a pointer touched the row. The table class is doubled to
   clear it, and the row keeps its own hover one step stronger again -- without
   that second rule the player's own row would be the one row in the table that
   does not answer the pointer. */
.sk-table.sk-table > tbody > tr.sk-is-current > :where(th, td),
.sk-table.sk-table > tr.sk-is-current > :where(th, td) {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 12%, transparent);
}

.sk-table.sk-table > tbody > tr.sk-is-current:hover > :where(th, td),
.sk-table.sk-table > tr.sk-is-current:hover > :where(th, td) {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 20%, transparent);
}

/* Centring stays available for the odd checkbox or icon column. */
.sk-table :where(th, td).sk-center {
  text-align: center;
}

.sk-table :where(input[type="checkbox"]) {
  width: 18px;
  height: 18px;
  accent-color: var(--sk-accent);
  cursor: pointer;
}

@media (max-width: 768px) {
  .sk-table :where(th, td) {
    padding: var(--sk-space-2);
    font-size: var(--sk-text-min);
  }
}

/* Player-chosen text size (js/table-text.js; the Buildings and Military
   settings sheets). One modifier per type-scale token; no modifier at all is
   the default above. Same (0,1,0) as the base cell rule and its phone
   override, so it wins by coming after them - at every width, which is the
   point: the player picked it. Header cells keep their own size (the thead /
   sk-table-head rules are more specific), so the labels hold the column
   widths they were tuned for. Only sizes that differ on a phone are here:
   --sk-text-xs/-sm resolve to the same ~12px as --sk-text-min at 375px. */
.sk-table--text-dense :where(th, td) { font-size: var(--sk-text-dense); }
.sk-table--text-xxs :where(th, td)   { font-size: var(--sk-text-xxs); }
.sk-table--text-min :where(th, td)   { font-size: var(--sk-text-min); }
.sk-table--text-base :where(th, td)  { font-size: var(--sk-text-base); }
.sk-table--text-lg :where(th, td)    { font-size: var(--sk-text-lg); }

/* Centred variant. For tables that are mostly short status, icon or yes/no
   columns, where left alignment leaves ragged gaps and there is nothing to
   compare numerically. Table-level so you set it once instead of tagging every
   cell.

   .sk-num still wins inside it - a numeric column stays right-aligned even
   here, because centring digits is what made the old tables hard to scan.
   Use .sk-table--center for the layout, .sk-num for the exceptions. */
.sk-table--center :where(th, td) {
  text-align: center;
}

/* Centred, numbers included. .sk-table--center keeps .sk-num right-aligned on purpose; this one does not:
   Sector View's kingdom table was asked to centre every column, figures too. It is declared after the
   .sk-num rule (same specificity), so it wins on order. Keep .sk-num on the numeric cells - they still
   need its tabular figures so the digits line up. Opt-in per table; the rest of the site keeps its
   digits right-aligned. */
.sk-table--center-all :where(th, td),
.sk-table--center-all :where(th, td).sk-num {
  text-align: center;
}

/* A sortable header's caret hangs off the label's right edge instead of taking layout space, so the label
   (not label + hidden caret) is what centres over its column. */
.sk-table--center-all button.sk-th-sort {
  position: relative;
}

.sk-table--center-all th.sk-num > button.sk-th-sort {
  flex-direction: row;
}

.sk-table--center-all .sk-th-sort-caret {
  position: absolute;
  left: 100%;
  margin-left: var(--sk-space-1);
}

/* Slight vertical rules between columns: 1px, the subtle border token, none before the first column.
   A row with a single colspan cell (an expander / detail row) has no second cell, so it gets no rule. */
.sk-table--col-rules :where(th, td) + :where(th, td) {
  border-left: 1px solid var(--sk-border-subtle);
}

/* A barely-there shade on every other column, so the eye can follow a column down a long table.
   Not a colour: --sk-text at 1.5% over whatever the table surface already is, which lifts the
   ground a touch in dark mode and dims it a touch in light mode, in the surface's own hue.
   th and td both, so the head bands with its column. A colspan cell (an expander / detail
   row) never bands.

   Specificity is deliberately the floor (0,1,0): the whole cell test sits inside :where(), so
   only the table class counts. The row hover (0,2,3) and the viewer's own row tint (0,3,2)
   both beat it, so a band never fights them. The one exception is handled below -
   sk-kd-table clears the viewer's row to `background: none` at rest, which would
   leave a gap in the stripe, so that row gets the band back. */
.sk-table--col-bands :where(th:nth-child(even), td:nth-child(even):not([colspan])) {
  background: color-mix(in srgb, var(--sk-text) 1.5%, transparent);
}

.sk-table.sk-table.sk-kd-table.sk-table--col-bands > tbody > tr.sk-is-current:not(:hover) > :where(th, td):nth-child(even):not([colspan]) {
  background: color-mix(in srgb, var(--sk-text) 1.5%, transparent);
}

/* Bare variant: no surface, no border, no radius. For a table nested inside
   another table's cell, or inside a card that already provides the surface —
   without this, a nested sk-table paints a second glass panel on top of the
   first and the rows stop lining up with anything. */
.sk-table--bare {
  background: transparent;
  border: 0;
  border-radius: 0;
}

/* No-divider variant: for a table whose rows carry nothing else - no
   artwork, no pattern, no alternating surface - where the per-row divider
   above is the only thing on the row and reads as a stray line rather than
   structure. Opt-in per table, not the default: most tables here (Buildings,
   the reports) DO carry enough per row that the divider earns its place. */
.sk-table--no-divider > tr:not(:last-child) > :where(th, td),
.sk-table--no-divider > tbody > tr:not(:last-child) > :where(th, td) {
  border-bottom: none;
}

/* Header row built from <td> instead of <th>, and without a <thead>.

   Not a style preference — a necessity. A <table runat="server"> compiles to
   HtmlTable, whose children must all be HtmlTableRow, so a <thead> becomes an
   HtmlGenericControl and throws BC30311 at page-compile time. Legacy markup
   here also tends to use <td> throughout. Put this on the row to get the
   header treatment that :where(thead) th would otherwise have given it. */
.sk-table > tr.sk-table-head > :where(th, td),
.sk-table > tbody > tr.sk-table-head > :where(th, td) {
  background: var(--sk-surface-raised);
  border-bottom: 1px solid var(--sk-border);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Row carrying a full-width note rather than columns — a detail or result line
   belonging to the row above it.

   A row and its note are ONE entry, so the boundary between entries has to read
   louder than the divider inside one. Without that every rule looks the same
   and a two-row entry is indistinguishable from two one-row entries. Three
   things do the work together:
     - a recessed surface, so the note reads as attached to its row rather than
       floating between two of them,
     - the subtle default divider left in place above it, joining the pair,
     - a heavier --sk-border rule below, closing the group.

   The tint is a LIFT, not a recess, and it is deliberately slight. A note is
   subordinate, so recessing it is the instinctive choice — but on a surface
   this dark there is no room below: --sk-surface-inner measures 1.09:1 against
   the table body, i.e. invisible. A white wash at least reaches 1.17:1 while
   keeping muted text at 5.2:1. Nothing available here gets past ~1.25:1, which
   is why the 2px rule below carries the grouping and the fill only supports it.

   The selector is spelled out for both table shapes because sk-table's own
   divider rule is (0,2,2); matching it and landing later in the file is what
   lets this win. A bare `.sk-table .sk-table-note > td` is only (0,2,1) and
   loses — the same specificity trap this system keeps meeting. */
.sk-table > tr.sk-table-note > td,
.sk-table > tbody > tr.sk-table-note > td {
  border-top: 0;
  border-bottom: 2px solid var(--sk-border);
  background: rgba(255, 255, 255, 0.06);
  color: var(--sk-text-muted);
  /* A note cell holds whatever a legacy control renders into it, so it cannot
     assume the content is willing to wrap. Under a fixed table layout an
     unwrappable child overflows the cell and takes the page sideways with it. */
  overflow-wrap: break-word;
  min-width: 0;
}

/* Last entry needs the closing rule too — sk-table drops the divider on the
   final row, which would leave the bottom group open. */
.sk-table > tr.sk-table-note:last-child > td,
.sk-table > tbody > tr.sk-table-note:last-child > td {
  border-bottom: 0;
}

/* A note that carries an action of its own - a copy button for the entry the
   note belongs to.

   Grid rather than a flex row, because the note has to stay centred while the
   action sits at the trailing edge, and `justify-content: center` with an auto
   margin gives you one or the other, not both. Two tracks: the note takes what
   is left, the action is sized by its content and lands hard right. When the
   action is not rendered - a spy report carries its own copy link, so the
   button is not emitted at all - the second track collapses and this is a plain
   centred note again.

   IT IS AN ELEMENT INSIDE THE CELL, NEVER THE CELL ITSELF. Setting
   `display: grid` on a <td> stops it being a table cell: it is no longer laid
   out by the table algorithm, so colspan is ignored and the cell shrinks to its
   own content while the rows above it stay full width. That is precisely what
   happened when this shipped as a modifier on the td - the history table looked
   broken from the note rows down. Any display change on a table cell has this
   effect; wrap the content instead. */
.sk-note-action {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sk-space-2);
  width: 100%;
}

/* Belt and braces for the note's content: a grid item's minimum is its content,
   so without this an over-wide child still widens the track it is supposed to
   be constrained by. Everything that scrolls does so inside its own box. */
.sk-note-action > * {
  min-width: 0;
  max-width: 100%;
}

/* ---- Nested rows: a parent and the levels beneath it ------------------------
   Some tables here list a FAMILY rather than a flat set: Probe Armour I-V,
   Longevity I-II, Race Token 1-12. One header row stands for the family and
   carries its progress ("Level 3 / 5"), and the member rows sit beneath it,
   collapsible.

   Two classes, both on the <tr>:

     .sk-table-group   the family header. Reads as a divider with a label, not
                       as another data row, so the eye can tell structure from
                       content without a second colour.
     .sk-table-child   a member row, indented under it.

   The indent is padding on the FIRST CELL, never a display change on the row.
   `display: grid` or `flex` on a <tr> or <td> removes it from the table's
   layout algorithm - colspan stops working and the cell shrinks to its own
   content while the rows above stay full width. See .sk-note-action above,
   which shipped that bug once already.

   Specificity: sk-table's own divider and hover rules are (0,2,2) and its
   "last row keeps no divider" rule is (0,3,2), so every selector here doubles
   the table class AND names the row class. A bare `.sk-table-group > td` is
   (0,1,1) and loses - on exactly the rows that matter. */
/* A family header is a band in the accent, so the eye finds where one group
   ends and the next begins (team review, 2026-10-20: Research's "separators lost
   their colours" - the colour had been a legacy button style leaking into the
   toggle). The static label rows below keep the plain inner surface. */
.sk-table.sk-table > tbody > tr.sk-table-group > :where(th, td),
.sk-table.sk-table > tr.sk-table-group > :where(th, td) {
  background: color-mix(in srgb, var(--sk-accent) 22%, var(--sk-surface-inner));
  border-top: 1px solid color-mix(in srgb, var(--sk-accent) 55%, transparent);
  color: var(--sk-text);
  font-weight: var(--sk-weight-semibold);
  cursor: pointer;
  user-select: none;
}

.sk-table.sk-table > tbody > tr.sk-table-group:hover > :where(th, td),
.sk-table.sk-table > tr.sk-table-group:hover > :where(th, td) {
  background: color-mix(in srgb, var(--sk-accent) 32%, var(--sk-surface-inner));
}

/* The group header's control.

   A real <button>, not a click handler on the <tr>: a row cannot be tabbed to,
   cannot be activated by Enter or Space, and is announced as a table row rather
   than as a control - while cursor:pointer above makes it look interactive.
   Bare and full-width so the whole header is still the hit area it was.

   font: inherit and color: inherit matter here: this app sets its own font on
   bare button elements (the form-control inheritance rule kept from Reboot), and
   without them the label would not match the row it heads.

   Put the toggle handler on THIS and never also on the row - a click here
   bubbles, and two handlers would toggle twice and cancel out.

   .sk-table qualifies it: general.css's `button:not(.table-col-submit)` (0,1,1)
   outranked the bare class and drew a boxed, shadowed, 160px-wide legacy button
   inside the header row (review, 2026-10-19: Research's "Land Bonuses" row). */
.sk-table .sk-table-group-toggle {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.sk-table-group-toggle:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

/* A group header that is only a LABEL - no toggle, nothing to click. Buildings
   merges its Economy and Military tables into one and needs a divider row with
   a name on it; the base class reads as a control because Research's groups
   are one. The selectors double the table class and name the row class for the
   same reason the base rules do - sk-table's own cell rules are (0,2,2), and a
   bare .sk-table-group--static > td would lose to the very rules it is
   overriding. */
.sk-table.sk-table > tbody > tr.sk-table-group--static > :where(th, td),
.sk-table.sk-table > tr.sk-table-group--static > :where(th, td) {
  background: var(--sk-surface-inner);
  border-top-color: var(--sk-border);
  cursor: default;
}

.sk-table.sk-table > tbody > tr.sk-table-group--static:hover > :where(th, td),
.sk-table.sk-table > tr.sk-table-group--static:hover > :where(th, td) {
  background: var(--sk-surface-inner);
}

/* A static group is a LABEL, so it costs a label's height: tight vertical
   padding and the eyebrow's type - uppercase at --sk-text-min with its
   tracking - so a band spanning every column reads as a section heading
   rather than as a row with one very long value in it. The design system's
   collapsible group is sized for a control with a caret; a divider that
   only names a section should not cost that much.

   Moved here from buildings-page.css, where Buildings' merged table was the
   only user. Any table that divides its rows into named sections gets the
   same band by adding sk-table-group--static. The icon, if the label has
   one, takes the accent the way every sk-eyebrow icon does.

   Only the vertical padding is set, so a fitted table's own inline padding
   still applies. (0,3,2) like the rules above, for the same reason. */
.sk-table.sk-table > tbody > tr.sk-table-group--static > :where(th, td),
.sk-table.sk-table > tr.sk-table-group--static > :where(th, td) {
  padding-top: var(--sk-space-1);
  padding-bottom: var(--sk-space-1);
  font-size: var(--sk-text-min);
  letter-spacing: 0.8px;
  text-transform: uppercase;
}

.sk-table.sk-table > tbody > tr.sk-table-group--static > :where(th, td) > i,
.sk-table.sk-table > tr.sk-table-group--static > :where(th, td) > i {
  margin-right: var(--sk-space-1);
  color: var(--sk-accent);
}

/* The disclosure marker turns with the group. A CSS rotation rather than two
   glyphs, so the icon cannot drift out of sync with the state. */
.sk-table-group-caret {
  display: inline-block;
  width: 1em;
  transition: transform var(--sk-transition);
}

.sk-table-group.is-open .sk-table-group-caret {
  transform: rotate(90deg);
}

/* Containment, not just indentation.

   An indent alone reads as "these rows are slightly misaligned", not as "these
   rows belong to the one above". Three things together make the group read as a
   container:

     1. a continuous accent rule down the left of every member row, which is the
        actual visual tie back to the header;
     2. a recessed surface, so the block sits BELOW the plane of the ungrouped
        rows either side of it rather than level with them;
     3. no divider under the last member, so the group closes as one block
        instead of bleeding into the next research.

   The rule is a border on the first cell rather than an outline on the row,
   because a <tr> cannot take a border under border-collapse: separate. */
.sk-table.sk-table > tbody > tr.sk-table-child > :where(th, td),
.sk-table.sk-table > tr.sk-table-child > :where(th, td) {
  background: var(--sk-surface-inner);
}

.sk-table.sk-table > tbody > tr.sk-table-child > :where(th, td):first-child,
.sk-table.sk-table > tr.sk-table-child > :where(th, td):first-child {
  padding-left: var(--sk-space-6);
  border-left: 3px solid var(--sk-border-accent);
}

/* The last member closes the block: no divider under it, and the accent rule
   stops with a rounded foot so the group has a visible bottom rather than
   running into whatever follows. */
.sk-table.sk-table > tbody > tr.sk-table-child--last > :where(th, td),
.sk-table.sk-table > tr.sk-table-child--last > :where(th, td) {
  border-bottom: 0;
}

.sk-table.sk-table > tbody > tr.sk-table-child--last > :where(th, td):first-child,
.sk-table.sk-table > tr.sk-table-child--last > :where(th, td):first-child {
  border-bottom-left-radius: var(--sk-radius-sm);
}

/* The header sits ON the block it opens, so it takes the same left rule and
   squares off against the members below it. */
.sk-table.sk-table > tbody > tr.sk-table-group.is-open > :where(th, td):first-child,
.sk-table.sk-table > tr.sk-table-group.is-open > :where(th, td):first-child {
  border-left: 3px solid var(--sk-border-accent);
}

/* ---- A table of cards ------------------------------------------------------
   .sk-table--cards turns each ENTRY — a row, plus the rows that belong to it —
   into its own card, and keeps one shared header above them so the columns are
   still labelled and still line up down the whole list.

   Why it exists. A record that spans several rows (a row of columns plus a
   full-width result line, ninety-six times over on the probe history) reads as
   one undifferentiated block: every horizontal rule looks alike, so nothing
   tells the eye where one record ends and the next begins. sk-table-note
   already fights that with a heavier rule closing the group, and past a dozen
   entries a rule is not enough. A card is the honest form for "one record".

   It stays a TABLE rather than becoming a list of divs, because that is the
   whole point: cards separate the records, columns compare them. A div list
   would have to re-invent column alignment, and every card would drift by its
   own padding.

   Markup contract, per entry:

     <tr class="sk-entry-gap"><td colspan="n"></td></tr>
     <tr class="sk-entry-row sk-entry-row--first"> ...cells... </tr>
     <tr class="sk-entry-row sk-entry-row--last"><td colspan="n">note</td></tr>

   - --first draws the top edge and top corners, --last the bottom ones. Neither
     is inferred from position: a row may be hidden at the current width, and a
     note is optional. Mark BOTH rows of a desktop/mobile pair (sk-hide-mobile /
     sk-hide-desktop) as --first — only one of them is ever in flow.
   - The gap is a real empty row, not a margin. A <tr> cannot take one, and
     border-spacing inserts its gap between EVERY row — including the two rows
     of one entry, splitting the card it is meant to separate. The gap leads the
     entry rather than trailing them, so the last card does not hang a gap off
     the bottom of the table and the header gets its separation for free.
   - No box-shadow. Shadows on a <tr> render inconsistently across browsers; the
     edge and the gap carry the card on their own.

   Specificity: sk-table's own divider and hover rules are (0,2,2), so every
   rule below doubles the table class, and the --first / --last rules also name
   sk-entry-row, to clear them. `.sk-table--cards > tr.sk-entry-row > td` alone
   is (0,2,1) and loses silently — the trap this system keeps meeting. */
.sk-table.sk-table--cards {
  /* The cards are the surface now. Leaving the table's own glass beneath them
     stacks a third pane inside an sk-card, and `overflow: hidden` would clip
     what the cards paint into the gaps between them. */
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;

  /* Fixed layout at every width, not only under 768px like sk-table--fit.
     A note cell here holds whatever a legacy control renders, and one of them -
     the Spy on Buildings report - is a 27-column table with per-column
     min-widths totalling around 1,000px. Under auto layout the cell grows to
     its content's minimum, the whole table grows with it, and the PAGE scrolls
     sideways; the inner scroll container never gets the chance, because it has
     no definite width to scroll within. Fixed layout gives the cell the table's
     width and hands that container something to work with. */
  table-layout: fixed;
}

/* The header is a free-standing bar once the body below it is cards, so it
   gets its own corners rather than the square ends of a clipped table. Both
   header shapes, since a table that can carry a <thead> should. */
.sk-table.sk-table--cards > tr.sk-table-head > :where(th, td):first-child,
.sk-table.sk-table--cards > tbody > tr.sk-table-head > :where(th, td):first-child,
.sk-table.sk-table--cards > thead > tr > :where(th, td):first-child {
  border-radius: var(--sk-radius) 0 0 var(--sk-radius);
}

.sk-table.sk-table--cards > tr.sk-table-head > :where(th, td):last-child,
.sk-table.sk-table--cards > tbody > tr.sk-table-head > :where(th, td):last-child,
.sk-table.sk-table--cards > thead > tr > :where(th, td):last-child {
  border-radius: 0 var(--sk-radius) var(--sk-radius) 0;
}

/* The card body. --sk-neutral-1 is the ramp's "raised fill on a dark surface",
   which is exactly this: a record lifted off the panel it sits on. */
.sk-table.sk-table--cards > tr.sk-entry-row > :where(th, td),
.sk-table.sk-table--cards > tbody > tr.sk-entry-row > :where(th, td) {
  background: var(--sk-neutral-1);
  border-top: 0;
  border-bottom: 0;
}

.sk-table.sk-table--cards > tr.sk-entry-row > :where(th, td):first-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row > :where(th, td):first-child {
  border-left: 1px solid var(--sk-border);
}

.sk-table.sk-table--cards > tr.sk-entry-row > :where(th, td):last-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row > :where(th, td):last-child {
  border-right: 1px solid var(--sk-border);
}

/* A note inside a card is a sub-panel of that card, not a group closer, so the
   2px rule sk-table-note draws under it goes and only the slight lift stays. */
.sk-table.sk-table--cards > tr.sk-entry-row.sk-table-note > td,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-table-note > td {
  background: color-mix(in srgb, var(--sk-neutral-1) 94%, #fff);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--first > :where(th, td),
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--first > :where(th, td) {
  border-top: 1px solid var(--sk-border);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--first > :where(th, td):first-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--first > :where(th, td):first-child {
  border-top-left-radius: var(--sk-radius);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--first > :where(th, td):last-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--first > :where(th, td):last-child {
  border-top-right-radius: var(--sk-radius);
}

/* Named twice over on purpose: sk-table's "last row keeps no divider" rule is
   (0,3,2), so a single class here would lose on the final entry only — the one
   card in the list that would silently render with its bottom edge missing. */
.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--last > :where(th, td),
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--last > :where(th, td) {
  border-bottom: 1px solid var(--sk-border);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--last > :where(th, td):first-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--last > :where(th, td):first-child {
  border-bottom-left-radius: var(--sk-radius);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-entry-row--last > :where(th, td):last-child,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-entry-row--last > :where(th, td):last-child {
  border-bottom-right-radius: var(--sk-radius);
}

/* Hover is switched off inside a card. sk-table's wash lands on one ROW, and a
   card is read as one object — lighting up half of it says the half is
   separately interactive when nothing in it is. The links inside keep their
   own hover. */
.sk-table.sk-table--cards > tr.sk-entry-row:hover > :where(th, td),
.sk-table.sk-table--cards > tbody > tr.sk-entry-row:hover > :where(th, td) {
  background: var(--sk-neutral-1);
}

.sk-table.sk-table--cards > tr.sk-entry-row.sk-table-note:hover > td,
.sk-table.sk-table--cards > tbody > tr.sk-entry-row.sk-table-note:hover > td {
  background: color-mix(in srgb, var(--sk-neutral-1) 94%, #fff);
}

/* The gap between two cards. Empty, unpainted, and out of the way of every
   rule above — including sk-table's divider and hover, which would otherwise
   draw a line and a wash across nothing. */
.sk-table.sk-table--cards > tr.sk-entry-gap > td,
.sk-table.sk-table--cards > tbody > tr.sk-entry-gap > td,
.sk-table.sk-table--cards > tr.sk-entry-gap:hover > td,
.sk-table.sk-table--cards > tbody > tr.sk-entry-gap:hover > td {
  height: var(--sk-space-2);
  padding: 0;
  background: transparent;
  border: 0;
}

/* --------------------------------------------------------------------------
   A table wider than its space: two supported answers
   --------------------------------------------------------------------------
   Both are first-class. Choose per table — they are not a fallback chain.

   .sk-table--fit      Every column stays on screen. Cells wrap instead of
                       forcing a minimum width, and padding tightens on narrow
                       viewports. This is the house preference: a table is read,
                       not navigated, and rows can only be compared when the
                       columns are all visible at once.

   .sk-table-scroll    The table keeps its natural column widths and scrolls
                       sideways inside its own box, so the PAGE never scrolls
                       horizontally. For content that genuinely cannot shorten.

   Reach for content changes before either one. "Networth" -> "NW",
   1,240,000 -> 1.24M is a markup or code-behind edit, and it is usually the
   real fix — CSS cannot recover space that the words are wasting.
   .sk-density-compact stacks with both and takes it out of the whitespace.

   Type size is not a lever in either direction: --sk-text-min is a 12px floor.
   -------------------------------------------------------------------------- */

.sk-table--fit {
  width: 100%;
}

/* break-word, not anywhere: `anywhere` splits inside any word at any
   character, which turned "STATUS" into "STATU / S" and — far worse — 469,463
   into "469,4 / 63". `break-word` only breaks a word that cannot fit on a
   line of its own. */
.sk-table--fit :where(th, td) {
  overflow-wrap: break-word;
  white-space: normal;
}

/* A figure is not broken mid-number on desktop, where there is room. Note the
   media query below releases this under 768px: `nowrap` there let the columns
   drive the table wider than the viewport and put the whole PAGE into a
   horizontal scroll, which is far worse than a wrapped figure. */
.sk-table--fit :where(th, td).sk-num {
  overflow-wrap: normal;
  white-space: nowrap;
}

@media (max-width: 768px) {
  .sk-table--fit {
    /* The guarantee that this variant is named for: a fixed layout makes the
       table exactly its container's width whatever the content is, so content
       overflows a CELL and never the table. With auto layout a table is sized
       from its content's max-content width, and `width: 100%` acts only as a
       minimum — which is how one nowrap column pushed the page sideways. */
    table-layout: fixed;
  }

  .sk-table--fit :where(th, td) {
    padding: var(--sk-space-1);
  }

  /* Released here on purpose - see above. */
  .sk-table--fit :where(th, td).sk-num {
    overflow-wrap: break-word;
    white-space: normal;
  }

  /* Headers only, and only on small screens. --sk-text-min stays a 12px floor
     for everything the player reads as prose; this is an uppercase label of
     one or two words, where there is no x-height or descender to lose, so a
     point below the floor costs nothing in legibility and buys a column.
     The letter-spacing goes too — at five columns in ~300px it is pure width.
     Do not copy this exception to mixed-case text.

     RULE: a header is Capitalized here, not UPPERCASE. UPPERCASE costs
     ~10% more width than Capitalized for the same word - affordable on
     desktop, but the difference between a header fitting its column and
     breaking mid-word ("RUNNING" -> "RUNN"/"ING") on a phone. That holds for
     every table on a phone, sk-table-scroll included - see the rule below
     this one; only the 11px size is --fit's alone. */
  .sk-table--fit > tr.sk-table-head > :where(th, td),
  .sk-table--fit > tbody > tr.sk-table-head > :where(th, td),
  .sk-table--fit :where(thead) th {
    font-size: 11px;
    letter-spacing: 0;
    text-transform: capitalize;
  }

  /* EVERY table's headers on a phone: capitalised, never uppercase (the
     player's call, and it saves the width uppercase costs). sk-table-scroll
     tables too - Sector View's 8px headers included. */
  .sk-table :where(thead) th,
  .sk-table > tr.sk-table-head > :where(th, td),
  .sk-table > tbody > tr.sk-table-head > :where(th, td) {
    letter-spacing: 0;
    text-transform: capitalize;
  }
}

/* Columns share the width evenly instead of being sized by their content, set
   by the first row. Composable with either answer above; keep it separate
   because fixed layout is a real behaviour change, not a nicety — a column
   given an explicit width keeps it, and everything else splits the remainder. */
.sk-table--fixed {
  table-layout: fixed;
}

/* ---- Stamp column ----------------------------------------------------------
   A two-column table whose first column is a fixed-size stamp — a type badge
   over a timestamp — and whose second is the prose that matters. The news
   rooms (KingdomNews.aspx, AllianceNews.aspx) are the case it was built for.

   It exists because sk-table--fit switches to `table-layout: fixed` under
   768px, and fixed layout with no declared widths splits the columns EVENLY.
   On a two-column table that is 50/50: the stamp took half the screen to show
   an eight-character badge, and the report — the whole reason the row exists —
   wrapped every three words down a ribbon beside it.

   A width on the first column is all fixed layout needs; the second takes the
   remainder on its own. Declared on the cells rather than via <col>, because a
   Repeater's HeaderTemplate would have to carry a <colgroup> across a template
   boundary to emit one. */
/* Sized by class, not by :first-child. Under table-layout:fixed only the
   first row's widths count, so these need only reach the <th>s - but naming
   the column means the layout does not depend on how many cells precede it. */
.sk-table--stamp .sk-col-stamp {
  width: 10rem;
}

@media (max-width: 768px) {
  .sk-table--stamp .sk-col-stamp {
    width: 7.5rem;
  }

  /* The badge sets the column's floor, so it has to come in with it. Padding
     and tracking only — the 12px type floor still holds, and the label stays
     readable rather than becoming an unlabelled colour swatch. */
  .sk-table--stamp .sk-badge {
    padding: 2px 6px;
    letter-spacing: 0;
  }
}

/* A tick box alone: no label text beside it, because the row is the label.
   sk-checkbox is inline-flex with a gap for the text that is not there, which
   would otherwise leave a hole next to the box. */
.sk-checkbox--bare {
  gap: 0;
}

/* The top line of a stamp cell: the row's tick box and its type badge, with the
   timestamp on the line below.

   The tick box rode in a column of its own first. That column had to be wide
   enough to be tappable and it sat outside the stamp, so on a phone the row
   opened with two narrow columns before any content - and the report, the
   thing being read, got what was left of a third. Folded in here the tick
   costs the stamp one line it was already tall enough for, and the table is
   back to two columns at every width.

   align-items rather than baseline: a tick box has no text baseline to sit on,
   and baseline alignment drops it below the badge's cap height. */
.sk-stamp-head {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  margin-bottom: var(--sk-space-1);
}

/* ---- Filter as a dropdown --------------------------------------------------
   The mobile half of a chip row. Chips are the better control while they fit on
   one or two lines; six of them on a phone is a block of wrapped buttons above
   the content they filter, and it pushes the news itself below the fold.

   A native select is the honest answer there: it collapses to one line, it is
   the control a phone already knows how to present full-screen, and it needs no
   custom keyboard or focus handling to be accessible.

   Both are rendered and swapped with sk-hide-mobile / sk-hide-desktop rather
   than one being built on resize - the options are generated FROM the chips
   (see news-room.js), so the two cannot list different filters or disagree
   about a count. */
.sk-filter-select {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  width: 100%;
}

/* ---- Key/value list --------------------------------------------------------
   A record shown as stacked label/value pairs instead of a row of columns.

   This is what a data table should become on a narrow screen. Five columns in
   ~300px cannot be made readable by tuning type or by wrapping — the honest
   answer is to turn the row ninety degrees and give each field its own line.
   Pair it with sk-hide-mobile / sk-hide-desktop to swap presentations:

       <tr class="sk-hide-mobile">  ...columns...  </tr>
       <tr class="sk-hide-desktop"><td colspan="6">
         <dl class="sk-datalist">
           <dt>Probes sent</dt><dd class="sk-num">469,463</dd>
         </dl>
       </td></tr>

   Both grid tracks are minmax(0, …) so a long value wraps inside its own cell
   instead of widening the grid and pushing the page into a horizontal scroll —
   which is exactly what a nowrap numeric column did to the probe history.
*/
.sk-datalist {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  width: 100%;
  margin: 0;
}

.sk-datalist > dt,
.sk-datalist > dd {
  min-width: 0;
  padding: var(--sk-space-2) 0;
  border-bottom: 1px solid var(--sk-border-subtle);
  overflow-wrap: break-word;
}

.sk-datalist > dt {
  padding-right: var(--sk-space-3);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-semibold);
}

.sk-datalist > dd {
  margin: 0;
  color: var(--sk-text);
  text-align: right;
}

/* ---- Dense table -----------------------------------------------------------
   Below the 12px floor, on purpose, and only where a table asks for it.

   The floor exists because this system was built to fix readability, and it
   holds for everything a player reads. But some tables here carry more columns
   than 12px type can fit in the width available, and the levers the guide
   prefers -- shortening headers, abbreviating figures, sk-table--fit -- do not
   always close the gap. The honest options at that point are horizontal
   scroll, dropping columns, or smaller type; this is the third, made explicit
   so it is a decision someone took rather than a default that crept in.

   Applies to the cells only. A caption or a note beside the table keeps its
   normal size. */
.sk-table--dense :where(th, td) {
  font-size: var(--sk-text-dense);
  padding: var(--sk-space-1) var(--sk-space-2);
  letter-spacing: 0;
}

/* Stacked variant: label on its own line, description beneath it, both left
   aligned — rather than two columns.

   The two-column form is right for a figure: a short value reads well flush
   right against its label. It is wrong for a sentence. Right-aligning prose in a
   narrow track gives a ragged block that is harder to read than the same words
   set flush left, and it squeezes the label into a column narrower than the
   words in it. Use this whenever the "value" is an explanation rather than a
   number, and note the type steps up — --sk-text-xs is the 12px floor, which is
   a caption size, not a reading size. */
.sk-datalist--stacked {
  grid-template-columns: minmax(0, 1fr);
}

.sk-datalist--stacked > dt {
  padding-bottom: 0;
  border-bottom: 0;
  font-size: var(--sk-text-sm);
}

.sk-datalist--stacked > dd {
  padding-top: var(--sk-space-1);
  text-align: left;
  font-size: var(--sk-text-base);
  line-height: 1.6;
}

/* The final pair closes on the row boundary rather than drawing its own rule. */
.sk-datalist > dt:last-of-type,
.sk-datalist > dd:last-of-type {
  border-bottom: 0;
}

.sk-table-scroll {
  width: 100%;
  /* Load-bearing: overflow-x only scrolls content wider than THIS box's
     resolved width, and without a cap an oversized table stretches the box,
     the card and the page above it. */
  max-width: 100%;
  overflow-x: auto;
  /* Don't chain the scroll to the page — on a trackpad or touchpad that turns
     into a browser back-navigation the moment the table hits its end. */
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--sk-neutral-3) var(--sk-surface-inner);
}

/* A fitted table that refuses to shrink below a width on a phone. sk-table--fit
   is width:100% under table-layout:fixed, so it always crushes every column to
   the screen - with the default columns that is right, but a player who turns
   more columns or controls on gets unreadable cells instead of a wider table.
   The page computes the real minimum (the sum of the visible columns' phone
   widths) and sets it as --sk-table-min-w on the table; at the default count it
   stays under the screen and changes nothing, and past it the excess scrolls
   inside this box. Generalised from Buildings' and Military's own copies
   (--buildings-min-w, --military-min-w), which were this rule twice. */
@media (max-width: 768px) {
  .sk-table-scroll > .sk-table.sk-table--fit {
    min-width: var(--sk-table-min-w, 100%);
  }
}

/* Vertical variant, for a long table rather than a wide one. The header sticks
   so the columns stay identified while the body scrolls under it — without
   that, a scrolled table is just numbers with no labels.

   Height is a custom property so a page can set its own without a new class:
       <div class="sk-table-scroll--y" style="--sk-table-max-h: 24rem"> */
.sk-table-scroll--y {
  max-height: var(--sk-table-max-h, 60vh);
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--sk-neutral-3) var(--sk-surface-inner);
}

.sk-table-scroll--y .sk-table :where(thead) th {
  position: sticky;
  top: 0;
  z-index: 1;
}

/* On a phone the table grows with the page instead: one scroll, the page's, not a box
   that scrolls inside a page that scrolls (review, 2026-10-19 - "on mobile we really
   don't need scroll bars on any pages"). The header stops sticking with it. */
@media (max-width: 768px) {
  .sk-table-scroll--y {
    max-height: none;
    overflow-y: visible;
  }

  .sk-table-scroll--y .sk-table :where(thead) th {
    position: static;
  }
}


/* --------------------------------------------------------------------------
   10. PROGRESS
   --------------------------------------------------------------------------
   The sanctioned pattern for a JS-driven value. The fill width is a custom
   property, so script sets one property and never an inline style:

       el.style.setProperty('--sk-fill', pct + '%');

   Do NOT write element.style.width — that reintroduces the inline styling this
   system exists to remove. */

.sk-progress {
  width: 100%;
  height: 10px;
  background: var(--sk-surface-raised);
  border: 1px solid var(--sk-border);
  border-radius: 100px;
  overflow: hidden;
}

.sk-progress-bar {
  display: block;
  height: 100%;
  width: var(--sk-fill, 0%);
  background: var(--sk-accent);
  border-radius: 100px;
  transition: width var(--sk-transition-slow);
}

.sk-progress-bar--success { background: var(--sk-success); }
.sk-progress-bar--danger { background: var(--sk-danger); }
.sk-progress-bar--warning { background: var(--sk-warning); }
/* In the hue of the sk-hue-* around it (Missile's silo stock, in the missile hue):
   a bar that is about a subject, where the three above are about a state. */
.sk-progress-bar--hued { background: var(--sk-hue, var(--sk-accent)); }

/* Two fills in one track: what is owned, then what is queued on top of it.
   Flex so the second bar starts where the first ends; each still reads its
   own --sk-fill. Square inner ends so the join is a clean edge. The track's
   own overflow: hidden rounds the outside. */
.sk-progress--segmented {
  display: flex;
}

.sk-progress--segmented > .sk-progress-bar {
  flex: 0 0 auto;
  border-radius: 0;
}

/* Queued, not yet bought: striped accent washes, so it reads as a promise on
   top of the solid fill rather than as more of it. */
.sk-progress-bar--queued {
  background: repeating-linear-gradient(45deg,
      var(--sk-wash-accent-strong) 0 6px,
      var(--sk-wash-accent) 6px 12px);
}

/* A track with its figure beside it, on one line. The figure keeps a floor
   width so "40% (+10%)" appearing does not jolt the track. */
.sk-progress-line {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  width: 100%;
}

.sk-progress-line > .sk-progress {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-progress-value {
  flex: 0 0 auto;
  min-width: 9ch;
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  font-size: var(--sk-text-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}


/* --------------------------------------------------------------------------
   10b. DRAWER
   --------------------------------------------------------------------------
   A panel that slides in over the page to explain something, triggered by an
   icon button beside the thing it explains. From the RIGHT on desktop and from
   the BOTTOM under 768px — a right-hand panel on a phone is a full-screen
   takeover with a slide animation nobody asked for, while a bottom sheet lands
   where the thumb already is.

   It is not a modal. A modal interrupts to demand a decision; a drawer is
   reference material you open beside your work and dismiss. Hence no confirm/
   cancel, no form, and a scrim you can click straight through to close.

   MARKUP CONTRACT — the drawer element must NOT be inside a card.

     <button type="button" class="sk-btn sk-btn--icon"
             data-sk-drawer-open="probe-quick-help" aria-label="About quick options">
       <i class="fas fa-info"></i>
     </button>

     <div class="sk-drawer" id="probe-quick-help" role="dialog" aria-modal="true"
          aria-labelledby="probe-quick-help-title">
       <div class="sk-drawer-scrim" data-sk-drawer-close></div>
       <div class="sk-drawer-panel">
         <div class="sk-drawer-header">
           <h4 id="probe-quick-help-title">Quick options</h4>
           <button type="button" class="sk-btn sk-btn--icon" data-sk-drawer-close
                   aria-label="Close"><i class="fas fa-times"></i></button>
         </div>
         <div class="sk-drawer-body"><p>…</p></div>
       </div>
     </div>

   Why "not inside a card" is a hard rule and not a preference: .sk-card carries
   backdrop-filter, and an element with a backdrop-filter becomes the containing
   block for its position:fixed descendants. A drawer nested in a card would be
   positioned against the CARD instead of the viewport — clipped, mispositioned,
   and impossible to debug from the drawer's own rules. This is the same trap
   documented at the top of this file for container queries. Put the drawer at
   the end of the page's content, outside every card, and let the trigger find
   it by id.

   Open state is the class .is-open, set by js/sk-drawer.js — rule 1 of
   "classnames only". Nothing here uses display:none, deliberately: display is
   not animatable, so the panel would jump rather than slide. Visibility is,
   given a delay equal to the slide, which is what the base rule buys. */
.sk-drawer {
  position: fixed;
  inset: 0;
  /* Above every card and the top bar, below .modal-overlay's 10000: a modal
     asks for a decision and must be able to cover reference material. */
  z-index: 9000;
  visibility: hidden;
  /* Held visible until the slide-out has finished. Without the delay the panel
     is removed from the page the instant it is closed and never slides. */
  transition: visibility 0s var(--sk-transition-slow);
}

.sk-drawer.is-open {
  visibility: visible;
  transition-delay: 0s;
}

.sk-drawer-scrim {
  position: absolute;
  inset: 0;
  background: var(--sk-surface-overlay);
  opacity: 0;
  transition: opacity var(--sk-transition);
  cursor: pointer;
}

.sk-drawer.is-open .sk-drawer-scrim {
  opacity: 1;
}

.sk-drawer-panel {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  /* Caller-tunable, the same way sk-table-scroll--y exposes --sk-table-max-h.
     420px is right for a help panel; a drawer carrying a table wants more. */
  width: min(var(--sk-drawer-width, 420px), 100%);
  /* dvh, not vh: on mobile browsers vh includes the collapsing address bar, so
     a 100vh panel is taller than the screen and its close button sits under
     the chrome. */
  height: 100dvh;
  padding: var(--sk-space-5);
  background: var(--sk-surface-card);
  backdrop-filter: var(--sk-blur);
  -webkit-backdrop-filter: var(--sk-blur);
  border-left: 1px solid var(--sk-border);
  box-shadow: var(--sk-shadow-lg);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(100%);
  transition: transform var(--sk-transition-slow);
}

.sk-drawer.is-open .sk-drawer-panel {
  transform: none;
}

/* A wide drawer, for a panel that lays its controls out in columns (Attack's
   Simulator: two sk-split--even columns). Only the width changes, through the
   panel's own --sk-drawer-width hook; under 768px the bottom sheet is full
   width either way, so the columns stack as sk-split does. A drawer that needs
   one exact width still sets the property itself (Military, Buildings). */
.sk-drawer--wide {
  --sk-drawer-width: min(56rem, 92vw);
}

/* A drawer holding a table of five or six figure columns: 420px cuts the last
   columns off behind a scrollbar, sk-drawer--wide leaves a small table lost in
   the panel. 620px is what Military and Buildings set inline for theirs; Probe's
   "Probe stats" uses this class. Under 768px it is the full-width sheet as
   always. */
.sk-drawer--table {
  --sk-drawer-width: 620px;
}

.sk-drawer-header {
  display: flex;
  align-items: center;
  gap: var(--sk-space-3);
  margin-bottom: var(--sk-space-4);
  padding-bottom: var(--sk-space-3);
  border-bottom: 1px solid var(--sk-border);
}

/* Same reset as sk-card-header: the base stylesheets style bare h1-h6.
   min-width: 0 lets the title be the one thing in the row that shrinks and
   wraps - the icon, a badge and the close button all get flex-shrink: 0
   below instead of sharing the squeeze. */
.sk-drawer-header :where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  min-width: 0;
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-semibold);
  color: var(--sk-text);
}

/* The leading icon never shrinks - see the title's own comment above. */
.sk-drawer-header > :where(i) {
  flex-shrink: 0;
}

/* A badge beside the title (e.g. Buildings' premium state) keeps its own
   size too - see the title's own comment above. */
.sk-drawer-header > .sk-badge {
  flex-shrink: 0;
}

/* Close button to the far end, however long the title runs. flex-shrink: 0
   is load-bearing: without it, a long badge beside the title squeezes the
   button's WIDTH (flex-shrink acts on the main axis; height is untouched)
   below its 32px, and a circle renders as an oval. */
.sk-drawer-header :where(button):last-child {
  flex-shrink: 0;
  margin-left: auto;
}

.sk-drawer-body {
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-base);
  line-height: 1.6;
}

.sk-drawer-body :where(p) {
  margin: 0 0 var(--sk-space-3);
  max-width: 70ch;
}

.sk-drawer-body :where(p):last-child {
  margin-bottom: 0;
}

/* Bottom sheet under 768px. Only the geometry changes — the same element, the
   same open class, the same script. */
@media (max-width: 768px) {
  .sk-drawer-panel {
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    height: auto;
    max-height: 80dvh;
    border-left: 0;
    border-top: 1px solid var(--sk-border);
    border-radius: var(--sk-radius-lg) var(--sk-radius-lg) 0 0;
    transform: translateY(100%);
  }
}

/* A drawer is reference material, so the slide is decoration. Honour the
   setting: it still opens, it just arrives. */
@media (prefers-reduced-motion: reduce) {
  .sk-drawer,
  .sk-drawer-scrim,
  .sk-drawer-panel {
    transition: none;
  }
}

/* Set on <body> while a drawer is open, so the page behind cannot scroll under
   the sheet — the standard mobile annoyance where flicking the drawer scrolls
   the article behind it. */
/* A drawer body filled by ONE scrolling block, rather than a column of prose.

   The panel is already a flex column with a definite height, so the body only
   has to claim the slack and pass it on. min-height: 0 on both is the part that
   is easy to miss: a flex item's minimum is its content, so without it the
   scroller grows to the height of every row it holds and pushes the panel into
   scrolling instead -- which is the nested-scroller bug, arrived at from the
   other direction.

   Use it with sk-table-scroll--fill on the block that should do the scrolling.
   A fixed --sk-table-max-h works too, but it leaves dead space under the table
   whenever the panel is taller than the cap. */
.sk-drawer-body--fill {
  display: flex;
  flex: 1;
  min-height: 0;
  flex-direction: column;
  gap: var(--sk-space-3);
}

.sk-table-scroll--fill {
  flex: 1;
  min-height: 0;
  max-height: none;
}

.sk-drawer-lock {
  overflow: hidden;
}


/* --------------------------------------------------------------------------
   11. MODAL
   --------------------------------------------------------------------------
   Moved out of components/ConfirmModal.ascx's inline <style> block, values
   unchanged, with one deliberate change: every rule is scoped under
   .modal-overlay.

   Why the scoping matters. As an inline block the CSS applied to the whole
   hosting page, and .modal-header / .modal-body / .modal-footer are Bootstrap's
   own classnames — lifting them into a globally-linked stylesheet unscoped
   would silently restyle every Bootstrap modal in the app. Scoped, they reach
   exactly what they reached before: the control's own markup. No markup change
   was needed, because .modal-overlay is already the component's root element.

   These keep their original names rather than sk-* because the markup that uses
   them is not being migrated by this issue. They become sk-modal-* when
   ConfirmModal.ascx itself migrates. */

.modal-overlay {
  position: fixed;
  inset: 0;
  background-color: var(--sk-surface-overlay);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
}

/* Unused today: the control's markup uses .pref-card for the dialog body.
   Ported so the move is provably behaviour-preserving; drop it when the modal
   migrates. */
.modal-overlay .modal-card {
  background-color: #1e1e24;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--sk-radius-lg);
  width: 90%;
  max-width: 420px;
  padding: var(--sk-space-6);
  box-shadow: var(--sk-shadow-lg);
  text-align: center;
  animation: skModalFadeIn 0.2s ease-out;
}

.modal-overlay .modal-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: var(--sk-space-4);
  font-size: 20px;
  font-weight: var(--sk-weight-semibold);
  color: #ffffff;
}

.modal-overlay .warning-icon {
  color: #ff9f43;
}

.modal-overlay .modal-body {
  margin-bottom: var(--sk-space-6);
  font-size: 15px;
  line-height: 1.5;
  color: #b2bec3;
}

.modal-overlay .highlight-text {
  display: block;
  margin-top: var(--sk-space-2);
  color: #ff7675;
  font-weight: bold;
}

.modal-overlay .modal-footer {
  display: flex;
  justify-content: center;
  gap: var(--sk-space-4);
}

.modal-overlay .modal-btn {
  padding: 10px 22px;
  border: none;
  border-radius: var(--sk-radius-sm);
  font-size: 14px;
  font-weight: var(--sk-weight-medium);
  cursor: pointer;
  transition: all var(--sk-transition);
  max-width: 120px !important;
  min-width: auto !important;
}

.modal-overlay .cancel-btn {
  background-color: red !important;
  color: #dfe6e9;
}

.modal-overlay .cancel-btn:hover {
  background-color: #633645ff;
}

.modal-overlay .confirm-btn {
  background-color: #0984e3;
  color: #ffffff;
}

.modal-overlay .confirm-btn:hover {
  background-color: #0b7ad1;
}

@keyframes skModalFadeIn {
  from { transform: scale(0.95); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* ---- sk-modal --------------------------------------------------------------
   The sk- markup for a dialog: scrim, panel, icon, title, body, actions.
   ConfirmModal.ascx keeps .modal-overlay above until it migrates onto this.

   Toggled by one class, .is-open, never display: the closed state is
   visibility: hidden with a delay equal to the fade, so it fades OUT as well
   as in. Colour through --sk-hue on the root (sk-hue-warning, sk-hue-danger).
   Must sit outside every sk-card: position: fixed inside a card is positioned
   against the card. */
.sk-modal {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sk-gutter);
  background: var(--sk-surface-overlay);
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--sk-transition), visibility 0s var(--sk-transition);
}

.sk-modal.is-open {
  visibility: visible;
  opacity: 1;
  transition: opacity var(--sk-transition), visibility 0s;
}

.sk-modal-panel {
  width: 100%;
  max-width: 28rem;
  padding: var(--sk-card-pad);
  background: var(--sk-surface-on-image);
  border: 1px solid var(--sk-border);
  border-top: 3px solid var(--sk-hue-deep, var(--sk-accent));
  border-radius: var(--sk-radius-lg);
  box-shadow: var(--sk-shadow-lg);
  color: var(--sk-text);
  font-family: var(--sk-font);
  text-align: center;
  transform: scale(0.96);
  transition: transform var(--sk-transition);
}

.sk-modal.is-open .sk-modal-panel {
  transform: scale(1);
}

.sk-modal-icon {
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-2xl);
  line-height: 1;
}

.sk-modal-title {
  margin: var(--sk-space-3) 0 var(--sk-space-2);
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-semibold);
  color: var(--sk-text);
}

.sk-modal-body {
  margin: 0 0 var(--sk-space-5);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
  line-height: 1.6;
}

.sk-modal-body :where(strong, b) {
  color: var(--sk-text);
}

.sk-modal-actions {
  display: flex;
  justify-content: center;
  gap: var(--sk-space-3);
}


/* --------------------------------------------------------------------------
   12. VARIANTS
   --------------------------------------------------------------------------
   The base components are deliberately neutral. This section is where a page
   gets its identity, driven by one custom property: --sk-hue. Set it on a card
   and the stripe, the header icon, the glow and any badge inside all follow.
   That is the whole mechanism - there is no per-colour duplication below.

   Taken from the live design rather than invented: Mine Gold already uses a
   4px gold left border (probe-styles.css:368), a gold pill badge (:375) and a
   gold gradient CTA (:422). This generalises those three into variants any
   page can use. */

/* Hue modifiers. Each sets the three values every variant below reads. */
.sk-hue-accent { --sk-hue: var(--sk-accent);  --sk-hue-deep: var(--sk-pal-accent-mid); --sk-hue-grad: var(--sk-grad-accent); --sk-hue-glow: var(--sk-glow-accent); }
.sk-hue-gold   { --sk-hue: var(--sk-gold);    --sk-hue-deep: var(--sk-gold-deep);  --sk-hue-grad: var(--sk-grad-gold);   --sk-hue-glow: var(--sk-glow-gold); }
.sk-hue-ion    { --sk-hue: var(--sk-ion);     --sk-hue-deep: var(--sk-ion-deep);   --sk-hue-grad: var(--sk-grad-ion);    --sk-hue-glow: var(--sk-glow-ion); }
.sk-hue-plasma { --sk-hue: var(--sk-plasma);  --sk-hue-deep: var(--sk-plasma-deep);--sk-hue-grad: var(--sk-grad-plasma); --sk-hue-glow: var(--sk-glow-plasma); }
.sk-hue-toxic  { --sk-hue: var(--sk-toxic);   --sk-hue-deep: var(--sk-toxic-deep); --sk-hue-grad: var(--sk-grad-accent); --sk-hue-glow: var(--sk-glow-accent); }
.sk-hue-alert  { --sk-hue: var(--sk-alert);   --sk-hue-deep: var(--sk-alert-deep); --sk-hue-grad: var(--sk-grad-alert);  --sk-hue-glow: var(--sk-glow-alert); }

/* Status hues, for anything coloured through --sk-hue - sk-pill's border and
   icon, sk-stat-value, sk-card--stripe - that has to say healthy / at the
   limit / wrong rather than which faction it belongs to. Straight onto the
   status tokens, so a theme that moves --sk-success moves these with it.
   --sk-hue-deep follows the hue because the status tokens have no deep
   variant of their own. */
.sk-hue-success { --sk-hue: var(--sk-success); --sk-hue-deep: var(--sk-success); }
.sk-hue-warning { --sk-hue: var(--sk-warning); --sk-hue-deep: var(--sk-warning); }
.sk-hue-danger  { --sk-hue: var(--sk-danger);  --sk-hue-deep: var(--sk-danger); }

/* Concept hues: what a reading is ABOUT, where the faction hues say which
   kind of panel it is and the status hues say how it is doing. Anything that
   shows population, barracks / ASB / ACH space, power, income (money and
   cost), training camps or probes takes its concept's class, on every page,
   so the colour is learned once. A judgement on the figure still goes on the
   figure (sk-num--done / --pending / --alert), never on the hue.
   --sk-hue-deep follows the hue, as the status hues' does. */
.sk-hue-pop      { --sk-hue: var(--sk-res-pop);      --sk-hue-deep: var(--sk-res-pop); }
.sk-hue-barracks { --sk-hue: var(--sk-res-barracks); --sk-hue-deep: var(--sk-res-barracks); }
.sk-hue-asb      { --sk-hue: var(--sk-res-asb);      --sk-hue-deep: var(--sk-res-asb); }
.sk-hue-ach      { --sk-hue: var(--sk-res-ach);      --sk-hue-deep: var(--sk-res-ach); }
.sk-hue-power    { --sk-hue: var(--sk-res-power);    --sk-hue-deep: var(--sk-res-power); }
.sk-hue-income   { --sk-hue: var(--sk-res-income);   --sk-hue-deep: var(--sk-res-income); }
.sk-hue-training { --sk-hue: var(--sk-res-training); --sk-hue-deep: var(--sk-res-training); }
.sk-hue-probes   { --sk-hue: var(--sk-res-probes);   --sk-hue-deep: var(--sk-res-probes); }

/* Subject, relation and unit hues: the same mechanism for the rest of what a
   reading can be about. One class per meaning even where two meanings share
   a colour today, so a page never has to change when they are split. */
.sk-hue-land           { --sk-hue: var(--sk-res-land);           --sk-hue-deep: var(--sk-res-land); }
.sk-hue-missile        { --sk-hue: var(--sk-res-missile);        --sk-hue-deep: var(--sk-res-missile); }
.sk-hue-networth       { --sk-hue: var(--sk-res-networth);       --sk-hue-deep: var(--sk-res-networth); }
.sk-hue-time           { --sk-hue: var(--sk-subject-time);       --sk-hue-deep: var(--sk-subject-time); }
.sk-hue-research       { --sk-hue: var(--sk-subject-research);   --sk-hue-deep: var(--sk-subject-research); }
.sk-hue-offense        { --sk-hue: var(--sk-subject-offense);    --sk-hue-deep: var(--sk-subject-offense); }
.sk-hue-defense        { --sk-hue: var(--sk-subject-defense);    --sk-hue-deep: var(--sk-subject-defense); }
.sk-hue-military       { --sk-hue: var(--sk-subject-military);   --sk-hue-deep: var(--sk-subject-military); }
.sk-hue-modules        { --sk-hue: var(--sk-subject-modules);    --sk-hue-deep: var(--sk-subject-modules); }
.sk-hue-rel-war        { --sk-hue: var(--sk-rel-war);            --sk-hue-deep: var(--sk-rel-war); }
.sk-hue-rel-barricade  { --sk-hue: var(--sk-rel-barricade);      --sk-hue-deep: var(--sk-rel-barricade); }
.sk-hue-rel-nap        { --sk-hue: var(--sk-rel-nap);            --sk-hue-deep: var(--sk-rel-nap); }
.sk-hue-rel-ally       { --sk-hue: var(--sk-rel-ally);           --sk-hue-deep: var(--sk-rel-ally); }
.sk-hue-unit-soldiers  { --sk-hue: var(--sk-unit-soldiers);      --sk-hue-deep: var(--sk-unit-soldiers); }
.sk-hue-unit-defensive { --sk-hue: var(--sk-unit-defensive);     --sk-hue-deep: var(--sk-unit-defensive); }
.sk-hue-unit-hybrid    { --sk-hue: var(--sk-unit-hybrid);        --sk-hue-deep: var(--sk-unit-hybrid); }
.sk-hue-unit-offensive { --sk-hue: var(--sk-unit-offensive);     --sk-hue-deep: var(--sk-unit-offensive); }
.sk-hue-unit-growth    { --sk-hue: var(--sk-unit-growth);        --sk-hue-deep: var(--sk-unit-growth); }

/* A glow and a gradient in the hue's own colour, for the selected chip, CTA,
   emblem and glow card that read --sk-hue-glow / --sk-hue-grad. Without them
   they fell back to the accent's blue. Declared on the hue element, so
   var(--sk-hue) is that element's own. */
:is(.sk-hue-pop, .sk-hue-barracks, .sk-hue-asb, .sk-hue-ach, .sk-hue-power, .sk-hue-income, .sk-hue-training, .sk-hue-probes,
  .sk-hue-land, .sk-hue-missile, .sk-hue-networth, .sk-hue-time, .sk-hue-research, .sk-hue-offense, .sk-hue-defense, .sk-hue-military, .sk-hue-modules,
  .sk-hue-rel-war, .sk-hue-rel-barricade, .sk-hue-rel-nap, .sk-hue-rel-ally,
  .sk-hue-unit-soldiers, .sk-hue-unit-defensive, .sk-hue-unit-hybrid, .sk-hue-unit-offensive, .sk-hue-unit-growth) {
  --sk-hue-glow: 0 4px 14px color-mix(in srgb, var(--sk-hue) 30%, transparent);
  /* A selected chip or a CTA fills with this under white text. The hue mixed
     into the page surface, 45% then 25%: white clears 4.5:1 on the light end
     for every concept and subject hue (yellow, the lightest, at 4.8:1). */
  --sk-hue-grad: linear-gradient(135deg,
    color-mix(in srgb, var(--sk-hue) 45%, var(--sk-surface-page)) 0%,
    color-mix(in srgb, var(--sk-hue) 25%, var(--sk-surface-page)) 100%);
}

/* general.css paints a handful of glyphs one fixed colour wherever they appear
   (body .fa-bolt, .fa-coins, .fa-user-astronaut, .fa-globe ... at (0,1,1)),
   which would leave a Power pill with an olive bolt inside a purple border.
   Inside a concept or subject hue the icon is the hue's. (0,1,2), so it
   does not depend on this sheet loading after that one. */
body :is(.sk-hue-pop, .sk-hue-barracks, .sk-hue-asb, .sk-hue-ach, .sk-hue-power, .sk-hue-income, .sk-hue-training, .sk-hue-probes,
  .sk-hue-land, .sk-hue-missile, .sk-hue-networth, .sk-hue-time, .sk-hue-research, .sk-hue-offense, .sk-hue-defense, .sk-hue-military, .sk-hue-modules,
  .sk-hue-rel-war, .sk-hue-rel-barricade, .sk-hue-rel-nap, .sk-hue-rel-ally,
  .sk-hue-unit-soldiers, .sk-hue-unit-defensive, .sk-hue-unit-hybrid, .sk-hue-unit-offensive, .sk-hue-unit-growth) i {
  color: var(--sk-hue);
}

/* A card standing on its own in a page-width column, rather than beside another
   one in an sk-split or a grid cell.

   Without this it stretches to the full page measure - up to --sk-page-max,
   1400px - and a form of label/control rows becomes a run of very long lines
   with the control marooned at the far right. This is the same argument as the
   70ch cap on body copy, applied to a form: the constraint is the reading
   distance between a label and the thing it labels.

   The width is a token so a page can widen it for a card that genuinely holds a
   table, without every solo card in the app moving with it. */
.sk-card--solo {
  max-width: var(--sk-card-solo-max, 720px);
  margin-inline: auto;
}

/* Card carrying a hue stripe down its leading edge. */
.sk-card--stripe {
  border-left: 4px solid var(--sk-hue-deep, var(--sk-accent));
}

/* The header icon adopts the hue, which is what makes a panel readable at a
   glance without reading the heading. */
.sk-card--stripe .sk-card-header :where(i) {
  color: var(--sk-hue, var(--sk-accent));
}

/* Hairline of hue along the top edge - quieter than a stripe, for a panel that
   should be marked but not shouted. */
.sk-card--edge {
  border-top: 1px solid var(--sk-hue-deep, var(--sk-accent));
  box-shadow: var(--sk-shadow), inset 0 1px 0 color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 40%, transparent);
}

/* For the one panel on a page that matters most. Used sparingly - if two cards
   glow, neither does. */
.sk-card--glow {
  border-color: var(--sk-hue-deep, var(--sk-accent));
  box-shadow: var(--sk-shadow), var(--sk-hue-glow, var(--sk-glow-accent));
}

/* Self-promotion banner: icon medallion, pitch, one CTA. Pair with sk-card,
   sk-card--glow and a sk-hue-*; the hue washes in from the leading edge. Text
   takes the room, the CTA never shrinks; under 768px the CTA drops to its own
   full-width row. */
.sk-promo {
  display: flex;
  align-items: center;
  gap: var(--sk-space-5);
  background-image: linear-gradient(110deg,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 28%, transparent) 0%,
      transparent 65%);
}

.sk-promo-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--sk-radius-lg);
  border: 1px solid var(--sk-hue-deep, var(--sk-accent));
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 22%, transparent);
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-2xl);
}

.sk-promo-body {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-promo-title {
  margin: 0 0 var(--sk-space-1);
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-bold);
}

.sk-promo-text {
  margin: 0;
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
}

.sk-promo .sk-btn {
  flex: none;
  white-space: nowrap;
}

@media (max-width: 768px) {
  .sk-promo {
    flex-wrap: wrap;
  }

  .sk-promo .sk-btn {
    width: 100%;
    justify-content: center;
  }
}

/* Header sits on a hue wash instead of a plain rule - for a card whose header
   is a section title rather than a label. */
.sk-card--header-fill .sk-card-header {
  margin: calc(var(--sk-card-pad) * -1) calc(var(--sk-card-pad) * -1) var(--sk-space-3);
  padding: var(--sk-space-3) var(--sk-card-pad);
  border-bottom: 1px solid var(--sk-hue-deep, var(--sk-accent));
  border-radius: var(--sk-radius-lg) var(--sk-radius-lg) 0 0;
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 22%, transparent) 0%,
      transparent 100%);
}

@media (min-width: 768.02px) {
  .sk-card--header-fill .sk-card-header {
    margin-bottom: var(--sk-space-5);
  }
}

/* A small utility card - a quick summary, a status readout - that should not
   claim as much vertical room as a card anchoring a page. Not
   sk-density-compact: that class is for a subtree of many rows and
   deliberately never touches type size, but a full sk-text-lg heading over
   two or three lines of content is disproportionate on its own, so this
   variant drops the heading a size too. Body copy is untouched - the same
   "density comes out of the whitespace, never legibility" rule, just applied
   to a card instead of a table. */
.sk-card--compact {
  padding: var(--sk-space-3);
  margin-bottom: var(--sk-space-3);
}

@media (min-width: 768.02px) {
  .sk-card--compact {
    padding: var(--sk-space-3) var(--sk-space-4);
    margin-bottom: var(--sk-space-4);
  }
}

.sk-card--compact .sk-card-header {
  margin-bottom: var(--sk-space-3);
  gap: var(--sk-space-2);
}

.sk-card--compact .sk-card-header :where(h1, h2, h3, h4, h5, h6) {
  font-size: var(--sk-text-sm);
}

.sk-card--compact .sk-card-header :where(i) {
  font-size: var(--sk-text-base);
}

/* A compact card holding ONE headline reading with its icon in a leading
   slot - for a row of several readings across a page, laid on equal tracks
   with sk-pill-row sk-pill-row--even, where a full card's padding would crowd
   a quarter of the content width.

   Everything steps down from a lone headline stat: tighter padding, the
   label without its tracking (so it may wrap between words, never inside
   one - "UNDER CONST / R."), and the figure at --sk-text-lg on one line,
   because "16 / ticks" is two readings. align-self: stretch so every tile in
   the row shares a height and the stripes all run the same length;
   min-width: 0 so a long label cannot widen its track. */
.sk-card--tile {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--sk-space-2);
  min-width: 0;
  padding: var(--sk-space-2) var(--sk-space-3);
}

.sk-card--tile .sk-stat {
  min-width: 0;
}

/* A tile's one action, at its far end: an icon button that acts on the reading
   (Military's Barracks space fills the Soldiers field with the free space).
   sk-btn sk-btn--icon sk-btn--icon-sm sk-card-tile-action. */
.sk-card--tile > .sk-card-tile-action {
  flex: none;
  margin-left: auto;
}

/* A mini-tile: the same tile with its reading on ONE line - icon, label, figure - held together
   in the middle. About two thirds of a tile's height, for a row of readings that should outweigh
   a strip of pills without costing a row of full tiles, and that should not be mistaken for the
   headline tiles it sits under.

   Centred rather than label-left / figure-right, because a mini-tile is usually wider than its
   reading: pinned to the edges, a label and its own figure end up a couple of hundred pixels
   apart. Centred, the leftover width is padding on both sides.

   The modifier lays out its own sk-stat, so the markup is a plain tile plus this one class. Rows
   of them go in sk-pill-row--mini. */
.sk-card--tile-mini {
  justify-content: center;
}

.sk-card--tile-mini > .sk-stat {
  flex-direction: row;
  align-items: baseline;
  gap: var(--sk-space-2);
}

.sk-card--tile-mini .sk-stat-value {
  flex: none;
}

/* A tile whose reading is several figures rather than one: the stat takes the tile's remaining
   width. */
.sk-card--tile .sk-stat--fill {
  flex: 1 1 auto;
}

/* sk-stat-row: several readings side by side in ONE tile - Pure / Flex / Total - each a small
   label over its figure, on equal columns, no table chrome. Three figures cost one line of height
   instead of three. sk-stat-title names the whole group above it, in the hue, so it does not read
   as a fourth column label. */
.sk-stat-row {
  display: flex;
  gap: var(--sk-space-3);
}

.sk-stat-row > .sk-stat {
  flex: 1 1 0;
  min-width: 0;
}

.sk-stat-title {
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-bold);
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--sk-hue, var(--sk-text));
}

.sk-card--tile .sk-stat-label {
  letter-spacing: 0;
}

.sk-card--tile .sk-stat-value {
  font-size: var(--sk-text-lg);
  white-space: nowrap;
}

/* An sk-stat-row as a tile's body takes the tile's width, so its last
   reading can sit at the far edge (sk-stat--end below). */
.sk-card--tile > .sk-stat-row {
  flex: 1 1 auto;
  min-width: 0;
}

/* A reading pinned to the trailing edge of its row: the qualifier beside a
   headline figure - Attack's "Multiplier +55%" opposite "Offense 1,204,330".
   Label and figure both right-aligned, so the pair reads as one column. */
.sk-stat--end {
  align-items: flex-end;
  text-align: right;
}

/* Peers, centred, a hairline between them: two or three figures of the same
   weight that are read against each other (Attack's Total defense | Defense
   remaining). The hairline is decoration, so --sk-border-subtle.

   The figure may wrap here, unlike a lone tile's: it is a half of the tile at
   most, and a figure with its sk-num-sub share ("151,863 (100%)") in half of a
   phone tile is about the width there is - the share drops under the figure
   rather than running out of the box. Declared after the tile's nowrap so the
   (0,2,0) tie goes this way. */
.sk-stat-row--center > .sk-stat {
  align-items: center;
  text-align: center;
}

.sk-stat-row--center > .sk-stat + .sk-stat {
  padding-left: var(--sk-space-3);
  border-left: 1px solid var(--sk-border-subtle);
}

.sk-stat-row--center .sk-stat-value {
  white-space: normal;
}

/* A tile drawn as a dashed box with no shadow: a panel of supporting figures
   sitting under the headline tiles it qualifies, which should read as part of
   the same readout but not as a third headline (Attack's Summary). */
.sk-card--dashed {
  border-style: dashed;
  box-shadow: none;
}

/* The tile's leading icon, in the card's hue. A fixed-width slot (Font
   Awesome's own fa-fw measure) so a narrow glyph and a wide one start the
   text at the same place. */
.sk-stat-icon {
  flex: none;
  width: 1.25em;
  text-align: center;
  font-size: var(--sk-text-xl);
  color: var(--sk-hue, var(--sk-accent));
}

/* ---- Primary call to action ------------------------------------------------
   Filled and glowing, versus the outline .sk-btn. One per view: the button the
   page exists for. Gradient + hue glow is the live Mine Gold button's language,
   generalised. */
.sk-btn--cta {
  position: relative;
  overflow: hidden;
  padding: 10px 28px;
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  border: none;
  color: #fff;
  font-size: var(--sk-text-sm);
  font-weight: var(--sk-weight-bold);
  letter-spacing: 0.3px;
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent));
}

/* A sheen that crosses the face on hover. It replaces the old brightness jump
   with something that reads as deliberate, and it animates transform only -
   no layout, no repaint of the gradient, so it stays smooth. */
.sk-btn--cta::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 45%;
  background: linear-gradient(100deg,
      transparent 0%,
      rgba(255, 255, 255, 0.28) 50%,
      transparent 100%);
  transform: skewX(-18deg) translateX(0);
  transition: transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
  pointer-events: none;
}

/* THE FIX: re-declare the gradient. .sk-btn:hover sets a flat color-mix
   background at the same specificity, so without this the CTA lost its
   gradient the instant the cursor arrived - the "flash" that looked like a
   missing transition but was actually the background being swapped out. */
.sk-btn--cta:hover {
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  color: #fff;
  border: none;
  filter: brightness(1.06);
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent)), 0 6px 20px rgba(0, 0, 0, 0.35);
}

.sk-btn--cta:hover::after {
  transform: skewX(-18deg) translateX(320%);
}

/* Pressed: settle back down rather than staying lifted. */
.sk-btn--cta:active {
  transform: translateY(0);
  filter: brightness(0.97);
}

/* A CTA's icon is its label's white. The card header paints every icon in it
   with the accent (.sk-card-header :where(i), and the stripe's hue at (0,2,0)),
   which on a filled hue gradient left Attack's Simulate with a cyan flask next
   to a white word. (0,2,0) and after those rules, so it wins in any header. */
.sk-btn.sk-btn--cta :where(i) {
  color: inherit;
}

/* Full-width button. Mirrors .sk-input--full. Order matters: this must sit
   after .sk-btn's `width: auto`, which has the same specificity. */
.sk-btn--full {
  width: 100%;
}

/* Full width on a phone only - a button easy to miss beside its neighbours at thumb size gets a
   row of its own there, and keeps its natural width on a desktop. Element-qualified for the same
   reason as sk-btn--full's own button rule: button.sk-btn sets width at (0,1,1). In a wrapping
   sk-row it simply takes the next line. */
@media (max-width: 768px) {
  .sk-btn--full-mobile,
  button.sk-btn--full-mobile,
  input[type="button"].sk-btn--full-mobile,
  input[type="submit"].sk-btn--full-mobile {
    width: 100%;
    justify-content: center;
  }
}

/* The row a form's primary action sits in, centred under the content it acts
   on. gap costs nothing on the single-button callers - only Buildings'
   settings sheet currently puts a secondary action (Defaults) beside the
   primary one. */
.sk-actions {
  display: flex;
  justify-content: center;
  gap: var(--sk-space-3);
  margin-block: var(--sk-space-3);
}

@media (min-width: 768.02px) {
  .sk-actions {
    margin-block: var(--sk-space-4);
  }
}

/* The primary action with a one-line note centred under it - a condition the
   action is about to be taken under ("Powerless is ON" under Attack). Same
   centred row, turned into a column; the note is a sibling of the button (or
   of its sk-emblem), not inside it. */
.sk-actions--stack {
  flex-direction: column;
  align-items: center;
  gap: var(--sk-space-1);
}

/* The page's committing action with more air above and below it, so it stands
   apart from the cards around it (Attack's ATTACK - the owner, 2026-10-19). */
.sk-actions--roomy {
  margin-top: calc(var(--sk-space-6) + var(--sk-space-2));
  margin-bottom: calc(var(--sk-space-6) + var(--sk-space-2));
}

/* A primary action with a real minimum width. Not sk-btn--full on a desktop:
   a CTA gradient across a 1,400px measure is the page's background, not a
   button - a primary action reads as primary because it is bigger than its
   neighbours, not because it is as wide as the document. Full width comes
   back on a phone, where a thumb-wide target is the right shape.

   Qualified with the element as well as the class, because what it overrides
   is: input[type="submit"].sk-btn zeroes min-width at (0,2,1), and a bare
   .sk-btn--wide would lose to it. The <a> and <button> forms are listed so the
   class works whatever element carries it. */
input[type="submit"].sk-btn--wide,
input[type="button"].sk-btn--wide,
button.sk-btn--wide,
a.sk-btn--wide {
  min-width: 260px;
}

@media (max-width: 768px) {
  input[type="submit"].sk-btn--wide,
  input[type="button"].sk-btn--wide,
  button.sk-btn--wide,
  a.sk-btn--wide {
    width: 100%;
  }
}

/* ==========================================================================
   4c. EMBLEM BUTTON, AND THE THEME SWITCH
   --------------------------------------------------------------------------
   The page's committing action - Build, Train, Launch, Attack - written once as
   markup and rendered two ways, so the app's whole primary-action look is one
   attribute and one block of values rather than a rewrite of every page.

     BASE THEME (the default). The wrapper, the disc and the plate are inert.
     The button is whatever its own classes say, which for a primary action is
     `sk-btn sk-btn--cta` - exactly the button this app ships today. Nothing is
     reimplemented here, so the base theme cannot drift from the real one.

     EMBLEM THEME. A bevelled medallion carrying the action's icon, overlapping
     the left end of a cut-cornered plate that holds the word, edge and glow
     both hue-driven.

   THE SWITCH is one attribute, on <html> in Auth.Master:

     <html data-sk-theme="emblem">     turn it on
     <html>                            base theme

   It is an ancestor selector, so it also works on any subtree - put it on one
   card to preview a theme without touching the rest of the page.

   THE VALUES are the --sk-emblem-* properties declared on the theme root below.
   They inherit, so changing the metal, the corner cut, the glow or the type for
   every primary action in the app is ONE value in ONE place; overriding one on
   a single .sk-emblem changes only that button. They are a component API, not
   palette tokens - no colour literal is introduced by any of them, every value
   is built from --sk-hue-deep and the neutral ramp, so a hue change and a theme
   change stay independent of each other.

   THE MARKUP is the same either way, and it is four elements:

     <span class="sk-emblem sk-hue-ion">
       <span class="sk-emblem-disc"><i class="fas fa-hammer"></i></span>
       <span class="sk-emblem-plate">
         <asp:Button runat="server" CssClass="sk-btn sk-btn--cta sk-btn--emblem" Text="Build" />
       </span>
     </span>

   WHY FOUR, when every other button in this file is one. <asp:Button> renders
   <input type="submit">, and an <input> can carry neither ::before nor ::after
   - the same limit that leaves an <asp:Button> CTA without its sheen. The
   emblem theme needs two things a lone input cannot give:

     1. A glowing edge tracing the cut corners. clip-path replaces the border
        box, so `border` is clipped away along every diagonal and an inset
        box-shadow is cut at the same places. The only way to trace a clipped
        silhouette is two stacked clips - an outer shape in the edge colour, an
        inner shape one edge-width in carrying the face. That is
        sk-emblem-plate and the button itself.
     2. A disc overhanging the plate's left edge. It cannot live inside the
        plate, which is clipped, so it needs an unclipped positioned ancestor:
        sk-emblem.

   So the markup is the shape, and it is identical for <input type="submit">,
   <button> and <a> - one recipe, whichever element a page happens to have. In
   the base theme those three extra elements cost a wrapper and two spans that
   render as nothing, which is the price of the switch being free.

   ONE PER PAGE. A form with three of these has three primary actions, which
   means it has none. Everything else stays sk-btn / sk-btn--cta, with no
   wrapper at all.
   ========================================================================== */

/* ---- base theme: a wrapper that does nothing -------------------------------
   No padding, no background, no clip. The button inside is the whole control,
   and .sk-btn--cta above has already styled it. */

.sk-emblem {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
}

.sk-emblem-plate {
  display: inline-flex;
  box-sizing: border-box;
}

/* Not `visibility` and not opacity: in the base theme the medallion is not a
   dimmed thing, it is not part of the design at all, and a hidden box that
   still takes a line box would push the button off centre inside sk-actions. */
.sk-emblem-disc {
  display: none;
}


/* ==========================================================================
   THE EMBLEM THEME
   --------------------------------------------------------------------------
   Everything below is scoped to the theme attribute, so the base theme cannot
   be affected by a change made here, and a third theme is another block of the
   same shape rather than an edit to this one.
   ========================================================================== */

/* ---- the values. Change one, change every primary action in the app. -------
   DECLARED ON THE ASSEMBLY, NOT ON THE THEME ROOT, and that is not a style
   choice. `--a: var(--b)` substitutes --b at the element that DECLARES --a and
   inherits the RESULT, not the expression. Composed on <html> these gradients
   would resolve --sk-hue-deep exactly once, against nothing, bake in the
   --sk-accent fallback and inherit that blue into every emblem on the site -
   an sk-hue-gold card below would have no effect whatever. Seen firsthand:
   Build and Mine Gold both came out accent blue. Declared here, on the same
   element that carries sk-hue-*, each assembly resolves its own.

   The same trap applies to --sk-emblem-clip, built from --sk-emblem-cut:
   resolve it higher up and a per-button cut override is read too late to
   reach the polygon.

   It is still one place and one value - this block IS the theme. */

[data-sk-theme="emblem"] .sk-emblem,
[data-sk-theme="emblem"] .sk-btn--emblem-match {
  /* Shape. --overhang is how far the medallion hangs off the plate's left edge;
     --pad-left has to clear what is left of it, which is
     (disc-size - overhang) plus a gap.

     THE PLATE'S SILHOUETTE IS ONE VALUE. --sk-emblem-radius rounds it;
     --sk-emblem-clip cuts it. Set one and zero the other - they are not meant
     to be combined, because clip-path replaces the border box and a radius
     underneath it is simply not drawn.

       rounded (default)  radius: 10px   clip: none
       angular            radius: 0      clip: var(--sk-emblem-clip-hex)

     Rounded is the default because this is a component of THIS system, and
     everything else in it is rounded - sk-card at 12px, sk-btn at 6px, every
     pill at 999px. The cut plate came from a reference image that had no design
     system around it, and on a page of rounded cards it read as imported from
     somewhere else. The medallion, the lit edge, the metal and the glow are
     what make this an emblem; the corners never were.

     --cut is the horizontal run of each corner cut, shared by the plate and the
     face so the two clips stay parallel. It is inert while --clip is none. */
  --sk-emblem-radius: 10px;
  --sk-emblem-clip: none;
  --sk-emblem-cut: 13px;
  --sk-emblem-disc-size: 44px;
  --sk-emblem-overhang: 18px;
  --sk-emblem-edge-width: 2px;
  --sk-emblem-min-width: 175px;
  --sk-emblem-pad-y: 9px;
  --sk-emblem-pad-right: 24px;
  --sk-emblem-pad-left: 37px;

  /* Surfaces. Every one is derived from --sk-hue-deep or the neutral ramp, so
     an emblem in a gold card is gold without anything here naming gold. */
  --sk-emblem-edge: linear-gradient(180deg,
      var(--sk-hue-deep, var(--sk-accent)) 0%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 55%, var(--sk-surface-page)) 50%,
      var(--sk-hue-deep, var(--sk-accent)) 100%);

  /* Plate steel: lit at the top, a sheen through the middle, dark at the foot,
     with the hue bleeding out from under the medallion on top of it. */
  --sk-emblem-face:
      linear-gradient(90deg,
          color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 22%, transparent) 0%,
          transparent 55%),
      linear-gradient(180deg,
          var(--sk-neutral-2) 0%,
          var(--sk-neutral-1) 22%,
          var(--sk-surface-page) 52%,
          var(--sk-surface-on-image-solid) 82%,
          var(--sk-neutral-1) 100%);

  /* The medallion's collar: brushed metal, lit from the top left, shadowed
     through the middle, catching the light again at the bottom right. */
  --sk-emblem-rim: linear-gradient(145deg,
      var(--sk-neutral-4) 0%,
      var(--sk-neutral-3) 18%,
      var(--sk-neutral-1) 46%,
      var(--sk-surface-page) 62%,
      var(--sk-neutral-3) 100%);

  /* The face the collar surrounds, tinted by the action's own hue so the
     medallion belongs to the button rather than sitting on it as a grey disc -
     the launch key's red dome, generalised.

     DARK-tinted, not saturated. --sk-hue-deep is #eab308 for gold and #22d3ee
     for ion; a dome painted in those needs a dark glyph, and every other icon
     in this system is light. Mixing the hue INTO the dark keeps one icon colour
     working across all five hues. The launch key overrides this with a properly
     saturated red cap, which is the point of it being the exception. */
  --sk-emblem-cap: radial-gradient(circle at 34% 24%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 52%, var(--sk-neutral-1)) 0%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 34%, var(--sk-surface-on-image-solid)) 58%,
      var(--sk-surface-on-image-solid) 100%);

  /* Silver, not the hue: the hue is already saying what kind of action this is
     along the whole edge, and a hue-coloured glyph on a hue-coloured rim leaves
     the icon nothing of its own to read as. */
  --sk-emblem-icon: var(--sk-neutral-5);

  /* drop-shadow, not box-shadow - see the plate rule. Two passes: a tight one
     for the lit edge, a wide one for the bloom.

     Both stops are color-mix() on purpose, even the 100% one. Chrome
     interpolates the 0.2s hover transition in whatever colour SPACE each end
     is expressed in, and a color-mix(in srgb, ...) result and a plain var()
     are not the same space as far as the animation is concerned - the
     in-between frames came out as clipped neon green (sampled as
     oklab(1 72.9 149.9 / ...) partway through) for about 230ms before
     settling on the right blue, on every emblem button on the site.
     color-mix() at both ends keeps the whole transition in one space. */
  --sk-emblem-glow:
      drop-shadow(0 0 5px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 70%, transparent))
      drop-shadow(0 0 18px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 45%, transparent));
  --sk-emblem-glow-hover:
      drop-shadow(0 0 7px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 100%, transparent))
      drop-shadow(0 0 26px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 60%, transparent));

  /* Type. */
  --sk-emblem-font: var(--sk-font-heading);
  /* Orbitron runs large for its point size and the tracking widens it further,
     so the emblem sits at sk-btn--cta's OWN --sk-text-sm and still reads as the
     bigger control - the medallion and the plate are what make it primary, not
     the type. A step up from here and the button outweighs the rows of settings
     it commits, which is what it was doing on AFKBuilder. */
  --sk-emblem-size: var(--sk-text-sm);
  --sk-emblem-tracking: 1.4px;

  /* The glyph inside the medallion. A value rather than a literal because it
     has to move whenever --sk-emblem-disc-size does: it was --sk-text-2xl,
     which filled a 54px dome and swamps a 44px one. */
  --sk-emblem-icon-size: var(--sk-text-lg);
}

/* The shape, written once and used by both clipped layers. A custom property
   holding a polygon() resolves its percentages against whichever element
   substitutes it, which is exactly what is wanted here: the plate and the face
   are different sizes and each needs its own corners. */
[data-sk-theme="emblem"] .sk-emblem {
  --sk-emblem-clip-hex: polygon(
      var(--sk-emblem-cut) 0,
      calc(100% - var(--sk-emblem-cut)) 0,
      100% var(--sk-emblem-cut),
      100% calc(100% - var(--sk-emblem-cut)),
      calc(100% - var(--sk-emblem-cut)) 100%,
      var(--sk-emblem-cut) 100%,
      0 calc(100% - var(--sk-emblem-cut)),
      0 var(--sk-emblem-cut));
}

/* ---- the assembly ---- */

[data-sk-theme="emblem"] .sk-emblem {
  position: relative;
  /* The overhang padding is part of the width, not added to it. As a flex item
     the shrink would absorb the difference anyway; in a block context nothing
     would, and the assembly would hang past its column. */
  padding-left: var(--sk-emblem-overhang);
  /* The lift lives here rather than on the button: .sk-btn:hover raises the
     button 1px, and a button rising inside a plate that stays put tears the
     edge open along the bottom. The whole assembly moves as one piece. */
  transition: transform var(--sk-transition);
}

[data-sk-theme="emblem"] .sk-emblem:hover {
  transform: translateY(-1px);
}

/* clip-path clips the outline too, so a focus ring drawn on the button is
   invisible - cut away at exactly the edge it needs to sit outside of. The ring
   goes on the unclipped wrapper instead, and the button's own is suppressed
   below. Base theme needs none of this: nothing there is clipped. */
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem:focus-visible) {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 4px;
  border-radius: var(--sk-radius-sm);
}

[data-sk-theme="emblem"] .sk-btn--emblem:focus-visible {
  outline: none;
}

/* ---- the medallion ---- */

[data-sk-theme="emblem"] .sk-emblem-disc {
  position: absolute;
  left: 0;
  top: 50%;
  /* Above the plate, which carries a filter and so opens its own stacking
     context. Siblings, so this is the only ordering needed. */
  z-index: 2;
  display: flex;
  width: var(--sk-emblem-disc-size);
  height: var(--sk-emblem-disc-size);
  /* The collar's thickness. The <i> inside is the face it surrounds. */
  padding: 5px;
  border-radius: 50%;
  /* The collar is NOT this element's background. It is ::before, so it can turn
     on hover without taking the glyph inside it round with it - a rotate on
     the disc itself would spin the icon too. */
  box-shadow: var(--sk-shadow-lg);
  /* Scaled up from the collar's own box (--sk-emblem-disc-size) rather than
     raising that variable: the disc is position:absolute inside .sk-emblem
     (position:relative), so transform is purely visual here - it was already
     true that growing the disc could never push the plate or reflow the row,
     scale() or not. Resting state is what the hover bump used to be; hover
     now goes further still. */
  transform: translate(var(--sk-emblem-drag, 0px), -50%) scale(1.15);
  /* The base transform already sets translateY - the hover rule below has to
     restate it alongside scale(), or the hover state would drop the -50%
     centring and the disc would jump down by half its own height. */
  transition: transform var(--sk-transition);
  /* Decoration, not a target. Without this the two thirds of the medallion
     lying over the plate would swallow clicks meant for the button. The sliver
     hanging off the left is not a target either - the word and the whole plate
     are. */
  pointer-events: none;
}

/* The collar: brushed metal lit from one corner, so turning it sweeps the
   light round the ring. */
[data-sk-theme="emblem"] .sk-emblem-disc::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--sk-emblem-rim);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sk-text) 16%, transparent);
  transition: transform 0.6s ease;
}

[data-sk-theme="emblem"] .sk-emblem:hover .sk-emblem-disc::before {
  transform: rotate(360deg);
}

@media (prefers-reduced-motion: reduce) {
  [data-sk-theme="emblem"] .sk-emblem:hover .sk-emblem-disc::before {
    transform: none;
  }
}

/* A hover cue on the medallion itself, on top of the plate's existing glow
   and the assembly's own lift. z-index above the plate (already true at rest)
   keeps the enlarged disc from being cut off by the plate's edge as it grows
   past the collar's resting footprint. */
[data-sk-theme="emblem"] .sk-emblem:hover .sk-emblem-disc {
  transform: translate(var(--sk-emblem-drag, 0px), -50%) scale(1.32);
}

/* Slide to commit (js/sk-emblem-slide.js). The medallion can be dragged across
   the plate; the script writes how far in --sk-emblem-drag and toggles
   .is-dragging while a finger or the mouse holds it. The transform above already
   carries the offset, so at rest (no property set) it is 0px and nothing about
   the resting or hover look changes - and on release short of the end the
   ordinary transition brings it home.

   pan-y: the page keeps scrolling vertically when a touch starts on the button,
   while a horizontal drag is handed to the script instead of being taken as a
   pan. Without it the browser cancels the pointer the moment the finger moves
   sideways. No transition while held, or the medallion trails the finger. */
[data-sk-theme="emblem"] .sk-emblem {
  touch-action: pan-y;
}

[data-sk-theme="emblem"] .sk-emblem.is-dragging {
  -webkit-user-select: none;
  user-select: none;
}

[data-sk-theme="emblem"] .sk-emblem.is-dragging .sk-emblem-disc {
  transition: none;
}

/* Held at the end: letting go here commits. The medallion lights in the hue and
   the plate takes the hover glow, so "you can release now" reads without text. */
[data-sk-theme="emblem"] .sk-emblem.is-armed .sk-emblem-disc {
  box-shadow:
    var(--sk-shadow-lg),
    0 0 14px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 75%, transparent);
}

[data-sk-theme="emblem"] .sk-emblem.is-armed .sk-emblem-plate {
  filter: var(--sk-emblem-glow-hover);
}

/* The affordance. A phone has no hover and the medallion is not obviously a
   handle, so the plate carries a "»" at its far end, in the room the face's
   right padding already reserves, nudging the way the medallion goes. It is the
   spot the medallion stops at. Plain U+00BB, not an icon-font glyph, so it needs
   no font loaded. Above the button (pointer-events: none, so the click still
   reaches it) and inside the plate, so the clip trims it with everything else.
   Off while dragging, and on a disabled button, which cannot slide. */
[data-sk-theme="emblem"] .sk-emblem-plate {
  position: relative;
}

[data-sk-theme="emblem"] .sk-emblem-plate::after {
  content: "\00bb";
  position: absolute;
  top: 50%;
  right: 8px;
  z-index: 1;
  transform: translateY(-50%);
  color: var(--sk-text-on-accent);
  font-size: var(--sk-text-lg, 1.1rem);
  font-weight: var(--sk-weight-bold, 700);
  line-height: 1;
  opacity: 0.6;
  pointer-events: none;
  animation: skEmblemNudge 1.6s ease-in-out infinite;
}

[data-sk-theme="emblem"] .sk-emblem.is-dragging .sk-emblem-plate::after,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem:disabled) .sk-emblem-plate::after,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem[disabled]) .sk-emblem-plate::after,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.aspNetDisabled) .sk-emblem-plate::after,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.is-inert) .sk-emblem-plate::after {
  display: none;
}

@keyframes skEmblemNudge {
  0%,
  100% {
    transform: translate(0, -50%);
    opacity: 0.35;
  }
  50% {
    transform: translate(3px, -50%);
    opacity: 0.85;
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-sk-theme="emblem"] .sk-emblem-plate::after {
    animation: none;
  }
}

[data-sk-theme="emblem"] .sk-emblem-disc > :where(i, img) {
  /* Above the collar, which is the disc's ::before and turns on hover. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: 50%;
  background: var(--sk-emblem-cap);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.7);
  font-size: var(--sk-emblem-icon-size);
  color: var(--sk-emblem-icon);
  text-shadow: 0 0 9px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 45%, transparent);
}

/* A picture in the medallion instead of a glyph.

     <span class="sk-emblem-disc"><img src="img/buttons/build-icon.webp" alt=""></span>

   Same collar, same dome - the art simply replaces the letterform, which is why
   `i` and `img` share the rule above and a page can swap one for the other
   without touching anything else. alt="" on purpose: the button's own text
   names the action, so the picture is decoration and repeating it would just
   make a screen reader say "Build" twice.

   The art is expected to carry its own alpha, but NOT --sk-emblem-cap's dark
   dome behind it - that gradient was tuned for a bright silver glyph (a
   font-weight shape, uniformly --sk-emblem-icon) sitting on it, and every
   render in this set is itself dark-cored with only thin rim-light picking
   out the edges (the house style - see the prompts). Two dark layers stacked
   read as a black blob with a few lit hairlines, exactly the "not visible on
   dark backgrounds" build-icon and afk-icon were reported as. img gets its
   own lighter backdrop below instead of inheriting the cap.

   object-fit + a percentage inset rather than fixed dimensions: the medallion
   is sized by --sk-emblem-disc-size and steps down on a phone, and art pinned
   to a px size would either overflow the collar or float loose inside it. */
[data-sk-theme="emblem"] .sk-emblem-disc > :where(img) {
  box-sizing: border-box;
  /* Only just enough to keep the art off the collar. A glyph is drawn inside
     its own em box and needs no help; a render like build-icon.webp fills 97% of
     its canvas, so a square-ish inset sized for "a square inscribed in a circle"
     (~14%) shrank it to about half the dome and threw the detail away. The
     dome's border-radius clips whatever reaches the corners, and on art with a
     diagonal subject the corners are empty anyway. */
  padding: 6%;
  object-fit: contain;
  /* Light, hue-tinted backdrop - close to the near-white canvas this art was
     actually painted against, so the dark core keeps the separation it had
     there instead of merging into --sk-emblem-cap's near-black. Still built
     from the hue ramp, not a literal, so it re-tints with every sk-hue-*. */
  background: radial-gradient(circle at 34% 24%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 35%, var(--sk-neutral-5)) 0%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 28%, var(--sk-neutral-4)) 55%,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 20%, var(--sk-neutral-3)) 100%);
  /* Lighter than the glyph rule's inset shadow (which was darkening a
     backdrop that is now already light) - just enough to seat the art in the
     dome, not enough to re-darken it. */
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.35);
  /* The glyph takes its lift from text-shadow, which does nothing to an image.
     drop-shadow is the same idea applied to the rendered pixels, so it follows
     the art's alpha instead of boxing it. A white pass first, so a dark-cored
     render gets a bright edge to separate it from the page behind the button,
     not just the hue-coloured bloom of the second pass. */
  filter:
    drop-shadow(0 0 2px rgba(255, 255, 255, 0.9))
    drop-shadow(0 1px 2px rgba(0, 0, 0, 0.75))
    drop-shadow(0 0 7px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 40%, transparent));
}

/* ---- the plate: outer clip, and the edge it traces ---- */

[data-sk-theme="emblem"] .sk-emblem-plate {
  /* Exactly the edge's width. This padding IS the visible line - the face
     clipped inside it is what covers everything else. */
  padding: var(--sk-emblem-edge-width);
  min-width: var(--sk-emblem-min-width);
  background: var(--sk-emblem-edge);
  border-radius: var(--sk-emblem-radius);
  clip-path: var(--sk-emblem-clip);
  /* drop-shadow, not box-shadow: a box-shadow is cast by the border box and
     would print a rectangle's halo around a cut-cornered plate. drop-shadow
     runs on the rendered result, so it follows the clip. */
  filter: var(--sk-emblem-glow);
  transition: filter var(--sk-transition);
}

[data-sk-theme="emblem"] .sk-emblem:hover .sk-emblem-plate {
  filter: var(--sk-emblem-glow-hover);
}

/* ---- the face ----
   Element-qualified throughout, for the reason 4b exists: `button.sk-btn` and
   `input[type="submit"].sk-btn` restate width, padding, border, radius,
   background and box-shadow at (0,2,1), and the CTA block does it again at the
   same weight. With the theme attribute in front these reach (0,3,1) and clear
   both. `a.sk-btn--emblem` rides along so the class works on a LinkButton.

   What this does NOT clear is an ID selector from a page stylesheet, which is
   (1,x,y). missile.css had exactly one aimed at Fire; it was deleted when Fire
   moved onto this component, which is the rule and not the exception. */
[data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem,
[data-sk-theme="emblem"] input[type="button"].sk-btn--emblem,
[data-sk-theme="emblem"] button.sk-btn--emblem,
[data-sk-theme="emblem"] a.sk-btn--emblem {
  /* iOS Safari gives <input type="submit"> a native push-button appearance, and
     this is the one control in the system that cannot survive it: the face is
     shaped by clip-path with no border of its own, so any native radius or
     inner shading the platform adds sits INSIDE the clipped plate with nothing
     to hide it. Everywhere else in this file the reset is only on number inputs
     (their spinners); a filled button got away without it because it keeps a
     border and a rectangle. Not tested on a real device - stated as the reason
     it is here, not as a verified fix. */
  appearance: none;
  -webkit-appearance: none;
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  padding: var(--sk-emblem-pad-y) var(--sk-emblem-pad-right) var(--sk-emblem-pad-y) var(--sk-emblem-pad-left);
  border: none;
  /* One edge-width tighter than the plate's, or the face's corner cuts inside
     the ring and the edge fattens at all four corners. max() keeps it sane if
     the radius is ever smaller than the edge. */
  border-radius: max(0px, calc(var(--sk-emblem-radius) - var(--sk-emblem-edge-width)));
  background: var(--sk-emblem-face);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--sk-text) 18%, transparent);
  color: var(--sk-text-on-accent);
  font-family: var(--sk-emblem-font);
  font-size: var(--sk-emblem-size);
  font-weight: var(--sk-weight-bold);
  letter-spacing: var(--sk-emblem-tracking);
  text-transform: uppercase;
  text-shadow:
    0 0 12px color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 60%, transparent),
    0 1px 2px rgba(0, 0, 0, 0.8);
  /* .sk-btn sets `nowrap`, which is right for a 90px button in a row and wrong
     for this one: on a phone the assembly is full width, and a long label with
     nowhere to go runs off the screen instead of taking a second line. */
  white-space: normal;
  text-wrap: balance;
  /* The same shape as the plate. The edge-width the plate pads by is what shows
     between the two clips. The cut stays at its px value rather than shrinking
     with the inset, so the diagonals read a little finer than the straight
     runs - a bevel, not a mistake. */
  clip-path: var(--sk-emblem-clip);
}

/* .sk-btn:hover swaps the background for a flat color-mix, and the CTA block
   does it again - the same trap that cost the CTA its own gradient once.
   Restated so the steel survives the cursor arriving; the lift is cancelled
   because the wrapper already did it, and a second 1px here would shear the
   face off its edge. */
[data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem:hover,
[data-sk-theme="emblem"] input[type="button"].sk-btn--emblem:hover,
[data-sk-theme="emblem"] button.sk-btn--emblem:hover,
[data-sk-theme="emblem"] a.sk-btn--emblem:hover {
  background: var(--sk-emblem-face);
  color: var(--sk-text-on-accent);
  filter: brightness(1.12);
  /* general.css:711 gives every hovered input[type="submit"] a hard-coded
     `box-shadow: 0 6px 20px rgba(68, 124, 255, 0.5)` - accent blue, whatever
     hue the button is. At (0,2,1) it beat the resting inset here, so a gold
     Mine Gold and a red Fire both grew a BLUE halo the moment the cursor
     arrived. Restated, because a hover rule that only changes some of what the
     base rule set inherits the rest from whatever else is still shouting. */
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--sk-text) 18%, transparent);
  transform: none;
}

[data-sk-theme="emblem"] .sk-btn--emblem:active {
  filter: brightness(0.94);
}

/* Disabled dims the whole assembly, not just the face - a lit edge and a live
   medallion around a greyed word reads as a button still working on something.
   `disabled` is only ever on an <input>/<asp:Button> here; a <button> in this
   app must not carry it (general.css paints it black on white with an
   !important), and a disabled LinkButton gets .aspNetDisabled instead. */
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem:disabled),
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem[disabled]),
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.aspNetDisabled),
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.is-inert) {
  opacity: 0.45;
  transform: none;
}

/* general.css:693 paints EVERY disabled <input type="submit"> `color: black
   !important`, and this component has no way round it. The documented dodge is
   to colour a child element instead (that is why sk-disclosure puts its colour
   on its children) - and an <input> has no children to colour. So this is the
   one declaration in the component that has to answer !important with
   !important.

   Not cosmetic: Explore's script disables its button until a number is typed,
   so that page renders its primary action disabled ON LOAD. Without this the
   first thing a player sees is black text on dark steel. (The background is
   fine without help - general.css sets background-color at (0,2,1) and the
   themed face is (0,3,1).) */
[data-sk-theme="emblem"] .sk-btn--emblem:disabled,
[data-sk-theme="emblem"] .sk-btn--emblem[disabled] {
  color: var(--sk-neutral-5) !important;
}

[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem:disabled) .sk-emblem-plate,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem[disabled]) .sk-emblem-plate,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.aspNetDisabled) .sk-emblem-plate,
[data-sk-theme="emblem"] .sk-emblem:has(.sk-btn--emblem.is-inert) .sk-emblem-plate {
  filter: none;
}

/* On a phone it is the page's one action and takes the width, the way
   sk-btn--wide does. The medallion and the cuts step down with it: a 64px
   medallion against a 375px measure is a logo with a button attached. Only
   values change here, which is the point of them being values. */
@media (max-width: 768px) {
  [data-sk-theme="emblem"] .sk-emblem {
    --sk-emblem-cut: 10px;
    --sk-emblem-disc-size: 40px;
    --sk-emblem-overhang: 16px;
    --sk-emblem-min-width: 0;
    --sk-emblem-pad-y: 9px;
    --sk-emblem-icon-size: var(--sk-text-base);
    /* Matched left and right so the word sits centred in what the medallion
       leaves, rather than drifting right across a full-width plate. */
    --sk-emblem-pad-left: 31px;
    --sk-emblem-pad-right: 31px;
    --sk-emblem-tracking: 1.3px;
  }

  /* 80%, centred, not the whole measure: a plate the full width of a phone reads as a
     banner, and the medallion overhangs its left edge besides. */
  [data-sk-theme="emblem"] .sk-emblem {
    display: flex;
    width: 80%;
    margin-inline: auto;
  }

  [data-sk-theme="emblem"] .sk-emblem-plate {
    flex: 1 1 auto;
  }

  [data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem,
  [data-sk-theme="emblem"] input[type="button"].sk-btn--emblem,
  [data-sk-theme="emblem"] button.sk-btn--emblem,
  [data-sk-theme="emblem"] a.sk-btn--emblem {
    width: 100%;
  }
}

/* ---- a plain button as wide as the page's emblem ----------------------------
   A secondary action that sits in the same centred column as the emblem and
   should line up with it: the medallion's left edge to the plate's right
   (Attack's Calculate over ATTACK - the owner, 2026-10-18). From the emblem's
   own values, declared on this class too (the variables block above), so a
   change to the plate or the medallion moves both. Desktop only; on a phone
   the button keeps whatever width its other classes give it. Matches an
   emblem whose label fits the plate's minimum - a short word like Attack. */
@media (min-width: 769px) {
  [data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem-match,
  [data-sk-theme="emblem"] input[type="button"].sk-btn--emblem-match,
  [data-sk-theme="emblem"] button.sk-btn--emblem-match {
    width: calc(var(--sk-emblem-min-width) + var(--sk-emblem-overhang));
    justify-content: center;
  }
}


/* ---- a longer label, and a second line under it ----------------------------
   Theme-independent: a second line reads the same inside a CTA as it does on a
   plate, so these carry no theme selector.

   The word IS the button, so a long one lengthens the plate rather than
   truncating - the medallion and the corner cuts do not change size, which is
   what keeps a row of unequal buttons looking like one set.

   A SECOND LINE is <button>/<a> only. An <input type="submit"> carries one
   string in its value attribute and can hold no elements at all, so an
   <asp:Button> gets the label by itself. When the action needs to state its
   price - "Fire" over "1 Nuclear, 4 in silo" - reach for <asp:LinkButton>,
   which renders an <a> and takes children:

     <a class="sk-btn sk-btn--cta sk-btn--emblem" href="#">
       <span class="sk-emblem-label">Continue Building
         <span class="sk-emblem-note">resumes at tick 42</span>
       </span>
     </a>
   -------------------------------------------------------------------------- */

.sk-emblem-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  line-height: 1.25;
}

/* Not the heading face and not the tracking: Orbitron at 12px with 2px of
   letter-spacing is a texture, not a sentence. The note drops back to the body
   font so it reads as the small print it is. */
.sk-emblem-note {
  font-family: var(--sk-font);
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-normal);
  letter-spacing: 0;
  text-transform: none;
  color: var(--sk-neutral-5);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}


/* ---- sk-emblem--armed: the launch key --------------------------------------
   Missile's Fire and Buildings' Raze, and nothing else without a reason. The one action in this app
   that destroys someone else's buildings should not look like Save Settings
   wearing a different hue - it should look like the button everybody already
   knows, the one under a flip-up cover.

   The hue is pinned OUTSIDE the theme block on purpose, so the launch key is
   red in both themes: in the base theme sk-btn--cta reads --sk-hue-grad and
   comes out a red CTA with a red glow, with no armed-specific rule needed at
   all. Everything below is the emblem theme's version of the same idea.

   Red is reserved for destructive and aggressive actions - Fire, Attack, Raze.
   If red also meant "merely primary", this button would stop meaning anything.
   -------------------------------------------------------------------------- */

.sk-emblem--armed {
  --sk-hue: var(--sk-alert);
  --sk-hue-deep: var(--sk-alert-deep);
  --sk-hue-grad: var(--sk-grad-alert);
  --sk-hue-glow: var(--sk-glow-alert);
}

/* Nine values and one animation - no second implementation of the component.
   That is the test of whether the property surface above is the right one. */
[data-sk-theme="emblem"] .sk-emblem--armed {
  /* Only a shade larger than the ordinary medallion. The launch key is not
     meant to be a BIGGER emblem, it is meant to be a different one - and past
     about 1.2x the plate's height the disc stops reading as part of the button
     and starts reading as a logo with a button attached. */
  --sk-emblem-disc-size: 46px;
  --sk-emblem-overhang: 19px;
  --sk-emblem-edge-width: 3px;
  --sk-emblem-icon: var(--sk-text-on-accent);

  /* A heavier collar to match the heavier edge. */
  --sk-emblem-rim: linear-gradient(145deg,
      var(--sk-neutral-4) 0%,
      var(--sk-neutral-3) 20%,
      var(--sk-surface-page) 58%,
      var(--sk-neutral-3) 100%);

  /* The cap. Off-centre highlight, mid red through the body, and the rim
     rolling into shadow - the three stops that make a circle read as a dome
     rather than a red coin. */
  --sk-emblem-cap: radial-gradient(circle at 34% 24%,
      color-mix(in srgb, var(--sk-text) 55%, var(--sk-alert)) 0%,
      var(--sk-alert) 18%,
      var(--sk-alert-deep) 52%,
      color-mix(in srgb, var(--sk-alert-deep) 45%, var(--sk-surface-on-image-solid)) 100%);

  /* Hazard chevrons over the steel. 20% is the whole trick - a stripe you
     register without ever reading it as a stripe. */
  --sk-emblem-face:
      repeating-linear-gradient(135deg,
          color-mix(in srgb, var(--sk-alert-deep) 20%, transparent) 0 9px,
          transparent 9px 22px),
      linear-gradient(90deg,
          color-mix(in srgb, var(--sk-alert-deep) 38%, transparent) 0%,
          transparent 70%),
      linear-gradient(180deg,
          var(--sk-neutral-2) 0%,
          var(--sk-neutral-1) 22%,
          var(--sk-surface-page) 52%,
          var(--sk-surface-on-image-solid) 82%,
          var(--sk-neutral-1) 100%);
}

/* The cap is a dome, so it gets depth the ordinary medallion does not want. */
[data-sk-theme="emblem"] .sk-emblem--armed .sk-emblem-disc {
  padding: 6px;
  box-shadow:
    var(--sk-shadow-lg),
    0 0 18px color-mix(in srgb, var(--sk-alert-deep) 55%, transparent),
    inset 0 0 0 1px color-mix(in srgb, var(--sk-text) 20%, transparent);
}

[data-sk-theme="emblem"] .sk-emblem--armed .sk-emblem-disc > :where(i, img) {
  box-shadow:
    inset 0 -4px 9px color-mix(in srgb, var(--sk-surface-on-image-solid) 70%, transparent),
    inset 0 2px 3px color-mix(in srgb, var(--sk-text) 35%, transparent);
  text-shadow: 0 1px 3px color-mix(in srgb, var(--sk-surface-on-image-solid) 85%, transparent);
}

/* The glow breathes instead of sitting still. Hover changes the RATE, not the
   value: an animated property beats a normal declaration, so the plate's hover
   rule cannot get a word in here. A button already armed before you touch it. */
[data-sk-theme="emblem"] .sk-emblem--armed .sk-emblem-plate {
  animation: skEmblemArmed 2.4s ease-in-out infinite;
}

[data-sk-theme="emblem"] .sk-emblem--armed:hover .sk-emblem-plate {
  animation-duration: 0.9s;
}

/* Both stops color-mix(), same reason as --sk-emblem-glow-hover above: a
   color-mix() keyframe animated against a plain var() one interpolates
   through a garbage colour (sampled as clipped neon green) for part of the
   cycle instead of staying red. */
@keyframes skEmblemArmed {
  0%,
  100% {
    filter:
      drop-shadow(0 0 5px color-mix(in srgb, var(--sk-alert-deep) 70%, transparent))
      drop-shadow(0 0 16px color-mix(in srgb, var(--sk-alert-deep) 40%, transparent));
  }
  50% {
    filter:
      drop-shadow(0 0 9px color-mix(in srgb, var(--sk-alert-deep) 100%, transparent))
      drop-shadow(0 0 32px color-mix(in srgb, var(--sk-alert-deep) 65%, transparent));
  }
}

@media (max-width: 768px) {
  [data-sk-theme="emblem"] .sk-emblem--armed {
    --sk-emblem-disc-size: 42px;
    --sk-emblem-overhang: 18px;
    --sk-emblem-pad-left: 33px;
    --sk-emblem-pad-right: 33px;
  }
}

/* Form body inside a card. Replaces .pref-card-form, which was missing from
   the old→new mapping table entirely despite being on five pages. */
.sk-card-form {
  width: 100%;
}

/* ---- Badge -----------------------------------------------------------------
   Generalised from .probe-quick-badge.

   .sk-badge-links badges every link inside it, for anchors the page does not
   write itself: the Alliance pages' relation values come from
   dbo.AllianceRelationDisplay as ready-made HTML, shared with pages that keep
   plain links, so the class goes on the container rather than into the SQL. */
.sk-badge,
.sk-badge-links a {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
  padding: 2px 10px;
  border: 1px solid color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 55%, transparent);
  border-radius: 20px;
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 18%, transparent);
  color: var(--sk-hue, var(--sk-accent));
  font-family: var(--sk-font);
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  white-space: nowrap;
}

/* Pushes a badge to the far end of a card header. */
.sk-card-header .sk-badge {
  margin-left: auto;
}

/* A badge in a card header, on a narrow screen.
   sk-badge is white-space: nowrap, which is right for the one-word status it
   was built for - WAR, PEACE - and wrong the moment a badge carries something
   a player named. A kingdom name runs to 24 characters, uppercased by the
   badge, and on a ~340px card that single unbreakable line overflows the card
   and takes the page into a horizontal scroll with it.
   Wrapping is the fix, plus min-width: 0 so the badge may actually shrink -
   a flex item's default min-width is auto, i.e. its content, which is what
   lets an over-long child push its container wider in the first place. The
   trailing icon button keeps its size (flex: 0 0 auto is the default for it
   here), so the two stay on one line and the name wraps inside the pill
   rather than shoving the button onto a line of its own. */
@media (max-width: 768px) {
  .sk-card-header .sk-badge {
    flex: 0 1 auto;
    min-width: 0;
    white-space: normal;
    overflow-wrap: break-word;
    text-align: center;
  }
}

/* A header whose actions take a line of their own on a phone: the icon, the
   title and the badge share the first line, the buttons go under them. With
   four icon buttons beside it the title was squeezed until "Probe Mission"
   broke letter by letter. Desktop keeps one line. Probe's mission card. */
@media (max-width: 768px) {
  .sk-card-header--actions-below-mobile > .sk-card-actions {
    order: 1;
    flex-basis: 100%;
  }
}

/* A badge that drops its label on a phone and keeps only its icon.

   sk-badge is white-space: nowrap and text-transform: uppercase, which is
   exactly right for a one-word status and exactly wrong inside a narrow table
   column: SUCCESSFUL and UNSUCCESSFUL are 10 and 12 unbreakable uppercase
   characters, so the Status column alone sets a floor of ~110px that the table
   cannot go under, and a six-column table hits a horizontal scrollbar on a
   375px screen. Wrapping is not the answer here the way it is for the card
   header above - a status badge broken across two lines in a table cell reads
   worse than no badge at all.

   So on a phone the word goes and the glyph stays: colour plus icon already
   carry the meaning, and the column collapses to something square.

   MARKUP CONTRACT - both halves are required:

     <span class="sk-badge sk-badge--icon-mobile sk-hue-toxic" title="Successful">
       <i class="fas fa-check"></i><span class="sk-badge-text">successful</span>
     </span>

   - WITHOUT AN <i> THIS COLLAPSES TO AN EMPTY PILL. The variant has no way to
     know the badge has no icon, so it is on the caller to supply one.
   - The label is hidden the sk-sr-only way - clipped, not display: none - so
     a screen reader still announces "successful" at every width. Losing the
     word visually must not mean losing it entirely.
   - Carry a title as well. It is the only way a sighted player on a desktop
     narrowed past 768px, or on a tablet, can recover the word. */
/* One figure in a square, coloured by what the figure means.

   For a short row of same-shaped readings where the NUMBER is the whole
   content and a label would repeat five times -- war leader return times, a
   row of slot counts. A pill is the wrong shape for that: it is sized by its
   text, so a row of them comes out as ragged lozenges of different widths for
   figures that are all the same kind of thing. A square says "these are peers,
   compare them".

   Hue carries the state, so the caller binds sk-hue-* rather than the tile
   knowing anything about the domain. min-width 44px is the touch target
   minimum, and aspect-ratio keeps it square as the figure grows a digit.

   Give it a title: a bare number with no label is only readable if the reader
   can ask what it counts. */
.sk-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  aspect-ratio: 1 / 1;
  padding: var(--sk-space-2);
  border: 1px solid color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 55%, transparent);
  border-radius: var(--sk-radius);
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 18%, transparent);
  color: var(--sk-hue, var(--sk-accent));
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* A set of field rows whose columns line up.

   sk-field-row is a flex row and flex has no memory across rows: each row
   sizes its own label and its own control, so three rows whose labels differ
   in length and whose selects differ in content width put their controls at
   three different x-positions. Down a short form that reads as three unrelated
   rows rather than one block -- which is exactly how the race and state
   controls came out.

   A grid is the fix, and `display: contents` is what makes it possible without
   flattening the markup: each row keeps its own class, its own v-if and its
   own meaning, while its CHILDREN become items of the shared grid and line up
   column by column. The row's own border-bottom goes with its box, which is
   the trade -- aligned columns carry the structure instead.

   Four tracks, matching label / control / action / status. A row with fewer
   children simply leaves the trailing tracks empty.

   Below 768px it unwinds completely and hands each row back to sk-field-row,
   whose own media query already stacks label above control. */
.sk-field-grid {
  margin-top: var(--sk-space-4);
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
  align-items: center;
  gap: var(--sk-space-2) var(--sk-space-4);
}

/* Three tracks for a row that carries a reading: label | meter | actions.
   The base grid sizes the label max-content, which is right for "Race" and
   wrong for a label with a sentence of description under it - that column
   would take the card. Here the label flexes, the meter sits in a bounded
   track so every bar in the block starts and ends at the same x, and the
   actions pack to the widest button group. Unwinds on a phone exactly like the
   base, which this does not touch. display: contents drops each row's rule,
   so the rows are separated by a wider row gap instead. */
.sk-field-grid--meter {
  margin-top: 0;
  grid-template-columns: minmax(0, 1fr) minmax(8rem, 13rem) max-content;
  row-gap: var(--sk-space-3);
}

/* On a phone each row is its own stack again (label, meter, control), and a list
   of them is long (Modules: nine to a tab), so the rows run tighter than a form's:
   the same space-2 above and below a row as between its three parts. */
@media (max-width: 768px) {
  .sk-field-grid--meter > .sk-field-row {
    padding-block: var(--sk-space-2);
  }
}

/* Row bands: the same barely-there shade sk-table--col-bands puts on every other
   column, on every other ROW instead, so a reader can tell one entry (label, meter
   and control together) from the next down a long list - Modules.

   The grid's rows are display: contents, which draws no box and so has nothing to
   paint. On a computer each row becomes a one-row subgrid instead: it gets its box
   (and its background) back while its children stay on the shared tracks, so the
   columns line up as before. gap: normal is what makes a subgrid take the parent's
   column gap - sk-field-row's own gap would replace it. Row padding stands in for
   the row gap, so the bands meet with no unshaded seam between rows, and the band
   stands in for the divider.

   The count skips sk-is-hidden rows (Modules' Active tab hides every module with no
   points in it), so the stripes still alternate down what is on screen. */
.sk-field-grid--row-bands {
  row-gap: 0;
}

.sk-field-grid--row-bands > .sk-field-row {
  padding-inline: var(--sk-space-2);
  border-bottom: 0;
}

.sk-field-grid--row-bands > .sk-field-row:nth-child(even of :not(.sk-is-hidden)) {
  background: color-mix(in srgb, var(--sk-text) 1.5%, transparent);
}

@media (min-width: 768.02px) {
  .sk-field-grid.sk-field-grid--row-bands > .sk-field-row {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    gap: normal;
    padding-block: var(--sk-space-2);
  }
}

/* A minus/Buy/Buy Max action group where the minus button starts hidden and
   appears once something is queued (Modules). On a phone the buttons are
   close enough together that repeated taps land by screen position, not by
   button identity - so a minus appearing BEFORE Buy shoves both Buy and Buy
   Max one slot to the right, and the next tap meant to buy another level
   lands on minus instead (confirmed on a real phone: tapping for level 5
   alternately bought and cancelled). Pushing minus to the END instead means
   neither purchase button ever moves, whether or not minus is showing.
   Desktop has room to tell the buttons apart without this. */
@media (max-width: 768px) {
  .sk-field-actions > button:first-child {
    order: 1;
  }
}

/* Label on its own line, then control and action side by side -- and the
   actions in one column, so every button in the block comes out the same
   width whatever its text says.

   That last part is the whole point and it cannot be done row by row: a
   button is sized by its label, so Unlock and Set end up different widths in
   separate rows no matter how they are aligned. One max-content track shared
   down the grid sizes itself to the widest of them and every button fills it.
   The control track is 1fr, so the selects match each other too. */
.sk-field-grid--stacked {
  /* The action track has a floor as well as a ceiling. max-content alone sizes
     it to the widest label in the block, and "Set" is three characters - which
     gave a button barely wider than its own text and far too small a target
     beside a full-width select. The floor makes every button in the block at
     least 5.5rem; max-content still wins when a longer label needs more. */
  grid-template-columns: minmax(0, 1fr) minmax(5.5rem, max-content);
  /* Tight between a label and its own control -- they are one thing. The air
     that separates one field from the next is put on the label below instead,
     because a shared row-gap cannot tell the two apart and spacing both the
     same made the card twice as tall as it needed to be. */
  row-gap: var(--sk-space-2);
}

.sk-field-grid--stacked > .sk-field-row:not(:first-child) > .sk-field-label {
  margin-top: var(--sk-space-2);
}

/* sk-field-row brings its own padding and rule, which is right when it IS the
   row. Inside this grid it is not -- display: contents means it draws no box,
   so on the mobile fallback below that padding reappears and doubles every gap
   in the card. Zeroed here and the spacing left to the grid. */
.sk-field-grid--stacked > .sk-field-row {
  padding: 0;
  border-bottom: 0;
}

@media (max-width: 768px) {
  .sk-field-grid--stacked > .sk-field-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sk-space-1);
  }

  .sk-field-grid--stacked > .sk-field-row + .sk-field-row {
    margin-top: var(--sk-space-3);
  }
}

/* Spans the full width, and reads as one line: name on the left, the timer or
   token count hard against the right edge, both on the same baseline.
   sk-field-label is a column by default, which stacked them and left the count
   dangling under the name. */
.sk-field-grid--stacked > .sk-field-row > .sk-field-label {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--sk-space-3);
}

/* Fills its track rather than its text, which is what makes the column of
   buttons line up on both edges instead of only the left.

   Element + attribute + class, NOT :where(). :where() contributes nothing to
   specificity, so the first version of this was (0,2,0) -- and the button
   normalisation above is `input[type="submit"].sk-btn` at (0,2,1), which quietly
   won and held every button at width: auto. An attribute selector counts at
   class level AND brings its element with it; two classes do not beat one class
   plus an attribute plus an element. Spelling the same shape out here reaches
   (0,3,1). */
.sk-field-grid--stacked input[type="submit"].sk-btn,
.sk-field-grid--stacked input[type="button"].sk-btn,
.sk-field-grid--stacked button.sk-btn,
.sk-field-grid--stacked a.sk-btn {
  width: 100%;
}

@media (max-width: 768px) {
  .sk-field-grid--stacked > .sk-field-row {
    display: flex;
  }
}

.sk-field-grid > .sk-field-row {
  display: contents;
}

@media (max-width: 768px) {
  .sk-field-grid {
    display: block;
  }

  .sk-field-grid > .sk-field-row {
    display: flex;
  }
}

/* ...but --stacked does NOT unwind, because it has nothing to unwind INTO.

   The base grid is four tracks - label / control / action / status - which
   genuinely cannot hold on a phone, so it hands each row back to sk-field-row
   and lets the label sit above a full-width control. --stacked is already that
   shape: its label is on its own line at every width, and its two remaining
   tracks are a control that flexes and an action sized to the widest button in
   the block. Unwinding it only made both of those full-width and stacked,
   which turned three short fields into six full-width blocks and pushed
   everything under them off the first screen.

   So the grid is restored here. `.sk-field-grid.sk-field-grid--stacked` doubles
   the class on purpose: the `display: block` above is (0,1,0) but the row rule
   it pairs with is (0,2,0), and both sit in a media query of the same width, so
   only weight and source order separate them. The row selector reaches (0,3,0)
   for the same reason.

   The label keeps `grid-column: 1 / -1` from its unscoped rule above, so the
   shape on a phone is the shape on a desktop: name and tick/token count on one
   line, then the select and its button side by side under it. */
@media (max-width: 768px) {
  .sk-field-grid.sk-field-grid--stacked {
    display: grid;
  }

  .sk-field-grid.sk-field-grid--stacked > .sk-field-row {
    display: contents;
  }

  /* Restoring the grid is not enough on its own, and this is the half that was
     missing. `.sk-field-label, .sk-field-status, .sk-field-control` sets
     `flex: 1 1 100%; width: 100%` at this width - correct for the BASE grid,
     which unwinds onto sk-field-row's flex fallback here and wants each part on
     its own line, and exactly wrong for this one. A 100% flex-basis is what puts
     the select on a line of its own with the button under it, which is the shape
     this whole block exists to prevent.

     Written so it holds either way: as grid items the flex declarations are
     ignored and `width: auto` is what a grid item wants anyway; if anything ever
     knocks the row back onto `display: flex`, an auto basis on the control and a
     rigid one on the action still put the select and its button on one line
     under the full-width label. The shape no longer depends on winning a
     display race.

     And the select's 160px floor comes off inside the grid. minmax(0, 1fr) lets
     the TRACK shrink but not the select in it, so on a 320px phone the field
     overflowed its track and shouldered the button out of the row - the same
     symptom by a different route. */
  .sk-field-grid.sk-field-grid--stacked > .sk-field-row > .sk-field-control {
    flex: 1 1 auto;
    width: auto;
  }

  .sk-field-grid.sk-field-grid--stacked > .sk-field-row > .sk-field-control:last-child {
    flex: 0 0 auto;
  }

  .sk-field-grid.sk-field-grid--stacked .sk-select {
    min-width: 0;
  }
}

/* The action strip under a spy report.

   .separator is a legacy bar from general.css -- a 30px-tall navy strip with
   underlined 13px links -- and general.css is do-not-edit. The classname
   cannot go either, and not for cosmetic reasons: copySox finds the report
   card with closest(".separator").previousElementSibling, so it is a
   behavioural hook and the card must stay its previous sibling.

   So the class stays and this cancels the bar. Every declaration below is
   answering one of general.css's: height 30px clips a button to less than its
   own padding, the navy fill and rule draw a second card edge under the real
   one, and the underline belongs to a text link rather than to a button.

   `color: white !important` at .separator a is deliberately NOT fought. It
   cannot be beaten without another !important, and it does not need to be:
   --sk-text is #f8fafc, so a button in this strip was going to be near-white
   anyway. */
.separator.sk-sox-actions {
  height: auto;
  background: transparent;
  border-bottom: 0;

  /* Reads as the report's footer: hairline above, breathing room under it, and
     no gap from the card it belongs to. It cannot BE inside the card -- copySox
     finds the report with closest(".separator").previousElementSibling, so it
     has to stay the card's next sibling -- so it is made to look like it is. */
  margin-top: 0;
  padding: var(--sk-space-4) 0 0;
  border-top: 1px solid var(--sk-border);
}

/* ...and the card above it gives up its own bottom margin, so the two read as
   one block rather than as a card with a strip floating under it. */
.sk-card:has(+ .separator.sk-sox-actions) {
  margin-bottom: 0;
}

/* margin-right: general.css gives the SECOND link a 20px right margin (it spaced two text
   links apart); the row's own gap spaces the buttons, so a third button sat further off. */
.separator.sk-sox-actions a {
  font-size: inherit;
  text-decoration: none;
  margin-right: 0;
}

/* Both actions on one line on a small phone. They are a pair -- go there, or
   copy it -- and stacked they read as two unrelated steps. sk-btn's padding is
   sized for a page's primary action, which these are not. */
@media (max-width: 420px) {
  .separator.sk-sox-actions .sk-btn {
    padding: 8px 10px;
    font-size: var(--sk-text-xs);
    white-space: nowrap;
  }

  .separator.sk-sox-actions .sk-row {
    flex-wrap: nowrap;
    gap: var(--sk-space-2);
  }
}

/* A badge that is a link. _typography.css sets `a:link` at (0,1,1), which beats
   .sk-badge at (0,1,0) and would repaint the badge its link blue -- the same
   trap a.sk-btn--on-image already has to answer, and for the same reason. These
   tie at (0,1,1) and win on source order.

   The hover lift is the pill's, not the button's: a badge is a label first, and
   a link badge should read as the same object with somewhere to go. */
a.sk-badge,
a.sk-badge:link,
a.sk-badge:visited,
a.sk-badge:hover,
.sk-badge-links a,
.sk-badge-links a:link,
.sk-badge-links a:visited,
.sk-badge-links a:hover {
  color: var(--sk-hue, var(--sk-accent));
  text-decoration: none;
}

a.sk-badge:hover,
.sk-badge-links a:hover {
  border-color: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 85%, transparent);
  background: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 30%, transparent);
}

@media (max-width: 768px) {
  .sk-badge--icon-mobile {
    gap: 0;
    padding: 4px 7px;
  }

  /* Bumped from --sk-text-min: as the badge's only remaining content the glyph
     is carrying the whole meaning, and it is also now a touch target. */
  .sk-badge--icon-mobile > :where(i) {
    font-size: var(--sk-text-sm);
  }

  .sk-badge--icon-mobile > .sk-badge-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* ---- Stat readout ----------------------------------------------------------
   A number that should be read from across the room. The hue carries the
   meaning; the label stays muted so the value wins. */
.sk-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* A stat standing on its own in a pill row - Buildings' Total income - is
   centred by the row, but its contents stayed left-aligned, so the figure sat
   under the start of a wider label instead of under its middle. The row
   centres its items; a stat that IS one of those items centres its own label
   and value to match. A stat inside a tile or a card keeps reading left. */
.sk-pill-row > .sk-stat {
  align-items: center;
  text-align: center;
}

/* Orbitron on the LABEL, not the value. Orbitron's digits are wide and
   idiosyncratic, which is charm on a heading and friction on the number a
   player is actually reading. The label carries the sci-fi voice; the value
   gets the plain face and tabular figures so it can be read and compared. */
.sk-stat-label {
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--sk-text-muted);
}

/* On a phone every stat label reads as written ("Success rate", "Max pop"),
   never UPPERCASE - the same rule as phone table heads, for every tile and
   pill on every page (the user's call). Tiles in an sk-pill-row already had
   it (below); this covers the rest - the Attack Summary's, a drawer's. */
@media (max-width: 768px) {
  .sk-stat-label {
    text-transform: none;
    letter-spacing: 0;
  }
}


.sk-stat-value {
  font-family: var(--sk-font);
  font-size: var(--sk-text-xl);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--sk-hue, var(--sk-text));
}

/* Small stat value, for a readout sitting in a field row rather than standing
   alone as a headline number. Replaces .probe-stat-value, which was 0.725rem
   (~11.6px) — below the 12px floor this system sets. Apply alongside
   .sk-stat-value, not instead of it. */
/* A stat value that keeps the text colour instead of taking the card's hue.

   sk-stat-value reads var(--sk-hue, var(--sk-text)), which is right when the
   figure IS the hued thing. It is wrong on a card whose hue belongs to its
   stripe: the four headline figures on Status sit in gold / ion / toxic /
   accent cards for the stripe alone, and inherited the hue onto the number, so
   every figure was a different colour for a reason the reader cannot see.
   White by default; a state class on top still wins, since those are declared
   later in this file. */
.sk-stat-value--plain {
  color: var(--sk-text);
}

.sk-stat-value--sm {
  font-size: var(--sk-text-sm);
}

/* ---- A reading on ONE line -------------------------------------------------
   Label left, figure right, instead of label over figure. For a tile carrying
   TWO readings that still has to be the height of its neighbours: Status's top
   strip is a row of equal tiles, and one tile twice as tall drags the whole row
   with it.

   WHY NOT THE OTHER ARRANGEMENT -- both labels on one line, both figures on the
   next. It is the tidier shape and it only works at half the page: these tiles
   DIVIDE the row between them, so at six across, or at half of a 375px phone,
   two nine-figure numbers are asked to share about 150px. One figure per line
   always fits; two side by side sometimes do.

   The figure drops a step to --sk-text-lg. At --sk-text-xl it outweighs a label
   it now shares a line with rather than stands under, and two of them stacked
   read as a headline with a caption bolted underneath.

   baseline, not center: the label and the figure are different sizes in
   different faces, and centring their BOXES leaves the smaller one floating off
   the line the reader is actually following. */
.sk-stat--inline {
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sk-space-2);
}

.sk-stat--inline > .sk-stat-label {
  min-width: 0;
}

/* The figure never wraps and never shrinks; the LABEL gives ground - the same
   bargain sk-card-actions strikes with sk-card-title, and for the same reason.
   A truncated label is still a label; a truncated figure is a wrong number. */
.sk-stat--inline > .sk-stat-value {
  flex: none;
  font-size: var(--sk-text-lg);
  text-align: right;
}

/* ---- Signed change ---------------------------------------------------------
   A delta between two figures. The arrow is generated content rather than a
   character in the markup, so direction is never carried by colour alone -
   which is what makes this usable without colour vision and still legible when
   the page is printed or pasted as plain text.

   --sk-success (7.64:1) and --sk-danger (6.29:1) both clear AA as text against
   the composited card surface, so no separate token is needed here. */
.sk-delta {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sk-space-1);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--sk-text-muted);
}

.sk-delta--up { color: var(--sk-success); }
.sk-delta--down { color: var(--sk-danger); }

.sk-delta--up::before { content: "\2191"; }
.sk-delta--down::before { content: "\2193"; }
.sk-delta--flat::before { content: "\2192"; }

/* ---- Report diff -----------------------------------------------------------
   A report compared with an earlier one of the same kind, drawn ON the report
   like a git diff: a figure that went up is tinted green, one that went down
   red, an unchanged one dimmed - never hidden, the report still reads whole.
   A changed figure reads "1,080 -> 1,204" with the old one muted, then a chip
   with the change. First user: the Probe Room's "Since your last SOK / SOM"
   (auth-scripts.js skSoxDiff).

   Two halves, on purpose:
   - sk-diff-figure / -was / -now / -chip are elements the script ADDS around a
     figure and takes away again;
   - every tint, dim and layout rule is scoped to .sk-diff-on, which goes on
     the report's ROOT - outside the element a copy clones. So a copied report
     never carries a tint, and its markup lays out as it always did.

   Inside a report row (sk-report-rows) the row becomes a wrapping flex line
   while the diff is on: the figure drops under the name when the row is too
   narrow, and keeps to the right edge. The chip never leaves its figure's line.
   Sized in em throughout, so it scales with a phone's table cell. */
.sk-diff-on .sk-diff-row--gain {
  background-color: var(--sk-wash-success);
}

.sk-diff-on .sk-diff-row--loss {
  background-color: var(--sk-wash-danger);
}

.sk-diff-on .sk-diff-row--same {
  opacity: 0.5;
}

/* A pill already paints its own surface and edge: the tint replaces both. */
.sk-diff-on .sk-pill.sk-diff-row--gain {
  background-color: var(--sk-wash-success);
  border-color: var(--sk-success-border);
}

.sk-diff-on .sk-pill.sk-diff-row--loss {
  background-color: var(--sk-wash-danger);
  border-color: var(--sk-danger-border);
}

/* A table cell's background belongs to the table's own rules (stripes, a war
   leader's portrait on its column); an inset shadow lays the tint over it
   without a specificity fight. */
.sk-diff-on td.sk-diff-row--gain {
  background-color: transparent;
  box-shadow: inset 0 0 0 100vmax var(--sk-wash-success);
}

.sk-diff-on td.sk-diff-row--loss {
  background-color: transparent;
  box-shadow: inset 0 0 0 100vmax var(--sk-wash-danger);
}

.sk-diff-on .sk-report-rows > .sk-diff-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--sk-space-2);
  margin-inline: calc(-1 * var(--sk-space-2));
  padding-inline: var(--sk-space-2);
  border-radius: var(--sk-radius-sm);
}

/* Stacked by default - the old figure on a small line above the new one - so
   a table cell, a pill or a headline figure grows down rather than across. */
.sk-diff-figure {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
}

.sk-diff-figure .sk-diff-was {
  font-size: 0.85em;
}

/* In a report row there is room across: one line, "old -> new chip", which
   wraps under the name (and keeps to the right) when the row is too narrow. */
.sk-diff-on .sk-report-rows .sk-diff-figure {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-end;
  column-gap: 0.4em;
  margin-left: auto;
}

.sk-diff-on .sk-report-rows .sk-diff-was {
  font-size: inherit;
}

/* A pill reads left to right, and may wrap while it carries a diff. */
.sk-diff-on .sk-pill.sk-diff-row {
  white-space: normal;
}

.sk-diff-on .sk-pill .sk-diff-figure {
  align-items: flex-start;
  vertical-align: top;
}

.sk-diff-was {
  color: var(--sk-text-muted);
  font-weight: var(--sk-weight-normal);
  white-space: nowrap;
}

.sk-diff-now {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  white-space: nowrap;
}

/* The change, in its hue (sk-hue-success / sk-hue-danger on the chip). The
   badge formula, in em rather than sk-badge's fixed px and without its
   uppercase, so it sits inside a figure at any size. */
.sk-diff-chip {
  display: inline-block;
  padding: 0.05em 0.5em;
  border: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 55%, transparent);
  border-radius: 1em;
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 16%, transparent);
  color: var(--sk-hue, var(--sk-accent));
  font-size: 0.8em;
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The one line above a compared report: "12 changes since your SOK 14 ticks
   ago". Wraps; never truncated. */
.sk-diff-summary {
  margin: 0 0 var(--sk-space-2);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
}

.sk-diff-summary :where(i) {
  color: var(--sk-accent);
}

/* The switch in the report's header: "Changes · SOK 14 ticks ago". Header
   sized, like the copy pair beside it; pressed while the diff shows. The
   label may wrap on a phone rather than push the card wider. */
.sk-diff-toggle {
  padding: 6px 12px;
  font-size: var(--sk-text-xs);
  white-space: normal;
  text-align: left;
}

/* (0,2,0): beats button.sk-btn's (0,1,1) resting wash. */
.sk-diff-toggle[aria-pressed="true"] {
  background: var(--sk-wash-accent-strong);
  border-color: var(--sk-accent);
}

/* On a phone the switch shares the copy buttons' line (SOK's beside Attack
   Room, Copy and the two icons - the owner, 2026-10-18), and that line does
   not wrap: tighter sides, and the label ("SOK 423T ago") may take two
   lines inside the button, keep it inside a 360px screen. (0,3,0) to clear
   .sok-header-actions .sk-btn / .sox-report-actions .sk-btn, which come
   later at (0,2,0) and set nowrap. */
@media (max-width: 768px) {
  .sk-card-header .sk-btn.sk-diff-toggle {
    padding-inline: 8px;
    white-space: normal;
  }
}

/* ---- HUD framing -----------------------------------------------------------
   Two corner brackets, top-left and bottom-right, in the card's hue. This is
   the cheapest way to read as a targeting overlay rather than a web panel, and
   because it is drawn with pseudo-elements the card keeps its real border - no
   clip-path, so nothing fights the backdrop blur.

   Opt-in on purpose. Brackets on every card is a costume; on the one panel
   that matters it is an accent. */
.sk-card--hud {
  position: relative;
}

.sk-card--hud::before,
.sk-card--hud::after {
  content: '';
  position: absolute;
  width: 16px;
  height: 16px;
  border-style: solid;
  border-color: var(--sk-hue-deep, var(--sk-accent));
  pointer-events: none;
}

.sk-card--hud::before {
  top: -1px;
  left: -1px;
  border-width: 2px 0 0 2px;
  border-top-left-radius: var(--sk-radius-lg);
}

.sk-card--hud::after {
  right: -1px;
  bottom: -1px;
  border-width: 0 2px 2px 0;
  border-bottom-right-radius: var(--sk-radius-lg);
}

/* Scanlines. Held at ~3% so it reads as texture on a CRT, not as stripes -
   anything stronger competes with the text sitting on top of it. */
.sk-card--scan {
  background-image: repeating-linear-gradient(
      0deg,
      rgba(255, 255, 255, 0.03) 0px,
      rgba(255, 255, 255, 0.03) 1px,
      transparent 1px,
      transparent 3px);
}

/* ---- Stat pill -------------------------------------------------------------
   The row of readouts above a page: "Available: 4 Pts", "Reset Cooldown: 18
   Ticks". Generalised from .stat-pill (general.css:2058ff), which already had
   gold / info / primary / success / relic variants - the same five hues this
   system names. The icon carries the hue, the value stays white so the number
   is what the eye lands on.

   These usually sit ON the starfield, not on a card, so the surface is nearly
   opaque rather than a 0.55 wash - a translucent pill over a bright nebula is
   unreadable. */
/* Ported from general.css's .stat-pill, which is the shape this app already
   uses on Alliance Home — icon, label, bold figure — and which sk-pill had only
   half of. The five *-pill colour variants (gold/info/primary/success/relic)
   became hue classes instead: `sk-pill sk-hue-gold` rather than a new classname
   per colour. */
.sk-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
  padding: 8px 16px;
  background: var(--sk-surface-on-image);
  /* Tinted by the pill's own hue, defaulting to accent — the old .stat-pill
     hardcoded rgba(68,124,255,.25), which is --sk-accent at 25%. */
  border: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, transparent);
  border-radius: 20px;
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  white-space: nowrap;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  transition: border-color var(--sk-transition), background var(--sk-transition), transform var(--sk-transition);
}

/* The icon carries the colour; the label stays muted and the figure stays
   bright. Colour is decoration here, never the only carrier of meaning. */
.sk-pill > :where(i) {
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-xs);
}

.sk-pill :where(b, strong) {
  color: var(--sk-text);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
}

.sk-pill:hover {
  border-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 50%, transparent);
  background: rgba(15, 23, 42, 0.7);
  transform: translateY(-1px);
}

.sk-pill :where(i) {
  color: var(--sk-hue, var(--sk-accent));
  font-size: 13px;
}

.sk-pill :where(strong, b) {
  color: var(--sk-text);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* The secondary clause in a pill that carries an explanation as well as a name -
   a universe event's "-10% return time" after "Caffeine Crusaders". Muted and
   divided off, so the pill reads name / effect / figure in three weights rather
   than as one run of text at equal emphasis.

   The rule is a border rather than a middot character because the divider then
   belongs to the element that owns it: a pill with no effect renders nothing
   extra, with no punctuation left stranded after the name. */
.sk-pill-effect {
  padding-left: var(--sk-space-2);
  border-left: 1px solid var(--sk-border-subtle);
  color: var(--sk-text-muted);
}

/* A pill that fills its container instead of hugging its text. For a pill that
   is the whole of a row -- a setting with a switch in it, say -- where a
   content-width lozenge floating in a wide card reads as unfinished. */
.sk-pill--full {
  display: flex;
  width: 100%;
}

/* The control goes to the far end. In a content-width pill everything packs
   together and that is right; once the pill spans the row, a switch left
   floating beside the label reads as unrelated to it. */
.sk-pill--full > .sk-switch {
  margin-left: auto;
}

/* A card in a pill row carries the row's gap and nothing more. Its own
   margin-bottom is meant to separate stacked cards down a page; inside a grid
   it lands on top of the row gap, so the vertical space came out several times
   the horizontal and the four tiles read as two pairs rather than one set. */
.sk-pill-row > .sk-card {
  margin-bottom: 0;
}

/* Type does come down here, which the density scale deliberately does not do.
   Four figures two-up on a 375px screen is the one case where the label, not
   the spacing, is what overflows: "Population Change" at the default stat size
   wraps to three lines and the tile stops being scannable. */
@media (max-width: 768px) {
  .sk-pill-row > .sk-card {
    padding: var(--sk-space-3);
  }

  .sk-pill-row > .sk-card .sk-stat-value {
    font-size: var(--sk-text-sm);
  }

  /* Below the 12px floor, deliberately, and this is the reasoning the floor
     asks for. These labels are two and three words -- "Population Change",
     "Networth Change" -- in a tile a quarter of a 375px screen wide. At
     --sk-text-min they wrap to two and three lines, which makes the tile taller
     than the figure it exists to show and pushes the number out of the scan.
     The label is a caption for a figure that is itself the size of body text;
     shrinking the caption is what keeps the figure readable. */
  .sk-pill-row > .sk-card .sk-stat-label {
    font-size: 10px;
    letter-spacing: 0;
    line-height: 1.35;
    text-transform: none;
  }
}

.sk-pill-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--sk-space-2);
  margin: var(--sk-space-3) 0;
}

@media (min-width: 768.02px) {
  .sk-pill-row {
    margin: var(--sk-space-4) 0 var(--sk-space-5);
  }
}

/* A pill row sized as a readout STRIP rather than a headline: several short
   readings in a line or two under a table or a row of cards, sitting close to
   what they annotate. Tighter gap, tighter pills, one type step down.
   Buildings' readings strip is this; Research's staged-assignment summary
   (research.css) is the same shape and can adopt it. */
.sk-pill-row--compact {
  gap: var(--sk-space-1) var(--sk-space-2);
  margin-top: var(--sk-space-2);
}

.sk-pill-row--compact > .sk-pill {
  padding: var(--sk-space-1) var(--sk-space-3);
  font-size: var(--sk-text-xs);
}

/* The step the other way: readings a player comes to the page FOR, still in
   the pill shape but with the figure at a tile's size (--sk-text-lg, what
   sk-card--tile gives its sk-stat-value). For a row that should outweigh a
   strip without changing shape. Buildings' readings strip is this when the
   player asks for it.

   (0,2,0) against sk-pill's own icon and figure rules at (0,1,0), so these win
   on specificity rather than on where in the file they sit. */
.sk-pill-row--large {
  gap: var(--sk-space-2) var(--sk-space-3);
}

.sk-pill-row--large > .sk-pill,
.sk-pill-row--large > .sk-pill > :where(i) {
  font-size: var(--sk-text-base);
}

.sk-pill-row--large > .sk-pill :where(b, strong) {
  font-size: var(--sk-text-lg);
}

/* One track each, all equal. A pill is sized by its content, so a row of them
   comes out ragged - and the ragged edges move between page loads, because the
   content is figures that change every tick. Equal tracks give the row a shape
   that stays put.

   Above 768px only: below that, --pairs owns the layout, and two rules setting
   grid-template on the same element would be decided by source order rather
   than by intent. Shorten the labels before reaching for this - equal tracks
   are all set by the widest one, so "Probes per land" costs every other pill
   width it could have used.

   min-width: 0 on the pill because a grid item's minimum is its content, which
   is what stops equal tracks from being equal in the first place. */
@media (min-width: 768.02px) {
  .sk-pill-row--even {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }

  .sk-pill-row--even > .sk-pill {
    justify-content: center;
  }

  /* Any child, not only a pill. A grid item's minimum is its content, which is
     what stops equal tracks from being equal -- and that is true of an sk-stat
     with a long label ("Population Change") just as much as of a pill. Broader
     than the rule above it because there is nothing pill-specific about it;
     every current caller passes pills, so nothing changes for them. */
  .sk-pill-row--even > * {
    min-width: 0;
  }
}

/* A pill that is a SENTENCE rather than a reading - "War with Vanguard (47
   ticks)" - takes the whole row and wraps inside it, instead of sharing a line
   with the next one and then overflowing, because .sk-pill is nowrap by design.

   sk-pill--span was documented as "full row" but only ever implemented inside
   the pairs grid below, where it means grid-column: 1 / -1. This is the same
   intent expressed for the flex form the class is used in everywhere else.

   :not(.sk-pill-row--pairs) keeps the two apart: that variant is a grid, it has
   its own rule for this class, and it deliberately re-applies nowrap there. */
.sk-pill-row:not(.sk-pill-row--pairs) > .sk-pill--span {
  flex: 1 0 100%;
  justify-content: flex-start;
  white-space: normal;
}

/* Two per line on a phone, instead of whatever the wrap happens to produce.
   Pills are sized by their content, so a row of four different widths wraps
   into a ragged 3-then-1 or 2-then-2 depending on how long the numbers are
   that tick - the layout moves under the reader between page loads. A grid
   fixes the shape: same pills, same order, predictable rows.

   The two overrides on the pill are both required, and both are about a
   content-sized component being asked to live in a fixed-width cell:
   white-space: nowrap is what a pill uses to keep its label and figure on one
   line, and in a half-width cell that overflows the row instead. */
@media (max-width: 768px) {
  .sk-pill-row--pairs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* sk-pill-row sets align-items:center for its flex form, and that carries
       into this grid: a pill whose value wraps to two or three lines then sits
       beside a one-line pill floating in the middle of the gap it leaves. Equal
       tracks were the point of this variant - equal heights are the same point,
       and stretch is what makes the pair read as one row. */
    align-items: stretch;
  }

  .sk-pill-row--pairs > .sk-pill {
    justify-content: center;
    min-width: 0;
    white-space: normal;
    text-align: center;
  }

  /* Any child, not only a pill - the same broadening --even > * already got,
     and for the same reason: "take the whole row" is about the ITEM's place in
     the grid, not about what kind of item it is. An odd number of readings
     otherwise leaves half the last row empty. */
  .sk-pill-row--pairs > .sk-pill--span {
    grid-column: 1 / -1;
  }

  /* One reading left, on a phone. Half the width, centred - a single tile
     stretched across a 375px screen reads as a banner rather than a reading. */
  .sk-pill-row--pairs.sk-pill-row--solo {
    grid-template-columns: minmax(0, 50%);
    justify-content: center;
  }
}

/* ---- Ordering inside a pill row -------------------------------------------
   The same sk-order-1..10 classes sk-split--order and sk-board use, for a row
   of readings the player can rearrange. Opt-in.

   THE `order: 99` FLOOR IS NOT OPTIONAL, for the third time in this file:
   `order` defaults to 0, which sorts BEFORE every positive value, so an
   unmarked child would jump to the FRONT of the row rather than trail the
   marked ones.

   Not inside a media query, and it does not need to be: --even is a grid above
   768px and --pairs a grid below it, and `order` sorts the auto-placement in
   both exactly as it sorts flex items in the plain form.

   This is the THIRD copy of the block. A generic `.sk-ordered` container is the
   obvious consolidation and is what a fourth caller should get; three scoped
   copies are still cheaper than a global utility whose order floor would land
   on every flex and grid container in the app. */
.sk-pill-row.sk-pill-row--order > * {
  order: 99;
}

.sk-pill-row.sk-pill-row--order .sk-order-1 { order: 1; }
.sk-pill-row.sk-pill-row--order .sk-order-2 { order: 2; }
.sk-pill-row.sk-pill-row--order .sk-order-3 { order: 3; }
.sk-pill-row.sk-pill-row--order .sk-order-4 { order: 4; }
.sk-pill-row.sk-pill-row--order .sk-order-5 { order: 5; }
.sk-pill-row.sk-pill-row--order .sk-order-6 { order: 6; }
.sk-pill-row.sk-pill-row--order .sk-order-7 { order: 7; }
.sk-pill-row.sk-pill-row--order .sk-order-8 { order: 8; }
.sk-pill-row.sk-pill-row--order .sk-order-9 { order: 9; }
.sk-pill-row.sk-pill-row--order .sk-order-10 { order: 10; }

/* One reading left, on a desktop. Equal tracks give the survivor the whole
   page, which reads as a banner; this puts it back at about the width it had
   when it had company. The PAGE sets the class - only it can count what is
   visible, because the others are still in the DOM with display: none. */
@media (min-width: 768.02px) {
  .sk-pill-row--even.sk-pill-row--solo {
    grid-auto-columns: minmax(0, 25%);
    justify-content: center;
  }
}

/* ---- sk-pill-row--tiles: tiles laid out by how many there are -------------
   A row of sk-card--tile readings that reads the same whether one, two or three
   are showing, on equal widths, without the page counting anything:

     desktop   1 tile: a third of the row, centred    2 tiles: half each
               3 tiles: a third each                  4 tiles: two rows of halves
               5 tiles: three, then two in halves
     phone     1 tile: the whole row                  2 tiles: half each
               3 tiles: two halves, then one whole row
               4: two rows of halves; 5: two, two, then one whole row

   The count is the row's own children, so a tile the page removes (v-if, or
   Visible = False on the server) is not counted; a tile only hidden with
   display: none still is. Track width is calc(), not a grid, so a lone tile
   keeps its share instead of stretching. --sk-tiles-cols is the one number
   the rules below change. */
.sk-pill-row--tiles {
  --sk-tiles-cols: 2;
}

.sk-pill-row--tiles > * {
  flex: 0 0 calc((100% - (var(--sk-tiles-cols) - 1) * var(--sk-space-2)) / var(--sk-tiles-cols));
  min-width: 0;
}

@media (min-width: 768.02px) {
  .sk-pill-row--tiles {
    --sk-tiles-cols: 3;
  }

  /* 2 tiles: halves. 4: two rows of halves. */
  .sk-pill-row--tiles:has(> :nth-child(2):last-child),
  .sk-pill-row--tiles:has(> :nth-child(4):last-child) {
    --sk-tiles-cols: 2;
  }

  /* 5 tiles: a row of three, then the last two share the second row in halves. */
  .sk-pill-row--tiles:has(> :nth-child(5):last-child) > :nth-child(n + 4) {
    flex-basis: calc((100% - var(--sk-space-2)) / 2);
  }
}

/* Phone: an odd last tile takes the whole row, and so does sk-tile--wide - a tile that holds a
   small table rather than one figure, where half a row is too narrow for several columns of
   large numbers. A plain tile left alone on a row by a wide neighbour (before it, after it, or
   both) also takes the whole row instead of sitting centred at half width. */
@media (max-width: 768px) {
  .sk-pill-row--tiles > :last-child:nth-child(odd),
  .sk-pill-row--tiles > .sk-tile--wide,
  .sk-pill-row--tiles > :not(.sk-tile--wide):nth-child(odd):has(+ .sk-tile--wide),
  .sk-pill-row--tiles > .sk-tile--wide + :not(.sk-tile--wide):last-child,
  .sk-pill-row--tiles > .sk-tile--wide + :not(.sk-tile--wide):has(+ .sk-tile--wide) {
    flex-basis: 100%;
  }
}

/* Tiles in a drawer: two across at every width. The counts above follow the
   WINDOW, and a drawer is 420-620px whatever the window is - three across in
   a 620px drawer squeezed Probe stats' Results ("Powerplants sabotaged",
   "3,120,000") into thirds. An odd last tile keeps its half on a desktop and
   takes the row on a phone (the rule above). */
.sk-drawer .sk-pill-row--tiles {
  --sk-tiles-cols: 2;
}

.sk-drawer .sk-pill-row--tiles > * {
  flex-basis: calc((100% - var(--sk-space-2)) / 2);
}

/* ---- sk-pill-row--mini: a row of mini-tiles --------------------------------
   Equal tracks for sk-card--tile-mini: three to a row, two under 1200px, one on a phone. A
   one-line tile needs its label and figure side by side, and the longest reading in use
   ("Barracks 92.4% 120 needed") does not fit a third of a window narrower than that, nor half of
   a 375px one.

   Three, not the four of a headline tile row, so the two do not line up into one grid. A page
   that needs another count on a wide screen sets --sk-mini-cols on the row.

   A grid, where the base sk-pill-row is a wrapping flex row: a page that hides and shows the row
   itself must show it as display: grid. */
.sk-pill-row--mini {
  display: grid;
  grid-template-columns: repeat(var(--sk-mini-cols, 3), minmax(0, 1fr));
}

@media (max-width: 1200px) {
  .sk-pill-row--mini {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 768px) {
  .sk-pill-row--mini {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Four across on a wide screen, for a row of exactly four - three would leave
   the fourth alone on a line of its own. A class rather than --sk-mini-cols
   written on the element: no style= in markup. Under 1200px the two-across
   rule above takes over as usual. (Attack's result card, when it has Honor.) */
@media (min-width: 1200.02px) {
  .sk-pill-row--mini.sk-pill-row--mini-four {
    --sk-mini-cols: 4;
  }
}

/* Mini-tiles two across on a phone, with sk-pill-row--pairs - for a short row
   of short figures, where one per line would make a tall column of mostly
   empty tiles. Half a 375px row is too narrow for label and figure side by
   side, so here each tile puts its label over its figure, as a full tile does.
   (0,2,0), and declared after the one-across rule above. */
@media (max-width: 768px) {
  .sk-pill-row--mini.sk-pill-row--pairs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .sk-pill-row--mini.sk-pill-row--pairs > .sk-card--tile-mini > .sk-stat {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
  }
}

/* Every tile on one line on a phone, however many - for three or four short
   figures that read as one strip (Attack's result card: Offense, Defense,
   War leaders and Honor - the owner, 2026-10-19). With sk-pill-row--pairs for
   its label-over-figure tiles. The icons go on a phone: a quarter of a 375px
   row has no room for one beside "War leaders". (0,2,0), declared after the
   two-across rule above. */
@media (max-width: 768px) {
  .sk-pill-row--mini.sk-pill-row--one-line-mobile {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }

  .sk-pill-row--one-line-mobile > .sk-card--tile-mini > .sk-stat-icon {
    display: none;
  }
}

/* sk-pill-row--mini-mobile: mini-tiles on a phone, the full headline tiles above 768px - label
   over figure, left-aligned, --sk-mini-cols to a row at every desktop width. For a page whose
   desktop keeps the tiles it had before the redesign (Buildings, Military): the redesign changes
   the phone view only. */
@media (min-width: 768.02px) {
  .sk-pill-row--mini-mobile {
    grid-template-columns: repeat(var(--sk-mini-cols, 3), minmax(0, 1fr));
  }

  .sk-pill-row--mini-mobile > .sk-card--tile-mini {
    justify-content: flex-start;
  }

  .sk-pill-row--mini-mobile > .sk-card--tile-mini > .sk-stat {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
  }
}

/* A row of ONE mini-tile - an action page whose headline is a single reading
   (Military: barracks space) - takes the row at every width, the same as it does
   on a phone, rather than sitting in the first of three tracks with two empty
   ones beside it. The tile centres its own reading, so the leftover width is
   padding either side. */
.sk-pill-row--mini:has(> :only-child) {
  grid-template-columns: minmax(0, 1fr);
}

/* sk-pill-row--pills-mobile: a row of mini-tile readings that reads as PILLS on a phone - the
   action pages' readings (slot 2). Review, 2026-10-19: pills over mini-cards. The row keeps its
   tiles' markup and its desktop look; under 768px each tile drops the card (stripe, square
   corners, padding) for the pill's shape, border and shadow (see .sk-pill), its icon, label and
   figure on one line. Two to a row with sk-pill-row--pairs, as before. */
@media (max-width: 768px) {
  /* Two pills to a row when both fit, otherwise one across (review, 2026-10-19:
     "half page width and row them" - then "if it doesn't fit don't force half
     width"). Flex, not a grid: each pill starts at its own content width, never
     under half the row, so two short ones share a line; a longer one wraps to its
     own line and grows to the full width, as does whichever pill is left alone. */
  .sk-pill-row--pills-mobile {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sk-space-2);
  }

  .sk-pill-row--pills-mobile > .sk-card--tile-mini {
    flex: 1 0 auto;
    min-width: calc(50% - var(--sk-space-2) / 2);
    max-width: 100%;
    justify-content: center;
    gap: var(--sk-space-2);
    min-height: 0;
    padding: var(--sk-space-1) var(--sk-space-3);
    background: var(--sk-surface-on-image);
    border: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 30%, transparent);
    border-radius: 20px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  }

  .sk-pill-row--pills-mobile > .sk-card--tile-mini > .sk-stat-icon {
    font-size: var(--sk-text-xs);
  }

  /* (0,3,0): beats sk-pill-row--pairs' label-over-figure rule. */
  .sk-pill-row--pills-mobile.sk-pill-row--pairs > .sk-card--tile-mini > .sk-stat,
  .sk-pill-row--pills-mobile > .sk-card--tile-mini > .sk-stat {
    flex-direction: row;
    align-items: baseline;
    gap: var(--sk-space-1);
    min-width: 0;
  }

  .sk-pill-row--pills-mobile > .sk-card--tile-mini .sk-stat-label {
    font-size: var(--sk-text-xs);
  }

  .sk-pill-row--pills-mobile > .sk-card--tile-mini .sk-stat-value {
    font-size: var(--sk-text-sm);
  }
}

/* ---- Artwork pill ----------------------------------------------------------
   A stat pill whose leading slot is a picture instead of a glyph. Built for the
   universe-event alerts under the top bar, where a Font Awesome icon cannot say
   WHICH event is running and the artwork can - but it is a plain sk-pill
   modifier, so anything with bespoke art per row can use it.

   The image is the only thing here that is not a token. Each event_{id}.webp is
   a transparent render whose halo is already hue-matched to its own subject, so
   it composites onto --sk-surface-on-image without a plate behind it. Pair the
   pill with the sk-hue-* class nearest the artwork's dominant colour: the border
   and the picture sit 8px apart, and a disagreement between them reads as a bug.

   20px is a deliberate floor, not a shrink-to-fit. These renders are 500-1024px
   of detailed subject; below about 18px they stop being legible as anything and
   become a coloured smudge, at which point the icon fallback is the better
   component. Nothing about the pill's height depends on the image, so a pill
   whose art fails to load (the markup drops the <img> on error) collapses to a
   text-only pill of exactly the same size rather than shifting the row. */
.sk-pill--art > :where(img) {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  object-fit: contain;
  /* The artwork's own glow is its shadow; this only lifts it off the surface so
     a dark-cored subject does not read as a hole in the pill. */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
  transition: transform var(--sk-transition);
}

.sk-pill--art:hover > :where(img) {
  transform: scale(1.12);
}

@media (prefers-reduced-motion: reduce) {
  .sk-pill--art > :where(img) {
    transition: none;
  }

  .sk-pill--art:hover > :where(img) {
    transform: none;
  }
}

/* ---- Pill tooltip body -----------------------------------------------------
   A titled card inside a buildings-tooltip: artwork and name on one line, then
   what it does, then the number that changes. Three levels of emphasis doing
   three different jobs - the name is the identity, the effect is the substance,
   the figure is the thing the player came to check - so the figure gets the
   accent colour and the effect drops to muted rather than all three competing.

   Every element is a span, not a div. The whole block travels inside a title=""
   attribute and Bootstrap injects it with innerHTML into .tooltip-inner, an
   element it also uses for plain one-line strings, so nothing here may depend
   on being a block-level child. Display is set explicitly for the same reason.

   Bootstrap 5 sanitises tooltip HTML by default. Every tag and attribute used
   here - span, b, img with src/alt, and class globally - is on its allowlist,
   so this survives with sanitize left on. Adding a tag outside that list means
   it silently vanishes rather than erroring.

   Only layout and type live here. general.css owns the tooltip's black surface
   and blue edge and is left alone. */

/* The authored tooltip content, sitting inside its own trigger. It is never
   displayed in place - auth-scripts.js reads its innerHTML into the Bootstrap
   tooltip. Writing it as markup instead of as an HTML string in title="" is the
   whole point: the template stays a template, and content carrying its own tags
   needs no escaping on the way in or out. */
.sk-tip {
  display: none;
}

/* An element with readings behind it says so. A bare figure gives no hint that
   hovering - or tapping, if it takes tabindex - opens anything; a dotted
   underline and the help cursor are the conventional signal and cost no
   width. Put it only on elements that really open a tooltip. */
.sk-tip-trigger {
  cursor: help;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

/* The cue as a glyph instead: no underline, a small muted (i) after the words.
   For names in running text, where a dotted line through every one makes a
   column hard to read (Research - the owner, 2026-10-19). Number columns keep
   the underline: a glyph beside every figure would cost the width they lack. */
.sk-tip-trigger--icon {
  text-decoration: none;
}

.sk-tip-trigger--icon::after {
  content: "\f05a";
  margin-left: 0.35em;
  font-family: "Font Awesome 5 Free";
  font-weight: 900;
  font-size: 0.8em;
  color: var(--sk-text-muted);
}

.sk-tip-trigger--icon:hover::after,
.sk-tip-trigger--icon:focus-visible::after {
  color: var(--sk-accent);
}

/* A table figure that puts itself into its row's field: a click (or Enter /
   Space - the markup gives it role="button" and tabindex="0") fills the box
   with that figure. Link-coloured and underlined so it reads as something to
   press (Attack's Deploy Available column - the owner, 2026-10-19).
   .sk-table qualifies it past the table's own cell colour. */
.sk-table .sk-fill-cell {
  cursor: pointer;
  color: var(--sk-accent);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--sk-accent) 45%, transparent);
  text-underline-offset: 3px;
}

.sk-table .sk-fill-cell:hover,
.sk-table .sk-fill-cell:focus-visible {
  color: var(--sk-accent-hover);
  text-decoration-color: currentColor;
}

/* An (i) beside a heading or a label: the explanation lives in its tooltip
   instead of a paragraph under the content (Attack's Simulator, "Hits until a
   successful grab"). Bare - no box, no border - and sized in em so it follows
   the text it sits beside. Element-qualified past general.css's button rule
   (background, border, padding, min-width); colour !important because that
   rule forces color: white !important on every <button>. */
button.sk-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6em;
  height: 1.6em;
  min-width: 0;
  margin: 0 0 0 var(--sk-space-1);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  box-shadow: none;
  color: var(--sk-text-muted) !important;
  font-size: 1.1em;
  line-height: 1;
  vertical-align: middle;
  cursor: help;
}

button.sk-info-btn:hover,
button.sk-info-btn:focus-visible {
  border: 0;
  background: none;
  box-shadow: none;
  color: var(--sk-accent) !important;
}

/* The tooltip's own layout: artwork held at the left edge, copy ranged right
   against it.

   The artwork is absolutely positioned and the text column is padded clear of
   it by MORE than the picture is wide, so the two never share space no matter
   how the copy wraps. Reserving the room with padding rather than floating the
   image is what guarantees that - a float would let a long line wrap underneath
   and run back across the picture. */
.sk-tip-card {
  position: relative;
  display: block;
  text-align: left;
}

/* The artwork variant. Carried as an explicit class from the model rather than
   detected with :has() - the selector is only decided at render time inside a
   tooltip, where a miss is invisible until someone hovers and reports it. */
.sk-tip-card--art {
  /* Floor for the picture's height, so a one-line tooltip still shows a
     reasonable amount of it rather than a letterbox slice. */
  min-height: 96px;
  /* Less than the picture is wide. A square render sized to the card's height
     is about as wide as it is tall, so the copy laps a few pixels over the
     artwork's faded edge - which is wanted here, not an accident. */
  padding-left: 84px;
}

/* Full bleed, top to bottom, width following the aspect ratio. object-position
   pins it left so a wide render crops off its right side, under the copy, and
   never off the outer edge.

   max-width: none matters: .tooltip-inner inherits the app's img{max-width:100%}
   which would cap the picture at the tooltip's width and stop it filling the
   height. The mask fades its right edge out so the text laps onto softness
   rather than onto a hard cut. */
.sk-tip-card--art > .sk-tip-art {
  position: absolute;
  /* Bled out through .tooltip-inner's padding (set to 10px/12px just below) so
     the picture reaches the tooltip's real edges. Without this it is inset by
     the padding and stops short of full height, which is what "the image is
     still small" looked like. */
  left: -12px;
  top: -10px;
  height: calc(100% + 20px);
  width: auto;
  max-width: none;
  object-fit: contain;
  object-position: left center;
  opacity: 0.9;
  pointer-events: none;
  user-select: none;
  -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 1) 55%, rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 1) 55%, rgba(0, 0, 0, 0) 100%);
}

/* Above the artwork, and ranged right so the copy stacks against the picture
   instead of running back across it. Every level sets its own alignment rather
   than inheriting: .tooltip-inner centres its text, and one child re-asserting
   that default is all it takes to break the column. */
.sk-tip-card--art > .sk-tip-text {
  position: relative;
  z-index: 1;
}

.sk-tip-card--art > .sk-tip-text,
.sk-tip-card--art .sk-tip-title,
.sk-tip-card--art .sk-tip-body,
.sk-tip-card--art .sk-tip-stat {
  text-align: right;
}

.sk-tip-text,
.sk-tip-title,
.sk-tip-body,
.sk-tip-stat {
  display: block;
}

.sk-tip-title {
  padding-bottom: var(--sk-space-1);
  margin-bottom: var(--sk-space-1);
  border-bottom: 1px solid var(--sk-border-subtle);
  color: var(--sk-text);
  font-weight: var(--sk-weight-bold);
}

.sk-tip-body {
  color: var(--sk-text-muted);
  /* The seeded UniverseEvents.ToolTip text is a sentence, sometimes two lines
     joined by a <br>. It needs leading; .tooltip-inner does not give it any. */
  line-height: 1.45;
}

.sk-tip-stat {
  margin-top: var(--sk-space-2);
  color: var(--sk-accent-hover);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* general.css caps .tooltip-inner at 300px, which was right when the tooltip
   was only text. With 96px of that spent on the artwork the copy would be left
   about 190px, and "-20% exploration cost and -10% building cost" would break
   into five lines. Widened only for tooltips carrying this card, and only by
   enough to leave the text roughly the width it had before.

   Four classes deep because the general.css rule it overrides is three, and
   this file loads after it - specificity has to win outright, not on order. */
/* general.css caps .tooltip-inner at 300px, which was right when the tooltip was
   only text. The artwork now takes 84px of the width, and the padding is stated
   here so the picture has a known amount to bleed back out through.

   Four classes deep because the general.css rule this overrides is three, and
   this file loads after it - specificity has to win outright, not on order.
   overflow: hidden clips the bled artwork to the tooltip's rounded corners. */
.buildings-tooltip.tooltip .tooltip-inner {
  max-width: 380px;
  padding: 10px 12px;
  overflow: hidden;
}

/* ---- Callout ---------------------------------------------------------------
   The banner that explains a mode or warns before an action ("Module purchases
   are now batched..."). Hue stripe on the leading edge, hue icon, body copy in
   full-strength text - never muted, because a notice nobody can read is worse
   than no notice. */
/* NOTE: this is a flex container, so it takes BLOCK children — an icon plus a
   <p>, or just a <p>. Putting raw inline content in it (text interleaved with
   <b>/<a>/<span>) makes every element and every run of text between them a
   separate flex item, and they lay out as narrow side-by-side columns instead
   of a sentence. Found the hard way on UniversalNews. If you are injecting a
   sentence with markup in it from code-behind, wrap it in one <p>. */
.sk-callout {
  display: flex;
  align-items: flex-start;
  gap: var(--sk-space-3);
  padding: var(--sk-space-3) var(--sk-space-4);
  margin-bottom: var(--sk-space-5);
  background: var(--sk-surface-inner);
  border: 1px solid var(--sk-border);
  border-left: 4px solid var(--sk-hue-deep, var(--sk-accent));
  border-radius: var(--sk-radius);
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  line-height: 1.6;
}

.sk-callout :where(i) {
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-lg);
}

.sk-callout :where(strong, b) {
  color: var(--sk-hue, var(--sk-text));
}

/* The base sheets give every <p> a 1rem bottom margin. In a callout the last
   one left that much empty space under the text, so a one-line callout's text
   sat in the middle of a taller box with its icon pinned to the top - it read
   as not vertically centred (Attack's "You have no SOK or SOM" note). */
.sk-callout > :where(p:last-child) {
  margin-bottom: 0;
}

/* And no top margin on a callout's own paragraphs (a page without Bootstrap's
   reboot keeps the browser's 1em). A callout of one paragraph centres its icon
   on the text, so a one-liner reads as vertically centred (Attack's "Protected
   by newbie laws", the owner, 2026-10-18); a callout with a block of several
   lines keeps the icon on the first line. */
.sk-callout > :where(p) {
  margin-top: 0;
}

.sk-callout:has(> p:only-of-type) {
  align-items: center;
}

.sk-callout:has(> p:only-of-type) > :where(i) {
  margin-top: 0;
}

/* ---- Verdict ---------------------------------------------------------------
   The outcome of a calculation, as one word over art: VICTORY / DEFEAT. Made
   for Attack's Summary card, where it takes whatever height the split leaves
   under the tiles - so its card is a flex column (sk-card--column) and this is
   the child that grows. min-height keeps it a block, not a sliver, on a phone,
   where the stacked card has no spare height to give it.

       <div class="sk-card sk-card--column"> ...tiles...
         <div class="sk-verdict sk-verdict--victory sk-hue-success">
           <strong class="sk-verdict-word">Victory</strong>
         </div>
       </div>

   No modifier = the neutral "nothing to judge yet" state: same box, muted,
   no art, with an sk-verdict-hint line instead of a word.

   sk-verdict--unknown (+ sk-hue-warning) = there is something to judge but
   not enough to judge it (Attack: a target picked, no defender units). The
   hue wash, its own art (a crest with a question mark) and the word, with
   the hint under it.

   Layers, back to front: the background-color hue wash, the art
   (background-image, contain - the PNGs carry their own transparency, so
   nothing of the art leaks outside its shape), the ::before scrim, the word.
   isolation: isolate makes the box its own stacking context, which is what
   lets the z-index: -1 scrim paint ABOVE the box's own background and still
   below the text.

   The scrim is a radial ellipse: 80% of the on-image surface across the band
   the word sits in, thinning to 35% at the edges so the art still reads.
   80%, not 70%: over a white highlight in the art, danger text measured
   2.58:1 on a 70% scrim (under the 3:1 large-text AA floor) and 3.74:1 on
   80%; success 3.13 / 4.54. The text-shadow halo is on top of that, not
   instead of it. */
.sk-verdict {
  position: relative;
  isolation: isolate;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 9rem;
  padding: var(--sk-space-4);
  overflow: hidden;
  background-color: var(--sk-surface-inner);
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  text-align: center;
}

.sk-verdict--victory,
.sk-verdict--defeat,
.sk-verdict--unknown {
  background-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 12%, var(--sk-surface-inner));
  border-color: color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 45%, transparent);
}

.sk-verdict--victory {
  background-image: url(../scripts/img/victory.png);
}

.sk-verdict--defeat {
  background-image: url(../scripts/img/defeat.png);
}

.sk-verdict--unknown {
  background-image: url(../scripts/img/unknown.png);
}

.sk-verdict--victory::before,
.sk-verdict--defeat::before,
.sk-verdict--unknown::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse 70% 60% at center,
    color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent) 55%,
    color-mix(in srgb, var(--sk-surface-on-image-solid) 35%, transparent) 100%);
}

/* letter-spacing trails the last letter as well, which would sit the word
   0.3em left of centre; the same amount of padding on the left evens it. */
.sk-verdict-word {
  padding-left: 0.3em;
  color: var(--sk-hue, var(--sk-text));
  font-family: var(--sk-font-heading);
  font-size: calc(var(--sk-text-2xl) * 1.5);
  font-weight: var(--sk-weight-bold);
  letter-spacing: 0.3em;
  line-height: 1.1;
  text-transform: uppercase;
  text-shadow:
    0 0 0.4em var(--sk-surface-on-image-solid),
    0 2px 2px var(--sk-surface-on-image-solid);
  /* "Expected victory" is two words at this size: where they do not fit one
     line they split evenly, EXPECTED over VICTORY, rather than leaving one
     word dangling. */
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.sk-verdict-hint {
  margin: 0;
  font-size: var(--sk-text-sm);
}

/* ---- Toast -----------------------------------------------------------------
   A short notice that appears, holds, and leaves. Replaces general.css's
   .toast-container / .custom-toast, which six pages had copied in along with
   their own showToast().

   The stack is created by js/sk-toast.js on first use and appended to <body>:
   never inside an sk-card (backdrop-filter makes the card the containing block
   for fixed descendants) and never a container a page has to remember.

   Its top edge is --sk-toast-top, measured by the script from the bottom of
   .top-info-bar, so a toast never covers the bar. The old stack sat at a fixed
   20px, on top of it. Below the modal (10000), above the drawer (9000). */
.sk-toast-stack {
  position: fixed;
  top: var(--sk-toast-top, var(--sk-space-4));
  right: var(--sk-space-4);
  z-index: 9500;
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-2);
  width: min(22rem, calc(100vw - 2 * var(--sk-space-4)));
  pointer-events: none;
}

.sk-toast {
  display: flex;
  align-items: center;
  gap: var(--sk-space-3);
  padding: var(--sk-space-3) var(--sk-space-4);
  background: var(--sk-surface-on-image);
  border: 1px solid var(--sk-border);
  border-left: 4px solid var(--sk-hue-deep, var(--sk-accent));
  border-radius: var(--sk-radius);
  box-shadow: var(--sk-shadow-lg);
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  line-height: 1.5;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(calc(var(--sk-space-2) * -1));
  transition: opacity var(--sk-transition-slow), transform var(--sk-transition-slow);
}

.sk-toast.is-open {
  opacity: 1;
  transform: translateY(0);
}

.sk-toast > :where(i) {
  flex: 0 0 auto;
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-lg);
}

/* The text takes the free width and wraps; the close button (sk-toast.js adds
   one to every toast) keeps its size at the far end. align-self: flex-start
   holds it on the FIRST line when the text wraps to several, on a phone too.
   The negative block margin lets the 26px circle overhang the padding rather
   than make a one-line toast taller than it was before the button existed. */
.sk-toast > :where(div) {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-toast-close {
  flex: 0 0 auto;
  align-self: flex-start;
  margin-block: calc(var(--sk-space-1) * -1);
  margin-right: calc(var(--sk-space-2) * -1);
}

/* ---- Dock ------------------------------------------------------------------
   A small bar that stays at the bottom of the viewport while its page scrolls:
   a running total and the one action that commits it. For a page where
   choices are staged across a long list and applied together, so the total
   and the button are never a scroll away from the row being changed.

   position: sticky, not fixed. Sticky keeps the dock inside the page's own
   column, so it lines up with the cards and never spans the left nav, and
   parks in the flow at the end of the page. It dies silently under any
   ancestor with overflow other than visible; none of Auth.Master's wrappers
   has one. Must NOT sit inside an sk-card, whose height would bound the stick.

   .sk-dock-items is the optional detail that opens ABOVE the bar, which is
   why it comes first in the markup. */
.sk-dock {
  position: sticky;
  bottom: var(--sk-space-2);
  z-index: 100;
  margin-top: var(--sk-space-4);
  padding: var(--sk-space-2) var(--sk-space-3);
  background: var(--sk-surface-on-image);
  backdrop-filter: var(--sk-blur);
  -webkit-backdrop-filter: var(--sk-blur);
  border: 1px solid var(--sk-border-accent);
  border-radius: var(--sk-radius);
  box-shadow: var(--sk-shadow-lg);
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
}

/* sk-dock--mobile: a phone-only dock holding a COPY of the page's action
   (js/sk-dock.js clicks the real one). The real button stays in its section at
   every width, so there is nothing to dock on a desktop. For long pages only -
   Buildings, Research, Military - where the button would otherwise be a scroll
   away on a phone. */
/* The dock floats: no bar, no box - the action alone over the page, with a soft
   shadow to lift it (review, 2026-10-19: the box around it looked heavy against the
   page). A readout riding along (Research's staged count) gets its own small backing so
   it stays readable over whatever scrolls under it. Outside the phone query because
   sk-dock--desktop shows the same dock on a desktop. */
.sk-dock--mobile {
  display: none;
  padding: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.sk-dock--mobile .sk-emblem {
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.65));
}

.sk-dock--mobile .sk-action-note {
  width: fit-content;
  margin: 0 auto var(--sk-space-1);
  padding: 2px var(--sk-space-3);
  background: var(--sk-surface-on-image);
  border-radius: 20px;
}

@media (max-width: 768px) {
  .sk-dock--mobile {
    display: block;
  }
}

/* sk-dock--desktop, beside sk-dock--mobile: the same floating copy on a desktop as
   well, for a page whose table is long there too (Research - team review,
   2026-10-22). js/sk-dock.js hides it at any width while the real button shows. */
@media (min-width: 768.02px) {
  .sk-dock--mobile.sk-dock--desktop {
    display: block;
  }
}

.sk-dock-bar {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  min-width: 0;
}

/* The action packs against the far end whatever precedes it. It never shrinks:
   .sk-btn--cta is overflow: hidden (for its hover sheen), which drops its flex
   min-width to 0, so a squeezed bar clipped its label ("Assign Scientist",
   "Submit All Changes (1") instead of taking the room from the readouts. */
.sk-dock-bar > :last-child {
  margin-left: auto;
  flex-shrink: 0;
}

/* The disclosure toggle is content-sized here. Its base rule is a full-width
   strip (button.sk-disclosure, width: 100% + vertical margin), which in this row
   claimed the whole bar, sat centred, and squeezed the CTA. (0,2,1) beats it. */
.sk-dock-bar button.sk-disclosure {
  width: auto;
  margin: 0;
  justify-content: flex-start;
}

/* An action-page dock with no running total is the action alone (slot 9 of
   the action-page anatomy): centred, the same place a primary action sits on
   every page, rather than pushed to the right edge with nothing beside it. */
.sk-dock-bar > :only-child {
  margin-inline: auto;
}

/* Compact CTA: the bar is a readout, not a hero. (0,2,1) beats §4b's
   button.sk-btn--cta at (0,1,1). NOT an emblem button: its padding is what
   clears the medallion, and at (0,3,1) - the emblem rule's own weight - this
   rule won on source order and ran the label under the disc. */
.sk-dock input[type="submit"].sk-btn--cta:not(.sk-btn--emblem),
.sk-dock button.sk-btn--cta:not(.sk-btn--emblem),
.sk-dock a.sk-btn--cta:not(.sk-btn--emblem) {
  padding: var(--sk-space-1) var(--sk-space-4);
}
/* A quiet line right above a CTA saying what it will commit - Explore's running cost
   over its button, or the staged count over a phone dock's copy of Assign / Submit
   (Research, Modules). The same muted xs text as an sk-disclosure summary. */
.sk-action-note {
  padding-bottom: var(--sk-space-1);
  text-align: center;
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

.sk-dock-items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-1) var(--sk-space-2);
  max-height: 30dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-bottom: var(--sk-space-2);
  padding-bottom: var(--sk-space-2);
  border-bottom: 1px solid var(--sk-border-subtle);
}

/* Chips, not headline pills: a leading cancel button, no lift, no shadow. */
.sk-dock-items > .sk-pill {
  padding: 2px var(--sk-space-3) 2px 2px;
  font-size: var(--sk-text-xs);
  box-shadow: none;
}

.sk-dock-items > .sk-pill:hover {
  transform: none;
}

@media (max-width: 768px) {
  .sk-dock {
    bottom: 0;
    border-radius: var(--sk-radius) var(--sk-radius) 0 0;
  }
}

/* ---- Section title ---------------------------------------------------------
   The accent-coloured uppercase heading used for KINGDOM DETAILS / RANKINGS.
   Not a card header - it labels a band inside one. */
.sk-section-title {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  margin: 0 0 var(--sk-space-3);
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-bold);
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--sk-hue, var(--sk-accent));
}

/* A short tick before the label, and a hairline running out to the right.
   Reads as a readout header rather than a plain heading. */
.sk-section-title::before {
  content: '';
  flex: 0 0 auto;
  width: 3px;
  height: 0.95em;
  background: currentColor;
}

.sk-section-title::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(90deg,
      color-mix(in srgb, currentColor 55%, transparent) 0%,
      transparent 100%);
}

/* ---- Text sitting directly on the starfield --------------------------------
   Page titles above the first card have no surface under them, so they are
   read against planets.jpg itself. White is necessary but NOT sufficient:
   measured against the image after its dark gradient, #ffffff gives 20:1 over
   the median backdrop but only 2.58:1 at p99 and 1.90:1 over the brightest
   regions - far below the 4.5:1 bar.

   The shadow is the fix. It puts a dark halo immediately around each glyph, so
   legibility no longer depends on which part of the image is behind it. Use
   this for ANY text that is not inside a card. */
.sk-on-image {
  color: #ffffff;
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.95),
    0 2px 8px rgba(0, 0, 0, 0.85),
    0 0 24px rgba(0, 0, 0, 0.6);
}

/* Secondary text on the image. Never use --sk-text-muted out here: #94a3b8 is
   tuned for the card surface and disappears against a bright starfield. */
.sk-on-image-muted {
  color: #dbe3ee;
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.95),
    0 2px 8px rgba(0, 0, 0, 0.85);
}

/* Divider carrying a hue, for splitting a card into bands. */
.sk-rule {
  height: 1px;
  border: 0;
  margin: var(--sk-space-4) 0;
  background: linear-gradient(90deg,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 70%, transparent) 0%,
      transparent 100%);
}

/* ---- Checkbox --------------------------------------------------------------
   A square tick box, for one option among several in a form the user submits.
   .sk-switch is the other half of this pair and is for a setting that takes
   effect the moment it is flipped.

   Canonical markup — the label WRAPS the input, deliberately unlike the sibling
   pattern .sk-switch uses:

       <label class="sk-checkbox">
         <input type="checkbox" />
         <span class="sk-checkbox-box"></span>
         <span>Use Shields</span>
       </label>

   Why wrapping, not <input><label for="...">: Web Forms rewrites ids. A control
   with runat="server" and id="chkShield" renders as
   "ContentPlaceHolder1_chkShield" unless ClientIDMode="Static" is set, so a
   `for=` pair silently stops toggling the box the moment the control moves into
   a different naming container. Wrapping needs no id at all.

   The input is transparent-but-present rather than display:none. The old
   .probe-shield-chk used display:none, which removes the control from the
   accessibility tree and from keyboard tab order outright. */
.sk-checkbox {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
  margin: 0;
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  cursor: pointer;
  user-select: none;
}

.sk-checkbox > input[type="checkbox"] {
  position: absolute;
  left: 0;
  width: 18px;
  height: 18px;
  margin: 0;
  opacity: 0;
  z-index: 2;
  cursor: pointer;
}

.sk-checkbox > input[type="checkbox"] + .sk-checkbox-box {
  position: relative;
  box-sizing: border-box;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  /* Solid. The old border was rgba(68,124,255,.5) — 2.06:1, under the 3:1
     a control edge needs. */
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius-sm);
  /* A checkbox is a field, not a raised plane — same surface as sk-input, and
     it takes the border from 2.66:1 to 4.4:1 against its own fill. */
  background: var(--sk-surface-field);
  transition: background-color var(--sk-transition), border-color var(--sk-transition);
}

.sk-checkbox > input[type="checkbox"]:checked + .sk-checkbox-box {
  background: var(--sk-accent);
  border-color: var(--sk-accent);
}

/* The tick, drawn from two borders of a rotated box. No glyph, so the page's
   font stack cannot affect it. */
.sk-checkbox > input[type="checkbox"]:checked + .sk-checkbox-box::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 1px;
  width: 5px;
  height: 10px;
  border: 2px solid var(--sk-text-on-accent);
  border-top: none;
  border-left: none;
  transform: rotate(45deg);
}

.sk-checkbox > input[type="checkbox"]:focus-visible + .sk-checkbox-box {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-checkbox > input[type="checkbox"]:disabled + .sk-checkbox-box {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---- Radio -----------------------------------------------------------------
   A round dot, for ONE choice among a few the user submits. Same wrapping-label
   markup and the same reasons as .sk-checkbox above; an optional second line
   (a muted explanation) goes in .sk-radio-note inside the text span:

       <label class="sk-radio">
         <input type="radio" name="order" />
         <span class="sk-radio-dot"></span>
         <span>Soldiers First<span class="sk-radio-note">Elite gets what is left</span></span>
       </label> */
.sk-radio {
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  gap: var(--sk-space-2);
  margin: 0;
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  cursor: pointer;
  user-select: none;
}

.sk-radio > input[type="radio"] {
  position: absolute;
  left: 0;
  top: 0;
  width: 18px;
  height: 18px;
  margin: 0;
  opacity: 0;
  z-index: 2;
  cursor: pointer;
}

.sk-radio > input[type="radio"] + .sk-radio-dot {
  position: relative;
  box-sizing: border-box;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border: 1px solid var(--sk-border);
  border-radius: 50%;
  background: var(--sk-surface-field);
  transition: background-color var(--sk-transition), border-color var(--sk-transition);
}

.sk-radio > input[type="radio"]:checked + .sk-radio-dot {
  border-color: var(--sk-accent);
}

.sk-radio > input[type="radio"]:checked + .sk-radio-dot::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--sk-accent);
}

.sk-radio > input[type="radio"]:focus-visible + .sk-radio-dot {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-radio > input[type="radio"]:disabled + .sk-radio-dot {
  opacity: 0.5;
  cursor: not-allowed;
}

.sk-radio-note {
  display: block;
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

/* ---- sk-settings-list: a table-settings sheet's rows --------------------
   Generalised from Buildings' own buildings-columns-list (buildings-page.css)
   - a checkbox or a lock, a label, and (COLUMNS only) reorder buttons, one
   row per <li>. Buildings' version stays where it is; this is for any OTHER
   settings sheet built the same way (Military's own #militaryColumns is the
   first). An <ol> for COLUMNS (order matters, list-style:none only strips
   the browser's own numbering - it does not change what the markup means);
   a <ul> with --plain for a flat DATA/READINGS/CONTROLS group, no reorder,
   no divider, an optional sk-hint caption under the label. */
.sk-settings-list {
  list-style: none;
  margin: var(--sk-space-4) 0;
  padding: 0;
}

.sk-settings-item {
  padding: var(--sk-space-2) 0;
  border-bottom: 1px solid var(--sk-border);
}

.sk-settings-item:last-child {
  border-bottom: 0;
}

/* Flat variant: no divider, and the list's own margin dropped so every
   item's spacing comes from its own padding alone - stacking a list margin
   on top of that made the gap between two DIFFERENT settings groups bigger
   than the gap between two items in the SAME group, for no reason a player
   could see once each group lost a sub-heading of its own. */
.sk-settings-list--plain {
  margin: 0;
}

.sk-settings-list--plain .sk-settings-item {
  border-bottom: 0;
}

/* The hint lines up with the label text, past the 18px tick box and its gap. */
.sk-settings-item .sk-hint {
  margin-top: var(--sk-space-1);
  padding-left: calc(18px + var(--sk-space-2));
}

/* Under a label + switch row (sk-row, the switch at the far end) there is no
   tick box in front of the label, so the hint starts where the label does. */
.sk-settings-item > .sk-row + .sk-hint {
  padding-left: 0;
}

/* A locked row: a lock where the tick box would be, the same label size. */
.sk-settings-locked {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
  color: var(--sk-text);
  font-size: var(--sk-text-sm);
}

.sk-settings-locked > i {
  width: 18px;
  color: var(--sk-text-muted);
  text-align: center;
}

/* An arrow at the end of the list: shown, visibly spent, not pressable. */
.sk-settings-item .sk-btn.is-inert {
  opacity: 0.35;
  pointer-events: none;
}


/* ---- Stacked field ---------------------------------------------------------
   sk-field-row puts a label BESIDE its control, which is what a settings page
   wants. This puts it ABOVE, which is what a compact action form wants — the
   shape Mine Gold's "Probes to send" and Probe's submit column both use.

       <div class="sk-field-stack-row">
         <div class="sk-field-stack sk-field-stack--grow">
           <label class="sk-field-stack-label" for="x">Probes to send</label>
           <input id="x" class="sk-input" />
         </div>
       </div>

   The label is a small uppercase cap, deliberately unlike sk-field-label: a cap
   sitting above its control needs less weight than one competing with it across
   a row. */
.sk-field-stack-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-4);
  align-items: flex-end;
}

@media (max-width: 576px) {
  .sk-field-stack-row {
    flex-direction: column;
    align-items: stretch;
  }
}

.sk-field-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-1);
}

/* Takes the leftover width, so a sibling submit column sizes to its content. */
.sk-field-stack--grow {
  flex: 1 1 auto;
}

/* Takes the leftover width and never more: a zero basis instead of the content's
   width. --grow starts from its content, so a select with a long option (the
   target picker's "Cookie Monster · (X:1;Y:5) · 16 ticks") claims that width and
   pushes the row's last control - the picker's Change button - onto a line of its
   own. With a zero basis the field shrinks first, down to its control's own floor
   (sk-select's min-width), and only then does the row wrap. Use it on the one
   field that should absorb the spare room in a row that must stay on one line. */
.sk-field-stack--fluid {
  flex: 1 1 0;
  min-width: 0;
}

/* ...and its options never size it. A zero basis is not enough on its own where
   the card is as wide as its content (the target picker on Missile, centred
   under the page header): the row's intrinsic width still counts the select's
   longest option, so the field grew and shrank with each sector loaded.
   contain: inline-size drops the options from that sum: the field takes all the
   room its row has left, never less than 18em. The open list can still be wider
   than the field - the browser sizes it to the options. On a phone the field
   takes the stacked column's full width (sk-row--stack-mobile). */
@media (min-width: 769px) {
  .sk-field-stack--fluid {
    contain: inline-size;
    min-width: 18em;
  }
}

/* Two thirds of the row from 769px up, centred - Bootstrap's col-md-8 width
   (and offset-md-2's centring) without its grid wrapper; a phone keeps the
   full width. Attack's target picker. */
@media (min-width: 769px) {
  .sk-block--two-thirds {
    width: calc(100% * 8 / 12);
    margin-inline: auto;
  }
}

/* One line of facts about something, icon + value each, wrapping; a group
   pushed to the end of the line (badges, a button). The icon takes the item's
   hue (sk-hue-land, -networth, ...). Attack's Target analytics line.

     <div class="sk-card sk-card--compact sk-infobar">
       <div class="sk-infobar-items">
         <span class="sk-infobar-item sk-hue-land"><i class="fas fa-globe"></i> 4,154</span>
       </div>
       <div class="sk-infobar-end"><span class="sk-badge">Offline</span> <button ...></button></div>
     </div> */
.sk-infobar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sk-space-3);
}

.sk-infobar-items {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sk-space-2) var(--sk-space-5);
  font-size: var(--sk-text-sm);
  color: var(--sk-text);
}

.sk-infobar-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
}

.sk-infobar-item > i {
  color: var(--sk-hue, var(--sk-text-muted));
}

.sk-infobar-end {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  margin-inline-start: auto;
}

/* A third of the width, centred, from 1200px; half between 769px and 1199px,
   where a third is too narrow for a card's figures; full width on a phone.
   Attack's Summary (the owner, 2026-10-18). */
@media (min-width: 769px) {
  .sk-block--third {
    width: 50%;
    margin-inline: auto;
  }
}

@media (min-width: 1200px) {
  .sk-block--third {
    width: calc(100% / 3);
  }
}

.sk-field-stack-label {
  font-family: var(--sk-font);
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-semibold);
  color: var(--sk-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}

/* Never uppercase on a phone: at 12px a column of capped unit names is a wall
   the eye has to spell out. The labels are written Capitalized in the markup,
   so dropping the transform is all it takes; the tracking goes with it. */
@media (max-width: 768px) {
  .sk-field-stack-label {
    text-transform: none;
    letter-spacing: 0;
  }
}

/* The same, at every width, for one group of fields: labels as written
   (Capitalized), where capped unit names do not fit their column on a computer
   either (Attack's defender units and readouts, the owner, 2026-10-18). */
.sk-field-groups--capitalized .sk-field-stack-label {
  text-transform: none;
  letter-spacing: 0;
}

/* ---- Pager -----------------------------------------------------------------
   Pages of a long result list (Target Finder): "Showing 21-40 of 1,204" and a
   rows-per-page select, then Prev, the page numbers and Next. Built from sk-btn:
   the arrows are sk-btn--icon with sk-btn--nudge-back/-fwd, a page number is a
   plain sk-btn and the current one sk-btn--cta with aria-current="page". The
   arrows at either end go inert (.is-inert + aria-disabled), never `disabled`.

       <nav class="sk-pager" aria-label="Result pages">
         <div class="sk-pager-info">
           <span>Showing 21-40 of 1,204</span>
           <label class="sk-row"><span>Rows</span><select class="sk-select">...</select></label>
         </div>
         <div class="sk-pager-nav">
           <button class="sk-btn sk-btn--icon sk-btn--nudge-back">...</button>
           <span class="sk-pager-pages sk-hide-mobile">
             <button class="sk-btn">1</button><span class="sk-pager-gap">&hellip;</span>
             <button class="sk-btn sk-btn--cta" aria-current="page">3</button> ...
           </span>
           <span class="sk-pager-of sk-hide-desktop">3 / 61</span>
           <button class="sk-btn sk-btn--icon sk-btn--nudge-fwd">...</button>
         </div>
       </nav>

   Desktop: info left, buttons right, one line while they fit. Phone: the nav
   comes first and spreads across the width (Prev, "3 / 61", Next - the page
   numbers hide), and the info line centres under it. */
.sk-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sk-space-3);
  margin: var(--sk-space-3) 0 var(--sk-space-5);
}

.sk-pager-info,
.sk-pager-nav,
.sk-pager-pages {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}

.sk-pager-info {
  gap: var(--sk-space-3);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
  font-variant-numeric: tabular-nums;
}

/* A page size is a two-digit count: sk-select's 160px floor is for content. */
.sk-pager-info select.sk-select {
  width: auto;
  min-width: 0;
}

.sk-pager-nav,
.sk-pager-pages {
  gap: var(--sk-space-1);
}

.sk-pager-pages > .sk-btn {
  min-width: 2.5em;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}

.sk-pager-gap {
  padding: 0 var(--sk-space-1);
  color: var(--sk-text-muted);
}

.sk-pager-of {
  min-width: 5em;
  text-align: center;
  color: var(--sk-text);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 768px) {
  .sk-pager {
    flex-direction: column;
    align-items: stretch;
  }

  .sk-pager-nav {
    order: -1;
    justify-content: space-between;
    flex-wrap: nowrap;
  }

  .sk-pager-info {
    justify-content: center;
  }
}

/* ---- Browse bar ------------------------------------------------------------
   Step through a list of places one at a time - Alliance View's alliances:
   home (your own), the previous one, a picker of them all, the next one. One
   line at every width, a phone included, in a compact card only as wide as
   the controls need (a page-width bar round four controls is mostly empty
   glass), centred. Sector View's navigator is the same shape, still on its
   own page rule (.sector-nav-card).

       <div class="sk-card sk-card--compact sk-browse">
         <nav class="sk-row sk-row--center sk-row--loose sk-browse-row" aria-label="Alliances">
           <a class="sk-btn sk-btn--icon" ...><i class="fas fa-home"></i></a>
           <a class="sk-btn sk-btn--icon sk-btn--nudge-back" ...>...</a>
           <select class="sk-select">...</select>
           <a class="sk-btn sk-btn--icon sk-btn--nudge-fwd" ...>...</a>
         </nav>
       </div>

   The row never wraps; the select gives instead: content-sized while it fits,
   shrinking (no 160px floor) when the line is short, so the arrows stay
   either side of it. Qualified select.sk-select against that rule's width, and
   the row's nowrap through .sk-browse: .sk-row's `wrap` comes later in this file
   at the same weight and would win a bare .sk-browse-row. */
.sk-browse {
  max-width: 500px;
  margin-inline: auto;
}

.sk-browse .sk-browse-row {
  flex-wrap: nowrap;
}

/* The buttons keep their 32px circle; only the select gives. Element restated
   (a.sk-btn / button.sk-btn) - a container rule on a bare .sk-btn loses. */
.sk-browse-row > a.sk-btn,
.sk-browse-row > button.sk-btn {
  flex-shrink: 0;
}

.sk-browse-row > select.sk-select {
  flex: 0 1 auto;
  width: auto;
  min-width: 0;
}

/* ---- Range pair ------------------------------------------------------------
   A min/max pair of fields under one label - Target Finder's Land, Networth
   and NW/Land filters. The two fields share the row evenly and stay side by
   side at every width, a phone included, with a muted dash between them.

       <div class="sk-field-stack">
         <span class="sk-field-stack-label" id="landLabel">Land</span>
         <div class="sk-range" role="group" aria-labelledby="landLabel">
           <input type="number" class="sk-input" placeholder="Min" aria-label="Minimum land">
           <span class="sk-range-sep" aria-hidden="true">&ndash;</span>
           <input type="number" class="sk-input" placeholder="Max" aria-label="Maximum land">
         </div>
       </div>

   sk-input's 160px floor is right for one field and wrong for two in a 14rem
   sk-grid--tight track: the pair would spill out of the track. Here each field
   flexes from 0 instead. (0,2,1) on purpose - it has to beat input.sk-input
   (width: 100%) and general.css's input[type="number"] (width: 80px), both
   (0,1,1). */
.sk-range {
  display: flex;
  align-items: center;
  gap: var(--sk-space-1);
}

.sk-range > input.sk-input {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  max-width: none;
}

.sk-range-sep {
  flex: 0 0 auto;
  color: var(--sk-text-muted);
}

/* Two selects can share the row the same way (Target Finder's "Rank by" and
   "Range"), or a select and its button or badge (the target picker's Sector [->]
   and its kill-target type): select.sk-select is width:100% with a 160px floor
   at (0,1,1).
   width stays 100%, not auto: flex-basis 0 already decides the size here, and a
   percentage width keeps the select compressible - its min-content is 0, while
   its max-content is still its longest option. With width:auto a long option
   (an alliance name, a kill target's "name · Sector · (X;Y) · Kill") became the
   pair's minimum, and on a phone, where sk-row--stack-mobile is a wrapping
   column whose line is as wide as its widest item, it pushed the whole picker
   past the card's edge. */
.sk-range > select.sk-select {
  flex: 1 1 0;
  width: 100%;
  min-width: 0;
  max-width: none;
}

/* ---- Field groups ----------------------------------------------------------
   A filter form in a few named groups side by side - Target Finder's Kingdom,
   Size, and Rank & reach. Three columns from 1200px, two from 769px (the third
   group wraps under the first), one on a phone. Inside a group every field is
   ONE row - label left (sk-field-stack-label), control right - so a form of
   nine fields is three rows tall on a desktop rather than nine.

       <div class="sk-field-groups">
         <div class="sk-field-group" role="group" aria-labelledby="g1">
           <span class="sk-eyebrow" id="g1">Size</span>
           <span class="sk-field-stack-label" id="landLabel">Land</span>
           <div class="sk-range" role="group" aria-labelledby="landLabel">...</div>
           <label class="sk-field-stack-label" for="type">Type</label>
           <select id="type" class="sk-select">...</select>
         </div>
       </div>

   The label track is max-content, so labels line up within a group and stay
   short; the control track takes the rest. Controls drop sk-input/sk-select's
   160px floor and 240px cap and fill their track - the group's width, not the
   field's, is what is sized. A control that is not a bare field (a wrapper
   holding one) is an sk-field-group-control. The group title is a SPAN
   eyebrow across both tracks: div.sk-eyebrow carries a section's top margin,
   which a group title must not. */
.sk-field-groups {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sk-space-4) var(--sk-space-6);
}

@media (min-width: 768.02px) {
  .sk-field-groups {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1200px) {
  .sk-field-groups {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* A tall group taking two rows, so a short group placed after it lands in
     the first column's free space instead of under the whole first row
     (Target Finder: Bookmarks / Notes under Kingdom, last on a phone). */
  .sk-field-group--span-2 {
    grid-row: span 2;
  }
}

.sk-field-group {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: center;
  align-content: start;
  gap: var(--sk-space-2) var(--sk-space-3);
  min-width: 0;
}

.sk-field-group > .sk-eyebrow {
  grid-column: 1 / -1;
  margin: 0;
}

.sk-field-group-control {
  min-width: 0;
}

.sk-field-group select.sk-select,
.sk-field-group button.sk-select-trigger {
  width: 100%;
  min-width: 0;
  max-width: none;
}

/* The same grid with bare sk-field-stacks for children instead of groups -
   one field per cell, label ABOVE its control - for a long run of short
   fields that should line up in columns (Attack's defender units and the
   Bonus / Research / Shields row under them). A label-beside-field group
   starts each control wherever its label ends, which is a ragged column per
   field. The control fills its track, like the group's controls above. */
.sk-field-groups > .sk-field-stack {
  min-width: 0;
}

.sk-field-groups > .sk-field-stack > input.sk-input,
.sk-field-groups > .sk-field-stack > select.sk-select {
  min-width: 0;
  max-width: none;
}

/* Eight fields as two groups of four standing side by side at every width:
   two columns filled top to bottom, four rows, so each group is a column
   (Attack's defender units: the defensive units left, Soldiers and the hybrids
   right). After the base rules, so it wins at every breakpoint. */
.sk-field-groups--pair-columns {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(4, auto);
  grid-auto-flow: column;
  column-gap: var(--sk-space-3);
}

/* Three fields on one row at every width (Attack's Bonus / Research /
   Shields). */
.sk-field-groups--thirds {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--sk-space-3);
}

/* A label that wraps pushes its own field down to the row's foot, so the
   fields in a row stay level. */
.sk-field-groups--pair-columns > .sk-field-stack,
.sk-field-groups--thirds > .sk-field-stack {
  justify-content: flex-end;
}

/* ---- Segmented control -----------------------------------------------------
   A row of mutually exclusive options in one inset track. Generalised from the
   probe quick-options grid.

   The track is a dark well, and every segment is its own rounded pill inside
   it, with a small gap between them. The first version butted the segments
   together with hairline dividers and filled the active one with a gradient
   and a bar under it, which read as a 1990s tab strip ("ugly" - the user, on
   Attack's Has power | Powerless). The gap does the separating the dividers
   did, and the active pill is told apart by a fill, an edge and a glow in its
   hue - a shape as well as a colour, so the state survives a washed-out phone
   screen in daylight.

   The track's border stays --sk-border: it is the control's own boundary and
   has to clear 3:1. overflow is visible because the active pill's glow sits
   outside it.

   Labels are letter-spaced capitals on a desktop and written case on a phone,
   like every other label in this file - so a segment's text is written
   Capitalized in the markup ("Has power"), and the capitals come from here. */
.sk-segmented {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-1);
  padding: var(--sk-space-1);
  background: var(--sk-surface-inner);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  box-shadow: inset 0 1px 3px var(--sk-surface-overlay);
}

.sk-segment {
  flex: 1;
  min-width: 60px;
  padding: var(--sk-space-2);
  border: 1px solid transparent;
  border-radius: var(--sk-radius-sm);
  background: transparent;
  color: var(--sk-text-muted);
  font-family: var(--sk-font);
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-medium);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--sk-transition),
    border-color var(--sk-transition),
    box-shadow var(--sk-transition),
    color var(--sk-transition);
}

@media (max-width: 768px) {
  .sk-segment {
    letter-spacing: 0;
    text-transform: none;
  }
}

/* A track sitting in a row of controls (Target selection: segments, two
   selects, Change) sizes to its content, and flex:1 segments then split the
   SUM of their labels evenly - so the longer label, bold when active, clipped
   ("BY ALLIANC"). Equal grid columns size every segment to the widest label
   instead; stacked on a phone, the grid still fills the line. The same in a
   section head's toolbar (Attack history's All / Successful / Failed, where
   "SUCCESSFUL" overflowed its third of the track). */
.sk-row > .sk-segmented,
.sk-section-head > .sk-segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}

/* A neutral lift, not a hue: hovering an inactive segment must never look like
   selecting it, and a segment with its own hue (Powerless) would otherwise
   preview red under the cursor. */
.sk-segment:hover,
button.sk-segment:hover,
input[type="submit"].sk-segment:hover,
input[type="button"].sk-segment:hover {
  background: color-mix(in srgb, var(--sk-text) 6%, transparent);
  color: var(--sk-text);
}

.sk-segment:focus-visible,
button.sk-segment:focus-visible,
input[type="submit"].sk-segment:focus-visible,
input[type="button"].sk-segment:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 1px;
}

/* The current segment: filled in its hue, edged in it, a soft glow round it,
   and its label in the hue's light tint (the hue mixed into the text colour,
   so it clears contrast on the dark fill for every hue).

   The box-shadow is two layers on purpose, an inset highlight then the outer
   glow: sk-blink animates a list of the same shape (an inset wash, then a
   glow), so a blinking active segment fades smoothly instead of flipping. */
.sk-segment.is-active,
button.sk-segment.is-active,
input[type="submit"].sk-segment.is-active,
input[type="button"].sk-segment.is-active {
  background: color-mix(in srgb, var(--sk-accent) 24%, transparent);
  border-color: color-mix(in srgb, var(--sk-accent) 70%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--sk-text) 10%, transparent),
    0 0 10px color-mix(in srgb, var(--sk-accent) 35%, transparent);
  color: color-mix(in srgb, var(--sk-accent) 55%, var(--sk-text));
  font-weight: var(--sk-weight-bold);
}

/* An option that cannot be chosen right now (Missile's empty warheads): still
   in the track, so the set reads the same every time, but dimmed and inert. */
.sk-segment:disabled,
.sk-segment:disabled:hover {
  background: transparent;
  color: var(--sk-text-muted);
  opacity: 0.45;
  cursor: not-allowed;
}

/* Hover must not walk on top of the selected state - without this, the
   generic :hover above repaints the active segment with the neutral lift and
   the row momentarily has no readable selection at all. */
.sk-segment.is-active:hover,
button.sk-segment.is-active:hover,
input[type="submit"].sk-segment.is-active:hover,
input[type="button"].sk-segment.is-active:hover {
  background: color-mix(in srgb, var(--sk-accent) 32%, transparent);
  color: color-mix(in srgb, var(--sk-accent) 55%, var(--sk-text));
}

/* An active segment carrying its OWN sk-hue-* class takes that hue instead of
   the accent - Attack's Has power (sk-hue-success) | Powerless
   (sk-hue-danger). Only the segment's own class counts, not a hue inherited
   from a hued card around the control, so the default stays the accent. The
   chips variant has its own hue rules and is left out. */
.sk-segmented:not(.sk-segmented--chips) > .sk-segment.is-active[class*="sk-hue-"] {
  background: color-mix(in srgb, var(--sk-hue) 24%, transparent);
  border-color: color-mix(in srgb, var(--sk-hue) 70%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--sk-text) 10%, transparent),
    0 0 10px color-mix(in srgb, var(--sk-hue) 35%, transparent);
  color: color-mix(in srgb, var(--sk-hue) 55%, var(--sk-text));
}

/* A hued segment that is NOT chosen keeps a muted tint of its hue, so both
   ends of the choice read at a glance (Attack's Powerless in muted red while
   Has power is on, and the other way round - the owner, 2026-10-18).
   !important because general.css forces color: white !important on every
   <button>, which no normal declaration beats (it is why every inactive
   segment rendered white); this one wins that tie on specificity. */
.sk-segmented:not(.sk-segmented--chips) > .sk-segment:not(.is-active)[class*="sk-hue-"] {
  color: color-mix(in srgb, var(--sk-hue) 55%, var(--sk-text-muted)) !important;
}

.sk-segmented:not(.sk-segmented--chips) > .sk-segment.is-active[class*="sk-hue-"]:hover {
  background: color-mix(in srgb, var(--sk-hue) 32%, transparent);
  color: color-mix(in srgb, var(--sk-hue) 55%, var(--sk-text));
}

.sk-segment.is-disabled,
button.sk-segment.is-disabled,
input[type="submit"].sk-segment.is-disabled,
input[type="button"].sk-segment.is-disabled {
  cursor: default;
  pointer-events: none;
  opacity: 0.4;
}

/* ---- Segment: the same defensive layer the buttons need (see 4b) ----------
   A segment is a <button>, so both base sheets style it as one BEFORE any of
   the rules above get a say:

     _main.css:7       input[type="submit"]:not(.table-col-submit),
                       button:not(.table-col-submit)
                       -> background-color, color, border, border-radius: 0,
                          padding, min-width: 160px, font-weight: 700 !important

   `button:not(.table-col-submit)` is (0,1,1) - one element plus the class
   inside :not() - and a bare `.sk-segment` is (0,1,0). It loses every one of
   those, which is why a segmented control came out as a row of navy boxes with
   a 160px floor under each: three artwork options needing 480px inside a
   drawer that is not that wide.

   `.sk-segment.is-active` at (0,2,0) already wins, so the SELECTED segment
   looked right while the rest did not - which is the tell, and it is why this
   was easy to miss.

   Qualifying the element reaches (0,1,1), and this file loads after both base
   sheets, so the tie breaks our way. min-width is restated at the component's
   own 60px rather than at 0: these tracks are meant to have a floor, just not
   that one.

   The border is a real 1px TRANSPARENT one, restated here rather than left to
   the base rule: _main.css's `button:hover, button:focus` white border-color
   is (0,1,1) too, and a segment's border is no longer zero-width, so without
   this tie (which this file wins on order) a hovered or focused inactive pill
   grew a white edge.

   font-weight carries !important because the base sheet's does, and there is
   no other way past it - it is the one declaration here that cannot be won on
   specificity. is-active is unaffected: it asks for bold, which is what the
   base was forcing anyway. */
button.sk-segment,
input[type="submit"].sk-segment,
input[type="button"].sk-segment {
  min-width: 60px;
  padding: var(--sk-space-2);
  border: 1px solid transparent;
  border-radius: var(--sk-radius-sm);
  background: transparent;
  color: var(--sk-text-muted);
  font-weight: var(--sk-weight-medium) !important;
}

/* is-active is (0,2,0) and beats the block above on every property it sets -
   except the one that had to be !important. This gives it back. */
.sk-segment.is-active,
button.sk-segment.is-active,
input[type="submit"].sk-segment.is-active,
input[type="button"].sk-segment.is-active {
  font-weight: var(--sk-weight-bold) !important;
}

/* A segment with the `disabled` attribute - an <asp:Button Enabled="False">,
   which cannot carry is-disabled's wrapper-free pointer-events trick because
   ASP.NET writes the attribute. general.css:693 paints a disabled input or
   button white with `color: black !important`, and :703 repeats it on hover at
   (0,3,1); these restate the muted, faded segment over both, colour included.
   pointer-events comes back (general.css sets none) so a tooltip saying WHY it
   is off can still show; a disabled control never fires click either way. */
button.sk-segment:disabled,
input[type="submit"].sk-segment:disabled,
input[type="button"].sk-segment:disabled,
button.sk-segment:disabled:hover,
input[type="submit"].sk-segment:disabled:hover,
input[type="button"].sk-segment:disabled:hover {
  background: transparent;
  color: var(--sk-text-muted) !important;
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: auto;
}

/* general.css:703 also whitens the border on hover; the segment's own stays. */
button.sk-segment:disabled:hover,
input[type="submit"].sk-segment:disabled:hover,
input[type="button"].sk-segment:disabled:hover {
  border-color: var(--sk-border-subtle);
}

/* ---- Chips variant ---------------------------------------------------------
   The same control with the track taken away: each option becomes its own chip,
   carrying the hue and the icon of the thing it filters to.

   The plain version keeps a shared inset track around its pills, which is right
   where the options are one quantity at different settings (Probe's -10% / 4x
   Land) or one either/or (Attack's power status), which is why this is a
   variant and not a replacement.

   The point of it is that the hue is not decoration. A chip wears the SAME hue
   and the SAME icon as the badges on the rows it shows, so the filter row is
   also the table's legend and the two are impossible to learn separately. That
   is why the hue lives on the markup as an sk-hue-* class per chip rather than
   being set here - the news room takes it from NewsRoomService.TypeHue, the one
   place that decides what colour a news type is.

   Chips size to their content and wrap, rather than splitting the width evenly.
   Six equal columns is what forced "ALLIANCE" and "AID 0" to reserve the same
   space, and on a phone it is the difference between two tidy rows and six
   cramped ones. */
.sk-segmented--chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-2);
  /* The track is the thing being removed: its border, its well and its inner
     padding all go, so the chips sit on the card itself. overflow stays
     visible or the glow and the corner brackets on the active chip are
     clipped away. */
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: visible;
}

/* Specificity note: this is (0,2,0), which beats the base `.sk-segment` and
   the element-qualified button.sk-segment (0,1,1) on weight. The border
   shorthand restates the whole edge; do not narrow it to border-color.

   A chip keeps the case its label is written in at every width: the plain
   segment's desktop capitals are for a short either/or, and a chip is a
   legend entry with an icon and a count beside its word. */
.sk-segmented--chips .sk-segment {
  position: relative;
  /* Content-sized, not an equal share of the row. */
  flex: 0 1 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-2);
  padding: var(--sk-space-2) var(--sk-space-3);
  border: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 32%, transparent);
  border-radius: var(--sk-radius);
  background: var(--sk-surface-table);
  color: var(--sk-text-muted);
  letter-spacing: normal;
  text-transform: none;
  transition:
    background var(--sk-transition),
    border-color var(--sk-transition),
    box-shadow var(--sk-transition),
    color var(--sk-transition);
}

/* The icon carries the hue while the chip is resting, which is what lets the
   row work as a legend without shouting - the label stays muted, and colour is
   never the only thing telling the two apart because the icon differs too. */
.sk-segmented--chips .sk-segment > :where(i) {
  color: var(--sk-hue, var(--sk-accent));
  font-size: var(--sk-text-xs);
}

/* The count is the figure a player actually scans for - "how many attacks
   today" - so it is set brighter than its own label and in tabular figures, so
   a column of chips has its numbers land in the same place. */
.sk-segmented--chips .sk-news-count {
  color: var(--sk-text);
  font-variant-numeric: tabular-nums;
  font-weight: var(--sk-weight-bold);
}

.sk-segmented--chips .sk-segment:hover {
  border-color: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 60%, transparent);
  background: var(--sk-surface-raised);
  color: var(--sk-text);
}

/* Separate chips, so the ring sits outside them - the base insets it because
   there it is flush against the track's own border. */
.sk-segmented--chips .sk-segment:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

/* Selected: the chip fills with its own hue's gradient and lights up. Each
   --sk-hue-grad is documented with its contrast against white where it is
   defined, which is what makes --sk-text-on-accent safe on all of them. */
.sk-segmented--chips .sk-segment.is-active {
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  border-color: var(--sk-hue, var(--sk-accent));
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent));
  color: var(--sk-text-on-accent);
  font-weight: var(--sk-weight-bold);
}

.sk-segmented--chips .sk-segment.is-active > :where(i),
.sk-segmented--chips .sk-segment.is-active .sk-news-count {
  color: var(--sk-text-on-accent);
}

.sk-segmented--chips .sk-segment.is-active:hover {
  background: var(--sk-hue-grad, var(--sk-grad-accent));
  color: var(--sk-text-on-accent);
}

/* Corner brackets on the selected chip, the same two-bracket figure and the
   same 2px weight sk-card--hud uses, scaled to a control. This is the second
   channel the state needs: a shape as well as a colour, so which filter is
   applied survives a phone screen in daylight and a player who cannot separate
   the gold chip from the red one. */
.sk-segmented--chips .sk-segment.is-active::before,
.sk-segmented--chips .sk-segment.is-active::after {
  content: '';
  position: absolute;
  width: 8px;
  height: 8px;
  border-style: solid;
  border-color: var(--sk-hue, var(--sk-accent));
  pointer-events: none;
}

.sk-segmented--chips .sk-segment.is-active::before {
  top: -3px;
  left: -3px;
  border-width: 2px 0 0 2px;
  border-top-left-radius: var(--sk-radius-sm);
}

.sk-segmented--chips .sk-segment.is-active::after {
  right: -3px;
  bottom: -3px;
  border-width: 0 2px 2px 0;
  border-bottom-right-radius: var(--sk-radius-sm);
}

/* An empty bucket keeps its icon and its zero - it is information that there
   were no missiles today, and a chip that vanished would make the row's shape
   change every time the period does. */
.sk-segmented--chips .sk-segment.is-disabled {
  background: transparent;
}

/* ---- A link that does not look like one ------------------------------------
   For linking a heading, a table cell or a card title, where the surrounding
   type is already carrying the styling and a blue underlined link would fight
   it. The affordance comes back on hover and focus, so it is still discoverable
   and still keyboard-visible — it is not a link disguised permanently as text. */
.sk-link-inherit {
  color: inherit;
  text-decoration: none;
}

.sk-link-inherit:hover,
.sk-link-inherit:focus-visible {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- A named entity inside prose -------------------------------------------
   An alliance or kingdom name called out mid-sentence, so a wall of explanation
   can be scanned for the party it is about.

   The brackets are generated content, not markup: they are decoration, and
   keeping them out of the text means a name stays clean when it is selected,
   copied, or read by a screen reader. */
.sk-name {
  color: var(--sk-warning);
  font-weight: var(--sk-weight-bold);
}

.sk-name::before {
  content: "[";
  opacity: 0.55;
}

.sk-name::after {
  content: "]";
  opacity: 0.55;
}

/* ---- Page header -----------------------------------------------------------
   The strip every page opens with: the page's name on a lit, pointed plate, a
   hexagonal medallion holding the page's art at its left end, a second hexagon
   of the same size peeking out behind it, and a streak of light off
   each side.

     <div class="sk-page-header">
       <span class="sk-page-header-art"><img src="img/buttons/build-icon.webp" alt=""></span>
       <h1 class="sk-page-header-title">Construction Site</h1>
     </div>

   NAME ONLY. It used to carry "Welcome back, <ruler>" and a sentence of
   instructions; the name is all a header is for. A page whose code-behind still
   writes lblRulerName keeps the control inside a hidden PlaceHolder - removing
   it would make those assignments throw.

   The art is optional and decoration (alt="") - the title names the page. A
   page without a render gets the same plate with the name alone.

   HOW IT IS BUILT, in one element and no wrapper. The header is a four-column
   grid: [streak] [art] [title] [streak]. Its two pseudo-elements are the plate
   - ::before the edge, ::after the face one edge-width in - spanning columns 2
   and 3 behind the content. Two stacked clip-paths is the only way to trace a
   clipped outline (clip-path takes a border with it), the same reason the
   emblem button is built from two layers; see section 4c.

   THE STREAKS are the header's own background: two 2px lines, each fading in
   from the page edge and finishing under the plate, whose opaque face hides
   where they meet.

   THE MEDALLION is taller than the plate, by negative margin rather than by
   positioning, so the row is as tall as the plate and the art hangs over it.
   The span is the twin hexagon (its own background, clipped); ::before is the
   main hexagon's edge, ::after its face, and the image is the top layer with no
   background of its own, so it is the only thing the glitch touches.

   The hue is --sk-hue-deep, falling back to the accent, as the emblem does. */
.sk-page-header {
  --sk-header-art: 80px;
  --sk-header-cut: 22px;
  --sk-header-twin: 5px;
  --sk-header-hue: var(--sk-hue-deep, var(--sk-accent));
  --sk-header-hex: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%);
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: 1fr auto minmax(0, auto) 1fr;
  align-items: center;
  margin: var(--sk-space-3) 0;
  background:
      linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--sk-header-hue) 85%, transparent) 100%)
          left center / 50% 2px no-repeat,
      linear-gradient(270deg, transparent 0%, color-mix(in srgb, var(--sk-header-hue) 85%, transparent) 100%)
          right center / 50% 2px no-repeat;
  /* Drop-shadow, not box-shadow: it follows the clipped plate and the streaks.
     Both stops are color-mix() - see --sk-emblem-glow for why. */
  filter:
    drop-shadow(0 0 5px color-mix(in srgb, var(--sk-header-hue) 75%, transparent))
    drop-shadow(0 0 18px color-mix(in srgb, var(--sk-header-hue) 45%, transparent));
  text-align: left;
}

@media (min-width: 768.02px) {
  .sk-page-header {
    margin-block: var(--sk-space-6);
  }
}

.sk-page-header::before,
.sk-page-header::after {
  content: "";
  grid-area: 1 / 2 / 2 / 4;
  z-index: -1;
  align-self: stretch;
  clip-path: polygon(
      0 50%,
      var(--sk-header-cut) 0,
      calc(100% - var(--sk-header-cut)) 0,
      100% 50%,
      calc(100% - var(--sk-header-cut)) 100%,
      var(--sk-header-cut) 100%);
}

.sk-page-header::before {
  /* Starts where the main hexagon does: the twin hangs out to its left. */
  margin-left: var(--sk-header-twin);
  background: linear-gradient(180deg,
      var(--sk-header-hue) 0%,
      color-mix(in srgb, var(--sk-header-hue) 55%, var(--sk-surface-page)) 50%,
      var(--sk-header-hue) 100%);
}

.sk-page-header::after {
  margin: 2px 2px 2px calc(var(--sk-header-twin) + 2px);
  /* The page's hue is the plate's colour, not a hint of it: a deep tone of it
     at the top, falling to a darker one at the foot. Each page sets its hue with
     an sk-hue-* class on the header, matching the colour of its submit button. */
  background:
      linear-gradient(90deg,
          color-mix(in srgb, var(--sk-header-hue) 30%, transparent) 0%,
          transparent 65%),
      linear-gradient(180deg,
          color-mix(in srgb, var(--sk-header-hue) 55%, var(--sk-neutral-1)) 0%,
          color-mix(in srgb, var(--sk-header-hue) 32%, var(--sk-surface-on-image-solid)) 55%,
          color-mix(in srgb, var(--sk-header-hue) 22%, var(--sk-surface-on-image-solid)) 100%);
}

.sk-page-header-art {
  position: relative;
  grid-column: 2;
  grid-row: 1;
  display: grid;
  place-items: center end;
  /* Wide enough for the twin hexagon as well: the span IS the twin (its own
     background, clipped to a hexagon at its left end) and the main hexagon sits
     in the right-hand --sk-header-art of it. Every layer is then a plain child
     of one clipped box, and the image can be the only thing that glitches. */
  width: calc(var(--sk-header-art) + var(--sk-header-twin));
  height: var(--sk-header-art);
  /* Hangs over the plate: the row only counts 72% of its height. */
  margin: calc(var(--sk-header-art) * -0.14) 0 calc(var(--sk-header-art) * -0.14) var(--sk-space-4);
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--sk-header-hue) 75%, var(--sk-neutral-1)) 0%,
      color-mix(in srgb, var(--sk-header-hue) 45%, var(--sk-surface-page)) 100%);
  /* The union of the two hexagons - the twin at 0, the main one --sk-header-twin
     to its right - which for two same-height hexagons this close is one
     elongated hexagon. It has to reach the main one's right vertex
     (twin + art); stopping at `art` cut the main hexagon in half. */
  clip-path: polygon(
      0 50%,
      calc(var(--sk-header-art) * 0.25) 3%,
      calc(var(--sk-header-twin) + var(--sk-header-art) * 0.75) 3%,
      calc(var(--sk-header-twin) + var(--sk-header-art)) 50%,
      calc(var(--sk-header-twin) + var(--sk-header-art) * 0.75) 97%,
      calc(var(--sk-header-art) * 0.25) 97%);
}

/* The main hexagon's edge. */
.sk-page-header-art::before {
  content: "";
  position: absolute;
  inset: 0 0 0 var(--sk-header-twin);
  background: linear-gradient(180deg,
      var(--sk-header-hue) 0%,
      color-mix(in srgb, var(--sk-header-hue) 55%, var(--sk-surface-page)) 50%,
      var(--sk-header-hue) 100%);
  clip-path: var(--sk-header-hex);
}

/* Its face - the plate colour, so the medallion belongs to it. This is its own
   layer rather than the image's background, so the image can glitch alone. */
.sk-page-header-art::after {
  content: "";
  position: absolute;
  inset: 3px 3px 3px calc(var(--sk-header-twin) + 3px);
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--sk-header-hue) 22%, var(--sk-neutral-1)) 0%,
      var(--sk-surface-on-image-solid) 100%);
  clip-path: var(--sk-header-hex);
}

.sk-page-header-art > img {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  width: calc(var(--sk-header-art) - 6px);
  height: calc(var(--sk-header-art) - 6px);
  margin-right: 3px;
  padding: 5%;
  object-fit: contain;
  /* No background and no clip: just the art. Renders in this set are
     dark-cored and painted for a light ground, so they are lifted to read on
     the plate colour behind them - line art that is already light needs no
     help, and is what this is designed for. */
  filter:
    brightness(1.9) contrast(1.05)
    drop-shadow(0 0 4px color-mix(in srgb, var(--sk-header-hue) 80%, transparent));
  /* A few seconds of hologram tremor when the page opens, then it stays lit:
     `forwards` holds the last keyframe's glow instead of dropping back to
     the plain filter above. */
  transform-origin: 50% 50%;
  animation: skHeaderGlitch 2.6s steps(1, end) 0.4s 1 forwards;
}

.sk-page-header-title {
  position: relative;
  grid-column: 3;
  grid-row: 1;
  margin: 0;
  padding: var(--sk-space-4) calc(var(--sk-header-cut) + var(--sk-space-4)) var(--sk-space-4) var(--sk-space-4);
  font-family: var(--sk-font-heading);
  font-size: var(--sk-text-xl);
  font-weight: var(--sk-weight-bold);
  letter-spacing: 0.5px;
  line-height: 1.2;
  color: var(--sk-text);
  text-shadow:
    0 0 12px color-mix(in srgb, var(--sk-header-hue) 60%, transparent),
    0 1px 2px rgba(0, 0, 0, 0.8);
  overflow-wrap: anywhere;
}

/* The divider between the medallion and the name. */
.sk-page-header-art + .sk-page-header-title::before {
  content: "";
  position: absolute;
  top: 22%;
  bottom: 22%;
  left: 0;
  width: 2px;
  background: linear-gradient(180deg,
      transparent 0%,
      color-mix(in srgb, var(--sk-header-hue) 80%, var(--sk-text)) 50%,
      transparent 100%);
}

/* No art: the name has the whole plate, so it gets the left cut's room too. */
.sk-page-header > .sk-page-header-title:first-child {
  padding-left: calc(var(--sk-header-cut) + var(--sk-space-4));
}

/* Hologram tremor, on the art inside the hexagon and nothing else. Two short
   bursts about the image's own centre: a couple of pixels of jitter and a thin
   red / blue split, nothing that moves far. steps(1, end) makes every keyframe
   a hard cut, which is what a glitch is. Once, on load, and it settles into the
   glow (the 18 / 68 / 100% frames are all the same lit state), which the
   animation's `forwards` fill keeps. The base filter is restated in every
   frame: a filter list only animates from a list of the same shape. */
@keyframes skHeaderGlitch {
  0% {
    transform: none;
  }
  10%, 60% {
    transform: translate(2px, 0);
    filter:
      brightness(1.9) contrast(1.05)
      drop-shadow(-2px 0 0 var(--sk-alert))
      drop-shadow(2px 0 0 var(--sk-accent));
  }
  14%, 64% {
    transform: translate(-2px, 0) scale(1.02);
    filter:
      brightness(2.1) contrast(1.1)
      drop-shadow(2px 0 0 var(--sk-alert))
      drop-shadow(-2px 0 0 var(--sk-accent));
  }
  18%, 68%, 100% {
    transform: none;
    filter:
      brightness(1.9) contrast(1.05)
      drop-shadow(0 0 3px color-mix(in srgb, var(--sk-header-hue) 90%, transparent))
      drop-shadow(0 0 9px color-mix(in srgb, var(--sk-header-hue) 65%, transparent));
  }
}

@media (prefers-reduced-motion: reduce) {
  .sk-page-header-art > img {
    animation: none;
  }
}

@media (max-width: 768px) {
  .sk-page-header {
    --sk-header-art: 68px;
    --sk-header-cut: 14px;
    --sk-header-twin: 5px;
  }

  .sk-page-header-art {
    margin-left: var(--sk-space-2);
  }

  .sk-page-header-title {
    padding: var(--sk-space-3) calc(var(--sk-header-cut) + var(--sk-space-3)) var(--sk-space-3) var(--sk-space-3);
    font-size: var(--sk-text-lg);
  }

  .sk-page-header > .sk-page-header-title:first-child {
    padding-left: calc(var(--sk-header-cut) + var(--sk-space-3));
  }
}

/* Hide-page-title preference (Preferences > Display, or an Arrange drawer). The
   modifier lives on <html>, set before first paint by the inline script in
   Auth.Master from localStorage (hidePageHeader - one key: the browser it is saved
   in already makes it a phone's or a computer's choice), so a hidden title never
   flashes. display: none takes the header's own block margin with it, and the
   page's first card simply moves up. */
.sk-hide-page-header .sk-page-header {
  display: none;
}

/* ---- Timeline --------------------------------------------------------------
   A sequence of events on a spine, newest first. Distinct from sk-datalist:
   that pairs a label with a value, this places entries in time, and the rail
   plus the node markers are what say "these are consecutive" without a word of
   copy doing it.

   The node sits ON the rail, not beside it: it is pulled left by the padding
   plus half its own width, and given a ring in the surface colour so the rail
   appears to pass behind it rather than through it. */
.sk-timeline {
  list-style: none;
  margin: var(--sk-space-2) 0 0;
  padding: 0 0 0 var(--sk-space-4);
  border-left: 2px solid var(--sk-border);
}

.sk-timeline-item {
  position: relative;
  padding-bottom: var(--sk-space-4);
}

.sk-timeline-item:last-child {
  padding-bottom: 0;
}

.sk-timeline-item::before {
  content: "";
  position: absolute;
  top: 0.35em;
  left: calc(-1 * var(--sk-space-4) - 5px);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--sk-hue, var(--sk-accent));
  box-shadow: 0 0 0 3px var(--sk-surface-inner);
}

/* The newest event is the one that explains the current state, so it leads. */
.sk-timeline-item:first-child::before {
  background: var(--sk-text);
}

.sk-timeline-time {
  display: block;
  font-size: var(--sk-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--sk-text-muted);
}

.sk-timeline-text {
  display: block;
  font-size: var(--sk-text-sm);
  line-height: 1.5;
  color: var(--sk-text);
}

/* ---- Card grid -------------------------------------------------------------
   As many equal columns as fit, reflowing to one on narrow screens with no
   breakpoint to maintain. Children stretch to equal height per row, so a row of
   cards with unequal content still lines up.

   `min(100%, 22rem)` rather than a bare `22rem` is load-bearing: with a bare
   minimum, auto-fill computes a track wider than the container below 22rem and
   the grid overflows horizontally — the page scrolls sideways, which this system
   forbids. The min() clamps the track to the container on small screens.

   --sk-grid-min overrides the track floor per instance without a modifier. */
.sk-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--sk-grid-min, 22rem)), 1fr));
  gap: var(--sk-space-4);
}

/* Denser floor, for smaller tiles rather than full cards. */
.sk-grid--tight {
  --sk-grid-min: 14rem;
  gap: var(--sk-space-2);
}

/* ---- Two-panel split -------------------------------------------------------
   Side-by-side panels that stack on narrow screens; the first is the narrower
   rail. Any direct children work — it does not assume sk-card. */
/* This is where the nesting bug was found, and the whole file is flat now
   because of it - see the note under "CSS nesting is not a safe default here"
   in SK_DESIGN_SYSTEM.md. A browser without nesting support dropped every
   nested block in this rule, which left .sk-split as a bare `display: flex` at
   every width: the panels never stacked, the cards came out as equal-height
   columns, and the page scrolled sideways on a phone. Reported on an older
   mobile Safari and confirmed from the screenshot - the TOP-LEVEL max-width
   rules were plainly applying in the same shot, which is what rules out the
   viewport and points at the nesting. */
.sk-split {
  display: flex;
  gap: var(--sk-space-4);
}

.sk-split > * {
  flex-grow: 1;
}

.sk-split > :first-child {
  width: 40%;
}

@media (max-width: 768px) {
  .sk-split {
    flex-direction: column;
    gap: var(--sk-space-2);
  }

  /* auto, not 100%. The job here is only to cancel the 40% above, and a
     stretched item in a column flex container already fills the line at auto.
     `width: 100%` overshoots for any child that carries padding or a border,
     because there is no global box-sizing: border-box in this file - it is set
     on five components and .sk-card is not one of them. An sk-card as the first
     child therefore rendered 100% WIDE PLUS its own padding and border and hung
     off the right edge of the phone, while the second child, which has no width
     of its own, sat correctly. Caught in the preview page the moment sk-split
     got its first demo. */
  .sk-split > :first-child {
    width: auto;
  }
}

/* Two panels of equal weight, rather than a rail plus a main panel. The base
   component's 40/60 says "the first one is subordinate"; this says the two are
   peers.

   MUST be declared after .sk-split: `.sk-split--even > :first-child` and the
   base's `.sk-split > :first-child` are both (0,2,0), so source order is the
   only thing separating them. Moved above, this silently loses and the split
   stays 40/60.

   Scoped to a min-width, and BOTH halves of that matter.

   1. The base sets `.sk-split > :first-child { width: auto }` inside its own
      @media (max-width: 768px). A media query adds no specificity, so an
      unqualified rule here - same (0,2,0), declared later - would beat it and
      render the STACKED first column at half width on a phone.
   2. Below 768px the base flips to flex-direction: column, where flex-basis
      applies to HEIGHT. `flex: 1 1 0` there would zero that basis and stretch
      both panels to fill the container.

   768.02px rather than 769px: the base's max-width is 768px, and at a
   fractional width such as 768.5px - which browser zoom and fractional DPR
   scaling both produce - NEITHER query would match and the row would fall
   back to 40/60.

   flex: 1 1 0 rather than width: 50%, because a width alone does NOT produce
   50/50 here: the base gives every child flex-grow: 1 with flex-basis: auto,
   so the first child would start at 50% and the second at its CONTENT width,
   and the leftover space would then split 1:1 on top of that. A zero basis
   makes the two genuine peers; min-width: 0 stops a wide table refusing to
   shrink, which in turn means a too-wide table overflows instead - wrap that
   one table in .sk-table-scroll, and only that one. */
@media (min-width: 768.02px) {
  .sk-split--even > * {
    flex: 1 1 0;
    min-width: 0;
  }

  /* Belt and braces, and honestly redundant: flex-basis: 0 above already
     overrides width for the flex base size. Kept because it states the intent
     - the base's 40% no longer applies here - not because anything breaks
     without it. */
  .sk-split--even > :first-child {
    width: auto;
  }
}

/* More air between the panels once they stack on a phone. The base's gap there
   is --sk-space-2, fine for two short blocks but cramped when each panel is a
   titled group of fields. Declared after the base's media block for the same
   (0,1,0) specificity reason as sk-split--even above. */
@media (max-width: 768px) {
  .sk-split--roomy {
    gap: var(--sk-space-6);
  }
}

/* ---- Pair grid: two panels whose rows pair up 1:1 ---------------------------
   Two panels side by side, each a heading and a list, where row N on the left
   answers row N on the right (the Attack Room Simulator: Your kingdom | Enemy
   kingdom - Race | Race, Kingdom state | Kingdom state, ...). sk-split--even
   gives the panels the same height but not their rows: a hint that wraps on
   one side only pushes every row under it out of line with its partner.

     <div class="sk-pair-grid">
       <div class="sk-card-inner">
         <div class="sk-eyebrow sk-eyebrow--head">Your kingdom</div>
         <ul class="sk-settings-list sk-settings-list--plain"> <li>... </ul>
       </div>
       <div class="sk-card-inner"> ...the same shape... </div>
     </div>

   On a computer the panels share one set of rows through subgrid: each panel
   spans every row of the pair grid and its list spans all but the first (the
   heading's), so row N's height is the taller of the two row Ns. The grid
   declares 16 auto rows - a heading and up to 15 list rows; an empty auto row
   is 0 tall and there is no row gap, so the unused ones cost nothing. A longer
   list needs a bigger number here: a subgrid cannot add rows of its own, and
   an extra item would land on top of the last one. Panels the same padding,
   so the edge rows line up. Without subgrid support the panels are plain
   grids in two equal columns - the old, unlevelled look, nothing broken.

   One column under 768px, as sk-split stacks: the panels follow each other
   and the lists are ordinary lists. */
.sk-pair-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sk-space-4);
}

@media (min-width: 768.02px) {
  .sk-pair-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(16, auto);
    row-gap: 0;
  }

  .sk-pair-grid > * {
    display: grid;
    grid-row: 1 / -1;
    grid-template-rows: subgrid;
    align-content: start;
  }

  .sk-pair-grid > * > :is(ul, ol) {
    display: grid;
    grid-row: 2 / -1;
    grid-template-rows: subgrid;
  }
}

/* Main panel first, rail second - the mirror of the base component, which
   reads rail-first. Status is the case: the spy report is what the page is
   FOR and the settings column is support, so the report leads at 60% and the
   rail follows at 40%.

   The same three constraints as sk-split--even, for the same reasons, and all
   three are load-bearing:

   1. Declared AFTER .sk-split. `.sk-split--main-first > :first-child` and the
      base's `.sk-split > :first-child` are both (0,2,0), so source order is the
      only thing separating them. Moved above, this silently loses and the row
      stays 40/60.
   2. Scoped to min-width: 768.02px. Below it the base has already stacked the
      columns and set the first back to `width: auto`; an unqualified rule here
      would win that tie and re-pin the STACKED first column to 60% of the
      phone. The fractional edge is the base's own - at 768.5px, which browser
      zoom and fractional DPR scaling both produce, neither query may match.
   3. flex-grow, not width. The base gives every child `flex-grow: 1` on a
      `flex-basis: auto`, so `width: 60%` would set the base size and then have
      the leftover space split 1:1 on top of it - not 60/40. A zero basis with
      1.5 against 1 is a true 60/40; min-width: 0 stops a wide child refusing
      to shrink, which in turn means a too-wide table overflows instead - wrap
      that one table in .sk-table-scroll, and only that one. */
@media (min-width: 768.02px) {
  .sk-split--main-first > * {
    flex: 1 1 0;
    min-width: 0;
  }

  .sk-split--main-first > :first-child {
    width: auto;
    flex-grow: 1.5;
  }
}

/* Below the split's breakpoint the two panels DISSOLVE and their cards become
   the items of the single stacked column - so a page can order its cards
   across the split rather than only within one side of it.

   Status is why it exists: on a phone the settings and the combat table are
   wanted above the spy report and the rest of the rail below it, and no
   `order` applied to the two PANELS can express that - a panel moves as a
   block, and the wanted sequence interleaves them.

   `display: contents` is what makes it possible with one copy of the markup.
   The panel's box goes away, and its `gap` goes with it - harmless here only
   because sk-stack and sk-split are both --sk-space-2 at this width, so the
   spacing does not move. A panel with a background, a border, a padding or a
   different gap must not use this.

   Ordering is opt-in per card, and the blanket rule is not optional: `order`
   defaults to 0, which sorts BEFORE every positive value, so an unmarked card
   would jump to the FRONT of the column rather than trail the marked ones.
   The floor at 99 puts them behind instead, in source order.

   EVERY SELECTOR HERE DOUBLES `.sk-split.sk-split--reflow`, and that is the fix
   for the bug this block shipped with. A universal selector contributes nothing,
   so `.sk-split--reflow > *` was (0,1,0) - exactly the weight of `.sk-stack`,
   which declares `display: flex` 300-odd lines FURTHER DOWN this file and
   therefore won the tie on source order alone. The panels never dissolved, the
   cards stayed children of their own panel, and `order` could only sort each
   panel internally: on a phone the report still came first because its panel
   did, which is the one thing this block exists to prevent.

   Doubling the class takes the dissolve rule to (0,2,0), which beats .sk-stack
   on weight and no longer depends on where in the file either one sits. The
   order floor moves with it, and .sk-order-N to (0,3,0), so the three keep the
   same relationship they were always meant to have. */
@media (max-width: 768px) {
  .sk-split.sk-split--reflow > * {
    display: contents;
  }

  .sk-split.sk-split--reflow > * > * {
    order: 99;
  }

  .sk-split.sk-split--reflow .sk-order-1 { order: 1; }
  .sk-split.sk-split--reflow .sk-order-2 { order: 2; }
  .sk-split.sk-split--reflow .sk-order-3 { order: 3; }
  .sk-split.sk-split--reflow .sk-order-4 { order: 4; }
  .sk-split.sk-split--reflow .sk-order-5 { order: 5; }
  .sk-split.sk-split--reflow .sk-order-6 { order: 6; }
  .sk-split.sk-split--reflow .sk-order-7 { order: 7; }
  .sk-split.sk-split--reflow .sk-order-8 { order: 8; }
  .sk-split.sk-split--reflow .sk-order-9 { order: 9; }
  .sk-split.sk-split--reflow .sk-order-10 { order: 10; }
}

/* ---- sk-split--order: the same classnames, sorting at every width ----------
   sk-split--reflow (above) only exists below 768px, where the panels dissolve
   and all the cards become items of ONE column - so sk-order-N there sorts
   GLOBALLY, across the split. Above the breakpoint the panels are real boxes
   again and each card is a child of its own .sk-stack, which is
   `display: flex; flex-direction: column` - so `order` works there too, but
   sorts a card ONLY AMONG ITS OWN COLUMN'S CARDS.

   That asymmetry is the feature's known constraint, not a bug to route around:
   on a phone reordering is global, on a desktop a card moves within its own
   column. One ordered array drives both correctly, because a rail card at
   global index 7 still sorts after a rail card at global index 3 regardless of
   what the main panel holds. Moving a card ACROSS the split on a desktop would
   mean abandoning the 60/40 sk-split, which is out of scope - drag-and-drop
   refuses a cross-column drop above 768px rather than snapping the card back.

   Opt-in, because most sk-split pages want source order on a desktop and
   nothing else.

   THE `order: 99` FLOOR IS NOT OPTIONAL, for exactly the reason the reflow
   block above spells out: `order` defaults to 0, which sorts BEFORE every
   positive value, so an unmarked card would jump to the FRONT of its column
   rather than trail the marked ones.

   THE SELECTORS DOUBLE `.sk-split.sk-split--order` for the same specificity
   reason the reflow block documents at :5398-5409 - the floor has to beat
   `.sk-stack` (0,1,0), which declares `display: flex` further down this file
   and would otherwise win the tie on source order alone. Floor (0,2,0),
   sk-order-N (0,3,0), so the two keep the relationship they are meant to. */
@media (min-width: 768.02px) {
  .sk-split.sk-split--order > * > * {
    order: 99;
  }

  .sk-split.sk-split--order .sk-order-1 { order: 1; }
  .sk-split.sk-split--order .sk-order-2 { order: 2; }
  .sk-split.sk-split--order .sk-order-3 { order: 3; }
  .sk-split.sk-split--order .sk-order-4 { order: 4; }
  .sk-split.sk-split--order .sk-order-5 { order: 5; }
  .sk-split.sk-split--order .sk-order-6 { order: 6; }
  .sk-split.sk-split--order .sk-order-7 { order: 7; }
  .sk-split.sk-split--order .sk-order-8 { order: 8; }
  .sk-split.sk-split--order .sk-order-9 { order: 9; }
  .sk-split.sk-split--order .sk-order-10 { order: 10; }
}

/* ---- sk-swap-desktop / sk-swap-mobile: two panels, the other way round ----
   For a flex container of TWO children the player may flip - an sk-split's
   two panels side by side, or an sk-stack's two blocks one above the other.
   The first child sorts after the second; nothing else changes. On a stacked
   sk-split (a phone) that decides which panel comes first.

   One class per device, each inside its own half of the 768px breakpoint, so a
   page that keeps a different arrangement per device renders BOTH as classes
   and the width picks - no script has to work out which device it is on before
   the first paint, and a window dragged across the breakpoint follows. The
   Attack Room is the first (its premium Settings drawer, per device).

   `order`, never moved nodes: the markup stays in source order, so a Vue
   template or a server control addressed by position is untouched.

   Widths come with the CHILD, not with the side: sk-split's 40% / 60% keys on
   `:first-child`, which is a DOM position, so under --main-first the main
   panel keeps its 60% on whichever side it lands.

   `order: 1` beats the second child's default 0, and only the first child
   gets it - a third child would land between the two, which is why this is
   for two. The 768.02px edge is sk-split's own (see sk-split--even). */
@media (min-width: 768.02px) {
  .sk-swap-desktop > :first-child {
    order: 1;
  }
}

@media (max-width: 768px) {
  .sk-swap-mobile > :first-child {
    order: 1;
  }
}

/* ---- sk-board: columns of cards the player arranges ------------------------
   N interchangeable columns at desktop width; ONE globally ordered column
   below 768px. That is the same pair of behaviours sk-split + --reflow +
   --order give together, with the one thing that trio cannot: a card can live
   in ANY column, so a UI built on top of it can offer a cross-column move
   instead of refusing one.

   WHY sk-split CANNOT DO THIS, and why this is a second component rather than
   another modifier on that one: sk-split's panels are not the same kind of box.
   Its first child is 40% wide, or 60% under --main-first, BY POSITION - so a
   card moving from one panel to the other changes width as well as place, and
   "put this card over there" has no stable meaning. sk-board's columns are
   equal by default and any one of them can be the wide one, which is what makes
   a card's column a value worth storing.

   A card is placed by TWO classnames and nothing else: the page puts the card's
   element in a column, and sk-order-N sorts it there. No inline style, no JS
   `order`, at either width.

   ORDERING IS NOT INSIDE A MEDIA QUERY, unlike sk-split's two separate blocks.
   The class means the same thing at both widths: below the breakpoint the
   columns dissolve and every card is an item of the one stacked board, so
   sk-order-N sorts globally; above it each card is an item of its own column
   and the same class sorts it among that column's cards. The BOXES change, the
   sort does not, so one set of rules covers both.

   THE `order: 99` FLOOR IS NOT OPTIONAL, for the reason sk-split--reflow spells
   out at length: `order` defaults to 0, which sorts BEFORE every positive
   value, so an unmarked card would jump to the FRONT of its column rather than
   trail the marked ones.

   Height: no align-items here, so the columns stretch and stay equal-height.
   That is deliberate - sk-card--fill takes its height from exactly this chain
   (see its own note), and a board that sized each column to its content would
   quietly stop the report card filling its panel. */
.sk-board {
  display: flex;
  gap: var(--sk-space-4);
}

.sk-board > .sk-board-col {
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-2);
  flex: 1 1 0;
  min-width: 0;
}

/* The lead column is half again as wide as the others - the same 60/40
   sk-split--main-first gives a two-panel page, by the same means and for the
   same reasons: flex-grow on a ZERO basis, not a width, because the base's
   `flex: 1 1 0` would otherwise split the leftover space 1:1 on top of the
   width and land nowhere near 60/40. min-width: 0 is already on every column,
   so a wide table inside one overflows its own scroller rather than refusing
   to shrink.

   WHICH column leads is the page's call, not this component's: any of the
   three can be the wide one, because the card that earns the extra width can
   be moved. Below 768px the columns are dissolved and there is no width to
   lead with, so these never apply there - no media query needed. */
.sk-board--lead-1 > .sk-board-col:nth-child(1),
.sk-board--lead-2 > .sk-board-col:nth-child(2),
.sk-board--lead-3 > .sk-board-col:nth-child(3) {
  flex-grow: 1.5;
}

/* A column holding nothing takes no width AND NO GAP. display: none rather
   than a zero width is what drops the gap with it, which is the whole point:
   a board of three columns with one empty has to read as a board of two, not
   as two columns and a hole. The page marks it - only the page knows whether a
   column's cards are all hidden. */
.sk-board > .sk-board-col.is-empty {
  display: none;
}

.sk-board > .sk-board-col > * {
  order: 99;
}

/* Doubled class, so the ordering wins on weight rather than on source order:
   the floor above is (0,2,0) and these are (0,3,0). The same fix
   sk-split--reflow documents, applied before it can go wrong rather than
   after. */
.sk-board.sk-board .sk-order-1 { order: 1; }
.sk-board.sk-board .sk-order-2 { order: 2; }
.sk-board.sk-board .sk-order-3 { order: 3; }
.sk-board.sk-board .sk-order-4 { order: 4; }
.sk-board.sk-board .sk-order-5 { order: 5; }
.sk-board.sk-board .sk-order-6 { order: 6; }
.sk-board.sk-board .sk-order-7 { order: 7; }
.sk-board.sk-board .sk-order-8 { order: 8; }
.sk-board.sk-board .sk-order-9 { order: 9; }
.sk-board.sk-board .sk-order-10 { order: 10; }

/* Below the breakpoint the columns DISSOLVE and every card becomes an item of
   the board itself, so sk-order-N sorts across all of them. Same mechanism and
   same price as sk-split--reflow: the column's own box is gone and its gap with
   it, which is safe only because the board's gap here and the column's gap are
   both --sk-space-2. A column carrying a background, a border or padding must
   not use this component.

   An EMPTY column needs no special case: display: contents on a box with no
   children contributes nothing either way, and this rule is declared after
   .is-empty at the same weight, so the dissolve wins the tie. */
/* A column that is not the lead one is a third of the page or less, and the
   spy report's three tracks inside it read as three cramped strips of two or
   three characters. It takes the single track it already takes on a phone.

   The state is on the COLUMN, not on the card: it is a fact about the BOX, and
   anything else laid out in tracks inside a narrow column wants the same
   answer. Reaching across to .sk-report-cols from here is the coupling that
   buys it - a container query would be the general answer, and is the thing to
   reach for if a third component ever needs this.

   (0,3,0), which beats the base .sk-report-cols (0,1,0). It does NOT on its own
   beat the SOK's own rules - see "A spy report in a narrow column" further
   down, next to the report they mirror. This rule shipped claiming that one was
   (0,2,0); :not() contributes the specificity of its ARGUMENT, so
   `.sok-root .sk-report-cols:not(.sk-report-cols--pills)` is (0,3,0) - a tie
   this rule then lost on source order, and the report stayed two columns
   wide. */
.sk-board-col.is-narrow .sk-report-cols {
  grid-template-columns: 1fr;
}

@media (max-width: 768px) {
  .sk-board {
    flex-direction: column;
    gap: var(--sk-space-2);
  }

  .sk-board.sk-board > .sk-board-col {
    display: contents;
  }
}

/* ---- Report columns --------------------------------------------------------
   A read-only report laid out as columns of label/value LINES - three tracks
   on a desktop, one on a phone. Built for SOK, the spy report, and it is a
   layout component rather than a table for the same reason SOK is not a table:
   its markup is a clipboard wire format.

   THE ROWS MUST STAY PLAIN BLOCK BOXES, and that is a hard constraint, not a
   stylistic one. auth-scripts.js copies the report card's innerHTML and players
   paste it into Discord, and Chrome's copy-to-plain-text walk gives every FLEX
   ITEM its own line - including the anonymous item it wraps around a bare run
   of text. So `Land: <b>1,204</b>` inside a `display: flex` row pastes as TWO
   lines instead of one. A two-track grid carries the same anonymous-item rule,
   and a float creates a block-level box, so all three of the obvious ways to
   push a value to the right edge are out.

   Which is why the value is not pushed. It is JUSTIFIED - see sk-report-rows
   below. The row stays exactly the block box it always was and nothing inside
   it changes box type, so the paste is byte-for-byte what it was when these
   rows were ruled and left-aligned. See SOM.ascx:15-19 for the publish cycle
   the flex version cost.

   The COLUMNS are grid items, which is safe: each is a block container, so
   everything inside one - the rows, the section eyebrows, the <br> that ends a
   section - lays out and copies exactly as it did when the card was a single
   column. */
/* The other half of the same report as .sk-split above: nested, the one-column
   rule was lost on a browser without nesting support and a spy report came out
   three columns wide on a phone. */
.sk-report-cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 var(--sk-space-6);
}

@media (max-width: 768px) {
  .sk-report-cols {
    grid-template-columns: 1fr;
  }
}

/* A row of pills laid on the SAME three tracks as the report columns under it,
   so the identity band and the columns share their edges instead of the pills
   sitting wherever their content ends.

   Grid items, where they used to be flex items of an sk-row - which is a change
   of container, not of the pills, and costs the clipboard nothing: a pill was
   already a blockified item on its own pasted line and still is.

   row-gap because the base sets `gap: 0 ...` for columns that end in a <br>;
   these do not. align-items so a pill that wraps does not stretch its
   neighbours to match. And white-space, because sk-pill is nowrap by default
   and a long ruler name in a third of a card has to wrap rather than run out
   of its track. */
.sk-report-cols--pills {
  gap: var(--sk-space-2) var(--sk-space-6);
  align-items: start;
}

.sk-report-cols--pills > .sk-pill {
  min-width: 0;
  white-space: normal;
}

/* One fact per line, name left and figure right, aligned down the column - a
   table without a table, and without a single flex or grid box on the row.

   `text-align-last: justify` is what does it. Justification is a PAINTING
   decision: it moves glyphs inside a line box and changes no element's display
   type, creates no anonymous boxes and reorders nothing, so Chrome's
   copy-to-plain-text walk sees the identical tree it saw when these rows were
   left-aligned. That is the whole reason it is used here instead of the three
   obvious alternatives, every one of which splits the pasted line in two.

   It needs exactly ONE expansion opportunity per row, and that is what
   sk-report-key below is for: a multi-word name would otherwise have its own
   inter-word spaces stretched too, and "Laser  Troopers:  1,204" is worse than
   no alignment at all. An inline-block is atomic to the line box, so the spaces
   inside it are not expansion opportunities and the only gap that opens is the
   one between the name and the figure.

   No rule between rows. The alignment carries the structure now, which is what
   a hairline was standing in for - and twelve garrison rows under three hued
   eyebrows do not need both.

   max-width because a justified pair needs a sane measure: inside a third of
   the card it never binds, but the Stratagem block sits full width under all
   three columns and would otherwise throw its value at the far edge of the
   card.

   The type steps UP, which is the one direction this system allows. The
   --sk-text-min floor exists to stop labels being shrunk to 8px; it is not a
   cap on a readout, and a spy report is the densest text on the page while
   inheriting _typography.css's 13px body size. This lands at 14.5px on a phone
   and 16px on a wide monitor. It is its own custom property so a host can tune
   it without earning a modifier.

   tabular-nums on the ROW, not on a `<b>`: some of these values arrive as a
   bare `<b>1,234</b>` and some as asp:Labels rendering
   `<span style="font-weight:bold">`, so no single element is always the figure.
   On the row it catches both, and it costs the names nothing. */
.sk-report-rows {
  max-width: 30rem;
}

.sk-report-rows > div {
  padding: var(--sk-space-1) 0;
  font-size: var(--sk-report-text, clamp(14.5px, 13.5px + 0.35vw, 16px));
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  text-align-last: justify;
}

/* The name half of a row, and the reason the justification above only opens ONE
   gap. inline-block, not inline: an inline-block is an atomic item to the line
   box, so its internal spaces are not expansion opportunities and "Laser
   Troopers:" keeps its own spacing while the gap after it does the stretching.

   inline-block is also why this is safe for the clipboard. Chrome emits a line
   break around a box whose computed display is NOT an inline type, and
   inline-block IS one - the same reason sk-pill can be inline-flex inside a
   copied report. What breaks a pasted line is being a flex or grid ITEM, which
   blockifies it; nothing here is either.

   A row with no key still justifies harmlessly: one item on a line has no
   expansion opportunity, so it simply sits left. */
.sk-report-key {
  display: inline-block;
}

/* The figure half, for the rare value that is several words rather than one
   number - "758,783 pure + 155,545 flex" on Status and SOK. Same trick as
   sk-report-key and for the same reason: left inline, each of its spaces was an
   expansion opportunity and the justification spread the phrase across the
   whole row. inline-block keeps it one atomic item, so it moves to the right
   edge in one piece and copies on the same line. */
.sk-report-value {
  display: inline-block;
}

/* Report rows set directly under an sk-table - Status's Total military power
   under the Military statistics matrix. Full width instead of the 30rem
   measure, and inset by the table's cell padding plus its 1px border, so the
   name starts where the cells' text starts and the figure ends on the same
   edge as the table's right-hand number column. */
.sk-report-rows--under-table {
  max-width: none;
  padding-inline: calc(var(--sk-space-3) + 1px);
}

/* An icon on the name half of a statistics row - SOK's left column, and on the
   page only.

   Fixed width and centred, so nine glyphs of nine different widths still leave
   the nine names on one axis; a bare inline <i> is as wide as its own glyph and
   the names come out ragged. Muted and a step down in size, because the row's
   job is the figure at the far end of it and an icon that competes with the
   figure is worse than no icon.

   The gap after it is a MARGIN and not a space in the markup, which matters:
   the icons carry sox-copy-exclude and are removed from the clipboard clone,
   and a removed element takes its margin with it, where a text-node space would
   be left behind at the head of every pasted row.

   inline-block, so it costs no pasted line - but that is not what keeps it out
   of the paste. Font Awesome draws the glyph as ::before generated content and
   Chrome serialises generated content into a plain-text copy, so an empty <i>
   is only free of LINE breaks, not of TEXT. sox-copy-exclude is the guarantee;
   the full reasoning is in SOK.ascx above the rows. */
.sok-stat-icon {
  display: inline-block;
  width: 1.15em;
  margin-right: 0.5em;
  text-align: center;
  font-size: 0.9em;
  color: var(--sk-text-muted);
}

/* ---- A report column the clipboard keeps and the page does not ------------
   SOK is a spy report AND a clipboard wire format, and on the Status page the
   two want different things: the page already carries its own Combat potential
   card, so the report's combat column is a duplicate on screen - but a pasted
   report must still carry every figure.

   Scoping the hide to .sok-root is what separates them, and it is not an
   arbitrary hook. copySox builds its clipboard from `soxContainer.innerHTML`
   and appends that to <body>: innerHTML excludes the container, so the clone is
   the report's children under a bare div, with NONE of the ancestors the page
   wraps them in. A rule that needs one of those ancestors therefore applies on
   the page and cannot apply in the clone - the column lays out there and the
   plain-text walk serialises it.

   .sok-root is on the control's root element, which sits outside the card and
   so outside the clone. It is a constant, carried by both reports, because both
   want this: the third column is Max Offense, Max Defense and the stratagem,
   which is three lines - a whole track of a three-track grid to say them, when
   they read better as a group under the statistics they qualify.

   The root ALSO carries .sok-self or .sok-enemy, off the same Local flag that
   decides which figures a report may show. Those are the hook for anything the
   two reports should do differently; this rule is not one of them.

   `display: none` on the column ITSELF would be the obvious way to do this and
   is the one thing that must not happen: an element with no box is not laid out
   and not serialised, so the figures would vanish from the paste too. Same trap
   as sox-copy-exclude in reverse - that class exists to take page chrome OUT of
   the clipboard, this arrangement keeps report data out of the PAGE. */
.sok-root .sok-combat-col {
  display: none;
}

/* Two columns, not three, once the third is gone - otherwise the grid holds an
   empty track and the report reads as two columns pushed to the left.

   :not(--pills) because the identity band is an sk-report-cols too, and it still
   has three pills in it. Dropping it to two tracks put Race on a second row under
   Ruler Name with an empty cell beside it. Three across, as it was. */
@media (min-width: 768.02px) {
  .sok-root .sk-report-cols:not(.sk-report-cols--pills) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The other half of the swap: figures repeated into the Vital statistics column
   for the eye, hidden everywhere the combat column is visible so they are never
   shown twice. They carry sox-copy-exclude, so the clipboard never sees them at
   all and the paste keeps exactly one copy of each figure. */
.sok-compact-only {
  display: none;
  /* Its own group, so it needs to look like one: these figures belong to the
     combat column, not to the vital statistics they now sit under. */
  margin-top: var(--sk-space-4);
}

.sok-root .sok-compact-only {
  display: block;
}

/* The report's actions, in its own header beside the kingdom name and the report
   date. They used to sit above the card in a second .separator strip, which drew
   its own border-top a few pixels under the card header's rule - two hairlines
   with a button floating between them, and nothing saying which report the button
   was for. In the header both problems go away.

   Left-aligned, following the date pill rather than pushed to the far edge with
   margin-left: auto. The header wraps, so on a narrow card the pair drops onto
   its own line - and a right-aligned pair down there sat alone against the edge
   with the name and date left of it, reading as detached from both. */
/* SOK ends in the headline panel, which draws its own border - and the footer
   strip's hairline landed a few pixels under it, which is two rules saying the
   same thing. The strip keeps its rule everywhere else: SOM, SOB, SON, SOR and
   SOS all end in plain rows and need something to separate the buttons from the
   report. Scoped to .sok-root rather than removed from the component. */
.sok-root .separator.sk-sox-actions {
  border-top: 0;
}

/* A line break the page does not want and the clipboard does. SOK closes each
   section with a <br /> that puts a blank line between sections in a pasted
   report; on screen it is just a gap at the foot of a column.

   Scoped to .sok-root, the class on the control's root, which sits outside the
   card - so it is absent from the clone copySox builds and the break renders
   there. Same mechanism as sok-combat-col, opposite direction to
   sox-copy-exclude: that class takes chrome OUT of the clipboard, this keeps
   something out of the PAGE. */
.sok-root .sok-copy-only {
  display: none;
}

/* The two vertical rules in a report - between its columns, and between the two
   halves of the headline pair - drawn as one thing so they line up.

   Drawn on the CONTAINER at left: 50%, not as a border on the second child. A
   border on the child sits at the START of the second track, which is half the
   grid gap to the RIGHT of centre - and the two grids have different gaps, so
   the two rules landed at different x and the mismatch was plainly visible with
   one directly under the other.

   50% resolves against the PADDING box of the positioned ancestor, so the
   headline panel's own 1px border cancels out either side and both rules land
   on exactly the same axis despite one box being inset from the other.

   An absolutely positioned ::before is not a grid item, so neither container
   gains a track. It carries no text either, which is what makes it safe inside
   a card the clipboard reads.

   Above 768.02px only: below it both grids stack, and a rule down the middle of
   a single column is a line through the middle of the report. */
@media (min-width: 768.02px) {
  .sok-root .sk-report-cols:not(.sk-report-cols--pills),
  .sok-headline {
    position: relative;
  }

  .sok-root .sk-report-cols:not(.sk-report-cols--pills)::before,
  .sok-headline::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    border-left: 1px solid var(--sk-border);
  }

  /* Inset by the panel's own padding, so its rule stops short of its border
     instead of running into the corners. */
  .sok-headline::before {
    top: var(--sk-space-4);
    bottom: var(--sk-space-4);
  }
}

/* Tighter section headings than the system default. sk-eyebrow's --sk-space-5
   top margin is right for a page that heads a section every few paragraphs; a
   spy report heads one every four lines, and at that density the air above each
   cap was doing more to break the report up than the cap itself. SOK only - the
   default stands everywhere else. */
.sok-root div.sk-eyebrow {
  margin: var(--sk-space-2) 0;
}

.sok-root div.sk-eyebrow:first-child {
  margin-top: 0;
}

/* A row that carries its concept hue down its leading edge, the table version of
   sk-card--stripe: put sk-table-row--stripe and an sk-hue-* on the <tr>. An inset
   shadow on the first cell rather than a border, so the column widths do not move.
   Built for the Shield Room, where each shield row keeps the hue its card had. */
.sk-table tr.sk-table-row--stripe > :where(th, td):first-child {
  box-shadow: inset 4px 0 0 var(--sk-hue-deep, var(--sk-hue, var(--sk-accent)));
  /* Clear of the stripe, which would otherwise sit on the cell's first letter -
     a full gutter past it, not a tight one (review, 2026-10-19: the shield
     names were pressed against the wall). */
  padding-left: calc(4px + var(--sk-space-3));
}

.sk-table tr.sk-table-row--stripe > :where(th, td):last-child {
  padding-right: var(--sk-space-4);
}

/* sk-table-row--muted: a row that cannot be acted on right now (Stratagems: spent,
   or another one running), greyed rather than removed, so the set reads the same
   every time. */
.sk-table tr.sk-table-row--muted > :where(th, td) {
  opacity: 0.45;
}

/* A table row that is a CONCLUSION rather than another reading - the At Home and
   Maximum rows of a combat table, under the Pure and Hybrid ones they summarise.
   Tinted, ruled and bolder, in whatever hue the row is given.

   Doubled .sk-table so it reaches (0,3,1) on the cells: the component draws its
   own row divider at `.sk-table > tbody > tr:not(:last-child) > :where(th, td)`,
   which is (0,2,2), and a single class here would lose the bottom border to it.

   The border is drawn on the CELLS, not the row: border-collapse is separate on
   this component, and a <tr> draws no box of its own under that model. The first
   and last cells carry the side edges, which is what closes the box. */
.sk-table.sk-table tr.sk-table-row--call > :where(th, td) {
  background: color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 14%, transparent);
  border-top: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  color: var(--sk-hue, var(--sk-accent));
  font-weight: var(--sk-weight-bold);
}

.sk-table.sk-table tr.sk-table-row--call > :where(th, td):first-child {
  border-left: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  border-top-left-radius: var(--sk-radius);
  border-bottom-left-radius: var(--sk-radius);
}

.sk-table.sk-table tr.sk-table-row--call > :where(th, td):last-child {
  border-right: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 45%, transparent);
  border-top-right-radius: var(--sk-radius);
  border-bottom-right-radius: var(--sk-radius);
}

.sk-table tr.sk-table-row--call i {
  margin-right: var(--sk-space-2);
}

/* The two figures a report is read FOR, under both its columns and weighted to
   match. Everything above them is what a kingdom has; these two are what it can
   do with it, so they get a panel rather than another pair of rows.

   Two tracks that collapse to one under 768px, and each item takes its own hue
   through the ordinary sk-hue-* classes - accent for offence, ion for defence -
   so the icon and the figure agree without a bespoke colour here.

   It is display: flex, which anywhere else in this control would be a bug: a
   flex item pastes on its own line. It is safe only because the whole panel
   carries sox-copy-exclude and never reaches the clipboard at all. Nothing
   inside the report proper may be laid out this way - see sk-report-rows. */
.sok-headline {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sk-space-2);
  margin-top: var(--sk-space-4);
  padding: var(--sk-space-4);
  background: var(--sk-surface-inner);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
}

@media (max-width: 768px) {
  .sok-headline {
    grid-template-columns: 1fr;
    gap: var(--sk-space-3);
  }
}

.sok-headline-item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: var(--sk-space-3);
  padding: 0 var(--sk-space-2);
}

.sok-headline-item > i {
  grid-row: span 2;
  color: var(--sk-hue, var(--sk-accent));
  font-size: 26px;
}

.sok-headline-label {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
}

/* Tinted by "Since your last SOK" (sk-diff-row): rounded like the rows above. */
.sok-headline-item.sk-diff-row {
  border-radius: var(--sk-radius-sm);
}

/* Its stacked figure follows the value: left under the label on a desktop,
   to the right edge on a phone (where the value is right-aligned). */
.sok-headline-value .sk-diff-figure {
  align-items: flex-start;
}

@media (max-width: 768px) {
  .sok-headline-value .sk-diff-figure {
    align-items: flex-end;
  }
}

.sok-headline-value {
  color: var(--sk-text);
  font-size: var(--sk-text-xl, 24px);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

/* On a phone the pair stops being a pair - the grid stacks, and two label-over-
   figure blocks one under the other are just a tall way of saying two lines. So
   each becomes one: icon and name left, figure hard right, at the same size and
   weight as the report rows above them, which is what they now sit in line with.

   text-align on the figure rather than justify: this is a grid here, not a block
   box, so the row is laid out as tracks and needs no expansion opportunity. The
   sk-report-rows trick does not apply and is not needed - and this panel never
   reaches the clipboard anyway. */
@media (max-width: 768px) {
  .sok-headline-item {
    grid-template-columns: auto 1fr auto;
    column-gap: var(--sk-space-2);
    padding: var(--sk-space-1) 0;
  }

  .sok-headline-item > i {
    grid-row: auto;
    font-size: var(--sk-text-lg);
  }

  .sok-headline-label,
  .sok-headline-value {
    font-size: var(--sk-report-text, clamp(14.5px, 13.5px + 0.35vw, 16px));
    line-height: 1.5;
  }

  .sok-headline-value {
    text-align: right;
  }
}

.sok-header-actions {
  display: flex;
  /* nowrap, with short labels to earn it. The pair is one control - copy this
     report, or go and attack it - and wrapping split them onto two rows on a
     probe page column under about 530px, which read as two unrelated buttons. */
  flex-wrap: nowrap;
  gap: var(--sk-space-2);
}

/* A shade smaller than the footer pair: these share a line with a heading, and at
   full size they out-weigh the kingdom name the report is about. SOM's header
   pair on a probe report (sox-report-actions) takes the same size. */
.sok-header-actions .sk-btn,
.sox-report-actions .sk-btn {
  padding: 6px 12px;
  font-size: var(--sk-text-xs);
  white-space: nowrap;
}

/* ...except the icon button among them ("Copy short report"), which is a fixed
   square and must keep sk-btn--icon's zero padding, or the glyph is pushed off
   centre. */
.sok-header-actions .sk-btn--icon,
.sox-report-actions .sk-btn--icon {
  padding: 0;
}

/* ---- The own-kingdom identity band ---------------------------------------
   Avatar on the left, and on the right the kingdom name with its ruler, planet
   and race stacked under it. OWN KINGDOM ONLY: a probe reports the same three
   facts and keeps the pills it always had.

   EVERY RULE HERE IS SCOPED TO .sok-self, AND THAT IS WHAT MAKES IT SAFE FOR
   THE CLIPBOARD. .sok-self is on .sok-root, which sits OUTSIDE the card, and
   copySox builds its clone from the card's innerHTML -- so none of these
   selectors can match inside the clone. The pasted report is laid out by the
   base rules exactly as it was before any of this existed: three pills, three
   lines, same words, same order. That is the same mechanism sok-combat-col
   relies on, used here for a second purpose.

   It is what lets these rules use flex freely, which anywhere in this control
   that the clipboard CAN see would be a bug (a flex item pastes on its own
   line - see sk-report-rows). Two notes on that anyway:

   - The pills were already inline-flex, so nothing about their item structure
     changes; only the chrome comes off.
   - The two wrapper divs sok-identity and sok-identity-main are in the clone,
     and they are plain BLOCK containers there. A block boundary is a line
     break in the paste either way, so they cost it nothing. */

/* "Identity" and the rule under it, both gone. The band says what it is now
   that it carries a face, a name and three named facts, and the heading was
   pushing the one thing worth seeing first down the card. */
.sok-self .sok-identity-head {
  display: none;
}

.sok-self .sok-identity {
  display: flex;
  align-items: flex-start;
  gap: var(--sk-space-5);
}

/* min-width: 0 so a long kingdom name wraps inside the track rather than
   forcing the flex line wider than the card. A flex item's min-width is auto
   by default, which is its content's minimum -- and that is what pushes a
   two-item row past its container. */
.sok-self .sok-identity-main {
  flex: 1;
  min-width: 0;
}

/* The PREMIUM badge on its own line above the kingdom name (SOK.ascx). Only
   the gap under it: the badge's look is sk-badge's. */
.sok-self .sok-identity-main > .sk-badge {
  margin-bottom: var(--sk-space-2);
}

/* Unscoped on purpose: the element only renders when Local is true, so there
   is nowhere else for it to apply, and its own looks do not depend on the
   layout around it.

   SQUARE, AT A FIXED SIZE, and the size is a judgement rather than a
   derivation. A square whose side EQUALS the column's height cannot be had in
   one flex row: a row flex container resolves main size (width) before it
   knows the line's cross size (height), so a width that follows the stretched
   height -- aspect-ratio: 1 with height from stretch -- is circular and the
   browser falls back to the image's intrinsic ratio. Stretching the height and
   letting the width alone stay fixed is the only version of "equal height"
   flexbox will actually give, and that is a portrait, not a square.

   So this is a square sized to sit close to the column at the type sizes above
   -- name at --sk-text-xl plus three lines at --sk-text-base -- and the column
   is allowed to end a few pixels either side of it. object-fit: cover absorbs
   whatever aspect ratio a player's upload happens to have. */
.sok-avatar {
  flex: none;
  display: block;
  width: 128px;
  height: 128px;
  object-fit: cover;
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  box-shadow: var(--sk-shadow);
}

/* The name is the headline of the band, so it takes a step up from the
   sk-card-header default -- one step, not two: at --sk-text-2xl it out-weighed
   the four lines it heads, and it is also what sets how tall this column runs
   next to a fixed-size avatar. (0,3,0) against the component's (0,1,0), the
   :where() contributing nothing, so no !important and no doubled class. */
.sok-self .sok-identity .sk-card-header :where(h1, h2, h3, h4, h5, h6) {
  font-size: var(--sk-text-xl);
}

/* The three pills become three plain lines under the name. The grid that laid
   them on the report's three tracks goes with them: they belong to the name
   above them now, not to the columns below. */
.sok-self .sok-identity .sk-report-cols--pills {
  display: block;
}

.sok-self .sk-report-cols--pills > .sk-pill {
  display: flex;
  align-items: baseline;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: var(--sk-text);
  font-size: var(--sk-text-base);
  white-space: normal;
}

.sok-self .sk-report-cols--pills > .sk-pill + .sk-pill {
  margin-top: var(--sk-space-1);
}

/* The pill's hover state, cancelled. The fill and the border it paints are
   already overridden above, but transform is not declared there and .sk-pill:hover
   is (0,2,0) -- so without this a line of plain text lifts a pixel under the
   pointer for no reason a reader could name. */
.sok-self .sk-report-cols--pills > .sk-pill:hover {
  transform: none;
}

/* Fixed width and centred, the same trick sok-stat-icon uses one column down:
   a user, a globe and a helix are three different widths, and a bare inline
   icon leaves the three lines starting on three different axes. It keeps the
   hue each pill was already given. */
.sok-self .sk-report-cols--pills > .sk-pill > i {
  width: 1.15em;
  flex: none;
  text-align: center;
  font-size: var(--sk-text-sm);
}

@media (max-width: 768px) {
  .sok-self .sok-identity {
    gap: var(--sk-space-3);
  }

  .sok-avatar {
    width: 88px;
    height: 88px;
  }
}

/* Under 375px the band stacks: the avatar centred on top, the name, badge,
   buttons and facts under it at full width - beside an 88px avatar the name
   column was squeezed to a sliver and the kingdom name broke over two lines.
   align-self: stretch keeps that column full width and left-aligned; only the
   picture is centred. */
@media (max-width: 374px) {
  .sok-self .sok-identity {
    flex-direction: column;
    align-items: center;
  }

  .sok-self .sok-identity-main {
    align-self: stretch;
  }
}

/* ---- A spy report in a narrow column --------------------------------------
   A DELIBERATE MIRROR of every `max-width: 768px` rule above, for a report that
   is not on a phone but is in a third of a page - sk-board lets a player put it
   in a column a third wide, and 30,609 in a track 80px across is the same
   problem a phone has. The two sets must move together; there is no way to
   share them, because one is a viewport query and this is a fact about a BOX.

   A container query is the real answer and is what to reach for the next time
   this is needed. It would replace both.

   EVERY SELECTOR HERE CARRIES `.sk-board-col.is-narrow` FOR SPECIFICITY, not
   only for scope. The report's own rules are already (0,3,0) - `:not()`
   contributes its argument, so
   `.sok-root .sk-report-cols:not(.sk-report-cols--pills)` is three classes, not
   two - and they are declared after the board's. Two extra classes clear them
   outright and stop this depending on where in the file either one sits.

   The pills band collapses too, which the min-width rule above deliberately
   exempts. That exemption is about a THREE-TRACK grid having room for three
   pills; at a third of the page it does not, and a phone stacks them for
   exactly the same reason. */
.sk-board-col.is-narrow .sok-root .sk-report-cols,
.sk-board-col.is-narrow .sk-report-cols {
  grid-template-columns: 1fr;
}

/* The rule down the middle goes with the second track. `content: none` removes
   the pseudo-element rather than hiding it, which is what the phone rules
   achieve by never creating it. A line down the centre of a single column is a
   line through the middle of the report. */
.sk-board-col.is-narrow .sok-root .sk-report-cols:not(.sk-report-cols--pills)::before,
.sk-board-col.is-narrow .sok-headline::before {
  content: none;
}

.sk-board-col.is-narrow .sok-headline {
  grid-template-columns: 1fr;
  gap: var(--sk-space-3);
}

/* Stacked, the pair stops being a pair: icon and name left, figure hard right,
   in line with the report rows above - the same reasoning as the phone rules,
   spelled out there. */
.sk-board-col.is-narrow .sok-headline-item {
  grid-template-columns: auto 1fr auto;
  column-gap: var(--sk-space-2);
  padding: var(--sk-space-1) 0;
}

.sk-board-col.is-narrow .sok-headline-item > i {
  grid-row: auto;
  font-size: var(--sk-text-lg);
}

.sk-board-col.is-narrow .sok-headline-label,
.sk-board-col.is-narrow .sok-headline-value {
  font-size: var(--sk-report-text, clamp(14.5px, 13.5px + 0.35vw, 16px));
  line-height: 1.5;
}

.sk-board-col.is-narrow .sok-headline-value {
  text-align: right;
}

.sk-board-col.is-narrow .sok-self .sok-identity {
  gap: var(--sk-space-3);
}

.sk-board-col.is-narrow .sok-avatar {
  width: 88px;
  height: 88px;
}

/* ---- Spy on Military (SOM.ascx) ------------------------------------------
   The table follows ConstructionGrid's recipe: real sk-table classes on the
   DataGrid, nothing re-implemented. What is local: the Status row reads as a
   caption row, empty leader slots recede as a dash, the Home Leader column is
   bold the way ConstructionGrid's Built column is, and the unit name holds one
   line. Qualified with .sk-table.som-grid to outrank sk-table's (0,2,2) cell
   rules. */
.som-grid-wrap {
  margin-top: var(--sk-space-3);
}

.sk-table.som-grid td.som-grid-unit {
  font-weight: var(--sk-weight-semibold);
  white-space: nowrap;
}

/* The table scrolls sideways on a phone rather than squeezing, so a header
   has no reason to wrap - "HOME / LEADER" over one figure is two lines of
   chrome for nothing. */
.sk-table.som-grid tr.sk-table-head > td {
  white-space: nowrap;
}

/* The Return time row: a caption-sized row, the leaders' ticks in the text colour (green, via
   sk-num--done, once a leader is home), only its own label muted. font-weight: it stays out of
   the Home column's bold below - (0,3,2) here outranks td.som-grid-home. */
.sk-table.som-grid tr.som-grid-status > td {
  font-size: var(--sk-text-xs);
  font-weight: var(--sk-weight-normal);
  white-space: nowrap;
}

.sk-table.som-grid tr.som-grid-status > td:first-child,
.sk-table.som-grid tr.som-grid-offense > td:first-child,
.sk-table.som-grid tr.som-grid-defense > td:first-child {
  color: var(--sk-text-muted);
}

/* The Offense and Defense rows under Return time: a caption-sized label like Return time's, the
   figures at the normal size (they are the answer), its label muted and nowrap. */
.sk-table.som-grid tr.som-grid-offense > td:first-child,
.sk-table.som-grid tr.som-grid-defense > td:first-child {
  font-size: var(--sk-text-xs);
  white-space: nowrap;
}

.sk-table.som-grid tr.som-grid-status > td.sk-num--done {
  color: var(--sk-success);
  font-weight: var(--sk-weight-bold);
}

/* Every Spy on Military report (som-grid--art, SOM.ascx) - your own, a probe result, probe
   history: each war leader's portrait fills its column, faded under the figures. The portrait sits
   on a <col> (som-lead-col-N, added by skSomLeaderColumns in auth-scripts.js - the DataGrid cannot
   write a colgroup), so ONE image spans the whole column instead of repeating per cell; the cells
   are already transparent. The copied image (copySomImage) keeps them - html2canvas paints a
   <col> background. The gradient keeps the figures readable - lighter
   at the top where the face is, darker down the column where the numbers are. */
.sk-table.som-grid--art .som-lead-col {
  background: center 15% / cover no-repeat;
}

.sk-table.som-grid--art .som-lead-col-0 { background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), url(../scripts/img/warleaders/warleader_0_col.webp); }
.sk-table.som-grid--art .som-lead-col-1 { background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), url(../scripts/img/warleaders/warleader_1_col.webp); }
.sk-table.som-grid--art .som-lead-col-2 { background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), url(../scripts/img/warleaders/warleader_2_col.webp); }
.sk-table.som-grid--art .som-lead-col-3 { background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), url(../scripts/img/warleaders/warleader_3_col.webp); }
.sk-table.som-grid--art .som-lead-col-4 { background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), url(../scripts/img/warleaders/warleader_4_col.webp); }

/* The reported kingdom's race (WarLeaderLore, via SOM.ascx.vb and skSomLeaderColumns): a column whose
   race portrait exists gets som-lead-col--race and the path in --som-lead-img, which replaces the
   default portrait above. A race still waiting for its portraits keeps the defaults. */
.sk-table.som-grid--art .som-lead-col--race {
  background-image: linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 80%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 95%, transparent)), var(--som-lead-img);
}

/* Home carries the race's own art (img/race, at the kingdom's race level) - a cut-out on a
   transparent ground, not a scene like the leaders' - so it stands on a soft glow instead of
   the page, near the top where a face would be. */
.sk-table.som-grid--art .som-lead-col-0.som-lead-col--race {
  background-image:
    linear-gradient(color-mix(in srgb, var(--sk-surface-on-image-solid) 72%, transparent), color-mix(in srgb, var(--sk-surface-on-image-solid) 94%, transparent)),
    var(--som-lead-img),
    radial-gradient(circle at 50% 25%, color-mix(in srgb, var(--sk-accent) 30%, transparent), transparent 70%);
  background-size: auto, cover, auto;
  background-position: center, center 10%, center;
}

/* The heading cells over a portrait let it through, and read in the text colour on it. */
.sk-table.som-grid--art > tbody > tr.sk-table-head > td.som-lead,
.sk-table.som-grid--art > tbody > tr.sk-table-head > td:first-child {
  background: color-mix(in srgb, var(--sk-surface-raised) 45%, transparent);
  color: var(--sk-text);
}

/* Every figure over a portrait gets a soft dark halo, so a bright patch of the art never sits
   behind it at full strength. */
.sk-table.som-grid--art td {
  text-shadow: 0 1px 3px var(--sk-surface-on-image-solid), 0 0 6px var(--sk-surface-on-image-solid);
}

/* A leader column is wide enough to show its portrait, not only its figure. */
.sk-table.som-grid--art td.som-lead {
  min-width: 110px;
}

/* The four leader columns share one width. Left to the table, each took the width of its
   heading, so a long name ("Captain Sasha Voronova") got a wider portrait than a short one
   ("Major Idris Holt"). Home and the unit names keep their content width. */
.sk-table.som-grid--art > tbody > tr.sk-table-head > td.som-lead:not(.som-lead-0) {
  width: 20%;
}

/* The report ends in its table, whose own edge already closes it - the action strip's hairline
   under it was a second line for nothing. */
.som-container + .separator.sk-sox-actions {
  border-top: 0;
}

.sk-table.som-grid td.som-grid-home {
  font-weight: var(--sk-weight-bold);
}

.sk-table.som-grid td.som-grid-zero {
  color: var(--sk-text-muted);
}

/* general.css:1827 hides every Bootstrap tooltip under 768px outright -
   `.tooltip:not(.buildings-tooltip) { display: none !important }` - which is why
   a tooltip on this system has never opened on a phone. general.css is
   do-not-edit, so a tooltip that WANTS to work on touch opts back in by name
   with data-bs-custom-class="sk-tooltip".

   !important answers !important; the extra class carries the specificity, and
   this file loads after general.css in any case. Opt-in per tooltip rather than
   a blanket unhide: the legacy rule is hiding a lot of tooltips written for a
   mouse, and turning them all on at once is a different change. */
@media (max-width: 768px) {
  .tooltip.sk-tooltip {
    display: block !important;
  }
}

/* A plain-text tooltip that carries several lines ("
"-separated), kept as
   text - never data-bs-html - because the lines can hold player-written names.
   data-bs-custom-class="sk-tooltip sk-tooltip--lines". */
.tooltip.sk-tooltip--lines .tooltip-inner {
  white-space: pre-line;
  text-align: left;
}

/* An eyebrow that takes its section's hue instead of the muted default. For a
   run of sections inside ONE card that mean different things - SOK's defensive
   / balanced / assault garrison blocks - where three identical grey caps read
   as one list with gaps in it rather than as three groups.

   Every hue token is a light pastel on a dark card (--sk-alert is the darkest
   at #fca5a5, above the 6.29:1 this document quotes for --sk-danger), so this
   stays well clear of AA at eyebrow size. */
.sk-eyebrow--hue {
  color: var(--sk-hue, var(--sk-accent));
}

/* An eyebrow that heads a SECTION rather than labelling a line - a rule under
   it and the full text colour, so it reads as a heading instead of as one more
   muted cap in the stack.

   A rule, not a fill: SOK already carries three pills across its identity band
   and a fourth tinted shape per column made the card read as a row of chips
   with data in them. The line is the least furniture that still says "new
   section starts here".

   Tinted with the section's hue rather than --sk-border, so a hued run of
   sections - SOK's defensive / balanced / assault garrison - keeps its coding
   in the rule as well as in the text, and an unhued one falls back to accent.

   display: block because sk-eyebrow is type styling with no box of its own, and
   a rule needs the full width of the section to sit under. */
.sk-eyebrow--head {
  display: block;
  padding-bottom: var(--sk-space-2);
  border-bottom: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 35%, transparent);
  color: var(--sk-text);
}

/* One step up inside a drawer, and only there. A drawer is a narrow panel read
   top to bottom, where the sections are the structure rather than an aside to
   a page that already has a title - at --sk-text-min they read as labels on the
   controls under them instead of as the divisions of the panel.

   In the drawer COMPONENT rather than on the page that noticed it, because
   every drawer with sections wants this and none of them wants to discover it
   separately. Still the same component: a step of size, not a second look. */
.sk-drawer-body div.sk-eyebrow--head {
  font-size: var(--sk-text-sm);
  letter-spacing: 0.6px;
}

/* ---- Small text roles ------------------------------------------------------
   One eyebrow, not two. The audit that started this system found #787f88,
   #b2bec3, #cbd5e1, #dfe6e9 and #e2e8f0 all meaning roughly "muted"; shipping
   both a "section label" and a "timestamp" style that differ by 0.3px of
   letter-spacing would be the same mistake in a new coat. */
.sk-eyebrow {
  font-family: var(--sk-font);
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-semibold);
  color: var(--sk-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.8px;
}

/* A block eyebrow heads a section, so it carries the section's spacing itself
   rather than waiting for a caller to remember. Scoped to div. on purpose: an
   inline eyebrow -- a date beside a heading, which is how Probe and Mine Gold
   use it -- takes no vertical margin anyway and must not start behaving like a
   heading. :first-child so the first section in a card does not push itself off
   its own top edge. */
div.sk-eyebrow {
  margin: var(--sk-space-5) 0 var(--sk-space-2);
}

div.sk-eyebrow:first-child {
  margin-top: 0;
}

.sk-eyebrow :where(i) {
  color: var(--sk-hue, var(--sk-accent));
}

/* A figure called out inside a sentence — the number a result card exists to
   report. Not sk-stat-value: that is a standalone readout with its own label,
   this one sits inline in running text. */
.sk-figure {
  font-size: var(--sk-text-lg);
  font-weight: var(--sk-weight-bold);
  font-variant-numeric: tabular-nums;
  text-decoration: underline;
  text-underline-offset: 2px;
}

@media (max-width: 576px) {
  .sk-figure {
    font-size: var(--sk-text-base);
  }
}

/* An explanatory aside under a control. */
.sk-hint {
  margin: var(--sk-space-3) 0 0;
  /* Same measure cap as sk-lede. A hint is usually a few words under a field,
     but used as a paragraph it ran the full page width — 200+ characters per
     line, which is the problem the cap exists to prevent. */
  max-width: 70ch;
  font-size: var(--sk-text-xs);
  color: var(--sk-text-muted);
}

.sk-hint :where(i) {
  color: var(--sk-hue, var(--sk-accent));
  margin-right: 3px;
}

/* The line under a page title. Centred to sit under the centred page header -
   a left-aligned lede under a centred hexagon read as a misalignment once pages
   went full-measure. Four users: Raze (Raze.aspx and the Buildings drawer) and
   Universal News' dossier card, where this is a visible change, and Explore and
   Research's empty state, which page CSS already centred. Raze and the dossier
   sit inside .sk-card, whose :where(p) rule caps paragraphs at 70ch with no side
   margins; the auto margins centre that capped box too, or the centred text sits
   in a box pinned to the card's left edge. :where() adds no specificity, so this
   later rule of equal weight wins. */
.sk-lede {
  font-size: var(--sk-text-sm);
  color: var(--sk-text);
  text-align: center;
  margin-inline: auto;
}

/* The row of buttons that closes a form. Exists because the submit button
   needs separating from whatever it follows — on Probe it sat flush against
   the quick-options grid, reading as part of the control rather than the
   action that commits it. */
.sk-form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sk-space-2);
  margin-top: var(--sk-space-5);
}

/* The actions at the form's bottom right (Target Finder's Reset and Search),
   stretched to one height; on a phone they split the width, side by side.
   input.sk-btn's own width/min-width are (0,1,1) - hence the element here. */
.sk-form-actions--end {
  justify-content: flex-end;
  align-items: stretch;
  margin-top: var(--sk-space-4);
}

@media (max-width: 768px) {
  .sk-form-actions--end > input.sk-btn,
  .sk-form-actions--end > button.sk-btn,
  .sk-form-actions--end > a.sk-btn {
    flex: 1 1 0;
    min-width: 0;
    justify-content: center;
  }
}

/* A block set off from what precedes it by a rule. Use this rather than an
   <hr>, so the spacing above and below the rule stays part of one box instead
   of depending on two neighbouring margins collapsing correctly. */
.sk-section {
  margin-top: var(--sk-space-5);
  padding-top: var(--sk-space-4);
  border-top: 1px solid var(--sk-border-subtle);
}

/* The line above a section's content: its eyebrow, plus whatever belongs beside
   the label rather than inside the content — a readout the section is about, a
   help trigger for it.

   It exists because sk-eyebrow is only type styling, with no box of its own, so
   a label sat directly on the content below it. The spacing belongs here rather
   than on sk-eyebrow: an eyebrow is also used inline (a date beside a heading),
   where a bottom margin would either do nothing or misalign it. Here it is a
   flex row, so the gap is the row's to own.

   The eyebrow takes the remaining space, which pushes everything after it to
   the right edge without those elements needing a class of their own. */
.sk-section-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sk-space-2);
  margin-bottom: var(--sk-space-3);
}

.sk-section-head > .sk-eyebrow {
  margin-right: auto;
}


/* A tab or panel that has nothing in it YET, because the feature behind it is
   paid and not built. Not sk-callout and not the plain empty state: both of
   those report a condition ("no rows matched"), and this announces something.
   It is the whole content of its container rather than a note inside it, so it
   carries the vertical space and the centring itself.

   Driven by --sk-hue like every other variant, so a caller picks the meaning:
   plasma for research, gold for economy. It sets no colour of its own beyond
   the muted body copy.

   USAGE - the eyebrow says WHAT it is, the title says WHEN:

     <div class="sk-premium-empty sk-hue-plasma">
       <div class="sk-premium-empty-icon"><i class="fas fa-star"></i></div>
       <p class="sk-premium-empty-eyebrow">Premium feature</p>
       <h4 class="sk-premium-empty-title">Coming soon</h4>
       <p class="sk-premium-empty-body">One sentence on what it will do.</p>
     </div>

   Deliberately has NO call to action. The reference design carried a "Check
   Back Later" button, which is a control that cannot be actioned - it looks
   operable, takes a tab stop and a tap target, and does nothing when pressed.
   A player learning that a button on this page is inert is a worse outcome
   than the blank space where it would have sat. */
.sk-premium-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--sk-space-6) var(--sk-space-4);
  gap: var(--sk-space-3);
}

@media (max-width: 768px) {
  .sk-premium-empty {
    padding: var(--sk-space-5) var(--sk-space-2);
  }
}

/* The ring. Sized in em so the whole medallion scales with one font-size
   change, and the glyph is centred by grid rather than by line-height - a
   Font Awesome glyph has no reliable relationship between its box and its ink,
   so line-height centring lands a pixel or two off and differs per icon. */
.sk-premium-empty-icon {
  display: grid;
  place-items: center;
  width: 3.4em;
  height: 3.4em;
  font-size: var(--sk-text-xl);
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--sk-hue, var(--sk-accent)) 55%, transparent);
  background:
    radial-gradient(circle at center,
      color-mix(in srgb, var(--sk-hue-deep, var(--sk-accent)) 26%, transparent) 0%,
      transparent 70%);
  box-shadow: var(--sk-hue-glow, var(--sk-glow-accent));
  color: var(--sk-hue, var(--sk-accent));
  margin-bottom: var(--sk-space-1);
}

.sk-premium-empty-eyebrow {
  font-size: var(--sk-text-min);
  font-weight: var(--sk-weight-semibold);
  color: var(--sk-hue, var(--sk-accent));
  text-transform: uppercase;
  letter-spacing: 1.2px;
  margin: 0;
}

/* Outranks `.sk-card :where(h4) { font-size: var(--sk-text-base) }` in section
   2 on SOURCE ORDER, not specificity - both selectors are (0,1,0), and this one
   is declared later. Moving this block above section 2 would silently shrink
   the title back to body size. */
.sk-premium-empty-title {
  font-size: var(--sk-text-xl);
  font-weight: var(--sk-weight-bold);
  color: var(--sk-text);
  margin: 0;
}

/* margin-inline is the load-bearing line, not text-align, and for the same
   reason .research-empty needs it: section 2 caps every <p> inside a card at
   70ch, and a max-width block with no auto margin hugs its container's left
   edge. text-align then centres the text inside a box that is itself pinned
   left, which reads as "centred, but a third of the way across". */
.sk-premium-empty-body {
  font-size: var(--sk-text-sm);
  color: var(--sk-text-muted);
  max-width: 46ch;
  margin: 0 auto;
}

/* Decoration. The medallion is the only thing on the block with a glow, so
   parking it is enough to take the animation out without flattening the
   component. */
@media (prefers-reduced-motion: reduce) {
  .sk-premium-empty-icon {
    box-shadow: none;
  }
}


/* --------------------------------------------------------------------------
   13. UTILITIES
   --------------------------------------------------------------------------
   Deliberately tiny. This is not a utility framework — components own their own
   spacing so markup does not need helpers to look right. Add here only when a
   real migration proves a case components cannot cover.

   Do not reach for Bootstrap's spacing or display utilities (mb-4, mt-4,
   d-flex) inside sk- components: a component that needs an external utility to
   look correct is unfinished. */

/* JS-toggled visibility. Replaces inline style="display:none" seeds and
   element.style.display mutations:
       el.classList.toggle('sk-is-hidden', !visible);
   !important is deliberate and is the one place it is justified: mid-migration
   this must beat an inline display left behind in older markup. */
.sk-is-hidden {
  display: none !important;
}

/* Hidden but still holding its place, so a row without one control lines up with
   the rows that have it (Modules: a one-off module has no MAX). visibility also
   takes it out of the tab order and the accessibility tree. */
.sk-is-invisible {
  visibility: hidden;
}

/* For icon-only controls that need an accessible name.

   top/left: 0 is load-bearing, not decoration. position:absolute with no
   offset resolves 'auto' against the element's STATIC position - where it
   would have landed in normal flow - measured from its CONTAINING BLOCK,
   the nearest positioned ancestor, or the document root if none exists.
   Every element this sits inside site-wide (sk-hide-desktop, a <th>, ...) is
   position:static, so that containing block is always the root: a copy deep
   in a wide table's later columns resolved to a real offset hundreds of px
   from the page's own left edge - invisible itself, but still full-width
   participants in the PAGE's scrollable area, stretching it sideways with a
   scrollbar and no visible cause (confirmed on Buildings' header row: the
   root cause of a ~36px horizontal overflow on a 389px viewport). Pinning to
   (0,0) - already inside every containing block, by definition - means it
   can never contribute overflow in any direction, regardless of which
   ancestor (if any) turns out to be positioned. */
.sk-sr-only {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* One-shot attention pulse: a ring that expands and fades plus a small pop,
   toggled by JS after a change the user did not click directly but should
   notice - e.g. Modules' Submit button after a purchase is queued elsewhere
   on the page. Re-triggering needs the class removed and re-added on the
   next frame (a forced reflow in between), since re-adding a class already
   present does not restart a CSS animation. */
@keyframes skFlashPulse {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--sk-success) 70%, transparent);
  }
  40% {
    transform: scale(1.08);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 10px color-mix(in srgb, var(--sk-success) 0%, transparent);
  }
}

.sk-flash {
  animation: skFlashPulse 0.5s ease-out;
}

/* A box that blinks four times, in its hue, when the page arrives with it
   already set - for a value something else filled in that moves every figure
   around it (Attack's Powerless segment, set by a probe's link). Not sk-flash,
   which is one pulse on a button the player just pressed. Pair it with an
   sk-hue-* class; it falls back to danger.

   box-shadow, not background-color: the box this was made for is an ACTIVE
   segment, whose fill is its own background, and a background-color animation
   paints under that and barely shows. An inset shadow paints above the
   background (below the text), so the wash is visible on a filled box and a
   bare one alike; the second layer is the glow round the outside.

   Only the 50% frame is given, so 0% and 100% are the element's own
   box-shadow and the blink returns to exactly what the box was. A list of the
   same shape (inset first, then outer - sk-segment.is-active is written to
   match) or `none` interpolates smoothly; any other shape still blinks, it
   just switches at the midpoint rather than fading. */
@keyframes skBlink {
  50% {
    box-shadow:
      inset 0 0 0 100vmax color-mix(in srgb, var(--sk-hue, var(--sk-danger)) 35%, transparent),
      0 0 14px color-mix(in srgb, var(--sk-hue, var(--sk-danger)) 60%, transparent);
  }
}

.sk-blink {
  animation: skBlink 0.7s ease-in-out 4;
}

/* Generic Vue transition: a v-if / v-show block that slides open/closed -
   <transition name="sk-slide">. While it runs, the block is a one-row grid
   whose row eases between 0fr and 1fr (its own height); its child clips. Unlike
   a max-height ceiling, this follows the block's real height, so the motion
   starts on the click and ends with the last pixel - a ceiling spent most of a
   close shrinking through empty space, then snapped shut. The block holds ONE
   element child (add a wrapper if it has more): a second child would be a
   second row that does not animate. Attack's Intelligence card; pair it with
   sk-card--collapsible (above) when the block is a card's body. */
.sk-slide-enter-active,
.sk-slide-leave-active {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--sk-transition-slow);
}

.sk-slide-enter-active > *,
.sk-slide-leave-active > * {
  min-height: 0;
  overflow: hidden;
}

.sk-slide-enter,
.sk-slide-leave-to {
  grid-template-rows: 0fr;
}

.sk-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-2);
}

/* Stack whose rows share a right edge. For a field that needs helper controls
   under it — the alternative is letting a flex row wrap, which produces the
   same two lines but with a ragged left edge and a break point that moves with
   the viewport. This makes the two-row shape deliberate and stable. */
/* A stack of control GROUPS rather than of lines - each group already spaces its own
   label and field, so the gap between groups is a step bigger (Alliance News' period,
   type filter and opponent filters, review 2026-10-19: "cramped"). */
.sk-stack--loose {
  gap: var(--sk-space-4);
}

.sk-stack--end {
  align-items: flex-end;
}

/* Fills its container instead of sizing to content.

   Needed whenever a stack is the child of a flex parent such as
   .sk-field-control: a flex item defaults to `flex: 0 1 auto`, so the stack
   collapses to the width of its widest row. Any `width: 100%` on a field
   INSIDE it then resolves against that collapsed box, not the column — which
   is how Probe's probes-to-send input ended up about 200px wide on mobile,
   sized by the MAX button and checkbox sitting under it, while
   `.sk-input`'s own `max-width: 100%` mobile rule looked like it should have
   handled it. */
.sk-stack--full {
  width: 100%;
}


.sk-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sk-space-2);
}

/* A row of controls that becomes a stack on a phone.

   sk-btn sets white-space: nowrap, so buttons never shrink below their label.
   Two or three of them side by side need horizontal room that a table cell in a
   six-column table simply does not have at 375px - they overflow the cell and
   overlap each other rather than wrapping cleanly, because each one is already
   wider than the column. Stacking gives each its own line and lets them share
   the column width instead of competing for it.

   align-items: stretch so they come out the same width; a ragged column of
   content-sized buttons is worse than the row it replaced. */
@media (max-width: 768px) {
  .sk-row--stack-mobile {
    flex-direction: column;
    align-items: stretch;
  }

  /* The same stack, last child on top: a field with its button BEFORE it on a
     desktop line ([Max][field]) shows the field first and the button under it
     on a phone, both the field's width. Attack's war-leaders cell.
     width: fit-content - the stack is as wide as its widest child (the field),
     not the table cell: stretched to the cell, the button came out ~117px
     beside 5.5em fields and widened the column. margin-inline-start: auto keeps
     it on the cell's right edge, where the other rows' fields sit. */
  .sk-row--stack-reverse-mobile {
    flex-direction: column-reverse;
    align-items: stretch;
    width: fit-content;
    margin-inline-start: auto;
  }
}

/* A row that fills its container rather than sizing to content. */
.sk-row--full {
  width: 100%;
}

/* Centred, for a row of pills or controls under a page header. */
.sk-row--center {
  justify-content: center;
}

/* Pushed to the row's own end, for a group of controls (an input plus its
   button) that should hug the far edge of a wider cell instead of sitting
   flush left with empty space trailing it. */
.sk-row--end {
  justify-content: flex-end;
}

/* One line at every width: a title and a short figure that belongs at its end
   (Modules: the module's name, then its cost per level). The first child takes
   the free width and its words wrap inside it, so the figure never drops to a
   line of its own. */
.sk-row--nowrap {
  flex-wrap: nowrap;
}

.sk-row--nowrap > :first-child {
  flex: 1 1 0;
  min-width: 0;
}


/* Wider gutter, for a row of distinct controls rather than a tight group. */
/* A row that closes a block -- the page's bottom navigation, say. Same margins
   sk-pill-row carries, so a row of buttons and a row of pills sit off their
   neighbours by the same amount instead of one of them needing an inline style. */
/* A row of buttons that becomes two even columns on a phone.

   sk-row wraps, which is right for pills of mixed length but wrong for a block
   of navigation: six buttons of six different widths wrap into a ragged
   two-two-two that re-flows every time a label changes, and the last row
   centres under the ones above it. A grid gives the same six buttons the same
   width and the same edges.

   a.sk-btn rather than a bare child selector: `a.sk-btn` in this file is
   (0,1,1), so `.sk-row--pairs-mobile > *` at (0,1,0) would lose the width and
   the buttons would stay content-sized inside even tracks. Same trap as the
   field grid's action column. */
@media (max-width: 768px) {
  .sk-row--pairs-mobile {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .sk-row--pairs-mobile > a.sk-btn,
  .sk-row--pairs-mobile > button.sk-btn {
    width: 100%;
    justify-content: center;
  }
}

.sk-row--spaced {
  margin: var(--sk-space-3) 0;
}

@media (min-width: 768.02px) {
  .sk-row--spaced {
    margin: var(--sk-space-4) 0 var(--sk-space-5);
  }
}

/* A row of actions at the foot of a card: room above it, and on a phone, in an
   sk-card--flush-mobile, the side gutter the card gave up (its table runs to
   the edge; the buttons should not). Attack's Set max row under Deploy. */
.sk-row--foot {
  margin-top: var(--sk-space-4);
}

/* In a column card the foot takes the free height above it, so it sits at the
   card's bottom - and two such cards side by side in an sk-split (the same
   height) have their feet on one line: Attack's Offense under Deploy
   Personnel, level with Defense under Defender Intelligence. */
.sk-card--column > .sk-row--foot {
  margin-top: auto;
  padding-top: var(--sk-space-4);
}

@media (max-width: 768px) {
  .sk-card--flush-mobile > .sk-row--foot {
    padding-inline: var(--sk-space-3);
  }
}

/* A button row moved under the page's main action by sk-arrange.js lands right
   after the anchor: more room above it there, so it does not read as part of
   that action (Attack's Settings / Simulate row under ATTACK, the owner,
   2026-10-18). (0,2,0) beats sk-row--spaced's margin. */
[data-sk-arrange-anchor] + [data-sk-arrange="buttons"] {
  margin-top: var(--sk-space-7);
}

/* Room under the row only - sk-row--spaced's bottom half, for a row whose
   top already sits right (the Simulator's Reset scenario, over the panels). */
.sk-row--spaced-below {
  margin-bottom: var(--sk-space-5);
}

.sk-row--loose {
  gap: var(--sk-space-3);
}

/* Pushes the row's items to opposite edges. For controls that are unrelated to
   each other — an action on one side, a setting on the other — where grouping
   them together implies they belong to the same decision. Pair with
   .sk-row--full, since a content-width row has no edges to push to. */
.sk-row--between {
  justify-content: space-between;
}

/* A row that never wraps: two controls that only read as one when they share a
   line - a button before the field it fills (Attack's War leaders "Max"). */
.sk-row--nowrap {
  flex-wrap: nowrap;
}

/* A row led by one control, the rest acting on it. On a desktop the lead sits at
   the start and the others pack to the end, one line. On a phone the lead takes
   the free width of its line beside the next control, and an sk-btn--full-mobile
   after them gets a line of its own. Attack's Power status: the Has power /
   Powerless segments with -5% units beside them, Calculate full width below. */
.sk-row--lead > :first-child {
  margin-right: auto;
}

@media (max-width: 768px) {
  .sk-row--lead > :first-child {
    flex: 1 1 0;
    min-width: 0;
    margin-right: 0;
  }
}

/* A row that sits at its end on a desktop and centres on a phone, where the
   "far edge" of a 375px screen is not a place anyone looks for controls. Pair
   with sk-row--end. */
@media (max-width: 768px) {
  .sk-row--center-mobile {
    justify-content: center;
  }
}

/* page-toolbar: the row of page-level actions, slot 3 of the action-page anatomy (see "Action page
   anatomy" in SK_DESIGN_SYSTEM.md) - directly under the readings, one copy at every width, in the
   order cross-links -> drawer actions -> Help -> Settings. The buttons are its direct children; it
   rides on top of the sk-row classes that do the layout:

     <div class="sk-row sk-row--end sk-row--loose sk-row--center-mobile page-toolbar">
       <a class="sk-btn" href="ConstructionStatus.aspx"><i class="fas fa-clipboard-list"></i> Status</a>
       <a class="sk-btn" href="/Help.aspx#buildings" target="_blank"><i class="fas fa-question-circle"></i> Help</a>
       <button type="button" class="sk-btn sk-btn--settings sk-btn--icon-mobile sk-hue-plasma" title="Settings"
               data-sk-drawer-open="..."><i class="fas fa-cog"></i><span class="sk-btn-text">Settings</span></button>
     </div>

   On a phone it is ONE row of glyphs that never wraps, and inside sk-page-top (below) it shares
   the header's row. It used to wrap button by button, so the same four buttons were one row on one
   page and two on the next depending on the words in them. Every toolbar button carries
   sk-btn--icon-mobile and shows its glyph alone on a phone (the word stays in the accessibility
   tree; give it a title for the hover). Everything is the phone control height, 32px.

   On a desktop a toolbar button keeps its icon AND its word: sk-btn--icon-mobile hides the glyph
   above 768px everywhere else (a worded button in a section head), so the toolbar shows it again. */
.page-toolbar {
  margin-bottom: var(--sk-space-3);
}

.page-toolbar > .sk-btn--icon-mobile > :where(i) {
  display: inline;
}

/* sk-page-top: the page header and its toolbar as one unit, slots 1 and 3 of
   the action-page anatomy.

     <div class="sk-page-top">
       <div class="sk-page-header sk-hue-income"> ... </div>
       <div class="sk-row sk-row--end sk-row--loose sk-row--center-mobile page-toolbar"> ... </div>
     </div>

   On a desktop the toolbar is a worded row under the plate (sk-page-top--inline: on the plate's
   line instead, below).
   On a phone they share ONE row - the plate on the left, the toolbar's glyphs at its right
   end - because a phone's first screen is the page's own table, and a header
   plus a toolbar row was ~95px of it before anything could be entered. To fit
   359px beside four to six 32px buttons, the plate drops its medallion there
   and its title wraps to a second line rather than being cut off. */
@media (max-width: 768px) {
  .sk-page-top {
    display: flex;
    align-items: center;
    gap: var(--sk-space-1);
    margin-block: var(--sk-space-3);
  }

  .sk-page-top > .sk-page-header {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
  }

  /* 3px between glyphs, not a token step: with five 32px buttons it is the
     last few px a two-word title needs on a 360px phone, and the buttons'
     own borders already separate them. */
  .sk-page-top > .page-toolbar {
    flex: none;
    margin: 0;
    gap: 3px;
  }

  /* A compact page has no side gutter on a phone (its cards run to the edge),
     so the toolbar keeps one of its own: its last glyph sat on the screen's
     edge (review, 2026-10-19 - Modules' Reset). */
  .sk-page--compact > .sk-page-top > .page-toolbar {
    margin-right: var(--sk-gutter);
  }

  /* Its reading pills keep the gutter too: a full-width pill ran from one edge
     of the screen to the other (review, 2026-10-19 - Shields' power generation).
     Not a row inside a card, which has the card's own padding. */
  .sk-page--compact .sk-pill-row:not(.sk-card *) {
    margin-inline: var(--sk-gutter);
  }

  .sk-page-top .sk-page-header-art {
    display: none;
  }

  .sk-page-top .sk-page-header-title {
    min-width: 0;
    /* space-2 past each cut, not the plate's usual space-3: the 8px is what
       lets "Training Room" sit beside five glyphs on a 360px phone. */
    padding-inline: calc(var(--sk-header-cut) + var(--sk-space-2));
    /* One step under the phone plate's --sk-text-lg, so a two-word name
       ("Construction Site", "Training Room") fits beside four glyphs. When it
       still does not (five glyphs on a 375px phone), it takes a second line -
       balanced, so the two words split evenly - rather than losing letters to
       an ellipsis: a cut-off page name was the review's complaint. */
    font-size: var(--sk-text-base);
    line-height: 1.15;
    text-wrap: balance;
  }
}

/* On a desktop the toolbar is the worded row under the plate that every page
   had before the redesign: the redesign changes the phone view only (John,
   2026-10-15). sk-page-top--inline keeps the redesign's desktop instead - the
   toolbar's glyphs on the header's own line, at its right end, the plate still
   centred - for a page that wants it on both widths (Modules). Absolute, so
   the plate keeps its centre; the header's right-hand streak runs behind the
   buttons. The buttons drop their words there as on a phone (each keeps its
   title), because five worded buttons ran into the plate even on a 1400px
   page. */
@media (min-width: 768.02px) {
  .sk-page-top--inline {
    position: relative;
  }

  .sk-page-top--inline > .page-toolbar {
    position: absolute;
    top: 50%;
    right: 0;
    margin: 0;
    transform: translateY(-50%);
  }
}

/* With page titles hidden (Preferences > Display, sk-hide-page-header on <html>)
   the toolbar has no plate to sit on or beside, so it is a centred row of its
   own at every width - it used to float at the right edge with nothing next to
   it (team review, 2026-10-19: Modules' Reset). (0,3,0), past the inline,
   compact-page and sk-row--end placements above. */
.sk-hide-page-header .sk-page-top {
  justify-content: center;
}

.sk-hide-page-header .sk-page-top > .page-toolbar {
  position: static;
  justify-content: center;
  margin-inline: auto;
  transform: none;
}

@media (min-width: 768.02px) {
  .sk-page-top--inline > .page-toolbar > .sk-btn--icon-mobile {
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 50%;
  }

  .sk-page-top--inline > .page-toolbar > .sk-btn--icon-mobile > .sk-btn-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

@media (max-width: 768px) {
  .page-toolbar {
    flex-wrap: nowrap;
    gap: var(--sk-space-2);
  }

  .page-toolbar > .sk-btn {
    flex: 0 1 auto;
    min-width: 0;
    min-height: 32px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  /* The icon button keeps its square: it is the one child that must not shrink. */
  .page-toolbar > .sk-btn--icon-mobile {
    flex: none;
  }
}

/* Centred block. An <asp:Label> renders a <span>, so centring one needs the
   display change too — which is why text-align alone kept not working. */
.sk-block-center {
  display: block;
  text-align: center;
  /* A centred block centres its own box too: card body copy is capped at
     70ch (.sk-card :where(p)), and without auto margins a capped, centred
     paragraph pins to the card's left edge - centred text in a left-pinned
     box reads as misalignment at the full page measure. No-op when the
     block is uncapped. */
  margin-inline: auto;
}

/* Page shell: keeps the last card clear of the viewport edge. */
/* The page wrapper owns its own measure. It previously set only bottom padding,
   so every sk- page ran the full width of the monitor unless it was also wrapped
   in Bootstrap's .container-md — which Probe.aspx does, as `container-md sk-page`.
   That is precisely the Bootstrap dependency this system exists to remove, and it
   left the width of a migrated page depending on which framework happened to be
   loaded. Pages already inside a narrower container are unaffected: the smaller
   of the two wins. */
.sk-page {
  max-width: var(--sk-page-max, 1400px);
  margin-inline: auto;
  padding-inline: var(--sk-gutter);
  padding-bottom: var(--sk-space-4);
}

@media (min-width: 768.02px) {
  .sk-page {
    padding-bottom: 50px;
  }
}

/* A page one column of cards wide. Same argument as sk-card--solo, one level
   up. Same specificity as .sk-page and declared after it, so it wins. The
   padding is added back because the box is border-box: the CONTENT comes out
   at --sk-compact-width, matching the Missile room's cards. */
.sk-page--compact {
  max-width: calc(var(--sk-compact-width) + 2 * var(--sk-gutter));
}

/* On a phone the compact page gives up its side gutter: its one column of
   cards runs to the screen edge (the owner, 2026-10-18). Declared after
   .sk-page at the same specificity, so it wins. */
@media (max-width: 768px) {
  .sk-page--compact {
    padding-inline: 0;
  }
}

/* A compact page on a phone, the full page measure on a computer: for a page
   whose rows have more to show than 900px gives them (Modules: name, cost,
   bar, stepper on one line instead of wrapped into three - team review,
   2026-10-20, "no need for it to be so tiny on desktop"). (0,2,0), after
   .sk-page--compact. */
@media (min-width: 768.02px) {
  .sk-page.sk-page--wide-desktop {
    max-width: var(--sk-page-max, 1400px);
  }
}

/* ...unless the page keeps it: sk-page--gutter-mobile holds the phone gutter on
   a compact page, for one whose card reads as pressed against the screen's
   sides (Shields' table of shields - the owner, 2026-10-19). Its pill rows then
   take no gutter of their own, or the two would add up. */
@media (max-width: 768px) {
  .sk-page.sk-page--compact.sk-page--gutter-mobile {
    padding-inline: var(--sk-gutter);
  }

  .sk-page.sk-page--compact.sk-page--gutter-mobile .sk-pill-row {
    margin-inline: 0;
  }
}

/* A page whose subject is one wide kingdom table - Sector View and Target
   Finder. From 1200px up the measure opens to 1920px, so a big screen does not
   see thirteen-plus columns squeezed into 1400px; below that it is the plain
   sk-page measure (the table fits the screen width there anyway). Sets the
   --sk-page-max that .sk-page reads, so the padding and centring are
   unchanged. A page that wants some blocks back at 1400px caps those blocks in
   its own stylesheet (sector-page.css does, for its nav, hero and totals). */
@media (min-width: 1200px) {
  .sk-page--wide {
    --sk-page-max: 1920px;
  }
}

/* ---- Readout: figures as lines of text ------------------------------------
   A short list of "label  figure (sub)" lines, no tiles - the compact form of
   a set of stats where the cards would take a phone screen (Attack's Summary
   on a phone, 2026-10-18). Label left in the muted text, figure right; a long
   line wraps its sub under the figure instead of running out.

     <ul class="sk-readout">
       <li><span class="sk-readout-label">Offense</span>
           <span class="sk-readout-value"><strong>12,345</strong><span class="sk-num-sub">(+8%)</span></span></li>
     </ul> */
.sk-readout {
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sk-readout > li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sk-space-2);
}

.sk-readout-label {
  color: var(--sk-text-muted);
}

.sk-readout-value {
  text-align: end;
}

/* Responsive visibility. These deliberately do not set a display value at rest
   — forcing `block` would flatten an inline or flex element that merely wanted
   to disappear on small screens. `revert` hands the element back its own
   default rather than guessing one. */
@media (max-width: 768px) {
  .sk-hide-mobile {
    display: none !important;
  }
}

.sk-hide-desktop {
  display: none;
}

/* ---- Flush on a phone ------------------------------------------------------
   For a page whose content on a phone IS one wide table: every px of gutter
   the page and the card take at the sides is a px its columns do not get.

     .sk-page--flush-mobile  the page gives up its side gutter.
     .sk-card--flush-mobile  the card gives up its side PADDING only - it keeps
                             its side borders and corners (the owner wants
                             the card's edge on a phone, 2026-10-18). It keeps a card's
                             normal top and bottom padding, so its title has
                             the same room above it as anywhere else (it once
                             kept a 4px sliver on top and nothing below, and a
                             title like Attack's "Deploy Personnel" sat on the
                             top border). Its section head keeps the card's
                             padding (only the table needs the width), so its
                             text lines up with the text in every other card
                             on the page. Its tables drop their bottom margin
                             and rule so three pieces of spacing do not stack
                             under the last row, and a fitted table's cells
                             take the narrowest padding.

   Only the sides go flush. The gap to the next card stays --sk-stack, the same
   as every other card, so the page keeps one vertical rhythm.

   sk-page--flush-mobile drops the page's own side gutter as well. Most pages
   do not need it - the phone gutter is 8px (--sk-gutter), so a table already
   gets the width - but the kingdom-list pages (Sector View, Target Finder)
   run edge to edge on a phone by design and keep it.

   Declared after sk-page, sk-card, sk-section-head and sk-table--fit, so each
   single-class override wins on source order. */
@media (max-width: 768px) {
  .sk-page--flush-mobile {
    padding-inline: 0;
  }

  /* A toolbar row or pager straight on a flush page, outside any card
     (Target Finder's), keeps the gutter the page gave up - cards supply
     their own. Not sk-page-header: its streaks are meant to run to the
     screen edge. */
  .sk-page--flush-mobile > :is(.sk-section-head, .sk-pager) {
    padding-inline: var(--sk-space-3);
  }

  /* An action row on a flush page keeps a gutter too: a full-width-on-phone button
     (Attack's Calculate) otherwise runs into both screen edges (review, 2026-10-19 -
     "no need for the text to be so glued to the side"). Inside a flush card this is the
     side padding the card gave up, so it lines up with the card's own content. */
  .sk-page--flush-mobile .sk-actions {
    padding-inline: var(--sk-space-3);
  }

  .sk-card--flush-mobile {
    padding-inline: 0;
  }

  .sk-card--flush-mobile .sk-section-head {
    padding-inline: var(--sk-card-pad);
  }

  .sk-card--flush-mobile .sk-section-head > .sk-eyebrow {
    padding-bottom: 0;
  }

  /* The hint under a section head lines up with it, rather than running to the
     card's edge (review, 2026-10-19 - Send Aid's recipient). */
  .sk-card--flush-mobile .sk-section-head + .sk-hint {
    padding-inline: var(--sk-card-pad);
  }

  .sk-card--flush-mobile .sk-table {
    margin-bottom: 0;
    border-bottom: 0;
  }

  .sk-card--flush-mobile .sk-table--fit :where(th, td) {
    padding: var(--sk-space-1);
  }

  /* ...except at the row's two ends: the outer cells keep the card's own side padding, so
     a unit name, a group heading or an input is not glued to the card's edge (review,
     2026-10-19 - Military, Attack). The inner columns stay tight, which is where the
     width a phone table needs comes from. (0,3,0): beats the rule above. */
  .sk-card--flush-mobile .sk-table :where(th, td):first-child {
    padding-left: var(--sk-space-3);
  }

  .sk-card--flush-mobile .sk-table :where(th, td):last-child {
    padding-right: var(--sk-space-3);
  }

  /* A card header straight in a flush card keeps a gutter, as the section
     head does. (Collapsed, it needs nothing more: the card keeps its own
     bottom padding now.) */
  .sk-card--flush-mobile > .sk-card-header {
    padding-inline: var(--sk-space-3);
  }

  /* Panels in a flush card become full-width bands: no side borders or
     corners, a gutter of their own, and a rule along the top only (the
     card's bottom border closes the last one). Otherwise page, card and
     panel padding stack up and a field gets two-thirds of a 375px screen
     (Attack's Intelligence card). Buildings' Income breakdown panels take
     the same shape. */
  .sk-card--flush-mobile .sk-card-inner {
    border-inline-width: 0;
    border-bottom-width: 0;
    border-radius: 0;
    padding-inline: var(--sk-space-3);
  }

  /* Two panels side by side on a desktop (an sk-split of sk-card-inners), in a
     flush card on a phone, are plain sections of the card rather than boxes in
     a box: no border, no recessed background, no inset shadow - the card's own
     frame is the only one, and its header's gap is the space above the first
     section (Attack's Intelligence card: card rule, a band, the panel's top
     edge, then the panel's own header rule made four lines before the first
     field). ONE rule between the two, on whichever of them comes second on
     screen: sk-swap-mobile puts the first child second with `order`, so the
     rule follows it there. Only this shape changes - a stack of panels in a
     flush card (Attack history's entries, Buildings' income breakdown) keeps
     the band look above. */
  .sk-card--flush-mobile .sk-split > .sk-card-inner {
    border-width: 0;
    background: none;
    box-shadow: none;
    padding-top: 0;
  }

  .sk-card--flush-mobile .sk-split:not(.sk-swap-mobile) > .sk-card-inner + .sk-card-inner,
  .sk-card--flush-mobile .sk-split.sk-swap-mobile > .sk-card-inner:first-child {
    border-top-width: 1px;
    padding-top: var(--sk-space-3);
  }

  /* A pill carrying two readings in a pairs grid takes the whole row, on one
     line, instead of wrapping to three lines in half of one and pulling the
     pill beside it out of alignment. (0,3,0), so it beats the pairs rule's
     white-space: normal without !important. */
  .sk-pill-row--pairs > .sk-pill.sk-pill--span {
    grid-column: 1 / -1;
    white-space: nowrap;
  }
}

@media (max-width: 768px) {
  .sk-hide-desktop {
    display: revert;
  }
}

.sk-text-muted {
  color: var(--sk-text-muted);
}

/* Tabular figures on demand, for a number outside a table or stat block. */
.sk-num {
  font-variant-numeric: tabular-nums;
}

/* An icon+figure pair (a cost, a coin count) inside a right-aligned numeric
   cell reads as right-aligned only if its own flex content is too - .sk-num
   sets text-align on the cell, but .sk-icon-label is a flex row and
   text-align does not reach into flex children. */
.sk-num > .sk-icon-label {
  justify-content: flex-end;
}

/* A figure coloured by whether the thing it measures is finished. Amber while
   there is still work to do, green once there is not - the two status tokens,
   not new colours (--sk-warning 10.43:1, --sk-success 7.64:1, both AAA).

   These replace the `.danger` / `.over` pair in general.css, whose names were
   inverted: `danger` meant INCOMPLETE there and `over` meant complete, which is
   how a rename onto --sk-danger would have turned nearly every row red.

   Declared after the sk-table rules on purpose. `.sk-table :where(th, td)` sets
   a cell colour at (0,1,0); these are (0,1,0) too, so the tie breaks on source
   order and they win - on the cell itself as well as on a span inside it. */
.sk-num--pending {
  color: var(--sk-warning);
}

.sk-num--done {
  color: var(--sk-success);
}

/* War leaders in a row, each a small round portrait with its ticks beside it (Status). A leader
   at home gets a green ring; one away is dimmed, so who can go is readable before the numbers. */
.sk-leaders {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sk-space-1) var(--sk-space-2);
}

.sk-leader {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
}

.sk-avatar {
  flex: none;
  width: 24px;
  height: 24px;
  border: 2px solid var(--sk-border-subtle);
  border-radius: 50%;
  object-fit: cover;
  object-position: center 15%;
}

.sk-leader--home .sk-avatar {
  border-color: var(--sk-success);
}

.sk-leader--away .sk-avatar {
  filter: grayscale(0.6);
  opacity: 0.75;
}

/* The third state: the figure is wrong, not merely unfinished.

   --sk-danger, NOT --sk-alert. The lighter tint is a decorative hue for rails
   and icons - the field-validation note at the top of section 7 spells this
   out - while this is body text and has to clear 4.5:1 on the card surface.
   --sk-danger is 6.29:1.

   Red against amber rather than a heavier weight. The pair this replaces on
   Status.aspx was #FF9900 bold against #fbc97d: two warm tones a couple of
   steps apart, where the bold was doing the real separating. Hue separates
   them at a glance and on a bad monitor. No font-weight here on purpose - one
   of three siblings rendering heavier would stop the set being
   interchangeable wherever a figure appears. */
.sk-num--alert {
  color: var(--sk-danger);
}

/* A secondary figure beside the primary one in a numeric cell - a share
   ("16.24%") after a count. Muted, a step down, spaced off the figure it
   qualifies. The state classes above recolour it; the doubled selectors are
   what let them beat its muted default regardless of source order. */
.sk-num-sub {
  margin-left: var(--sk-space-1);
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

.sk-num-sub.sk-num--done    { color: var(--sk-success); }
.sk-num-sub.sk-num--pending { color: var(--sk-warning); }
.sk-num-sub.sk-num--alert   { color: var(--sk-danger); }

/* The secondary figure on its own line under a stat's figure - a direct child
   of sk-stat, after the sk-stat-value, instead of inside it (Attack's
   "-3,150 short" under Offense). A tile's value is nowrap, so a sub inside it
   runs out of a phone tile; as the stat's next row it wraps under the figure.
   Flush with the figure: the indent is only for a sub beside it. */
.sk-stat > .sk-num-sub {
  margin-left: 0;
}

/* On a phone, inside a fitted table, the secondary figure takes its own line.
   sk-table--fit releases nowrap on .sk-num under 768px so a cell can wrap
   rather than overflow, and left alone the browser broke INSIDE the share -
   "779 16.51" with the % stranded on a third line. A block puts the break
   between the two figures; nowrap keeps each one whole. */
@media (max-width: 768px) {
  .sk-table--fit .sk-num-sub {
    display: block;
    margin-left: 0;
    white-space: nowrap;
  }
}

/* Compact density. Put it on any subtree - a card, a table, a whole page - and
   the spacing tokens shrink for everything inside. This is the payoff of
   spacing being tokens rather than literals: one class, no component edits.

   For 30-row tables (Scores, Alliance Members) where the default rhythm wastes
   a screenful. Type sizes are NOT reduced - density comes out of the spacing,
   never out of legibility. */
/* A card header is chrome, and chrome should read the same on every card. The
   density scale is for a card's CONTENT - it exists to fit more rows into the
   same box - and a header dragged down with it makes one card's title sit
   visibly tighter to its rule than its neighbours', which is what Kingdom
   Management looked like beside Combat potential on the Status page.

   Re-declaring the two tokens the header uses, scoped to the header element,
   is the whole fix: everything below it keeps the compact scale. Only these two
   are restored because only these two are the header's own spacing - a pill in
   a header still takes its --sk-space-2 from the card, which is right, since
   the pill IS content. */
.sk-density-compact > .sk-card-header {
  --sk-space-3: 12px;
  --sk-space-5: 20px;
}

.sk-density-compact {
  --sk-space-1: 2px;
  --sk-space-2: 5px;
  --sk-space-3: 8px;
  --sk-space-4: 10px;
  --sk-space-5: 12px;
  --sk-space-6: 14px;
  /* The layout tokens are resolved on :root and inherited as plain lengths, so
     rescaling the scale above does not reach them - they are re-pointed at it
     here, on the same breakpoints :root uses. Same values a compact card had
     when its padding read --sk-space-6 / --sk-space-4 directly. */
  --sk-card-pad: var(--sk-space-4);
  --sk-stack: var(--sk-space-5);
}

@media (min-width: 768.02px) {
  .sk-density-compact {
    --sk-card-pad: var(--sk-space-6);
    --sk-stack: var(--sk-space-6);
  }
}


/* ---- Stepper ---------------------------------------------------------------
   A number field flanked by decrement/increment and a quick-pick trigger.
   Used to be one fused, bordered group - abandoned because a single
   non-wrapping flex box has no room to give on a phone: three fixed button
   widths plus the input all have to fit on one line inside whatever the
   column leaves them, and a fused group can't give up width by wrapping
   without becoming three lines of pure chrome.

   Drawn instead as separate, individually bordered controls in a wrapping
   row - each one is a plain .sk-btn/.sk-input, the same components Convert
   and Research already build their own row-of-controls from - so a narrow
   column just wraps the row instead of crushing one shared box.

   Still a wrapper DIV placed INSIDE a table cell, never a modifier on the
   <td>: display:flex on a cell takes it out of the table's layout algorithm,
   colspan stops applying, and the cell shrinks to its own content. */
.sk-stepper {
  display: flex;
  /* ONE LINE on the wide layout - minus, field, plus, MAX, quick-pick. Never
     wrap here: the field carries min-width:0 and shrinks, so a column too
     narrow squeezes the field rather than throwing a control onto a second
     row. Wrapping is the phone layout's job, and it says so itself. */
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--sk-space-1);
  width: 100%;
}

/* SQUARE buttons, not circles. Research's action column settled this: an
   icon-only round button sized to its glyph comes out around 21x20px, and two
   of those a few px apart is a coin toss on a phone. Apple asks for 44pt,
   Material for 48dp, WCAG 2.5.5 for 44x44 CSS px - 40px is what a table
   column can give without starving the name beside it, and a rectangle gets
   there without the width a circle of the same height would demand.

   .sk-btn already beats general.css's bare `button` rule (min-width:160px,
   its own border/background), which is why no defensive overrides are needed
   here the way the old fused .sk-stepper-btn needed them. */
.sk-stepper-btn {
  flex: 0 0 auto;
  min-width: 0;
  width: 2.1rem;
  /* Height, not min-height. A 2.4rem floor made every button taller than the
     field beside it, which is what a stepper must never be: the row reads as
     one control, and the controls in it have to agree on a height. The phone
     layout raises this back to a tap target. */
  height: 1.85rem;
  padding: 0;
  /* sk-btn's 12px is a size for a word. These buttons are a single glyph and
     read as undersized chrome at that size inside a 40px target. */
  font-size: var(--sk-text-base);
}

/* Minus, field and plus, as one object. They are a single control that happens
   to be three elements, so they are grouped in the markup rather than left as
   three siblings for a wrapping row to split up - which is exactly what a
   wrapping row did, breaking between the minus and the field and stranding the
   minus on a line of its own. */
.sk-stepper-field {
  display: flex;
  align-items: center;
  gap: var(--sk-space-1);
  flex: 1 1 auto;
  min-width: 0;
}

/* MAX is a word, not a glyph, so it is the one stepper button sized by its
   label rather than by the square the others sit in.

   order puts MAX before the quick-pick trigger at every width. It is set here
   rather than in the markup because the DOM order - trigger, then MAX - is the
   one a keyboard and a screen reader should get. */
.sk-stepper-max {
  order: 4;
  width: auto;
  padding: 0 var(--sk-space-2);
  font-size: var(--sk-text-xs);
}

.sk-stepper-more {
  order: 5;
}

/* input.sk-stepper-input rides on .sk-input for its border, background, radius
   and padding - the same plain field Convert and Research use. It only sets
   what a spinner-free numeric field needs on top: its own width (sk-input's
   160px floor is a form-field width, absurd on a count inside a table column),
   centred text, and no native up/down arrows, since the buttons beside it
   already are the up/down control. */
input.sk-stepper-input {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  /* Matched to .sk-stepper-btn so the row lines up, rather than left to
     .sk-input's padding to arrive at the same number by luck. */
  height: 1.85rem;
  padding-block: 0;
  text-align: center;
  -moz-appearance: textfield;
}

input.sk-stepper-input::-webkit-outer-spin-button,
input.sk-stepper-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Inert state lives on the WRAPPER, and the buttons keep no `disabled`
   attribute. general.css:693 sets `button:disabled { background:
   rgba(255,255,255,.8); color: black !important }`, and no selector beats an
   !important - a disabled stepper button would render black-on-white inside a
   dark card. pointer-events plus aria-disabled gives the same behaviour with
   none of that fight. The <input> DOES keep a real `disabled`: that rule
   matches input[type=submit|button], never input[type=number]. */
.sk-stepper.is-disabled {
  opacity: .45;
}

.sk-stepper.is-disabled .sk-stepper-btn {
  pointer-events: none;
  cursor: not-allowed;
}

/* On a phone the controls STACK when several are visible at once: side by
   side, minus/field/plus/MAX/quick-pick need about 145px of column; the
   field and MAX alone need about 78px. Whether minus, plus and the
   quick-pick are drawn on a phone AT ALL is no longer this component's
   decision - it is Buildings' own per-device setting (buildings-columns.js
   CONTROL_DEFAULTS, buildings-page.css .buildings-ctrl-hide-mobile /
   -desktop). Mobile defaults to MAX only, reproducing this rule's old
   unconditional behaviour exactly, but the player can turn the rest back on
   - which is also why this block does not hide anything itself any more.

   The row inherits nowrap from the base rule (.sk-stepper), so whatever is
   visible cannot be split onto separate lines by a few pixels of rounding -
   the field shrinks instead. Earlier versions wrapped here and spent several
   rounds being wrong about where the break would land. */
@media (max-width: 768px) {
  /* The phone control size: 32px, the same as sk-btn--icon-mobile, so a
     stepper in a table row and the icon buttons in a page toolbar are one
     size on every action page. 36px stood every row a few px taller than its
     neighbours on the next page; 32px is still a full square tap target in a
     column that cannot give a 44px one. A floor over the desktop `height`. */
  .sk-stepper-btn {
    min-height: 32px;
  }

  input.sk-stepper-input {
    height: auto;
    min-height: 32px;
    padding-block: var(--sk-space-1);
    max-width: none;
  }

  .sk-stepper-max {
    width: 32px;
    padding: 0;
  }
}

/* ---- Menu ------------------------------------------------------------------
   A short list of choices anchored to the control that opened it.

   position:fixed, and it MUST be rendered outside every sk-card AND every
   sk-table. Two separate reasons, both load-bearing:
     - .sk-table is overflow:hidden (it needs it for its rounded corners), so a
       popover positioned inside a cell is clipped by its own table.
     - .sk-card carries backdrop-filter, which makes it the containing block for
       fixed descendants - the trap already documented for sk-drawer.
   One shared menu element per page, filled on open. Position arrives as two
   custom properties set by js/sk-menu.js, never as inline top/left: rule 2 of
   "classnames only". */
.sk-menu {
  position: fixed;
  top: var(--sk-menu-top, 0);
  left: var(--sk-menu-left, 0);
  z-index: 8500;
  min-width: 12rem;
  /* The default --sk-menu-left is 0 (see the var() fallback above), which is
     what renders before js/sk-menu.js has measured and repositioned it on
     open. Without a width ceiling that first, unpositioned paint is 12rem
     wide from the viewport's left edge - harmless on a normal window, but on
     one narrower than 12rem it pushes the PAGE into a horizontal scroll for
     the single frame before JS repositions it, and does so again any time the
     menu opens wider than the viewport can hold. */
  max-width: calc(100vw - 8px);
  max-height: 60dvh;
  padding: var(--sk-space-1);
  background: var(--sk-surface-raised);
  border: 1px solid var(--sk-border);
  border-radius: var(--sk-radius);
  box-shadow: var(--sk-shadow-lg);
  overflow-y: auto;
  overscroll-behavior: contain;
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--sk-transition), visibility 0s var(--sk-transition);
}

.sk-menu.is-open {
  visibility: visible;
  opacity: 1;
  transition: opacity var(--sk-transition);
}

.sk-menu-item {
  display: block;
  width: 100%;
  min-width: 0;
  padding: var(--sk-space-2) var(--sk-space-3);
  border: 0;
  border-radius: var(--sk-radius-sm);
  background: transparent;
  color: var(--sk-text);
  font-family: var(--sk-font);
  font-size: var(--sk-text-sm);
  font-variant-numeric: tabular-nums;
  text-align: left;
  cursor: pointer;
}

/* general.css styles bare `button` with its own border, background, box-shadow
   and a 160px min-width, and one class does not settle it - the rows rendered
   as a stack of bordered boxes rather than as a list. Element-qualified, which
   is how the rest of this file fights that rule rather than reaching for
   !important. Weight is restated for the same reason. */
button.sk-menu-item {
  min-width: 0;
  border: 0;
  box-shadow: none;
  background: transparent;
  font-weight: 400;
  text-align: left;
}

.sk-menu-item:hover,
button.sk-menu-item:hover {
  background: var(--sk-wash-accent-strong);
}

/* The last row is the ceiling, and it is the one most often wanted - given its
   own weight so the eye lands on it without hunting to the bottom of the list. */
.sk-menu-item:last-child {
  font-weight: var(--sk-weight-semibold);
}

.sk-menu-item:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: -2px;
}

/* An explanatory line rather than a choice - "nothing left to build". A <div>,
   not a disabled <button>, so general.css:693's black-on-white cannot reach it. */
.sk-menu-item.is-inert {
  color: var(--sk-text-muted);
  cursor: default;
}

.sk-menu-item.is-inert:hover {
  background: transparent;
}

.sk-menu-sep {
  height: 1px;
  margin: var(--sk-space-1) 0;
  background: var(--sk-border-subtle);
}

/* A menu of checkboxes rather than choices - Target Finder's Planet Type, any
   number ticked. Ticking one keeps the menu open (js/sk-menu.js closes on an
   sk-menu-item only); a click outside or Escape closes it, and below 768px it
   is the same bottom sheet as every sk-menu. Each sk-checkbox becomes a full
   row with the item's padding and hover. */
.sk-menu--checklist .sk-checkbox {
  display: flex;
  width: 100%;
  padding: var(--sk-space-2) var(--sk-space-3);
  border-radius: var(--sk-radius-sm);
}

.sk-menu--checklist .sk-checkbox:hover {
  background: var(--sk-wash-accent-strong);
}

/* A floating popover anchored to a control near the bottom of a phone screen is
   a bad bet; below 768px it becomes a sheet. Same element, same class, same
   script - only the geometry changes, exactly as sk-drawer does. */
@media (max-width: 768px) {
  .sk-menu {
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    max-height: 55dvh;
    border-radius: var(--sk-radius-lg) var(--sk-radius-lg) 0 0;
    padding: var(--sk-space-3);
  }

  .sk-menu-item {
    padding: var(--sk-space-3);
    font-size: var(--sk-text-base);
  }
}

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

/* ---- Disclosure ------------------------------------------------------------
   A quiet "show more" control: a caret and a short label, no button chrome,
   muted. For a panel a phone keeps collapsed behind it. WHEN it shows is the
   page's decision; what it looks like is this.

   It carries sk-btn as well, only so general.css's bare <button> rule
   (min-width 160px, its own border and fill) is neutralised - and then undoes
   sk-btn's look, including on hover. Qualified with the element because
   button.sk-btn is (0,1,1) and its hover (0,2,1).

   Colour and weight sit on the CHILDREN: general.css forces
   color: white !important and font-weight: 700 !important on every <button>,
   which no normal declaration beats - but they land on the button, so a child
   with its own rule escapes them. Weight on the LABEL only: .fas is weight 900
   and that is how Font Awesome 5 picks its Solid face, so 400 on the caret
   renders it as nothing.

   Full width and centred, with 8px above and below (--sk-space-2): a disclosure is a row of its
   own between the content it opens and what surrounds it, and it is the same on every page. A
   page does not restyle it - no .buildings-*-toggle spacing rule, no .military-page override.

   .is-open on the control turns the caret: one glyph rotated, so it cannot
   drift out of step with the panel. No transition, so there is nothing for
   prefers-reduced-motion to switch off.

       <button type="button" class="sk-btn sk-disclosure" aria-expanded="false">
         <i class="fas fa-chevron-down sk-disclosure-caret"></i>
         <span class="sk-disclosure-label">More info</span>
       </button> */
button.sk-disclosure {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sk-space-1);
  width: 100%;
  text-align: center;
  margin: var(--sk-space-2) 0;
  padding: var(--sk-space-1) var(--sk-space-1) var(--sk-space-1) 0;
  border: 0;
  background: none;
  box-shadow: none;
}

button.sk-disclosure:hover {
  border: 0;
  background: none;
  box-shadow: none;
}

.sk-disclosure-caret,
.sk-disclosure-label {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

.sk-disclosure-label {
  font-weight: 400;
}

.sk-disclosure.is-open .sk-disclosure-caret {
  transform: rotate(180deg);
}


/* ---- Icon + label ----------------------------------------------------------
   A small square image before a label, on one line, with the text free to wrap
   beside it. Built for the building artwork in scripts/img/buildings/, which is
   drawn to stay legible at 24x24.

   Like sk-stepper, it is a wrapper DIV placed INSIDE a table cell and never a
   modifier on the <td>: display:flex on a cell takes it out of the table's
   layout algorithm and colspan stops applying. */
.sk-icon-label {
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  /* Without this a long name cannot shrink below its max-content width and
     pushes the cell wider than its column, which sk-table--fixed then clips. */
  min-width: 0;
}

.sk-icon-label-img {
  flex: 0 0 auto;
  width: var(--sk-icon-label-size, 24px);
  height: var(--sk-icon-label-size, 24px);
  border-radius: var(--sk-radius-sm);
  object-fit: contain;
}

/* A wrapping ROW, not a column: the name and its lock badge sit side by side
   while they fit and the badge drops to a second line when they do not. The
   row gap is what keeps that second line from touching the first. */
.sk-icon-label-text {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sk-space-1) var(--sk-space-2);
  min-width: 0;
}

/* Nothing in the label may overflow into the next cell, whatever width the
   column ends up. The name is a flex item inside flex items, and a flex item's
   automatic minimum size is its longest WORD - so a one-word name like
   "Residences" could not shrink and printed straight over the figure beside
   it. break-word only breaks a word that cannot fit a line on its own. */
.sk-icon-label-text > span {
  min-width: 0;
  overflow-wrap: break-word;
}

/* A requirement or caveat under the label: a quiet second line, not a badge
   competing with the name. The full-width basis is what puts it on its own
   line inside the wrapping row. */
.sk-icon-label-note {
  flex-basis: 100%;
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
}

/* A row for something the player has not researched yet: UpgradeUnits' own
   five fixed paths, and (once toggled on) an unresearched row on the
   Buildings/Military tables. Dimmed rather than hidden - opacity, not
   display/visibility, so the row still holds its place in the table and its
   own note/lock icon stay readable at reduced strength rather than vanishing
   outright. Shared here because three tables use it; a table-specific
   stylesheet duplicating this would be the exact thing this system's own
   migration checklist argues against. */
.sk-row-locked {
  opacity: 0.5;
}

.sk-row-locked-icon {
  color: var(--sk-text-muted);
  margin-right: var(--sk-space-1);
}

/* On a phone, the label and its note become a LEFT-ALIGNED STACK and the label
   steps down a size: it has two lines to use and is still read as a label,
   while the figures beside it are the content and keep their size. Leading,
   not centred - centred, a short name floated in whatever the image left and
   the gap to the image varied row by row with the name's length.

   Opt-in: a table with room for the image beside the name (Convert, Raze)
   should not give that up. */
@media (max-width: 768px) {
  .sk-icon-label--stack-mobile {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sk-space-1);
    text-align: left;
  }

  .sk-icon-label--stack-mobile .sk-icon-label-text {
    flex-direction: column;
    align-items: flex-start;
    font-size: var(--sk-text-min);
    line-height: 1.25;
  }
}

/* ---- sk-unit-art: illustrated-row artwork and pattern --------------------
   A unit row's own colour (sampled from its artwork by js/sk-art-textures.js)
   painted through two layers behind the name cell - a masked procedural
   pattern (--sk-unit-tex, one of sk-art-textures.js's six generic motifs -
   there is no per-unit motif to author, these are real illustrations, not
   building line art) and a plain fading wash - plus a glow on the artwork
   itself. Same technique buildings-page.css uses behind a building's name
   (--bld-hue/--bld-tex); promoted here, generically named, because Military,
   Disband and Upgrade all carry the same unit rows and duplicating this
   per page stopped being defensible at the third copy. Buildings' own
   version stays page-scoped on purpose - one page, its own convention.

   data-motif and the inline --sk-unit-hue/--sk-unit-tex style are set by
   the host page/component (motifFor()/textureStyle() in Military.aspx and
   DisbandUnits.ascx); both come back null/{} when there is nothing to
   paint (Unit artwork off, or no premium gate at all), which removes the
   attribute and the style rather than leaving a stale colour - the CSS
   selector then simply stops matching. */
.sk-table td.sk-unit-name-cell {
  position: relative;
  isolation: isolate;
}

.sk-table td.sk-unit-name-cell[data-motif]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--sk-unit-hue, transparent);
  opacity: 0.3;
  -webkit-mask-image: var(--sk-unit-tex), linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.55) 40%, rgba(0, 0, 0, 0) 90%);
  mask-image: var(--sk-unit-tex), linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.55) 40%, rgba(0, 0, 0, 0) 90%);
  -webkit-mask-size: cover, 100% 100%;
  mask-size: cover, 100% 100%;
  -webkit-mask-position: left center, 0 0;
  mask-position: left center, 0 0;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The wash: tinted by the unit, strongest behind the artwork, so the pattern
   reads as belonging to the row rather than laid on the table. */
.sk-table td.sk-unit-name-cell[data-motif]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: linear-gradient(to right, color-mix(in srgb, var(--sk-unit-hue, transparent) 14%, transparent), transparent 75%);
}

/* The glow, on every row with a colour. drop-shadow rather than box-shadow:
   it follows the image's alpha, so the glow hugs the artwork instead of its
   square box. */
.sk-unit-art img {
  width: 28px;
  height: 28px;
  margin-bottom: -14px;
  object-fit: contain;
  filter: drop-shadow(0 0 2px color-mix(in srgb, var(--sk-unit-hue, transparent) 80%, transparent))
          drop-shadow(0 0 6px color-mix(in srgb, var(--sk-unit-hue, transparent) 45%, transparent));
}

/* Under 375px: the artwork sits BEHIND the name instead of beside it. Beside
   it, it costs the name column 36px - on a ~330px screen (a small phone, or a
   normal one zoomed in) that pushed Military's Train column off the right edge.
   From 375px up there is room, and it stays beside the name. Out of the flow it costs no width; faded and under the text
   (z-index -1 inside the cell's own isolation, above the pattern's ::before by
   source order), so the name stays readable over it. Military's phone column
   budget stops counting it at the same width (viewport.isNarrow in
   Military.aspx). Same treatment as Buildings' name cell (buildings-page.css). */
@media (max-width: 374px) {
  .sk-table td.sk-unit-name-cell .sk-unit-art img {
    position: absolute;
    left: var(--sk-space-1);
    top: 50%;
    z-index: -1;
    width: 36px;
    height: 36px;
    margin: 0;
    transform: translateY(-50%);
    opacity: 0.4;
  }
}

/* ---- sk-unit-name-tooltip: the same artwork and pattern, in a bubble -----
   Same technique as buildings-page.css's .buildings-name-tooltip. The
   trigger carries data-bs-custom-class="buildings-tooltip
   sk-unit-name-tooltip" - buildings-tooltip for the shared dark skin and the
   mobile exemption (general.css:1782 hides every tooltip WITHOUT that class
   under 768px), sk-unit-name-tooltip for this layout.

   NOT scoped to any page class: Bootstrap renders a tooltip as a child of
   <body>, where a page-scoped selector can never reach it - the same reason
   buildings-page.css's own tooltip rules are unscoped. --sk-unit-hue/
   --sk-unit-tex are copied onto the bubble from the row's name cell when it
   opens (the shown.bs.tooltip listener in Military.aspx/DisbandUnits.ascx/
   UpgradeUnits.ascx). */
.sk-unit-name-tooltip.tooltip .tooltip-inner {
  max-width: 320px;
  padding: 0;
  overflow: hidden;
  text-align: left;
}

.sk-unit-name-tooltip .sk-unit-name-tip {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: var(--sk-space-2);
  padding: var(--sk-space-2);
}

.sk-unit-name-tooltip .sk-unit-name-tip::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--sk-unit-hue, transparent);
  opacity: 0.3;
  -webkit-mask-image: var(--sk-unit-tex, none), linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.55) 40%, rgba(0, 0, 0, 0) 90%);
  mask-image: var(--sk-unit-tex, none), linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.55) 40%, rgba(0, 0, 0, 0) 90%);
  -webkit-mask-size: cover, 100% 100%;
  mask-size: cover, 100% 100%;
  -webkit-mask-position: left center, 0 0;
  mask-position: left center, 0 0;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The wash: strongest behind the artwork, gone by the end of the text. */
.sk-unit-name-tooltip .sk-unit-name-tip::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: linear-gradient(to right, color-mix(in srgb, var(--sk-unit-hue, transparent) 22%, transparent), transparent 80%);
}

.sk-unit-name-tip-img {
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: var(--sk-radius-sm);
  object-fit: contain;
}

.sk-unit-name-tip-text {
  min-width: 0;
}


/* --------------------------------------------------------------------------
   14. SCROLLBARS
   --------------------------------------------------------------------------
   THIS IS THE ONE GLOBAL SECTION IN THIS FILE, and the only place here that
   changes a page which has not migrated. Everything else is inert until markup
   opts in by using an sk- class; these selectors are not, by nature — a
   scrollbar has no classname to hook.

   It holds two exceptions, each with its own reason.

   The scrollbar chrome is deliberate. general.css:57-75 already themes the
   scrollbar (rgba(27,52,105,.7) thumb on a near-black track), so the colours
   and widths replace one global treatment with another rather than introducing
   behaviour; the same selectors here win on load order. The alternative —
   leaving the app's most visible piece of chrome in the old palette until the
   last page migrates — makes every migrated page look half-done.

   scrollbar-gutter on :root is the second exception, and it IS new behaviour,
   at the layout level: it stops the layout jumping when a page gains or loses
   a scrollbar. Its app-wide cost: a page shorter than the window still reserves
   roughly the scrollbar's width on the right, so centred content on every page,
   migrated or not, sits about half that width left of where it did.

   Scoped overrides still win, so .sk-tabs and .pref-tabs keep hiding theirs. */

:root {
  scrollbar-width: thin;
  scrollbar-color: var(--sk-neutral-3) var(--sk-surface-inner);
  /* Reserve the gutter whether or not the page scrolls, so a centred column
     does not shift sideways when content grows past the viewport - the shift
     made every page transition twitch. The second global exception; the
     section header states its app-wide cost. Progressive enhancement - a
     browser without scrollbar-gutter keeps today's behaviour. */
  scrollbar-gutter: stable;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: var(--sk-surface-inner);
}

/* The transparent border plus background-clip gives the thumb breathing room
   inside its track without a second element to style. */
::-webkit-scrollbar-thumb {
  background: var(--sk-neutral-3);
  border: 2px solid transparent;
  background-clip: content-box;
  border-radius: 10px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--sk-border-strong);
  background-clip: content-box;
}

::-webkit-scrollbar-corner {
  background: var(--sk-surface-inner);
}


/* --------------------------------------------------------------------------
   15. PREFERENCES
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .sk-btn,
  button.sk-tab,
  .sk-input,
  .sk-select,
  .sk-card-inner,
  .sk-progress-bar,
  .sk-switch > input[type="checkbox"] + label::before,
  .sk-switch > input[type="checkbox"] + label::after,
  .sk-checkbox > input[type="checkbox"] + .sk-checkbox-box {
    transition: none;
  }

  .sk-btn:hover {
    transform: none;
  }

  /* The emblem moves in two places - the assembly lifts and the plate's glow
     swells - and both are decoration. The glow's resting value stays; only the
     change is dropped.

     THE THEME ATTRIBUTE IS LOAD-BEARING HERE, not decoration copied from the
     component. A media query adds no specificity, so these have to MATCH the
     themed rules they are switching off and win on source order instead. Drop
     the attribute and `.sk-emblem-plate` falls to (0,1,0) against the theme's
     (0,2,0) - the transition and the launch key's animation both survive a
     reduced-motion preference, silently. */
  [data-sk-theme="emblem"] .sk-emblem,
  [data-sk-theme="emblem"] .sk-emblem-plate {
    transition: none;
  }

  [data-sk-theme="emblem"] .sk-emblem:hover {
    transform: none;
  }

  /* The launch key stops breathing. It keeps the cap, the chevrons and the
     resting glow - everything that says what it is - and loses only the part
     that moves. */
  [data-sk-theme="emblem"] .sk-emblem--armed .sk-emblem-plate,
  [data-sk-theme="emblem"] .sk-emblem--armed:hover .sk-emblem-plate {
    animation: none;
  }

  /* The sheen is decoration; park it off-screen rather than sweeping it. */
  .sk-btn--cta::after {
    transition: none;
    opacity: 0;
  }

  .modal-overlay .modal-card {
    animation: none;
  }

  .sk-flash,
  .sk-blink {
    animation: none;
  }

  .sk-slide-enter-active,
  .sk-slide-leave-active,
  .sk-card.sk-card--collapsible > .sk-card-header {
    transition: none;
  }
}

/* Windows high-contrast mode replaces colours wholesale; without explicit
   borders the glass surfaces collapse into one another. */
@media (forced-colors: active) {
  .sk-card,
  .sk-card-inner,
  button.sk-tab,
  .sk-input,
  .sk-select,
  .sk-table,
  .sk-progress,
  .sk-checkbox-box {
    border: 1px solid CanvasText;
  }

  .sk-tab.is-active {
    border: 2px solid Highlight;
  }

  /* Every surface the emblem is built from is a gradient, and forced-colors
     replaces all of them - leaving a cut-cornered silhouette with no edge and
     no face. Drop both clips and hand it back an ordinary border, so it is a
     button again rather than an outline of one. */
  [data-sk-theme="emblem"] .sk-emblem-plate,
  [data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem,
  [data-sk-theme="emblem"] input[type="button"].sk-btn--emblem,
  [data-sk-theme="emblem"] button.sk-btn--emblem,
  [data-sk-theme="emblem"] a.sk-btn--emblem {
    clip-path: none;
  }

  [data-sk-theme="emblem"] input[type="submit"].sk-btn--emblem,
  [data-sk-theme="emblem"] input[type="button"].sk-btn--emblem,
  [data-sk-theme="emblem"] button.sk-btn--emblem,
  [data-sk-theme="emblem"] a.sk-btn--emblem {
    border: 1px solid CanvasText;
  }

  [data-sk-theme="emblem"] .sk-emblem-disc {
    border: 1px solid CanvasText;
  }
}

/* ---- Clip backdrop -----------------------------------------------------------
   A short muted video that fades in behind a card's content, loops a few times
   and goes away (js/sk-clip-bg.js adds and removes both the video and the
   sk-card--clip class, so nothing here shows on a page that never plays one).
   z-index -1 inside the card's own stacking context (backdrop-filter, plus
   isolation for the cards that lack it) lands above the card background and
   below everything it contains. */
.sk-card--clip {
  position: relative;
  isolation: isolate;
}

.sk-clip-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
}

/* The still (skClipBackdrop's `still` option): an <img> in the same layer and
   at the same opacity as the clip, so the card reads the same over either. It
   sits under the video in DOM order and stays hidden while the clip plays,
   then takes is-shown as the clip fades out - a cross-fade from the last
   frames to the picture they were made from. When the clip does not play at
   all (hourly cooldown, reduced motion, refused autoplay, load error) it is
   shown straight away. Unlike the video it stays until the page reloads, and
   sk-card--clip stays with it. */
.sk-clip-bg.is-playing,
.sk-clip-bg.is-shown {
  opacity: 0.4;
}

/* ---- Clip panel --------------------------------------------------------------
   skClipFloat(src, { still }) in js/sk-clip-bg.js: a small video panel that
   slides in at the bottom-right of the viewport, plays its clip once, slides
   out and is removed. A full-width strip along the bottom on a phone, sliding
   up. One at a time - a new call removes the old panel.

   Appended to <body> (never inside an sk-card: backdrop-filter would make the
   card the containing block). z-index 1025: above the page and Bootstrap's
   sticky elements (1020), below the phone's fixed navbar (1030, whose open
   menu fills the screen), the menus (8500), the drawer (9000), the toast stack
   (9500) and the modal (10000).

   The script adds is-open two painted frames after appending the panel
   ("A transition needs a frame it has already painted"). Under reduced motion
   only a still is ever shown, and it fades rather than slides. */
.sk-clip-float {
  position: fixed;
  right: var(--sk-space-4);
  bottom: var(--sk-space-4);
  z-index: 1025;
  width: min(22rem, calc(100vw - 2 * var(--sk-space-4)));
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--sk-surface-on-image);
  border: 1px solid var(--sk-border-strong);
  border-radius: var(--sk-radius);
  box-shadow: var(--sk-shadow-lg);
  transform: translateX(calc(100% + var(--sk-space-4)));
  transition: transform 0.45s ease;
}

.sk-clip-float.is-open {
  transform: translateX(0);
}

.sk-clip-float-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.sk-clip-float-close {
  position: absolute;
  top: var(--sk-space-2);
  right: var(--sk-space-2);
  z-index: 1;
}

/* On a phone: the top right, sliding in from the right (the owner, 2026-10-18),
   not along the bottom edge. top clears the game menu's fixed phone bar (GameMenu.ascx,
   53px, about 3.5rem), which sits above this panel (1030 > 1025). */
@media (max-width: 768px) {
  .sk-clip-float {
    top: calc(3.5rem + var(--sk-space-2));
    bottom: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sk-clip-float,
  .sk-clip-float.is-open {
    transform: none;
  }

  .sk-clip-float {
    opacity: 0;
    transition: opacity 0.45s ease;
  }

  .sk-clip-float.is-open {
    opacity: 1;
  }
}

/* ==========================================================================
   Kingdom list tables - sk-kd-table
   ==========================================================================
   The table that lists kingdoms with their status, figures and actions:
   Sector View (MySector.aspx), Alliance Info's members, Target Finder. Written
   out by hand on each page (Vue templates, not a shared control), so the look
   lives here and the logic in js/kd-table.js. Replaces css/sector-styles.css.

       <div class="sk-table-scroll">
         <table class="sk-table sk-density-compact sk-kd-table sk-table--dense-mobile">
           <thead><tr><th><button type="button" class="sk-th-sort is-sorted">
             <span class="sk-th-sort-label">Land</span>
             <i class="fas fa-sort-down sk-th-sort-caret" aria-hidden="true"></i>
           </button></th></tr></thead>
           <tbody>
             <tr class="sk-kd-row--newbie sk-is-current">...</tr>
             <tr class="sk-kd-actions-row"><td colspan="N">
               <div class="sk-kd-actions sk-kd-actions--labelled">...</div></td></tr>
           </tbody>
         </table>
       </div>

   NOT sk-table--fit. That variant goes table-layout:fixed under 768px, and
   fixed layout with no declared <col> widths splits every column EVENLY (see
   the stamp-column note below) - on a 10+ column kingdom table that squeezes
   Kingdom Name to the same width as "St", which is the opposite of the old
   page's phone layout. sk-table-scroll keeps natural, content-driven widths
   and only scrolls sideways inside its own box if a player's own text-size
   choice ever makes the row wider than the screen.

   ROW INK. The database names the status (dbo.GetKingdomStatus), js/kd-table.js
   turns it into sk-kd-row--<word>, and each word sets two properties:
     --sk-kd-ink    the row's text colour
     --sk-kd-force  set only where the status must also beat a RANK colour
   Links read force > rank > ink > text. That order is the old behaviour
   exactly: current and SL rows kept their rank colours (general.css's
   `.sector-table .current a.topN`), newbie/inactive/vacation/dead/cheater rows
   did not (sector-styles.css's status rules won on load order). The viewer's
   own row clears --sk-kd-force (`initial` = guaranteed-invalid, so the var()
   fallback applies) because "current" always won over the row's status.

   SPECIFICITY. Cell colour is (0,2,2) against sk-table's (0,1,0); link colour
   is (0,3,3) against _typography.css's a:link (0,1,1) and general.css's
   `body table td a` (0,0,4), whose phone font-size the same rule resets. */

.sk-kd-row--sl       { --sk-kd-ink: var(--sk-gold); }
.sk-kd-row--newbie   { --sk-kd-ink: var(--sk-ion-deep);   --sk-kd-force: var(--sk-ion-deep); }
/* Inactive and vacation are kingdoms nobody is playing right now: their hue
   half-mixed into the muted grey, so they read as faded - still yellow / green
   at a glance, and still apart from dead's plain grey. */
.sk-kd-row--inactive { --sk-kd-ink: color-mix(in srgb, var(--sk-gold-deep) 55%, var(--sk-text-muted));
                       --sk-kd-force: color-mix(in srgb, var(--sk-gold-deep) 55%, var(--sk-text-muted)); }
.sk-kd-row--vacation { --sk-kd-ink: color-mix(in srgb, var(--sk-success) 55%, var(--sk-text-muted));
                       --sk-kd-force: color-mix(in srgb, var(--sk-success) 55%, var(--sk-text-muted)); }
.sk-kd-row--cheater  { --sk-kd-ink: var(--sk-danger);     --sk-kd-force: var(--sk-danger); }
.sk-kd-row--dead     { --sk-kd-ink: var(--sk-text-muted); --sk-kd-force: var(--sk-text-muted); }

/* After the status words and more specific than them: the viewer's own row,
   in orange text and nothing else - no ground tint at rest (the pointer still
   tints it on hover, in the same orange via --sk-hue, like any other row
   answers the pointer). The palette has no orange and this is its only use, so
   it is a literal here, not a token (orange-400, about 8:1 on the card). */
.sk-kd-table tr.sk-is-current {
  --sk-hue: #fb923c;
  --sk-kd-ink: #fb923c;
  --sk-kd-force: initial;
}

.sk-table.sk-table.sk-kd-table > tbody > tr.sk-is-current:not(:hover) > :where(th, td) {
  background: none;
}

.sk-table.sk-kd-table > tbody > tr > td {
  color: var(--sk-kd-ink, var(--sk-text));
}

.sk-table.sk-kd-table > tbody > tr > td a:not(.sk-btn) {
  color: var(--sk-kd-force, var(--sk-rank-ink, var(--sk-kd-ink, var(--sk-text))));
  font-size: inherit;
  text-decoration: none;
}

.sk-table.sk-kd-table > tbody > tr > td a:not(.sk-btn):hover {
  text-decoration: underline;
}

/* The alliance tag never took the row colour (it was an inline darkorange). */
.sk-table.sk-kd-table > tbody > tr > td a.sk-kd-alliance:not(.sk-btn) {
  color: var(--sk-warning);
}

/* The alliance name: never truncated, never clamped - it wraps as many lines
   as it needs (the user's rule: no ellipsis/line-clamp on player-facing text,
   at any width). width: max-content capped by max-width still caps the
   column at about 8em so one long name doesn't widen the whole column - a
   longer name just wraps further instead of losing characters. Names run to
   50 characters. In em, so it is about 64px in an 8px phone cell and scales
   with the player's text size. Badges after it (War / Grace) go to the line
   below. */
.sk-table.sk-kd-table .sk-kd-alliance {
  width: max-content;
  max-width: 8em;
  margin-inline: auto;
  overflow-wrap: anywhere;
}

/* A dead kingdom is not a target: its links go inert, as general.css's
   `.dead a` made them. */
.sk-table.sk-kd-table > tbody > tr.sk-kd-row--dead a {
  pointer-events: none;
}

/* Rank: 1-5 and top-10 on land/networth/VP/honor, purple to cyan. Sets a
   property rather than a colour so the row-ink chain above can rank it; the
   standalone rule is for a ranked link OUTSIDE a kd table (Sector View's
   sector networth pill). */
.sk-rank--1  { --sk-rank-ink: var(--sk-plasma-deep); font-weight: var(--sk-weight-bold); }
.sk-rank--2  { --sk-rank-ink: var(--sk-plasma); }
.sk-rank--3  { --sk-rank-ink: var(--sk-accent-hover); }
.sk-rank--4  { --sk-rank-ink: var(--sk-accent); }
.sk-rank--5  { --sk-rank-ink: var(--sk-link); }
.sk-rank--10 { --sk-rank-ink: var(--sk-ion); }

a.sk-rank:link,
a.sk-rank:visited {
  color: var(--sk-rank-ink, inherit);
}

/* Kingdom state icon in the status cell: the game's exact pre-sk colours (--sk-kd-state-*).
   Research keeps no colour, as before.
   (0,1,2) to beat general.css's body .fa-* glyph colours (body .fa-bolt is (0,1,1)) without relying on load order. */
body i.sk-kd-state--growth       { color: var(--sk-kd-state-growth); }
body i.sk-kd-state--mobilisation { color: var(--sk-kd-state-mobilisation); }
body i.sk-kd-state--offensive    { color: var(--sk-kd-state-offensive); }
body i.sk-kd-state--defensive    { color: var(--sk-kd-state-defensive); }
body i.sk-kd-state--dead         { color: var(--sk-text-muted); }

/* Status cell content: icon + word, one line. A wrapper, never the td. */
.sk-kd-status {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
  white-space: nowrap;
}

/* Kingdom name cell content: the expander caret (when shown) then the name.
   A wrapper, never the td - the old sector CSS made the td position:relative
   to pin the caret, which the system forbids. */
.sk-kd-name {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sk-space-1);
  min-width: 0;
}

/* break-word, not anywhere: `anywhere` also shrinks the name's min-content
   width, so an auto-layout table squeezes Kingdom Name below its longest word
   and breaks names mid-word ("Damdre / d"). break-word only splits a word
   that cannot fit on a line of its own. */
.sk-kd-name > a {
  overflow-wrap: break-word;
}

/* The name's tags (Sector View: kill-target badge, private-note glyph): beside
   the name on a desktop; on a phone a line of their own under it, so an 8px
   cell's name keeps its whole 5.5em width instead of sharing it with a badge.
   A grid on a phone, not flex-wrap: a wrapping flex row's min-content is one
   item, so a squeezed table could also wrap the caret off the name. Grid
   columns never wrap - caret and name stay one row, the tags span row two. */
.sk-kd-name-tags {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
}

@media (max-width: 768px) {
  .sk-kd-name:has(> .sk-kd-name-tags) {
    display: inline-grid;
    grid-template-columns: auto auto;
    column-gap: var(--sk-space-1);
    row-gap: var(--sk-space-1);
  }

  .sk-kd-name-tags {
    grid-column: 1 / -1;
    justify-content: center;
  }
}

/* sk-kd-table--wrap-names: kingdom names split onto more lines instead of
   setting the column's width (Sector View, whose default columns have to fit
   a phone). A line holds about 5.5em on a phone - roughly 10 characters, so
   "Baltu Miltu Salis" is "Baltu Miltu" / "Salis" - and 12em on a desktop
   (roughly 24). Breaks fall between words; a single word longer than a line
   is split inside it, so a 50-character name with no spaces still fits.
   width: max-content capped by max-width makes the name exactly min(name,
   cap) wide - which is why `anywhere` is safe here: the box has its own width,
   so the table cannot squeeze it thinner (the "Damdre / d" problem above). In
   em, so the caps scale with the player's text size. On a phone the alliance
   name (sk-kd-alliance, 8em) narrows to 5em for the same reason. */
.sk-table.sk-kd-table--wrap-names .sk-kd-name > a {
  display: inline-block;
  width: max-content;
  max-width: 12em;
  overflow-wrap: anywhere;
}

@media (max-width: 768px) {
  .sk-table.sk-kd-table--wrap-names .sk-kd-name > a {
    max-width: 5.5em;
  }

  .sk-table.sk-kd-table--wrap-names .sk-kd-alliance {
    max-width: 5em;
  }

  /* sk-kd-words: one word a line ("Eternal" / "Plains") - the column is as
     wide as its longest single word, not its longest name. */
  .sk-table.sk-kd-table--wrap-names .sk-kd-words {
    display: inline-block;
    width: min-content;
  }
}

/* Tablet width, or a phone with "Desktop site" on (769-1199px): the desktop
   column set is a lot of columns for not much room, and Planet Type
   (sk-kd-words) has no cap outside 768px - text wraps wherever the browser's
   auto table layout squeezes it on the way to a definite column width, which
   without a cap means a column whose min-content is its single longest word,
   same risk as sk-kd-actions above. Capped the same way sk-kd-name is capped
   (width: max-content + max-width, so `anywhere` is safe - the box owns its
   width, the table cannot squeeze it thinner) keeps it to at most two lines
   ("Forest and" / "Wilderness") instead of one word a line. */
@media (min-width: 769px) and (max-width: 1199px) {
  .sk-table.sk-kd-table--wrap-names .sk-kd-words {
    display: inline-block;
    width: max-content;
    max-width: 6.5em;
    overflow-wrap: anywhere;
  }

  /* Kingdom names on two lines too ("Dustfall" / "Holding"), not the 12em
     desktop line: same cap as Planet Type, so the table fits without
     scrolling sideways. 6.5em still holds the longest one-word planet type
     ("Mountainous") whole. */
  .sk-table.sk-kd-table--wrap-names .sk-kd-name > a {
    max-width: 6.5em;
  }
}

/* A figure's change, on its own line under the figure (Sector View's premium
   "last change" on Land/NW). Small and normal weight - it qualifies the
   figure, it must not outweigh it - at 0.8em, so it scales with whatever size
   the table is (8px phone cells included). The arrow comes from sk-delta. */
/* A badge inside a kingdom table cell (Sector View's War / Grace beside the
   alliance). sk-badge is sized for headers - --sk-text-min, 10px side padding -
   which inside an 8px phone cell is a pill bigger than the name it sits next
   to. Here it is sized in em, so it scales with the table's text at every
   width and every player text size: a step below the cell, tight padding, no
   tracking. */
.sk-table.sk-kd-table .sk-badge {
  padding: 0 0.45em;
  font-size: 0.85em;
  letter-spacing: 0;
  line-height: 1.5;
}

/* A cell's second line, kept whole (Sector View's race level: "Gistrami" over
   "Lvl 1"). Same size and colour as the cell - it is part of the value, not a
   qualifier like sk-kd-delta. */
.sk-table.sk-kd-table .sk-kd-sub {
  display: block;
  white-space: nowrap;
}

/* No nowrap on the whole line: a phone's Land/NW column is not held open by
   its widest change line; the figure itself has no break inside it. */
.sk-table.sk-kd-table .sk-kd-delta {
  display: block;
  font-size: 0.8em;
  font-weight: var(--sk-weight-normal);
  line-height: 1.1;
}

/* The expander trigger: a bare caret, no chrome - a 26px icon button beside
   8px phone text would outweigh the name. Qualified by element to beat
   _main.css's button:not(...) min-width:160px at (0,1,1); hover restated at
   (0,2,1) against general.css's button:hover. Colour sits on the glyph because
   general.css forces the button's own colour with !important.
   sk-kd-note is the same bare glyph after the name (Sector View's private
   note, premium; only on a kingdom that has one - adding one is the Note
   action in sk-kd-actions), coloured by the sk-hue-* on the button.
   sk-kd-bookmark (Target Finder, premium) is a bare filled star after the name,
   only on a bookmarked kingdom - not a button: adding and removing it is the
   Bookmark action in sk-kd-actions. em-sized like the name. */
button.sk-kd-expander,
button.sk-kd-note {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  min-width: 0;
  margin: 0;
  padding: 0 var(--sk-space-1) 0 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-size: inherit;
  line-height: 1;
  cursor: pointer;
}

button.sk-kd-note {
  padding: 0;
}

button.sk-kd-expander:hover,
button.sk-kd-note:hover {
  border: 0;
  background: none;
  box-shadow: none;
}

button.sk-kd-expander:focus-visible,
button.sk-kd-note:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-kd-expander :where(i) {
  color: var(--sk-text-muted);
  font-size: inherit;
}

.sk-kd-note :where(i) {
  color: var(--sk-hue, var(--sk-text-muted));
  font-size: inherit;
}

.sk-kd-bookmark {
  display: inline-flex;
  align-items: center;
  line-height: 1;
}

.sk-kd-bookmark :where(i) {
  color: var(--sk-warning);
  font-size: inherit;
}

/* Actions: a row of icon buttons in the Actions column, or labelled links in
   the expander row. The per-action hue comes from sk-hue-* on each link.
   --sk-space-2 rather than --sk-space-1: these buttons sit inside
   sk-density-compact, where space-1 shrinks to 2px - too tight a gap for
   four adjacent round tap targets to read as separate controls.

   flex-wrap: nowrap, not wrap - a tablet-width "desktop" layout (769-1199px,
   or a phone with "Desktop site" on) has a LOT of columns competing for not
   much room, and table-layout: auto shrinks a column towards its MIN-content
   before it ever lets the table overflow and scroll. A wrapping flex row's
   min-content is one button wide, so the Actions column was squeezed down to
   a 2x2 (even 4x1) block of buttons well before the table ran out of room to
   give. nowrap raises this column's min-content to all four buttons plus
   their gaps, so the table hits genuine overflow - and scrolls sideways
   inside sk-table-scroll, never inside its own cell - instead. */
.sk-kd-actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: var(--sk-space-2);
}

.sk-kd-actions .sk-btn :where(i) {
  color: var(--sk-hue, var(--sk-text-muted));
}

.sk-kd-actions--labelled > a {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
  padding: var(--sk-space-2) var(--sk-space-1);
}

.sk-kd-actions--labelled > a :where(i) {
  color: var(--sk-hue, var(--sk-text-muted));
}

/* The expander row on a phone: the links share the WHOLE row, icon above label.
   flex: 1 1 0 gives five links or six an equal share (no gap, so the shares are the
   row), each a full-height tap target; a hairline between them keeps the cells
   readable. Sizes are em, off the phone table cell (sk-table--dense-mobile sets it to
   8px): the icon at 1.4x the label, the label one step up from the cell. No dividers
   between the links (the owner, 2026-10-20). Never an ellipsis - nowrap, and at 375px
   with eight links (Target Finder premium: Note and Bookmark too) a link is ~46px
   against ~40px for "Bookmark", the longest label. Scoped to --labelled, so the Actions
   COLUMN of icon buttons (a.sk-btn) is untouched. Above 768px the row is as it was.

   The selector carries the table's classes because a:not(.sk-btn) in sk-kd-table sets
   font-size: inherit at (0,3,4) and would otherwise win. */
@media (max-width: 768px) {
  .sk-kd-actions--labelled {
    gap: 0;
  }

  .sk-table.sk-kd-table > tbody > tr.sk-kd-actions-row > td > .sk-kd-actions--labelled > a {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    justify-content: center;
    gap: 0.3em;
    padding: 0.8em 0.1em;
    font-size: 1.1em;
    line-height: 1.1;
    text-align: center;
    white-space: nowrap;
  }

  .sk-kd-actions--labelled > a :where(i) {
    font-size: 1.4em;
    line-height: 1;
  }
}

/* The expander row: the old blue band, on a token. (0,3,3) beats sk-table's
   row hover (0,1,3), so the band does not flicker under the pointer. */
.sk-table.sk-kd-table > tbody > tr.sk-kd-actions-row > td {
  padding: 0;
  background: var(--sk-wash-accent-strong);
}

/* Sortable header. A real button (keyboard, focus ring) that looks like the
   header text it replaced. Same base-rule fight as sk-kd-expander above. The
   caret shows on hover/focus, and always on the column that is sorted - on a
   phone, only on that column. */
button.sk-th-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--sk-space-1);
  width: auto;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font: inherit;
  text-align: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

/* A header lines up with the cells under it. text-align: inherit above stops
   the browser's button default (centre) re-centring a wrapped label ("LAND /
   GAIN") inside a left- or right-aligned column; on a right-aligned (sk-num)
   column the caret goes BEFORE the label, because it keeps its width while
   hidden and would otherwise push the label in from the edge the figures sit
   on. */
th.sk-num > button.sk-th-sort {
  flex-direction: row-reverse;
}

button.sk-th-sort:hover {
  border: 0;
  background: none;
  box-shadow: none;
}

button.sk-th-sort:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-th-sort-label,
.sk-th-sort-caret {
  color: var(--sk-text-muted);
}

.sk-th-sort-caret {
  opacity: 0;
  transition: opacity var(--sk-transition);
}

button.sk-th-sort:hover .sk-th-sort-caret,
button.sk-th-sort:focus-visible .sk-th-sort-caret,
.sk-th-sort.is-sorted .sk-th-sort-caret {
  opacity: 1;
}

.sk-th-sort.is-sorted .sk-th-sort-label {
  color: var(--sk-text);
}

@media (max-width: 768px) {
  .sk-th-sort-caret {
    display: none;
  }

  .sk-th-sort.is-sorted .sk-th-sort-caret {
    display: inline-block;
  }
}

/* Phone-only dense cells: sk-table--dense (--sk-text-dense, 8px), but only at
   768px and under, so the desktop keeps its normal size. For a table that fits
   a phone only at that size - Sector View's thirteen columns, which is what
   sector-styles.css did with a hard 8px. Same (0,1,0)/(0,1,1) as the base cell
   and header rules, winning by coming later in the file. A player-chosen size
   (sk-table--text-*) replaces this class rather than fighting it. */
@media (max-width: 768px) {
  .sk-table--dense-mobile :where(th, td) {
    font-size: var(--sk-text-dense);
    padding: var(--sk-space-1);
    letter-spacing: 0;
  }

  .sk-table--dense-mobile :where(thead) th {
    font-size: var(--sk-text-dense);
  }
}

/* Tablet-width compact kingdom table (769-1199px: a tablet, or a phone with
   "Desktop site" on). The desktop column set - Actions included, never hidden
   here - has to FIT without scrolling sideways, so this range borrows the
   phone's presentation: the short header labels and short status (the
   sk-hide-mobile / sk-hide-desktop pairs inside the table swap as they do under
   768px), cells at the 12px floor with tight padding, and smaller action
   buttons. Only while the player has not picked a text size: the page adds this
   class in the same place it adds sk-table--dense-mobile (the default), and a
   chosen sk-table--text-* replaces both. (0,2,0)+ so it beats the base cell,
   header and density rules without relying on file order. */
@media (min-width: 769px) and (max-width: 1199px) {
  .sk-table.sk-kd-table--compact-tablet :where(th, td) {
    font-size: var(--sk-text-min);
    padding: var(--sk-space-1);
    letter-spacing: 0;
  }

  .sk-table.sk-kd-table--compact-tablet :where(thead) th {
    font-size: var(--sk-text-min);
    letter-spacing: 0;
  }

  .sk-table.sk-kd-table--compact-tablet .sk-hide-mobile {
    display: none !important;
  }

  .sk-table.sk-kd-table--compact-tablet .sk-hide-desktop {
    display: revert;
  }

  .sk-table.sk-kd-table--compact-tablet .sk-kd-actions {
    gap: var(--sk-space-1);
  }

  .sk-table.sk-kd-table--compact-tablet .sk-kd-actions .sk-btn--icon-sm {
    width: 22px;
    height: 22px;
  }
}

/* Pager arrows that nudge in their direction on hover - the double chevron
   Sector View's prev/next always had (sector-styles.css goLeft/goRight),
   shared with Target Finder's pager. Put two <i> in the button; the first
   trails the second. */
@keyframes sk-nudge-fwd {
  0%   { opacity: 1; transform: translateX(0) scale(1); }
  25%  { opacity: 0; transform: translateX(10px) scale(0.9); }
  26%  { opacity: 0; transform: translateX(-10px) scale(0.9); }
  55%  { opacity: 1; transform: translateX(0) scale(1); }
}

@keyframes sk-nudge-back {
  0%   { opacity: 1; transform: translateX(0) scale(1); }
  25%  { opacity: 0; transform: translateX(-10px) scale(0.9); }
  26%  { opacity: 0; transform: translateX(10px) scale(0.9); }
  55%  { opacity: 1; transform: translateX(0) scale(1); }
}

.sk-btn--nudge-fwd:hover :where(i),
.sk-btn--nudge-back:hover :where(i) {
  animation-duration: 1.4s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}

.sk-btn--nudge-fwd:hover :where(i)  { animation-name: sk-nudge-fwd; }
.sk-btn--nudge-back:hover :where(i) { animation-name: sk-nudge-back; }

.sk-btn--nudge-fwd:hover :where(i:first-child),
.sk-btn--nudge-back:hover :where(i:first-child) {
  animation-delay: 0.2s;
}

@media (prefers-reduced-motion: reduce) {
  .sk-btn--nudge-fwd:hover :where(i),
  .sk-btn--nudge-back:hover :where(i) {
    animation: none;
  }
}

/* Scores: foundation */

/* Two rows a scores board marks, set by js/scores-table.js's rowClass(row).
   Both are distinct from .sk-table-row--call (a hue-tinted, ruled, bold
   conclusion row) and from each other: the viewer's own row is a steady green
   ground with a bar at its left edge, the linked row a warm flash that fades.

   Doubled .sk-table for the same reason .sk-is-current gives above: the row
   hover rule is (0,1,3), and a single class on the cells would lose to it the
   moment the pointer touched the row. The viewer's row keeps answering the
   pointer one step stronger again. */

/* The viewer's own row (config.isMine). */
.sk-table.sk-table > tbody > tr.sk-table-row--mine > :where(th, td) {
  background: color-mix(in srgb, var(--sk-success) 14%, transparent);
}

.sk-table.sk-table > tbody > tr.sk-table-row--mine:hover > :where(th, td) {
  background: color-mix(in srgb, var(--sk-success) 22%, transparent);
}

.sk-table.sk-table > tbody > tr.sk-table-row--mine > :where(th, td):first-child {
  box-shadow: inset 3px 0 0 var(--sk-success);
}

/* The row a ?id= link named (config.idKey): lit when the page opens, then
   fades over about five seconds - the time the old scores.js kept its
   highlight - and stays plain. `forwards` holds the last keyframe. On a row
   that is also the viewer's, the flash fades back to the green above. */
@keyframes skScoresFocus {
  0%   { background-color: color-mix(in srgb, var(--sk-warning) 45%, transparent); }
  60%  { background-color: color-mix(in srgb, var(--sk-warning) 40%, transparent); }
  100% { background-color: transparent; }
}

.sk-table.sk-table > tbody > tr.sk-table-row--focus > :where(th, td) {
  animation: skScoresFocus 5s ease-out forwards;
}

.sk-table.sk-table > tbody > tr.sk-table-row--focus > :where(th, td):first-child {
  box-shadow: inset 3px 0 0 var(--sk-warning);
}

.sk-table.sk-table > tbody > tr.sk-table-row--mine.sk-table-row--focus > :where(th, td):first-child {
  box-shadow: inset 3px 0 0 var(--sk-success);
}

@media (prefers-reduced-motion: reduce) {
  .sk-table.sk-table > tbody > tr.sk-table-row--focus > :where(th, td) {
    animation: none;
    background-color: color-mix(in srgb, var(--sk-warning) 22%, transparent);
  }
}

/* sk-scores-ranked: a board whose first column is the rank (at most three
   digits). Left to the auto layout it takes a share of the spare width like
   any other column; width: 1% shrinks it to its widest figure or head. */
.sk-table.sk-scores-ranked > thead > tr > :where(th, td):first-child {
  width: 1%;
  white-space: nowrap;
}

/* Scores: NW, Land, Honor */

/* The kingdom cell on the NW / Land / Honor boards: the name, its (G:S)
   beside it (muted - where the kingdom is, not part of its name), and the
   alliance on a line of its own under them. The location never breaks inside
   "(3:12)"; the name wraps before it does (sk-kd-table--wrap-names). */
.sk-scores-kd-loc {
  color: var(--sk-text-muted);
  white-space: nowrap;
}

/* sk-kd-alliance centres itself (margin-inline: auto) for Sector View's
   centred alliance column; under a left-aligned name it starts at the left.
   (0,4,0) to beat that rule's (0,3,0). */
.sk-table.sk-kd-table .sk-kd-alliance.sk-scores-kd-alliance {
  display: block;
  margin-inline: 0;
}

/* Scores: menu and domination */

/* The Scores menu's board tiles: an sk-card--tile as a link - icon, title
   with its one-line description under it, a chevron at the far end. Three
   across on a computer, one on a phone. The title and description wrap,
   never cut. */
.sk-scores-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sk-space-3);
}

@media (max-width: 768px) {
  .sk-scores-tiles {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sk-space-2);
  }
}

/* (0,2,1): a:link / a:visited in _typography.css are (0,1,1), and a base
   sheet drops every link to 10px on a phone - the size is set here. */
a.sk-scores-tile:link,
a.sk-scores-tile:visited {
  margin: 0;
  padding: var(--sk-space-3);
  color: var(--sk-text);
  font-size: var(--sk-text-sm);
  text-decoration: none;
  transition: border-color var(--sk-transition), background-color var(--sk-transition);
}

a.sk-scores-tile:hover {
  border-color: var(--sk-border-accent);
  border-left-color: var(--sk-hue-deep, var(--sk-accent));
  background: var(--sk-wash-accent);
  color: var(--sk-text);
  text-decoration: none;
}

a.sk-scores-tile:focus-visible {
  outline: 2px solid var(--sk-border-strong);
  outline-offset: 2px;
}

.sk-scores-tile-text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.sk-scores-tile-title {
  font-size: var(--sk-text-sm);
  font-weight: var(--sk-weight-semibold);
  overflow-wrap: break-word;
}

.sk-scores-tile-desc {
  color: var(--sk-text-muted);
  font-size: var(--sk-text-xs);
  overflow-wrap: break-word;
}

.sk-scores-tile-go {
  flex: none;
  color: var(--sk-text-muted);
}

/* The alliance on its own line under a kingdom's name (Domination board, the
   menu's top kingdoms). sk-kd-alliance keeps its colour, cap and wrapping;
   this puts it on its own line, flush with the name. (0,4,0) to beat the
   base's centring margin at (0,3,0). */
.sk-table.sk-kd-table .sk-kd-alliance.sk-scores-alliance {
  display: block;
  margin-inline: 0;
}

/* On a computer it wraps at the kingdom name's own 12em, not 8em, so the two
   lines of one cell break at the same measure. A phone keeps the base's 5em. */
@media (min-width: 769px) {
  .sk-table.sk-kd-table .sk-kd-alliance.sk-scores-alliance {
    max-width: 12em;
  }
}

/* The menu's Domination card: the two tables' headings, then the viewer's
   own line under them. */
.sk-scores-dom-title {
  font-size: var(--sk-text-sm);
}

.sk-scores-you {
  margin: var(--sk-space-4) 0 0;
  color: var(--sk-text-muted);
  font-size: var(--sk-text-sm);
}

.sk-scores-you strong {
  color: var(--sk-text);
}

/* In a flush card on a phone, only the tables run to the card's edge: the
   headings, the viewer's line and the pager keep a gutter, as the card
   header does. */
@media (max-width: 768px) {
  .sk-scores-dom .sk-scores-dom-title,
  .sk-scores-dom > .sk-scores-you,
  .sk-scores-board > .sk-pager {
    padding-inline: var(--sk-space-3);
  }
}

/* On a phone: the menu's two headings read as written, never UPPERCASE. And
   a table with only three or four columns (sk-scores-roomy: the menu's two,
   the board's alliances, its kingdoms with the breakdown shut) gives a name
   the room the 5.5em cap keeps for a ten-column table - otherwise a name
   breaks one word a line beside empty figure columns. */
@media (max-width: 768px) {
  .sk-scores-dom-title {
    text-transform: none;
    letter-spacing: 0;
  }

  .sk-table.sk-scores-roomy.sk-kd-table--wrap-names .sk-kd-name > a,
  .sk-table.sk-scores-roomy.sk-kd-table .sk-kd-alliance.sk-scores-alliance {
    max-width: 12em;
  }
}

/* Scores: hidden-name boards */

/* The text Top Land Gain, Most Kills, Most Aggressive Probing, Most Worthy
   and Career Stats show in place of a name the viewer may not see: italic
   (as the old <i> was) and muted, so it reads as a stand-in, not a name. */
.sk-scores-hidden {
  color: var(--sk-text-muted);
  font-style: italic;
}
