/* ============================================================================
   DESKSCALE — COMPONENTS
   Built on tokens.css. Load order: tokens.css → site.css → page styles.
   Spec: IDENTITY.md §5. Nothing here invents a value; everything reads a token.
   ========================================================================= */

/* ==========================================================================
   1. LAYOUT
   ========================================================================== */

.ds-wrap { max-width: var(--ds-maxw); margin-inline: auto; padding-inline: var(--ds-gutter) }

.ds-section { padding-block: var(--ds-section) }
.ds-section-sm { padding-block: calc(var(--ds-section) * .55) }

/* Signature #2 — rules reach the true viewport edge. This is what makes a
   layout read as a technical drawing rather than a landing page.

   STRUCTURAL, not a 100vw pseudo-element. Put .ds-ruled on a FULL-WIDTH
   element and nest .ds-wrap inside it; the element is already exactly as wide
   as the viewport minus the scrollbar, so a plain border-top lands on the real
   edge with nothing to overflow.

   The 100vw approach was tried and removed. 100vw INCLUDES the scrollbar, so
   every rule overhung by half the scrollbar width (measured: 7px of a 14px
   bar) and the page scrolled sideways. Neither html{overflow-x:clip} nor
   body{overflow-x:clip} reliably clipped it, because root and body overflow
   both propagate to the viewport under different conditions. Do not reintroduce
   it: if something needs to bleed from inside a container, move the element
   out of the container instead. */
.ds-ruled { border-top: var(--ds-hair) solid var(--ds-rule) }
.ds-ruled-strong { border-top-color: var(--ds-rule-2) }

/* Paper grain. Makes #FBFBF9 read as stock rather than as a hex value.
   Invisible in isolation, obvious by its absence. */
.ds-grain {
  position: fixed; inset: 0; z-index: var(--ds-z-grain); pointer-events: none;
  opacity: var(--ds-grain-opacity); mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ==========================================================================
   2. TYPE UTILITIES
   ========================================================================== */

.ds-hero { font-family: var(--ds-font-display); font-size: var(--ds-t-hero); font-weight: 600;
           letter-spacing: var(--ds-tr-hero); line-height: var(--ds-lh-hero);
           text-transform: uppercase; margin: 0 }
.ds-display { font-family: var(--ds-font-display); font-size: var(--ds-t-display); font-weight: 600;
              letter-spacing: var(--ds-tr-display); line-height: var(--ds-lh-head); margin: 0 }
.ds-h2 { font-size: var(--ds-t-h2); letter-spacing: var(--ds-tr-h2) }
.ds-h3 { font-size: var(--ds-t-h3); letter-spacing: var(--ds-tr-h3) }

.ds-lead { font-size: var(--ds-t-lead); letter-spacing: var(--ds-tr-lead);
           line-height: var(--ds-lh-lead); color: var(--ds-ink-2); max-width: 46ch }
.ds-body { max-width: var(--ds-measure); color: var(--ds-ink-2) }
.ds-body strong { color: var(--ds-ink); font-weight: 600 }

/* Mono is a voice, not a texture. Labels, timestamps, states, units. */
.ds-label { font-family: var(--ds-font-mono); font-size: var(--ds-t-label); font-weight: 500;
            letter-spacing: var(--ds-tr-label); text-transform: uppercase; color: var(--ds-ink-3) }
.ds-micro { font-family: var(--ds-font-mono); font-size: var(--ds-t-micro); font-weight: 500;
            letter-spacing: var(--ds-tr-micro); text-transform: uppercase; color: var(--ds-ink-3) }
.ds-num   { font-family: var(--ds-font-mono); font-weight: 500; letter-spacing: -.01em;
            font-variant-numeric: tabular-nums }

.ds-up   { color: var(--ds-up) }
.ds-risk { color: var(--ds-risk) }
.ds-warn { color: var(--ds-warn) }

/* ==========================================================================
   3. NAV — flush bar on a hairline. No blur, no capsule, no shadow.
   A floating pill reads as creative-agency; this brand must read analytical.
   ========================================================================== */

.ds-nav {
  position: sticky; top: 0; z-index: var(--ds-z-nav); background: var(--ds-bg);
  border-bottom: var(--ds-hair) solid transparent;
  transition: border-color var(--ds-dur-2) var(--ds-ease-out);
}
.ds-nav.is-stuck { border-bottom-color: var(--ds-rule) }
.ds-nav-in { display: flex; align-items: center; gap: var(--ds-s-32); height: var(--ds-nav-h) }
.ds-nav-links { display: flex; gap: var(--ds-s-24); margin-inline: auto }
.ds-nav-links a {
  position: relative; text-decoration: none; color: var(--ds-ink-2);
  font-family: var(--ds-font-mono); font-size: var(--ds-t-label); font-weight: 500;
  letter-spacing: var(--ds-tr-label); text-transform: uppercase;
  padding-block: calc((var(--ds-nav-h) - 1em) / 2);
}
.ds-nav-links a:hover { color: var(--ds-ink) }
/* Active state drawn by the bar's own base rule doing double duty. */
.ds-nav-links a[aria-current]::after {
  content: ''; position: absolute; inset-inline: 0; bottom: -1px; height: 1px; background: var(--ds-blue);
}
.ds-nav-cta { display: flex; align-items: center; gap: var(--ds-s-12) }
@media (max-width: 900px) { .ds-nav-links { display: none } }

/* ==========================================================================
   4. BUTTONS — square-ish, flat, no gradient, no pill, no shadow.
   Hover latches in ONE step rather than fading. Instruments do not glide.
   ========================================================================== */

.ds-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 44px; padding-inline: 20px; border-radius: var(--ds-r-1);
  border: var(--ds-hair) solid transparent; cursor: pointer; text-decoration: none;
  font-family: var(--ds-font-ui); font-size: .9rem; font-weight: 500; white-space: nowrap;
  transition: background-color var(--ds-dur-1) steps(1),
              border-color var(--ds-dur-1) steps(1),
              color var(--ds-dur-1) steps(1);
}
.ds-btn-primary { background: var(--ds-blue); color: var(--ds-bg) }
.ds-btn-primary:hover { background: var(--ds-blue-2); color: var(--ds-bg) }
.ds-btn-ghost { border-color: var(--ds-rule-2); color: var(--ds-ink) }
.ds-btn-ghost:hover { border-color: var(--ds-ink); color: var(--ds-ink) }
.ds-btn-sm { min-height: 36px; padding-inline: 14px; font-size: .8rem }

/* ==========================================================================
   5. GRID & CELLS
   Zero gap is load-bearing: cells SHARE a rule instead of floating apart, so a
   row reads as one continuous instrument face rather than as N cards.
   ========================================================================== */

.ds-grid { display: grid; gap: var(--ds-col-gap);
           border: var(--ds-hair) solid var(--ds-rule); background: var(--ds-rule) }
.ds-grid > * { background: var(--ds-bg); outline: var(--ds-hair) solid var(--ds-rule) }
.ds-g2 { grid-template-columns: repeat(2, 1fr) }
.ds-g3 { grid-template-columns: repeat(3, 1fr) }
.ds-g4 { grid-template-columns: repeat(4, 1fr) }
.ds-g5 { grid-template-columns: repeat(5, 1fr) }
@media (max-width: 1024px) { .ds-g5, .ds-g4 { grid-template-columns: repeat(2, 1fr) } }
@media (max-width: 768px)  { .ds-g5, .ds-g4, .ds-g3, .ds-g2 { grid-template-columns: 1fr } }

.ds-cell { position: relative; padding: var(--ds-s-24) }
.ds-cell-lg { padding: var(--ds-s-32) }

/* Signature #1 — corner registration marks replace the card border everywhere.
   Print crop marks. Drawn at ACTIVE size and scaled down at rest so the hover
   transition animates transform + colour only; animating width/height here
   would thrash layout on every pointer move across a grid. */
.ds-ticks::before, .ds-ticks::after,
.ds-ticks > .ds-tk::before, .ds-ticks > .ds-tk::after {
  content: ''; position: absolute; pointer-events: none;
  width: var(--ds-tick-active); height: var(--ds-tick-active);
  border: 0 solid var(--ds-rule-2); transform: scale(.625);
  transition: transform var(--ds-dur-2) var(--ds-ease-out),
              border-color var(--ds-dur-2) var(--ds-ease-out);
}
.ds-ticks::before          { top: -1px; left: -1px;   border-top-width: 1px; border-left-width: 1px;
                             transform-origin: top left }
.ds-ticks::after           { bottom: -1px; right: -1px; border-bottom-width: 1px; border-right-width: 1px;
                             transform-origin: bottom right }
.ds-ticks > .ds-tk::before { top: -1px; right: -1px;  border-top-width: 1px; border-right-width: 1px;
                             transform-origin: top right }
.ds-ticks > .ds-tk::after  { bottom: -1px; left: -1px; border-bottom-width: 1px; border-left-width: 1px;
                             transform-origin: bottom left }
.ds-ticks:hover::before, .ds-ticks:hover::after,
.ds-ticks:hover > .ds-tk::before, .ds-ticks:hover > .ds-tk::after {
  transform: scale(1); border-color: var(--ds-blue);
}

/* ==========================================================================
   6. DATA READOUTS — label / value / delta.
   The recurring pattern for a number with meaning. This is what fills the
   grid cells the reference layout filled with diagonal hatching.
   ========================================================================== */

.ds-stat { display: flex; flex-direction: column; gap: 6px }
.ds-stat-v { font-family: var(--ds-font-mono); font-weight: 500; font-size: 1.5rem;
             letter-spacing: -.01em; font-variant-numeric: tabular-nums; color: var(--ds-ink) }
.ds-stat-v-lg { font-size: clamp(1.6rem, 2.6vw, 2.1rem) }

/* Colour is never the sole carrier of meaning: every up/down pairs its colour
   with a glyph and a word, so it survives colour-vision deficiency and reads
   correctly in a greyscale screenshot. */
.ds-delta { display: inline-flex; align-items: baseline; gap: 5px;
            font-family: var(--ds-font-mono); font-variant-numeric: tabular-nums }

/* Big machinery figures. Mono and tabular like every other number in the
   system, and INK — four blue numerals in a row would blow the accent ceiling
   (§2 of IDENTITY.md) three times over. */
.ds-figure {
  font-family: var(--ds-font-mono); font-weight: 500;
  font-size: clamp(2.2rem, 3.6vw, 3.2rem); letter-spacing: -.03em;
  font-variant-numeric: tabular-nums; color: var(--ds-ink); line-height: 1;
}

.ds-strip { display: grid; grid-template-columns: repeat(3, 1fr) }
.ds-strip > * { padding: var(--ds-s-24) 0; border-left: var(--ds-hair) solid var(--ds-rule);
                padding-inline-start: var(--ds-s-24) }
.ds-strip > *:first-child { border-left: 0; padding-inline-start: 0 }
@media (max-width: 768px) { .ds-strip { grid-template-columns: 1fr }
  .ds-strip > * { border-left: 0; padding-inline-start: 0;
                  border-top: var(--ds-hair) solid var(--ds-rule) }
  .ds-strip > *:first-child { border-top: 0 } }

/* Market coverage band. Occupies the slot a fake enterprise-logo bar would,
   and is the honest version of it: verifiable index coverage rather than
   borrowed credibility. Set in display type, separated by the same hairlines
   as everything else, so it reads as part of the instrument face. */
.ds-coverage { display: grid; grid-template-columns: repeat(5, 1fr); margin-top: var(--ds-s-24) }
.ds-coverage > * {
  font-family: var(--ds-font-display); font-size: clamp(1rem, 1.5vw, 1.35rem);
  font-weight: 500; letter-spacing: -.02em; color: var(--ds-ink);
  padding-inline-start: var(--ds-s-24);
  border-left: var(--ds-hair) solid var(--ds-rule);
}
.ds-coverage > *:first-child { border-left: 0; padding-inline-start: 0 }
@media (max-width: 768px) {
  .ds-coverage { grid-template-columns: repeat(2, 1fr); gap: var(--ds-s-16) }
  .ds-coverage > * { border-left: 0; padding-inline-start: 0 }
}

/* Notification cards: kind + elapsed time on one baseline, headline, plain
   English. No icon set — this system has none by design, because structure
   replaces decoration (IDENTITY.md §1). */
.ds-notif-head { display: flex; justify-content: space-between; align-items: baseline;
                 gap: var(--ds-s-16) }

/* ==========================================================================
   7. SCALE RAIL — signature #3
   The logomark enlarged into navigation furniture. Decorative: aria-hidden,
   and hidden entirely below 1024px where it is furniture, not content.
   ========================================================================== */

.ds-rail { position: fixed; left: 18px; top: 50%; transform: translateY(-50%);
           display: flex; flex-direction: column; gap: 10px; z-index: 50 }
/* Drawn at full 18px and scaled down at rest, so extension animates transform
   rather than width. Same reasoning as the corner ticks above. */
.ds-rail-tick { display: block; width: 18px; height: 1px; background: var(--ds-rule-2);
                transform: scaleX(.278); transform-origin: left center;
                transition: transform var(--ds-dur-2) var(--ds-ease-out),
                            background-color var(--ds-dur-2) var(--ds-ease-out) }
.ds-rail-tick.is-on { transform: scaleX(1); background: var(--ds-blue) }
@media (max-width: 1180px) { .ds-rail { display: none } }

/* ==========================================================================
   8. PULSE — a 3px square travels a rule line.
   One at a time site-wide; a page that twinkles everywhere is decoration.
   ========================================================================== */

.ds-pulserail { position: relative; height: 1px; background: var(--ds-rule) }
.ds-pulserail > i { position: absolute; top: -1px; width: 3px; height: 3px;
                    background: var(--ds-blue);
                    animation: ds-pulse var(--ds-dur-pulse) var(--ds-ease-out) infinite }

/* ==========================================================================
   9. INSTRUMENT PLATES — 1-bit dithered renders of real Deskscale output.
   Every plate carries source · window · timestamp. A plate without provenance
   violates BRAND.md §5.1 and does not ship.
   ========================================================================== */

.ds-plate { position: relative }
.ds-plate-row { display: grid; grid-template-columns: 176px 1fr; align-items: center;
                gap: var(--ds-s-16); padding-block: 5px }
@media (max-width: 640px) { .ds-plate-row { grid-template-columns: 96px 1fr } }
.ds-plate-row canvas { display: block; width: 100%; height: 34px }

.ds-plate-stack { position: relative; padding-left: var(--ds-s-24) }
.ds-plate-conn { position: absolute; left: 8px; top: 8px; bottom: 8px; width: 1px;
                 background: var(--ds-rule-2) }
.ds-plate-conn > i { position: absolute; left: -1px; width: 3px; height: 3px;
                     background: var(--ds-blue);
                     animation: ds-pulse-y 3200ms var(--ds-ease-out) infinite }
.ds-plate-io { display: flex; justify-content: space-between; align-items: center;
               padding-block: var(--ds-s-12) }

/* sweep — the plate resolves behind a travelling blue line */
/* Strata are PRESENT, not swept in. Same operator call as the scroll reveals
   in §13: eight rows resolving 90ms apart is the "one by one" pattern, just in
   a different component.

   ⚠️ The base `clip-path: inset(0 100% 0 0)` had to be removed WITH the
   animation, not instead of it. It clipped every stratum to zero width until
   .is-swept landed, so dropping only the animation would have left the plate
   permanently invisible — the exact failure the watchdog in ds-motion.js
   exists to catch.

   To restore: re-add the clip, this ds-sweep-reveal rule, and the ds-sweep
   rule on .ds-sweepline below. All three, or the plate disappears. */
/* The sweep line is a FULL-WIDTH element whose only visible part is a 1px blue
   edge on its right, painted by the gradient.

   It must be full width because ds-sweep translates by 100%, and 100% of a
   1px-wide element is 1px. The first version was literally a 1px-wide div: it
   "swept" a total of two pixels, then sat parked over the plate labels at 90%
   opacity forever. Do not shrink this element to the width of the visible line.

   Duration is longer than --ds-dur-4 on purpose: it has to span the whole
   staggered strata reveal (640ms + 6 rows x 90ms) so the line appears to be
   what causes each band to resolve, rather than finishing before they do. */
[data-sweep] .ds-sweepline {
  position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg,
              transparent 0 calc(100% - 1px),
              var(--ds-blue) calc(100% - 1px) 100%);
}
/* The sweep line stays at opacity 0. With the strata no longer resolving
   behind it there is nothing for it to appear to cause, so a blue line
   crossing the plate would be pure decoration. */

/* ==========================================================================
   12. MARKET INTELLIGENCE FIELD — the hero object
   Structure is generated by js/ds-field.js; all styling and motion live here.
   ========================================================================== */

/* The SVG is deliberately 24px wider than its container with a -12px offset,
   and the container clips. Cursor parallax translates the SVG up to ±7px; at
   exactly 100% width that pushed it past the container and put 2px of
   horizontal scroll on the page, while also exposing a sliver of bare
   background on whichever side it moved away from. The 12px of bleed per side
   absorbs the full parallax range. Do not "tidy" this back to width:100%. */
/* Capped, not full-width. The viewBox is 1000x600, so at full container width
   the field renders 5:3 — 809px tall at a 1363px viewport and ~1200px on a
   wide monitor, which dominates the page instead of illustrating it. Capping
   at 1080 puts it near its native 1:1 scale (labels land at 11.9px against the
   11px they were drawn at) and keeps the height around 648px.
   THIS is the knob for the field's size. Do not scale the ring radii instead:
   the labels sit at fixed viewBox coordinates tuned to those radii, and the
   orbit containment rule in ds-field.js is written against RMAX=288. */
.ds-field { position: relative; width: 100%; max-width: 1080px;
            margin-inline: auto; overflow: hidden }
.ds-field-svg { display: block; width: calc(100% + 24px); margin-inline-start: -12px;
                height: auto; transition: transform 380ms var(--ds-ease-out) }

/* Construction scaffolding — the same hairline as every rule on the site, so
   the drawing belongs to the page rather than sitting on top of it. */
.ds-field-rules line { stroke: var(--ds-rule); stroke-width: 1 }
/* Each orbit is drawn twice. `-hit` is a fat transparent stroke that catches
   the pointer, because a 1px hairline is effectively impossible to hover;
   `-line` is the visible hairline and carries no pointer events at all.
   Only stroke colour and width transition — both are paint, not layout. */
.ds-orb-hit {
  fill: none; stroke: transparent; stroke-width: 22;
  pointer-events: stroke; cursor: crosshair;
}
.ds-orb-line {
  fill: none; stroke: var(--ds-rule-2); stroke-width: 1; pointer-events: none;
  transition: stroke var(--ds-dur-2) var(--ds-ease-out),
              stroke-width var(--ds-dur-2) var(--ds-ease-out);
}
.ds-orb-p:hover .ds-orb-line { stroke: var(--ds-blue); stroke-width: 1.75 }

/* Rings carry literal fills from ds-field.js rather than tokens: they are a
   seven-step ramp between --ds-rule and --ds-ink, and expressing that as seven
   more tokens would bloat the palette for one consumer.

   Rings never highlight — not on hover, not on the sweep. Outlining the band
   as well as lighting its name made the field busy and turned a quiet readout
   into a flashing one. Each still carries a <title>, so the browser surfaces
   the layer name natively and accessibly without any visual noise. */
.ds-field-rings ellipse { shape-rendering: geometricPrecision }

.ds-core-halo { fill: var(--ds-bg) }
.ds-core-dot  { fill: var(--ds-blue) }
.ds-orb-node { fill: var(--ds-blue) }
.ds-field-scan rect  { fill: var(--ds-blue); opacity: 0 }

/* The SVG scales uniformly, so an 11px label in a 1000-unit viewBox renders at
   11 x (renderedWidth / 1000). Below ~860px that falls under 9.5px and the
   names become illegible texture rather than information — measured 4.1px at
   375. Hide them there and let the field read as pure geometry; the seven
   layers are named in body text on the page regardless, and the SVG carries a
   full aria-label, so nothing is actually lost. */
@media (max-width: 860px) { .ds-field-labels { display: none } }

/* No cursor affordance and no pointer events: the names are driven entirely by
   the sweep, so there is nothing here to click or hover and implying otherwise
   would be a lie. */
.ds-field-labels g { pointer-events: none }
.ds-field-labels text {
  font-family: var(--ds-font-mono); font-size: 11px; font-weight: 500;
  letter-spacing: .14em; fill: var(--ds-ink-3);
  /* Not selectable — drag-selecting across the field left highlight boxes over
     the names. Pointer events stay ON, because the label is a hover target. */
  user-select: none; -webkit-user-select: none;
  transition: fill var(--ds-dur-2) var(--ds-ease-out);
}
.ds-field-labels path { stroke: var(--ds-rule-2); stroke-width: 1;
                        transition: stroke var(--ds-dur-2) var(--ds-ease-out) }

/* Lit by the scan sweep only. Names light in sequence as the line crosses
   their band, so the band-to-name mapping teaches itself with no interaction.

   A colour change alone was too quiet against --ds-ink-3 at 11px. The lit
   state now also pins opacity to full (overriding the ambient flicker),
   thickens the tick, and draws a blue rule under the name — a rule being the
   most on-brand emphasis available in this system. */
.ds-field-labels g.is-lit { animation: none !important; opacity: 1 }
.ds-field-labels g.is-lit text { fill: var(--ds-blue) }
.ds-field-labels g.is-lit path { stroke: var(--ds-blue); stroke-width: 1.5 }

.ds-lab-bar { fill: var(--ds-blue); opacity: 0;
              transition: opacity var(--ds-dur-2) var(--ds-ease-out) }
.ds-field-labels g.is-lit .ds-lab-bar { opacity: 1 }

/* Motion. Rotation is applied to wrapper groups so each shape keeps its own
   static tilt; transform-box: view-box makes the SVG centre the origin. */
.ds-orb { transform-box: view-box; transform-origin: 50% 50%;
          animation: ds-orbit var(--dur, 240s) linear infinite }
.ds-orb-rev { animation-direction: reverse }

.ds-field-core { transform-box: view-box; transform-origin: 50% 50%;
                 animation: ds-core 7s var(--ds-ease-out) infinite }
/* 20s, not 9s. The sweep drives the name highlight, so its speed IS the dwell
   time per layer — at 9s each name lit for well under half a second, which
   read as flicker rather than a readout. At 20s a band holds for roughly a
   second, which is long enough to actually read the name. */
.ds-field-scan { transform-box: view-box; transform-origin: 50% 50%;
                 animation: ds-scan 20s var(--ds-ease-out) infinite }
.ds-field-labels g { animation: ds-flicker 11s var(--ds-ease-out) infinite;
                     animation-delay: calc(var(--i, 0) * 1.4s) }

/* The field is CONTENT, not decoration: it must be fully present with motion
   off. Only the movement stops. */
@media (prefers-reduced-motion: reduce) {
  html:not(.ds-force) .ds-orb,
  html:not(.ds-force) .ds-field-core,
  html:not(.ds-force) .ds-field-scan,
  html:not(.ds-force) .ds-field-labels g { animation: none !important }
  html:not(.ds-force) .ds-field-labels g { opacity: 1 }
  html:not(.ds-force) .ds-field-scan { display: none }
  html:not(.ds-force) .ds-field-svg { transition: none }
}

/* ==========================================================================
   14. SESSION GRID — one cell per recorded session
   Ink, square, uniform. NOT green: --ds-up means bullish, and a field of it
   would make a directional claim out of decoration. NOT shaded by outcome
   either — that would be a performance claim (BRAND.md §7). A cell means a
   session was recorded, nothing more.
   ========================================================================== */

/* Regime calendar: weeks as columns, Mon-Fri as rows. Square cells, because
   the reference was rounded and this system is not. Colour here is DATA
   MEANING (risk-on / risk-off are directional states), which is exactly what
   --ds-up and --ds-risk exist for — unlike a decorative green field. */
.ds-cal { display: grid; grid-auto-flow: column; grid-auto-columns: 14px; gap: 4px }
.ds-cal-col { display: grid; grid-template-rows: repeat(5, 14px); gap: 4px }
.ds-cal-cell { width: 14px; height: 14px; display: block;
               transition: outline-color var(--ds-dur-1) var(--ds-ease-out);
               outline: 1px solid transparent; outline-offset: 1px }
.ds-cal-cell:hover { outline-color: var(--ds-ink) }
.ds-cal-cell.is-empty { background: transparent }
/* A trading day inside the archive window with no session recorded. Drawn as
   an outline rather than filled: the engine did not run, and colouring it
   would claim it did. */
.ds-cal-cell.is-none { background: transparent; box-shadow: inset 0 0 0 1px var(--ds-rule) }
.ds-cal-k > i.is-none { background: transparent; box-shadow: inset 0 0 0 1px var(--ds-rule-2) }

/* One label per month turnover, so the row reads as a timeline. */
.ds-cal-months { display: grid; grid-auto-flow: column; grid-auto-columns: 14px;
                 gap: 4px; margin-bottom: 8px }
.ds-cal-months span { white-space: nowrap; color: var(--ds-ink-3) }

/* Every swatch is named. Colour is never the sole carrier of meaning. */
.ds-cal-key { display: flex; flex-wrap: wrap; gap: var(--ds-s-16);
              margin-top: var(--ds-s-24) }
.ds-cal-k { display: inline-flex; align-items: center; gap: 7px }
.ds-cal-k > i { width: 11px; height: 11px; display: block }

.ds-cal-wrap { overflow-x: auto }

.ds-sess-foot { display: flex; flex-wrap: wrap; justify-content: space-between;
                align-items: baseline; gap: var(--ds-s-12);
                margin-top: var(--ds-s-24); padding-top: var(--ds-s-12);
                border-top: var(--ds-hair) solid var(--ds-rule) }

/* ==========================================================================
   13. SESSION SHAPE — morphing day-type columns
   Structure from js/ds-shape.js. Flat ink columns, one accent, no glow and no
   gradient: the source reference was luminous and iridescent, which §9 bans
   and which would read as a second visual language on warm stock.
   ========================================================================== */

.ds-shape { width: 100%; border-bottom: var(--ds-hair) solid var(--ds-rule-2) }
.ds-shape-svg { display: block; width: 100%; height: clamp(150px, 20vw, 240px) }

/* Only transform animates, so this composites rather than repainting geometry.
   transform-box: fill-box makes the origin the rect's own bottom edge. */
.ds-shape-col {
  fill: var(--ds-rule-2);
  transform-box: fill-box; transform-origin: bottom;
  transition: transform 1600ms var(--ds-ease-exp), fill 900ms var(--ds-ease-out);
}
.ds-shape-col.is-peak { fill: var(--ds-blue) }

.ds-shape-foot { display: flex; justify-content: space-between; align-items: baseline;
                 gap: var(--ds-s-16); padding-top: var(--ds-s-12) }
.ds-shape-name { font-size: var(--ds-t-label); letter-spacing: var(--ds-tr-label);
                 text-transform: uppercase; color: var(--ds-blue) }

@media (prefers-reduced-motion: reduce) {
  html:not(.ds-force) .ds-shape-col { transition: none }
}

/* ==========================================================================
   10. SETTLE — scroll reveal
   .ds-js is set by an inline head script with a watchdog that removes it if
   the motion engine never boots. Content must NEVER be permanently hidden by
   a script that failed to load.
   ========================================================================== */

/* SCROLL REVEALS ARE OFF (operator call, 2026-08-12). Sections used to start
   at opacity 0 and settle in on enter, with [data-reveal-group] children
   staggered off --ds-i — which is what read as "everything showing up one by
   one" on the way down the page.

   Turned off HERE rather than in ds-motion.js on purpose. The observer in
   initReveal() also fires runTick() for [data-tick] counters, so disabling the
   JS would silently freeze every stat at its HTML placeholder — correct only
   by accident when the placeholder happens to equal the target. The observer
   still runs and still adds .is-in; it simply no longer paints anything, and
   the hero headline below still uses .is-in on its ancestor.

   To restore: put back `opacity: 0` plus the two ds-settle rules. */
.ds-js [data-reveal],
.ds-js [data-reveal-group] > * { opacity: 1 }

/* The hero headline reveals line by line. This is the page's one orchestrated
   moment; everything else is restrained on purpose. */
.ds-hero-line { display: block; overflow: hidden }
.ds-js .ds-hero-line > span { display: block; opacity: 0 }
.ds-js .is-in .ds-hero-line > span,
.ds-js .ds-hero-line.is-in > span {
  animation: ds-settle var(--ds-dur-4) var(--ds-ease-exp) both;
  animation-delay: calc(var(--ds-i, 0) * 90ms);
}

/* Under reduced motion the animation is REMOVED, not shortened.
   `animation: none` is load-bearing: a running animation with fill:both always
   beats a normal `opacity` declaration, so simply setting opacity:1 here loses.
   Worse, if the animation never advances past t=0 for any reason (a throttled
   background tab, a static-snapshot renderer, a stalled clock) it pins the
   element at its `from` state and the content is invisible. Content visibility
   must never depend on an animation completing. */
/* Print renders the whole document with no scrolling, so the reveal observer
   never fires and every below-fold section would print as blank paper. Same
   principle as the reveal watchdog: content must never depend on an effect. */
@media print {
  [data-reveal], [data-reveal-group] > *,
  .ds-hero-line > span { opacity: 1 !important; animation: none !important }
  [data-sweep] .ds-plate-row canvas { clip-path: none !important; animation: none !important }
}

@media (prefers-reduced-motion: reduce) {
  html:not(.ds-force) [data-reveal],
  html:not(.ds-force) [data-reveal-group] > *,
  html:not(.ds-force) .ds-hero-line > span { opacity: 1 !important; animation: none !important }
  html:not(.ds-force) [data-sweep] .ds-plate-row canvas { clip-path: none !important;
                                                          animation: none !important }
  html:not(.ds-force) [data-sweep] .ds-sweepline { display: none }
  html:not(.ds-force) .ds-pulserail > i,
  html:not(.ds-force) .ds-plate-conn > i { display: none }
}

/* ==========================================================================
   11. FOOTER
   ========================================================================== */

.ds-foot { padding-block: var(--ds-s-64) var(--ds-s-96);
           border-top: var(--ds-hair) solid var(--ds-rule) }
.ds-foot a { color: var(--ds-ink-2); text-decoration: none }
.ds-foot a:hover { color: var(--ds-ink) }
/* Disclaimer sits in the footer of EVERY page, not only legal.html. */
.ds-disclaimer { margin-top: var(--ds-s-32); color: var(--ds-ink-3) }
