/* ============================================================================
   ASTRAMIR — TOKEN LAYER
   Direction: DISPERSION. A precision optical bench.

   Three layers, in order. Never reach past a layer to the one below it:
     1. PRIMITIVE  — raw values. Never referenced by a component.
     2. SEMANTIC   — role-named. This is what UI code consumes.
     3. RESERVED   — status ramp. Product-only. Withheld from marketing.

   Mode is a specification, not a theme:
     [data-mode="operate"]  dark — RTCC, investigator, EOC. The instrument observing.
     [data-mode="publish"]  light — Commander, council, print. The measurement published.

   Contrast rule that follows from the palette, and is not optional:
     on OPERATE, the accent that carries text is --a-cyan   (7.2:1)
     on PUBLISH, the accent that carries text is --a-azure  (4.8:1)
     --a-azure on operate is 3.9:1 — legal for large text, borders and icons only.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   1. PRIMITIVE
   ------------------------------------------------------------------------ */
:root {
  /* The dispersion band. The identity. Short-wavelength end of the visible
     spectrum, so it can never collide with the reserved status ramp.
     Ordered by wavelength, long-travel to short. */
  --p-violet:      #2a1a63;
  --p-indigo:      #33409e;
  --p-azure:       #2a6bce;
  --p-cyan:        #17a6db;
  --p-aperture:    #7fe6f4;

  /* Optical void. Not neutral black and not navy — carries a violet cast so
     the band sits inside the ground's own hue family rather than on top of it. */
  --p-void-900:    #05070e;
  --p-void-800:    #090d18;
  --p-void-700:    #101728;
  --p-void-600:    #1e2739;
  --p-void-500:    #2c3850;
  --p-void-400:    #45526e;
  --p-void-300:    #6e7b93;
  --p-void-200:    #a8b3c7;
  --p-void-100:    #d7dde8;
  --p-void-050:    #eef2f8;

  /* Calibration sheet. Cool near-white. Deliberately not cream — paper warmth
     belongs to the artifact worlds this direction replaced. */
  --p-sheet-000:   #ffffff;
  --p-sheet-050:   #f6f7fa;
  --p-sheet-100:   #edeff5;
  --p-sheet-200:   #d5dae5;
  --p-sheet-300:   #b3bccd;

  /* Reserved. Semantic state only. See layer 3. */
  --p-nominal:     #1e9e6a;
  --p-nominal-dk:  #106b46;
  --p-caution:     #e0a32e;
  --p-caution-dk:  #7a5200;
  --p-critical:    #dc3b2e;
  --p-critical-dk: #c42a1e;

  /* ---- Type ------------------------------------------------------------ */
  /* Archivo carries a width axis (62–125) and a weight axis (100–900). One
     superfamily does display, UI and dense label work, which is why the system
     needs only two faces. Martian Mono is for measured values and nothing else. */
  --font-sans: "Archivo", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Martian Mono", ui-monospace, "Cascadia Mono", monospace;

  --wdth-narrow: 82;
  --wdth-normal: 100;
  --wdth-wide:   112;

  /* Fluid scale. Display ceiling is 6rem and the tracking floor is -0.04em;
     wide widths at large sizes need negative tracking or they fall apart. */
  --text-display-1: clamp(2.75rem, 7vw, 6rem);
  /* Display 1 alt: for a display line of three authored clauses, where the
     Display 1 ceiling wraps and orphans a word. A documented step, not an
     ad-hoc override. */
  --text-display-1-alt: clamp(2.25rem, 5.4vw, 4.75rem);
  --text-display-2: clamp(2rem, 4.5vw, 3.5rem);
  /* Display 3: smallest display step, for specimens and glyph settings. */
  --text-display-3: clamp(1.75rem, 4.5vw, 3.25rem);
  --text-title:     clamp(1.5rem, 2.5vw, 2rem);
  --text-heading:   1.25rem;
  --text-body-lg:   1.125rem;
  --text-body:      1rem;
  --text-small:     0.875rem;
  --text-micro:     0.75rem;
  --text-notation:  0.8125rem;

  --track-display: -0.03em;
  --track-title:   -0.015em;
  --track-body:    0em;
  --track-label:   0.16em;   /* data labels, legends, axis marks — never an eyebrow */
  --track-notation: 0.02em;

  --leading-display: 0.98;
  --leading-title:   1.15;
  --leading-body:    1.6;
  --leading-notation: 1.45;

  --measure: 68ch;

  /* ---- Space. 4px base. ------------------------------------------------ */
  --space-1: 0.25rem;  --space-2: 0.5rem;   --space-3: 0.75rem;
  --space-4: 1rem;     --space-6: 1.5rem;   --space-8: 2rem;
  --space-12: 3rem;    --space-16: 4rem;    --space-24: 6rem;
  --space-32: 8rem;    --space-48: 12rem;

  /* ---- Optical geometry ------------------------------------------------
     A bench is machined, not moulded. Radii stay small; the only circle in
     the system is an aperture, which is always fully round. */
  --radius-hair: 1px;
  --radius-edge: 3px;
  --radius-plate: 6px;
  --radius-aperture: 50%;

  --hairline: 1px;
  --axis: 1px;
  --beam: 2px;

  /* ---- Motion ----------------------------------------------------------
     Four named motions. Light does not bounce, so nothing here overshoots.
       DISPERSE — one input fans into measured components
       CONVERGE — components resolve into a single conclusion
       FOCUS    — blur collapses to a sharp edge as certainty rises
       SWEEP    — a measuring pass travels across a field at constant rate
     Everything eases out exponentially from an already-visible default. */
  --ease-disperse: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-converge: cubic-bezier(0.33, 1, 0.68, 1);
  --ease-focus:    cubic-bezier(0.22, 1, 0.36, 1);
  --ease-sweep:    linear;

  --dur-instant: 90ms;
  --dur-fast:    180ms;
  --dur-base:    320ms;
  --dur-slow:    620ms;
  --dur-sweep:   2400ms;
}

/* ---------------------------------------------------------------------------
   2. SEMANTIC — OPERATE (dark). The instrument observing.
   The default. Verity is used in dim rooms for ten-hour shifts, so the ground
   is not pure black: #05070e keeps a floor of luminance so hairlines resolve
   without needing glow to be seen.
   ------------------------------------------------------------------------ */
:root,
[data-mode="operate"] {
  --color-ground:        var(--p-void-900);
  --color-ground-raised: var(--p-void-800);
  --color-surface:       var(--p-void-700);
  --color-surface-hover: var(--p-void-600);

  --color-rule:          var(--p-void-600);
  --color-rule-strong:   var(--p-void-500);
  --color-axis:          var(--p-void-500);

  --color-ink:           var(--p-void-050);
  --color-ink-2:         var(--p-void-200);
  --color-ink-3:         var(--p-void-300);
  --color-ink-inverse:   var(--p-void-900);

  /* Accent that may carry text on this mode. 7.2:1 on ground. */
  --color-accent:        var(--p-cyan);
  --color-accent-hover:  var(--p-aperture);
  /* Accent for borders, icons and large type only. 3.9:1 on ground. */
  --color-accent-quiet:  var(--p-azure);
  --color-accent-ink:    var(--p-void-900);

  --color-focus:         var(--p-aperture);

  /* The band, as a gradient. Rendered as a thin dispersed edge — a rule, a
     spine, a 2px band under a measured channel. Never a button fill, never a
     large wash, and never behind text. */
  --band: linear-gradient(90deg,
            var(--p-violet) 0%,
            var(--p-indigo) 26%,
            var(--p-azure) 52%,
            var(--p-cyan) 78%,
            var(--p-aperture) 100%);
  --band-vertical: linear-gradient(180deg,
            var(--p-violet) 0%,
            var(--p-indigo) 26%,
            var(--p-azure) 52%,
            var(--p-cyan) 78%,
            var(--p-aperture) 100%);

  /* Depth carries an offset and a real blur. A zero-offset coloured halo is
     decoration, and on an optical bench it reads as lens flare. */
  --shadow-plate: 0 1px 2px rgb(0 0 0 / 0.5), 0 8px 24px -8px rgb(0 0 0 / 0.6);
  --shadow-lift:  0 2px 4px rgb(0 0 0 / 0.5), 0 18px 48px -12px rgb(0 0 0 / 0.7);

  color-scheme: dark;
}

/* ---------------------------------------------------------------------------
   2. SEMANTIC — PUBLISH (light). The measurement published.
   Commander, council, procurement, print. Same geometry, printed rather than lit.
   ------------------------------------------------------------------------ */
[data-mode="publish"] {
  --color-ground:        var(--p-sheet-050);
  --color-ground-raised: var(--p-sheet-000);
  --color-surface:       var(--p-sheet-100);
  --color-surface-hover: var(--p-sheet-200);

  --color-rule:          var(--p-sheet-200);
  --color-rule-strong:   var(--p-sheet-300);
  --color-axis:          var(--p-sheet-300);

  --color-ink:           #0a0e1a;          /* 18.0:1 */
  --color-ink-2:         #414d66;          /*  7.9:1 */
  /* Not --p-void-300. That value reaches 4.72:1 on the void but only 4.02:1 on
     the sheet, and it carries every data label in Publish. Darkened to clear
     the floor: 5.3:1. The two modes do not share a tertiary ink. */
  --color-ink-3:         #5b6780;          /*  5.3:1 */
  --color-ink-inverse:   var(--p-sheet-000);

  /* Accent that may carry text on this mode. 4.8:1 on sheet. */
  --color-accent:        var(--p-azure);
  --color-accent-hover:  var(--p-indigo);
  --color-accent-quiet:  var(--p-indigo);
  --color-accent-ink:    var(--p-sheet-000);

  --color-focus:         var(--p-azure);

  /* On paper the band loses its bright end — cyan-white disappears into the
     sheet. The published band stops at azure and gains violet weight instead. */
  --band: linear-gradient(90deg,
            var(--p-violet) 0%,
            var(--p-indigo) 38%,
            var(--p-azure) 74%,
            var(--p-cyan) 100%);
  --band-vertical: linear-gradient(180deg,
            var(--p-violet) 0%,
            var(--p-indigo) 38%,
            var(--p-azure) 74%,
            var(--p-cyan) 100%);

  --shadow-plate: 0 1px 2px rgb(10 14 26 / 0.06), 0 8px 24px -10px rgb(10 14 26 / 0.12);
  --shadow-lift:  0 2px 4px rgb(10 14 26 / 0.08), 0 18px 48px -14px rgb(10 14 26 / 0.16);

  color-scheme: light;
}

/* ---------------------------------------------------------------------------
   3. RESERVED — the status ramp.

   Green, amber and red are STATE in a public safety interface: nominal,
   caution, critical. An operator reads them as fact, not as styling. So:

     · No identity element may use them. Not the logo, not a link, not a
       heading, not a chart series, not a hover, not an illustration.
     · They are withheld from marketing surfaces entirely. A brand that
       decorates with the colours it needs for meaning has spent its own
       vocabulary.
     · A chart with no status dimension uses the dispersion band. Only a chart
       that genuinely encodes state may use this ramp.
     · Never colour alone. Every state carries a label, a glyph or a position.

   Amber cannot carry text on PUBLISH at any usable size — 2.1:1. Use the
   -text token for type and reserve the plain token for fills and indicators.
   ------------------------------------------------------------------------ */
[data-mode="operate"] {
  --state-nominal:       var(--p-nominal);
  --state-nominal-text:  var(--p-nominal);      /* 5.9:1 */
  --state-caution:       var(--p-caution);
  --state-caution-text:  var(--p-caution);      /* 9.1:1 */
  --state-critical:      var(--p-critical);
  --state-critical-text: var(--p-critical);     /* 4.5:1 */
}

[data-mode="publish"] {
  --state-nominal:       var(--p-nominal);
  --state-nominal-text:  var(--p-nominal-dk);   /* 6.1:1 */
  --state-caution:       var(--p-caution);
  --state-caution-text:  var(--p-caution-dk);   /* 6.5:1, and amber never carries text at full chroma */
  --state-critical:      var(--p-critical-dk);
  --state-critical-text: var(--p-critical-dk);  /* 5.3:1 */
}

/* ---------------------------------------------------------------------------
   Reduced motion.

   The finite motions — DISPERSE, CONVERGE, FOCUS — collapse to 1ms so they
   resolve instantly to their end state and no information is lost.

   --dur-sweep is deliberately NOT collapsed. Accelerating a duration is only
   safe for an animation that ends. A 1ms duration on an `infinite` animation
   is a ~1000Hz strobe delivered to precisely the users who asked for less
   motion: a WCAG 2.3.1 seizure hazard, not a degradation. Looping motion must
   be STOPPED, never sped up.

   The iteration-count override below is the safety net that makes that true
   for any component consuming these tokens, including ones not written yet.
   Do not remove it in the belief that each component handles its own loops.

   The `:not([data-motion="full"])` guard lets the in-page motion control opt a
   user back into motion. The OS preference is the default and wins when nobody
   has said otherwise; an explicit click in the page is a clearer statement of
   intent than a global system flag, and without the guard that control would be
   dead for exactly the users most likely to go looking for it.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) {
    --dur-instant: 1ms; --dur-fast: 1ms; --dur-base: 1ms; --dur-slow: 1ms;
    /* --dur-sweep keeps its full 2400ms on purpose. See above. */
  }

  /* Iteration count only. `animation-play-state: paused` would hold every
     animation at its current time, which for one starting under this query is
     the FROM keyframe: the finite motions would freeze at their start instead
     of resolving to their end, which is the opposite of the rule above. */
  :root:not([data-motion="full"]) *,
  :root:not([data-motion="full"]) *::before,
  :root:not([data-motion="full"]) *::after {
    animation-iteration-count: 1 !important;
  }
}

/* ---------------------------------------------------------------------------
   WCAG 2.2.2 Pause, Stop, Hide  —  LEVEL A, and an OS preference does not
   satisfy it.

   Any motion that starts automatically, runs longer than five seconds and sits
   alongside other content must have an in-page mechanism to pause, stop or hide
   it. SWEEP loops indefinitely, so `prefers-reduced-motion` alone leaves the
   system non-conformant for every user who has not set that OS flag — which is
   most of them.

   Every Astramir surface that runs a sweep MUST therefore ship a visible motion
   control that sets `data-motion="reduced"` on the document element. This block
   is what that control actuates, and it mirrors the media query above exactly
   so there is one behaviour to reason about, not two.
   ------------------------------------------------------------------------ */
:root[data-motion="reduced"] {
  --dur-instant: 1ms; --dur-fast: 1ms; --dur-base: 1ms; --dur-slow: 1ms;
  /* --dur-sweep keeps its full value here too: loops are stopped, not sped up. */
}
:root[data-motion="reduced"] *,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after {
  animation-iteration-count: 1 !important;
}

/* ---------------------------------------------------------------------------
   FORCED COLORS  (Windows High Contrast and equivalents)

   Not optional for this buyer. Forced-colors mode is common in government
   workplaces, and it replaces every author colour with a system colour. Three
   consequences the system has to answer for:

   1. Gradients are dropped. The dispersion band becomes nothing, so any band
      that was carrying meaning becomes a missing element rather than a plain
      one. Band surfaces resolve to a solid system-coloured rule instead.
   2. Status fills are flattened to Canvas. The reserved ramp stops encoding
      anything — which is survivable only because "never colour alone" is a
      standing rule, so the label or glyph is already carrying the state. This
      is the payoff for that rule, not a lucky escape.
   3. Colour specimens are content. A swatch whose entire purpose is to show a
      colour is destroyed by forcing it, so specimens opt out deliberately with
      `forced-color-adjust: none`. Nothing else may opt out: a control or a
      status indicator that opts out is overriding a user's stated need.
   ------------------------------------------------------------------------ */
@media (forced-colors: active) {
  :root {
    --band: linear-gradient(90deg, CanvasText, CanvasText);
    --band-vertical: linear-gradient(180deg, CanvasText, CanvasText);
    --color-focus: Highlight;
  }
}
