/* ══════════════════════════════════════════════════════════════════════
   Nivasik DS — the design system layer.          Loaded AFTER tokens.css.

   tokens.css answers "what colour, what size, what curve". It is good and
   it is not changed here: every contrast ratio in it was computed, not
   estimated, and that work is the best thing in this codebase.

   This file answers the question tokens.css does not: WHAT IS THIS MADE OF.
   Six design audits reached the same verdict independently — "one material,
   one elevation, 271 times", "42 independent impressions of one design",
   "everything drawn at the same weight". A palette cannot fix that. A
   palette says what colour a card is; it never says why a card and a menu
   and an input well should not be the same object.

   So this layer declares four things tokens.css has no opinion about:

     § 1  MATERIALS   six surfaces, each with a job it alone does
     § 2  THE SPINE   the signature: a rule with nodes on it
     § 3  THE FIELD   the texture (.tex): a ruled ledger, what we replace
     § 4  THE SCALES  a marketing type scale, separate from the dense
                      15px operations scale, plus tabular numerals

   …and then § 5–7 give the primitives, figures and motion recipes that
   are built out of them.

   RULES
   · No build step. Plain CSS, custom properties, same-origin.
   · Every colour is composed from a tokens.css value or is measured in a
     comment beside it. No unmeasured hex reaches a text role.
   · NEW NAMES ONLY, with exactly one deliberate exception. Every class
     here was diffed against tokens/components/layout/motion.css and against
     the marketing page's own stylesheet before it was named. Three had to be
     renamed after that diff and the reason is worth keeping: `.field` is the
     homepage's FORM-FIELD class, `.stat` is a dl/dt/dd pattern components.css
     already owns, and `.s-*` is the homepage's SECTION namespace (.s-hero,
     .s-pricing). They are `.tex*`, `.metric*` and `.tx-*` here. Adding this
     file to a page must not change how that page currently renders.

     THE ONE EXCEPTION is § 4b, which adds `font-variant-numeric: tabular-nums`
     to `.num`, `.table td.num` and `.kpi .value`. That is an extension, not a
     fork — it adds one property to classes it does not otherwise touch, and
     column alignment in a PMS is not optional.
   ══════════════════════════════════════════════════════════════════════ */


/* ══ § 1 · MATERIALS ════════════════════════════════════════════════════
   Six materials. A material is a surface PLUS its edge PLUS its light —
   the three always travel together, which is precisely what was being
   half-applied before.

   The job column is the whole point. If a new element does not match one
   of these six jobs, the answer is not a seventh material.

     page     the ground. Nothing sits behind it.
     panel    a thing you read. Cards, list rows, sections.
     raised   a thing that came from somewhere. Menus, sheets, popovers,
              anything that will be dismissed.
     sunken   a well. Table headers, code, a recessed band.
              NOT inputs — see the note under --edge-sunken below.
     glass    chrome the content scrolls under. Sidebar, topbar, banner.
     brand    the one saturated fill on the page. Primary button, mark.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  /* An alternating band for long marketing pages. Measured against every
     ink role that lands on it: text 15.17 · text-soft 9.67 · muted 5.89 ·
     accent 8.45 · primary 7.01. It is 1.07:1 against --bg, which sounds
     invisible and is not: a band is read by its EDGE, and the hairline
     below does that work. A band that actually contrasted would read as a
     card the width of the window. */
  --band:        #12151B;
  --band-line:   #1C222B;   /* the edge that makes the band legible */

  --mat-page:    var(--bg);
  --mat-panel:   var(--surface);
  --mat-raised:  var(--surface-2);
  --mat-sunken:  var(--sunken);
  --mat-glass:   var(--chrome-glass);
  --mat-brand:   var(--accent-fill);

  /* Edges, by job. A panel's edge is decorative; a well's edge is the only
     thing telling you it is inset; a raised thing has no edge at all in
     dark — its shadow is the edge.

     A CONTROL'S OUTLINE IS NEVER A MATERIAL EDGE. --edge-sunken is decorative
     and measures 1.02:1 on the well it surrounds in dark and 1.13:1 in light.
     WCAG 1.4.11 requires 3:1 for the boundary of a user-interface component,
     which is the entire reason tokens.css has --border-control (3.76–4.26:1
     dark, 4.14–4.72:1 light) and why it replaced #636C69. Putting .m-sunken
     on an <input> would drop its outline from ~4:1 to ~1.05:1 and silently
     undo that fix. components.css's .input/.select/.textarea already carry
     --border-control; inputs need no material applied to them at all.
     This was caught in review before it shipped — measured, not argued. */
  --edge-panel:  var(--border);
  --edge-sunken: #10141A;
  --edge-raised: transparent;

  /* Light. In dark an inset top highlight makes a surface look lit from
     above; in light the same job is done by a drop shadow. Both are in
     tokens.css as --shadow-1/2 and are reused here rather than forked. */
  --lit-panel:   var(--shadow-1);
  --lit-raised:  var(--shadow-2);
  --lit-sunken:  inset 0 1px 2px rgba(0,0,0,.28);

  --blur-glass:  saturate(160%) blur(14px);
}

:root[data-theme="light"] {
  /* 1.06:1 against the page, same reasoning as dark. Inks measured on it:
     text 16.04 · text-soft 8.39 · muted 5.12 · accent 4.75 · primary 5.06. */
  --band:        #EDEFF3;
  --band-line:   #E2E5EA;
  --edge-sunken: #DFE3E8;
  --lit-sunken:  inset 0 1px 2px rgba(17,20,24,.06);
}

/* The materials, bound as classes. Use these instead of writing
   background/border/box-shadow by hand — that is how forks start. */
.m-page   { background: var(--mat-page);   color: var(--text); }
.m-panel  { background: var(--mat-panel);  border: 1px solid var(--edge-panel);
            border-radius: var(--r-md);    box-shadow: var(--lit-panel); }
.m-raised { background: var(--mat-raised); border: 1px solid var(--edge-raised);
            border-radius: var(--r-md);    box-shadow: var(--lit-raised); }
.m-sunken { background: var(--mat-sunken); border: 1px solid var(--edge-sunken);
            border-radius: var(--r-sm);    box-shadow: var(--lit-sunken); }
.m-glass  { background: var(--mat-glass);  -webkit-backdrop-filter: var(--blur-glass);
            backdrop-filter: var(--blur-glass); }
.m-brand  { background: var(--mat-brand);  color: var(--on-accent); border: 0; }

/* A band. `.m-band + .m-band` would stack two identical grounds with no
   seam, so the edge is on the element, not between them. */
.m-band   { background: var(--band);
            border-top: 1px solid var(--band-line);
            border-bottom: 1px solid var(--band-line); }

/* Apple treats reduced transparency as its own signal, independent of
   reduced motion. Glass becomes an opaque surface; it does not become a
   different colour. */
@media (prefers-reduced-transparency: reduce) {
  .m-glass { background: var(--surface-chrome);
             -webkit-backdrop-filter: none; backdrop-filter: none; }
}


/* ══ § 2 · THE SPINE ════════════════════════════════════════════════════
   The signature, and the first thing in this product that is a drawing
   rather than a rectangle.

   It is not decoration chosen for looks. Every object Nivasik holds is the
   same shape: an event on a line, with an owner and a clock. A ticket is
   raised → owned → worked → resolved. A stay is booked → arrived → billed
   → closed. A day is opened → traded → audited → closed. The register the
   product replaces is literally a ruled line with entries on it.

   So: a 1px rule, with nodes on it. Horizontal or vertical. The node is
   the event; the rule is the time between events; a filled node has
   happened and a hollow one has not.

   This is the one graphic device the product owns. Use it instead of
   inventing a second one.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --spine-line:    var(--border);
  --spine-node:    9px;
  --spine-node-sm: 6px;
  --spine-gap:     var(--sp-5);
}

.spine {
  position: relative;
  display: grid;
  gap: var(--spine-gap);
  padding-inline-start: calc(var(--spine-node) + var(--sp-4));
}
/* The rule is drawn from the first node's centre to the last one's, never
   edge to edge: a line that overshoots its last event reads as an unfinished
   list rather than a finished one. */
.spine::before {
  content: '';
  position: absolute;
  inset-block: calc(var(--lh-body) / 2);
  inset-inline-start: calc(var(--spine-node) / 2 - .5px);
  width: 1px;
  background: var(--spine-line);
}
.spine > * { position: relative; }
.spine > *::before {
  content: '';
  position: absolute;
  inset-inline-start: calc(-1 * (var(--spine-node) + var(--sp-4)));
  top: calc(var(--lh-body) / 2 - var(--spine-node) / 2);
  width: var(--spine-node);
  height: var(--spine-node);
  border-radius: var(--r-pill);
  background: var(--mat-page);
  box-shadow: inset 0 0 0 1.5px var(--spine-line);
}
/* State. `is-done` is a MARK, not a fill — success and accent are the same
   value in this palette (tokens.css says so and says why), so a filled green
   node would read as "press me". */
.spine > .is-now::before  { box-shadow: inset 0 0 0 1.5px var(--accent),
                                        0 0 0 4px var(--accent-soft); }
.spine > .is-done::before { background: var(--accent); box-shadow: none; }
.spine > .is-late::before { background: var(--danger); box-shadow: none; }

/* Horizontal — for a 4-beat flow across a page. */
.spine-row {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--sp-4);
}
.spine-row::before {
  content: '';
  position: absolute;
  top: calc(var(--spine-node) / 2 - .5px);
  inset-inline: calc(100% / var(--spine-n, 4) / 2);
  height: 1px;
  background: var(--spine-line);
}
.spine-row > * { position: relative; padding-top: calc(var(--spine-node) + var(--sp-4)); }
.spine-row > *::before {
  content: '';
  position: absolute;
  top: 0; inset-inline-start: calc(50% - var(--spine-node) / 2);
  width: var(--spine-node);
  height: var(--spine-node);
  border-radius: var(--r-pill);
  background: var(--mat-page);
  box-shadow: inset 0 0 0 1.5px var(--spine-line);
}
.spine-row > .is-done::before { background: var(--accent); box-shadow: none; }

/* A section mark: a short spine stub above a heading. Replaces the
   uppercase eyebrow label used alone. */
.spine-mark {
  display: flex; align-items: center; gap: var(--sp-3);
  font: var(--fw-semi) var(--fs-micro)/var(--lh-micro) var(--font-body);
  letter-spacing: var(--tr-micro); text-transform: uppercase;
  color: var(--muted);
}
.spine-mark::before {
  content: ''; width: var(--spine-node-sm); height: var(--spine-node-sm);
  border-radius: var(--r-pill); background: var(--accent); flex: none;
}
.spine-mark::after {
  content: ''; height: 1px; flex: 1 1 auto; background: var(--spine-line);
}


/* ══ § 3 · THE FIELD ════════════════════════════════════════════════════
   The page texture: a ruled register.

   The alternative every SaaS page reaches for is a radial glow behind the
   hero. It is the single most recognisable tell of a generated template and
   this page has already had one removed once. A ruled field is the opposite
   choice — it is quiet, it tiles at any size, it costs one gradient, and it
   is a picture of the paper register this product is sold against.

   Two densities. `--field-line` is deliberately near-invisible: it should
   read as tooth on the surface, not as a grid you can count.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --field-line: rgba(255,255,255,.028);
  --field-step: 28px;
}
:root[data-theme="light"] { --field-line: rgba(17,20,24,.035); }

.tex {
  background-image:
    linear-gradient(to right,  var(--field-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--field-line) 1px, transparent 1px);
  background-size: var(--field-step) var(--field-step);
}
/* Ruled, not squared — closer to a register, and it does not fight a
   layout that is already a grid. */
.tex-ruled {
  background-image: linear-gradient(to bottom, var(--field-line) 1px, transparent 1px);
  background-size: 100% var(--field-step);
}
/* The field must never run to a hard edge — a grid that stops mid-page
   looks like a rendering bug. It always fades out. */
.tex-fade {
  -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
          mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
}
@media (prefers-contrast: more) {
  .tex, .tex-ruled { background-image: none; }
}

/* ⚠ `.tex-fade` IS A MASK, AND A MASK APPLIES TO THE ELEMENT *AND ALL ITS
   CHILDREN*. Putting `.tex tex-ruled tex-fade` on a section does not soften
   that section's texture — it ERASES THAT SECTION'S CONTENT wherever the
   gradient goes transparent.

   This has cost the product twice. The demo door hit it first
   (public/demo/index.html:77 — "measured: the primary action and the progress
   bar were invisible") and solved it with a dedicated layer. The rebuilt
   homepage then shipped the same mistake on its hero: the film sat 50–94% down
   a 1255px section against a mask that is fully transparent by 78%, so the
   bottom half of the product film and most of the drawn ledger were masked
   away. It read as a large black gap, and as the film being too dark.

   `.tex-bg` is that fix as a class, so nobody has to remember the rule: the
   field is painted on a pseudo-element BEHIND the content and the mask goes on
   that. Put `.tex-bg` where you would have put `.tex .tex-ruled .tex-fade`;
   nothing else about the markup changes.

   `.tex`, `.tex-ruled` and `.tex-fade` are kept: they are correct on an
   element that holds NO content — a dedicated ground layer, which is what the
   demo door uses them for. */
.tex-bg { position: relative; isolation: isolate; }
.tex-bg::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-image: linear-gradient(to bottom, var(--field-line) 1px, transparent 1px);
  background-size: 100% var(--field-step);
  -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
          mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
}
/* Squared rather than ruled, same containment. */
.tex-bg-grid::before {
  background-image:
    linear-gradient(to right,  var(--field-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--field-line) 1px, transparent 1px);
  background-size: var(--field-step) var(--field-step);
}
@media (prefers-contrast: more) { .tex-bg::before { background-image: none; } }


/* ══ § 4 · THE SCALES ═══════════════════════════════════════════════════
   4a. A MARKETING TYPE SCALE, separate from the app's.

   The app scale is argued from a nine-column table on a 360px phone: 15px
   body, 76px ceiling, tight leading. Those are the right numbers for a
   receptionist and the wrong ones for a landing page, where the job is a
   single claim read across a room. Reusing the operations scale is why the
   homepage reads as a long memo.

   Same family, same four weights, same tracking METHOD — only the steps
   differ, and they are fluid because a marketing page is read at every
   width from 360 to 2560.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --ms-display: clamp(40px, 7.4vw, 88px);   --ms-display-lh: 1.02;  --ms-display-tr: -0.028em;
  --ms-title:   clamp(28px, 3.6vw, 46px);   --ms-title-lh:   1.08;  --ms-title-tr:   -0.022em;
  --ms-head:    clamp(21px, 2.0vw, 27px);   --ms-head-lh:    1.22;  --ms-head-tr:    -0.014em;
  --ms-lede:    clamp(17px, 1.25vw, 20px);  --ms-lede-lh:    1.55;  --ms-lede-tr:    -0.008em;
  --ms-body:    16px;                       --ms-body-lh:    1.62;  --ms-body-tr:    -0.004em;
  --ms-small:   14px;                       --ms-small-lh:   1.5;   --ms-small-tr:    0;
  --ms-micro:   12px;                       --ms-micro-lh:   1.4;   --ms-micro-tr:    0.09em;
}
.tx-display { font: var(--fw-semi)   var(--ms-display)/var(--ms-display-lh) var(--font-display);
             letter-spacing: var(--ms-display-tr); text-wrap: balance; margin: 0; }
.tx-title   { font: var(--fw-semi)   var(--ms-title)/var(--ms-title-lh)     var(--font-display);
             letter-spacing: var(--ms-title-tr);   text-wrap: balance; margin: 0; }
.tx-head    { font: var(--fw-medium) var(--ms-head)/var(--ms-head-lh)       var(--font-display);
             letter-spacing: var(--ms-head-tr);    text-wrap: balance; margin: 0; }
.tx-lede    { font: var(--fw-body)   var(--ms-lede)/var(--ms-lede-lh)       var(--font-body);
             letter-spacing: var(--ms-lede-tr);    color: var(--text-soft);
             text-wrap: pretty; margin: 0; }
.tx-body    { font: var(--fw-body)   var(--ms-body)/var(--ms-body-lh)       var(--font-body);
             letter-spacing: var(--ms-body-tr);    color: var(--text-soft);
             text-wrap: pretty; margin: 0; }
.tx-small   { font: var(--fw-body)   var(--ms-small)/var(--ms-small-lh)     var(--font-body);
             letter-spacing: var(--ms-small-tr);   color: var(--muted); margin: 0; }
.tx-micro   { font: var(--fw-semi)   var(--ms-micro)/var(--ms-micro-lh)     var(--font-body);
             letter-spacing: var(--ms-micro-tr);   text-transform: uppercase;
             color: var(--muted); margin: 0; }

/* Measure. Prose wider than ~68 characters loses the line return; a lede
   set at display width is the commonest reason a page "feels" unedited. */
.measure    { max-width: 62ch; }
.measure-sm { max-width: 46ch; }

/* 4b. NUMERALS.
   This is a property management system: money, room numbers and clocks are
   read in columns and compared down the column. Proportional digits make
   ₹1,11,000 narrower than ₹4,88,888 and the column stops lining up. Satoshi
   carries tabular figures; nothing was asking for them. */
/* ⚠ `.num` IS A TABLE-COLUMN UTILITY, NOT A "TABULAR FIGURES" MARKER.
   tokens.css:633 declares `.num.num { text-align: right }` — specificity
   doubled on purpose so a cell cannot lose it — so putting `.num` on anything
   that is NOT in a column silently right-aligns it. It cost a hero metric on
   the marketing page, where a 92px figure sat flush right inside a left-set
   card and looked like a layout bug.
   For tabular figures OUTSIDE a column, use `[data-num]`. */
.num, .table td.num, .table th.num,
.kpi .value, [data-num] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
}
/* A money figure. The minus is U+2212, matching the folio and the cashier,
   which hand-roll it — a hyphen is shorter than a digit and breaks the
   column it was supposed to align. */
.fig-money { font-variant-numeric: tabular-nums;
             letter-spacing: -0.01em; white-space: nowrap; }


/* ══ § 5 · PRIMITIVES ═══════════════════════════════════════════════════
   New components only. Nothing here overrides an existing class.
   ═══════════════════════════════════════════════════════════════════════ */

/* A stat that is a SENTENCE, not a tile. The data-viz audit's finding was
   "a shape without a number" — the fix is that the number is the element
   and everything else is set around it. */
.metric        { display: grid; gap: 2px; }
.metric-n      { font: var(--fw-semi) var(--ms-title)/1.05 var(--font-display);
               letter-spacing: -0.02em; color: var(--text);
               font-variant-numeric: tabular-nums; }
.metric-k      { font: var(--fw-semi) var(--fs-micro)/var(--lh-micro) var(--font-body);
               letter-spacing: var(--tr-micro); text-transform: uppercase;
               color: var(--muted); }
.metric-sub    { font: var(--fw-body) var(--fs-caption)/var(--lh-caption) var(--font-body);
               color: var(--text-soft); }

/* A claim with its evidence attached. The product's honesty rule (CLAUDE.md
   rule 10) needs a shape: every capability statement on the marketing site
   should be able to carry the mechanism you can go and check. */
.claim       { display: grid; gap: var(--sp-2); }
.claim-check { font: var(--fw-body) var(--fs-caption)/var(--lh-caption) var(--font-body);
               color: var(--muted); padding-top: var(--sp-3);
               border-top: 1px solid var(--edge-panel); }
.claim-check b { color: var(--primary); font-weight: var(--fw-medium); }

/* The honest-limit note. Three modules in this product do less than a buyer
   assumes (card payment, OTA push, automatic sending) and saying so is the
   selling point. It gets its own object so it cannot be quietly dropped. */
.limit {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-4);
  border-radius: var(--r-sm);
  background: var(--mat-sunken);
  border: 1px solid var(--edge-sunken);
  font: var(--fw-body) var(--fs-caption)/1.55 var(--font-body);
  color: var(--text-soft);
}
.limit::before {
  content: ''; flex: none; width: 3px; align-self: stretch;
  border-radius: var(--r-pill); background: var(--warn);
}

/* A tag: naming a real mechanism inline. NOT a control.
   It is `.nv-tag` and not `.chip` for a measured reason. `.chip` already means
   two different things in this codebase — a decorative <span> on v2/pricing.html
   and an interactive <button class="btn btn-sm chip" data-filter> on
   gst-invoice.html:938 — and tools/drive.js:162 lists `.chip` among the things
   it presses. A name that is a control on one page and a label on another makes
   the harness report a dead click every run, and a harness that cries wolf stops
   being read. One name, one meaning. */
.nv-tag {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 3px var(--sp-3);
  border-radius: var(--r-pill);
  border: 1px solid var(--edge-panel);
  background: var(--mat-sunken);
  font: var(--fw-medium) var(--fs-caption)/1 var(--font-body);
  color: var(--text-soft);
  white-space: nowrap;
}
.nv-tag-accent  { border-color: color-mix(in srgb, var(--accent) 38%, transparent);
                color: var(--accent);  background: var(--accent-soft); }
.nv-tag-primary { border-color: color-mix(in srgb, var(--primary) 38%, transparent);
                color: var(--primary); background: var(--primary-soft); }

/* Rule with a label — a section divider that says what changed. */
.rule { display: flex; align-items: center; gap: var(--sp-4); }
.rule::before, .rule::after {
  content: ''; height: 1px; flex: 1; background: var(--spine-line);
}


/* ══ § 6 · THE FIGURE KIT ═══════════════════════════════════════════════
   Drawn diagrams, not screenshots.

   A screenshot of a dashboard shown at 1/3 size is unreadable — the rule
   this repo already learned the hard way is that a 13px product label in a
   324px phone slot lands at 3.3px inside a 1280px frame. A drawing is
   composed at the size it is shown, follows the theme for free, weighs
   about a kilobyte, and stays sharp on every screen.

   Every figure is an inline <svg class="fig"> using currentColor and the
   variables below, so one figure works in both themes with no second copy.

   ANIMATION CONTRACT — read this before adding a figure.
   The global reduced-motion block in tokens.css collapses animation-duration
   to .01ms. That is only safe if a figure's FINAL keyframe is its readable
   state: an animation that ends invisible would become permanently invisible.
   So every animation here ends at the state the figure is meant to hold, and
   every one is `forwards`.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --fig-line:  var(--border-control);
  --fig-faint: var(--border);
  --fig-ink:   var(--text-soft);
  --fig-a:     var(--data-1);
  --fig-b:     var(--data-2);
  --fig-c:     var(--data-3);
  --fig-d:     var(--data-4);
  --fig-e:     var(--data-5);
}

.fig { display: block; width: 100%; height: auto; overflow: visible; }
.fig text {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: var(--fw-medium);
  fill: var(--muted);
  letter-spacing: .01em;
}
.fig .fig-n    { font-variant-numeric: tabular-nums; fill: var(--text); font-weight: var(--fw-semi); }
.fig .fig-rule { stroke: var(--fig-faint); stroke-width: 1; fill: none; }
.fig .fig-path { stroke: var(--fig-line);  stroke-width: 1.5; fill: none;
                 stroke-linecap: round; stroke-linejoin: round; }
.fig .fig-fill { fill: var(--mat-panel); stroke: var(--fig-faint); stroke-width: 1; }
.fig .fig-a    { stroke: var(--fig-a); } .fig .fig-a-f { fill: var(--fig-a); }
.fig .fig-b    { stroke: var(--fig-b); } .fig .fig-b-f { fill: var(--fig-b); }
.fig .fig-c    { stroke: var(--fig-c); } .fig .fig-c-f { fill: var(--fig-c); }
.fig .fig-d    { stroke: var(--fig-d); } .fig .fig-d-f { fill: var(--fig-d); }
.fig .fig-e    { stroke: var(--fig-e); } .fig .fig-e-f { fill: var(--fig-e); }

/* ── Draw-on. A stroke reveals itself by retracting its own dash gap.
   `pathLength="1"` is set on the element so one keyframe serves every path
   regardless of its real length — without it each path needs its own
   measured dasharray, which is how a diagram desyncs. ── */
.fig [data-draw] {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.is-lit .fig [data-draw],
.fig.is-lit [data-draw] {
  animation: fig-draw var(--dur-5) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}
@keyframes fig-draw { to { stroke-dashoffset: 0; } }

/* ── Pop. A node arrives after the line that leads to it. ── */
.fig [data-pop] { opacity: 0; transform: scale(.6); transform-origin: center;
                  transform-box: fill-box; }
.is-lit .fig [data-pop],
.fig.is-lit [data-pop] {
  animation: fig-pop var(--dur-3) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 90ms + 160ms);
}
@keyframes fig-pop { to { opacity: 1; transform: scale(1); } }

/* ── Rise. A bar or a cell grows from its own baseline. ── */
.fig [data-rise] { transform: scaleY(0); transform-origin: bottom;
                   transform-box: fill-box; }
.is-lit .fig [data-rise],
.fig.is-lit [data-rise] {
  animation: fig-rise var(--dur-4) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 55ms);
}
@keyframes fig-rise { to { transform: scaleY(1); } }

/* ── Fade. Labels and areas. ── */
.fig [data-fade] { opacity: 0; }
.is-lit .fig [data-fade],
.fig.is-lit [data-fade] {
  animation: fig-fade var(--dur-3) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 70ms + 220ms);
}
@keyframes fig-fade { to { opacity: 1; } }

/* ── Sweep. A ring fills to its value. The value is an inline
   `--p` (0–1) on the circle; @property in tokens.css lets it tween. ── */
.fig [data-sweep] {
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p, 0));
  transform: rotate(-90deg);
  transform-origin: center;
  transform-box: fill-box;
}
.is-lit .fig [data-sweep],
.fig.is-lit [data-sweep] {
  animation: fig-sweep var(--dur-5) var(--ease-out) forwards;
}
@keyframes fig-sweep { from { stroke-dashoffset: 1; } }

/* A pulse for one live thing per page, and never more than one. Infinite
   animations are linear — an eased loop reads as a stutter. */
.fig [data-pulse] { animation: fig-pulse 2.4s linear infinite; transform-origin: center;
                    transform-box: fill-box; }
@keyframes fig-pulse {
  0%, 100% { opacity: .25; transform: scale(1); }
  50%      { opacity: .7;  transform: scale(1.9); }
}
@media (prefers-reduced-motion: reduce) {
  .fig [data-pulse] { animation: none; opacity: .5; }
}


/* ══ § 7 · MOTION RECIPES ═══════════════════════════════════════════════
   Named, so that "how does a thing enter here" has one answer.
   Durations and curves come from tokens.css; nothing new is invented.
   ═══════════════════════════════════════════════════════════════════════ */

/* RISE — the entrance. Content lifts 10px into place, staggered, once per
   document. The stagger is what makes a group read as a group. */
.rise > * { opacity: 0; transform: translate3d(0, 10px, 0); }
.rise.is-lit > * {
  animation: nv-rise var(--dur-3) var(--ease-out) forwards;
  animation-delay: calc(var(--i, 0) * 60ms);
}
.rise-self { opacity: 0; transform: translate3d(0, 10px, 0); }
.rise-self.is-lit {
  animation: nv-rise var(--dur-3) var(--ease-out) forwards;
}
@keyframes nv-rise { to { opacity: 1; transform: none; } }

/* PRESS — every pressable thing, one rule. */
@media (hover: hover) and (pointer: fine) {
  .press { transition: transform var(--dur-1) var(--ease-out),
                       border-color var(--dur-1) var(--ease-out),
                       box-shadow var(--dur-1) var(--ease-out); }
  .press:hover { transform: translateY(-2px); border-color: var(--muted-2); }
}
.press:active { transform: scale(.985); transition-duration: 70ms; }

/* TICK — a figure counts up on first paint only. The element must carry its
   final value as text so it is correct with JS off and after the count. */
.tick { font-variant-numeric: tabular-nums; }

/* Reduced motion: movement goes, the arrival stays legible. tokens.css
   already collapses duration and clears delay globally; this makes the
   from-state harmless for anything that never gets `is-lit` because its
   observer did not fire. */
@media (prefers-reduced-motion: reduce) {
  .rise > *, .rise-self,
  .fig [data-draw], .fig [data-pop], .fig [data-rise], .fig [data-fade] {
    opacity: 1; transform: none; stroke-dashoffset: 0;
  }
  .press:hover, .press:active { transform: none; }
}

/* The safety net for JS-off and for an observer that never fires: after
   1.2s of no script, everything is visible. `.rise` only hides its children
   while the document has told it to — an inert page shows its content. */
html.no-js .rise > *, html.no-js .rise-self { opacity: 1; transform: none; }


/* ══ § 8 · RANK ═════════════════════════════════════════════════════════
   2026-09-19. The answer to the one finding six independent audits wrote
   six different ways: "one material, one elevation, 271 times" · "42
   independent impressions of one design" · "everything drawn at the same
   weight" · "nothing is ranked".

   A screen has to say which of its panels is the one to read. Rank is a
   MODIFIER ON A MATERIAL, never a seventh material: it changes the edge
   and the ink, never the surface — so it composes with all six and adds
   no new ground to measure.

   NAMED `rank-*` DELIBERATELY. A bare `.lead` was diffed against the four
   core sheets first and, while free, sits one hyphen away from
   components.css's `.lead-n`/`.lead-money`/`.lead-in` (the Command
   Center's money card) and one letter away from the homepage's `.lede`.
   Three classes were already renamed in this system after exactly that
   diff; this is the fourth.
   ═══════════════════════════════════════════════════════════════════════ */

/* Exactly one per screen. The accent hairline is welded to the top edge
   and bleeds into the radius, which is why the element clips.

   This is the ONLY place other than the primary button where the brand
   green is a FILL. Everywhere else --accent is an ink role, and spending
   the fill on one 2px edge per screen is what keeps the green meaning
   "this one" rather than "this is our colour". */
.rank-lead { position: relative; overflow: hidden; }
.rank-lead::before { content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
                     background: var(--accent-fill); }

/* No fill, no radius, no shadow — a rule and nothing else. For the three
   to nine supporting things on a screen that would otherwise each claim
   to be a card. Applied ON a material it strips it back; that is the
   point, and it is why it declares every property the material set. */
.rank-quiet { background: none; border: 0; border-top: 1px solid var(--border);
              border-radius: 0; box-shadow: none; }


/* ══ § 9 · THE REGISTER — the spine promoted to a LAYOUT ════════════════
   § 2 gave this product its one graphic device. This makes it structural.

   A ruled register whose GUTTER carries the index, the clock or the
   state, and whose BODY carries the entry. Build a long page of records
   from this instead of a stack of cards that all weigh the same: a card
   says "I am a separate object", and nine cards in a column say it nine
   times and rank nothing. A register says "these are entries, in order,
   on one line", which is what they actually are.
   ═══════════════════════════════════════════════════════════════════════ */

.reg { display: grid; gap: 0; }
.reg-row { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr);
           gap: var(--sp-5); align-items: start;
           padding: var(--sp-5) 0; border-top: 1px solid var(--border); }
/* border-top on every row plus border-bottom on the last gives a register
   that CLOSES. A `> * + *` rule leaves the first entry hanging off an
   open edge, which reads as a list that was cut. */
.reg-row:last-child { border-bottom: 1px solid var(--border); }

/* The gutter mark. Mono and TABULAR: a column of indices that does not
   line up is not an index. Prose is Satoshi; a record is mono — the same
   rule the beat timestamps already follow. */
.reg-n { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
         font-size: var(--fs-caption); line-height: var(--lh-caption);
         letter-spacing: .04em; color: var(--muted);
         white-space: nowrap; padding-top: 2px; }
/* State colours the MARK, never the row's fill: a tinted row turns a
   register into a heat map. */
.reg-row.is-now  .reg-n { color: var(--accent); }
.reg-row.is-late .reg-n { color: var(--danger); }

@media (max-width: 700px) { .reg-row { grid-template-columns: 1fr; gap: var(--sp-2); } }


/* ══ § 10 · THE TOOLBAR — the component that did not exist ══════════════
   Thirty of the forty-two dashboards hand-assembled a filter bar out of a
   div.row, an .input, a .select and two small buttons, then patched the
   result with a private class — .tk-search, .gx-row, .pm-row.pm-between,
   .cl-filters. Thirty private filter bars is thirty chances to disagree
   about the same control, and `.toolbar` had ZERO definitions in any
   shared sheet.

   It is a WELL and not a card on purpose. A filter bar is where you go to
   narrow what is below it; a recessed plane says "this is the lip of the
   thing", where a card would say "this is another thing" — and then the
   page has two cards where it has one list.
   ═══════════════════════════════════════════════════════════════════════ */

.toolbar { display: flex; align-items: center; flex-wrap: wrap;
           gap: var(--sp-2) var(--sp-3); padding: var(--sp-3);
           background: var(--mat-sunken); border: 1px solid var(--edge-sunken);
           border-radius: var(--r-sm); box-shadow: var(--lit-sunken); }
.toolbar-find { flex: 1 1 14rem; min-width: 0; }
/* One class instead of thirty `-between` variants. */
.toolbar-end  { margin-left: auto; }
.toolbar-sep  { flex: none; width: 1px; align-self: stretch;
                background: var(--border); margin: 0 var(--sp-1); }
@media (max-width: 640px) {
  .toolbar { gap: var(--sp-2); }
  .toolbar-find { flex-basis: 100%; }
  .toolbar-end { margin-left: 0; }
}


/* ══ § 11 · STATUS — a second axis, because hue alone is not one ════════
   Status was five flat colours 217 times over (.badge + success/warn/
   info/danger), so a breached SLA and a routine category label were drawn
   identically apart from hue, and the only differentiation available was
   an inline style="color:..." on the element.

   Two problems, and they compound: nothing was RANKED, and roughly one
   man in twelve cannot separate the green from the red on the axis that
   was carrying the entire meaning.

   So .status carries SHAPE as well as colour, and every state keeps its
   WORD. Colour is the second cue here, never the only one. Never render
   a .status without its label — the shape axis exists so that colour is
   not load-bearing, and a bare dot puts it straight back.
   ═══════════════════════════════════════════════════════════════════════ */

.status { display: inline-flex; align-items: center; gap: 6px;
          font-size: var(--fs-micro); line-height: var(--lh-micro);
          font-weight: var(--fw-medium); color: var(--text-soft); white-space: nowrap; }
.status::before { content: ''; flex: none; width: 8px; height: 8px;
                  border-radius: var(--r-pill); background: var(--border-control); }
/* open — hollow. Nothing has happened to it yet. */
.status-open::before { background: none; box-shadow: inset 0 0 0 1.5px var(--muted); }
/* working — solid, the one live thing in the row. */
.status-now         { color: var(--info-text); }
.status-now::before { background: var(--primary); }
/* done — a SQUARE. --success is byte-identical to --accent in this
   palette (measured; every alternative green came within 1.58:1 because
   the difference is hue, not lightness), so rather than re-tint the brand
   the done/late pair is separated on SHAPE: a square and a diamond are
   told apart with the colour removed entirely. */
.status-done         { color: var(--success-text); }
.status-done::before { background: var(--accent); border-radius: 2px; }
/* late — a diamond. The only rotated mark, because it is the only state
   that has to be findable by shape down a column of two hundred rows. */
.status-late         { color: var(--danger-text); }
.status-late::before { background: var(--danger); border-radius: 1px; transform: rotate(45deg); }
/* at risk — a bar, wider than tall, so it reads as a LEVEL not an event. */
.status-risk         { color: var(--warn-text); }
.status-risk::before { background: var(--warn); width: 10px; height: 4px; border-radius: 1px; }

/* For the one status a row is actually about. */
.status-lead { font-size: var(--fs-body); line-height: var(--lh-body); }
.status-lead::before { width: 10px; height: 10px; }
.status-lead.status-risk::before { width: 12px; height: 5px; }


/* ══ § 12 · TILES — a ranked figure row ════════════════════════════════
   Twenty-one of the forty-two dashboards open on four identical .kpi
   tiles: same ground, same border, same type scale, same size, so the
   screen says nothing about which number matters.

   Four cards in a row is four objects. One panel divided by hairlines is
   ONE object with four readings, which is what a figure row actually is —
   and it removes three borders and three shadows from every page that
   carries one.
   ═══════════════════════════════════════════════════════════════════════ */

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
         gap: 0; border: 1px solid var(--border); border-radius: var(--r-md);
         background: var(--mat-panel); box-shadow: var(--lit-panel); overflow: hidden; }
.tile { padding: var(--sp-5); border-left: 1px solid var(--border);
        display: grid; gap: 2px; align-content: start; min-width: 0; }
.tile:first-child { border-left: 0; }
.tile .metric-n { font-size: var(--fs-title); line-height: var(--lh-title);
                  letter-spacing: var(--tr-title); }
/* Exactly one per group, and it goes FIRST: a ranked thing in the middle
   of a row reads as an accident. */
.tile-lead { position: relative; }
.tile-lead::before { content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
                     background: var(--accent-fill); }
.tile-lead .metric-n { font-size: clamp(30px, 3vw, 40px); line-height: 1.12;
                       letter-spacing: -.02em; }
@media (max-width: 640px) {
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .tile { border-left: 0; border-top: 1px solid var(--border); padding: var(--sp-4); }
  .tiles > .tile:nth-child(-n+2) { border-top: 0; }
  .tiles > .tile:nth-child(even) { border-left: 1px solid var(--border); }
}

/* The warn variant of the decorative tag. accent and primary already
   existed; a limit needs to be nameable inline too. */
.nv-tag-warn { border-color: color-mix(in srgb, var(--warn) 38%, transparent);
               color: var(--warn-text); }


/* ══ § 13 · THE BUTTON ══════════════════════════════════════════════════
   These are components.css's real values, and they are here because THE
   PRODUCT AND THE PAGE THAT SELLS IT WERE SHIPPING TWO DIFFERENT BUTTONS.
   index.html declared its own .btn — 10px/16px padding, 15px type, and a
   decorative `border` instead of a control outline — so the first button a
   buyer ever sees had a boundary below WCAG 1.4.11's 3:1 floor while every
   button inside the product cleared it.

   On the 44 app pages this changes NOTHING: components.css loads after
   ds.css and re-declares every one of these with identical resolved values
   (--border-strong IS --border-control, --radius-sm IS --r-sm, --fs-sm IS
   14px). On a page that loads ds.css WITHOUT components.css — the homepage,
   the pricing page, the demo door — this is now the single definition.
   ═══════════════════════════════════════════════════════════════════════ */

.btn { position: relative; display: inline-flex; align-items: center;
       justify-content: center; gap: 6px; padding: 8px 14px; min-height: 34px;
       border-radius: var(--r-sm); border: 1px solid var(--border-control);
       background: var(--surface-2); color: var(--text);
       font-family: var(--font-body); font-size: var(--fs-sm);
       font-weight: var(--fw-medium); line-height: 1.2;
       white-space: nowrap; user-select: none; cursor: pointer;
       text-decoration: none;
       transition: background-color var(--dur-1) var(--ease-out),
                   border-color var(--dur-1) var(--ease-out),
                   color var(--dur-1) var(--ease-out),
                   transform var(--dur-1) var(--ease-out); }
.btn:hover  { border-color: var(--primary); background: var(--surface-3);
              text-decoration: none; }
.btn:active { transform: scale(.975); transition-duration: 70ms; }
/* --accent-fill and --on-accent, never --accent plus a literal: --accent is
   the INK role and is #00785C in light, so a primary CTA built from it
   renders as dark green carrying near-black ink at 3.07:1. */
.btn-primary { background: var(--accent-fill); border-color: var(--accent-fill);
               color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover);
                     color: var(--on-accent); }
.btn-ghost  { background: transparent; border-color: transparent; color: var(--text); }
.btn-ghost:hover { background: var(--surface-2); border-color: transparent; }
.btn-danger { background: var(--danger-soft); color: var(--danger-text);
              border-color: var(--danger); }
.btn-sm { padding: 5px 10px; min-height: 30px; font-size: var(--fs-tiny); }
.btn-lg { padding: 12px 20px; min-height: 44px; font-size: var(--fs-body); }
.btn:disabled, .btn[disabled] { opacity: .5; cursor: not-allowed; transform: none;
                                filter: grayscale(.35); }
