/* ============================================================================
   DESKSCALE — DESIGN TOKENS
   The single source of truth for the visual system.
   Spec: IDENTITY.md · Brand: BRAND.md

   Consumed by deskscalelabs.com today. Written so Deskscale Platform can adopt
   the same file later (see the dark block) and the two surfaces stop drifting.

   Load order:  tokens.css  →  site.css  →  page styles
   Nothing in this file is a component. Components live in site.css.
   ---------------------------------------------------------------------------

   FONTS — no @import here on purpose. @import serialises the request behind the
   stylesheet and costs a render-blocking round trip. Put this in <head> instead:

     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?
       family=Geist:wght@400;500;600&
       family=Geist+Mono:wght@400;500&
       family=Instrument+Sans:wght@400;500;600&display=swap">

   ⚠️ Self-host before launch. One less third party, no layout shift, and it
   removes an external request from a site whose whole argument is provenance.
   ========================================================================= */

/* ==========================================================================
   1. COLOUR — LIGHT (site default)
   Contrast ratios are measured against --ds-bg and are load-bearing.
   Do not adjust a value without re-checking the ratio in the comment.
   ========================================================================== */

:root {
  color-scheme: light;

  /* Surfaces — warm white. Never #FFF; the warmth is what makes it read as stock. */
  --ds-bg:            #FBFBF9;
  --ds-bg-2:          #F2F2EE;   /* recessed cells, code, table stripes */
  --ds-bg-3:          #E8E8E2;   /* rare — footer, inset plates */

  /* Ink — near-black. Never #000. */
  --ds-ink:           #0E0E0F;   /* 19.0:1  headlines, body */
  --ds-ink-2:         #56565A;   /*  7.0:1  secondary copy, captions */
  --ds-ink-3:         #8B8B8F;   /*  3.3:1  ⚠ UI furniture + ≥18.66px bold ONLY.
                                            Never body text. CSS cannot enforce
                                            this — it is enforced at review. */

  /* Rules — the most-used tokens in the system. */
  --ds-rule:          #E2E2DC;   /* the hairline */
  --ds-rule-2:        #C9C9C2;   /* section boundaries, active cell edges */

  /* Accent — electric blue. 8.1:1 → passes AAA, so it is usable as TEXT,
     not only as a fill. Most brand blues are not; this one is chosen for it. */
  --ds-blue:          #1B2AE8;
  --ds-blue-2:        #1420B4;   /* hover, visited, pressed */
  --ds-blue-wash:     rgba(27, 42, 232, .07);
  --ds-blue-rgb:      27, 42, 232;

  /* Data semantics — DATA MEANING ONLY.
     Never for UI states (success/error/validation). Interface states use ink
     and blue. This single rule is most of why the system reads professional. */
  --ds-up:            #127A56;   /* 5.2:1  bullish */
  --ds-risk:          #B3261E;   /* 6.4:1  risk */
  --ds-warn:          #8A5A00;   /* 5.6:1  stale data, low confidence, degraded */

  /* Grain — makes #FBFBF9 read as paper stock rather than as a hex value. */
  --ds-grain-opacity: .022;
}

/* ==========================================================================
   2. COLOUR — DARK
   Used by the marketing site since 2026-09-29: pages ship
   <html data-theme="auto"> (follows the visitor's OS) and a header toggle
   that pins "light" or "dark" (js/ds-theme.js, remembered in localStorage).
   Deskscale Platform consumes the same block.
   ========================================================================== */

[data-theme="dark"] {
  color-scheme: dark;

  --ds-bg:            #0B0B0C;
  --ds-bg-2:          #131316;
  --ds-bg-3:          #1B1B1F;

  --ds-ink:           #F4F4F2;   /* 17.4:1 */
  --ds-ink-2:         #A0A09C;   /*  7.1:1 */
  --ds-ink-3:         #6B6B68;   /*  3.4:1  furniture only */

  --ds-rule:          #26262A;
  --ds-rule-2:        #37373C;

  /* #1B2AE8 fails on dark (2.4:1) — the accent must lighten, not carry over. */
  --ds-blue:          #6B78FF;   /* 6.9:1 */
  --ds-blue-2:        #98A2FF;   /* hover LIGHTENS on dark — inverted from light */
  --ds-blue-wash:     rgba(107, 120, 255, .10);
  --ds-blue-rgb:      107, 120, 255;

  --ds-up:            #3FBF8F;
  --ds-risk:          #F2645A;
  --ds-warn:          #D69A2E;

  --ds-grain-opacity: .030;      /* grain needs slightly more presence on dark */
}

/* Honour the OS preference while data-theme="auto" (the site's default until
   the visitor uses the toggle). */
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    color-scheme: dark;
    --ds-bg: #0B0B0C;      --ds-bg-2: #131316;    --ds-bg-3: #1B1B1F;
    --ds-ink: #F4F4F2;     --ds-ink-2: #A0A09C;   --ds-ink-3: #6B6B68;
    --ds-rule: #26262A;    --ds-rule-2: #37373C;
    --ds-blue: #6B78FF;    --ds-blue-2: #98A2FF;
    --ds-blue-wash: rgba(107, 120, 255, .10);
    --ds-blue-rgb: 107, 120, 255;
    --ds-up: #3FBF8F;      --ds-risk: #F2645A;    --ds-warn: #D69A2E;
    --ds-grain-opacity: .030;
  }
}

/* ==========================================================================
   3. TYPOGRAPHY
   Three faces, one job each.
   Geist            — display. Geometric grotesque, monumental at scale.
   Instrument Sans  — interface. Narrow neo-grotesque, tall x-height.
   Geist Mono       — data. Every number, ticker, timestamp, label, state word.

   NOT Inter. Inter is the most-used UI face on the web and carries no signal —
   it is the fastest way to make a distinctive grid look generic. Instrument
   Sans contrasts with Geist on the WIDTH axis, not just weight: headlines read
   wide and monumental, body reads efficient and dense.
   ========================================================================== */

:root {
  --ds-font-display: "Geist", "Geist Variable", ui-sans-serif, system-ui, sans-serif;
  --ds-font-ui:      "Instrument Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --ds-font-mono:    "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* Scale — note the deliberate GAP between --ds-t-lead and --ds-t-label.
     Jumping from display type straight to mono micro with little in between is
     what makes the page read editorial rather than SaaS. Keep the gap. */
  /* Two hero sizes, because a hero headline's size depends on how much WIDTH
     it has, not just the viewport. Using the full-width value in a two-column
     hero wraps "TURN MARKET NOISE" onto three lines instead of one and the
     whole composition collapses. Pick by layout: */
  /* Min is 2.1rem, not 3.2. At 375px there is ~295px of measure, and 3.2rem
     caps fit roughly 7 characters per line — so "TURN MARKET NOISE" broke into
     three rendered lines and the deliberate three-line structure collapsed
     into seven. 2.1rem holds two of the three written lines intact on the
     narrowest phones. Only viewports under ~420px ever reach this floor. */
  --ds-t-hero:     clamp(2.1rem, 8vw,   7rem);     /* full-width hero, ONE per page, caps */
  --ds-t-hero-split: clamp(2.3rem, 4.4vw, 4.1rem); /* hero sharing a row with a plate */
  --ds-t-display:  clamp(2.4rem, 5.2vw, 4.2rem);   /* section openers */
  --ds-t-h2:       clamp(1.9rem, 3.4vw, 2.8rem);
  --ds-t-h3:       1.25rem;
  --ds-t-lead:     clamp(1.05rem, 1.5vw, 1.2rem);
  --ds-t-body:     1rem;
  --ds-t-small:    .875rem;
  --ds-t-label:    .6875rem;                       /* mono, uppercase */
  --ds-t-micro:    .625rem;                        /* mono, uppercase */

  /* Tracking — the only tracked type in the system is mono labels.
     Do not stack a tracked eyebrow on every sub-section. Eyebrow-creep is the
     failure mode; it is a section-head device, not universal scaffolding. */
  --ds-tr-hero:    -.045em;
  --ds-tr-display: -.038em;
  --ds-tr-h2:      -.028em;
  --ds-tr-h3:      -.015em;
  --ds-tr-lead:    -.005em;
  --ds-tr-label:    .16em;
  --ds-tr-micro:    .12em;

  --ds-lh-hero:     .92;    /* caps set tight — no descender collisions */
  --ds-lh-head:    1.04;
  --ds-lh-lead:    1.50;
  --ds-lh-body:    1.60;

  --ds-measure:     68ch;   /* body prose cap */
  --ds-measure-sm:  30ch;   /* hero sub-copy cap */
}

/* ==========================================================================
   4. SPACE, LAYOUT, SHAPE
   ========================================================================== */

:root {
  --ds-s-4: 4px;   --ds-s-8: 8px;    --ds-s-12: 12px;  --ds-s-16: 16px;
  --ds-s-24: 24px; --ds-s-32: 32px;  --ds-s-48: 48px;  --ds-s-64: 64px;
  --ds-s-96: 96px; --ds-s-128: 128px; --ds-s-160: 160px;

  --ds-maxw:    1360px;
  --ds-gutter:  clamp(20px, 4vw, 40px);
  --ds-cols:    12;

  /* ⚠ ZERO column gap is load-bearing. Cards with gaps between them read as
     cards; cells sharing a 1px rule read as one continuous instrument face.
     This single value is most of the difference between the reference layout
     and a normal SaaS page. Do not introduce a gap. */
  --ds-col-gap: 0px;

  /* One token controls vertical breathing room site-wide. */
  --ds-section: clamp(88px, 10vh, 168px);
  --ds-nav-h:   64px;

  /* Corners are square or nearly so. 3px is the ceiling ANYWHERE.
     Pill radii belong to the consumer-fintech register we are defined against. */
  --ds-r-0: 0;
  --ds-r-1: 2px;    /* buttons, inputs */
  --ds-r-2: 3px;    /* maximum, anywhere */

  --ds-hair:        1px;    /* every rule in the system */
  --ds-tick:        10px;   /* corner registration mark leg */
  --ds-tick-active: 16px;   /* leg on hover / active */

  --ds-z-grain: 5;  --ds-z-nav: 100;  --ds-z-modal: 200;
}

/* ==========================================================================
   5. MOTION
   Five named motions. Nothing outside this vocabulary ships.

   pulse   — 3px square travels a rule line              (rules, plate strata)
   sweep   — 1px blue line crosses a plate, revealing it (instrument plates)
   tick    — numerals resolve in 4 discrete steps        (every stat on enter)
   settle  — content rises 8px + fades                   (scroll reveal)
   latch   — state word swaps with NO crossfade          (regime/state chips)

   tick and latch use steps() deliberately. Smooth interpolation reads as an
   animation; discrete steps read as an instrument LATCHING onto a reading.
   Highest-value, lowest-cost distinctive detail in the whole system.
   ========================================================================== */

:root {
  --ds-ease-out: cubic-bezier(.2, .7, .2, 1);
  --ds-ease-exp: cubic-bezier(.16, 1, .3, 1);
  --ds-ease-latch: steps(2, end);
  --ds-ease-tick:  steps(4, end);

  --ds-dur-1: 120ms;
  --ds-dur-2: 200ms;
  --ds-dur-3: 320ms;
  --ds-dur-4: 640ms;
  --ds-dur-pulse: 2600ms;

  --ds-stagger: 40ms;   /* per-sibling delay for settle */
}

@keyframes ds-pulse   { 0% { left: 0; opacity: 0 } 10%,90% { opacity: 1 } 100% { left: 100%; opacity: 0 } }
@keyframes ds-pulse-y { 0% { top: 0;  opacity: 0 } 10%,90% { opacity: 1 } 100% { top: 100%;  opacity: 0 } }

/* The sweep line fades itself out at the end. Without the opacity ramp, a
   `both` fill leaves the line parked at its final position permanently — which
   is exactly what shipped and produced a stationary blue bar across the plate.
   The element this runs on MUST be full width; see the note in site.css §9. */
@keyframes ds-sweep {
  0%        { transform: translateX(-100%); opacity: 0 }
  12%, 88%  { opacity: .9 }
  100%      { transform: translateX(0);     opacity: 0 }
}
@keyframes ds-sweep-reveal { from { clip-path: inset(0 100% 0 0) } to { clip-path: inset(0 0 0 0) } }

@keyframes ds-tick   { from { opacity: .35; transform: translateY(2px) } to { opacity: 1; transform: none } }
@keyframes ds-settle { from { opacity: 0;   transform: translateY(8px) } to { opacity: 1; transform: none } }

@keyframes ds-latch-out { from { transform: translateY(0);    opacity: 1 } to { transform: translateY(-100%); opacity: 0 } }
@keyframes ds-latch-in  { from { transform: translateY(100%); opacity: 0 } to { transform: translateY(0);     opacity: 1 } }

/* ---- orbit — the SIXTH named motion -------------------------------------
   Everything above is stepped or one-shot, because instruments latch. `orbit`
   is deliberately the exception: continuous, linear, and measured in minutes
   rather than milliseconds. An orbit that ticked would be wrong — the whole
   point of the construction is that it never arrives.

   Only the Market Intelligence Field may use it. If a second consumer appears,
   that is the signal to check whether the vocabulary is drifting. */
@keyframes ds-orbit { to { transform: rotate(360deg) } }
@keyframes ds-core  { 0%, 100% { transform: scale(1) } 50% { transform: scale(1.08) } }
@keyframes ds-scan  { 0% { transform: translateX(-52%); opacity: 0 }
                      12%, 88% { opacity: .55 }
                      100% { transform: translateX(52%); opacity: 0 } }
/* Floor is .5, not .28. The labels are already --ds-ink-3 (3.3:1), so fading
   to .28 of that makes them effectively invisible rather than ambient. They
   should breathe, not disappear. */
@keyframes ds-flicker { 0%, 100% { opacity: .5 } 45%, 55% { opacity: 1 } }

/* ==========================================================================
   6. ACCESSIBILITY GATES
   Reduced motion is mandatory, not a nicety. One switch kills the entire
   vocabulary — no per-effect opt-out to forget.
   ========================================================================== */

/* `html:not(.ds-force)` is the review escape hatch (?motion=force), set by an
   inline script before this file is evaluated. It exists ONLY so the motion
   design can be reviewed on a machine with reduce-motion enabled at OS level.
   It is opt-in per URL and never a default — a visitor who asked for reduced
   motion always gets it. */
@media (prefers-reduced-motion: reduce) {
  html:not(.ds-force), html:not(.ds-force) *,
  html:not(.ds-force) *::before, html:not(.ds-force) *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Grain is a transparency effect — gate it independently. */
@media (prefers-reduced-transparency: reduce) {
  :root, [data-theme="dark"] { --ds-grain-opacity: 0 }
}

@media (prefers-contrast: more) {
  :root       { --ds-rule: #B8B8B0; --ds-rule-2: #8E8E86; --ds-ink-2: #3A3A3E }
  [data-theme="dark"] { --ds-rule: #43434A; --ds-rule-2: #5C5C64; --ds-ink-2: #C2C2BE }
}
/* The :root rule above ties with the auto-dark block and comes later, so without this a dark-OS visitor on the
   default auto theme gets light secondary text on the dark background. */
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
  [data-theme="auto"] { --ds-rule: #43434A; --ds-rule-2: #5C5C64; --ds-ink-2: #C2C2BE }
}

/* ==========================================================================
   7. BASE
   System-level element defaults only. Anything with a class name belongs in
   site.css, not here.
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--ds-nav-h) + var(--ds-s-24));

}

body {
  margin: 0;

  /* Backstop only. Full-bleed rules are structural in site.css (.ds-ruled on a
     full-width element), NOT 100vw pseudo-elements, so nothing should overflow
     sideways in the first place. This catches anything that later does.

     `clip` rather than `hidden`: hidden makes body a scroll container and
     silently breaks position:sticky on the nav. Note that neither this nor
     html{overflow-x:clip} reliably clips a 100vw child — both propagate to the
     viewport under different conditions. That is exactly why the bleed is
     structural now; see the long note in site.css §1. */
  overflow-x: clip;

  background: var(--ds-bg);
  color: var(--ds-ink);
  font-family: var(--ds-font-ui);
  font-size: var(--ds-t-body);
  line-height: var(--ds-lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: var(--ds-font-display);
  font-weight: 500;
  line-height: var(--ds-lh-head);
  margin: 0;
}

/* Every numeral in the system is mono and tabular. Non-negotiable — it is what
   makes the page read as a readout rather than as marketing. */
code, kbd, samp, pre, time, [data-num] {
  font-family: var(--ds-font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "zero" 1;
}

a { color: var(--ds-blue); text-underline-offset: .18em; text-decoration-thickness: 1px }
a:hover { color: var(--ds-blue-2) }

hr { border: 0; border-top: var(--ds-hair) solid var(--ds-rule); margin: 0 }

img, svg, canvas { max-width: 100%; height: auto; display: block }

::selection { background: var(--ds-blue); color: var(--ds-bg) }

/* Branded, always visible. Never outline:none. */
:focus-visible {
  outline: 2px solid var(--ds-blue);
  outline-offset: 2px;
  border-radius: var(--ds-r-1);
}
:focus:not(:focus-visible) { outline: none }
