/* ════════════════════════════════════════════════════════════════════════
   css/components/demos.css — THE FIVE RUNNING MECHANICS
   service.tnaado.ca · pairs with js/demos.js

   PORTED from the old site's css/tech-expertise.css (§3, the demo stages).
   What came across is the MECHANIC AND ITS CHROME. What did not: the old
   accordion shell, the old section copy, and the old page's type scale.

   WHY THESE EXIST. Each stage shows a thing the firm does instead of
   claiming it. The RLS table filters in front of the reader; the payment
   walks its real states including the decline; the migrations apply in
   order; the scheduled functions fire across a day. A paragraph asserting
   any of that is strictly weaker and should be deleted when a stage lands.

   NO CARD, NO PLATE, NO BORDER. A stage sits on the page's own ink so it
   reads as part of the page rather than a widget dropped onto it. The only
   chrome is the mono rail across the top — the instrument label.

   TOKENS. Mapped from the old site's names to this one's on the way in:
     --font-body → --sans      --font-display → --serif
     --font-mono → --mono      --red-text     → --red-ink
     --on-black  → --bone
   The --dx-* family below is component-scoped and defined here, so this
   file adds no global token and cannot collide with another stylesheet.

   THE INLINE CUSTOM PROPERTIES (--i, --k, --n, --rh, --tx, --px, --at) are
   set by markup or by js/demos.js. They are geometry, not theme, which is
   why they are not in the block below.

   REDUCED MOTION is handled in js/demos.js, not here: every demo paints
   its single most informative frame via freeze() and never ticks. That is
   also the no-JS rendering, so a blocked script loses the motion and
   nothing else.
   ════════════════════════════════════════════════════════════════════════ */

[data-demo] {
  /* ── TOKEN BRIDGE. Every global token this file reads is re-declared here
     WITH A LITERAL FALLBACK, so the component is genuinely drop-in.

     Why it matters: --mono/--serif/--red/--bone are declared at :root by
     css/reference.css, css/firm-pages.css and css/util.css, but NOT by
     css/scene.css + css/chrome.css — which is all technology.html links.
     Mounted there, a var(--dx-mono) with no fallback silently renders the
     browser's default serif at the wrong size and the instrument rails
     stop reading as instruments. Nothing below reads a bare global. */
  --dx-mono:  var(--mono,  'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace);
  --dx-serif: var(--serif, 'Fraunces', 'Times New Roman', Times, serif);
  --dx-sans:  var(--sans,  'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);
  --dx-ink:     var(--ink,     #0a0a0a);
  --dx-bone:    var(--bone,    #f5f1e8);
  --dx-red:     var(--red,     #c8102e);
  --dx-red-ink: var(--red-ink, #ef5c6e);

  --dx-line:      rgba(245, 241, 232, .14);
  --dx-line-soft: rgba(245, 241, 232, .08);
  --dx-mute:      rgba(245, 241, 232, .55);
  /* WAS .38, WHICH FAILED WCAG ON AN ENABLED CONTROL. Composited over the
     panel's ink ground, rgba(245,241,232,.38) computes to rgb(99,98,94) --
     3.24:1 against 10,10,10, measured, where interactive text needs 4.5:1. It
     is the resting colour of the three segmented buttons in .rls__who ("signed
     out", "a client", "staff"), which are ENABLED controls a person is meant to
     press, not disabled ones. .55 computes to rgb(139,137,132) at 5.67:1 and
     still reads as the quiet, unselected state beside the pressed button's full
     bone. This token has exactly one consumer, so nothing else moves. */
  --dx-dim:       rgba(245, 241, 232, .55);
  --dx-ease:      cubic-bezier(.22, .61, .36, 1);
}

/* ══════════════════════════════════════════════════════════════════════
   §3 · THE STAGE — shared chrome for all five demos.

   No border, no card, no plate. The demo elements carry their own weight
   on the page's own ink, so a panel never reads as a box dropped into the
   page. The only chrome is a mono caption rail across the top, which is
   the demo's own instrument label.
   ══════════════════════════════════════════════════════════════════════ */
.tdx-stage { min-width: 0; }
.tdx-stage__rail {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .5rem 1rem; padding-bottom: .6rem;
  border-bottom: 1px solid var(--dx-line);
  font-family: var(--dx-mono); font-size: .6rem;
  letter-spacing: .12em; text-transform: uppercase;
  color: rgba(245,241,232,.55);
}
.tdx-stage__rail b { font-weight: 500; color: var(--dx-red-ink); letter-spacing: .12em; }
.tdx-stage__rail code {
  font-family: var(--dx-mono); text-transform: none; letter-spacing: .02em;
  color: var(--dx-bone);
}
.tdx-stage__note {
  margin: .9rem 0 0;
  font-family: var(--dx-mono); font-size: .62rem;
  letter-spacing: .05em; line-height: 1.6;
  color: rgba(245,241,232,.52);
}

/* ── DEMO 01 · ROW-LEVEL SECURITY ──────────────────────────────────────
   The mechanic, literally: the reader changes who is asking, and the rows
   that the policy denies physically leave the table while the survivors
   close the gap. Rows are a fixed --rh tall and the viewport is a fixed
   height, so a filter is pure transform on the rows — no reflow, no layout
   read, nothing repaints but the row that changed. */
.rls { --rh: 30px; padding-top: 1rem; }
.rls__who {
  display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .9rem;
}
.rls__who button {
  font-family: var(--dx-mono); font-size: .62rem; letter-spacing: .06em;
  padding: .34rem .6rem;
  color: var(--dx-dim); background: none;
  border: 1px solid var(--dx-line);
  transition: color 160ms var(--dx-ease), border-color 160ms var(--dx-ease), background-color 160ms var(--dx-ease);
}
.rls__who button[aria-pressed="true"] {
  color: var(--dx-bone); border-color: rgba(200,16,46,.62);
  background-color: rgba(200,16,46,.09);
}
@media (hover: hover) and (pointer: fine) {
  .rls__who button:hover { color: var(--dx-bone); border-color: rgba(245,241,232,.28); }
}
.rls__who button:focus-visible { outline: 2px solid var(--dx-red-ink); outline-offset: 1px; }

.rls__policy {
  font-family: var(--dx-mono); font-size: .655rem; line-height: 1.7;
  color: rgba(245,241,232,.5); margin: 0 0 1rem;
  overflow-x: auto; white-space: nowrap; padding-bottom: .2rem;
}
.rls__policy b { color: var(--dx-bone); font-weight: 500; }
.rls__policy i { font-style: normal; color: var(--dx-red-ink); }

.rls__tablehead,
.rls__row {
  display: grid;
  grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr) minmax(0, 5rem) minmax(0, 4.5rem);
  gap: .8rem;
  font-family: var(--dx-mono); font-size: .64rem;
  align-items: center;
}
.rls__tablehead {
  height: 24px; color: rgba(245,241,232,.52);
  letter-spacing: .12em; text-transform: uppercase;
  border-bottom: 1px solid var(--dx-line);
}
.rls__body {
  position: relative;
  height: calc(var(--rh) * var(--n, 6));
  overflow: hidden;
  transition: height 320ms var(--dx-ease);
}
/* anon returns nothing at all. An empty frame is the honest result and the
   most persuasive one, so it is labelled rather than padded out. */
.rls__empty {
  position: absolute; inset: 0;
  display: flex; align-items: center;
  font-family: var(--dx-mono); font-size: .62rem; letter-spacing: .06em;
  color: rgba(245,241,232,.58);
  opacity: 0; transition: opacity 200ms var(--dx-ease);
  pointer-events: none;
}
.rls[data-empty="1"] .rls__empty { opacity: 1; }
.rls__row {
  position: absolute; left: 0; right: 0; top: 0;
  height: var(--rh);
  color: rgba(245,241,232,.72);
  border-bottom: 1px solid var(--dx-line-soft);
  /* index position, set once in JS */
  transform: translateY(calc(var(--i, 0) * var(--rh)));
  transition: transform 320ms var(--dx-ease), opacity 200ms var(--dx-ease);
}
/* will-change ONLY while the row is open. ART-GUIDE §2.3 caps a page at
   ~3 promoted layers and the hero already spends several; leaving six
   table rows and the payment token permanently promoted would keep seven
   compositor layers alive for panels nobody has opened. */
.tdx-row[data-open="1"] .rls__row { will-change: transform; }
.rls__row[data-vis="0"] {
  opacity: 0;
  transform: translateY(calc(var(--i, 0) * var(--rh))) translateX(-18px);
  transition: transform 260ms var(--dx-ease), opacity 140ms var(--dx-ease);
}
.rls__row[data-vis="1"] { transform: translateY(calc(var(--k, 0) * var(--rh))); opacity: 1; }
.rls__row em { font-style: normal; color: var(--dx-red-ink); }
.rls__row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rls__count {
  margin: .85rem 0 0;
  font-family: var(--dx-mono); font-size: .62rem; letter-spacing: .08em;
  color: rgba(245,241,232,.58);
}
.rls__count b {
  color: var(--dx-bone); font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.rls__count i { font-style: normal; color: var(--dx-red-ink); }
@media (max-width: 620px) {
  .rls__tablehead, .rls__row { grid-template-columns: minmax(0, 5.6rem) minmax(0, 1fr) minmax(0, 4rem); font-size: .6rem; }
  .rls__col--amt { display: none; }
}
/* The policy is nowrap + horizontally scrollable on wide screens so the SQL
   reads as one statement. On a phone that clipped it mid-keyword ("...for
   sel") against the right edge, which looks like a rendering fault rather
   than a scroll affordance, so it wraps there instead. */
@media (max-width: 760px) {
  .rls__policy { white-space: normal; overflow-x: visible; font-size: .6rem; }
}

/* ── DEMO 02 · PAYMENT FLOW ────────────────────────────────────────────
   A real Stripe PaymentIntent walking its real states, including the
   decline branch and the retry that follows it. The token is one absolutely
   positioned dot moved by transform; the rail and the nodes never move. */
.pay { padding-top: 1.4rem; }
.pay__track {
  position: relative;
  height: 104px;
  margin-bottom: .6rem;
}
.pay__line {
  position: absolute; left: 0; right: 0; top: 22px; height: 1px;
  background: var(--dx-line);
}
.pay__nodes {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; justify-content: space-between; align-items: flex-start;
}
.pay__node {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: .5rem; width: 20%;
}
.pay__dot {
  width: 9px; height: 9px; margin-top: 18px;
  background: var(--dx-ink);
  border: 1px solid rgba(245,241,232,.3);
  border-radius: 50%;
  transition: border-color 200ms var(--dx-ease), background-color 200ms var(--dx-ease), transform 200ms var(--dx-ease);
}
.pay__node[data-done="1"] .pay__dot { border-color: rgba(245,241,232,.6); background: rgba(245,241,232,.6); }
.pay__node[data-at="1"] .pay__dot { border-color: var(--dx-red); background: var(--dx-red); transform: scale(1.35); }
.pay__label {
  font-family: var(--dx-mono); font-size: .555rem;
  letter-spacing: .05em; text-align: center; line-height: 1.4;
  color: rgba(245,241,232,.52);
  transition: color 200ms var(--dx-ease);
}
.pay__node[data-at="1"] .pay__label,
.pay__node[data-done="1"] .pay__label { color: rgba(245,241,232,.72); }

/* The decline branch, hanging below the rail. */
.pay__fail {
  position: absolute; top: 64px; left: 0;
  display: flex; align-items: center; gap: .5rem;
  opacity: 0; transform: translateY(-6px);
  transition: opacity 180ms var(--dx-ease), transform 220ms var(--dx-ease);
}
.pay[data-branch="1"] .pay__fail { opacity: 1; transform: none; }
.pay__fail span {
  font-family: var(--dx-mono); font-size: .62rem; letter-spacing: .05em;
  color: var(--dx-red-ink); white-space: nowrap;
}
.pay__drop {
  position: absolute; top: 23px; left: var(--at, 90%); width: 1px; height: 40px;
  background: linear-gradient(to bottom, var(--dx-red), rgba(200,16,46,0));
  transform: scaleY(0); transform-origin: 50% 0;
  transition: transform 220ms var(--dx-ease);
}
.pay[data-branch="1"] .pay__drop { transform: scaleY(1); }

/* GEOMETRY IS PERCENTAGE, NOT MEASURED PIXELS.
   The old build read getBoundingClientRect() on the node's dot every step
   and wrote a px --tx, which meant a layout read per beat and a debounced
   resize handler to re-measure. It is unnecessary: .pay__nodes is a
   space-between flex row of N equal 20% nodes, so node k's centre is
   exactly (2k+1)/(2N) of the track — 10/30/50/70/90% for five. --at is
   that percentage, and `left` on an absolutely-positioned element is not
   animated here (transform still carries the travel), so there is no
   layout thrash and nothing to re-measure on resize.

   THE TOKEN IS HIDDEN UNTIL data-live="1". With JS blocked the honest
   frame is the five named states of a payment with no playhead sitting
   wrongly at the far left. js/demos.js sets data-live when it mounts. */
.pay__token {
  position: absolute; top: 18px; left: var(--at, 90%);
  width: 17px; height: 17px; margin-left: -8.5px;
  border-radius: 50%;
  background: rgba(200,16,46,.16);
  border: 1px solid var(--dx-red);
  opacity: 0;
  transition: left 460ms var(--dx-ease), transform 460ms var(--dx-ease),
              opacity 200ms var(--dx-ease), background-color 300ms var(--dx-ease);
}
.pay[data-live="1"] .pay__token { opacity: 1; }
.tdx-row[data-open="1"] .pay__token { will-change: left, transform; }
.pay[data-branch="1"] .pay__token { transform: translateY(42px); }
.pay__token::after {
  content: ""; position: absolute; inset: 4px;
  border-radius: 50%; background: var(--dx-red);
}

/* THE AUTHORED SEQUENCE, AND THE LIVE LOG THAT REPLACES IT.
   [data-pay-script] is a real ordered list of every state in the markup.
   It is what a reader with the script blocked sees, and it is the whole
   section's text — so it is styled to be read, not hidden. js/demos.js
   sets data-live on mount, which swaps the list for the one-line log.
   Neither is ever removed from the DOM. */
.pay__script {
  list-style: none; margin: 0; padding: .8rem 0 0;
  border-top: 1px solid var(--dx-line);
  display: grid; gap: .45rem;
  font-family: var(--dx-mono); font-size: .65rem; letter-spacing: .03em;
  color: rgba(245,241,232,.62);
}
.pay__script b { font-weight: 500; color: var(--dx-bone); }
.pay__script [data-pay-fail] { color: var(--dx-red-ink); }
.pay[data-live="1"] .pay__script { display: none; }

.pay__log { display: none; }
.pay[data-live="1"] .pay__log {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: .5rem .9rem; align-items: baseline;
  border-top: 1px solid var(--dx-line);
  padding-top: .8rem;
  font-family: var(--dx-mono); font-size: .65rem; letter-spacing: .03em;
}
.pay__log dt { color: rgba(245,241,232,.52); }
.pay__log dd { margin: 0; color: var(--dx-bone); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pay__log dd.is-fail { color: var(--dx-red-ink); }
.pay__amt { font-variant-numeric: tabular-nums; color: rgba(245,241,232,.6); }
@media (max-width: 700px) {
  .pay__label { font-size: .48rem; }
  .pay__track { height: 112px; }
}
@media (max-width: 460px) {
  /* Below 460 the node labels cannot fit and are dropped, which leaves five
     unlabelled dots and a one-line log. So the authored script list stays
     VISIBLE here even while the demo runs: the dots carry the position, the
     list carries the names. Measured at 390 without this, the payment
     section read as five anonymous circles. */
  .pay__label { display: none; }
  .pay__track { height: 92px; }
  .pay__fail { top: 56px; }
  .pay[data-live="1"] .pay__script { display: grid; }
  .pay__script { font-size: .6rem; }
}

/* ── DEMO 03 · IDEA TO SCREENSHOTS ─────────────────────────────────────
   One phone, four stages, crossfading in place: the written brief, the
   grey wireframe, the built interface, and the App Store screenshot. The
   rail underneath is the same four stages as a progress track. Entering
   stages start at scale(.985), never at zero. */
.app { display: grid; grid-template-columns: 186px minmax(0, 1fr); gap: clamp(1.2rem, 3vw, 2.4rem); padding-top: 1.2rem; align-items: start; }
.app__phone {
  position: relative;
  width: 186px; height: 380px;
  border: 1px solid rgba(245,241,232,.22);
  border-radius: 22px;
  background: #0d0d0d;
  overflow: hidden;
  flex: 0 0 auto;
}
.app__phone::before {
  content: ""; position: absolute; top: 8px; left: 50%; width: 54px; height: 5px;
  margin-left: -27px; border-radius: 3px; background: rgba(245,241,232,.18); z-index: 4;
}
.app__screen {
  position: absolute; inset: 0;
  padding: 26px 14px 14px;
  opacity: 0; transform: scale(.985);
  transition: opacity 260ms var(--dx-ease), transform 300ms var(--dx-ease);
}
.tdx-row[data-open="1"] .app__screen { will-change: transform, opacity; }
.app__screen[data-on="1"] { opacity: 1; transform: none; }

.app__screen--brief { display: flex; align-items: center; }
.app__brief {
  font-family: var(--dx-mono); font-size: .585rem; line-height: 1.85;
  color: rgba(245,241,232,.66);
}
.app__brief b { display: block; color: var(--dx-red-ink); font-weight: 500; letter-spacing: .12em; margin-bottom: .7rem; font-size: .53rem; }

.app__wire { display: flex; flex-direction: column; gap: 7px; }
.app__wire i {
  display: block; background: rgba(245,241,232,.09);
  border: 1px dashed rgba(245,241,232,.16);
}
.app__wire i:nth-child(1) { height: 15px; width: 62%; }
.app__wire i:nth-child(2) { height: 76px; }
.app__wire i:nth-child(3) { height: 34px; }
.app__wire i:nth-child(4) { height: 34px; }
.app__wire i:nth-child(5) { height: 34px; }
.app__wire i:nth-child(6) { height: 26px; width: 48%; margin-top: 4px; }

.app__built { display: flex; flex-direction: column; gap: 7px; }
.app__built .app__built-label {
  display: block;
  font-family: var(--dx-serif); font-weight: 400; font-size: .82rem;
  color: var(--dx-bone); margin: 0 0 3px;
}
.app__built i {
  display: block; height: 34px;
  background: rgba(245,241,232,.055);
  border-left: 2px solid var(--dx-red);
}
.app__built i:first-of-type { height: 76px; background: rgba(200,16,46,.11); }
.app__built u {
  display: block; margin-top: 4px; height: 26px; width: 48%;
  background: var(--dx-red); text-decoration: none;
}

.app__shot { padding-top: 30px; }
.app__shot strong {
  display: block; font-family: var(--dx-serif); font-weight: 400;
  font-size: .95rem; line-height: 1.2; color: var(--dx-bone); margin-bottom: .35rem;
}
.app__shot small {
  display: block; font-family: var(--dx-sans); font-size: .58rem;
  line-height: 1.5; color: rgba(245,241,232,.55); margin-bottom: .8rem;
}
.app__shotui { display: flex; flex-direction: column; gap: 6px; }
.app__shotui i { display: block; height: 30px; background: rgba(245,241,232,.06); border-left: 2px solid var(--dx-red); }
.app__shotui i:first-child { height: 64px; background: rgba(200,16,46,.13); }

.app__steps { list-style: none; margin: 0; padding: 0; }
.app__step {
  position: relative;
  padding: .62rem 0 .62rem 1.5rem;
  border-bottom: 1px solid var(--dx-line-soft);
  font-family: var(--dx-mono); font-size: .645rem; letter-spacing: .04em;
  color: rgba(245,241,232,.52);
  transition: color 220ms var(--dx-ease);
}
.app__step::before {
  content: ""; position: absolute; left: 0; top: 1.02rem;
  width: 7px; height: 7px; border: 1px solid rgba(245,241,232,.3);
  transform: rotate(45deg) scale(.85);
  transition: transform 240ms var(--dx-ease), background-color 240ms var(--dx-ease), border-color 240ms var(--dx-ease);
}
.app__step[data-on="1"] { color: var(--dx-bone); }
.app__step[data-on="1"]::before { background: var(--dx-red); border-color: var(--dx-red); transform: rotate(45deg) scale(1); }
.app__step[data-done="1"] { color: rgba(245,241,232,.6); }
.app__step[data-done="1"]::before { background: rgba(245,241,232,.55); border-color: rgba(245,241,232,.55); }
.app__step { max-width: 74ch; }
.app__step b { display: block; font-weight: 400; color: inherit; }
.app__step em {
  display: block; font-style: normal; margin-top: .2rem;
  font-size: .62rem; color: rgba(245,241,232,.52);
}
.app__step[data-on="1"] em { color: rgba(245,241,232,.8); }
.app__step[data-done="1"] em { color: rgba(245,241,232,.58); }
@media (max-width: 560px) {
  .app { grid-template-columns: 1fr; justify-items: start; }
  .app__phone { width: 168px; height: 344px; }
}

/* ── DEMO 04 · MIGRATIONS ──────────────────────────────────────────────
   Real filenames from TNAADO Inc's own admin repo, applying in order, one
   at a time, with the schema version climbing as each one lands. This is
   what "every schema change is a numbered step" looks like; the sentence
   that used to make that claim in prose has been deleted. */
.mig { padding-top: 1.2rem; }
.mig__head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; padding-bottom: .55rem;
  border-bottom: 1px solid var(--dx-line);
  font-family: var(--dx-mono); font-size: .6rem;
  letter-spacing: .12em; text-transform: uppercase;
  color: rgba(245,241,232,.52);
}
.mig__ver { color: var(--dx-bone); font-variant-numeric: tabular-nums; letter-spacing: .06em; }
.mig__list { list-style: none; margin: 0; padding: 0; }
.mig__item {
  position: relative;
  display: grid; grid-template-columns: 1.1rem minmax(0, 1fr) auto;
  gap: .7rem; align-items: center;
  padding: .46rem 0 .46rem .55rem;
  border-bottom: 1px solid var(--dx-line-soft);
  font-family: var(--dx-mono); font-size: .625rem; letter-spacing: .01em;
  color: rgba(245,241,232,.52);
  transition: color 260ms var(--dx-ease);
}
.mig__item::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--dx-red);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 240ms var(--dx-ease);
}
.mig__item[data-state="applied"] { color: rgba(245,241,232,.9); }
.mig__item[data-state="applied"]::before { transform: scaleX(1); }
.mig__tick {
  width: 11px; height: 11px; position: relative;
  border: 1px solid rgba(245,241,232,.22);
  transition: border-color 240ms var(--dx-ease);
}
.mig__tick::after {
  content: ""; position: absolute; inset: 2px;
  background: var(--dx-red);
  transform: scale(.4); opacity: 0;
  transition: transform 240ms var(--dx-ease), opacity 200ms var(--dx-ease);
}
.mig__item[data-state="applied"] .mig__tick { border-color: rgba(200,16,46,.65); }
.mig__item[data-state="applied"] .mig__tick::after { transform: scale(1); opacity: 1; }
.mig__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mig__ms {
  font-variant-numeric: tabular-nums; color: rgba(245,241,232,.52);
  opacity: 0; transition: opacity 240ms var(--dx-ease);
}
.mig__item[data-state="applied"] .mig__ms { opacity: 1; }
@media (max-width: 560px) { .mig__item { font-size: .585rem; } }

/* ── DEMO 05 · SCHEDULED WORK ──────────────────────────────────────────
   A 24-hour clock with a playhead. Each lane is a real server-side
   function; each marker is when it actually runs. The log underneath
   prints as the playhead crosses. */
/* --job-lab is the width of the function-name column PLUS the grid gap. The
   ruler, the lane tracks and the playhead must all start at exactly that
   offset or the clock lies: before this was a variable the ruler ran the
   full width while the tracks began after the labels, so the 03:00 tick sat
   about 165px left of the 03:00 marker it was supposed to be labelling. If
   you change .job__lane's first column, change this with it — one number. */
.job { padding-top: 1.2rem; --job-lab: 10.3rem; }
.job__ruler {
  position: relative; height: 16px;
  margin-left: var(--job-lab);
  border-bottom: 1px solid var(--dx-line);
  margin-bottom: .5rem;
}
.job__ruler span {
  position: absolute; bottom: 2px;
  font-family: var(--dx-mono); font-size: .58rem; letter-spacing: .08em;
  color: rgba(245,241,232,.55); transform: translateX(-50%);
}
.job__ruler span:first-child { transform: none; }
.job__ruler span:last-child { transform: translateX(-100%); }

.job__lanes { position: relative; }
.job__lane {
  position: relative;
  display: grid; grid-template-columns: minmax(0, 9.5rem) minmax(0, 1fr);
  gap: .8rem; align-items: center;
  height: 30px;
  border-bottom: 1px solid var(--dx-line-soft);
}
.job__fn {
  font-family: var(--dx-mono); font-size: .625rem; letter-spacing: .01em;
  color: rgba(245,241,232,.6);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.job__track { position: relative; height: 100%; }
.job__hit {
  position: absolute; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: rgba(245,241,232,.16);
  left: var(--at, 0%);
  transition: background-color 220ms var(--dx-ease), transform 260ms var(--dx-ease);
}
.job__hit[data-fired="1"] { background: var(--dx-red); transform: scale(1.5); }
.job__hit::after {
  content: ""; position: absolute; inset: -5px;
  border: 1px solid var(--dx-red); border-radius: 50%;
  opacity: 0; transform: scale(.6);
  transition: opacity 420ms var(--dx-ease), transform 420ms var(--dx-ease);
}
.job__hit[data-fired="1"]::after { opacity: 0; transform: scale(2.1); }
.job__hit[data-flash="1"]::after { opacity: .9; transform: scale(.6); transition: none; }

/* THE PLAYHEAD IS A FULL-WIDTH SLED, TRANSLATED BY PERCENT.
   The old build measured .job__headwrap's width each cycle and animated
   transform to that many pixels. translateX(%) resolves against the
   element's OWN width, so making the sled 100% of the wrap turns "the end
   of the day" into translateX(100%) — no measurement, no re-measure on
   resize, and no wrong answer if the wrap is still laying out when the
   cycle starts. The visible hairline is the ::before at the sled's left
   edge. Hidden until data-live: with JS blocked the honest frame is the
   schedule itself, not a stray line parked at midnight. */
.job__head {
  position: absolute; top: 0; bottom: 0; left: 0; width: 100%;
  transform: translateX(var(--pct, 0%));
  opacity: 0;
  pointer-events: none;
}
.job[data-live="1"] .job__head { opacity: 1; }
.job__head::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 1px;
  background: rgba(245,241,232,.34);
}
.job__headwrap { position: absolute; left: var(--job-lab); right: 0; top: 0; bottom: 0; pointer-events: none; }
.job__log {
  margin: .9rem 0 0; min-height: 1.7rem;
  font-family: var(--dx-mono); font-size: .62rem; letter-spacing: .03em;
  color: rgba(245,241,232,.5);
}
.job__log b { color: var(--dx-bone); font-weight: 500; }
.job__log i { font-style: normal; color: var(--dx-red-ink); }
@media (max-width: 620px) {
  .job { --job-lab: 7.8rem; }        /* 7rem column + .8rem gap */
  .job__lane { grid-template-columns: minmax(0, 7rem) minmax(0, 1fr); }
  .job__fn { font-size: .585rem; }
}

