/* styles.css, scoped to #ttx so it outranks the HubSpot theme.
 * Generated -- edit styles.css and regenerate, never this file.
 * The wrapper element must be: <div id="ttx"> â€¦ </div>
 */

/* ---- page level: outside the wrapper, so not scoped ---- */
html, body {
  margin: 0;
  background-color: oklch(0.170 0.055 250) !important;
  overflow-x: clip;
}

/* TTXAgent â€” landing page
 *
 * Palette is anchored on the Glass House Systems brand blue (#1c75bc), taken
 * from their own stylesheet. The system encodes the product's subject matter:
 * BLUE is us â€” brand, chrome, calls to action. The WARM RAMP is the intrusion,
 * heating orange to red as a campaign moves toward command and control. Green
 * is reserved for validation PASS and is never spent on decoration.
 *
 * Every colour and font below is a token. Nothing in the markup carries a raw
 * value; if a new one is needed it gets a name here first.
 */

#ttx#ttx {
  /* --- ground and paper ------------------------------------------------ */
  --ground:        oklch(0.155 0.013 250);
  --ground-lift:   oklch(0.200 0.013 250);
  --paper:         oklch(0.975 0.003 250);
  --surface:       oklch(0.990 0.002 250);
  --surface-sunk:  oklch(0.955 0.004 250);

  --ink:           oklch(0.220 0.012 250);
  --ink-soft:      oklch(0.440 0.010 250);
  --ink-faint:     oklch(0.550 0.010 250);   /* 0.580 measured 3.98:1 on --paper; body-size floor is 4.5 */
  --ink-invert:    oklch(0.990 0.003 250);
  --ink-invert-2:  oklch(0.920 0.008 250);
  --ink-invert-3:  oklch(0.800 0.012 250);

  --rule:          oklch(0.900 0.005 250);
  --rule-invert:   oklch(0.300 0.014 250);

  /* --- brand: Glass House Systems --------------------------------------- */
  --brand:         oklch(0.549 0.136 248);   /* #1c75bc */
  --brand-lift:    oklch(0.720 0.130 248);
  --brand-press:   oklch(0.470 0.130 248);

  /* --- the intrusion ---------------------------------------------------- */
  --warm:          oklch(0.685 0.175 28);
  --warm-mid:      oklch(0.640 0.190 16);
  --adversary:      oklch(0.575 0.200 6);    /* text on LIGHT ground -- 0.600 measured 4.09:1 on --paper */
  --adversary-lift: oklch(0.700 0.175 6);    /* text on DARK ground -- 0.600 measures 4.45:1 there */
  /* the featured run has five stages, so the ramp needs five steps. Derived by
     mixing the three above rather than picking two new reds beside them, so the
     ramp stays one gradient and not five separate decisions. */
  --warm-2:        color-mix(in oklch, var(--warm), var(--warm-mid));
  --warm-4:        color-mix(in oklch, var(--warm-mid), var(--adversary));
  --pass:          oklch(0.500 0.130 155);
  --caution:       oklch(0.560 0.130 95);

  /* scrim, as channels so alpha can vary at the call site */
  --scrim: 9 17 30;

  /* --- type -------------------------------------------------------------- */
  /* Inter for both. It was Space Grotesk for display and IBM Plex Sans for
     body; one family now carries both, which is why the display size and
     weight do the separating instead of a change of voice. */
  --font-display: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-body:    "Inter", ui-sans-serif, system-ui, sans-serif;
  /* Inter for the technical labels too. It is not a monospace, so the
     event rows and log tables no longer align on a character grid --
     `font-variant-numeric: tabular-nums` keeps the FIGURES lined up,
     which is the part that was doing the work. */
  --font-mono:    "Inter", ui-sans-serif, system-ui, sans-serif;

  --text-hero:  clamp(1.95rem, 0.95rem + 4.60vw, 4.30rem);
  --text-h2:    clamp(1.65rem, 1.15rem + 2.05vw, 2.50rem);
  --text-h3:    clamp(1.00rem, 0.94rem + 0.28vw, 1.13rem);
  --text-lead:  clamp(0.98rem, 0.92rem + 0.30vw, 1.06rem);
  --text-body:  clamp(0.92rem, 0.89rem + 0.16vw, 1.00rem);
  --text-sm:    0.84rem;
  --text-micro: 0.66rem;

  /* --- space: 4pt -------------------------------------------------------- */
  --space-1: 4px;   --space-2: 8px;   --space-3: 12px;  --space-4: 16px;
  --space-5: 20px;  --space-6: 24px;  --space-8: 32px;  --space-10: 40px;
  --space-12: 48px; --space-14: 56px; --space-16: 64px; --space-20: 80px; --space-28: 112px;
  --space-36: 144px; --space-44: 176px;

  --radius-sm: 5px; --radius: 8px; --radius-lg: 12px; --radius-pill: 999px;

  --ease-out: cubic-bezier(0.16, 1, 0.30, 1);
  --dur-short: 140ms;
  --dur-mid: 260ms;

  --measure: 1180px;
}
/* -------------------------------------------------------------------------- */
#ttx#ttx *, #ttx#ttx *::before, #ttx#ttx *::after { box-sizing: border-box; }



#ttx#ttx {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

#ttx#ttx h1, #ttx#ttx h2, #ttx#ttx h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-style: normal;          /* headings are roman. always. */
  /* -0.028em was tuned for Space Grotesk, which sets loose. Inter is already
     tight at display sizes and went cramped at that value. Revert this one
     number to -0.028em if the old spacing is wanted back. */
  letter-spacing: -0.021em;
  line-height: 1.08;
  margin: 0;
  /* long compound words must break rather than push the page sideways */
  overflow-wrap: anywhere;
  min-width: 0;
}

#ttx#ttx p { margin: 0; }

#ttx#ttx a { color: var(--brand); text-decoration: none; }
#ttx#ttx a:hover { color: var(--brand-press); }

#ttx#ttx :focus-visible {
  /* appears instantly â€” never transitioned */
  outline: 2px solid var(--brand-lift);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

#ttx#ttx .wrap { max-width: var(--measure); margin-inline: auto; padding-inline: var(--space-6); }

#ttx#ttx .mono {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

#ttx#ttx .num { font-variant-numeric: tabular-nums; }
/* --- buttons -------------------------------------------------------------- */
#ttx#ttx .btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 13px 22px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: 0.91rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;         /* a CTA never wraps to two lines */
  cursor: pointer;
  transition: background-color var(--dur-short) var(--ease-out),
              border-color var(--dur-short) var(--ease-out),
              box-shadow var(--dur-short) var(--ease-out),
              transform 100ms var(--ease-out);
}
#ttx#ttx .btn:active { transform: translateY(1px); }
#ttx#ttx .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
/* The bloom runs DOWNWARD, and that is not a stylistic choice.

   White text on --brand measures 4.72:1 and the floor is 4.5, so the lightest
   this hue can go behind white type is L 0.560 -- eleven thousandths above
   --brand. There is no headroom to bloom UP into: any highlight bright enough
   to see would put the label under AA.

   So the base is darker than --brand and the bloom brings it back UP to exactly
   --brand at its peak. The brightest pixel behind the label is therefore the
   same colour the button was before, and every other pixel is darker -- the
   finish is free, contrast-wise. Measured worst case is unchanged at 4.72:1 and
   the median improves.

   Two blooms, not one: a single centred highlight reads as a gradient, two
   offset ones read as a lit object. Same move as .slab's mesh, same direction
   (upper-left primary, lower-right secondary), so the buttons belong to the
   dark blocks rather than looking imported.

   background-color is kept SEPARATE from background-image so hover still has
   something transitionable -- gradients do not interpolate. */
#ttx#ttx .btn--primary {
  color: var(--ink-invert);
  background-color: oklch(0.468 0.132 248);
  background-image:
    radial-gradient(125% 150% at 20% -12%, var(--brand) 0%, transparent 62%),
    radial-gradient(115% 175% at 92% 118%, oklch(0.515 0.150 258) 0%, transparent 64%);
  border-color: rgb(255 255 255 / 0.10);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.18),
    0 6px 16px -8px color-mix(in oklch, var(--brand) 62%, transparent);
}
#ttx#ttx .btn--primary:hover {
  color: var(--ink-invert);
  background-color: oklch(0.505 0.136 248);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.24),
    0 8px 22px -8px color-mix(in oklch, var(--brand) 78%, transparent);
}
#ttx#ttx .btn--primary:active { box-shadow: inset 0 1px 3px rgb(var(--scrim) / 0.30); }
/* The pale CTA is the same object in the other direction -- cool-white base,
   bloom toward white, so the two read as one family rather than two buttons
   that happen to sit near each other. Its text is --ground on near-white, so
   the ceiling problem does not apply here; the constraint is only that it must
   not go grey enough to look disabled. */
/* A pill, because it lives inside one. .nav__pill is fully rounded with a 7px
   inset, and .btn's 5px default put a hard-cornered rectangle inside that --
   the corners fought the container and the button read as pasted in rather than
   seated. Same reason .capture__go is a pill: it sits in the capture pill.

   The rule: a button inherits the shape of what encloses it. The topbar and the
   closing form are flat surfaces, so their buttons keep the 5px default. */
#ttx#ttx .btn--pale {
  border-radius: var(--radius-pill);
  padding: 9px 20px;
  background-color: oklch(0.945 0.004 250);
  background-image:
    radial-gradient(130% 160% at 22% -14%, var(--ink-invert) 0%, transparent 64%),
    radial-gradient(110% 170% at 94% 120%, oklch(0.905 0.010 250) 0%, transparent 62%);
  color: var(--ground); font-weight: 500;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.85),
    0 4px 14px -6px rgb(var(--scrim) / 0.45);
}
#ttx#ttx .btn--pale:hover {
  background-color: var(--ink-invert); color: var(--ground);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.9),
    0 6px 18px -6px rgb(var(--scrim) / 0.55);
}
/* --- hero ----------------------------------------------------------------- */
/* The hero is a scroll stage, not a band. The media pins to the viewport while
   the copy travels up over it, so the whole 4:3 frame gets used instead of a
   single 1040px slice â€” you scroll down through the landscape. */
/* height is 100vh PLUS A FIXED TRAVEL, not 220vh.

   At 220vh the stage was 2376px on a 1080-tall screen and 1452px on a 660-tall
   one, and the sequence is scrubbed against it -- so the same nine beats were
   crammed into 61% of the travel on the short screen. The agent arrived before
   the copy had finished clearing and the whole thing read as jumpy rather than
   deliberate.

   The pinned length is `height - 100vh`, so this makes it exactly --stage-travel
   at every viewport height, and the sequence runs at one speed for everyone.
   motion.js ends its trigger just inside that number; the two must stay in
   sync. */
#ttx#ttx { --stage-travel: 1150px; }
#ttx#ttx .hero { position: relative; height: calc(100vh + var(--stage-travel)); background: var(--brand-press); }

#ttx#ttx .hero__media { position: sticky; top: 0; height: 100vh; overflow: hidden; }

#ttx#ttx .hero__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 30%;
}
/* Scroll-linked pan. Progressive enhancement: without scroll timelines the
   image simply holds still behind the copy â€” the same effect minus the travel.
   No JS either way. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero__img {
      animation: hero-pan linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 220vh;
      will-change: object-position, transform;
    }
    @keyframes hero-pan {
      from { object-position: 50% 24%; transform: scale(1.05); }
      to   { object-position: 50% 88%; transform: scale(1); }
    }
  }
}
/* --- texture ---------------------------------------------------------------
   Sits between the image and the scrims, so the scrims still own contrast.
   Switch with data-texture on .hero. */
#ttx#ttx .hero__texture { position: absolute; inset: 0; pointer-events: none; }

#ttx#ttx .hero[data-texture="none"] .hero__texture { display: none; }
/* dot screen over the whole frame â€” the print / CRT quality */
#ttx#ttx .hero[data-texture="halftone"] .hero__texture {
  background-image: radial-gradient(circle at center, rgb(var(--scrim) / 0.45) 0.9px, transparent 1.05px);
  background-size: 3px 3px;
  opacity: 0.30;
}
/* sparse technical matrix â€” reads as instrument, not as print */
#ttx#ttx .hero[data-texture="grid"] .hero__texture {
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 0.62) 1.15px, transparent 1.35px);
  background-size: 22px 22px;
  opacity: 0.75;
}
/* horizontal rule lines â€” console / packet capture */
#ttx#ttx .hero[data-texture="scanlines"] .hero__texture {
  background-image: repeating-linear-gradient(to bottom,
    rgb(var(--scrim) / 0.34) 0 1px, transparent 1px 3px);
  opacity: 0.62;
}
/* film grain â€” material without pattern */
#ttx#ttx .hero[data-texture="grain"] .hero__texture {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 240px 240px;
  mix-blend-mode: overlay;
  opacity: 0.40;
}
/* both â€” dot screen carrying grain, closest to the Token Wrapped reference */
#ttx#ttx .hero[data-texture="halftone-grain"] .hero__texture {
  background-image:
    radial-gradient(circle at center, rgb(var(--scrim) / 0.60) 1.05px, transparent 1.25px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 4px 4px, 240px 240px;
  opacity: 0.58;
}
/* The copy band sits between roughly 5% and 76% of the stage, and the elements
   that need the most help -- subhead, capture pill, allowlist note -- are all in
   the 40-62% range, which is exactly where this used to have fallen away to
   0.34 and then 0.14. The extra weight is added in that band only: the top is
   unchanged, and it still clears to zero by 82% so the valley and the sunrise
   below the fold are untouched. Re-measure after any change -- see README. */
#ttx#ttx .hero__scrim-top {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgb(var(--scrim) / 0.80) 0%,
    rgb(var(--scrim) / 0.64) 18%,
    rgb(var(--scrim) / 0.54) 36%,
    rgb(var(--scrim) / 0.46) 52%,
    rgb(var(--scrim) / 0.30) 66%,
    rgb(var(--scrim) / 0) 82%);
}
#ttx#ttx .hero__scrim-bottom {
  position: absolute; inset-inline: 0; bottom: 0; height: 260px;
  background: linear-gradient(to bottom,
    rgb(var(--scrim) / 0) 0%,
    rgb(var(--scrim) / 0.62) 55%,
    rgb(var(--scrim) / 0.86) 100%);
}

#ttx#ttx .hero__inner { position: absolute; inset-inline: 0; top: 0; height: 100vh; }
/* --- ambient run chrome ------------------------------------------------------
   Real panels rather than pixels baked into the artwork: the values stay true,
   stay updatable, and never fabricate a number to fill a slot. They live inside
   .hero__inner so they scroll away with the copy, which keeps the clean
   full-frame moment further down the stage. */
#ttx#ttx .hud { position: absolute; inset: 0; pointer-events: none; }

#ttx#ttx .hud__card {
  position: absolute;
  text-align: left;                 /* .hero__copy centres its children */
  background: rgb(var(--scrim) / 0.34);
  border: 1px solid rgb(255 255 255 / 0.16);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  backdrop-filter: blur(20px) saturate(125%);
  -webkit-backdrop-filter: blur(20px) saturate(125%);
  box-shadow: 0 12px 38px rgb(var(--scrim) / 0.34),
              inset 0 1px 0 rgb(255 255 255 / 0.13);
  color: var(--ink-invert-2);
}
/* Laid out in the order they are read, not the order they were drawn. The
   sequence used to start on the RIGHT and zigzag right-left-right-left while
   travelling upward -- measured, and about as far from a reading path as four
   boxes can get. It now runs down the left column, then down the right:
   01 top-left, 02 below it, 03 top-right, 04 below that.

   THE COLUMNS OWN PLACEMENT; THE CARDS ONLY OWN THEIR WIDTH.
   Each card used to carry its own `top: NNvh`, which meant the gap between a
   stacked pair was whatever was left after the upper card had finished growing
   -- and the chain card does grow, because its last two technique labels wrap
   below about 1200px. At 1100 the pairs overlapped and the 02 and 04 step
   numbers disappeared under the card above. A flex gap cannot be eaten by
   content, so this cannot come back. */
#ttx#ttx .hud__col {
  position: absolute;
  display: flex; flex-direction: column;
  gap: clamp(12px, 2.6vh, 30px);
}
/* Top offsets are clamped, not plain vh. On a short viewport a flat 13vh left
   the right-hand column ending 1px inside the stats band -- which is 141px tall
   at narrow widths, not the ~88px it looks, because its labels wrap to two
   lines there. The clamp floors buy that back. */
#ttx#ttx .hud__col--left { left:  clamp(14px, 1.9vw, 44px); top: clamp(56px, 14vh, 170px); align-items: flex-start; }
#ttx#ttx .hud__col--right { right: clamp(14px, 1.9vw, 44px); top: clamp(42px, 11vh, 140px); align-items: flex-end; }

#ttx#ttx .hud__card { position: static; }
/* Widths track the viewport instead of stepping at breakpoints, so there is no
   width at which a card is suddenly the wrong size for the room it is in. */
#ttx#ttx .hud__card--chain { width: clamp(198px, 15.5vw, 250px); }
/* 01 */
#ttx#ttx .hud__card--stream { width: clamp(272px, 26.0vw, 392px); }
/* 02 */
#ttx#ttx .hud__card--valid { width: clamp(198px, 15.5vw, 250px); }
/* 03 */
#ttx#ttx .hud__card--run { width: clamp(200px, 16.0vw, 258px); }
/* 04 */
/* each panel enters from the edge it is anchored to; the copy still rises */
#ttx#ttx .hud__card--chain, #ttx#ttx .hud__card--stream { --rise-x: -16px; --rise-y: 6px; }
#ttx#ttx .hud__card--valid, #ttx#ttx .hud__card--run { --rise-x: 16px;  --rise-y: 6px; }
#ttx#ttx .hud__tag--a { --rise-x: -12px; --rise-y: 4px; }
#ttx#ttx .hud__tag--b { --rise-x: 12px;  --rise-y: 4px; }
/* The ordinal is the fix for the thing the animation could not carry: reveal
   order is invisible once every panel is up, and the held beat parks the reader
   on exactly that state. A number survives the animation. */
#ttx#ttx .hud__step {
  display: inline-block;
  margin-right: 7px;
  padding: 1px 4px;
  border-radius: 3px;
  background: rgb(255 255 255 / 0.16);
  color: var(--ink-invert);
  font-weight: 500;
  letter-spacing: 0.08em;
}

#ttx#ttx .hud__eyebrow {
  font-family: var(--font-mono); font-size: var(--text-micro);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--ink-invert-3);
}
#ttx#ttx .hud__title {
  font-family: var(--font-display); font-size: 1.05rem; font-weight: 600;
  letter-spacing: -0.015em; color: var(--ink-invert); margin-top: 2px;
}

#ttx#ttx .hud__rows { margin: var(--space-3) 0 0; display: flex; flex-direction: column; gap: 6px; }
#ttx#ttx .hud__rows > div { display: flex; justify-content: space-between; gap: var(--space-4); font-size: 0.76rem; }
#ttx#ttx .hud__rows dt { color: var(--ink-invert-3); }
#ttx#ttx .hud__rows dd { margin: 0; color: var(--ink-invert); font-family: var(--font-mono); }

#ttx#ttx .hud__checks { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
#ttx#ttx .hud__checks li { display: flex; justify-content: space-between; gap: var(--space-3); font-size: 0.76rem; }
#ttx#ttx .hud__checks span { color: var(--ink-invert-2); }
#ttx#ttx .hud__checks b { font-family: var(--font-mono); font-size: var(--text-micro); letter-spacing: 0.08em; color: oklch(0.80 0.13 155); }
#ttx#ttx .hud__checks .is-warn b { color: oklch(0.86 0.145 95); }
#ttx#ttx .hud__checks .is-warn span { color: oklch(0.92 0.055 95); }
#ttx#ttx .hud__foot {
  margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 1px solid rgb(255 255 255 / 0.14);
  font-family: var(--font-mono); font-size: var(--text-micro);
  letter-spacing: 0.08em; color: var(--ink-invert-3); white-space: nowrap;
}

#ttx#ttx .hud__stream { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
#ttx#ttx .hud__stream li {
  display: grid; grid-template-columns: 62px 22px 92px minmax(0, 1fr);
  gap: var(--space-2); align-items: baseline;
  font-family: var(--font-mono); font-size: 0.7rem;
}
#ttx#ttx .hud__stream time { color: var(--ink-invert-3); }
#ttx#ttx .hud__eid { color: color-mix(in oklch, var(--warm) 72%, white); }
#ttx#ttx .hud__stream span:not(.hud__eid) { color: var(--ink-invert-2); }
#ttx#ttx .hud__stream em {
  font-style: normal; color: var(--ink-invert);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

#ttx#ttx .hud__chain { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
#ttx#ttx .hud__chain li { display: flex; align-items: center; gap: var(--space-2); font-size: 0.74rem; }
#ttx#ttx .hud__chain i { width: 6px; height: 6px; border-radius: 50%; background: var(--n); flex: none; }
#ttx#ttx .hud__chain b { font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500; color: var(--ink-invert); }
#ttx#ttx .hud__chain span { color: var(--ink-invert-3); }
/* small floating host markers â€” depth, at almost no cost */
#ttx#ttx .hud__tag {
  position: absolute;
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: 5px 10px; border-radius: var(--radius-sm);
  background: rgb(var(--scrim) / 0.34);
  border: 1px solid rgb(255 255 255 / 0.13);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em;
  color: var(--ink-invert-2); white-space: nowrap;
}
#ttx#ttx .hud__tag span { color: var(--ink-invert-3); }
/* Pushed down and outward. They used to sit at 66-70vh directly under the
   agent, which is where its label has to go. */
/* Anchored into the corridor between the two columns, not to the frame -- at
   17% the right-hand tag slid under the run card once the columns started
   tracking the viewport.

   Anchored from the BOTTOM, not the top, because the thing they have to stay
   clear of is the stats band, which is bottom-anchored and up to 141px tall.
   `top: 78vh` put tag A five pixels inside it on a 726-tall viewport. */
/* Raised clear of the agent's own caption. At 24vh the tag's bottom-right
   corner landed 10px inside the .agent__id box on a 726-tall viewport -- the
   glyphs happened to miss because the caption is centred inside a wider box,
   which is not clearance, it is luck. */
#ttx#ttx .hud__tag--a { left: 30%;  bottom: clamp(196px, 27.5vh, 330px); }
#ttx#ttx .hud__tag--b { right: 26%; bottom: clamp(214px, 30vh, 380px); }
/* --- how far down each panel survives ----------------------------------------
   All four used to share one threshold, which fitted none of them exactly: it
   kept the widest panel 60px past where it stopped working and threw away the
   narrowest 170px before it had to.

   The usable gutter beside the centred headline is `(viewport âˆ’ 880) / 2`, the
   headline being clamped at 3.75rem above 1073px and so a fixed ~880px of ink.
   A panel needs `24px inset + its own width + 16px of air` inside that. Against
   those, the max-content width of each panel is what decides its floor:

     run     153px   â†’  survives to ~1270
     chain   232px   â†’  survives to ~1424
     valid   257px   â†’  the footer is a nowrap 215px string; wrapping it drops
                        the card to the widest check row and buys ~50px
     stream  430px   â†’  not bound by the headline at all â€” it sits at 49vh,
                        level with the lead paragraph, and that is the edge it
                        actually has to clear

   Each panel now leaves at its own number. */
/* Nothing is hidden between 1360 and 1500 any more, and that is a consequence
   of the reveal rather than a relaxation of the standard.

   The old thresholds existed because all four panels sat beside the copy at
   scroll 0 and had to clear the headline and the lead. They no longer do:
   panels 02, 03 and 04 arrive only after the copy has travelled past them, so
   the only thing they have to fit is the viewport. Re-measured at their own
   reveal moments and all three are clear at 1440 and at 1366.

   Panel 01 is the exception, because it is the one visible AT REST, so it still
   has to clear the headline at scroll 0: measured 62px of air at 1500, 32px at
   1440, and -5px at 1366. Hence the narrowing below 1440 -- and it applies to
   01 now, not to the panels that used to sit there.

   This matters for more than pixels: hiding a numbered step leaves a hole in
   the sequence, and "01, 03, 04" is worse than a slightly narrower card. */
@media (max-width: 1439px) {
  .hud__card--chain, .hud__card--valid, .hud__card--run { width: 200px; }
}
/* "9 pass Â· 1 warn Â· non-blocking" is 215px of nowrap text, which is wider than
   the 232px card's 190px content box â€” it was overflowing the right padding by
   5px at every width below 1600. Let it take two lines instead. */
@media (max-width: 1599px) {
  .hud__card--valid .hud__foot { white-space: normal; }
}
/* --- THE GATE ---------------------------------------------------------------
   This was 1360px wide / 760px tall, and it was simply wrong.

   A browser at any zoom above 100% on a 1440 laptop lands under it â€” which is a
   very large share of real visitors â€” and they got no panels, no agent, no
   wires, and a hero collapsed to one flat screen. The best moment on the page
   was switched off for the median viewer.

   The old reasoning was that panel 01 "leaves 27px beside the headline at
   1366". That measured a collision that cannot happen: the panels are at
   opacity 0 until 14% of the way through the stage, by which point the copy has
   scrolled several hundred pixels up and is nowhere near them. Measured with
   the gate lifted, the whole sequence runs at 1345x726 with no overlap
   anywhere.

   What actually stops working is room: the left column, the agent and the right
   column need about 980px between them, and two stacked cards need vertical
   space under the column's top offset. The gate is set at those real limits
   with a little margin.

   Keep these numbers in sync in THREE places: here, the .agent/.wires rule
   further down, and the guard at the top of motion.js. */
@media (max-width: 1015px), #ttx#ttx (max-height: 600px) {
  .hud { display: none; }
}
/* Nothing left to reveal, so the stage stops being a stage â€” otherwise it is a
   full extra screen of empty landscape before the seam card. 860px and below
   already sets height:auto and this must not fight it. */
@media (min-width: 861px) and (max-width: 1015px), #ttx#ttx (min-width: 861px) and (max-height: 600px) {
  .hero { height: 100vh; }
}

#ttx#ttx .nav { display: flex; justify-content: center; padding-top: var(--space-6); --rise-y: -12px; }
#ttx#ttx .nav__pill {
  display: flex; align-items: center; gap: var(--space-6);
  padding: 7px 7px 7px 18px;
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / 0.10);
  border: 1px solid rgb(255 255 255 / 0.20);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
#ttx#ttx .nav__brand { display: flex; align-items: center; gap: var(--space-2); }
#ttx#ttx .nav__word {
  font-family: var(--font-display); font-size: 0.91rem; font-weight: 600;
  color: var(--ink-invert); letter-spacing: -0.01em; white-space: nowrap;
}
#ttx#ttx .nav__links { display: flex; align-items: center; gap: var(--space-5); }
#ttx#ttx .nav__links a { font-size: var(--text-sm); color: var(--ink-invert-2); white-space: nowrap; }
#ttx#ttx .nav__links a:hover { color: var(--ink-invert); }
/* --- condensed header -------------------------------------------------------
   The nav pill belongs to the hero and leaves with it. This takes over once the
   stage is behind you, so the one action that matters stays reachable for the
   remaining four thousand pixels.

   It is revealed by a scroll timeline rather than a scroll handler: no JS, no
   listener to throttle, no state that can disagree with where the page actually
   is. Where scroll timelines are unsupported it simply never appears, which is
   the same progressive-enhancement stance the hero pan takes â€” the page still
   has the pill at the top and the form at the bottom.

   It starts visibility:hidden, not just transparent. An opacity-0 link is still
   in the tab order, and a keyboard user should not land on a header that is not
   on screen yet. */
#ttx#ttx .topbar {
  position: fixed; inset-inline: 0; top: 0; z-index: 40;
  background: rgb(255 255 255 / 0.82);
  border-bottom: 1px solid var(--rule);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  opacity: 0;
  visibility: hidden;
}

#ttx#ttx .topbar__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-6);
  padding-block: 11px;
}

#ttx#ttx .topbar__brand {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-display); font-size: 0.91rem; font-weight: 600;
  letter-spacing: -0.01em; color: var(--ink); white-space: nowrap;
}
#ttx#ttx .topbar__brand:hover { color: var(--ink); }
#ttx#ttx .topbar__brand svg { color: var(--brand); }

#ttx#ttx .topbar__cta { padding: 9px 15px; font-size: 0.86rem; }
/* the fixed bar would otherwise land on top of whatever an in-page link jumps to */
#ttx#ttx #coherence, #ttx#ttx #safety, #ttx#ttx #bundle, #ttx#ttx #request { scroll-margin-top: 88px; }

@supports (animation-timeline: scroll()) {
  #ttx#ttx .topbar {
    animation: topbar-in linear both;
    animation-timeline: scroll(root block);
    animation-range: 142vh 186vh;
  }
/* the hero is content-height below 860px, so the desktop stops mean nothing */
@media (max-width: 860px) {
    .topbar { animation-range: 64vh 104vh; }
  }
/* visibility is discrete, so it is flipped on its own stop rather than left
     to interpolate to the midpoint of the range */
@keyframes topbar-in {
    0%   { opacity: 0; visibility: hidden; translate: 0 -100%; }
    1%   { visibility: visible; }
    100% { opacity: 1; visibility: visible; translate: 0 0; }
  }
/* reduce still gets the header â€” it is an affordance, not decoration. It
     arrives by opacity alone, and note the global reduce rule cannot be relied
     on to stop this: animation-duration is ignored on a scroll timeline. */
@keyframes topbar-in-flat {
    0%   { opacity: 0; visibility: hidden; }
    1%   { visibility: visible; }
    100% { opacity: 1; visibility: visible; }
  }
  @media (prefers-reduced-motion: reduce) {
    #ttx#ttx .topbar { animation-name: topbar-in-flat; }
  }
}

#ttx#ttx .hero__copy { max-width: 1080px; margin-inline: auto; padding-top: var(--space-28); text-align: center; }
/* NO LOCAL SCRIM HERE. There was one -- a radial ellipse on .hero__copy::before
   -- and it was a bug that only showed on a wide display.
   A radial-gradient is painted inside its element's box and CLIPPED there. The
   ellipse was 64% x 68%, i.e. radii larger than half the box, so the gradient
   was still around 0.2 alpha when it hit the element's edge and got cut off
   square. On a 1440 laptop the surrounding photo is dark enough to hide it; at
   1800 there is bright sky either side and it reads as a dark card sitting on
   the photograph.
   The contrast work lives in .hero__scrim-top instead: it is full-bleed, so
   there is no box for a gradient to clip against and no edge can ever appear.
   If you ever put a local scrim back, its radii must be under 50% so the
   falloff completes before the edge. */
#ttx#ttx .eyebrow {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 6px 14px; border-radius: var(--radius-pill);
  border: 1px solid rgb(255 255 255 / 0.22);
  background: rgb(255 255 255 / 0.10);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--ink-invert); font-family: var(--font-mono);
  font-size: var(--text-micro); letter-spacing: 0.14em;
}
#ttx#ttx .eyebrow::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--warm); flex: none;
}

#ttx#ttx .hero h1 {
  font-size: var(--text-hero);
  line-height: 1.02;
  /* The hero sets its own tracking, tighter than the h1/h2/h3 rule because it
     is much larger. Moved with it when the face became Inter. */
  letter-spacing: -0.024em;
  color: var(--ink-invert);
  margin-top: var(--space-6);
  text-shadow: 0 2px 30px rgb(var(--scrim) / 0.45);
}
/* The setup sits one step back in tone; the turn carries full white. The gap is
   deliberately small -- 0.92 against 0.99 lightness -- because the job is to
   land the eye on the second half, not to make the first half look disabled.
   Both still measure well clear of AA over the copy scrim. */
#ttx#ttx .hero__setup, #ttx#ttx .hero__turn { display: block; }
#ttx#ttx .hero__setup { color: var(--ink-invert-2); }
/* leading between the beats, not inside them. At 390px the setup wraps to two
   lines and without this the three lines read as one run-on sentence -- the
   whole point of the headline is that it is two. */
#ttx#ttx .hero__turn { color: var(--ink-invert); margin-top: 0.09em; }
#ttx#ttx .hero__lead {
  font-size: var(--text-lead); line-height: 1.62;
  color: var(--ink-invert);
  max-width: 660px; margin: var(--space-5) auto 0;
  /* A shadow is glyph-shaped, so unlike a scrim it can never clip into a
     rectangle. It is also not counted by the WCAG contrast formula -- see the
     README; the measured number here is below AA and this carries the real
     readability until a backing is decided on. */
  text-shadow: 0 1px 2px rgb(var(--scrim) / 0.85),
               0 2px 10px rgb(var(--scrim) / 0.80),
               0 1px 28px rgb(var(--scrim) / 0.70);
  text-wrap: pretty;
}
#ttx#ttx .hero__actions { display: flex; align-items: center; justify-content: center; gap: var(--space-3); margin-top: var(--space-8); flex-wrap: wrap; }
#ttx#ttx .hero__actions .btn--primary { box-shadow: 0 6px 26px rgb(var(--scrim) / 0.42); }
/* --- hero capture ------------------------------------------------------------
   One field and one button in a single glass pill, so it reads as one control
   rather than a form dropped on a photograph. The pill carries the focus ring:
   the input's own outline is removed and replaced by one on the container, so
   the indicator surrounds what the eye treats as the control. */
#ttx#ttx .capture {
  display: flex; align-items: center; gap: var(--space-2);
  max-width: 470px; margin: var(--space-8) auto 0;
  padding: 6px;
  border: 1px solid rgb(255 255 255 / 0.22);
  border-radius: var(--radius-pill);
  background: rgb(var(--scrim) / 0.34);
  backdrop-filter: blur(20px) saturate(125%);
  -webkit-backdrop-filter: blur(20px) saturate(125%);
  box-shadow: 0 12px 38px rgb(var(--scrim) / 0.34),
              inset 0 1px 0 rgb(255 255 255 / 0.13);
  transition: border-color var(--dur-short) var(--ease-out);
}
#ttx#ttx .capture:hover { border-color: rgb(255 255 255 / 0.34); }
#ttx#ttx .capture:focus-within { outline: 2px solid var(--brand-lift); outline-offset: 3px; }

#ttx#ttx .capture__input {
  flex: 1 1 auto; min-width: 0;
  padding: 10px 8px 10px 16px;
  border: 0; background: transparent;
  color: var(--ink-invert);
  font-family: var(--font-body); font-size: 0.95rem; line-height: 1.4;
}
#ttx#ttx .capture__input::placeholder { color: var(--ink-invert-3); }
#ttx#ttx .capture__input:focus, #ttx#ttx .capture__input:focus-visible { outline: none; }

#ttx#ttx .capture__go { flex: none; border-radius: var(--radius-pill); padding: 11px 20px; }

#ttx#ttx .capture__note {
  display: inline-flex; align-items: flex-start; gap: 7px;
  text-align: left;
  margin-top: var(--space-4);
  font-size: var(--text-sm); color: var(--ink-invert);
  text-shadow: 0 1px 2px rgb(var(--scrim) / 0.85),
               0 2px 12px rgb(var(--scrim) / 0.78);
}
#ttx#ttx .capture__note svg { color: var(--brand-lift); flex: none; margin-top: 4px; }

#ttx#ttx .u-hidden {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* The stats rail used to live here: a sticky 100vh box holding four figures
   against the bottom of the first screen, with a scroll-timeline fade so it
   cleared before the seam card rose through the same space.

   All of it is gone. The four figures moved into the run card below, where they
   belong to the run they describe, and with them went a 141px band that was the
   binding constraint on how short a viewport the panel sequence could survive.
   If a band ever comes back here, note what it cost: it has to fade before the
   seam card enters at 113vh, because both are anchored to the hero's bottom
   edge and cannot be separated by timing. */
/* --- the hero rail: the pipeline, revealed -----------------------------------
   The four panels are the tool's own order -- start the campaign, resolve the
   chain, generate the telemetry, validate it. They used to arrive together in
   the entrance and then sit still for 220vh. Now they hold on a pinned rail and
   arrive one at a time as the stage descends, each wired back to the agent.

   Pinned exactly like .stats-rail, and for the same reason: sticky with a
   -100vh pull so it starts pinned rather than arriving at 100vh, releasing on
   the hero's bottom edge so it leaves with the image.

   As everywhere else on this page, EVERYTHING RENDERS FINISHED BY DEFAULT. Only
   [data-hero="live"] hides anything, and only motion.js sets it. Script blocked,
   reduced motion, narrow screen or a thrown exception all land on the hero the
   page has always had. */
#ttx#ttx .hud-rail {
  position: sticky; top: 0;
  height: 100vh;
  margin-top: -100vh;
  pointer-events: none;
}
/* the wires are drawn in CSS pixels from JS, so no viewBox */
#ttx#ttx .wires {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
  opacity: 0;                       /* static hero has no wires at all */
}
[data-hero="live"] #ttx#ttx .wires { opacity: 1; }
/* --draw is normalised 0..1 so a path whose length changes on resize cannot
   desync from a tween that captured the old length */
#ttx#ttx .wires path {
  stroke: rgb(255 255 255 / 0.55);
  stroke-width: 1;
  stroke-dasharray: var(--len, 1);
  stroke-dashoffset: calc(var(--len, 1) * (1 - var(--draw, 0)));
}
#ttx#ttx .wires circle { fill: rgb(255 255 255 / 0.65); }
/* The agent. An aperture, not a mascot: the page already has four connector
   fans that converge on and disperse from a single node, so this is drawn in
   the same hairline vocabulary. Placeholder geometry -- site/assets/agent.svg
   drops in keeping these ids. */
#ttx#ttx .agent {
  position: absolute;
  left: 50%; top: 54%;
  width: 252px; height: 252px;
  transform: translate(-50%, -50%);
  color: rgb(255 255 255 / 0.96);
  opacity: 0;
  /* The machine sits over the brightest part of the frame -- pale sky and haze
     -- and white hairlines simply vanished there. Every other element floating
     on this hero carries its own surface (the panels are dark glass, the
     eyebrow is a bordered pill); this is the same move, a soft disc rather than
     a box so the chassis reads without drawing an edge around it. */
  background: radial-gradient(circle at center,
    rgb(var(--scrim) / 0.46) 0%,
    rgb(var(--scrim) / 0.30) 46%,
    rgb(var(--scrim) / 0) 72%);
  border-radius: 50%;
}
/* Every rotating part pivots on the chassis centre. This is the ONLY place the
   origin is declared -- motion.js deliberately does not use GSAP's svgOrigin,
   because setting both made the origin compound on each scrub re-render until
   the iris carried a translation of 1.5 quadrillion px and simply vanished.
   For an SVG element transform-box defaults to view-box, so 100px means 100
   viewBox units: the centre, at any rendered size. */
#ttx#ttx #agent-gyro-a, #ttx#ttx #agent-gyro-b, #ttx#ttx #agent-head, #ttx#ttx #agent-aim, #ttx#ttx #agent-iris, #ttx#ttx #agent-core, #ttx#ttx .agent__pulse {
  transform-origin: 100px 100px;
}
#ttx#ttx .agent__pulse { fill: none; opacity: 0; }
/* Says what the machine is. Without it a stranger sees a reticle emitting
   cards and has no way to know it stands for the agent. */
#ttx#ttx .agent__id {
  position: absolute; isolation: isolate;
  left: 50%; top: calc(54% + 150px);
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  font-size: var(--text-micro); letter-spacing: 0.14em;
  color: var(--ink-invert);
  white-space: nowrap;
  text-transform: uppercase;
  opacity: 0;
  text-shadow: 0 1px 10px rgb(var(--scrim) / 0.8);
}
#ttx#ttx .agent__id span {
  color: var(--ink-invert); letter-spacing: 0.1em;
  text-shadow: 0 1px 2px rgb(var(--scrim) / 0.85), 0 2px 10px rgb(var(--scrim) / 0.75);
}
/* Every other floating label in the HUD sits on a glass pill; this one sits on
   bare photograph, over the brightest part of the valley, and measured 2.66:1.
   A soft radial rather than a pill, so it does not read as another card.

   RADII UNDER 50%. 42% x 46% means the gradient reaches full transparency
   inside its own box and there is nothing for the element edge to clip -- which
   is exactly what went wrong with the hero copy scrim. Do not raise these. */
#ttx#ttx .agent__id::before {
  content: ""; position: absolute; z-index: -1;
  inset: -70% -34%;
  background: radial-gradient(42% 46% at 50% 50%,
    rgb(var(--scrim) / 0.62) 0%,
    rgb(var(--scrim) / 0.40) 55%,
    rgb(var(--scrim) / 0) 100%);
  pointer-events: none;
}
/* live: the rail owns the reveal, so the entrance animation is switched off.
   A running CSS animation outranks the inline styles GSAP writes -- leaving
   .rise on would pin every card at opacity 1 and the reveal would silently do
   nothing. */
[data-hero="live"] #ttx#ttx .hud__card, [data-hero="live"] #ttx#ttx .hud__tag {
  animation: none !important;
  opacity: 0;
}
/* same gate as .hud above and the guard in motion.js â€” all three must agree */
@media (max-width: 1015px), #ttx#ttx (max-height: 600px) {
  .agent, .wires { display: none; }
}
/* --- the seam ----------------------------------------------------------------
   Straight from Valley: the image does not dissolve into the page, it stops on a
   hard edge and a card straddles the boundary. Fading a dark photograph into a
   light page always routes through a muddy mid-grey; overlapping an object
   across the cut hides the join and gives the eye something to hold instead. */
/* The pull and the clearance the hero must leave for it are ONE number. They
   were two, in blocks 1200 lines apart, and on a phone -- where the hero is
   content-height and its last line sits flush on the bottom edge -- the seam
   painted straight over "Every request is read by a person." */
#ttx#ttx { --seam-pull: 58px; }
#ttx#ttx .seam { position: relative; z-index: 1; margin-top: calc(-1 * var(--seam-pull)); }

#ttx#ttx .seam__card {
  max-width: 940px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: 0 18px 48px rgb(var(--scrim) / 0.22);
  padding: var(--space-6) var(--space-8);
  text-align: center;
}

#ttx#ttx .seam__label { color: var(--ink-faint); }
/* --- the run's dimensions ----------------------------------------------------
   This is the proof slot -- the position every competitor fills with a logo
   wall. Five figures, each one read off the committed bundle, each one asserted
   by the provenance suite.

   The numbers are display-face and large because they are the argument; the
   icons are hairline and faint because they are only there to give each figure
   a shape the eye can come back to. If an icon starts competing with its number
   it has been drawn too heavily. */
#ttx#ttx .runfig {
  margin: var(--space-6) 0 0;
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--rule);
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-4);
}
#ttx#ttx .runfig__item { display: flex; flex-direction: column; align-items: center; gap: 4px; }
#ttx#ttx .runfig__i { color: var(--ink-faint); opacity: 0.7; }
#ttx#ttx .runfig__n {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(1.3rem, 0.95rem + 0.95vw, 1.7rem);
  letter-spacing: -0.02em; color: var(--ink); line-height: 1.1;
  white-space: nowrap;
}
#ttx#ttx .runfig__l { margin: 0; color: var(--ink-faint); text-align: center; }

@media (max-width: 760px) {
  #ttx#ttx .runfig { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5) var(--space-3); }
}
@media (max-width: 420px) {
  #ttx#ttx .runfig { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* A wrapping flex row could not hold five techniques and four arrows in 940px at
   any width â€” the last stage always broke to its own line. A fixed track per
   stage with the arrows in the gutters cannot wrap, and stacking the id over the
   tactic is what buys back the width. */
#ttx#ttx .seam__chain {
  list-style: none; margin: var(--space-6) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr) auto) minmax(0, 1fr);
  align-items: start; gap: var(--space-3);
}
#ttx#ttx .seam__stage { display: flex; flex-direction: column; align-items: center; gap: 3px; text-align: center; }
#ttx#ttx .seam__id { display: flex; align-items: center; gap: 6px; }
#ttx#ttx .seam__chain i { width: 7px; height: 7px; border-radius: 50%; background: var(--n); flex: none; }
#ttx#ttx .seam__chain b { font-family: var(--font-mono); font-size: 0.82rem; font-weight: 500; color: var(--ink); }
#ttx#ttx .seam__tac { font-size: 0.86rem; line-height: 1.35; color: var(--ink-soft); text-wrap: balance; }
/* level with the mono ids, not with the middle of a two-line stage */
#ttx#ttx .seam__arrow { display: flex; align-self: start; margin-top: 5px; color: var(--rule); }
/* Below this the five tracks stop fitting, so the chain becomes a vertical one
   rather than a wrap â€” same reading, turned 90 degrees, arrows intact. */
@media (max-width: 760px) {
  .seam__chain { grid-template-columns: 1fr; gap: var(--space-2); justify-items: center; }
  .seam__arrow { align-self: center; margin-top: 0; rotate: 90deg; }
}

#ttx#ttx .seam__foot {
  margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono); font-size: var(--text-micro);
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--ink-faint);
}
/* --- section links ----------------------------------------------------------
   Four junctions, four different shapes. Each one connects different things and
   is drawn to say so â€” the same fan repeated four times is a pattern applied,
   not a design.

     caps â†’ 01   five strands CONVERGE to one point over the lineage panel.
                 Five stages above, one process tree below.
     01 â†’ 02     one stem SORTS into six blue and two red, all eight landing
                 inside the campaign record. Six fields computed, two written.
     02 â†’ 03     one run DISPERSES into seven rows of a SIEM that is not ours;
                 three of the seven are the campaign.
     03 â†’ 04     nine strands COLLECT into one node, one per file in the run
                 folder below. It closes where the page closes.

   TWO RULES, both learned from getting them wrong.

   1. A connector never claims a source. Only the destination is asserted. The
      top edge used to carry node dots, which read as "these come out of the
      thing above" â€” five dots under a four-card grid, nine under five SIEM
      names. The counts were right about what they point INTO and wrong about
      what sat above them. The dots are gone; the strands now fade in from the
      top edge and only the arrowhead makes a claim.

   2. A connector aligns with the element it points at, not with the page. The
      first one converged on x=713 â€” the exact centre of the 80px gutter between
      a heading and a figure. It pointed at nothing. .link is now the same grid
      as .section__split, so --col1 / --col2 put the fan over the real column and
      stay right if the column widths ever change.

   Counts are never decorative â€” 5, 6+2, 4+3, 9 all match what is actually in
   the section they point at. */
/* The wrap's inline padding is carried as real grid tracks rather than as
   padding. It has to be: for an absolutely positioned grid child, a grid area
   that ends on the FIRST or LAST line is resolved to the container's padding
   edge, not its content edge â€” so a fan in a padded two-column grid landed 12px
   (half the padding) off its column. Making the padding into tracks 1 and 5
   puts both content columns on interior lines, where no such rule applies, and
   tracks 2 and 4 then measure exactly the same 526px as .section__split. */
#ttx#ttx .link {
  position: relative; height: 0; z-index: 1;
  max-width: var(--measure); margin-inline: auto;
  display: grid;
  grid-template-columns:
    var(--space-6) minmax(0, 1fr) var(--space-20) minmax(0, 1fr) var(--space-6);
}
#ttx#ttx .link .fan {
  position: absolute; top: 4px; left: 50%; translate: -50% 0;
  grid-column: 2 / 5; max-width: 100%; height: auto; overflow: visible;
}
/* Both lines, always. `grid-column: 2` leaves the end line `auto`, and for an
   absolutely positioned item that resolves to the grid container's end padding
   edge â€” so left:50% measured against 1156px instead of 526px and the fan came
   out 145px right of its column. */
#ttx#ttx .link--col1 .fan { grid-column: 2 / 3; }
#ttx#ttx .link--col2 .fan { grid-column: 4 / 5; }

#ttx#ttx .fan__stem { stroke: oklch(0.815 0.010 250); stroke-width: 1; }
#ttx#ttx .fan__node { fill: var(--paper); stroke: oklch(0.815 0.010 250); stroke-width: 1.2; }
#ttx#ttx .fan__node--collect { fill: var(--brand); stroke: var(--brand); }
#ttx#ttx .fan__node--stage1 { stroke: var(--warm); }
#ttx#ttx .fan__node--stage2 { stroke: var(--warm-mid); }
#ttx#ttx .fan__node--stage3 { stroke: var(--adversary); }

#ttx#ttx .fan__p, #ttx#ttx .fan__h { stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; }

#ttx#ttx .fan__p--dim, #ttx#ttx .fan__h--dim { stroke: oklch(0.815 0.010 250); }
#ttx#ttx .fan__p--soft, #ttx#ttx .fan__h--soft { stroke: color-mix(in oklch, var(--brand) 42%, transparent); }
#ttx#ttx .fan__p--brand, #ttx#ttx .fan__h--brand { stroke: color-mix(in oklch, var(--brand) 78%, transparent); }
#ttx#ttx .fan__p--red, #ttx#ttx .fan__h--red { stroke: color-mix(in oklch, var(--adversary) 82%, transparent); }
#ttx#ttx .fan__p--stage1, #ttx#ttx .fan__h--stage1 { stroke: var(--warm); }
#ttx#ttx .fan__p--stage2, #ttx#ttx .fan__h--stage2 { stroke: var(--warm-mid); }
#ttx#ttx .fan__p--stage3, #ttx#ttx .fan__h--stage3 { stroke: var(--adversary); }
/* dashed reads as signal, solid as flow */
#ttx#ttx .fan__p--dim { stroke-dasharray: 3 4; }
#ttx#ttx .fan__p--soft { stroke-dasharray: 5 5; }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    #ttx#ttx .fan__p, #ttx#ttx .fan__h, #ttx#ttx .fan__node {
      opacity: 0;
      animation: fan-in linear both;
      animation-timeline: view();
      animation-range: entry var(--a, 8%) entry var(--b, 40%);
    }
    #ttx#ttx .fan__stem { transform-origin: top; scale: 1 0; animation: fan-stem linear both; animation-timeline: view(); animation-range: entry 2% entry 20%; }
    @keyframes fan-in   { to { opacity: 1; } }
    @keyframes fan-stem { to { scale: 1 1; } }
  }
}
/* .section__split is one column from here down, so there is no column to align
   with â€” every connector goes back to the page centre. */
@media (max-width: 860px) {
  .link--col1 .fan, .link--col2 .fan { grid-column: 2 / 5; }
  .link .fan { width: 320px; }
}
@media (max-width: 560px) { #ttx#ttx .link .fan { width: 208px; } }
/* --- page ground -------------------------------------------------------------
   Valley keeps a dotted grid under every light section, which stops flat white
   reading as empty. Same dot idiom as the hero texture, so the two surfaces feel
   related. .band paints its own background over it. */
#ttx#ttx main {
  background-image: radial-gradient(oklch(0.875 0.006 250) 1px, transparent 1px);
  background-size: 24px 24px;
}

@media (max-width: 560px) {
  #ttx#ttx { --seam-pull: 34px; }
  #ttx#ttx .seam__card { padding: var(--space-5); text-align: left; }
/* The chain was a wrapping flex row here and the arrows were dropped because
     horizontal arrows in a vertical list said nothing. The 760px rule above
     already stacks it as a real chain with the arrows turned, so this only has
     to follow the card's left alignment -- hiding the arrows now would remove
     the one thing making it read as a sequence on the smallest screens. */
#ttx#ttx .seam__chain { justify-items: start; }
/* each stage centres its id against its tactic; left-aligned in a left-aligned
     card that reads as a ragged indent, so flatten it here too */
#ttx#ttx .seam__stage { align-items: flex-start; text-align: left; }
  #ttx#ttx .seam__arrow { margin-left: 2px; }
}
/* --- sections ------------------------------------------------------------- */
#ttx#ttx .section { padding-top: var(--space-44); }
#ttx#ttx .section--tight { padding-top: var(--space-36); }
/* align-items: start, not center.

   Centred, a short text column floats against a tall figure -- measured 98px of
   void above the copy in section 05 and 165px in section 06, which is what made
   both read as half-empty. Top-aligned gives the two columns a shared shoulder,
   which is a stronger compositional device than centring, and moves the
   remaining slack below the text where the eye is not looking for it. */
#ttx#ttx .section__split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-20); align-items: start; }
#ttx#ttx .section h2 { font-size: var(--text-h2); text-wrap: balance; }
/* :not([class]) is the fix for a bug this file kept producing.

   `.section p` is specificity 0,1,1 and sets both size and colour, so EVERY
   classed paragraph inside a section silently lost to it -- .panel__foot
   rendered --ink-soft on --ground at 2.51:1, .rec__foot and .tree2__foot lost
   their mono treatment for lead size, and .route__label rendered at 17px
   instead of 10.5. Four separate bugs, one cause, and each was fixed
   individually before the pattern was obvious.

   Raising specificity on each component is the wrong fix -- the next component
   makes the same mistake. This makes the selector simply NOT MATCH a classed
   paragraph, so a component's own rule applies unopposed regardless of where it
   sits in this file or how specific it is.

   Consequence: a classed paragraph inside .section now styles itself
   completely. `.turn` is the one that wanted these defaults, so it states them. */
#ttx#ttx .section p:not([class]) { font-size: var(--text-lead); line-height: 1.68; color: var(--ink-soft); margin-top: var(--space-5); text-wrap: pretty; }
/* .panel carries the padding only -- ground, mesh, noise, border and highlight
   all come from .slab now. It was its own flat dark treatment, which made three
   different dark blocks on one page; this makes it one system. */
#ttx#ttx .panel { padding: var(--space-8); }
#ttx#ttx .slab .panel__label { color: var(--ink-invert-3); margin-bottom: var(--space-5); }
/* Two classes. `.section p` is 0,1,1 and sets both size and colour, so a
   single-class rule here lost -- the footer rendered --ink-soft on --ground at
   2.51:1 and had done since it was written. Third instance of this trap on this
   page; see the sweep in the README. */
#ttx#ttx .slab .panel__foot {
  font-size: var(--text-sm); line-height: 1.6;
  color: var(--ink-invert-2); margin-top: var(--space-5);
}

#ttx#ttx .lineage { display: flex; flex-direction: column; gap: 3px; font-family: var(--font-mono); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
#ttx#ttx .lineage__row { display: flex; gap: var(--space-3); padding: 7px 0; color: var(--ink-invert-3); }
#ttx#ttx .lineage__pid { width: 44px; text-align: right; flex: none; }
#ttx#ttx .lineage__row--hit { padding-inline: 10px; margin-inline: -10px; border-radius: var(--radius-sm); background: oklch(0.245 0.038 26); }
#ttx#ttx .lineage__row--hit .lineage__pid { color: var(--ink-invert-2); }
#ttx#ttx .lineage__proc--warm { color: oklch(0.80 0.15 26); }
#ttx#ttx .lineage__row--c2 { background: oklch(0.24 0.028 28); }
#ttx#ttx .lineage__proc--adv { color: oklch(0.80 0.14 28); }
#ttx#ttx .lineage__tech { margin-left: auto; font-size: var(--text-micro); color: oklch(0.60 0.12 26); }
#ttx#ttx .lineage__tech--adv { color: oklch(0.62 0.10 28); }

#ttx#ttx .card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius-lg); padding: var(--space-8); }
/* --- capability grid ---------------------------------------------------------
   States what the tool does before any of the arguments for it. Deliberately
   plain: no card chrome, no icons, just a rule, a number and two lines. The
   sections below carry the evidence; this one only has to be scannable.

   Copy is capped near 18 words a cell. Anything longer and it stops being a
   summary and starts being another essay, which is the failure this section
   exists to fix. */
/* padding-bottom balances the connector below: .link is height 0 and its fan
   hangs 4px under whatever precedes it, so with no padding here the strands
   started 4px under "Hand it over" and read as coming out of that card. Now the
   fan sits in open ground, ~22px clear either side. */
/* --- 03 the problem, and the turn --------------------------------------------
   Two tracks on ONE four-column grid. The second track puts its two steps in
   columns 1 and 4, so the long empty run of rail between them is the whole
   argument -- that stretch is what you skip. Give both tracks their own grid
   and the comparison stops working, because the nodes no longer line up.

   Built in HTML, not as one SVG. A fixed viewBox would scale the step labels
   down with the diagram and they would be unreadable on a phone; this reflows
   to two columns instead. Same reason the caps rail is CSS. */
#ttx#ttx .turn {
  font-size: var(--text-lead); line-height: 1.68; margin-top: var(--space-5);
  text-wrap: pretty; color: var(--ink-soft);
}
#ttx#ttx .turn strong {
  font-family: var(--font-display); font-weight: 600;
  font-size: 1.06em; color: var(--ink); letter-spacing: -0.015em;
}
/* --- 10 three ways teams use a run ------------------------------------------
   Structurally different from .caps on purpose. That grid is numbered with icon
   tiles because it is a sequence; this one is three alternatives, so it gets a
   hairline top rule per item and an eyebrow naming the axis instead.

   The axis is what makes these three distinct rather than three phrasings of
   "run an exercise": content, process, people. Without it, detection
   engineering and tabletop exercises read as the same use case twice -- which
   is exactly what the first draft did. */
#ttx#ttx .uses { padding-top: var(--space-28); }
#ttx#ttx .uses h2 { font-size: var(--text-h2); text-wrap: balance; max-width: 20ch; }

#ttx#ttx .uses__grid {
  list-style: none; margin: var(--space-12) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-10);
}
#ttx#ttx .uses__item {
  display: grid; align-content: start; gap: var(--space-2);
  padding-top: var(--space-5);
  border-top: 1px solid var(--ink);
}
#ttx#ttx .uses__axis { color: var(--brand); }
#ttx#ttx .uses__item h3 {
  font-family: var(--font-display); font-size: var(--text-h3); font-weight: 600;
  letter-spacing: -0.02em; margin: var(--space-1) 0 0;
}
#ttx#ttx .uses__item p:last-child {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm); line-height: 1.62; color: var(--ink-soft); text-wrap: pretty;
}
/* 3 -> 1, never 3 -> 2: two columns orphans one item on a second row */
@media (max-width: 900px) {
  .uses__grid { grid-template-columns: 1fr; gap: var(--space-8); }
}
/* --- .slab : the dark artifact block -----------------------------------------
   The rule this page follows: LIGHT GROUND FOR THE ARGUMENT, DARK GROUND FOR
   THE ARTIFACT. The lineage panel, the ingest band and the closing block all
   work that way; this is that treatment made reusable so the next section that
   needs it does not reinvent the recipe.

   Three layers, cheapest first:
     ::before  a blue mesh -- three soft blooms in the brand hue, low alpha, so
               the block reads as lit rather than as a flat swatch
     ::after   fine noise from an inline feTurbulence, on soft-light, which is
               what stops a large dark rectangle looking like dead ink
     inset     a 1px top highlight, the same glass cue .hud__card uses

   No backdrop-filter. There is nothing behind a slab but flat page, so a blur
   would cost a compositor layer and render identically. The glass here is the
   highlight and the border, not a filter.

   USING IT
   - add `slab` alongside the component class: `class="route slab fig"`
   - text inside must use the invert tokens; .slab sets the common ones below
   - measure contrast against the BRIGHTEST point of the mesh, not the bare
     ground -- that is what a label may actually sit on
   - `.slab--flip` mirrors the blooms, so two slabs in view do not look stamped
   - `.fig { margin: 0 }` is 0,1,0 and sits late in this file, so a slab that is
     also a .fig needs two classes to set its own spacing. See .fig.slab below. */
#ttx#ttx .slab {
  position: relative; isolation: isolate;
  /* CLIP, NOT HIDDEN. Both clip the mesh and noise to the rounded corners, but
     `hidden` also makes the slab a SCROLL CONTAINER -- and a view() timeline
     resolves against its nearest scrollport, so every scroll-driven reveal
     inside a slab was measuring against a box that never scrolls. All 7
     .log__row, all 4 .tree__row and .close__body sat at opacity 1 permanently:
     the reveal had never once run on them. `clip` does the same clipping and
     establishes no scroll container. */
  overflow: clip;
  border-radius: var(--radius-lg);
  background: var(--ground);
  border: 1px solid rgb(255 255 255 / 0.08);
  box-shadow: 0 18px 48px rgb(var(--scrim) / 0.20),
              inset 0 1px 0 rgb(255 255 255 / 0.10);
  color: var(--ink-invert-2);
}
#ttx#ttx .slab::before, #ttx#ttx .slab::after { content: ""; position: absolute; inset: 0; pointer-events: none; z-index: -1; }

#ttx#ttx .slab::before {
  background:
    radial-gradient(58% 78% at 10% 12%,  color-mix(in oklch, var(--brand) 34%, transparent) 0%, transparent 62%),
    radial-gradient(52% 66% at 92% 26%,  color-mix(in oklch, var(--brand-lift) 20%, transparent) 0%, transparent 64%),
    radial-gradient(66% 84% at 74% 104%, color-mix(in oklch, var(--brand) 26%, transparent) 0%, transparent 66%);
}
#ttx#ttx .slab--flip::before { scale: -1 1; }
/* fractalNoise rather than turbulence: turbulence is directional and reads as
   fabric. stitchTiles keeps the 160px tile from showing its seams. */
#ttx#ttx .slab::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: 0.16; mix-blend-mode: soft-light;
}
/* Common inversions, so a slab's contents do not each have to remember.

   :where() so these contribute ZERO specificity. They are defaults, not rules,
   and a component must always win without caring where it sits in this file --
   `.tree__box b` currently beats `.slab b` only because it is declared later,
   which is not something to rely on. With :where() the slab layer is 0,1,0 and
   any component selector outranks it outright. */
#ttx#ttx .slab :where(.mono) { color: var(--ink-invert-3); }
#ttx#ttx .slab :where(strong, #ttx#ttx b) { color: var(--ink-invert); }
/* A slab used as a figure has to beat `.fig { margin: 0 }`, which is the same
   0,1,0 specificity and sits later in this file. This was a real bug: the route
   block wanted 40px of air above it and rendered flush against the paragraph. */
#ttx#ttx .fig.slab { margin-block: var(--space-14) 0; }
/* --- 03 the problem, and the turn: the two-track diagram --------------------- */
#ttx#ttx .route {
  padding: var(--space-10) clamp(var(--space-5), 3vw, var(--space-10));
  display: grid; gap: var(--space-8);
}
#ttx#ttx .route__track { position: relative; }
/* font-size is restated, not inherited from .mono. These are <p> elements
   inside .section, and `.section p { font-size: var(--text-lead) }` is 0,1,1
   while `.mono` is 0,1,0 -- so .mono loses and the labels rendered at 17px
   instead of 10.5px. Any mono <p> added inside a .section has the same problem.
   Two-class selectors here so the point cannot be lost again. */
#ttx#ttx .route .route__label {
  font-size: var(--text-micro);
  color: var(--ink-invert-3); margin-bottom: var(--space-5);
}

#ttx#ttx .route__steps {
  position: relative;
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-4);
}
/* The rail stops ON the last node rather than running to the container edge --
   an open-ended line reads as "and it continues", which is the opposite claim.

   Both tracks end on the column-4 dot, so the geometry is the same for both.
   With four columns and gap g, column 4 starts at 3c + 3g where c = (100% - 3g)/4,
   which reduces to 75% + 0.75g; the dot centre is 5.5px past that. Hence
   right: 25% - 0.75g - 5.5px. Do not eyeball this as a round percentage -- the
   gaps make 12.5% wrong by about 18px. */
#ttx#ttx .route__steps::before {
  content: ""; position: absolute; z-index: 0;
  top: 5px; height: 1px;
  left: 5.5px;
  right: calc(25% - 0.75 * var(--space-4) - 5.5px);
  background: rgb(255 255 255 / 0.16);
}
/* .route only ever lives on a .slab, so these are written for a dark ground
   directly rather than as a light default plus an override layer. If the
   diagram is ever moved onto light ground, every colour here has to be
   revisited -- there is no fallback. */
#ttx#ttx .route__steps li {
  position: relative; z-index: 1;
  padding-top: var(--space-6);
  font-size: var(--text-sm); line-height: 1.45; color: var(--ink-invert-2);
  text-wrap: pretty;
}
#ttx#ttx .route__steps li::before {
  content: ""; position: absolute; left: 0; top: 0;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--ground); border: 1.5px solid rgb(255 255 255 / 0.30);
}

#ttx#ttx .route .route__again {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-5);
  font-family: var(--font-body); font-size: var(--text-sm);
  letter-spacing: 0; text-transform: none; color: var(--ink-invert-3);
}
/* The product track: brand-coloured, second node filled and parked in column 4
   so the gap it leaves is visible. */
/* the lifted brand is what carries on a dark ground -- var(--brand) at 0.549
   lightness does not */
#ttx#ttx .route__track--new .route__steps li { font-weight: 500; color: var(--ink-invert); }
#ttx#ttx .route__track--new .route__steps li:last-child { grid-column: 4; }
#ttx#ttx .route__track--new .route__steps li::before { border-color: var(--brand-lift); }
#ttx#ttx .route__track--new .route__steps li:last-child::before { background: var(--brand-lift); }
#ttx#ttx .route__track--new .route__steps::before {
  background: color-mix(in oklch, var(--brand-lift) 55%, transparent);
}

@media (max-width: 720px) {
/* two columns keeps the labels readable; the rail would be meaningless across
     a wrap, so it goes and the dots carry the sequence on their own */
#ttx#ttx .route__steps { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5) var(--space-4); }
  #ttx#ttx .route__steps::before, #ttx#ttx .route__track--new .route__steps::before { display: none; }
  #ttx#ttx .route__track--new .route__steps li:last-child { grid-column: auto; }
}

#ttx#ttx .caps { padding-top: var(--space-20); padding-bottom: var(--space-5); }
/* Three columns, and no rail.

   There used to be one continuous rule across the four cards with a node on
   each, on the reasoning that these are a pipeline rather than four separate
   features. That reasoning still holds -- but the route diagram in the section
   directly above is now a rail with nodes on it, and repeating the idiom forty
   pixels later made a reader parse the two as one diagram drawn twice. The
   sequence is carried by the numbers instead, which is cheaper and unambiguous.

   Numbering is legitimate here: build, write, hand over is a real order, not a
   decorative 01/02/03. */
#ttx#ttx .caps__grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-10);
}

#ttx#ttx .caps__item { display: grid; gap: 0; align-content: start; }
/* The icon sits in its own bordered tile rather than floating loose, so three
   very different silhouettes still line up as one row. Hairline weight, faint
   colour: an icon that competes with its own heading is drawn too heavily. */
#ttx#ttx .caps__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--brand);
  margin-bottom: var(--space-5);
}

#ttx#ttx .caps__n { color: var(--ink-faint); letter-spacing: 0.14em; }
#ttx#ttx .caps__item h3 {
  font-family: var(--font-display); font-size: var(--text-h3); font-weight: 600;
  letter-spacing: -0.02em; margin: var(--space-3) 0 var(--space-2);
}
#ttx#ttx .caps__item p:last-child {
  font-size: var(--text-sm); line-height: 1.62; color: var(--ink-soft); text-wrap: pretty;
}
/* --- where it lands ----------------------------------------------------------
   The slot a customer logo bar occupies on a page like this. There are no
   customers to name yet and inventing them is the one thing this page cannot
   do, so it carries the format and what reads it instead -- which is the more
   useful fact for this audience anyway. */
#ttx#ttx .lands {
  margin-top: var(--space-24);
  padding-block: var(--space-12);
  border-block: 1px solid var(--rule);
  text-align: center;
}
#ttx#ttx .lands__label {
  display: inline-flex; align-items: center; gap: var(--space-2);
  color: var(--ink-faint);
}
/* Equal COLUMNS, not equal gaps. These names run 7 to 18 characters, so a
   flex row with one gap value puts "Elastic" adrift between two long
   neighbours. A slot each gives the strip the cadence a logo wall gets for
   free, which is the whole reason it reads as considered rather than as a
   list of things someone typed. */
#ttx#ttx .lands__row {
  list-style: none; margin: var(--space-8) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  font-family: var(--font-display); font-size: 1.14rem; font-weight: 500;
  letter-spacing: -0.015em; color: var(--ink);
}
#ttx#ttx .lands__row li {
  display: flex; align-items: center; justify-content: center;
  padding-inline: var(--space-4);
  text-align: center; text-wrap: balance;
}
/* between the slots only -- a rule on the outer edges would box the strip in,
   and it already sits between two full-width hairlines */
#ttx#ttx .lands__row li + li { border-left: 1px solid var(--rule); }

#ttx#ttx .lands__note {
  margin-top: var(--space-8);
  font-size: var(--text-sm); color: var(--ink-soft);
}
/* Five slots stop being slots once they are narrower than the longest name.
   Below that the dividers go with them -- a divider that lands mid-wrap says
   nothing. */
@media (max-width: 1000px) {
  .lands__row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-6) 0; }
  .lands__row li:nth-child(3n+1) { border-left: 0; }
}
@media (max-width: 620px) {
  #ttx#ttx .lands__row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* `li + li` is 0,1,2 -- a bare `li` reset here is 0,1,1 and loses. Clear it
     with the same shape, then re-add on the second cell of each row only. */
#ttx#ttx .lands__row li + li { border-left: 0; }
  #ttx#ttx .lands__row li:nth-child(2n) { border-left: 1px solid var(--rule); }
}
/* Three cards go 3 -> 1, never 3 -> 2. Two columns leaves one card orphaned on
   a second row, which reads as a layout fault rather than a choice. Three
   columns hold to 860px, where each is still ~250px for a 20-word paragraph. */
@media (max-width: 1080px) {
  .caps__grid { gap: var(--space-8); }
}
@media (max-width: 860px) {
  #ttx#ttx .caps__grid { grid-template-columns: 1fr; gap: var(--space-10); }
  #ttx#ttx .caps__item { grid-template-columns: 38px minmax(0, 1fr); column-gap: var(--space-5); }
  #ttx#ttx .caps__icon { grid-row: span 3; margin-bottom: 0; }
}
@media (max-width: 640px) {
  #ttx#ttx .lands__row { gap: var(--space-4) var(--space-6); font-size: 0.96rem; }
}
/* --- illustrations -----------------------------------------------------------
   Two diagrams doing work the prose beside them cannot: a list of PIDs does not
   show inheritance, and "the structure is computed" lands faster as a record
   with most fields already filled than as a sentence.

   Both animate once on entry, staggered by per-row --a/--b offsets fed into
   animation-range. Nothing loops. */
#ttx#ttx .fig { margin: 0; }
/* the lineage, drawn as a tree */
#ttx#ttx .tree { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-5); }

#ttx#ttx .tree__row { position: relative; padding-left: calc(var(--lvl, 0) * 30px); display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/* elbow: down out of the parent, then right into this box */
#ttx#ttx .tree__row::before {
  content: "";
  position: absolute;
  left: calc((var(--lvl, 0) - 1) * 30px + 11px);
  top: -14px; width: 16px; height: 26px;
  border-left: 1px solid oklch(0.42 0.014 250);
  border-bottom: 1px solid oklch(0.42 0.014 250);
  border-bottom-left-radius: 5px;
}
#ttx#ttx .tree__row:first-child::before, #ttx#ttx .tree__row--back::before { display: none; }

#ttx#ttx .tree__box {
  display: inline-flex; align-items: baseline; gap: var(--space-3);
  padding: 7px 12px; border-radius: var(--radius-sm);
  background: oklch(0.21 0.013 250); border: 1px solid oklch(0.30 0.014 250);
  font-family: var(--font-mono); font-size: 0.8rem;
}
#ttx#ttx .tree__box b { font-weight: 500; color: var(--ink-invert-2); }
#ttx#ttx .tree__box i { font-style: normal; font-size: var(--text-micro); color: var(--ink-invert-3); }
#ttx#ttx .tree__box.is-hit { border-color: color-mix(in oklch, var(--n) 45%, transparent); background: color-mix(in oklch, var(--n) 12%, oklch(0.175 0.013 250)); }
#ttx#ttx .tree__box.is-hit b { color: var(--n); }

#ttx#ttx .tree__tag { font-family: var(--font-mono); font-size: var(--text-micro); letter-spacing: 0.07em; color: var(--n, var(--ink-invert-3)); }

#ttx#ttx .tree__loop { display: inline-flex; color: var(--n); margin-left: 4px; }
/* the ingest log â€” what marking looks like once the data lands.
   Customer rows are drawn as anonymous bars on purpose: inventing plausible
   production hostnames would be fabricating someone else's data to make a
   point, and the contrast reads better anyway.

   Dark card on the light ground, same as the lineage panel in section 01 â€” that
   is where the crispness comes from, and it is why this section is no longer a
   dark band. */
/* Was max-width:1020px, left-aligned inside an 1180px wrap: ragged 136px short
   on the right, and its centre sat 56px left of the page centre the connector
   above disperses from â€” so the strands did not span the rows they stand for.
   Full wrap width aligns it with the heading on the left and with the connector
   on the centre. */
/* padding only -- ground, mesh, noise, border and highlight come from .slab.
   Third dark block on the page to join it; 03, 05 and 07 are now one system
   rather than three separately-invented dark treatments. */
#ttx#ttx .log { padding: var(--space-6) var(--space-6) var(--space-5); }
#ttx#ttx .log__head { color: var(--ink-invert-3); }
#ttx#ttx .log__rows { list-style: none; margin: var(--space-5) 0 0; padding: 0; }

#ttx#ttx .log__row {
  display: grid;
  grid-template-columns: 64px 194px 78px minmax(0, 1fr) 76px;
  gap: var(--space-3); align-items: center;
  padding: 8px 10px; margin-inline: -10px; border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: 0.72rem; white-space: nowrap;
}

#ttx#ttx .log__bars { grid-column: 1 / 5; display: flex; align-items: center; gap: 11px; }
#ttx#ttx .log__bars i { display: block; height: 5px; width: var(--w); border-radius: 3px; background: oklch(0.30 0.012 250); }
#ttx#ttx .log__mine { font-size: var(--text-micro); letter-spacing: 0.10em; text-transform: uppercase; color: oklch(0.620 0.010 250); text-align: right; }
/* was 0.42 -- dark grey ON --ground, 2.32:1 */
#ttx#ttx .log__row--ttx { background: color-mix(in oklch, var(--warm) 13%, transparent); box-shadow: inset 2px 0 0 var(--warm); }
#ttx#ttx .log__row--ttx time { color: var(--ink-invert-3); }
#ttx#ttx .log__row--ttx em { font-style: normal; color: var(--warm); }
#ttx#ttx .log__row--ttx b { font-weight: 400; color: var(--ink-invert-3); }
#ttx#ttx .log__d { color: var(--ink-invert-2); overflow: hidden; text-overflow: ellipsis; }
#ttx#ttx .log__mark { font-size: var(--text-micro); letter-spacing: 0.10em; text-transform: uppercase; color: var(--warm); text-align: right; }
/* the three queries, out of the pane and onto the page ground */
#ttx#ttx .keys { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-6); margin-top: var(--space-8); }
#ttx#ttx .keys > div { display: flex; flex-direction: column; gap: 7px; padding-top: var(--space-4); border-top: 1px solid var(--rule); }
#ttx#ttx .keys dt { color: var(--ink-faint); }
#ttx#ttx .keys dd { margin: 0; font-family: var(--font-mono); font-size: 0.74rem; color: var(--ink-soft); overflow-wrap: anywhere; }
#ttx#ttx .keys b { font-weight: 400; color: var(--adversary); }
/* the run folder, as a directory listing */
#ttx#ttx .tree2 { margin: var(--space-12) 0 0; border: 1px solid var(--rule); border-radius: var(--radius-lg); background: var(--surface); padding: var(--space-6) var(--space-8) var(--space-5); }
#ttx#ttx .tree2__head { color: var(--brand); }
#ttx#ttx .tree2__rows { list-style: none; margin: var(--space-4) 0 0; padding: 0; }
#ttx#ttx .tree2__row {
  display: grid; grid-template-columns: 226px 74px minmax(0, 1fr);
  gap: var(--space-4); align-items: baseline;
  padding: 9px 0 9px 26px; position: relative;
  font-size: 0.82rem;
}
#ttx#ttx .tree2__row::before {
  content: ""; position: absolute; left: 4px; top: 0; bottom: 0;
  border-left: 1px solid var(--rule);
}
#ttx#ttx .tree2__row::after {
  content: ""; position: absolute; left: 4px; top: 50%; width: 12px;
  border-top: 1px solid var(--rule);
}
#ttx#ttx .tree2__row--last::before { bottom: 50%; }
#ttx#ttx .tree2__f { font-family: var(--font-mono); color: var(--ink-soft); overflow-wrap: anywhere; }
#ttx#ttx .tree2__row.is-lead .tree2__f { color: var(--ink); font-weight: 500; }
#ttx#ttx .tree2__s { font-family: var(--font-mono); font-size: var(--text-micro); color: var(--ink-faint); text-align: right; }
#ttx#ttx .tree2__d { color: var(--ink-faint); font-size: 0.8rem; }
#ttx#ttx .tree2__row.is-lead .tree2__d { color: var(--ink-soft); }
#ttx#ttx .tree2__foot { margin-top: var(--space-5); padding-top: var(--space-5); border-top: 1px solid var(--rule); font-size: 0.84rem; color: var(--ink-soft); }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    #ttx#ttx .log__row, #ttx#ttx .tree2__row {
      opacity: 0; translate: -8px 0;
      animation: fig-in linear both;
      animation-timeline: view();
      /* The end derives from the start plus a LENGTH. It was `entry var(--b,
         40%)`, and an entry range is as long as the element -- so a 30px log
         row revealed across 9px of scroll, finishing at the very bottom edge of
         the screen. Measured over all 53: the median finished 94% down. The
         authored --a values still carry the order; only the duration changed. */
      animation-range: entry var(--a, 0%) entry calc(var(--a, 0%) + var(--rv-run, 270px));
    }
  }
}

@media (max-width: 860px) {
  #ttx#ttx .keys { grid-template-columns: 1fr; gap: var(--space-5); }
  #ttx#ttx .log { padding-inline: var(--space-4); }
  #ttx#ttx .log__row { grid-template-columns: 54px minmax(0, 1fr) 84px; gap: var(--space-2); padding-inline: 8px; margin-inline: -8px; font-size: 0.64rem; }
  #ttx#ttx .log__mine, #ttx#ttx .log__mark { font-size: 0.54rem; letter-spacing: 0.04em; }
  #ttx#ttx .log__row--ttx b, #ttx#ttx .log__d { display: none; }
  #ttx#ttx .log__row--ttx em { overflow: hidden; text-overflow: ellipsis; }
  #ttx#ttx .log__bars { grid-column: 1 / 3; gap: 8px; }
  #ttx#ttx .log__bars i { width: min(var(--w), 30%); }
  #ttx#ttx .tree2__row { grid-template-columns: minmax(0, 1fr) 66px; row-gap: 2px; }
  #ttx#ttx .tree2__d { grid-column: 1 / -1; }
}
/* the campaign record */
#ttx#ttx .rec__head { color: var(--ink-faint); }
#ttx#ttx .rec { margin: var(--space-5) 0 0; display: flex; flex-direction: column; }
#ttx#ttx .rec > div {
  display: grid; grid-template-columns: 104px minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: baseline;
  padding: 9px 0; border-bottom: 1px solid var(--rule);
}
#ttx#ttx .rec dt { font-size: 0.78rem; color: var(--ink-faint); }
/* balance stops the tactic order breaking 4 + 1 and leaving TA0011 alone on a
   second line, which read as a rendering fault in a panel arguing that
   everything here is computed */
#ttx#ttx .rec dd { margin: 0; font-size: 0.8rem; color: var(--ink); overflow-wrap: anywhere; text-wrap: balance; }
#ttx#ttx .rec__by {
  font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--brand);
  border: 1px solid color-mix(in oklch, var(--brand) 32%, transparent);
  border-radius: var(--radius-pill); padding: 2px 8px; white-space: nowrap;
}
#ttx#ttx .rec__model dd { color: var(--ink); font-weight: 500; }
#ttx#ttx .rec__model .rec__by { color: oklch(0.505 0.165 24); border-color: color-mix(in oklch, var(--warm) 45%, transparent); }
#ttx#ttx .rec__model dt { color: var(--ink-soft); }
/* the last row's own border plus the footer's made two rules 15px apart */
#ttx#ttx .rec > div:last-child { border-bottom: 0; }
/* Was uppercase mono, which suited "SIX FIELDS COMPUTED. TWO FILLED IN." -- a
   label. The line is a sentence now, and 85 characters of uppercase mono is
   shouting. Same treatment as .tree2__foot, which closes its card the same way. */
#ttx#ttx .rec__foot {
  margin-top: var(--space-5); padding-top: var(--space-5);
  border-top: 1px solid var(--rule);
  font-size: 0.84rem; line-height: 1.6; color: var(--ink-soft); text-wrap: pretty;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    #ttx#ttx .tree__row, #ttx#ttx .rec > div {
      opacity: 0;
      animation: fig-in linear both;
      animation-timeline: view();
      /* The end derives from the start plus a LENGTH. It was `entry var(--b,
         40%)`, and an entry range is as long as the element -- so a 30px log
         row revealed across 9px of scroll, finishing at the very bottom edge of
         the screen. Measured over all 53: the median finished 94% down. The
         authored --a values still carry the order; only the duration changed. */
      animation-range: entry var(--a, 0%) entry calc(var(--a, 0%) + var(--rv-run, 270px));
    }
    @keyframes fig-in { to { opacity: 1; translate: 0 0; } }
    #ttx#ttx .tree__row { translate: -10px 0; }
    #ttx#ttx .rec > div { translate: 0 8px; }
/* the object that straddles the boundary should arrive at it, not be
       waiting there already â€” it is the one thing on the page that crosses a
       surface, and a static card makes the cut read as a layout accident */
#ttx#ttx .seam__card {
      opacity: 0; translate: 0 16px;
      animation: fig-in linear both;
      animation-timeline: view();
      animation-range: entry 14% entry 54%;
    }
  }
}
/* --- section reveals ---------------------------------------------------------
   The rows INSIDE the figures already arrive one at a time (fig-in, above), and
   so do the connector fans. This is the level above it: the block of copy that
   opens each section, the cards in the three grids, and the one figure whose
   interior is static.

   SAME MECHANISM, deliberately, not a second one -- same keyframes, same
   @supports guard, same --a/--b stagger declared inline. The page already had
   two entrance systems (.rise on load, fig-in on scroll); a third would be the
   point where nobody can predict what a given element does.

   The hiding lives INSIDE the guard, not in a base rule. Where scroll timelines
   are unsupported, `opacity: 0` never applies either, so the page is simply the
   page -- the failure mode cannot be invisible content.

   A FIGURE WHOSE ROWS ALREADY REVEAL DOES NOT ALSO REVEAL AS A BLOCK. Fading a
   container while its contents fade inside it reads as a double exposure.
   .route is the exception, because its steps are static. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rv {
      opacity: 0; translate: 0 26px;
      animation: fig-in linear both;
      animation-timeline: view();
      /* LENGTHS, not the percentages the figure rows use, and the difference
         matters: an `entry` range is as long as the ELEMENT, so `entry 5% ->
         34%` gave a 125px card 36px of scroll to reveal in -- a flick of the
         wheel and it was over. The rows get away with percentages because they
         are stacked, so their own positions sequence them. These blocks run
         from 40px pills to 600px figures and need one duration, not one
         proportion. --a is the only number per element; the end derives from
         it, so a stagger cannot be given a start without an end to match.

         210px WAS TOO SHORT AND THE EFFECT WAS INVISIBLE. `entry 0` is the
         moment the element's top edge touches the bottom of the screen, so a
         210px run finished with the element still 77% of the way down -- the
         bottom quarter, at the edge of vision, settled long before anyone
         looked at it. Measured across all 24: the median finished 74% down.
         380px lands the median around 57%, which is where reading starts. */
      --rv-run: 380px;
      animation-range: entry var(--a, 0px) entry calc(var(--a, 0px) + var(--rv-run));
    }

    /* A GRID'S STAGGER BELONGS TO THE GRID. The figure rows carry --a inline
       because each value was chosen for that row; the cards in these grids are
       a uniform set, so repeating it per item would state one decision five
       times. Siblings in a row share a view() timeline -- same top, same
       height -- so offsetting the start is the only thing that separates them.
       One knob per set: --rv-step. */
    .rv-set { --rv-step: 70px; }
    .rv-set > .rv:nth-child(2)   { --a: calc(1 * var(--rv-step)); }
    .rv-set > .rv:nth-child(3)   { --a: calc(2 * var(--rv-step)); }
    .rv-set > .rv:nth-child(4)   { --a: calc(3 * var(--rv-step)); }
    .rv-set > .rv:nth-child(n+5) { --a: calc(4 * var(--rv-step)); }

    /* Five short pills on one line. At the default step the row spreads over
       375px of scroll -- most of a screen for a 40px row, so it reads as broken
       rather than sequenced if you stop mid-way. */
    .lands__row { --rv-step: 40px; }
  }
}

@media (max-width: 560px) {
  #ttx#ttx .tree__row { padding-left: calc(var(--lvl, 0) * 18px); }
  #ttx#ttx .tree__row::before { left: calc((var(--lvl, 0) - 1) * 18px + 9px); width: 10px; }
  #ttx#ttx .rec > div { grid-template-columns: 92px minmax(0, 1fr); }
  #ttx#ttx .rec__by { grid-column: 2; justify-self: start; margin-top: 4px; }
}
#ttx#ttx .checklist { display: flex; flex-direction: column; gap: var(--space-3); }
#ttx#ttx .checklist li { display: flex; align-items: center; gap: var(--space-3); font-size: 0.88rem; color: var(--ink); }
#ttx#ttx .checklist svg { flex: none; }
#ttx#ttx .card hr { border: 0; border-top: 1px solid var(--rule); margin-block: var(--space-6); }
/* .callout / .callout__i / .callout__id lived here. Removed with the T1197
   aside in section 06 -- see the comment in index.html. */
#ttx#ttx .band { margin-top: var(--space-44); padding-block: 0 var(--space-8); }
/* :not([class]) for the same reason as .section p -- see the README. A classed
   paragraph inside this band styles itself completely. */
#ttx#ttx .band p:not([class]) { font-size: var(--text-lead); line-height: 1.68; color: var(--ink-soft); margin-top: var(--space-5); text-wrap: pretty; }
/* the bundle is a file manifest, so it is a spec sheet â€” not feature cards */
#ttx#ttx .spec { margin-top: var(--space-12); border-top: 1px solid var(--ink); }
#ttx#ttx .spec__row { transition: background-color var(--dur-short) var(--ease-out); display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--space-10); padding-block: var(--space-8); border-bottom: 1px solid var(--rule); }
#ttx#ttx .spec__file { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--ink-faint); padding-top: 4px; overflow-wrap: anywhere; }
#ttx#ttx .spec h3 { font-size: var(--text-h3); }
#ttx#ttx .spec__row:hover { background: var(--surface-sunk); }
#ttx#ttx .spec__row:hover .spec__file { color: var(--brand); }
#ttx#ttx .spec__file { transition: color var(--dur-short) var(--ease-out); }
#ttx#ttx .spec p { margin-top: 7px; font-size: 0.94rem; line-height: 1.66; max-width: 620px; }

#ttx#ttx .close { margin-block: var(--space-44); }
/* Was text-align:center with the form left-aligned inside it â€” the heading and
   lead floated over a left edge nothing else shared. Everything in the block now
   hangs off the same 680px column as the form. Padding is symmetric: the old
   block left ~180px of empty ground below the button because the centred copy
   made the top look shorter than it was. */
/* padding only -- ground, mesh, noise, border and highlight come from .slab.
   .close__dots is gone with it. */
#ttx#ttx .close__inner { padding: var(--space-16) var(--space-6); }
#ttx#ttx .close__body { max-width: 680px; margin-inline: auto; }
/* --- the robot on the card's edge --------------------------------------------
   The seat line is y=160 of a 250-unit viewBox = 64%, so `translate: 0 -64%`
   from `top: 0` puts exactly that line on the card's top edge. A percentage
   translate resolves against the element's own height, so this holds at any
   size without recomputing an offset. CHANGE IT IF THE VIEWBOX CHANGES.

   There was a .close__lip here -- a lit top face giving the card the thickness
   of a bench. It went, and it is worth saying why so it does not come back: the
   rest of this page is drawn face-on with no perspective anywhere, so a top
   face implies a camera above the card that nothing else shares. It read as a
   dark flap laid over the panel rather than as depth. The figure needs no help
   from it; the pose carries the sitting on its own.

   Anchored to .close, not .close__inner: .slab sets overflow:hidden to clip its
   mesh to the rounded corners, and inside it everything above the edge would be
   cut off at the waist. */
#ttx#ttx .close {
  position: relative;
  /* Shared by the figure and its callout so the two cannot drift apart. */
  --agent-right: clamp(40px, 7.5vw, 104px);
  --agent-w:     clamp(96px, 10vw, 132px);
}

#ttx#ttx .cagent {
  position: absolute; z-index: 2;
  top: 0; right: var(--agent-right);
  width: var(--agent-w); height: auto;
  translate: 0 -64%;
  /* The ROOT stays inert and the SHAPES opt in. Two-thirds of this 132x174 box
     is empty space around the figure. Measured on one 6px grid, 638 points:
     root `auto` and root `painted` are IDENTICAL -- 214 on the figure, 424
     absorbed by the box, 0 reaching the card -- because the outermost svg is
     hit-tested over its whole viewport, so the property never gets to matter.
     Only pushing `painted` down to the drawn elements lets those 424 through. */
  pointer-events: none;
}
/* :where() so this stays at zero specificity and .cagent__cast below wins. */
#ttx#ttx .cagent :where(path, #ttx#ttx rect, #ttx#ttx circle, #ttx#ttx ellipse) { pointer-events: painted; }
/* the contact shadow is a big soft ellipse under the figure, not part of it */
#ttx#ttx .cagent__cast { pointer-events: none; }
/* The head tilts toward the pointer. transform-origin is the neck (94,82),
   which is what a head actually pivots on; --tilt is set on the <svg> and
   inherits to both .cagent__head groups, so they cannot fall out of step. */
#ttx#ttx .cagent__head {
  transform-box: view-box;
  transform-origin: 94px 82px;
  rotate: calc(-5deg + var(--tilt, 0deg));
}
@media (hover: hover) and (pointer: fine) {
  #ttx#ttx .cagent__head { transition: rotate 260ms cubic-bezier(0.22, 1, 0.36, 1); }
}
/* --- the wave ---------------------------------------------------------------
   transform-box: view-box puts transform-origin in viewBox user units, so the
   joints are written as the coordinates they actually are in the drawing.
   The shoulder is (60,106) and the elbow (56,136); if the arm is redrawn, these
   move with it. */
#ttx#ttx .cagent__arm { transform-box: view-box; transform-origin: 60px 106px; }
#ttx#ttx .cagent__hand { transform-box: view-box; transform-origin: 56px 136px; }

@keyframes cagent-arm {
  0%, 8%   { rotate: 0deg; }
  26%, 74% { rotate: 105deg; }
  100%     { rotate: 0deg; }
}
/* The forearm's rest pose is already 49deg off vertical, so the angle that
   brings it straight up is 229 minus the shoulder angle -- 124 here. The rock
   either side of that is the wave itself. Re-derive it if the shoulder angle
   above changes, or the forearm waves off to one side. */
@keyframes cagent-hand {
  0%, 8% { rotate: 0deg; }
  26%    { rotate: 124deg; }
  38%    { rotate: 104deg; }
  50%    { rotate: 142deg; }
  62%    { rotate: 108deg; }
  74%    { rotate: 132deg; }
  100%   { rotate: 0deg; }
}
/* --- the callout ------------------------------------------------------------
   Dark on the light page, because on this site dark ground is machine output
   and this is the agent speaking. It clears the raised hand by sitting above
   the shoulder line -- the wave has to swing wide, since the arm is shorter
   than the head is tall and cannot reach up beside it. */
#ttx#ttx .cbubble {
  position: absolute; z-index: 3;
  right: calc(var(--agent-right) + var(--agent-w) - 16px);
  top: calc(-0.70 * var(--agent-w));
  translate: 0 -100%;
  width: max-content; max-width: 216px;
  padding: 10px 14px 11px;
  border-radius: 14px;
  background: var(--ground);
  color: var(--ink-invert);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.35;
  /* garnish, and aria-hidden: it was acting as a hover target that does nothing
     and intercepting selection over the card behind it */
  pointer-events: none;
  box-shadow: 0 12px 28px -10px rgb(var(--scrim) / 0.38),
              inset 0 1px 0 rgb(255 255 255 / 0.10);
}
/* a rotated square, not a border triangle: the fill is flat, so the corner
   matches the body exactly and picks up the same radius */
/* One sentence per line. text-wrap:balance broke it as "Fill this in. I'll
   play / the bad guy.", splitting the phrase rather than the sentence -- it
   balances line lengths, which is the wrong objective for a two-beat line.
   Blocks rather than a <br> so a long line can still wrap on its own. */
#ttx#ttx .cbubble span { display: block; }

#ttx#ttx .cbubble::after {
  content: ''; position: absolute;
  right: 16px; bottom: -5px;
  width: 14px; height: 14px;
  border-radius: 0 0 3px 0;
  background: var(--ground);
  rotate: 45deg;
}
/* FINISHED BY DEFAULT. The callout above is its own end state; the script opts
   in to hover behaviour by setting data-agent="live", so a blocked script or a
   thrown exception leaves the callout showing rather than missing.

   HOVER IS CSS, not script: it is a hover state, and a class toggled from
   mouseenter/mouseleave would only be a worse version of one. */
[data-agent="live"] #ttx#ttx .cbubble {
  opacity: 0; scale: 0.9;
  /* grown from the tail, so it reads as coming out of the agent */
  transform-origin: calc(100% - 23px) 100%;
  /* LEAVING: short, and delayed, so dragging the pointer across the figure on
     the way to somewhere else does not make it blink. */
  transition: opacity 150ms ease 240ms, scale 150ms ease 240ms;
}
/* :hover reaches the root even though the root is pointer-events:none -- the
   painted child under the pointer is its descendant, which is the same reason
   mouseenter fires on it. */
[data-agent="live"] #ttx#ttx .cagent:hover ~ .cbubble {
  opacity: 1; scale: 1;
  transition: opacity 180ms ease, scale 400ms cubic-bezier(0.34, 1.45, 0.5, 1);
}
/* .is-waving is the wave alone now. The callout is hover-only, so scrolling in
   waves and says nothing; hovering does both. */
#ttx#ttx .is-waving .cagent__arm { animation: cagent-arm  2.2s ease-in-out both; }
#ttx#ttx .is-waving .cagent__hand { animation: cagent-hand 2.2s ease-in-out both; }

@media (prefers-reduced-motion: reduce) {
  #ttx#ttx .is-waving .cagent__arm, #ttx#ttx .is-waving .cagent__hand { animation: none; }
  #ttx#ttx .cagent__head { rotate: -5deg; transition: none; }
/* still hover-driven -- reduced motion asks for less movement, not for the
     interaction to be replaced by something permanently on screen */
[data-agent="live"] #ttx#ttx .cbubble, [data-agent="live"] #ttx#ttx .cagent:hover ~ .cbubble { scale: 1; transition: none; }
}
/* A callout with no speaker is nonsense, so it goes exactly where the figure
   goes. */
@media (max-width: 1080px) { .cbubble { display: none; } }
/* ANCHOR OFFSET. Both #request links jump here, and the figure plus its callout
   stand about 150px clear above the card, so the shared 88px offset -- which
   only clears the topbar -- landed them off-screen. The robot's head was
   already being cropped this way before the callout existed. Derived from
   --agent-w so it tracks the figure instead of being a number to re-measure. */
@media (min-width: 1081px) {
  #request { scroll-margin-top: calc(0.70 * var(--agent-w) + 140px); }
}
/* STROKED AS WELL AS FILLED, because the character crosses two grounds: the
   light page above the edge and the dark card below it. A near-white fill alone
   disappears against --paper; the outline is what carries it there. */
#ttx#ttx .cagent__ink {
  stroke: oklch(0.33 0.016 250);
  stroke-width: 3;
  stroke-linejoin: round;
  paint-order: stroke fill;
}
#ttx#ttx .cagent__shell { fill: oklch(0.93 0.005 250); }
#ttx#ttx .cagent__limb { fill: oklch(0.88 0.006 250); }
/* Mid tone on purpose: the knees and feet hang over the dark card while the
   shoulders sit on the light dotted ground, so this one fill has to read
   against both. Darker and the lower half sinks into the card. */
#ttx#ttx .cagent__joint { fill: oklch(0.480 0.014 250); }
#ttx#ttx .cagent__pod-in { fill: oklch(0.46 0.016 250); }
/* Far-side limbs step down a shade. Depth in a flat illustration comes from
   tone separation, not from outline weight. */
#ttx#ttx .cagent__far { fill: oklch(0.790 0.006 250); }
#ttx#ttx .cagent__far-j { fill: oklch(0.395 0.014 250); }
/* Interior shading and highlight: unstroked, or the group's outline would draw
   a hard line across the middle of the shape. */
#ttx#ttx .cagent__shade { fill: oklch(0.845 0.008 250); stroke: none; }
#ttx#ttx .cagent__hi { fill: #fff; opacity: 0.5; stroke: none; }
#ttx#ttx .cagent__cast { fill: oklch(0.08 0.020 250); opacity: 0.62; }
#ttx#ttx .cagent__neck { fill: oklch(0.55 0.014 250); }
#ttx#ttx .cagent__band { fill: oklch(0.38 0.016 250); }

#ttx#ttx .cagent__visor { fill: oklch(0.17 0.014 250); }
#ttx#ttx .cagent__eye { fill: var(--brand-lift); }
#ttx#ttx .cagent__smile {
  stroke: var(--brand-lift); stroke-width: 2.6;
  stroke-linecap: round; fill: none;
}
/* Eyes TRANSLATE, they do not rotate -- a swivelling head reads as a turret,
   which is what the hero agent is. This one is a character, so it glances. */
/* 380ms with overshoot read as attention when this only answered focus. It
   lags and wobbles once it is following a pointer, so it is now a short
   smoothing curve that serves both. */
#ttx#ttx #cagent-eyes { transition: translate 200ms cubic-bezier(0.22, 1, 0.36, 1); }
/* Fires once, when the request actually goes -- the only thing here that moves
   without the reader causing it, and it is an acknowledgement, not decoration. */
#ttx#ttx .cagent.is-sent .cagent__smile, #ttx#ttx .cagent.is-sent .cagent__eye { animation: cagent-ack 900ms var(--ease-out) 1; }
@keyframes cagent-ack {
  0%   { opacity: 1; }
  22%  { opacity: 1; filter: drop-shadow(0 0 7px var(--brand-lift)); }
  100% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  #ttx#ttx #cagent-eyes { transition: none; }
  #ttx#ttx .cagent.is-sent .cagent__smile, #ttx#ttx .cagent.is-sent .cagent__eye { animation: none; }
}
/* Below this the block narrows until the robot would sit over the heading. */
@media (max-width: 1080px) { .cagent { display: none; } }


#ttx#ttx .close h2 { color: var(--ink-invert); max-width: 560px; text-wrap: balance; }
/* margin-top is stated here because `.section p` does not reach this block --
   .close is not a .section -- so nothing else was setting it and the lead sat
   flush against the heading. */
#ttx#ttx .close p { color: var(--ink-invert-2); max-width: 540px; margin-top: var(--space-4); }
/* --- access request form -----------------------------------------------------
   Sits in the closing block on the dark ground. Plain POST underneath; the
   inline states are an enhancement, not a requirement. */
#ttx#ttx .req { max-width: 680px; margin: var(--space-8) 0 0; }

#ttx#ttx .req__trap { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

#ttx#ttx .req__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4) var(--space-5); }
#ttx#ttx .req__field { display: flex; flex-direction: column; gap: 7px; }
#ttx#ttx .req .req__field { margin: 0; max-width: none; }
#ttx#ttx .req .req__note { margin: 0; }
#ttx#ttx .req .req__status { margin: var(--space-4) 0 0; max-width: none; }
/* an empty status paragraph still reserved a line plus its margin, which is
   where ~43px of the dead ground at the bottom of the closing panel came from */
#ttx#ttx .req__status:empty { display: none; }
#ttx#ttx .req__field--wide { grid-column: 1 / -1; }

#ttx#ttx .req label {
  font-family: var(--font-mono); font-size: var(--text-micro);
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--ink-invert-3);
}
#ttx#ttx .req label span { text-transform: none; letter-spacing: 0.02em; color: oklch(0.58 0.010 250); }

#ttx#ttx .req input, #ttx#ttx .req select, #ttx#ttx .req textarea {
  width: 100%;
  padding: 11px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid rgb(255 255 255 / 0.18);
  background: rgb(255 255 255 / 0.06);
  color: var(--ink-invert);
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.4;
  transition: border-color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out);
}
#ttx#ttx .req textarea { resize: vertical; min-height: 84px; }
/* THE ROOT CAUSE OF THE WINDOWS BUG, and the fallback fix for it.

   A native <select> paints its dropdown with the OPERATING SYSTEM's colours,
   but the <option> elements inherit the author's `color` -- here a near-white
   on a dark card. On Windows that put near-white text on the OS's white popup
   and the options were effectively invisible. macOS never showed it, because it
   ignores author styling on that popup almost entirely.

   color-scheme tells the browser this control is dark, so the popup, its
   scrollbar and any autofill are painted dark rather than light. The explicit
   option colours are belt and braces for browsers that paint them anyway.

   This matters even though a listbox replaces the popup further down: that is
   an enhancement, and when the script does not run THIS is what a reader gets.
   It has to be legible on its own. */
#ttx#ttx .req input, #ttx#ttx .req select, #ttx#ttx .req textarea { color-scheme: dark; }
#ttx#ttx .req select option {
  background-color: oklch(0.205 0.018 250);
  color: oklch(0.990 0.003 250);
}

#ttx#ttx .req select { appearance: none; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-invert-3) 50%),
                    linear-gradient(135deg, var(--ink-invert-3) 50%, transparent 50%);
  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 34px;
}
/* MEASURED, not chosen by eye. 0.56 came out at 2.63:1 against this field --
   below AA, and below even the 3:1 for large text. Placeholder text says what
   a field wants and what the dropdown's unset state is; it has to be readable.
   0.71 measures 4.75:1, still visibly dimmer than a filled value at 11.9:1. */
#ttx#ttx .req input::placeholder, #ttx#ttx .req textarea::placeholder { color: oklch(0.71 0.010 250); }
#ttx#ttx .req input:hover, #ttx#ttx .req select:hover, #ttx#ttx .req textarea:hover { border-color: rgb(255 255 255 / 0.30); }
#ttx#ttx .req input:focus, #ttx#ttx .req select:focus, #ttx#ttx .req textarea:focus {
  background: rgb(255 255 255 / 0.10);
  border-color: var(--brand-lift);
}
/* only after the browser has judged it â€” not while the field is still empty */
#ttx#ttx .req input:user-invalid, #ttx#ttx .req textarea:user-invalid { border-color: var(--adversary); }
/* --- the SIEM dropdown ------------------------------------------------------
   A listbox that replaces the native popup entirely, so nothing about it is
   left to the operating system. Built by the script from the <select> above;
   without the script the native control is what shows, which is why the
   color-scheme fix above is kept rather than replaced.

   Everything here is painted by us, so it reads the same on Windows, macOS and
   Linux, and in the OS's light mode or dark mode -- the card is dark in both,
   so the menu is dark in both. */
/* Hidden, not disabled: a disabled control is left out of the submission
   entirely, and this one still carries the value. */
#ttx#ttx .sel__native { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
#ttx#ttx .sel { position: relative; }
#ttx#ttx .sel__btn {
  width: 100%;
  padding: 11px 34px 11px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid rgb(255 255 255 / 0.18);
  background: rgb(255 255 255 / 0.06);
  color: var(--ink-invert);
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
  display: block;
  transition: border-color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out);
}
/* one line, always: a long option must not make the field grow taller than the
   inputs beside it */
#ttx#ttx .sel__value { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the same tone the real placeholders use, so an unset dropdown reads as unset
   rather than as a chosen value -- see the contrast note on ::placeholder */
#ttx#ttx .sel__value[data-placeholder] { color: oklch(0.71 0.010 250); }

#ttx#ttx .sel__btn::after {
  content: ''; position: absolute; right: 14px; top: 50%;
  width: 7px; height: 7px; margin-top: -6px;
  border-right: 1.7px solid var(--ink-invert-3);
  border-bottom: 1.7px solid var(--ink-invert-3);
  rotate: 45deg;
  transition: rotate var(--dur-short) var(--ease-out), margin-top var(--dur-short) var(--ease-out);
}
#ttx#ttx .sel[data-open] .sel__btn::after { rotate: -135deg; margin-top: -2px; }

#ttx#ttx .sel__btn:hover { border-color: rgb(255 255 255 / 0.30); }
#ttx#ttx .sel__btn:focus-visible, #ttx#ttx .sel[data-open] .sel__btn {
  background: rgb(255 255 255 / 0.10);
  border-color: var(--brand-lift);
  outline: none;
}
/* a border colour alone is not a focus indicator for someone who cannot see
   hue difference, so keyboard focus also gets a ring */
#ttx#ttx .sel__btn:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklch, var(--brand-lift) 38%, transparent); }

#ttx#ttx .sel__list {
  position: absolute; z-index: 30;
  inset-inline: 0; top: calc(100% + 6px);
  margin: 0; padding: 5px;
  list-style: none;
  max-height: 268px; overflow-y: auto;
  border-radius: var(--radius);
  border: 1px solid rgb(255 255 255 / 0.15);
  /* OPAQUE on purpose. The card behind this carries a mesh gradient and a noise
     texture; a translucent menu over it is unreadable. */
  background: oklch(0.205 0.018 250);
  box-shadow: 0 18px 44px -14px rgb(var(--scrim) / 0.60);
  color-scheme: dark;                 /* so the scrollbar is dark too */
}
#ttx#ttx .sel__list[hidden] { display: none; }
/* flipped above the field when there is not enough room below it */
#ttx#ttx .sel[data-drop="up"] .sel__list { top: auto; bottom: calc(100% + 6px); }

#ttx#ttx .sel__opt {
  position: relative;
  padding: 9px 30px 9px 11px;
  border-radius: 5px;
  color: var(--ink-invert-2);
  font-size: 0.9rem;
  line-height: 1.35;
  cursor: pointer;
}
/* Hover and keyboard share one state. Two different highlights for "the mouse
   is here" and "the keyboard is here" is how you end up with two visible
   selections at once. */
#ttx#ttx .sel__opt[data-active] { background: rgb(255 255 255 / 0.10); color: var(--ink-invert); }
#ttx#ttx .sel__opt[aria-selected="true"] { color: var(--ink-invert); }
/* a tick, so the chosen one is identifiable without relying on weight alone */
#ttx#ttx .sel__opt[aria-selected="true"]::after {
  content: ''; position: absolute; right: 12px; top: 50%;
  width: 5px; height: 9px; margin-top: -6px;
  border-right: 1.8px solid var(--brand-lift);
  border-bottom: 1.8px solid var(--brand-lift);
  rotate: 45deg;
}
/* A FINGER IS NOT A MOUSE POINTER. Both conditions, not just the first:
   `pointer: coarse` is the correct question to ask, but it is reported
   inconsistently -- device emulation does not set it at all -- and a phone-width
   viewport is the case that actually matters. A slightly taller row in a narrow
   desktop window costs nothing. 44px is the target size to clear. */
@media (pointer: coarse), #ttx#ttx (max-width: 640px) {
  .sel__opt { padding-block: 13px; }
  .sel__list { max-height: 60vh; }
}
/* WINDOWS HIGH CONTRAST. Forced-colors mode strips author backgrounds, which
   would take the active-option highlight with it and leave a keyboard user
   unable to see where they are in the list. */
@media (forced-colors: active) {
  .sel__btn, .sel__list { border: 1px solid CanvasText; background: Canvas; color: CanvasText; }
  .sel__value[data-placeholder] { color: GrayText; }
  .sel__opt { color: CanvasText; }
  .sel__opt[data-active] { background: Highlight; color: HighlightText; forced-color-adjust: none; }
  .sel__opt[aria-selected="true"]::after { border-color: CanvasText; }
  .sel__btn::after { border-color: CanvasText; }
}

@media (prefers-reduced-motion: reduce) {
  #ttx#ttx .sel__btn, #ttx#ttx .sel__btn::after { transition: none; }
}


#ttx#ttx .req__foot { display: flex; align-items: center; gap: var(--space-5); margin-top: var(--space-6); flex-wrap: wrap; }
#ttx#ttx .req__note { font-size: 0.82rem; color: var(--ink-invert-3); max-width: 340px; }

#ttx#ttx .req__status { margin-top: var(--space-4); font-size: 0.88rem; color: var(--ink-invert-2); min-height: 1.2em; }
#ttx#ttx .req[data-state="error"] .req__status { color: oklch(0.78 0.14 22); }
#ttx#ttx .req[data-state="sent"] .req__status { color: oklch(0.78 0.13 155); font-size: 0.95rem; }
#ttx#ttx .req[data-state="sent"] .req__grid, #ttx#ttx .req[data-state="sent"] .req__foot { display: none; }

@media (max-width: 640px) {
  #ttx#ttx .req__grid { grid-template-columns: 1fr; }
  #ttx#ttx .req__foot { flex-direction: column; align-items: stretch; gap: var(--space-4); }
  #ttx#ttx .req__foot .btn { justify-content: center; }
  #ttx#ttx .req__note { max-width: none; }
}

#ttx#ttx .close__actions { display: flex; justify-content: center; gap: var(--space-3); margin-top: var(--space-8); flex-wrap: wrap; }

#ttx#ttx .foot { border-top: 1px solid var(--rule); }
#ttx#ttx .foot__inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-6); padding-block: var(--space-8); flex-wrap: wrap; }
#ttx#ttx .foot__brand { display: flex; align-items: center; gap: var(--space-2); font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; }
#ttx#ttx .foot__by { color: var(--ink-faint); font-size: var(--text-sm); }
/* Its own row, not a nav item. It sits in .foot__inner rather than
   .foot__links because that one has no flex-wrap, so a 100% basis there just
   squeezed it onto the same line as Contact / Safety. */
#ttx#ttx .foot__note { flex-basis: 100%; margin: 0; color: var(--ink-faint); font-size: var(--text-micro); }
#ttx#ttx .foot__links { display: flex; align-items: center; gap: var(--space-6); font-size: var(--text-sm); color: var(--ink-faint); }
#ttx#ttx .foot__links a { color: var(--ink-faint); white-space: nowrap; }
#ttx#ttx .foot__links a:hover { color: var(--ink); }
/* --- one orchestrated entrance, then the page is simply there -------------
   Delay arrives as --d rather than an inline animation-delay. An inline delay
   is set on the element, so any later rule that gives that element a second
   animation inherits the entrance's timing along with it; a custom property
   only applies where it is read.

   Direction arrives as --rise-x / --rise-y, because eleven elements sharing one
   upward nudge is a pattern applied rather than a design. Each one now enters
   from the edge it sits against â€” left chrome from the left, the nav pill down
   from above the top edge â€” so the entrance describes the layout instead of
   flattening it. */
#ttx#ttx .rise {
  opacity: 0;
  animation: rise 0.70s var(--ease-out) forwards;
  animation-delay: var(--d, 0s);
}
@keyframes rise {
  from { opacity: 0; transform: translate(var(--rise-x, 0px), var(--rise-y, 18px)); }
  to   { opacity: 1; transform: none; }
}
/* --- responsive ------------------------------------------------------------ */
@media (max-width: 1080px) {
  .section__split { gap: var(--space-10); }
  /* .link mirrors .section__split's track geometry, so its gutter track has to
     follow the same change or the column-aligned fans drift by half the delta */
  .link { grid-template-columns:
    var(--space-6) minmax(0, 1fr) var(--space-10) minmax(0, 1fr) var(--space-6); }
  .spec__row { grid-template-columns: 180px minmax(0, 1fr); gap: var(--space-6); }
}

@media (max-width: 860px) {
  #ttx#ttx .hero { height: auto; }
/* a phone does not need a 220vh stage */
#ttx#ttx .hero__media { position: absolute; inset: 0; height: 100%; }
/* clears the seam's overlap, so the closing line is never painted over */
#ttx#ttx .hero__inner {
    position: relative; height: auto;
    padding-bottom: calc(var(--seam-pull) + var(--space-4));
  }
  #ttx#ttx .hero__img { animation: none !important; object-position: 50% 40%; transform: none; }
/* hero is content-height here, so the percentage stops match the copy, not the art */
#ttx#ttx .hero__scrim-top {
    background: linear-gradient(to bottom,
      rgb(var(--scrim) / 0.82) 0%,
      rgb(var(--scrim) / 0.70) 40%,
      rgb(var(--scrim) / 0.45) 72%,
      rgb(var(--scrim) / 0) 92%);
  }
  #ttx#ttx .hero__copy { padding-top: var(--space-16); }
  #ttx#ttx .section__split { grid-template-columns: 1fr; }
/* the checklist card reads as evidence for the prose â€” it belongs after it */
#ttx#ttx .section__split > .card { order: 2; }
/* relative, not static: .hero__media is absolute here and would otherwise
     paint straight over a non-positioned rail. No pin and no fade â€” the hero is
     content-height below 860px, so the viewport-relative stops mean nothing. */
#ttx#ttx .spec__row { grid-template-columns: 1fr; gap: var(--space-2); }
  #ttx#ttx .spec__file { padding-top: 0; order: 2; }
}

@media (max-width: 560px) {
  #ttx#ttx .nav__links { display: none; }
/* wordmark + one action; no wrapping links */
#ttx#ttx .nav__pill { gap: var(--space-3); }
  #ttx#ttx .section { padding-top: var(--space-20); }
  #ttx#ttx .section--tight { padding-top: var(--space-20); }
  #ttx#ttx .band { margin-top: var(--space-20); padding-block: var(--space-20); }
  #ttx#ttx .close { margin-block: var(--space-20); }
  #ttx#ttx .close__inner { padding-block: var(--space-12); }
  #ttx#ttx .hero__actions .btn, #ttx#ttx .close__actions .btn { flex: 1 1 auto; justify-content: center; }
  #ttx#ttx .capture { flex-direction: column; align-items: stretch; gap: var(--space-2); padding: 8px; border-radius: var(--radius); }
  #ttx#ttx .capture__input { padding: 10px 12px; }
  #ttx#ttx .capture__go { justify-content: center; border-radius: var(--radius-sm); }
  #ttx#ttx .foot__inner { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
  #ttx#ttx .foot__links { flex-wrap: wrap; gap: var(--space-4); }
}

@media (prefers-reduced-motion: reduce) {
  #ttx#ttx .rise { opacity: 1; animation: none; transform: none; }
  #ttx#ttx * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ===========================================================================
   SITE GROUND: DARK BLUE EVERYWHERE
   ---------------------------------------------------------------------------
   The page was built light -- dark ink on paper, with a handful of dark bands
   for contrast. This flips the ground tokens so the whole site is the dark blue
   the hero already carried, and lifts the ink to match.

   Done at the token level on purpose: 77% of this stylesheet's colour comes
   through var(--...), so swapping the tokens moves the page coherently instead
   of chasing several hundred rules. Everything below the token block is a
   literal that had to be handled by hand; there was only one.
   =========================================================================== */
#ttx#ttx {
  /* ground and paper -- paper IS the ground now */
  --ground:        oklch(0.130 0.060 250);
  --ground-lift:   oklch(0.195 0.055 250);
  --paper:         oklch(0.170 0.055 250);
  --surface:       oklch(0.215 0.050 250);
  --surface-sunk:  oklch(0.150 0.052 250);

  /* ink, inverted: light type on a dark ground */
  --ink:           oklch(0.985 0.003 250);
  --ink-soft:      oklch(0.865 0.010 250);
  --ink-faint:     oklch(0.720 0.014 250);

  /* the inverted set stays light -- it is what the dark bands already used */
  --ink-invert:    oklch(0.990 0.003 250);
  --ink-invert-2:  oklch(0.920 0.008 250);
  --ink-invert-3:  oklch(0.800 0.012 250);

  --rule:          oklch(0.330 0.030 250);
  --rule-invert:   oklch(0.380 0.030 250);

  /* shadows fall on a dark ground now, so the scrim goes near-black */
  --scrim:         3 8 18;
}
/* The dot grid was drawn for a light ground and vanishes on a dark one. */
#ttx#ttx main {
  background-image: radial-gradient(oklch(0.300 0.040 250) 1px, transparent 1px);
}
/* The one hard-coded light surface in the sheet: a translucent white pane with
   dark type, which on a dark ground read as a strip of the old site that forgot
   to change. Same idea, inverted. */
#ttx#ttx .topbar {
  background: oklch(0.170 0.055 250 / 0.82);
}
/* The hero photograph carries a warm sunrise on the right, which was the one
   thing still reading as a different palette once the site went dark blue. A
   blue wash over the image pulls it into the same family without replacing the
   photograph -- `color` blend keeps the valley's shape and light, and takes its
   hue from the wash. Drop the opacity for more photograph, raise it for more
   blue; delete the rule to have the original back. */
#ttx#ttx .hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: oklch(0.300 0.090 250);
  mix-blend-mode: color;
  opacity: 0.72;
  pointer-events: none;
}
/* The last section carried 176px of bottom margin, which was the air above the
   footer. With the footer gone that became a strip of nothing at the end of the
   page. */
#ttx#ttx main > :last-child { margin-bottom: 0; }