/*
 * tokens.css
 *
 * Lifted from PROPSCALR-DESIGN-DOCTRINE-v1.md Section 3. This file is the
 * doctrine's palette expressed as CSS, and nothing here is a design decision
 * made in code: every value traces to a table in Section 3 or Section 2.
 *
 * Rule 3.8.4 — no color is introduced outside Section 3. A surface needing a
 * color that is not here has found a doctrine gap: amend the doctrine, do not
 * add a local hex.
 */

/*
 * ── The wordmark's face, SELF-HOSTED and SUBSET (31 Aug 2026) ─────────────
 *
 * Poppins SemiBold, cut to the eight characters the logo uses: space and
 * A C L O P R S. 816 bytes. It is the LOGO's face and nothing else may use it,
 * which is exactly why subsetting it to the lockup's own alphabet is safe -
 * the file physically cannot render a heading, so it cannot become a second UI
 * family by accident (§4.1, one family).
 *
 * ⚠️ LOCAL, NOT GOOGLE FONTS. The session brief asserted the app already
 * self-hosts Inter; it does not - index.html still fetches Inter and Source
 * Serif 4 from fonts.googleapis.com. That is a pre-existing condition this
 * pass did not create and did not fix. What it does hold to is the acceptance
 * bar: NO NEW network font request. A third remote family for eight glyphs
 * would have been the regression the brief was guarding against, so this one
 * ships from our own origin.
 *
 * `swap` on purpose: the lockup is the first thing painted on every screen and
 * an invisible header while 816 bytes arrive is worse than one reflow. Poppins
 * is the fallback for anyone who has it installed; --font-ui catches the rest.
 *
 * Licence: SIL Open Font License 1.1, `public/fonts/Poppins-OFL.txt`.
 */
@font-face {
  font-family: 'Poppins Wordmark';
  src: url('/fonts/poppins-semibold-wordmark.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  /* Belt and braces on the subset: even if the file grew, the browser would
     only reach for it to draw these characters. */
  unicode-range: U+0020, U+0041, U+0043, U+004C, U+004F, U+0050, U+0052, U+0053;
}

:root {
  /* ── Fonts (§4.1, §9.1). §3.9 row 3 is CLOSED, 16 Aug 2026. ───────────── */
  /* THE ONLY font-family VALUES IN THE PRODUCT. Components and every other
     stylesheet reference these tokens and never name a face, so a future
     change is still one edit here.

     DECIDED, not pending. The candidate harness that carried Geist, General
     Sans, Instrument Sans, Source Serif 4 and Lora behind ?font= and ?serif=
     is deleted; these two values are the outcome. General Sans was measured
     and disqualified under §4.2.2 for ignoring `font-variant-numeric:
     tabular-nums`; Geist and Instrument Sans were tabular and lighter than
     Inter but too close to it to be worth the churn. */
  --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* The greeting header only, and nothing else may reference this token.
     §9.1 ruled a second face out; the operator reversed it for this one
     element by approving design-spec/TARGET.png (the 15 Aug amendment).
     Now a REAL face rather than the export's generic `serif` keyword, which
     resolved to a different letterform on every device. Loaded in
     index.html at 400 and 500. */
  --font-greeting: "Source Serif 4", Georgia, "Times New Roman", serif;

  /* ── Neutrals (§3.1) ──────────────────────────────────────────────────── */
  --n-950: #0F1419;   /* Hero money figures, primary headings */
  --n-800: #232B33;   /* Body text */
  --n-700: #39434D;   /* Secondary text, table data */
  --n-600: #55606B;   /* Supporting text */
  --n-500: #6F7A85;   /* Muted labels, column headers, explainers */
  --n-400: #97A1AA;   /* Placeholders, disabled text, icon rest state */
  --n-300: #C2C9CF;   /* Strong hairline, input border */
  /* #E2E8F0 exactly, per DESIGN.md Colors ("a consistent 1px stroke in a light
     gray (#E2E8F0)") and the brief's "card borders (1px #E2E8F0-register)".
     Was #DDE2E6: same register, half a step warmer. */
  --n-200: #E2E8F0;   /* Default hairline, card border, dividers */
  --n-100: #EEF1F3;   /* Subtle fill, table zebra, skeleton base */
  --n-075: #F1F4F6;   /* Was the page ground; still the subtle-fill step */
  --n-050: #F6F8F9;   /* Subtle fill one step lighter than the ground */
  --surface: #FFFFFF; /* Cards, panels, inputs */

  /* ── Surface roles (§3.1) ─────────────────────────────────────────────── */
  /* Named by ROLE, so a surface never picks a ramp step by eye. The field
     verdict was that page and card read as the same grey and nothing looked
     like an object; the fix is a real step between the two, not a shadow. */
  /* #F5F5F7, the export's `background` token (code.html tailwind config). Was
     --n-075 (#F1F4F6): the same cool near-white, a shade lighter and less
     green. Cards stay pure white so the step between page and object holds. */
  --surface-page: #F5F5F7;
  --surface-card: var(--surface);

  /* ── Semantic (§3.1) ──────────────────────────────────────────────────── */
  /* The two marks below are the export's functional accents, adopted as tokens
     rather than as literal hexes in a component (Rule 3.8.4). DESIGN.md Colors:
     "Success Green: positive yield"; "Critical Red: reserved specifically for
     losing money". They are MARKS: 4px accent bars and the one red figure, not
     text or fills, so they are never asked to carry body contrast. */
  --positive: #1F7A5A;
  --positive-mark: #10B981;  /* export unit-row accent bar, positive */
  --positive-weak: #E8F3EE;
  --warning: #9A6B1F;
  --warning-mark: #B8862E;
  --warning-weak: #FBF3E3;
  /* #BA1A1A, the export's `error` token. Was #A03D33. 5.9:1 on white, so it
     still carries the "Losing money" figure as text (Rule 3.2.2). */
  --critical: #BA1A1A;
  --critical-weak: #FFDAD6;  /* the export's error-container */

  /* ── Accent (§3.2) ────────────────────────────────────────────────────── */
  /* The single brand input. Replace this line when brand/ lands.
     Working value 16 Aug 2026: #4F46E5, read straight off the approved Stitch
     export, where it is the colour of "Show the math" and "View all 9 units"
     (code.html: text-[#4f46e5], hover #4338ca). 6.56:1 on white, so it passes
     Rule 3.2.2 as authored and needs no darkening. Was #7267D6, the Origin
     sample. Still BRAND-PENDING.
     NOTE: the accent is links only now. The active nav item is a soft grey
     pill per the brief, not an indigo one. */
  --accent: #4F46E5;

  /* Derived. Never hand-authored, never a literal hex. */
  --accent-hover:   color-mix(in oklab, var(--accent) 85%, #000 15%);
  --accent-active:  color-mix(in oklab, var(--accent) 75%, #000 25%);
  --accent-weak:    color-mix(in oklab, var(--accent) 12%, #FFF 88%);
  --accent-border:  color-mix(in oklab, var(--accent) 35%, #FFF 65%);
  /* The rail's family trait (§2.7). 28% resolves to #D4D5F6 at the working
     accent, hue 238 against the Origin recap border's 237. Lighter than
     --accent-border, which stays reserved for a single marked element such as
     the recommended pricing tier (3.3.2). */
  --accent-border-soft: color-mix(in oklab, var(--accent) 28%, #FFF 72%);
  --accent-ring:    color-mix(in srgb,  var(--accent) 25%, transparent);
  /*
   * ── The brand pass, 31 Aug 2026 ─────────────────────────────────────────
   * TWO tokens added, and only two. The brief also restated --accent-hover
   * (#4338CA) and --accent-border (#C7D2FE); the operator ruled those stay as
   * they are. Both already existed, both were chosen deliberately, and fifteen
   * call sites depend on them, so restating them would have been churn dressed
   * as a brand decision.
   *
   * DERIVED, not hardcoded (Rule 3.2.1), which is what keeps the brand swap a
   * one-line edit. The brief wrote --accent-soft as the literal #EEF2FF;
   * color-mix at 8% resolves to #EFF1FF, an rgb distance of 1.4 out of 255,
   * which is not a visible difference and is worth strictly more than the
   * exact hex, because this one moves when --accent does.
   */
  /* Selected rows, the sync pill, quiet accent fills. §3.3.3 as amended:
     it marks INTERACTION, never data. Accent on it measures 5.60:1. */
  --accent-soft:    color-mix(in oklab, var(--accent) 8%, #FFF 92%);
  /* The empty-state mark. Transparent, so it takes the tint of whatever ground
     it sits on rather than assuming white, which --accent-soft would. */
  --accent-tint:    color-mix(in srgb,  var(--accent) 10%, transparent);
  /*
   * The wordmark's mark on an ink ground. Brand delivered #8B84F5; this is the
   * derivation, srgb 66%, which resolves to #8B85EE - an rgb distance of 7.1,
   * the widest of the three derivations in this file and still not a visible
   * step on a 24px mark. Derived on purpose: a reversed logo pinned to a
   * literal would be the one piece of the lockup that did NOT move on a brand
   * swap, which is precisely the failure Rule 3.2.1 exists to prevent.
   * Measures 5.85:1 on --n-950.
   */
  --accent-reversed: color-mix(in srgb, var(--accent) 66%, #FFF 34%);
  /* Highlight card (§2.7). A wash, not a fill: lighter than --accent-weak so
     ink keeps its contrast on top of it. */
  --accent-wash:    color-mix(in oklab, var(--accent) 7%, #FFF 93%);
  /* Tier 2 as a DARK object (§2.7). The wash had no presence against white
     cards; the Origin tax-filing card is a dark rich surface. */
  /* ⚠️ NOT DERIVED, and deliberately so. This is the approved export's exact
     surface, copied stop for stop from code.html and ruled into the brief as
     "the export's EXACT gradient". It is a slate-blue ramp, not the accent
     hue, so a brand swap does NOT move it: it is its own approved value and
     changing it needs a new design round, not a token edit. */
  --highlight-surface: linear-gradient(
    to right bottom,
    rgb(28, 57, 104), rgb(31, 63, 110), rgb(64, 93, 144), rgb(96, 121, 175),
    rgb(102, 122, 174), rgb(102, 121, 170), rgb(90, 104, 154), rgb(89, 99, 148),
    rgb(72, 97, 145)
  );
  /* The 1px gradient edge on the three bottom cards (export: p-[1px] with
     bg-gradient-to-r from-[#5d7ab2] to-[#5e77ae]). Same slate-blue family as
     the highlight surface, which is what makes the bottom row read as one
     group (§2.7 tier 1, the family trait). */
  --railcard-edge: linear-gradient(to right, #5D7AB2, #5E77AE);
  --on-accent-deep:      #FFFFFF;
  --on-accent-deep-mute: color-mix(in srgb, #FFFFFF 78%, transparent);
  --on-accent-deep-line: color-mix(in srgb, #FFFFFF 18%, transparent);
  --accent-on:      #FFFFFF; /* text on accent; constrained by 3.9 */

  /* ── Auth panel (auth design round, 28 Aug 2026) ──────────────────────── */
  /* ⚠️ NOT DERIVED, on the same footing as --highlight-surface. This is the
     design round's approved flat value, read off DESIGN.md ("Left panel flat
     #2B2F5C, no gradient, no ornament"), and it is deliberately NOT the
     highlight gradient: that surface is the Overview's one dark object and
     reusing it here would make the sign-in screen read as a dashboard card.
     A brand swap does not move this; changing it needs a design round.
     It lives here rather than in a component because the brief's colour law
     is that every colour maps to tokens.css, including the ones the exports
     brought with them. */
  --auth-panel: #2B2F5C;
  /* The bullet icons' chip. The export draws white at 10%; --on-accent-deep-line
     is 18% and belongs to the Overview's hairlines, so this is its own value
     rather than a reuse that would quietly lighten the panel. */
  --auth-panel-chip: color-mix(in srgb, #FFFFFF 10%, transparent);

  /* ── Spacing scale (§2.2). Permitted steps only. ──────────────────────── */
  /* ── The phone's bottom bar (§7.3) ────────────────────────────────────── */
  /* One tab's height. Read by the bar AND by the content clearance below it,
     so the two can never disagree about how much room the bar takes. */
  --btab-h: 52px;
  /*
   * ⚠️ THE FLOOR IS THE WHOLE PADDING TODAY, not a fallback. `env()` returns 0
   * unless the viewport meta carries `viewport-fit=cover`, which ours does
   * not - so on the operator's notched phone the inset was 0 and the bar sat
   * flush against the home-indicator gesture area.
   *
   * 12 rather than the 8 the brief suggested, for exactly that reason: with
   * `env()` inert this number is the entire clearance on EVERY device
   * including the notched one, so it has to be worth something on its own. It
   * is on the spacing scale, and `max()` means the real 34px inset takes over
   * untouched if `viewport-fit=cover` is ever adopted.
   */
  --btabs-pad-b: max(env(safe-area-inset-bottom), 12px);

  --s-4: 4px;   --s-8: 8px;   --s-12: 12px; --s-16: 16px;
  --s-20: 20px; --s-24: 24px; --s-32: 32px; --s-40: 40px;
  --s-48: 48px; --s-64: 64px; --s-96: 96px;

  /* ── Type scale (§4.3, extended) ──────────────────────────────────────── */
  /* The build previously had roughly two sizes, and that flatness is what
     reads as prototype. Five named steps, used everywhere. Values are the
     1440 targets; the display step steps down on mobile. */
  /* Scale REPLACED 16 Aug 2026 by DESIGN.md's `typography` block, which the
     brief rules into tokens.css verbatim. The names below are ours; the
     values are the export's, one for one:
       --t-display  = headline-xl  32/40/600/-0.02em
       --t-heading  = headline-lg  24/32/600/-0.01em
       --t-section  = headline-md  18/24/600/-0.01em
       --t-lead     = body-lg      16/24/400/-0.01em
       --t-body     = body-md      14/20/400/-0.01em
       --t-foot     = body-sm      13/18/400/0
       --t-cardhead = label-md     12/16/600/0.02em
       --t-label    = label-sm     11/14/500/0.03em
     `mono-data` (JetBrains Mono) is the one entry NOT adopted: the operator
     rejected mono figures for their slashed zero. Inter tabular everywhere.
     --t-title is the greeting and comes from code.html's inline 26px, not from
     the DESIGN.md block, which has no serif step. */
  --t-display:  32px; --t-display-lh:  40px;  /* hero month figure, was 34/40 */
  --t-title:    26px; --t-title-lh:    34px;  /* greeting, serif; was 28/34 */
  --t-heading:  24px; --t-heading-lh:  32px;  /* hero YTD, was 21/26 */
  --t-section:  18px; --t-section-lh:  24px;  /* new step, headline-md */
  --t-figure:   20px; --t-figure-lh:   26px;  /* bottom-row figures, was 26/32 */
  --t-lead:     16px; --t-lead-lh:     24px;  /* new step, body-lg */
  --t-row:      15px; --t-row-lh:      20px;  /* unit row name, per the brief */
  --t-body:     14px; --t-body-lh:     20px;  /* was 14/21 */
  --t-foot:     13px; --t-foot-lh:     18px;  /* was 13/19 */
  --t-cardhead: 12px; --t-cardhead-lh: 16px;  /* new step, label-md */
  --t-label:    11px; --t-label-lh:    14px;  /* caps, tracked */

  /* Tracking, out of the same block. Was a single hand-set 0.08em on every
     caps label; DESIGN.md splits it by step. */
  --track-label:    0.03em;
  --track-cardhead: 0.02em;
  /* The obligations card's head only, per code.html tracking-[0.15em]. */
  --track-highlight: 0.15em;

  /* ── Bar geometry (item 4 and 5) ──────────────────────────────────────── */
  /* Two bars, deliberately different so a stranger can tell them apart:
     the ranking bar is edge-anchored by sign and short; the coverage bar is
     left-anchored toward an obligation and full width. */
  /* Ranking bar restored 16 Aug 2026 to the doctrine's zero-centred diverging
     form (§6.8.1). The export's 64px edge-anchored track and its coloured dot
     are gone: they put the whole signal in colour and left the shape saying
     nothing, which is what Rule 6.8.1a exists to prevent. 200px is the widest
     track the glance card holds while the longest real unit name still fits on
     one line, and it gives each direction 100px to be read against. */
  --bar-rank-h: 8px;      /* §6.8.1, was 6            */
  --bar-rank-r: 2px;      /* §6.8.1, was fully round  */
  --bar-rank-w: 200px;    /* was 64                   */
  --bar-cover-h: 5px;     /* fill toward obligation   */
  --bar-cover-r: 2px;
  --col-amount-w: 96px;   /* the shared right rail. Holds "-AED 11,359" at 11px */
  --row-h: 44px;          /* export h-[44px], was 54 */

  /* ── Shell (§7) ───────────────────────────────────────────────────────── */
  --sidebar-w: 240px;     /* per the brief; the export drew 220 */
  --apphead-h: 56px;      /* export h-14 */
  --card-pad-x: 20px;     /* export px-5 */
  --card-pad-y: 12px;     /* export py-3 */
  --card-pad: 20px;       /* kept for the panels the Overview does not own */
  --rail-gap: 16px;       /* between rail cards, was --s-24  */
  --section-gap: 24px;    /* between major sections, was --s-32 */
  /* One-viewport geometry at 1440x900 (the brief's ONE-VIEWPORT LAW). */
  --canvas-pad-x: 32px;   /* export px-8 */
  --canvas-pad-y: 12px;   /* export pt-3 pb-3 */
  --bento-gap: 12px;      /* export gap-3 between the top section and the row */
  /* The export drew 200. Compressed to the row's own measured content height
     plus one 4px step of slack: the tallest of the three is Forecast cash with
     its two-line arrears disclosure at 156px. Compressing DENSITY to hold the
     one-viewport law is what DESIGN.md Layout permits; cutting the disclosure
     to fit is what it forbids. */
  --bottomrow-h: 160px;

  /* ── Radius (§2.1) ────────────────────────────────────────────────────── */
  /* 8px, ruled by the operator 16 Aug 2026, and the settlement of a genuine
     contradiction inside the approved export: code.html draws every card at
     rounded-[16px] while its own DESIGN.md Shapes section says 4px. The
     implementation session shipped 4px on the brief's authority and flagged it
     as the highest-risk call in the handover; the verdict on the deployed page
     is 8px, between the two. Was 4px this morning, 12px before that. */
  --r-card: 8px;
  --r-control: 4px;
  /* The one exception DESIGN.md names: a container nested INSIDE a card, which
     is only the obligation rows (export rounded-xl). */
  --r-inner: 8px;
  --r-pill: 999px;

  /* ── Hairline (§2.1, §2.5) ────────────────────────────────────────────── */
  --hairline: 1px solid var(--n-200);

  /* Permitted only on layers that genuinely float (§2.5.1). Neutral, never
     colored (§3.8.3). */
  --shadow-floating: 0 8px 24px color-mix(in srgb, var(--n-950) 10%, transparent);

  /* The card whisper (§2.5.1 as amended). Derived from the neutral ramp so it
     introduces no new colour, and small enough that it seats an edge rather
     than lifting the card. Elevation is still carried by the hairline. */
  --shadow-card: 0 1px 2px color-mix(in srgb, var(--n-950) 5%, transparent);

  /* Scrim behind a floating layer (§3.1). Derived from the neutral ramp so it
     introduces no new colour. */
  --scrim: color-mix(in srgb, var(--n-950) 35%, transparent);

  /* ── Motion (27 Aug brief §1) ─────────────────────────────────────────────
   * Mercury/Origin restraint: nothing bounces, nothing slides further than it
   * needs to, nothing takes longer than 220ms. EVERY transition in the app
   * reads these three durations and two curves; an inline duration anywhere
   * else is a defect.
   *   fast   hover, press, focus, close - feedback the hand is waiting on
   *   base   drawers arriving, content fading in
   *   slow   a row leaving a list, a figure ticking to its new value
   * The AMBIENT durations below are loops and holds, not transitions - the
   * skeleton shimmer, the sync dot's breath, the saved-tick's linger. They
   * are tokenised so no duration lives inline, and reduced-motion stills
   * them all the same. */
  --dur-fast: 120ms;
  --dur-base: 160ms;
  --dur-slow: 220ms;
  --ease-out: cubic-bezier(.2, .8, .2, 1);
  --ease-in-out: cubic-bezier(.4, 0, .2, 1);
  --dur-shimmer: 1400ms;
  --dur-hold: 2000ms;
}

/* Reduced motion is a fact about the operator, not a preference of ours:
   every duration collapses to zero, so every transition and animation lands
   on its final frame instantly. The number tick reads the same signal in JS
   and skips itself. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
  }
  /* The ambient loops are stilled by name rather than by a 0ms duration: an
     infinite animation at 0ms is an edge browsers disagree on, and `none` is
     the honest word for what reduced motion asks. app.css carries the
     per-animation `animation: none` rules beside each keyframe. */
}
