/* ============================================================================
   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
   NOT used by the marketing site. The site is light, deliberately, and ships
   no theme toggle. This block exists so Deskscale Platform can consume the
   same token file.  Opt in with <html data-theme="dark">.
   ========================================================================== */

[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 */
}

/* Platform-side convenience: honour the OS preference only when no explicit
   theme is set. The marketing site never sets data-theme, so it stays light. */
@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 }
}

/* ==========================================================================
   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 }
