/* ============================================================
   ENTEL product site. One stylesheet for /, /beta, /briefing.

   Tokens come from entel-tokens.css, which every page links
   BEFORE this file. Nothing here redefines a token.

   The rhythm rule from DESIGN-SYSTEM.md 6 is load-bearing and
   drives the whole page order: alternate dark and light masses,
   never stack two darks, green is a line / dot / flag / button
   and never a large fill.
   ============================================================ */

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 74px; }  /* the sticky header's height, so an anchor lands below it */
body {
  background: var(--gray-light);
  color: var(--text);
  font-family: var(--sans);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

/* Visually hidden, still read aloud. Was living in entel-chrome.css, which the
   product pages no longer link, so it moved here with them. Without it every
   sr-only <label> renders as visible text next to its field. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ONE GUTTER, NAMED. .wrap is not the only thing that has to hold the page's
   left edge: the hero stage sits outside it, the showcase frames bleed out of
   their own padding, and the caption under the hero is centred on nothing. They
   were each carrying a literal and they had already drifted (28, 26, 18 within
   one viewport). --gut is declared on :root so any of them can read it, and it
   steps down twice: at 768 where the measure starts costing words per line, and
   at 480 where the glass itself is the constraint. */
:root { --gut: 28px; }
@media (max-width: 767px) { :root { --gut: 20px; } }
@media (max-width: 479px)  { :root { --gut: 16px; } }
.wrap { max-width: 1140px; margin: 0 auto; padding: 0 var(--gut); }

/* ---- PAGE RAILS ----
   Two hairlines standing at the container edges, running unbroken from the end
   of the hero down to the footer. Ported from attio.com (measured 2026-08-27,
   1512px viewport: rails at x=60 and x=1451, which is their max-w-360 container
   plus its px-6 gutter). The job is structural, not decorative. Without them
   every band below the fold is a block of content floating on a full-bleed
   ground and the page reads as a stack of unrelated slabs; with them the same
   bands read as content inside one column, and the eye gets a fixed left edge
   to track down instead of re-finding the measure at every section.

   They start BELOW the hero, which is also where attio.com starts them. The hero
   is the one full-bleed mass on the page and rails drawn across it would cage
   the thing that is supposed to open the page. They stop above the footer for
   the same reason attio.com stops them there: the footer is chrome, not content
   in the column, and it closes with a full-width rule instead.

   DRAWN PER BAND, not once over the whole page. A single fixed overlay is fewer
   lines and was the first attempt, but it cannot survive rule 6 of
   DESIGN-SYSTEM.md: this page alternates dark and light masses, and a hairline
   that reads on #0f1f0f is invisible on #f2f2f0. Each band paints its own
   segment in its own ground's hairline colour and the segments stack into one
   continuous line. It also keeps every band's own border-top meeting the rails
   rather than crossing an overlay drawn above it.

   THE RAIL BOX IS WIDER THAN THE TEXT COLUMN, and it is a fixed distance from
   the window rather than a share of anything. The first build put the rails on
   .wrap's own edge, 186px in at 1512px, which made them a border drawn around
   the copy instead of the edges of the page. 44px (Jace, 2026-08-27), against
   attio.com's 60px on the same viewport, and their content column sits well
   inside their rails too. That is the whole point of the device: the rails are
   the page's edges, and the measure lives somewhere inside them.

   A CONSTANT, not a fraction of .wrap's gutter. The fraction was tried and
   pulled: it is zero exactly when the wrap fills the window, so the rails crept
   toward the glass as the window narrowed and the breakpoint had to keep
   climbing to outrun them, which cost every 1280-wide laptop its rails. A fixed
   inset holds the same distance at every width and the breakpoint stops moving.

   --rail-inset is declared here and read by .proj-bg further down, which has to
   end on the same line. One number, two use sites, no way to drift.

   Hidden at 1239px and below, which is where .wrap's own gutter closes to 44px
   and the rails would cross inside the text column they are supposed to sit
   outside of. attio.com hides its own at max-lg for the same reason. */
.rails {
  position: relative;
  --rail-inset: 44px;
}
.rails::before,
.rails::after {
  content: "";
  position: absolute; top: 0; bottom: 0;
  width: 1px;
  background: var(--rail, var(--rule));
  pointer-events: none;
  /* Above the band's own backgrounds and figures, below anything that has
     deliberately raised itself (the sticky nav is z-40 and clears this). */
  z-index: 2;
}
.rails::before { left: var(--rail-inset); }
.rails::after { right: var(--rail-inset); }

/* The dark bands. --green-line is the token that exists for exactly this:
   a green-black hairline that stays legible on the dark ground. */
.surfaces, .wedge, .whynow { --rail: var(--green-line); }

@media (max-width: 1239px) {
  .rails::before, .rails::after { display: none; }
  /* No rails to stop at, so the dot matrix goes back to full bleed. Zeroing the
     inset collapses both ground strips; see DOT GRID. */
  .rails.dotgrid { --rail-inset: 0px; }
}

/* ---- type ---- */
.kicker {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* THE SECTION EYEBROW IS A CHIP, ported from attio.com (measured 2026-08-27).
   It used to share the .kicker treatment above: 11px mono, uppercase, +0.14em.
   Every one of those is what made it read as fine print floating over a 40px
   serif headline, and none of it was the colour. Attio's is 14px sans, weight
   500, sentence case, NEGATIVE tracking, on a tinted fill with an 8px radius
   and no border. Bigger and calmer, which is why it reads as a confident tag.

   display:flex + width:fit-content, not inline-flex: the chip has to stay a
   block-level box so its margin-bottom still pushes the headline down, and so
   .centered is just auto margins.

   The fills are the accent at 10% (light) and 12% (dark) over the ground. Only
   two grounds exist in practice, --gray-light and --dark, checked across all
   four built pages: 4.84:1 and 5.39:1, both clear of the 4.5 floor. The label
   text is sentence case in the HTML already, so nothing had to be retyped. */
.eyebrow {
  display: flex; width: fit-content; align-items: center; gap: 7px;
  height: 24px; padding: 0 9px; border-radius: 8px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.14px;
  text-transform: none;
  color: var(--green-bold);
  background: rgba(47,110,34,0.10);
  margin-bottom: 18px;
  /* THE OPTICAL PULL. A chip is a box and the type under it aligns on glyphs,
     so 9px of left padding held every label 9px off a column whose headline,
     stat rules and caption all sit flush. Pulling the box left by exactly its
     padding puts the LETTERS on the column and lets the fill hang, which is
     the normal fix and the only one that keeps a chip in a left-aligned
     column. Centred heads override this below: they have no edge to break. */
  margin-left: -9px;
}

/* THE FILL IS ONLY FOR CENTRED HEADS, and this is the whole reason:
   a chip is a BOX and everything else in a section head is TYPE. Boxes align
   on their edges, type aligns on its glyphs, so 9px of horizontal padding put
   the label's letters at 187 while the headline, all four stat rules and the
   caption underneath sat at 178. In a left-aligned column that broken margin
   is the first thing the eye catches. Centred heads have no left edge to
   break, which is why the fill reads well on the beta CTA and nowhere else.
   The Attio type change (14px sans, sentence case, negative tracking) is the
   part that was actually fixing the old 11px mono fine print, and it stays
   everywhere. Only the surface is conditional. */
.eyebrow.centered { margin-left: auto; margin-right: auto; }
/* Colour and fill always travel together, so a ground can never get one
   without the other. */
/* ON THE DARK BANDS THE CHIP IS THE SAME RECIPE, MIRRORED. The light chip's
   fill is its own text colour at 10% over the ground; this is that rule read
   on dark, --green at 12% with --green text. One recipe covers both grounds,
   so adding a section later means no second decision to keep in sync.

   The solid pale chip this replaces was brighter than the white serif headline
   it labelled. A solid --green chip is not an option either: it is pixel
   identical to the CTA button (same fill, same text colour, same radius), so
   in the wedge band the section label and "Join the open beta" became the same
   object and the label read as clickable.

   Weight goes to 600 here against 500 on light. Green on a dark tint measures
   5.39:1 where the light chip measures 4.84:1 on a far brighter ground, and
   the extra weight is what keeps 14px legible at that ratio. */
.dark .eyebrow, .hero .eyebrow, .surfaces .eyebrow, .wedge .eyebrow, .whynow .eyebrow,
.bottom-cta .eyebrow, .tools2-copy .eyebrow, .page-head .eyebrow {
  background: rgba(122,173,106,0.12);
  color: var(--green);
  font-weight: 600;
}
.kicker { color: var(--green); display: inline-flex; align-items: center; gap: 9px; margin-bottom: 22px; }
.blink {
  width: 7px; height: 7px; border-radius: 50%; background: var(--green);
  animation: blink 2.4s var(--ease-in-out-cubic) infinite;
}
@keyframes blink { 0%, 100% { opacity: 1 } 50% { opacity: 0.22 } }

.h2 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(27px, 3.6vw, 40px);
  line-height: 1.14;
  letter-spacing: -0.022em;
  margin-bottom: 20px;
}
.centered { text-align: center; }

/* ---- buttons ---- */
.btn {
  display: inline-block;
  padding: 11px 21px;
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  border-radius: 4px;
  text-decoration: none;
  border: 1px solid transparent;
  /* Spring on arrival, per motion rule 9. Anything a hand touches gets one. */
  transition: transform 0.22s var(--ease-out-back), background 0.16s, border-color 0.16s, color 0.16s;
}
.btn-primary { background: var(--green); color: var(--dark); }
.btn-primary:hover { background: var(--green-hi); transform: translateY(-2px); }
.btn-primary:active { transform: translateY(0) scale(0.98); }
.btn-ghost { color: var(--white); border-color: #2f5230; }
.btn-ghost:hover { border-color: var(--green); color: var(--green); transform: translateY(-2px); }

/* THE PRIMARY CTA INVERTS ON LIGHT GROUND. Everything else on this page is
   green, so green is no longer a signal here: the oldest trick in the book is
   to make the one button you want pressed the opposite of every other button
   around it. Near black against a green page reads as the only thing to click.
   The design system already splits accents by ground (--green vs --green-bold),
   so this follows the same rule rather than inventing one.
   THE SELECTOR LIST IS THE GROUND, NOT THE SECTION. .btn-primary still has to
   stay green on the dark bands, where var(--dark) on var(--dark) is an
   invisible button, so the light bands are named one by one. That means THIS
   LIST HAS TO GROW WHENEVER A BAND CHANGES GROUND: .final was dark when this
   rule was written and was later moved to --gray-light, which left the same
   "Join the open beta" button near black in the hero and green at the foot of
   the same page, separating from its own band at 2.34:1. */
.hero .btn-primary,
.final .btn-primary { background: var(--dark); color: var(--white); border-color: var(--dark); }
.hero .btn-primary:hover,
.final .btn-primary:hover { background: #1a3520; border-color: #1a3520; }
.btn-lg { padding: 16px 34px; font-size: 15px; }

/* ---- reveal, blur not fade (DESIGN-SYSTEM 5) ---- */
.reveal { opacity: 0; filter: blur(7px); transform: translateY(16px); }
.reveal.in { opacity: 1; filter: none; transform: none; transition: all var(--reveal-dur) var(--ease-out-cubic); }
.stagger-in > * { opacity: 0; filter: blur(6px); transform: translateY(14px); }
.stagger-in.in > * { opacity: 1; filter: none; transform: none; transition: all 0.62s var(--ease-out-cubic); }
.stagger-in.in > *:nth-child(2) { transition-delay: 0.08s }
.stagger-in.in > *:nth-child(3) { transition-delay: 0.16s }
.stagger-in.in > *:nth-child(4) { transition-delay: 0.24s }
.stagger-in.in > *:nth-child(5) { transition-delay: 0.32s }

/* ============ HERO ============
   Light, not dark. The fuel chain sheet is ink on paper, and floating a
   near-white document inside a dark green mass fought it: the frame read as a
   hole punched in the page. On light ground the diagram sits on the page
   rather than on top of it.

   The nav stays dark, so the brand mass is still the first thing on screen and
   the alternation rule holds: dark nav, light hero, white proof band would be
   two lights in a row, which is why the proof band below now carries a rule
   instead of a fill.
   ============================================================ */
.hero {
  background: var(--gray-light);
  color: var(--text);
  /* Deep top padding is doing real work: it pushes the product down so the
     FOLD lands in its upper third. Attio's product starts at y=580 of a 900
     viewport and is guillotined there, and the crop is the whole trick, since
     what you cannot see is what makes it read as a deep product. Ours starts
     around the same line. Bottom padding is small because the product is
     MEANT to run past the fold, not to be contained by the section. */
  padding: 128px 0 84px;
  position: relative;
  overflow: hidden;
}
/* THE GROUND, in three layers, which is what a flat fill cannot do.
 *
 * Attio stacks a saturated periwinkle radial rising from the bottom centre
 * (rgb(134,160,238) at full strength), a fine texture over it, and a white
 * radial from the top that erases both behind the headline. Measured off the
 * live page. The effect is that the product looks LIT and the copy still sits
 * on clean ground. One flat colour cannot be both.
 *
 * ::before is the light source. Green rather than blue, because ENTEL has one
 * accent and this is it. Anchored at 50% 100%, so it is brightest exactly
 * where the product sits and falls off before it reaches the type. */
.hero::before {
  content: "";
  /* FIXED HEIGHT, not a percentage of the section. The hero is 1309px tall
     because the product deliberately runs past the fold, so a radial anchored
     at "50% 100%" centres its light more than 400px BELOW the screen and the
     first view gets no glow at all. That is exactly what happened on the first
     build. Anchoring to a fixed 1150px box puts the light at roughly y=780,
     which is behind the visible upper third of the product, so it rims the
     shot instead of hiding under it. */
  position: absolute; inset: 0 0 auto 0; height: 1150px;
  background: radial-gradient(104% 56% at 50% 62%,
              rgba(122,173,106,0.50) 0%,
              rgba(122,173,106,0.22) 42%,
              rgba(122,173,106,0) 72%);
  pointer-events: none; z-index: 0;
}
/* ::after is texture THEN mask, and the order inside the shorthand is the whole
   rule: the first background listed paints on top. So the light wash is
   declared first and erases the dot matrix behind the headline; the dots are
   declared second and sit under it, over the green. Reversed, the matrix runs
   straight through the type and the headline sits on graph paper.
   Same 22px matrix as .dotgrid, in a green-grey that survives the light. */
.hero::after {
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(780px 440px at 50% 0px, var(--gray-light) 26%, rgba(242,242,240,0) 76%),
    radial-gradient(rgba(88,116,80,0.20) 1px, transparent 1px);
  background-size: auto, 22px 22px;
  background-position: 0 0, -1px -1px;
  pointer-events: none; z-index: 0;
}
.hero-in { position: relative; z-index: 1; text-align: center; max-width: 1000px; }
/* THE PILL, AND THE LIGHT THAT TRAVELS ITS BORDER.
 *
 * CONSTANT SPEED IS THE WHOLE POINT, and it is why this is a stroked SVG path
 * rather than the rotating conic gradient it started as. A conic gradient turns
 * at a constant ANGULAR rate, but this pill is a 310x29 rounded rectangle, so a
 * fixed slice of angle covers a huge distance along the long straight edges and
 * almost none around the end caps. The light visibly raced the sides and
 * crawled the corners. Animating a dash along the path instead moves it at a
 * constant ARC LENGTH rate, which is the even circling Attio has.
 *
 * pathLength="100" is what makes it responsive. It renormalises the path to 100
 * user units whatever the pill's real size, so the dash lengths below are
 * effectively percentages and the timing never changes when the copy does.
 * rx AND ry are both 13.75, which is half the ring box's height (the pill is
 * 29px tall and the svg is inset 0.75px on each side). rx="999" was tried first
 * on the assumption SVG clamps it to a pill: it does not. It clamps rx to half
 * the WIDTH and ry to half the HEIGHT independently, so a 310x27.5 box became a
 * 155x13.75 ellipse corner and the "ring" rendered as a giant arc sweeping
 * outside the pill entirely. If the pill's height ever changes, this changes
 * with it.
 *
 * Three strokes, one animation: a constant faint ring, a long dim tail, and a
 * short bright head riding the same offset. That is what reads as a comet
 * rather than a dash. All three greens, because the head was previously a near
 * white (#eafbe2) that disappeared against the light ground. */
.hero-pill {
  position: relative;
  display: inline-flex; align-items: center; gap: 8px;
  margin: 0 auto 26px;
  padding: 7px 16px;
  border-radius: 999px;
  background: var(--gray-light);
  color: var(--text);
  font-size: 12.5px; font-weight: 600; letter-spacing: -0.01em;
  text-decoration: none; white-space: nowrap;
  transition: transform 0.22s var(--ease-out-back), background 0.16s;
}
/* WIDTH AND HEIGHT ARE NOT OPTIONAL HERE. <svg> is a replaced element, so with
   width:auto it falls back to its intrinsic default of 300x150 and `inset`
   alone does NOT resize it: the ring rendered as a 300x150 box hanging off the
   pill. It looked correct only because the frame I checked was cropped tight
   enough to show just the box's top edge, which passed for a pill outline.
   Inset by half the stroke so the ring lands ON the pill edge rather than being
   clipped in half by the element box; overflow visible for the head's glow. */
.pill-ring {
  position: absolute; left: 0.75px; top: 0.75px;
  width: calc(100% - 1.5px); height: calc(100% - 1.5px);
  overflow: visible; pointer-events: none;
}
.pill-ring rect { fill: none; stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.ring-base { stroke: rgba(47,110,34,0.22); }

/* THE COMET.
 *
 * A stroke cannot carry a gradient ALONG its own length, so the fade is built
 * from many short dashes riding the same lap, each one STEP further back than
 * the last and a shade lighter, ending on the band's own ground colour so the
 * tail dissolves rather than stopping.
 *
 * DIRECTION: the trail offsets are POSITIVE. stroke-dashoffset moves a dash
 * BACKWARD along the path as it grows, so negative offsets put each successive
 * segment further FORWARD, which ran the comet tail first with the dark head
 * chasing it. Positive is what puts the head in front.
 *
 * COUNT: 26 segments of 1.6 units on a 0.62 step, so each one covers roughly
 * two and a half times its own spacing and its neighbours overlap heavily. An
 * earlier version used 9 segments of 2.7 on a 2.5 step, which barely overlapped
 * at all and read as a row of separate dashes rather than one fading line. The
 * overlap is what removes the banding; the count alone would not.
 *
 * Only the head is thickened and given a glow. That is the "dot". */
.ring-seg { stroke-dasharray: 1.6 98.4; }
.rc0 { stroke: rgb(47,110,34); opacity: 1; stroke-width: 1.9; filter: drop-shadow(0 0 2.6px rgba(47,110,34,0.5)); animation: run0 3.6s linear infinite; }
@keyframes run0 { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } }
.rc1 { stroke: rgb(62,120,50); opacity: 0.9406; animation: run1 3.6s linear infinite; }
@keyframes run1 { from { stroke-dashoffset: 0.62; } to { stroke-dashoffset: -99.38; } }
.rc2 { stroke: rgb(73,128,61); opacity: 0.8824; animation: run2 3.6s linear infinite; }
@keyframes run2 { from { stroke-dashoffset: 1.24; } to { stroke-dashoffset: -98.76; } }
.rc3 { stroke: rgb(83,134,72); opacity: 0.8255; animation: run3 3.6s linear infinite; }
@keyframes run3 { from { stroke-dashoffset: 1.86; } to { stroke-dashoffset: -98.14; } }
.rc4 { stroke: rgb(92,140,82); opacity: 0.7699; animation: run4 3.6s linear infinite; }
@keyframes run4 { from { stroke-dashoffset: 2.48; } to { stroke-dashoffset: -97.52; } }
.rc5 { stroke: rgb(101,146,91); opacity: 0.7155; animation: run5 3.6s linear infinite; }
@keyframes run5 { from { stroke-dashoffset: 3.1; } to { stroke-dashoffset: -96.9; } }
.rc6 { stroke: rgb(109,152,100); opacity: 0.6626; animation: run6 3.6s linear infinite; }
@keyframes run6 { from { stroke-dashoffset: 3.72; } to { stroke-dashoffset: -96.28; } }
.rc7 { stroke: rgb(117,158,108); opacity: 0.6109; animation: run7 3.6s linear infinite; }
@keyframes run7 { from { stroke-dashoffset: 4.34; } to { stroke-dashoffset: -95.66; } }
.rc8 { stroke: rgb(125,163,117); opacity: 0.5607; animation: run8 3.6s linear infinite; }
@keyframes run8 { from { stroke-dashoffset: 4.96; } to { stroke-dashoffset: -95.04; } }
.rc9 { stroke: rgb(133,168,125); opacity: 0.512; animation: run9 3.6s linear infinite; }
@keyframes run9 { from { stroke-dashoffset: 5.58; } to { stroke-dashoffset: -94.42; } }
.rc10 { stroke: rgb(141,173,133); opacity: 0.4648; animation: run10 3.6s linear infinite; }
@keyframes run10 { from { stroke-dashoffset: 6.2; } to { stroke-dashoffset: -93.8; } }
.rc11 { stroke: rgb(148,178,141); opacity: 0.4191; animation: run11 3.6s linear infinite; }
@keyframes run11 { from { stroke-dashoffset: 6.82; } to { stroke-dashoffset: -93.18; } }
.rc12 { stroke: rgb(155,183,149); opacity: 0.375; animation: run12 3.6s linear infinite; }
@keyframes run12 { from { stroke-dashoffset: 7.44; } to { stroke-dashoffset: -92.56; } }
.rc13 { stroke: rgb(163,188,156); opacity: 0.3326; animation: run13 3.6s linear infinite; }
@keyframes run13 { from { stroke-dashoffset: 8.06; } to { stroke-dashoffset: -91.94; } }
.rc14 { stroke: rgb(170,193,164); opacity: 0.2919; animation: run14 3.6s linear infinite; }
@keyframes run14 { from { stroke-dashoffset: 8.68; } to { stroke-dashoffset: -91.32; } }
.rc15 { stroke: rgb(177,198,171); opacity: 0.253; animation: run15 3.6s linear infinite; }
@keyframes run15 { from { stroke-dashoffset: 9.3; } to { stroke-dashoffset: -90.7; } }
.rc16 { stroke: rgb(183,202,178); opacity: 0.216; animation: run16 3.6s linear infinite; }
@keyframes run16 { from { stroke-dashoffset: 9.92; } to { stroke-dashoffset: -90.08; } }
.rc17 { stroke: rgb(190,207,185); opacity: 0.181; animation: run17 3.6s linear infinite; }
@keyframes run17 { from { stroke-dashoffset: 10.54; } to { stroke-dashoffset: -89.46; } }
.rc18 { stroke: rgb(197,211,192); opacity: 0.1482; animation: run18 3.6s linear infinite; }
@keyframes run18 { from { stroke-dashoffset: 11.16; } to { stroke-dashoffset: -88.84; } }
.rc19 { stroke: rgb(204,216,199); opacity: 0.1176; animation: run19 3.6s linear infinite; }
@keyframes run19 { from { stroke-dashoffset: 11.78; } to { stroke-dashoffset: -88.22; } }
.rc20 { stroke: rgb(210,220,206); opacity: 0.0894; animation: run20 3.6s linear infinite; }
@keyframes run20 { from { stroke-dashoffset: 12.4; } to { stroke-dashoffset: -87.6; } }
.rc21 { stroke: rgb(217,225,213); opacity: 0.064; animation: run21 3.6s linear infinite; }
@keyframes run21 { from { stroke-dashoffset: 13.02; } to { stroke-dashoffset: -86.98; } }
.rc22 { stroke: rgb(223,229,220); opacity: 0.0416; animation: run22 3.6s linear infinite; }
@keyframes run22 { from { stroke-dashoffset: 13.64; } to { stroke-dashoffset: -86.36; } }
.rc23 { stroke: rgb(229,233,227); opacity: 0.0226; animation: run23 3.6s linear infinite; }
@keyframes run23 { from { stroke-dashoffset: 14.26; } to { stroke-dashoffset: -85.74; } }
.rc24 { stroke: rgb(236,238,233); opacity: 0.008; animation: run24 3.6s linear infinite; }
@keyframes run24 { from { stroke-dashoffset: 14.88; } to { stroke-dashoffset: -85.12; } }
.rc25 { stroke: rgb(242,242,240); opacity: 0; animation: run25 3.6s linear infinite; }
@keyframes run25 { from { stroke-dashoffset: 15.5; } to { stroke-dashoffset: -84.5; } }

.hero-pill:hover { background: #eef1ec; transform: translateY(-1px); }
.pill-arrow { width: 13px; height: 13px; color: var(--text-muted); transition: transform 0.22s var(--ease-out-back); }
.hero-pill:hover .pill-arrow { transform: translateX(3px); }

/* THE SAME ARROW, ON A LINK. Carried by the hero's second action once it stops
   being a button (below 767, see the hero ladder). Same size, same curve, same
   3px travel as .pill-arrow, because they are the same gesture and a second
   easing for it would read as a second component. Hidden until the button
   becomes a link: an arrow inside a bordered button is two affordances arguing.
   :active as well as :hover, because the width where this appears is the width
   with no hover at all, and an arrow that only moves for a mouse is an arrow
   that never moves. */
.link-arrow {
  display: none; flex: none; width: 13px; height: 13px;
  transition: transform 0.22s var(--ease-out-back);
}

/* THE HERO CAPTURE. Phone only, so it is absent from the flow (not merely
   invisible) at every width where the fold already has room to send someone to
   /beta and back. Styled here rather than borrowed from .wl-form because that
   form is written for /beta's dark band: reusing it would have put a
   near-black field on the hero's paper ground. */
.hero-join { display: none; width: 100%; }
.hero-join input[type="email"] {
  width: 100%; padding: 14px 15px;
  font-family: var(--sans); font-size: 16px;   /* 16px, or iOS zooms the page on focus */
  color: var(--text); background: var(--white);
  border: 1px solid var(--rule); border-radius: 5px; outline: none;
  transition: border-color 0.16s, box-shadow 0.16s;
}
.hero-join input::placeholder { color: var(--text-faint); }
.hero-join input:focus {
  border-color: var(--green-bold);
  box-shadow: 0 0 0 3px rgba(47,110,34,0.14);
}
.hero-join button {
  width: 100%; margin-top: 10px; padding: 14px 22px;
  font-family: var(--sans); font-size: 14.5px; font-weight: 700; letter-spacing: 0.01em;
  color: var(--white); background: var(--dark);
  border: 0; border-radius: 5px; cursor: pointer;
  transition: transform 0.22s var(--ease-out-back), background 0.16s;
}
.hero-join button:hover:not(:disabled) { background: #1a3520; }
.hero-join button:active:not(:disabled) { transform: scale(0.985); }
.hero-join button:disabled { opacity: 0.6; cursor: default; }
/* The confirmation takes the form's place rather than appearing under it, so
   the fold does not grow by a line at the moment it is answered. */
.hero-join-msg { display: none; font-size: 13.5px; line-height: 1.55; color: var(--green-bold); margin: 0; text-align: center; }
.hero-join-msg.err { color: #a4331d; }
.hero-join.done input[type="email"], .hero-join.done button { display: none; }
.hero-join-msg:not(:empty) { display: block; padding-top: 10px; }
.btn-ghost:hover .link-arrow, .btn-ghost:active .link-arrow { transform: translateX(3px); }

.hero-h1 {
  font-family: var(--serif);
  font-weight: 400;
  /* ONE LINE, and the size follows from that. The old headline was ten words
     over two lines at 50px, which is a sentence set large; attio's is four
     words on one line at 64px with the leading crushed to 0.95, which is a
     statement. A wrapped headline can never land the way an unwrapped one
     does, so the copy was cut to fit the line rather than the size cut to fit
     the copy. Leading below 1 is deliberate: it makes the words read as a
     single mass instead of a row of text. */
  font-size: clamp(36px, 6vw, 72px);
  line-height: 0.98;
  letter-spacing: -0.035em;
  margin-bottom: 30px;
  color: var(--text);
  text-wrap: balance;
}
/* The second beat, dimmed. Same device .whynow-h, .proj-h and .ov-dim already
   use: claim in ink, consequence in muted, so a two-line headline reads as one
   sentence with a turn rather than as two competing claims. The one-line intent
   above is already spent (the headline names the buyer now, which costs words),
   so the choice here is between one long wrapped line and two deliberate ones. */
.hero-dim { color: var(--text-muted); }
/* NO ACCENT COLOUR IN THE HEADLINE. Attio spends zero saturated colour outside
   the product: the page is black type on white with one dark button, and the
   only colour on the screen is inside the app shot. That is why the eye has
   nowhere else to go. Ours was spending green on the headline, the primary
   button and the nav button before the reader reached the product. */
/* STATIC, and back inside .hero-in. Revealing this on scroll was tried and
   pulled: it defers the one sentence that says what the product is until after
   the reader has decided whether to keep scrolling, and it faded in through the
   same 200px where the headline was blurring out, so two motions fought over
   one band of the page. Attio's deck is static in the first view for the same
   reason. Cut from 152 characters to 83 at the same time; the cadence spec it
   used to carry now lives in the FAQ, where it is explained rather than
   asserted, and the source count is what the section below the hero is for. */
.hero-deck {
  font-size: clamp(14px, 1.2vw, 16.5px);
  line-height: 1.62;
  color: #4d5a4a;
  max-width: 520px;
  margin: 0 auto 30px;
}
/* The deck breaks before "that" so the two halves of the claim sit on their own
   lines. Below 640 the deck already wraps on its own and the forced break just
   orphans the tail, so it is dropped there. */
@media (max-width: 640px) { .deck-br { display: none; } }
.hero-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.centered-cta { justify-content: center; }
/* NOT USED BY THE HOMEPAGE ANY MORE. The Wire line was a third call to action
   in a fold that should have one job, and the nav already carries "Newsletter",
   so it was cut from the hero entirely. These rules stay because briefing.html
   also loads site.css and uses .hero-fine in two places; removing them because
   this page stopped using the class would break a page that never asked. */
.hero-fine { font-size: 13px; color: #6b7a68; margin: 26px auto 0; max-width: 940px; text-align: center; position: relative; z-index: 1; }
.hero-fine a { color: var(--green-bold); text-decoration-color: rgba(47,110,34,0.4); text-underline-offset: 3px; }
.hero-fine a:hover { color: var(--green); }

/* On light ground the ghost button needs a real border to exist at all. Same
   rule as the primary above and the same maintenance burden: name every light
   band. .final joined this list when its band went from dark to --gray-light. */
.hero .btn-ghost, .final .btn-ghost { color: var(--text); border-color: var(--rule); background: var(--white); }
.hero .btn-ghost:hover, .final .btn-ghost:hover { border-color: var(--green-bold); color: var(--green-bold); transform: translateY(-2px); }

/* THE HANDOFF.
 *
 * Attio's move, and the reason it works: the headline is not scrolled past, it
 * is handed over. As the page moves, the words blur back and lift out while the
 * product cluster rises into the space they vacate, so one object replaces
 * another rather than a column of things sliding by.
 *
 * Driven by a scroll timeline, not a scroll listener, so it composites off the
 * main thread (motion rule 13). Browsers without scroll-driven animations get
 * the finished state, which is the hero fully legible and static: the effect is
 * a flourish, never load-bearing.
 *
 * Two animations, two elements. The entrance runs on load against the inner
 * children; the handoff runs on the scroll timeline against their wrappers, so
 * the two never fight for the same properties.
 */
@keyframes handoffOut {
  to { opacity: 0; filter: blur(14px); transform: translateY(-64px) scale(0.97); }
}
@keyframes stageRise { to { transform: translateY(-76px) scale(1.03); } }
@keyframes flDriftA  { to { transform: translate(-38px, -34px) rotate(-3.2deg); } }
@keyframes flDriftB  { to { transform: translate(34px, -50px) rotate(2.6deg); } }
@keyframes flDriftC  { to { transform: translate(30px, -26px) rotate(2.2deg); } }

@supports (animation-timeline: scroll()) {
  .hero-in, .hero-stage {
    animation-timeline: scroll(root block);
    animation-fill-mode: both;
    animation-timing-function: linear;
  }
  .hero-in    { animation-name: handoffOut; animation-range: 0 62vh; }
  .hero-stage { animation-name: stageRise;  animation-range: 0 62vh; }
}

/* The stage: one lead clip with three widgets hung off it. */
/* 940. Briefly widened to 1120 for legibility, then pulled back: at 1120 the
   clip stood 735px tall and the fuel chain read as zoomed in rather than as a
   product being shown, and it drove the whole section to 1309px. The card is
   scaled to its container, so its width is the only lever on the section's
   height. Attio's is 1080 of a 1440 viewport, but its content is dense small
   UI where ours is a large diagram, so matching its ratio oversells ours. */
.hero-stage { position: relative; max-width: 940px; margin: 52px auto 0; z-index: 3; }
.shot-lead {
  opacity: 0;
  transform: translateY(34px) scale(0.975);
  filter: blur(8px);
  animation: heroland 1.15s var(--ease-out-expo) 0.18s forwards;
}
@keyframes heroland { to { opacity: 1; transform: none; filter: none; } }

.floater {
  position: absolute;
  border-radius: 10px;
  overflow: hidden;
  background: var(--dark-deep);
  border: 1px solid rgba(30,58,30,0.5);
  box-shadow: 0 26px 54px -20px rgba(20,40,20,0.42), 0 2px 8px rgba(20,40,20,0.1);
  /* No load animation any more. The three widgets used to all fade in within
     0.86s of page load, which spent them before the reader had read the
     headline, and left the first view carrying four things at once. They are
     now scroll-driven and staggered, so they arrive one at a time as the
     product rises. See flA/flB/flC below. */
  opacity: 0;
}
@keyframes floatIn { to { opacity: 1; } }
.floater video { display: block; width: 100%; height: auto; }
.fl-ring { width: 300px; left: -168px;  top: 104px; }
.fl-map  { width: 384px; right: -186px; top: 12px; }
/* 25% wider than the other two carried it (336 -> 420). Anchored by `right`,
   so the extra width grows LEFT and up: the gap to the viewport edge is still
   the 132px the 1259px breakpoint below is calculated from, and nothing about
   that threshold moves. */
.fl-opps { width: 420px; right: -132px; bottom: -78px; }

/* Each floater carries TWO animations: a time-based fade on load and a
 * scroll-driven drift, each on its own timeline. They have to be declared
 * together in one shorthand, because setting animation-name alone would replace
 * the entrance rather than join it, and the floater would never fade in.
 *
 * Declared after .floater on purpose: an earlier @supports block lost to the
 * .floater shorthand at equal specificity, so the drift silently never ran.
 *
 * Different rates and rotations per floater, which is what stops the cluster
 * reading as one flat image being slid around. */
/* POP, THEN DRIFT, IN ONE ANIMATION EACH.
 *
 * It has to be one animation per element, not two. Pop wants to animate
 * transform and so does drift, and when two animations touch the same property
 * the later one in the list wins outright: a separate pop would be silently
 * overwritten by the drift for the entire scroll, so the widgets would slide
 * without ever popping. Folding both into one keyframe set makes the conflict
 * impossible rather than merely avoided.
 *
 * The stagger lives in the keyframe percentages, not in animation-delay, which
 * has no meaning on a scroll timeline. Each widget is still at 0 opacity when
 * the one before it is already landing, so they arrive one at a time over the
 * first ~20vh, quickly, then all three drift for the rest of the range. The
 * overshoot on the landing stop is what makes it read as a pop rather than a
 * fade (motion rule 9: arrivals get a spring, not a duration). */
@keyframes flA {
  0%   { opacity: 0; transform: translateY(22px) scale(0.90); }
  7%   { opacity: 1; transform: translateY(-3px) scale(1.012); }
  11%  { opacity: 1; transform: none; }
  100% { opacity: 1; transform: translate(-38px, -34px) rotate(-3.2deg); }
}
@keyframes flB {
  0%,  8% { opacity: 0; transform: translateY(22px) scale(0.90); }
  15%     { opacity: 1; transform: translateY(-3px) scale(1.012); }
  19%     { opacity: 1; transform: none; }
  100%    { opacity: 1; transform: translate(34px, -50px) rotate(2.6deg); }
}
@keyframes flC {
  0%,  16% { opacity: 0; transform: translateY(22px) scale(0.90); }
  23%      { opacity: 1; transform: translateY(-3px) scale(1.012); }
  27%      { opacity: 1; transform: none; }
  100%     { opacity: 1; transform: translate(30px, -26px) rotate(2.2deg); }
}
@supports (animation-timeline: scroll()) {
  .fl-ring { animation: flA linear both; }
  .fl-map  { animation: flB linear both; }
  .fl-opps { animation: flC linear both; }
  /* AFTER the shorthand, never before. The `animation` shorthand resets every
     animation-* longhand including animation-timeline, so declaring the
     timeline first silently reverted these to time-based animations: they ran
     once on load and sat at 100%, leaving the widgets permanently at their end
     position and never responding to scroll at all. */
  .fl-ring, .fl-map, .fl-opps {
    animation-timeline: scroll(root block);
    animation-range: 0 78vh;
  }
}
/* No scroll timeline: they fade in on load, staggered, exactly as before. The
   pop is a flourish and is never the thing that makes the widgets exist. */
@supports not (animation-timeline: scroll()) {
  .fl-ring { animation: floatIn 1s var(--ease-out-expo) 0.62s forwards }
  .fl-map  { animation: floatIn 1s var(--ease-out-expo) 0.74s forwards }
  .fl-opps { animation: floatIn 1s var(--ease-out-expo) 0.86s forwards }
}

.shot-cap {
  margin: 22px auto 0; max-width: 940px; text-align: center;
  font-size: 13px; line-height: 1.6; color: var(--text-muted);
  opacity: 0; animation: heroland 0.9s var(--ease-out-cubic) 0.72s forwards;
}
.shot-cap a { color: var(--green-bold); text-underline-offset: 3px; }
.shot-cap-left { text-align: left; }

/* .shot in a normal page band, rather than the homepage hero stage. Same frame
   and same chrome bar, since this is the same object doing the same job; it just
   needs its own width and rhythm because there is no .hero-stage around it. */
.shot-page { max-width: 940px; margin: 0 auto 8px; }
.shot-page + .shot-cap { margin-bottom: 46px; }
.shot-page:last-child { margin-bottom: 34px; }
.shot-page img { display: block; width: 100%; height: auto; }
@media (max-width: 767px) {
  .shot-page { margin-bottom: 4px; }
  .shot-page .shot-bar { padding: 7px 10px; }
  .shot-page .shot-bar span { width: 9px; height: 9px; }
}

.hero-in > * { opacity: 0; animation: heroland 0.85s var(--ease-out-cubic) forwards; }
.hero-in > *:nth-child(1) { animation-delay: 0.04s }
.hero-in > *:nth-child(2) { animation-delay: 0.12s }
.hero-in > *:nth-child(3) { animation-delay: 0.20s }
.hero-in > *:nth-child(4) { animation-delay: 0.28s }

.shot {
  border-radius: 9px;
  overflow: hidden;
  border: 1px solid var(--rule);
  box-shadow: 0 34px 70px -26px rgba(20,40,20,0.34), 0 3px 10px rgba(20,40,20,0.08);
  background: var(--white);
}
/* Real browser chrome: light bar, real traffic lights. The dark version read as
   part of the product rather than as the window around it, which is the opposite
   of what the frame is for. Colours are the macOS ones. */
.shot-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px;
  background: #e8e8e6;
  border-bottom: 1px solid #d3d3d0;
}
.shot-bar span { width: 11px; height: 11px; border-radius: 50%; }
.shot-bar span:nth-child(1) { background: #ff5f57; }
.shot-bar span:nth-child(2) { background: #febc2e; }
.shot-bar span:nth-child(3) { background: #28c840; }

/* Dark chrome, for a frame on a dark band. Same traffic lights, dimmed, so the
   window still reads as a window rather than as part of the product. */
.shot-dark { border-color: rgba(255,255,255,0.14); background: #0f1f0f; }
.shot-dark .shot-bar { background: #1b2f1b; border-bottom-color: rgba(255,255,255,0.10); }
.shot-dark .shot-bar span { opacity: 0.62; }

/* ---- THE HERO, DOWN THE LADDER ----
 * Three things break as the window narrows and each has its own fix.
 *
 * 1. THE STAGE HAS NO GUTTER. .hero-stage is max-width 940 with auto margins
 *    and it is not inside .wrap, so the moment the window is narrower than 940
 *    the margins go to zero and the frame runs to the glass. Worse, .stageRise
 *    ends on scale(1.03), which put the left edge at -6px on a phone: the page
 *    was showing a product card with a corner missing. A gutter, held by the
 *    same number .wrap uses, so the frame lines up with every band below it.
 *
 * 2. THE SHEET GOES ILLEGIBLE BEFORE IT GOES SMALL. Same argument as the
 *    showcase frames: the fuel chain is a wide diagram of small labels, and
 *    scaled to a 390px phone it is a grey rectangle that argues against itself.
 *    So the video keeps a readable width and .shot-scroll crops it. The chrome
 *    bar stays put above, which is why the scroller is a wrapper and not .shot.
 *
 * 3. THE CAPTION AND THE DECK KEEP DESKTOP MEASURES. Both are centred with a
 *    max-width and no padding of their own.
 */
@media (max-width: 1023px) {
  .hero { padding: 96px 0 68px; }
  /* A wider gutter than the page's, because this is the one object on the page
     that is a picture of a window: it needs air around it to read as one, and
     the page gutter that is right for a paragraph is too tight for a frame. */
  .hero-stage { max-width: 940px; padding: 0 calc(var(--gut) + 20px); }
  /* Breaks earlier and sits inside the frame's own margins. Full measure, this
     caption ran the width of the sheet in two long lines and read as body copy
     under a figure rather than as the figure's label. */
  .shot-cap { padding: 0 calc(var(--gut) + 28px); max-width: 62ch; }


  /* The sheet, cropped rather than shrunk. Same clamp as .sc-frame video and
     for the same two reasons: 720px is where its own labels stop being
     readable, 1060px is the widest worth asking a thumb to travel. */
  .shot-scroll {
    overflow-x: auto; overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .shot-scroll::-webkit-scrollbar { display: none; }
  .shot-scroll video { display: block; width: 100%; max-width: 100%; height: auto; }
}
/* THE CAPTURE COMES UP TO THE TABLET (Jace, 2026-08-28).
   Same argument as the phone: a tablet reader is one tap from leaving and the
   fold is where the ask belongs.

   KEYED ON TOUCH, NOT ON WIDTH ALONE, and that is not a flourish: iPad
   landscape is exactly 1024 CSS pixels, so a `max-width: 1023px` rule missed
   the single most common tablet in the world by one pixel. The second clause
   catches every touch device up to the tablet-landscape band while leaving a
   1100px laptop window, which has a mouse and a full-size fold, with the two
   buttons it should have.

   Held to a measure rather than run full width, because a 712px email field is
   a form pretending to be a section. */
@media (max-width: 1023px), (max-width: 1239px) and (hover: none) {
  .hero-cta { flex-direction: column; align-items: stretch; gap: 10px;
              max-width: 400px; margin-left: auto; margin-right: auto; }
  .hero-cta .btn-primary { display: none; }
  .hero-join { display: block; }
  .hero .hero-cta .btn-ghost {
    width: auto; align-self: center;
    display: inline-flex; align-items: center; gap: 7px;
    background: none; border: 0; box-shadow: none;
    padding: 10px 4px; margin-top: 2px;
    color: var(--green-bold);
    text-decoration: underline;
    text-decoration-color: rgba(47,110,34,0.34);
    text-underline-offset: 4px;
  }
  .hero .hero-cta .btn-ghost:hover,
  .hero .hero-cta .btn-ghost:active {
    background: none; border: 0; transform: none;
    color: var(--green-bold); text-decoration-color: var(--green-bold);
  }
  .hero .hero-cta .link-arrow { display: block; }
}

/* THE PHONE FITS TOO (Jace, 2026-08-28, reversing the crop here).
   The crop is right for the five showcase clips, which are frames inside a
   section and read as "there is more app here". The hero sheet is the FIRST
   object on the page and sits under a caption telling you to hover it, so a
   reader arriving on a phone met a diagram they had to drag sideways before
   they could see what it was. A small complete diagram beats a legible third of
   one in the position that has to land in a second. The scroller stays in the
   markup and in the rule above; at this width it has nothing to scroll. */
@media (max-width: 767px) {
  .hero { padding: 66px 0 54px; }
  .hero-stage { margin-top: 38px; }
  /* Stacked, and the two actions stop being peers.
     Two full-width buttons on a phone are two equal asks stacked on top of each
     other, and the fold has one job. The primary keeps the button; the second
     becomes what it always was underneath, a link to the demo, and it takes the
     Wire pill's arrow so the page's two "go and look" gestures are the same
     gesture. attio.com does this exact thing at 390: "Send me a demo" is the
     filled button and "Talk to sales ->" underneath it is a text link. */
  /* The CTA block, the form and the arrow link are all inherited from the
     tablet rule above. On a phone the measure is the window. */
  .hero-cta { max-width: none; }
  .hero-stage { padding: 0 var(--gut); }
  .shot-cap { padding: 0 var(--gut); }
}

/* ============ PROOF ============ */
.proof { background: var(--gray-light); padding: 88px 0 92px; position: relative; z-index: 1; border-top: 1px solid var(--rule); }
.proof .h2 { margin-bottom: 44px; }
.stats {
  list-style: none;
  display: grid; grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.stats li { padding: 30px 16px; text-align: center; border-right: 1px solid var(--rule); }
.stats li:last-child { border-right: 0; }
.stats b {
  display: block;
  font-family: var(--serif);
  font-size: clamp(30px, 3.6vw, 46px);
  font-weight: 400;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.stats span {
  display: block; margin-top: 8px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-muted);
}
.proof-fine {
  max-width: 640px; margin: 26px auto 0; text-align: center;
  font-size: 14px; line-height: 1.65; color: var(--text-muted);
}

/* ============ SURFACES : the pinned scroll-through ============
   Attio's platform showcase, in ENTEL's system. The section is five viewports
   tall; the stage inside is sticky, so a scroll advances the panel rather than
   leaving the section. Five zero-height .sc-trip markers spaced down the
   section are what an IntersectionObserver watches, which keeps the whole thing
   off the scroll event and therefore off the main thread (motion rule 13).

   The panel is copy above, product below, full width. The rail across the top
   is a progress indicator: it answers "where am I in five", and the panel's own
   headline answers "what does this surface claim". Before this the rail was the
   section's entire structure and no panel made a claim at all, so five real
   product recordings read as a slideshow of screens.
   ============================================================ */
.surfaces { background: var(--dark); color: var(--white); padding: 92px 0 0; }
.sc-head { padding-bottom: 6px; }
.sc-head .h2 { margin-bottom: 0; }

.sc { position: relative; height: 500vh; }
.sc-trip { position: absolute; left: 0; width: 1px; height: 1px; }
.sc-trip:nth-of-type(1) { top: 10vh }
.sc-trip:nth-of-type(2) { top: 110vh }
.sc-trip:nth-of-type(3) { top: 210vh }
.sc-trip:nth-of-type(4) { top: 310vh }
.sc-trip:nth-of-type(5) { top: 410vh }

/* Capped rather than full-viewport. The clip is width-constrained at 446px tall,
   so a full 838px stage left 180px that had to go somewhere and always landed
   under the product. Capping the stage at the height the content actually needs
   lets .sc-body's auto margins land on 113px above AND below, which is the same
   air the wedge section puts over the DOE letter. .surfaces is var(--dark), so
   whatever the viewport has spare below the stage is the same ground. */
.sc-stage {
  position: sticky; top: 62px; height: min(calc(100vh - 62px), 730px);
  display: flex; align-items: center;
}
/* Wider than the page's reading measure on purpose. This section's job is to
   let someone watch the product work, so the frame is the dominant object. */
/* The width follows the PRODUCT, not the page.
   A 1280x800 recording in a stage 838px tall can never be wider than about
   1010px once the rail and the copy have taken their share, so a 1400px
   container left 190px of empty dark down each side and the panel read as a
   small picture floating in a big band. Sized to what the clip can actually
   fill, the copy above it gets a better measure as well. */
.sc-inner {
  width: 100%; max-width: 1180px; margin: 0 auto;
  /* Fills the sticky stage so no slack accumulates OUTSIDE the box. Hugging the
     content instead pushed the rail 141px down from the section head and left
     the same amount stranded under the product, which read as the panel being
     bottom-heavy. The rail now sits at the top of the stage and .sc-body's auto
     block margins split every remaining pixel evenly above and below the clip. */
  height: 100%;
  padding: 18px 32px 0; display: flex; flex-direction: column; gap: 0;
}

/* The rail, horizontal. Five equal columns so the moving segment underneath is
   a true fifth of the width and its position reads as a fraction of the whole. */
.tab-rail {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  position: relative; flex: none; padding-bottom: 13px;
}
.tab-rail button {
  text-align: left; background: none; border: 0; cursor: pointer;
  padding: 0 18px 0 0; font-family: var(--sans); color: #7d9678;
  transition: color 0.28s var(--ease-out-cubic);
}
.tab-rail .tnum {
  /* 11, the floor. 0.14em of tracking on a two-digit number is buying nothing
     that a legible size is not worth more. */
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em;
  color: #4d6a4b; display: block; margin-bottom: 7px; transition: color 0.28s;
}
.tab-rail button b { display: block; font-size: 14px; font-weight: 700; color: #a9c0a3; transition: color 0.28s; }
.tab-rail button:hover b { color: var(--white); }
.tab-rail button[aria-selected="true"] .tnum { color: var(--green); }
.tab-rail button[aria-selected="true"] b { color: var(--white); }
.tab-rail button:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; border-radius: 3px; }

/* One rule that travels along the rail, rather than five that switch on and
   off. The move is the wayfinding: it says where you are in a sequence. */
.sc-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgba(122,173,106,0.14); }
.sc-progress span {
  position: absolute; left: 0; top: 0; height: 2px; width: 20%;
  background: var(--green); transform: translateX(0);
  transition: transform 0.5s var(--ease-out-cubic);
}

/* MOTION: one object moving, not two images cross-fading.
   At full-panel scale a blur cross-fade reads as a cut, because nothing on
   screen travels. So the outgoing panel drifts out in the direction of travel
   while the incoming one arrives from the opposite side, and the eye tracks a
   single object through the change. Direction is a system, not a per-element
   decision (motion rule 2): scrolling down always moves content up. */
/* The panels are absolutely positioned, so this box has no intrinsic height and
   used to take whatever the stage had left: a 447px-tall clip floating in a
   726px box, with the difference reading as dead air above and below it. All
   five showcase clips are 1280x800, and the frame is 68% of the row less the
   gap, so the box can be locked to the shape the product actually makes.
   2.5 is that ratio; the residual error across the pinned range is a few px,
   which the video's own max-height absorbs. */
.sc-body {
  position: relative; flex: none; aspect-ratio: 2.5; max-height: 100%;
  margin-block: auto; min-height: 0; --enter: 34px; --exit: -34px;
}
.sc-body.back { --enter: -34px; --exit: 34px; }

.sc-panel {
  position: absolute; inset: 0; margin: 0;
  /* L2 for all five: copy holds a narrow left column, the product takes the rest
     at FULL stage height. A dense table under a copy bar was letterboxed to
     roughly half the stage; beside it, the product keeps its vertical run and
     the claim sits next to the rows it describes instead of above them. */
  display: flex; flex-direction: row; gap: 44px; align-items: stretch;
  opacity: 0; transform: translateY(var(--enter)); pointer-events: none;
  transition: opacity 0.46s var(--ease-out-cubic), transform 0.58s var(--ease-out-cubic);
}
.sc-panel.on { opacity: 1; transform: none; pointer-events: auto; }
.sc-panel.out { opacity: 0; transform: translateY(var(--exit)); }
/* Applied for one frame when a panel is re-entered from the far side, so it
   jumps to the arrival offset instead of animating across to it first. */
.sc-panel.no-t { transition: none; }

.sc-copy {
  flex: 0 0 32%; display: flex; flex-direction: column; justify-content: center;
  gap: 0; min-height: 0; padding-right: 30px;
}
/* Headline and deck are separated by a rule rather than by space alone, so the
   claim reads as a heading over its own note instead of one long block. Held
   under the tab rail's own green bar in weight: that bar says WHERE YOU ARE and
   has to stay the brightest green in the section, so this one carries the same
   hue at lower opacity and reads as trim rather than as a second indicator. */
.sc-lede { position: relative; padding-bottom: 22px; }
.sc-lede::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 156px; height: 3px; border-radius: 2px; background: var(--green-hi);
}
.sc-kick {
  display: none;
  font-family: var(--mono); font-size: var(--fs-eyebrow); font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--green); margin: 0 0 9px;
}
.sc-copy h3 {
  font-family: var(--serif); font-weight: 700; margin: 0;
  font-size: clamp(21px, 2.4vw, 30px); line-height: 1.16; letter-spacing: -0.4px;
  color: var(--white);
}
.sc-deck { margin: 0; padding-top: 22px; font-size: 15px; line-height: 1.62; color: #a9c0a3; }

/* Height-driven, so the product always fits the pinned stage whatever the
   viewport is. Sizing it from the width instead put an 800px-tall recording
   into a 578px box and the panel lost its own bottom edge. */
/* Beside a copy column the frame is ~68% of the stage, and these recordings are
   WIDE (1280x800). Fitting one inside that width letterboxes it to roughly half
   the height it had when the frame ran full width, so the product got smaller by
   moving it. Same answer as the narrow-viewport block below: hold full height,
   keep the asset's own scale, and lose the right edge. The crop says there is
   more app here rather than shrinking it into grey static. */
.sc-frame {
  flex: 1; min-height: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
}
.sc-frame video {
  width: 100%; height: auto; max-height: 100%; object-fit: contain;
  display: block; border-radius: 8px;
  /* Two of the five surfaces are dark screens, so without an edge they dissolve
     into the band while the cream ones read as cards. The hairline gives every
     panel the same frame whatever the app is doing inside it. */
  box-shadow: 0 0 0 1px var(--green-line), 0 22px 54px rgba(0,0,0,0.34);
  /* .clip-v paints --dark-deep behind itself, which on a tall viewport turns
     the contain-fit letterbox into two visibly darker bars. Nothing sits behind
     this one but the band, so let the band show through. */
  background: transparent;
}

/* ---- THE BREAKPOINT LADDER, ported from attio.com (measured 2026-08-28) ----
 *
 * The rail is the section's navigation, so it survives every width. Attio's own
 * ladder is three shapes for one control, and each shape exists because the one
 * above it stops fitting:
 *
 *   >= 1240   horizontal rail across the top, five equal columns, section
 *             pinned. One panel on screen and the scroll advances it.
 *   1024-1239 rail turns ninety degrees into a sticky sidebar and the panels
 *             unroll into a column beside it. The pin goes because this band is
 *             mostly iPad landscape, and 500vh of scroll-jack on a touch device
 *             is five swipes to get past a section you did not ask to be held by.
 *   768-1023  no room for a sidebar, so the rail lies back down as a full-bleed
 *             tab strip that sticks under the nav while its panels scroll under it.
 *   < 768     same strip, now scrollable, sized so the fourth tab is cut by the
 *             glass. The cut edge is the affordance: it says the row continues.
 *
 * In all three lower modes the rail is a scroll-spy rather than a switch: it
 * follows the panel you are reading and a tap scrolls to one. That is why
 * nothing here has to hide. The old rule hid the rail below 980 on the argument
 * that it had nothing left to indicate once every panel was on screen, which
 * was true of a rail that only ever meant "which one is showing" and false of
 * one that means "you are third of five and here is the way to the fifth".
 */

/* MODE B, C and D all share this: the section stops pinning and becomes a
   column of panels. Everything below only rearranges the rail. */
@media (max-width: 1239px) {
  .sc { height: auto; --sc-gut: var(--gut); }
  .sc-trip { display: none; }
  .sc-stage { position: static; height: auto; display: block; }
  .sc-inner { height: auto; padding: 0 0 60px; gap: 0; }
  .sc-body { position: static; display: flex; flex-direction: column; aspect-ratio: auto; max-height: none; }
  .sc-panel {
    position: static; opacity: 1; transform: none; pointer-events: auto; gap: 16px;
    flex-direction: column; align-items: stretch;
    /* The rail scrolls to a panel with scrollIntoView, so the distance it has
       to clear (nav, and the sticky strip when there is one) is CSS's to know,
       not the script's. One property, redeclared per mode. */
    scroll-margin-top: var(--sc-jump);
  }
  .sc-panel.out { opacity: 1; transform: none; }
  .sc-copy { flex: none; justify-content: flex-start; gap: 0; min-height: 0; padding-right: 0; }
  .sc-lede { padding-bottom: 14px; }
  .sc-deck { padding-top: 14px; }

  /* THE PRODUCT KEEPS ITS SCALE AND LOSES ITS RIGHT EDGE.
     A 1280x800 recording of a dense table, scaled to fit a 390px phone, is not
     a small picture of the product: it is grey static, and it argues against
     the thing it is there to prove. attio.com never shrinks one. Their record
     view at 768 runs off the glass mid-column and their opportunities table is
     cut at the ICP score. So the asset holds a legible width and the frame
     becomes a horizontal scroller: the crop says there is more app here, and a
     swipe reaches it.

     clamp, not a fixed width, because the floor and the ceiling are different
     arguments. 720px is where the recording's own 11px UI type stops being
     readable, so nothing may go below it. 1060px is the widest the frame can be
     before a tablet reader is scrolling for no reason. Between them the crop
     holds at roughly a third of the asset, which is Attio's ratio. */
  .sc-frame {
    display: block;
    overflow-x: auto; overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    border-radius: 8px;
    /* The hairline and the drop move off the video and onto the frame. On the
       video they were inside the scroller and the left and right edges of both
       were clipped away the moment it overflowed. */
    box-shadow: 0 0 0 1px var(--green-line), 0 18px 44px rgba(0,0,0,0.34);
    background: var(--dark-deep);
  }
  .sc-frame::-webkit-scrollbar { display: none; }
  .sc-frame video {
    display: block;
    width: clamp(720px, 150%, 1060px);
    max-width: none; height: auto;
    border-radius: 0; box-shadow: none;
  }
  .sc-copy, .sc-frame { margin-inline: var(--sc-gut); }
}

/* MODE B : 1024-1239. The rail turns ninety degrees.
   The travelling fifth-of-the-width bar cannot come with it, because a vertical
   list is not five equal rows: it is five buttons with a gap between them, so a
   segment sized at a fifth of the track lands between items rather than on one.
   Attio's vertical rail marks the active item instead of measuring progress,
   which is also the more honest control once the section is not pinned. */
@media (min-width: 1024px) and (max-width: 1239px) {
  .sc { --sc-jump: 92px; --sc-gut: 0px; }
  .sc-inner {
    display: grid; grid-template-columns: 188px minmax(0, 1fr);
    column-gap: 48px; align-items: start;
    max-width: 1180px; padding: 4px 28px 66px;
  }
  .tab-rail {
    position: sticky; top: 92px;
    display: flex; flex-direction: column; gap: 4px;
    padding: 0; border-left: 1px solid var(--green-line);
  }
  .tab-rail button { position: relative; padding: 9px 14px 9px 18px; }
  .tab-rail .tnum { margin-bottom: 4px; }
  /* The marker, not a measure. Grows from 40% to full height on arrival so the
     move reads as the rail following you rather than a light switching on. */
  /* 900, not 1060. iPad landscape gives 705px under the nav, and a 1060px-wide
     recording is 662px tall, which put a panel's headline and its clip 847px
     apart: the reader could never see the claim and the evidence at once, in
     the one mode that exists for that exact device. At 900 the clip is 562px
     tall and the whole panel is about 690. */
  .sc-frame video { width: clamp(720px, 132%, 900px); }
  .tab-rail button::before {
    content: ""; position: absolute; left: -1px; top: 6px; bottom: 6px; width: 2px;
    background: var(--green); border-radius: 1px;
    opacity: 0; transform: scaleY(0.4);
    transition: opacity 0.24s var(--ease-out-cubic), transform 0.36s var(--ease-out-cubic);
  }
  .tab-rail button[aria-selected="true"]::before { opacity: 1; transform: none; }
  .sc-progress { display: none; }
  .sc-body { gap: 78px; }
}

/* MODE C and D : below 1024. The rail lies back down, full bleed, and sticks
   under the nav so it is still reachable four panels later. */
@media (max-width: 1023px) {
  .sc { --sc-jump: 132px; }
  .sc-inner { max-width: none; }
  .tab-rail {
    /* 63, matching the nav's measured height. At 61 the strip's own top border
       and two pixels of it lived under the bar at every width in this mode. */
    position: sticky; top: 63px; z-index: 5;
    display: flex; grid-auto-flow: unset; grid-auto-columns: auto;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* Opaque, because panels scroll under it. --dark is the band's own ground,
       so the strip reads as part of the section rather than as a floating bar. */
    background: var(--dark);
    border-top: 1px solid var(--green-line);
    border-bottom: 1px solid var(--green-line);
    padding: 0; margin: 0 0 38px;
  }
  .tab-rail::-webkit-scrollbar { display: none; }
  /* Empty track after the last tab so it can be scrolled flush left like the
     other four. Width is measured and set by site.js (railTail); 0 until then,
     and 0 at any width where all five tabs already fit. */
  .tab-rail::after { content: ""; flex: 0 0 var(--rail-tail, 0px); }
  .tab-rail button {
    flex: 1 0 auto;
    padding: 12px 20px 11px;
    border-right: 1px solid var(--green-line);
  }
  /* The strip is full bleed, so its FIRST label is the only thing in it that
     has to meet the page's left edge. Everything else in the section reads
     --gut; this was the one place still carrying a literal, and it sat the
     opening tab 8px inside every heading above it. */
  .tab-rail button:first-of-type { padding-left: var(--gut); }
  .tab-rail button:last-of-type { border-right: 0; }
  .tab-rail button[aria-selected="true"] { background: rgba(122,173,106,0.11); }
  .tab-rail button b { font-size: 13.5px; }
  .tab-rail .tnum { margin-bottom: 5px; }
  .sc-progress { display: none; }
  .sc-body { gap: 64px; }
  .sc-panel { gap: 14px; }
  /* .sc-kick stays hidden (it is display:none in its base rule): the tab strip
     names the surface directly above the panel, so the kicker would be the same
     word twice, 40px apart. */
}

/* MODE D : below 768. Same strip, now longer than the glass.
   The fourth tab is deliberately cut. A row that ends exactly at the edge reads
   as a row of four; one cut through a word reads as a row that continues, and
   that is the only thing telling a thumb to swipe it. */
@media (max-width: 767px) {
  .sc { --sc-jump: 128px; }
  /* THE CLIPS FIT HERE TOO (Jace, 2026-08-28).
     The crop is right at tablet width, where a third of a 1060px recording is
     still most of a readable table. At 390 it is not: every panel became a
     frame you had to drag sideways before it said anything, five times over,
     and a reader who will not drag the FIRST one will not reach the fifth. Same
     call the hero sheet already made, so the phone has one rule for product
     shots instead of two. The scroller stays and has nothing to scroll.

     Declared HERE, inside mode D, and not next to the hero rule it agrees with:
     the clamp it overrides lives in this section further down the file, so at
     equal specificity the later rule wins and a copy up in the hero block was
     silently doing nothing. */
  .sc-frame video { width: 100%; max-width: 100%; }
  .tab-rail { scroll-snap-type: x proximity; }
  .tab-rail button { flex: 0 0 auto; min-width: 138px; scroll-snap-align: start; padding: 11px 16px 10px; }
  .sc-body { gap: 52px; }
  .sc-inner { padding-bottom: 48px; }
}

/* ---- PIN AT EVERY WIDTH (Jace, 2026-08-30) ------------------------------
 * Overrides the unpinning in modes B, C and D above. The section now holds the
 * viewport on tablet and phone the same way it does on desktop: a reader
 * reaches the next section only after stepping through all five panels.
 * Declared after the whole ladder so it wins at equal specificity, and deleting
 * this one block restores the scrolling ladder exactly as it was.
 *
 * svh, not vh, throughout. A sticky stage sized in vh is sized against the
 * LARGEST viewport, so on iOS every panel would sit ~60-90px taller than the
 * glass until the toolbar collapsed, and the fifth panel could never centre.
 * svh is the toolbar-visible viewport, so the stage fits from the first frame
 * and does not resize mid-scroll. .sc and the trip markers must use the same
 * unit as the stage or the markers drift out of step with the panels.
 *
 * Guarded on min-height. A landscape phone has no room to pin anything, so
 * under 520px tall the original ladder is left alone. site.js reads the same
 * two conditions, so CSS and JS can never disagree about the mode. */
@media (max-width: 1239px) and (min-height: 520px) {
  .sc { height: 500svh; --sc-gut: 0px; }
  .sc-trip { display: block; }
  .sc-trip:nth-of-type(1) { top: 10svh }
  .sc-trip:nth-of-type(2) { top: 110svh }
  .sc-trip:nth-of-type(3) { top: 210svh }
  .sc-trip:nth-of-type(4) { top: 310svh }
  .sc-trip:nth-of-type(5) { top: 410svh }

  .sc-stage {
    position: sticky; top: 62px;
    height: min(calc(100svh - 62px), 730px);
    display: flex; align-items: center;
  }
  .sc-inner {
    display: flex; flex-direction: column; height: 100%; max-width: 100%;
    /* Explicit, because mode B sets align-items:start for its grid sidebar and
       that survives the switch to flex. In a flex column it stops children
       stretching, so .sc-body (a block whose panels are all absolute) collapsed
       to zero width, the copy wrapped at 0px into a 729px-tall column and the
       panel overflowed the stage. Only the 1024-1239 band was affected. */
    align-items: stretch;
    padding: 14px 22px 22px; gap: 0; column-gap: 0;
  }
  /* The full-bleed label strip from mode C, restored and made to apply at every
     pinned width (mode B's vertical sidebar cannot sit inside a pinned column).
     position:static, not sticky: it is already inside a stage that is holding
     the viewport, so sticking it again did nothing but add a stacking context.
     The 38px margin mode C carried was sized for a scrolling section; pinned,
     that gap pushed the clip's bottom edge off the glass, so it is 14px here. */
  .tab-rail {
    position: static; top: auto; z-index: 5; border-left: 0;
    display: flex; flex-direction: row; grid-auto-flow: unset; grid-auto-columns: auto;
    overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
    background: var(--dark);
    border-top: 1px solid var(--green-line);
    border-bottom: 1px solid var(--green-line);
    gap: 0; padding: 0; margin: 0 -22px 14px;
  }
  .tab-rail::-webkit-scrollbar { display: none; }
  .tab-rail button {
    flex: 1 0 auto; min-width: 0;
    padding: 12px 20px 11px; border-right: 1px solid var(--green-line);
  }
  .tab-rail button:first-of-type { padding-left: var(--gut); }
  .tab-rail button:last-of-type { border-right: 0; }
  .tab-rail button[aria-selected="true"] { background: rgba(122,173,106,0.11); }
  .tab-rail button b { display: block; font-size: 13.5px; }
  .tab-rail .tnum { margin-bottom: 5px; }
  .sc-progress { display: none; }

  .sc-body {
    position: relative; display: block; flex: 1; min-height: 0;
    aspect-ratio: auto; max-height: none; margin-block: 0; gap: 0;
  }
  /* Top-aligned, with the frame sized to its content. Centring the group put
     half the leftover height between the rail and the headline and pushed the
     clip's bottom edge past the fold; starting at the top spends that slack
     below the product, where nothing is waiting to be read. */
  .sc-panel {
    position: absolute; inset: 0; gap: 14px;
    display: flex; flex-direction: column; align-items: stretch;
    justify-content: flex-start;
    opacity: 0; transform: translateY(var(--enter)); pointer-events: none;
  }
  .sc-panel.on  { opacity: 1; transform: none; pointer-events: auto; }
  .sc-panel.out { opacity: 0; transform: translateY(var(--exit)); }

  .sc-copy { flex: none; padding-right: 0; justify-content: flex-start; margin-inline: 0; }
  /* THE PRODUCT FITS, IT NO LONGER DRAGS. The 720-900px horizontal scroller
     was the right answer while the panel could be any height it liked. Pinned,
     the panel has a fixed height, and a reader cannot be asked to drag sideways
     inside a section that is already holding their scroll. Same call mode D
     made for the phone, now applied wherever the section pins. */
  /* flex:1 with a definite height, and the clip fitted to BOTH axes.
     Sized to the video's natural height instead, the frame grew with the
     viewport width: at 1024 the clip came out 612px tall against a 643px body
     that also had to hold the copy, so it overflowed the stage by 100px and was
     cut off. Constrained on height as well, it fits every width, and
     align-items:flex-start keeps it directly under the copy rather than
     floating in the middle of the leftover space on a phone. */
  /* flex-basis 0, not auto. A percentage max-height on the clip resolves against
     this box, and with basis:auto the box is sized FROM the clip, so the limit
     was circular and never applied: at 1024 the video still came out 612px tall
     and hung 118px below the stage. Basis 0 makes the height come from the flex
     line instead, which is definite, so max-height:100% finally binds. */
  .sc-frame {
    flex: 1 1 0; min-height: 0; margin-inline: 0;
    display: flex; align-items: flex-start; justify-content: center;
    overflow: visible;
  }
  .sc-frame video {
    width: auto; height: auto; max-width: 100%; max-height: 100%;
    object-fit: contain;
  }
}
@media (max-width: 767px) and (min-height: 520px) {
  .sc-inner { padding: 12px 16px 18px; }
  .sc-panel { gap: 12px; }
  /* The strip runs past the glass here and the fourth tab is cut on purpose:
     a row ending flush reads as a row of four, one cut mid-word reads as a row
     that continues, which is the only thing telling a thumb to swipe it. */
  .tab-rail { margin: 0 -16px 14px; }
  .tab-rail button { flex: 0 0 auto; min-width: 138px; }
}

/* ============ ONE VIEW : fragmented in, one instrument out ============
   The transformation is carried by the surfaces, not by an illustration. The
   inputs are four pieces of paper, separate, each with its own edge. The output
   is one dark card, the instrument's own ground, and it is the only heavy mass
   in a light band so the eye lands on it. That is the argument the section is
   making, made in the palette rather than described. ============ */
/* GROUND. Every light section on this page is --gray-light now, and the seams
   between them are hairlines rather than colour changes. attio.com runs 9,000px
   of one ground and separates its sections with a 1px grid instead (audit in
   WEBSITE-PLAN 15b); this page changed tone six times in 12,250px, and two of
   those changes were a 1.121:1 step nobody can see. --white goes back to
   meaning CARD, which is what its token comment always said. */
.oneview { background: var(--gray-light); padding: 92px 0; border-top: 1px solid var(--rule); }
.oneview .h2 { max-width: 800px; }
.oneview .ov-deck:not(:last-of-type) { margin-bottom: 18px; }
.ov-deck-2 { margin-top: 0; }
.ov-deck { max-width: 62ch; margin: 16px 0 46px; font-size: 15.5px; line-height: 1.7; color: var(--text-muted); }

.ov-flow { display: grid; grid-template-columns: minmax(0, 1fr) 150px minmax(0, 1.04fr); align-items: center; }

.ov-in { list-style: none; display: grid; gap: 10px; }
.ov-in li {
  background: var(--white); border: 1px solid var(--rule); border-radius: var(--r-md);
  padding: 14px 18px 15px; position: relative;
}
/* Each input reaches the spine. Four ticks, one spine, one line out: the claim
   is that four things become one thing, and a connector that does not actually
   join them is decoration pretending to be a diagram. */
.ov-in li::after {
  content: ""; position: absolute; left: 100%; top: 50%;
  width: 40px; height: 1px; background: var(--green-light);
}
.ov-in b { display: block; font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 3px; }
.ov-in span { display: block; font-size: 13px; line-height: 1.5; color: var(--text-muted); }

/* Green is a line here, which is the only large thing the accent is allowed to
   be. The spine runs from the middle of the first input to the middle of the
   last, the four ticks meet it, and one line leaves it for the card. */
.ov-mid { position: relative; align-self: stretch; }
.ov-mid::before {                       /* the spine the four ticks land on */
  content: ""; position: absolute; left: 40px; top: 12%; bottom: 12%;
  width: 1px; background: var(--green-light);
}
.ov-mid::after {                        /* and the one line that leaves it */
  content: ""; position: absolute; left: 40px; right: 26px; top: 50%;
  height: 1px; background: var(--green-light);
}
.ov-line { display: none; }
.ov-node {
  /* 11px is the floor, DESIGN-SYSTEM 7.6. */
  position: absolute; left: 40px; right: 20px; top: 50%;
  transform: translateY(-100%); padding-bottom: 9px; text-align: center;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--green-bold); white-space: nowrap;
}
.ov-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--green-bold); font-size: 17px; line-height: 1; }

.ov-out {
  background: var(--dark); color: var(--white); border-radius: var(--r-md);
  padding: 30px 32px 32px; box-shadow: 0 26px 60px -28px rgba(15,31,15,0.55);
}
.ov-out-kick {
  font-family: var(--mono); font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--green); margin-bottom: 12px;
}
.ov-out-h {
  font-family: var(--serif); font-size: clamp(21px, 2.3vw, 27px); font-weight: 700;
  line-height: 1.2; letter-spacing: -0.4px; margin-bottom: 20px;
}
.ov-out ul { list-style: none; display: grid; gap: 11px; margin-bottom: 24px; }
.ov-out li {
  font-size: 14px; line-height: 1.55; color: #b4c9ad;
  padding-left: 18px; position: relative;
}
.ov-out li::before {
  content: ""; position: absolute; left: 0; top: 9px;
  width: 7px; height: 1px; background: var(--green);
}
.ov-go { color: var(--green); text-decoration: none; font-size: 14px; font-weight: 600; }
.ov-go:hover { color: var(--green-hi); }
.ov-go span { display: inline-block; transition: transform 0.24s var(--ease-out-cubic); }
.ov-go:hover span { transform: translateX(4px); }

/* THE DIAGRAM IS THE ARGUMENT, so it keeps its shape as long as it fits.
   Stacked, "four sources resolve to one view" becomes "here are four cards and
   then here is a fifth", which is a list and not a resolution: the whole claim
   lives in the geometry. iPad portrait has 712px, which is enough for the two
   masses side by side once the connector column stops asking for 90 of them.
   (Jace, 2026-08-28: tablet vertical should look like tablet horizontal.) */
@media (min-width: 768px) and (max-width: 900px) {
  /* 96, not 44. The connector column has to be wider than the word standing on
     it: at 44 the RESOLVED label, which is centred on the column, hung 30px
     into the dark panel on its right and got clipped by it. The label is the
     one thing in this diagram that names the transformation, so the column is
     sized to the label rather than the label shrunk out of legibility. */
  .ov-flow { grid-template-columns: minmax(0, 1fr) 68px minmax(0, 1fr); gap: 0; }
  .ov-in li { padding: 13px 15px; }
  .ov-in .rc-title, .ov-node b { font-size: 16px; }
  .ov-out { padding: 24px 22px; }
  .ov-out h3 { font-size: 21px; }
}
/* THE FOUR SOURCES BECOME A BLOCK, NOT A COLUMN (Jace, 2026-08-28).
   Stacked as four full-width cards with a description each, the inputs took
   about 380px to say "there are four of these", and the diagram's whole claim,
   four things resolving into one, was spread over more screen than a phone can
   hold at once. You cannot see a convergence you have to scroll through.

   Two columns of two, and the descriptions go. The titles ARE the claim here
   ("The federal record", "Federal procurement", "Company disclosure", "The
   trade press"): the reader needs to see that there are four separate kinds of
   paper, not to read what each contains, and the section's own deck two
   paragraphs up already says what they contain. The block reads as one mass
   with four parts, which is what has to arrive before the arrow means anything. */
@media (max-width: 767px) {
  /* Stacked, the connector turns a quarter turn: the ticks go away with the
     column they pointed at, and what is left is one rule down the middle. */
  .ov-flow { grid-template-columns: 1fr; }
  .ov-in { grid-template-columns: 1fr 1fr; gap: 8px; }
  .ov-in li { padding: 12px 13px 13px; }
  .ov-in li span { display: none; }
  .ov-in b { font-size: 13.5px; line-height: 1.3; margin-bottom: 0; }
  /* The card is already inside a section whose eyebrow reads "One view", so its
     own "One view" kicker is the same two words twice within 200px. */
  .ov-out-kick { display: none; }
  .ov-in li::after { display: none; }
  /* THE RUN SHORTENS WITH THE THING IT JOINS. 84px of empty rule under a block
     that was just rebuilt to be compact is the gap the rebuild removed. The
     rule stops short of the arrow and the arrow stops short of the card, so the
     three read as a sequence rather than one line shoved into a panel.

     THESE NUMBERS LIVE HERE, on the rules that already own the stacked
     connector, and not in a second set above them. Two attempts at this shipped
     as duplicate .ov-mid / .ov-arrow declarations earlier in this same block,
     which the ones below simply overrode: same selector, same media query, same
     specificity, later wins. Nothing was broken and nothing was applied. */
  .ov-mid { align-self: auto; height: 42px; }
  .ov-mid::before { left: 50%; top: 0; bottom: 30px; }
  .ov-mid::after { display: none; }
  .ov-node { left: 50%; right: auto; top: 50%; transform: translate(-50%, -50%);
             padding: 5px 11px; background: var(--gray-light); }
  .ov-arrow { left: 50%; right: auto; top: auto; bottom: 13px; transform: translateX(-50%) rotate(90deg); }
}

/* ============ WEDGE ============
   Dark, and inside the reveal shell between the pinned band and the quote, so
   the middle of the page is ONE dark passage rather than a dark island in a
   light run (which is why it was on paper before, and why that reason is gone).
   z-index 1 puts it in the same stacking layer as the quote, so it rides up
   over the pinned .whynow exactly the way the quote does.
   DESIGN-SYSTEM 7.1 still holds: the BAND frames, so it is dark. The row cards
   are the evidence the section rests on, so they stay white paper. */
.wedge {
  /* An ordinary section: no sticky, no z-index, no 100vh. It sits in the dark
     run before the reveal shell so it reads as one continuous dark passage, but
     the PIN belongs to .whynow alone and the RIDE belongs to .pullquote alone.
     This block has been both of those and Jace rejected each: riding read as
     attached to the bottom of the quote, pinning read as the reveal happening
     twice. One gesture, once. */
  background: var(--dark); color: var(--white);
  padding: 96px 0; border-top: 1px solid var(--green-line);
}
.wedge .h2 { color: var(--white); }
.wedge-in { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: start; }
/* :not(.eyebrow) because this selector is (0,1,1) and .eyebrow is (0,1,0), so
   the body rule was winning the chip's font-size and rendering it at 15.5px. */
.wedge-copy p:not(.eyebrow) { font-size: 15.5px; line-height: 1.68; color: #b4c9ad; margin-bottom: 18px; }
.wedge-copy .btn { margin-top: 8px; }
.row-card {
  /* PAPER, not white. Pure #ffffff cards on the dark band are the brightest
     thing on the page, and the band is pinned full-viewport so the reader sits
     with the glare. Jace called it: "it hurts the eyes." --paper is also the
     semantically correct token, DESIGN-SYSTEM 7.1: the evidence renders on
     paper, and --card/#ffffff is raised paper for a light ground. One notch
     further (--paper-2, #e9e8dd) if this still reads hot. */
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 17px 19px;
  margin-bottom: 10px;
  transition: transform 0.24s var(--ease-out-back), border-color 0.2s;
}
.row-card:hover { transform: translateX(5px); border-color: var(--green-bold); }
.rc-src { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--green-bold); margin-bottom: 7px; }
.rc-title { font-family: var(--serif); font-size: 18px; margin-bottom: 6px; color: var(--text); }
.rc-meta { font-size: 13px; color: var(--text-muted); }
.rc-meta b { color: var(--text); font-variant-numeric: tabular-nums; }
.rc-fine { font-size: 12px; color: #6f8a6c; margin-top: 14px; }
/* The lane a row came from, riding in .rc-src. Federal and commercial rows sit
   in one stack, so the lane needs to read at a glance without a second badge
   system: brighter token, and a gap where the stack changes lane. */
/* THE SIX LANES.
   Two readings in one object: outer ring open now, inner ring within a year,
   centre reads out the active lane. Lane colours are the Watchlist widget's own,
   so a lane is the same colour on the marketing page and inside the product.

   The rail is a list of real buttons, not decoration, because it is the whole
   interaction on touch. It also prints both numbers per row, so a phone reader
   never needs the hover the device cannot give them. */
.lanes { background: var(--gray-light); padding: 84px 0; border-top: 1px solid var(--rule); }
.lanes-deck { max-width: 620px; margin: 14px 0 0; font-size: 17px; line-height: 1.62; color: var(--text-muted); }
.lr {
  margin-top: 34px; display: grid; gap: 44px; align-items: center;
  grid-template-columns: minmax(0, 1fr) auto;
}
.lr-chart svg { display: block; width: 100%; height: auto; max-width: 500px; margin: 0 auto; overflow: visible; }
.lr-seg { cursor: pointer; transition: fill-opacity .22s var(--ease-out-cubic); outline: none; }
.lr-seg:focus-visible { stroke: var(--text); stroke-width: 2; }
.lr-arcn { font-family: var(--serif); font-size: 15px; }
.lr-cx { pointer-events: none; }
.lr-big { font-family: var(--serif); font-size: 46px; fill: var(--green-bold); font-variant-numeric: tabular-nums; }
.lr-lbl { font-family: var(--mono); font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; fill: #7d8a79; }

.lr-rail { display: flex; flex-direction: column; width: max-content; margin-left: auto; }
.lr-row {
  display: grid; grid-template-columns: 14px 1fr; gap: 15px; align-items: center;
  width: 100%; padding: 17px 10px; border: 0; border-bottom: 1px solid var(--rule);
  background: none; text-align: left; font: inherit; cursor: pointer;
  transition: background .16s ease;
}
.lr-row:hover, .lr-row.on { background: #eceee7; }
.lr-row:focus-visible { outline: 2px solid var(--green-bold); outline-offset: -2px; }
.lr-sw { width: 14px; height: 14px; border-radius: 4px; }
.lr-txt { display: flex; flex-direction: column; gap: 4px; }
.lr-name { font-size: 17px; font-weight: 600; color: var(--text); letter-spacing: -0.005em; }
.lr-sub { font-size: 13px; color: var(--text-muted); }
.lanes-fine { margin-top: 26px; font-size: 12.5px; color: #6f8a6c; }
.lanes-fine a { color: var(--green-bold); text-underline-offset: 3px; }

/* Tablet: the rail cannot hold a 340px column and a legible ring side by side,
   so the ring goes on top and the rail runs beneath it in two columns. */
/* Tablet and phone: the ring goes on top and the legend runs three across in two
   rows. Below the ring there is full width and no second column to compete with,
   so three-up reads as one block rather than a long list, and the swatch moves
   above the name because a 3-column cell has no room for it beside one. */
@media (max-width: 1000px) {
  .lr { grid-template-columns: 1fr; gap: 26px; }
  .lr-chart svg { max-width: 360px; }
  .lr-rail {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 20px; row-gap: 4px; width: auto; margin-left: 0;
  }
  .lr-row {
    grid-template-columns: 1fr; align-items: start; gap: 8px;
    padding: 14px 2px; align-content: start;
  }
  .lr-sw { width: 12px; height: 12px; }
  .lr-name { font-size: 15px; }
  .lr-sub { font-size: 12px; }
}
@media (max-width: 620px) {
  .lanes { padding: 60px 0; }
  .lr { margin-top: 26px; gap: 20px; }
  .lr-chart svg { max-width: 290px; }
  /* Still three across on a phone, so the legend stays two rows rather than
     becoming six and pushing everything below the fold. Type steps down to fit
     roughly 110px of column. */
  .lr-rail { column-gap: 12px; }
  .lr-row { padding: 11px 1px; gap: 6px; }
  .lr-sw { width: 10px; height: 10px; }
  .lr-name { font-size: 12.5px; line-height: 1.25; }
  .lr-sub { font-size: 10.5px; line-height: 1.35; }
  .lr-big { font-size: 38px; }
}
@media (prefers-reduced-motion: reduce) { .lr-seg, .lr-row { transition: none; } }

/* Upstream: copy left, the document and the row it became right. The row sits
   directly under the letter because it IS the letter's output, so the two read
   as one object rather than two exhibits.

   The provenance line rides ON each artifact instead of under it. At rest the
   artifact is clean; on hover, or on focus where there is no hover, the artifact
   blurs and darkens back and the text comes forward over it. That keeps two
   captions from stacking into a wall of small grey type beside a document, and
   it makes the caption feel attached to the thing it describes.

   tabindex on the wrapper is what makes the tap work with no script: a touch
   focuses it, a touch anywhere else drops the focus. */
.wedge-proof { display: flex; flex-direction: column; gap: 14px; }

.reveal { position: relative; margin: 0; border-radius: 6px; overflow: hidden; outline: none; }
.reveal > img, .reveal > .row-card {
  display: block; width: 100%; height: auto;
  transition: filter 0.32s var(--ease-out-cubic), transform 0.32s var(--ease-out-cubic);
}
.reveal > img { border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; box-shadow: 0 24px 52px -22px rgba(0,0,0,0.55); }
/* The scrim and the text share a layer, so one fade moves both. */
.reveal-text {
  position: absolute; inset: 0; z-index: 2; margin: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 22px 26px; text-align: center;
  background: rgba(8, 20, 10, 0.72);
  color: var(--white); font-size: 13.5px; line-height: 1.65; text-wrap: balance;
  opacity: 0; transition: opacity 0.32s var(--ease-out-cubic);
}
.reveal-text > span { max-width: 42ch; }
.reveal-text a { color: var(--green); text-underline-offset: 3px; white-space: nowrap; }
.reveal:hover > img, .reveal:focus > img, .reveal:focus-within > img,
.reveal:hover > .row-card, .reveal:focus > .row-card, .reveal:focus-within > .row-card {
  filter: blur(3px) brightness(0.72);
}
.reveal:hover .reveal-text, .reveal:focus .reveal-text, .reveal:focus-within .reveal-text { opacity: 1; }
/* A hairline while focused, so a keyboard user can see which one is open. */
.reveal:focus, .reveal:focus-within { box-shadow: 0 0 0 2px var(--green); }

.row-wrap { border-radius: 8px; }
.wedge-cta { margin-top: 26px; }
/* The date stays whole. Only break where there is room to. */
.rc-br { display: none; }
@media (min-width: 861px) { .rc-br { display: inline; } }
.wedge-note { margin-top: 20px; font-size: 14px; line-height: 1.65; color: #a9c0a3; }
@media (max-width: 620px) { .reveal-text { font-size: 12.5px; padding: 16px 18px; } }
@media (prefers-reduced-motion: reduce) {
  .reveal > img, .reveal > .row-card, .reveal-text { transition: none; }
}

}
.rc-lane { color: var(--text); font-weight: 700; }
/* The lane, carried in colour as well as in the word. Federal takes a muted
   navy and commercial keeps the house green, so the split reads before the text
   does. Coded by LANE and not by row order, because a colour that maps to
   nothing is decoration on a page whose argument is that everything traces to a
   record. Kept to a hairline and the label; the card stays neutral. */
.rc-lane-fed { color: #2c4a72; }
.rc-lane-com { color: var(--green-bold); }

/* ============ PROOF OF WORK ============ */
.pow { background: var(--off-white); padding: 92px 0; }
.pow .h2 { margin-bottom: 40px; }
.pow-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.pow-card {
  display: block; text-decoration: none;
  background: var(--white); border: 1px solid var(--rule); border-radius: 6px;
  padding: 26px 24px 22px;
  transition: transform 0.26s var(--ease-out-back), border-color 0.2s, box-shadow 0.2s;
}
.pow-card:hover { transform: translateY(-4px); border-color: var(--green); box-shadow: 0 14px 34px -18px rgba(0,0,0,0.3); }
.pow-card h3 { font-family: var(--serif); font-size: 20px; font-weight: 400; margin-bottom: 10px; }
.pow-card p { font-size: 14px; line-height: 1.62; color: var(--text-muted); margin-bottom: 16px; }
.pow-card .go { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--green-bold); }

/* ============ FAQ ============ */
/* Half the top padding it had, plus the hairline every other seam on this page
   now carries. Two 92px paddings meeting on one uninterrupted ground stack into
   180px of empty grey that reads as a hole rather than as the space around a
   heading. That now applies to .projection above it, which is the same ground
   since the page went to a single light band. */
.faq { background: var(--gray-light); padding: 62px 0 92px; border-top: 1px solid var(--rule); }
.faq .h2 { margin-bottom: 34px; }
.faq-list { border-top: 1px solid var(--rule); }
.faq details { border-bottom: 1px solid var(--rule); }
.faq summary {
  cursor: pointer; list-style: none; padding: 20px 34px 20px 0;
  font-size: 16.5px; font-weight: 600; position: relative;
}
.faq summary::-webkit-details-marker { display: none }
.faq summary::after {
  content: "+"; position: absolute; right: 4px; top: 17px;
  font-size: 22px; font-weight: 400; color: var(--green-bold);
  transition: transform 0.28s var(--ease-out-cubic);
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details p { padding: 0 40px 22px 0; font-size: 15px; line-height: 1.7; color: #3d3d3d; }
.faq summary:focus-visible { outline: 2px solid var(--green-bold); outline-offset: 3px; }

/* ============ FINAL ============
   The CTA and the footer are ONE dark mass, no seam. attio.com closes on
   1,102px of unbroken near-black with no rule and no colour shift between the
   ask and the footer, and that single edge is most of why the close reads as an
   ending rather than as two more sections. Ours were --dark against --dark-deep,
   which put a visible join exactly where the page wants to feel continuous. */
/* ============ CONTACT ============
   Same --gray-light ground as the FAQ above it, separated by the dot matrix
   rather than a second colour: one light ground is the rule for this run, and
   texture is what marks a new band. The card is --white because it is the one
   thing on the band meant to be acted on. */
/* Two ranked actions, not two bands. Row on desktop, stacked on narrow, and
   the ghost is deliberately the same size as the primary: it is the smaller ask
   by weight, not by target. */
.final-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.final-mail { margin-top: 20px; font-size: 13.5px; color: var(--text-faint); }
.final-mail a { color: var(--green-bold); text-decoration-color: rgba(47,110,34,0.4); text-underline-offset: 3px; }
.final-mail a:hover { color: var(--green); }

/* The page's single closing band, carrying both asks. Was two sections on this
   same ground; see the markup comment for why they merged. Still one light
   ground: off-white was tried here and pulled. */
.final { background: var(--gray-light); border-top: 1px solid var(--rule); padding: 104px 0 96px; text-align: center; }
.final-deck { max-width: 620px; margin: 0 auto 30px; font-size: 16px; line-height: 1.65; color: var(--text-muted); }

/* ============ RESPONSIVE ============ */
@media (max-width: 1023px) {
  /* The three proof-of-work cards go 3 -> 2 here and 2 -> 1 at phone width; the
     wedge is copy beside evidence and has nowhere to go but stacked. */
  .pow-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 620px) { .pow-grid { grid-template-columns: 1fr; } }
/* Copy beside the evidence, down to the phone line. Stacked at 768 the section
   was a paragraph followed by three cards in a column: five stacked rows where
   two columns fit, which is the shape Jace named as too vertical. */
@media (min-width: 768px) and (max-width: 980px) {
  .wedge-in { grid-template-columns: minmax(0, 0.95fr) minmax(0, 1fr); gap: 30px; }
  .wedge { padding: 76px 0; }
  .row-card { padding: 14px 16px; }
  .rc-title { font-size: 16.5px; }
}
@media (max-width: 980px) {
  .tabs { grid-template-columns: 1fr; gap: 22px; }
}
/* UPSTREAM: THE EVIDENCE COMES BEFORE THE ASK (Jace, 2026-08-28).
   Stacked, the grid put .wedge-copy (which ends in the button) above
   .wedge-rows, so a phone read paragraph, THEN a call to action, THEN three
   cards sitting under it with nothing left to do. The rows are what the
   paragraph is promising, so they belong between the promise and the ask.

   display:contents on the copy, so its four children join the same flex column
   as the rows and can be ordered against them. The alternative was moving the
   button out of .wedge-copy in the markup, which would then have to be undone
   with a grid placement on the desktop layout that is already correct.

   AND THEY STOP BEING CARDS. Three bordered paper cards on a dark band read as
   three separate objects; the claim is one dataset with rows in it, so on the
   phone they become rows with a rule between them, which is what the why-now
   metrics above them already do. Colour flips with the ground: paper cards
   carried dark text, and flush on the dark band that text has to be light. */
@media (max-width: 767px) {
  /* gap: 0 is load-bearing. .wedge-in is a grid with `gap: 56px` in its base
     rule, and switching display to flex here KEPT that gap, so all five items
     in the column (eyebrow, headline, paragraph, rows, button) were spaced 56px
     apart and the section read as five loose blocks. The elements carry their
     own margins; the container should not be adding a sixth opinion. */
  .wedge-in { display: flex; flex-direction: column; gap: 0; }
  .wedge-copy { display: contents; }
  .wedge-copy .btn { order: 3; align-self: flex-start; margin-top: 22px; }
  .wedge-rows { order: 2; margin-top: 4px; }
  .row-card {
    background: none; border: 0; border-radius: 0;
    border-top: 1px solid var(--green-line);
    padding: 13px 0 14px; margin-bottom: 0;
  }
  .wedge-rows .row-card:last-of-type { border-bottom: 1px solid var(--green-line); }
  .row-card:hover { transform: none; border-color: var(--green-line); }
  .wedge .rc-src { color: var(--green); margin-bottom: 5px; }
  .wedge .rc-title { color: var(--white); font-size: 17px; margin-bottom: 4px; }
  .wedge .rc-meta { color: #a9c0a3; }
  .wedge .rc-meta b { color: var(--white); }
  .wedge .rc-lane { color: var(--white); }
  .wedge .rc-lane-fed { color: #9db8dc; }
  .wedge .rc-lane-com { color: var(--green); }
  .wedge .rc-fine { margin-top: 12px; margin-bottom: 0; }
  .wedge-rows { margin-bottom: 0; }
  .stats { grid-template-columns: repeat(2, 1fr); }
  .stats li:nth-child(2n) { border-right: 0 }
  .stats li { border-bottom: 1px solid var(--rule) }
  .stats li:last-child { grid-column: 1 / -1; border-bottom: 0 }
}
/* THE FLOATERS COME IN ONE AT A TIME, not all at once.
 *
 * They hang off the 940px stage: fl-map 186px past the right edge, fl-opps 132px
 * past it, fl-ring 168px past the left. So each one needs its own viewport to
 * sit inside the page, and hiding all three at the widest of those thresholds
 * spent the other two for nothing. Both tablet-landscape sizes (1024 and 1180)
 * were losing the whole cluster to a rule written for the widest member of it.
 *
 * The ring is the one that stays, and it is not an arbitrary pick: it is the
 * narrowest, it hangs off the LEFT where the sheet's own content is emptiest,
 * and it is the only one of the three that reads at a glance (a radial chart,
 * not a screenshot of a list). Two panels over a narrowing screenshot start
 * covering the product they are there to advertise. */
/* Each threshold is 940 + 2 x overhang + 2 x gutter, because the stage is
   CENTRED: the spare window splits between the two sides, so a widget hanging
   168px past the left edge needs 336px of spare window and not 168. Getting
   that wrong is what produced the first attempt at this ladder, where the ring
   was still hanging 48px off the glass at 1180. */
@media (max-width: 1367px) { .fl-map  { display: none; } }  /* 940 + 372 + 56 */
@media (max-width: 1259px) { .fl-opps { display: none; } }  /* 940 + 264 + 56 */
@media (max-width: 1331px) {
  /* The ring stops hanging and tucks against the sheet's left edge, which is
     the emptiest part of the diagram. Its own threshold would be 1332; instead
     of dying there it changes shape, which is the whole point of keeping it. */
  .fl-ring { width: 236px; left: -6px; top: 84px; }
}
@media (max-width: 999px) {
  /* Below this the stage has no spare window at all, so any overlay is sitting
     on the sheet rather than beside it, and the lead clip carries the hero. */
  .fl-ring { display: none; }
}
@media (max-width: 767px) {
  /* The buildout's own phone rules are NOT here. They are in one block at the
     END of the PROJECTION section, because .proj-in's base rule and the 1023
     block both sit further down this file and beat anything written up here. */

  /* .wrap's gutter and the hero's own padding moved to the ladder above; what
     is left here is the vertical rhythm, which is the only thing this rule was
     ever really about. 680 was also the wrong number: it left 681-767 with
     desktop section padding on a phone-sized window. */
  .proof { padding: 58px 0 64px }
  .surfaces { padding: 56px 0 0 }
  .wedge, .pow, .faq, .final { padding: 60px 0 }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto }
  .blink { animation: none }
  /* Arrival and handoff are both flourishes, so both go first. */
  .shot-lead, .shot-cap, .hero-in, .hero-in > *, .hero-stage, .floater { opacity: 1 !important; transform: none !important; filter: none !important; animation: none !important }
  .reveal, .stagger-in > * { opacity: 1 !important; filter: none !important; transform: none !important; transition: none !important }
  .sc-panel { transition: none !important; transform: none !important; }
  .sc-panel.out { opacity: 0 }
  .btn, .row-card, .pow-card { transition: none }
  .ring-seg, .pill-arrow, .link-arrow { animation: none !important; transition: none !important }
}

/* ============================================================
   SHARED CHROME : nav + footer.
   Markup lives in site/partials/. Never edit the built copy in
   a page; edit the partial and run site/build.py.
   ============================================================ */

/* TONE: the bar follows the band underneath it.
   The page alternates dark and light the whole way down, so one treatment is
   wrong for half of it. Two discrete tones that swap at the boundary, never a
   translucent bar: a blurred nav picking up whatever is sliding under it was
   tried and rejected, because the nav then read as a different colour in every
   screenshot. Which tone is live is decided in site.js from each section's own
   computed background, so this can never disagree with the rules above. */
.site-nav {
  position: sticky; top: 0; z-index: 40;
  background: var(--dark);
  border-bottom: 1px solid var(--green-line);
  color: var(--white);
  transition: background 0.26s var(--ease-out-cubic), border-color 0.26s var(--ease-out-cubic);
}
.site-nav .brand, .site-nav .nav-top, .site-nav .nav-burger span, .site-nav .brand .atom {
  transition: color 0.26s var(--ease-out-cubic), background 0.16s;
}

.site-nav[data-tone="light"] {
  background: var(--gray-light);
  border-bottom-color: var(--rule);
  color: var(--text);
}
.site-nav[data-tone="light"] .brand { color: var(--text); }
.site-nav[data-tone="light"] .brand .atom { color: var(--green-bold); }
.site-nav[data-tone="light"] .nav-top { color: var(--text-muted); }
.site-nav[data-tone="light"] .nav-top:hover,
.site-nav[data-tone="light"] .has-menu:hover .nav-top { color: var(--text); background: rgba(92,158,74,0.1); }
.site-nav[data-tone="light"] .nav-top[aria-current="page"] { color: var(--green-bold); }
.site-nav[data-tone="light"] .nav-burger span { background: var(--text); }
.site-nav[data-tone="light"] .nav-drawer { border-top-color: var(--rule); }
.site-nav[data-tone="light"] .nav-drawer a { color: var(--text-muted); border-bottom-color: var(--gray-mid); }
/* The dropdown is a raised surface, so on a light bar it becomes paper. */
.site-nav[data-tone="light"] .menu { background: var(--white); border-color: var(--rule); box-shadow: 0 22px 50px -20px rgba(20,24,26,0.28); }
.site-nav[data-tone="light"] .menu a:hover { background: rgba(92,158,74,0.09); }
.site-nav[data-tone="light"] .menu b { color: var(--text); }
.site-nav[data-tone="light"] .menu i { color: var(--text-faint); }
/* The CTA does NOT change with the tone. It is the one action on the bar, and
   an action that restyles itself down the page reads as a different button. */
/* FULL BLEED, not the 1140 wrap. .nav-in is also .wrap, which centred the bar
   in a 1140 column and left the logo sitting 178px in from the edge at desktop
   width. Attio's mark is 28px from the screen edge and its CTA is hard against
   the right, which is what makes the bar read as chrome rather than as another
   centred content block. Only max-width is overridden; the wrap's own 28px
   gutter is exactly the inset we want, and it keeps the mobile behaviour. */
/* CENTRED RAIL. Flex packed the four links against the brand and left ~700px
   of nothing between them and the CTA, which is what made the bar read as
   unfinished at desktop width. A 1fr auto 1fr grid puts the links dead centre
   and pins brand and CTA to the two gutters, so the dead space is split into
   two equal margins instead of pooling in the middle. Falls back to flex below
   940px, where the links and CTA are hidden and only the brand and the burger
   are left to place. */
.nav-in {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 26px; height: 62px; max-width: none;
}
.nav-in > .brand { justify-self: start; }
/* The wordmark is the home link. Size is deliberately back at 20/22 after a
   larger version was tried and pulled: it changed the top of the page. What
   makes it read as clickable is the HOVER, lifted from the demo's own
   .mark:hover .atom, so the atom turns and the word brightens as one object. */
.brand { display: flex; align-items: center; gap: 9px; text-decoration: none; color: var(--white); }
.brand .atom {
  width: 22px; height: 22px; color: var(--green); flex: none;
  transition: color 0.22s var(--ease-out-cubic), transform 0.5s var(--ease-out-cubic);
}
.brand:hover .atom { color: var(--green-hi); transform: rotate(60deg); }
.wordmark { font-family: var(--serif); font-size: 20px; letter-spacing: 0.08em; font-weight: 400; transition: color 0.22s var(--ease-out-cubic); }
.brand:hover .wordmark { color: var(--green-hi); }
.site-nav[data-tone="light"] .brand:hover .atom,
.site-nav[data-tone="light"] .brand:hover .wordmark { color: var(--green-bold); }

.nav-links { display: flex; align-items: center; justify-content: center; gap: 4px; }
.nav-top {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 8px 12px; border-radius: 5px;
  font-family: var(--sans); font-size: 14px; font-weight: 500;
  color: #b4c9ad; text-decoration: none; background: none; border: 0; cursor: pointer;
  transition: color 0.16s, background 0.16s;
}
.nav-top:hover, .has-menu:hover .nav-top { color: var(--white); background: rgba(122,173,106,0.1); }
.nav-top[aria-current="page"] { color: var(--green); }
.chev { width: 14px; height: 14px; transition: transform 0.24s var(--ease-out-cubic); }
.has-menu:hover .chev, .has-menu:focus-within .chev { transform: rotate(180deg); }

.has-menu { position: relative; }
.menu {
  position: absolute; top: calc(100% + 6px); left: 0; width: 290px;
  background: var(--dark-panel);
  border: 1px solid var(--green-line);
  border-radius: 8px; padding: 7px;
  box-shadow: 0 22px 50px -18px rgba(0,0,0,0.7);
  opacity: 0; visibility: hidden;
  transform: translateY(-6px);
  /* Exits animate too, per motion rule 11: the menu leaves the way it arrived
     instead of being switched off. */
  transition: opacity 0.2s var(--ease-out-cubic), transform 0.24s var(--ease-out-cubic), visibility 0.24s;
}
.has-menu:hover .menu, .has-menu:focus-within .menu { opacity: 1; visibility: visible; transform: none; }
.menu a {
  display: block; padding: 10px 12px; border-radius: 5px; text-decoration: none;
  transition: background 0.15s;
}
.menu a:hover { background: rgba(122,173,106,0.12); }
.menu b { display: block; font-size: 13.5px; font-weight: 600; color: var(--white); margin-bottom: 2px; }
.menu i { display: block; font-style: normal; font-size: 12px; color: var(--green-muted); }
.menu-out { border-top: 1px solid var(--green-line); margin-top: 5px; padding-top: 12px !important; border-radius: 0 0 5px 5px; }
.menu-out b { color: var(--green); }

.nav-cta {
  justify-self: end;
  padding: 9px 17px; border-radius: 5px;
  background: var(--green); color: var(--dark);
  font-size: 13.5px; font-weight: 700; text-decoration: none; white-space: nowrap;
  transition: transform 0.22s var(--ease-out-back), background 0.16s;
}
.nav-cta:hover { background: var(--green-hi); transform: translateY(-1px); }
.site-nav[data-tone="light"] .nav-cta { background: var(--dark); color: var(--white); }
.site-nav[data-tone="light"] .nav-cta:hover { background: #1a3520; }
.nav-cta:active { transform: scale(0.97); }

.nav-burger { display: none; margin-left: auto; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav-burger span { display: block; width: 20px; height: 2px; background: var(--white); border-radius: 2px; transition: transform 0.26s var(--ease-out-cubic), opacity 0.2s; }
.nav-burger span + span { margin-top: 4px; }
.nav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.nav-drawer { display: none; flex-direction: column; padding: 8px 18px 20px; border-top: 1px solid var(--green-line); }
.nav-drawer a { padding: 13px 0; font-size: 15px; color: #b4c9ad; text-decoration: none; border-bottom: 1px solid rgba(30,58,30,0.7); }
.nav-drawer a:last-child { border-bottom: 0; }
.drawer-cta { margin-top: 14px; text-align: center; background: var(--green); color: var(--dark) !important; font-weight: 700; border-radius: 5px; }

@media (max-width: 940px) {
  .nav-in { display: flex; }
  .nav-links { display: none; }
  .nav-burger { display: block; }
  .nav-drawer:not([hidden]) { display: flex; }
  /* The CTA does NOT go behind the burger. It is the site's one conversion
     action, and hiding it put the only way to convert two taps away on every
     phone and every iPad portrait. The five nav LINKS are navigation and belong
     in a drawer; a button is not navigation. attio.com keeps a visible action
     in its collapsed bar for the same reason. It shrinks instead. */
  .nav-cta { display: inline-block; margin-left: auto; padding: 8px 14px; font-size: 12.5px; }
  .nav-burger { margin-left: 6px; }
}
@media (max-width: 430px) {
  /* Tighter, not gone. 390 is the iPhone Pro width and 430 the Pro Max, so a
     rule that dropped the site's one conversion action below 400 dropped it on
     the single most common phone there is. The label still fits at 12px with
     the padding closed up; measured 146px of the 204px the bar has spare. */
  .nav-cta { padding: 7px 11px; font-size: 12px; }
}
@media (max-width: 359px) {
  /* Below this the wordmark, the button and the burger genuinely stop sharing a
     line. The drawer's own CTA is the fallback and it is first in the drawer. */
  .nav-cta { display: none; }
  .nav-burger { margin-left: auto; }
}

/* ---- footer ---- */
/* On a page carrying both, there were two identical primary buttons about
   300px apart. That was written when the CTA band was dark and continuous with
   the footer; the band is off-white now and the duplication is the reason the
   rule stays, since the buttons are still 300px apart. The
   ask above is the ask; the footer's own copy stands down. Written as an
   adjacent-sibling rule rather than removed from the partial, because /briefing
   and /method have no CTA band and there the footer button is the only one. */
/* :has(), not the adjacent sibling it used to be. The rule asks "does this
   PAGE end in a closing band", and the old selector asked "is .final the
   element immediately before the footer", which are the same question only
   until something sits between them. On /newsletter the work strip and the
   email capture moved in below the band, the selector stopped matching, and the
   footer served a second identical "Join the open beta" about 600px under the
   first. Where :has() is unsupported the button simply shows, which is the
   behaviour before this rule existed rather than a broken layout. */
body:has(.final) .site-foot .foot-brand .btn { display: none; }
body:has(.final) .site-foot .foot-brand p { margin-bottom: 0; }

/* font-family declared, not inherited. This is a PARTIAL: it renders inside
   whatever page includes it, and /newsletter sets the serif on its body wrapper,
   so the same footer was rendering in Georgia there and in the sans everywhere
   else. A shared component states what it is rather than asking. */
.site-foot { background: var(--dark-deep); color: var(--white); padding: 68px 0 0; font-family: var(--sans); text-align: left; }
.foot-in { display: grid; grid-template-columns: 1.15fr 2fr; gap: 48px; padding-bottom: 48px; }
/* The mark, not just the word: same .brand object as the nav, so the atom and
   its hover turn come with it and the footer wordmark is also a home link. */
.foot-brand .brand { margin-bottom: 14px; }
.foot-brand .wordmark { margin-bottom: 0; }
.foot-brand p { font-size: 14px; line-height: 1.6; color: #8ea589; margin-bottom: 14px; max-width: 340px; }
/* The sign-off, not a third sentence of the descriptor: serif and green so it
   reads as the mark's own line rather than more body copy. */
.foot-brand .foot-line { font-family: var(--serif); font-size: 15px; color: var(--green); margin-bottom: 22px; }
.foot-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; }
.foot-cols h4 {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--green); font-weight: 400; margin-bottom: 14px;
}
.foot-cols a {
  display: block; font-size: 14px; color: #b4c9ad; text-decoration: none;
  padding: 5px 0; transition: color 0.15s;
}
/* 44px, on touch only. A 14px link with 5px of padding is a 31px target with
   5px between it and its neighbour, which is under every published minimum and
   is the footer of a site whose readers are mostly on a phone. Gated on
   (hover: none) so the desktop footer keeps its tight rhythm: the pointer that
   needs the room is the one that gets it. entel-chrome.css already does this
   for the app nav; the marketing footer never got the same pass. */
@media (hover: none) {
  .foot-cols a { min-height: 44px; display: flex; align-items: center; padding: 0; }
  .ab-links a { min-height: 44px; display: inline-flex; align-items: center; }
  /* The CONTROLS, not just the link lists. The burger is the only navigation on
     a phone and it was a 30px target; the wordmark is the home link on every
     page; the pill is the hero's third action. Padding, so nothing moves. */
  /* Sizing only, never `display`. The first version of this rule set
     display:flex to centre the bars, which also overrode the burger's own
     `display: none` above 940 and put a hamburger next to the full nav on every
     iPad. A touch rule may say how big a control is; whether it exists at this
     width belongs to the nav's own ladder. Symmetric padding does the centring:
     12 + 20 + 12 = 44 across, 15 + 14 + 15 = 44 down. */
  .nav-burger { min-width: 44px; min-height: 44px; padding: 15px 12px; }
  .brand { min-height: 44px; display: inline-flex; align-items: center; }
  .hero-pill { min-height: 44px; }
  /* NOT the legal-bar links. "Jace Arnold" and "Unsubscribe" sit inline inside
     a sentence, which is the case WCAG 2.5.8 exempts, and forcing 44px on them
     wrapped the bar into three rows and put 78px back onto a footer whose
     height was the complaint. The rule buys nothing a sentence does not already
     give: there is no adjacent target to mis-hit. */
  .tour-tab { min-height: 44px; }
}
.foot-cols a:hover { color: var(--green); }
.foot-legal {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  border-top: 1px solid var(--green-line); padding-top: 22px; padding-bottom: 30px;
}
.foot-legal p { font-size: 12.5px; color: #6b846a; }
.foot-legal a { color: var(--green-muted); }
.foot-year { font-family: var(--mono); letter-spacing: 0.05em; }

/* 860 was stacking the brand over the links on every iPad, which cost the
   footer a whole column's worth of height to buy nothing: at 768 the two masses
   still fit side by side. Stack at the phone line, where they genuinely do not.
   The three link groups stay three across the whole way down; see the phone
   block below for why that is the load-bearing choice. */
@media (max-width: 1023px) {
  .site-foot { padding-top: 48px; }
  .foot-in { gap: 34px; padding-bottom: 32px; }
  .foot-cols { gap: 20px 16px; }
  .foot-cols a { font-size: 13.5px; }
  .foot-brand p { font-size: 13.5px; line-height: 1.58; max-width: 46ch; }
  .foot-legal { padding-top: 18px; padding-bottom: 22px; }
}
@media (max-width: 767px) {
  .foot-in { grid-template-columns: 1fr; }
}

/* THE FOOTER STOPS BEING A PAGE (Jace, 2026-08-28).
   Stacked, it was running about 640px on a phone: desktop's 68px opening
   padding, a 48px gap, three link groups each spending 44px a row, and a legal
   bar with 52px of its own padding. None of that is content, it is desktop
   rhythm surviving into a window a third the width, and the footer is the one
   place on the site where nobody is reading and everybody is looking for one
   specific link.

   The 44px touch target is the one thing NOT traded away. It is the only rule
   here that exists for a person rather than for a look, and shrinking targets
   to buy back height would be optimising the wrong number. The height comes out
   of padding and gaps instead. */
@media (max-width: 767px) {
  .site-foot { padding-top: 40px; }
  .foot-in { gap: 26px; padding-bottom: 26px; }
  .foot-brand .brand { margin-bottom: 10px; }
  .foot-brand p { font-size: 13.5px; line-height: 1.55; margin-bottom: 10px; max-width: 42ch; }
  .foot-brand .foot-line { font-size: 14px; margin-bottom: 16px; }
  .foot-brand p { font-size: 13px; max-width: none; }
  /* THREE ACROSS, ONE ROW OF GROUPS.
     The footer's height is its link ROWS, and 44px targets mean rows are the
     expensive unit. Two columns stacks seven links down the first one; three
     columns puts all three groups side by side and the tallest group (Free
     tools, five links) sets the height ONCE. 128px, bought by using the width
     that was already sitting empty. Which is the same trade as the metrics
     above: less tall, more wide. */
  .foot-cols {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-auto-flow: row; grid-template-rows: auto;
    gap: 20px 12px; align-content: start;
  }
  .foot-cols h4 { margin-bottom: 4px; font-size: 11px; letter-spacing: 0.06em; }
  /* 13px so "The ENTEL Wire" still holds one line in a 107px column. The 44px
     target is untouched: it is the only rule in this block that exists for a
     thumb rather than for a look. */
  .foot-cols a { font-size: 13px; }
  .foot-legal { padding-top: 16px; padding-bottom: 20px; gap: 6px; }
  .foot-legal p { font-size: 11.5px; line-height: 1.5; }
}

/* ============ INTERIOR PAGE HEAD (pricing, and pages that follow it) ============ */
.page-head { background: var(--dark); color: var(--white); padding: 76px 0 68px; position: relative; overflow: hidden; }
.page-head::before {
  content: ""; position: absolute; inset: -30% -10% auto -10%; height: 520px;
  background: radial-gradient(ellipse 46% 42% at 50% 30%, rgba(122,173,106,0.15), transparent 68%);
  pointer-events: none;
}
.page-head .wrap { position: relative; z-index: 1; max-width: 800px; }
.page-h1 {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(30px, 4.6vw, 50px); line-height: 1.1; letter-spacing: -0.028em;
  margin-bottom: 18px;
}
.page-deck { font-size: 16.5px; line-height: 1.65; color: #b4c9ad; max-width: 640px; }

/* ============ ABOUT ============
   Deliberately thin: the page reuses .page-head, .price-grid/.price-card and
   .final rather than growing its own components. These four rules are the only
   things the shared set did not already cover. */
.about-body { background: var(--gray-light); padding: 72px 0 80px; }
.ab-block { max-width: 720px; }
.ab-block + .ab-block { margin-top: 58px; padding-top: 54px; border-top: 1px solid var(--rule); }
.ab-p { font-size: 15.5px; line-height: 1.72; color: var(--text-muted); margin-bottom: 16px; max-width: 66ch; }
.ab-links { display: flex; gap: 22px; flex-wrap: wrap; margin-top: 6px; }
.ab-links a, .pc-mail a { font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.06em; color: var(--green-bold); text-decoration: none; }
.ab-links a:hover, .pc-mail a:hover { text-decoration: underline; text-underline-offset: 3px; }
/* The evidence classes, on About. Five rows of label + definition, which is a
   table's job, so it is a table. Paper, because the classes are the evidence
   rule itself (DESIGN-SYSTEM 7.1). */
.ab-classes { width: 100%; border-collapse: collapse; margin: 22px 0 18px; max-width: 720px; }
.ab-classes th { text-align: left; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--green-bold); padding: 0 14px 10px 0; border-bottom: 1px solid var(--rule); font-weight: 400; }
.ab-classes td { padding: 12px 14px 12px 0; border-bottom: 1px solid var(--rule); font-size: 14.5px; line-height: 1.55; color: var(--text-muted); vertical-align: top; }
.ab-classes td:first-child { font-family: var(--serif); font-size: 19px; color: var(--text); width: 54px; }
.about-method { background: var(--white); padding: 76px 0 82px; border-top: 1px solid var(--rule); }
.about-people { background: var(--white); padding: 76px 0 84px; border-top: 1px solid var(--rule); }
.about-people .h2 { margin-bottom: 34px; }
/* Zeroed, and QUALIFIED so it actually lands. This shipped as a bare
   `.ab-people { margin-bottom: 26px }` that never applied: .price-grid sets 76px
   on the same element at equal specificity and is declared further down the
   file, so it always won and the real gap under these cards has been 76px the
   whole time. The 26px was meant to hold the addresses and the fine print close
   under the grid, and both are gone now (the addresses moved into the cards), so
   the section should close on its own 84px bottom padding like .about-body and
   .about-method above it. Qualified rather than moved, so it cannot silently
   lose this argument again if either block gets reordered. */
.about-people .ab-people { margin-bottom: 0; }

.price-body { background: var(--gray-light); padding: 62px 0 92px; }
/* start, not stretch. Stretched, the shorter of the two cards grows to the
   taller one's height and parks its button 130-180px below its own last line,
   which reads as a card missing a bullet. */
.price-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 76px; align-items: start; }
.price-card {
  background: var(--white); border: 1px solid var(--rule); border-radius: 8px;
  padding: 32px 30px 30px; display: flex; flex-direction: column;
}
.price-card-alt { border-left: 3px solid var(--green); }
.price-card h2 { font-family: var(--serif); font-size: 25px; font-weight: 400; letter-spacing: -0.015em; margin-bottom: 8px; }
.pc-lede { font-size: 15px; color: var(--text-muted); margin-bottom: 22px; }
.price-card ul { list-style: none; margin-bottom: 26px; flex: 1; }
.price-card li {
  font-size: 14.5px; line-height: 1.6; color: #3d3d3d;
  padding: 10px 0 10px 22px; border-bottom: 1px solid var(--gray-mid); position: relative;
}
.price-card li::before { content: ""; position: absolute; left: 0; top: 18px; width: 9px; height: 1px; background: var(--green-bold); }
.price-card li:last-child { border-bottom: 0; }
.price-card .btn { align-self: flex-start; }
.pc-mail { margin-top: 2px; }

.price-notes .h2 { margin-bottom: 28px; }
.note-row { display: grid; grid-template-columns: 1fr 1fr; gap: 34px; }
.note-row h3 { font-family: var(--serif); font-size: 19px; font-weight: 400; margin-bottom: 10px; letter-spacing: -0.01em; }
.note-row p { font-size: 15px; line-height: 1.68; color: #3d3d3d; }
.note-row a, .price-card li a { color: var(--green-bold); text-underline-offset: 3px; }

@media (max-width: 767px) {
  /* Was 860, which stacked two ~350px cards inside a 712px window and left the
     CTA sitting at the far left of a card with 530px of void beside it. They
     fit side by side down to phone width; below that the stacked card takes a
     measure rather than the full window, which is also what attio.com does. */
  .price-card, .note-row > * { max-width: 560px; margin-inline: auto; }
}
@media (max-width: 1023px) {
  /* .note-row p had no cap at all, so at 768 the body copy ran to 102
     characters a line. 68ch is the top of the readable band. */
  .note-row p { max-width: 68ch; }
}
@media (max-width: 767px) {
  .price-grid, .note-row { grid-template-columns: 1fr; }
  .page-head { padding: 54px 0 48px; }
}

/* ============ METHOD ============
   /method shipped with markup and no stylesheet. Every class its body uses
   (.m-block, .m-p, .m-table, .cls-a..e, .deriv, .m-cta) resolved to nothing, so
   the page was unstyled prose at 150 characters a line with a raw table in the
   middle of it, at every width including the laptop. Found while grading the
   breakpoints, which is where an unstyled page shows up: you cannot make a page
   read well narrow when it does not read at all wide.

   Built on /about's components rather than beside them. Both pages are the same
   object, a column of prose blocks separated by rules, and the reason they had
   different class names is that they were written a week apart. The measure,
   the rule between blocks and the table are literally /about's; only the three
   things /method has and /about does not (the class chips, the derived-rule
   card, the closing CTA) are new. */
.method-body { background: var(--white); padding: 76px 0 88px; }
.m-block { max-width: 720px; }
.m-block + .m-block { margin-top: 58px; padding-top: 54px; border-top: 1px solid var(--rule); }
.m-block .h2 { font-size: clamp(23px, 2.6vw, 31px); margin-bottom: 18px; }
.m-p { font-size: 15.5px; line-height: 1.72; color: var(--text-muted); margin-bottom: 16px; max-width: 66ch; }
.m-fine { font-size: 13.5px; line-height: 1.66; color: var(--text-faint); max-width: 62ch; margin-top: 18px; }

.m-table { width: 100%; border-collapse: collapse; margin: 24px 0 4px; max-width: 720px; }
.m-table th {
  text-align: left; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--green-bold); font-weight: 400;
  padding: 0 14px 10px 0; border-bottom: 1px solid var(--rule);
}
.m-table td { padding: 12px 14px 12px 0; border-bottom: 1px solid var(--rule); font-size: 14.5px; line-height: 1.55; color: var(--text-muted); vertical-align: top; }
.m-table td:first-child { width: 52px; }
/* The 52px above is sized for a one-character key column (the A/B table).
   Tables whose first column is a phrase need a real share of the width, or
   the label wraps to five lines beside a paragraph that does not wrap at all. */
.m-table--labels td:first-child { width: 34%; padding-right: 22px; color: var(--text); }

/* The class chip. One recipe, five grades, and the grade is carried by the
   BORDER rather than by five fills: five saturated badges in a column would be
   the only colour on the page and would rank the classes by loudness. */
.cls {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 5px;
  font-family: var(--mono); font-size: 12px; font-weight: 700;
  color: var(--text); background: var(--paper); border: 1px solid var(--rule);
}
.cls-a { border-color: var(--green-bold); color: var(--green-bold); }
.cls-b { border-color: rgba(47,110,34,0.55); }
.cls-c { border-color: rgba(47,110,34,0.38); }
.cls-d, .cls-e { color: var(--text-muted); }

/* The published rule, rendered as the object it claims to be. Paper and a
   hairline, same as .row-card: this is evidence, not a callout. */
.deriv { background: var(--paper); border: 1px solid var(--rule); border-radius: 6px; padding: 20px 22px 18px; margin: 22px 0 4px; max-width: 560px; }
.deriv-h { font-family: var(--serif); font-size: 17px; color: var(--text); margin-bottom: 14px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.deriv-lanes { list-style: none; margin: 0; padding: 0; }
.deriv-lanes li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 8px 0; border-bottom: 1px solid var(--rule); font-size: 14.5px; color: var(--text-muted);
}
.deriv-lanes li:last-child { border-bottom: 0; }
.deriv-lanes b { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint); font-weight: 400; white-space: nowrap; }
.deriv-lanes .w-open { color: var(--green-bold); font-weight: 700; }
.deriv-lanes .w-near { color: var(--text); font-weight: 700; }
.deriv-basis { font-size: 13px; line-height: 1.6; color: var(--text-faint); margin: 14px 0 0; padding-top: 13px; border-top: 1px solid var(--rule); }

.m-cta { max-width: 720px; margin-top: 58px; padding-top: 54px; border-top: 1px solid var(--rule); }
/* Qualified, because .m-cta p below is (0,1,1) and a bare .m-cta-h is (0,1,0),
   so the block's body rule was winning and rendering the closing line at 15px. */
.m-cta .m-cta-h { font-family: var(--serif); font-size: 22px; color: var(--text); margin-bottom: 10px; }
/* The Derived stamp exists in this file only scoped to .tour-fine, so on
   /method it was rendering as the word "Derived" in running prose, which is the
   one thing it must not do: the whole point is that a reading is MARKED as a
   reading wherever it appears. Same recipe, this page's ground. */
.method-body .stamp {
  display: inline-block; vertical-align: baseline;
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--green-bold); background: rgba(47,110,34,0.09);
  border: 1px solid rgba(47,110,34,0.28); border-radius: 3px;
  padding: 2px 6px; white-space: nowrap;
}
.m-cta p { font-size: 15px; line-height: 1.7; color: var(--text-muted); margin-bottom: 22px; max-width: 64ch; }
.m-cta .hero-cta { justify-content: flex-start; }
.method-body .btn-ghost { color: var(--text); border-color: var(--rule); background: var(--white); }
.method-body .btn-ghost:hover { border-color: var(--green-bold); color: var(--green-bold); }

/* Enumerated prose list for the method-family pages. The global reset strips
   ul padding, so a bare <ul> in an .m-block would clip its own markers; this is
   the same tick the price cards use, at .m-p's measure and colour. Added for
   /terms and /privacy, where an obligation list is the readable shape and a
   paragraph is not. No new token. */
.m-list { list-style: none; margin: 4px 0 18px; max-width: 66ch; }
.m-list li { position: relative; padding: 0 0 11px 18px; font-size: 15.5px; line-height: 1.72; color: var(--text-muted); }
.m-list li:last-child { padding-bottom: 0; }
.m-list li::before { content: ""; position: absolute; left: 0; top: 12px; width: 9px; height: 1px; background: var(--green-bold); }
.m-list b { color: var(--text); font-weight: 600; }
.m-p a, .m-fine a, .m-list a, .m-cta p a { color: var(--green-bold); text-underline-offset: 3px; }

@media (max-width: 767px) {
  .method-body { padding: 56px 0 66px; }
  .m-block + .m-block, .m-cta { margin-top: 42px; padding-top: 38px; }
  /* The class table is label + definition, so the definition is what has to
     keep its measure. Below this the two-column row stops working and the whole
     table becomes a scroller rather than two cramped columns. */
  .m-table { display: block; overflow-x: auto; }
  .m-cta .hero-cta { flex-direction: column; align-items: stretch; }
  .m-cta .hero-cta .btn { width: 100%; text-align: center; }
}

/* ============ WAITLIST FORM (the /beta conversion surface) ============ */
.beta-head { padding-bottom: 78px; text-align: center; }
.beta-head .wrap { max-width: 860px; }
.beta-head .kicker { justify-content: center; }
.beta-head .page-deck { margin-left: auto; margin-right: auto; }
.beta-head .page-h1 .accent { color: var(--green); }

.form-shell { max-width: 560px; margin: 34px auto 0; }

.wl-form { display: flex; gap: 9px; flex-wrap: wrap; }
.wl-form.gone { display: none; }
.wl-form input[type="email"], .wl-form input[type="text"], .wl-form select {
  flex: 1; min-width: 0; padding: 14px 15px;
  font-family: var(--sans); font-size: 15px;
  border: 1px solid #2f5230; background: rgba(8,18,8,0.72); color: var(--white);
  border-radius: 5px; outline: none;
  transition: border-color 0.16s, box-shadow 0.16s;
}
.wl-form input::placeholder { color: #6d8a6a; }
.wl-form input:focus, .wl-form select:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(122,173,106,0.16);
}
.wl-form button {
  padding: 14px 24px; font-family: var(--sans); font-size: 14px; font-weight: 700;
  color: var(--dark); background: var(--green); border: 0; border-radius: 5px;
  cursor: pointer; letter-spacing: 0.02em; white-space: nowrap;
  transition: transform 0.22s var(--ease-out-back), background 0.16s;
}
.wl-form button:hover:not(:disabled) { background: var(--green-hi); transform: translateY(-2px); }
.wl-form button:active:not(:disabled) { transform: translateY(0) scale(0.98); }
.wl-form button:disabled { opacity: 0.55; cursor: default; }

/* Honeypot. Out of flow, so nothing on the page moves. Not type="hidden":
   better bots skip those, and this one has to look fillable. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

.form-fine { font-size: 13px; color: #8ba587; margin-top: 12px; }
.form-fine.gone { display: none; }
.form-err {
  display: none; font-size: 13.5px; line-height: 1.5; color: #ffc4b4;
  margin-top: 12px; text-align: left;
  border-left: 2px solid #ffc4b4; padding-left: 11px;
}
.form-err.on { display: block; }

/* Step two arrives as one gesture, so it reads as a continuation of step one
   rather than a second form the visitor did not ask for. */
.step2 { display: none; flex-direction: column; gap: 9px; text-align: left; }
.step2.on { display: flex; }
.step2.gone { display: none; }
.step2.on > *:not(.sr):not([type="hidden"]) { animation: streamin 0.4s var(--ease-out-cubic) both; }
/* Targeted rather than nth-child: the hidden id input and the sr-only labels are
   children too, so counting positions spends the stagger on things nobody sees
   and the visible fields arrive late. */
.step2.on input[name="company"] { animation-delay: 0.06s }
.step2.on .step2-row           { animation-delay: 0.12s }
.step2.on button               { animation-delay: 0.18s }
@keyframes streamin { from { opacity: 0; filter: blur(6px); transform: translateY(10px) } to { opacity: 1; filter: none; transform: none } }
.step2-lede { font-size: 14.5px; line-height: 1.55; color: #b4c9ad; margin-bottom: 2px; }
.step2-row { display: flex; gap: 9px; }
.step2-row select { flex: 1; min-width: 0; appearance: none; cursor: pointer; }
.step2 select option { background: var(--dark-panel); color: var(--white); }
.step2 button { align-self: flex-start; }

.wl-done { display: none; text-align: left; border-left: 2px solid var(--green); padding-left: 16px; }
.wl-done.on { display: block; animation: streamin 0.5s var(--ease-out-cubic) both; }
.wl-done-h { font-size: 17px; font-weight: 700; color: var(--green); margin-bottom: 6px; }
.wl-done p { font-size: 14.5px; line-height: 1.6; color: #b4c9ad; }

/* ---- /beta body ---- */
.beta-body { background: var(--gray-light); padding: 76px 0 88px; }
.beta-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 52px; align-items: start; }
.beta-cols .h2 { font-size: clamp(23px, 2.6vw, 30px); margin-bottom: 20px; }
.beta-list { list-style: none; }
.beta-list li {
  font-size: 15px; line-height: 1.62; color: #3d3d3d;
  padding: 15px 0 15px 18px; border-left: 2px solid var(--green-light);
  border-bottom: 1px solid var(--rule); background: var(--white);
  padding-right: 18px;
}
.beta-list li:last-child { border-bottom: 0; }
.beta-list strong { color: var(--text); }
.beta-p { font-size: 15.5px; line-height: 1.7; color: #3d3d3d; margin-bottom: 26px; }
.beta-aside { background: var(--white); border: 1px solid var(--rule); border-radius: 7px; padding: 22px 24px; }
.ba-h { font-family: var(--serif); font-size: 18px; margin-bottom: 8px; }
.beta-aside p { font-size: 14.5px; line-height: 1.62; color: var(--text-muted); }
.ba-links { margin-top: 14px; display: flex; gap: 18px; flex-wrap: wrap; }
.ba-links a { font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.06em; color: var(--green-bold); text-decoration: none; }
.ba-links a:hover { text-decoration: underline; text-underline-offset: 3px; }

@media (max-width: 860px) {
  .beta-cols { grid-template-columns: 1fr; gap: 40px; }
  .wl-form input, .wl-form button { width: 100%; flex: none; }
  .step2-row { flex-direction: column; }
}
@media (prefers-reduced-motion: reduce) {
  .step2.on > *, .wl-done.on { animation: none !important; }
}

/* ============================================================
   CLIP SLOTS.
   Placeholders for the product demo clips, holding real layout
   and real copy so the page can be judged at full length before
   a single frame is recorded. Each slot states what it will
   show and which screen it is recorded from, so the shot list
   lives on the page rather than in a separate document.
   Swap .clip-frame's contents for <video> and keep the frame.
   ============================================================ */

.clip-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: 9px;
  overflow: hidden;
  background: var(--dark-deep);
  border: 1px solid var(--green-line);
  display: flex; flex-direction: column;
}
.clip-meta {
  display: flex; justify-content: space-between; align-items: center;
  padding: 11px 15px;
  border-bottom: 1px solid var(--green-line);
  background: var(--dark-deep);
}
.clip-no {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em;
  color: var(--green); text-transform: uppercase;
}
.clip-no::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 0; height: 0; margin-right: 8px;
  border-left: 7px solid var(--green);
  border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;
}
.clip-dur { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; color: #5e7a5c; }

.clip-v { width: 100%; height: 100%; object-fit: cover; display: block; background: var(--dark-deep); }
/* The hero's lead clip has no fixed-ratio frame around it, so height:100% would
   resolve against nothing and it would sit at its intrinsic size in a wider
   column. Let it lay out on its own aspect instead. The showcase panel does have
   a sized frame, and its rule lives with the rest of the showcase above. */
.shot-lead .clip-v { height: auto; object-fit: fill; }

/* Caption sits under the frame, not inside it: at grid size, text laid over a
   moving picture is unreadable, and the claim is the half that has to survive. */
.clip-cap {
  display: flex; align-items: baseline; gap: 12px;
  padding-top: 11px; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
}
.clip-cap-lg { display: block; }
.clip-cap-lg h3 {
  font-family: var(--serif); font-size: 19px; font-weight: 400; letter-spacing: -0.01em;
  margin: 9px 0 7px; color: var(--text);
}
.clipfeat .clip-cap { justify-content: space-between; }
.clip-cap-lg .clip-what {
  /* The caption strip is mono for the clip number and duration. Body copy has
     to opt back out, or the claim renders as code. */
  font-family: var(--sans); font-size: 14.5px; line-height: 1.6; letter-spacing: 0;
  color: #3d3d3d;
}
.clip-cap-lg .clip-src { margin-top: 10px; color: var(--text-faint); }

.clip-body { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 22px 26px; }
.clip-body h3 {
  font-family: var(--serif); font-size: 19px; font-weight: 400;
  color: var(--white); margin-bottom: 10px; letter-spacing: -0.01em;
}
.clip-what { font-size: 14.5px; line-height: 1.62; color: #b4c9ad; }
.clip-src { font-family: var(--mono); font-size: 11px; color: #5e7a5c; margin-top: 12px; letter-spacing: 0.03em; }
.clip-src code { color: var(--green-muted); }

.clip-lg .clip-body { padding: 30px 34px; }
.clip-lg .clip-what { font-size: 16px; line-height: 1.6; }

/* Feature rows: copy beside a large slot, direction alternating. */
.clipfeat { background: var(--dark); color: var(--white); padding: 88px 0; }
.clipfeat-alt { background: var(--dark-deep); }
/* The clip takes the wider column: it is the evidence, the copy is the caption.
   align-items:start rather than center, because centring short copy against a
   tall frame opens a void under the paragraph. */
.clipfeat-in { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 52px; align-items: center; }
.clipfeat:not(.clipfeat-alt) .clipfeat-in { grid-template-columns: 0.85fr 1.15fr; }
.clipfeat-copy p { font-size: 15.5px; line-height: 1.68; color: #b4c9ad; }
.clipfeat-copy p + p { margin-top: 16px; }
.clipfeat-copy .h2 { margin-bottom: 18px; }

/* Grid of the remaining four. */
.clipgrid-sec { background: var(--gray-light); padding: 88px 0; }
.clipgrid-sec .eyebrow { color: var(--green-bold); }
.clipgrid-sec .h2 { margin-bottom: 38px; }
.clipgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
/* On light ground the slot inverts to stay a dark product surface, which is the
   rule the rest of the site follows: product renders dark, prose renders light. */
.clipgrid .clip-frame { border-color: #cdd5c8; }
.clipgrid-note {
  margin-top: 26px; font-size: 13.5px; line-height: 1.65; color: var(--text-muted);
  max-width: 720px;
}
.clipgrid-note code { font-family: var(--mono); font-size: 12.5px; color: var(--green-bold); }

@media (max-width: 900px) {
  .clipfeat-in { grid-template-columns: 1fr; gap: 30px; }
  /* Copy leads on narrow screens regardless of desktop order, so the slot never
     arrives before the sentence it illustrates. */
  .clipfeat-alt .clipfeat-copy { order: -1; }
  .clipgrid { grid-template-columns: 1fr; }
  .clipfeat, .clipgrid-sec { padding: 60px 0; }
}

/* ============ TRUSTED BY ============
   Directly under the hero. Renders only when data/proof.json has entries; see
   build.py render_proof for why the empty state here is absence. */
.trustedby { background: var(--dark-deep); color: var(--white); padding: 30px 0 32px; border-top: 1px solid var(--green-line); }
.tb-label {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--green); text-align: center; margin-bottom: 18px;
}
.tb-list { list-style: none; display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 40px; }
.tb-list li { text-align: center; }
.tb-org { display: block; font-family: var(--serif); font-size: 19px; color: #d3e2ce; letter-spacing: -0.01em; }
.tb-role { display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: #6f8a6c; margin-top: 4px; }

/* ============ WHY NOW  +  PULLQUOTE ============
   The scroll reveal. .whynow pins inside .reveal-wrap and .pullquote rides up
   over it on a higher stacking context. Pure CSS: no scroll listener, so there
   is nothing to jank, and where sticky is unsupported it degrades to a plain
   stack that reads the same.

   The pin is scoped to the wrapper on purpose. A page-level sticky would stay
   parked behind every section below it, and because the nav tone observer
   watches a one pixel band under the bar, a permanently pinned band would keep
   intersecting it and fight whatever section is actually on screen. */
.reveal-wrap { position: relative; }

.whynow {
  position: sticky; top: 0; z-index: 0;
  background: var(--dark);          /* the house dark, same as every other band */
  min-height: 100vh;
  display: flex; align-items: center;
  padding: 96px 0;
  /* Upstream to Why now, the same seam .wedge draws above itself for Product to
     Upstream (Jace, 2026-08-27). Three dark bands run back to back here, so
     without it the passage from the wedge into the pinned band is the only
     section change on the page with nothing marking it, and the run reads as one
     very long band rather than as three.

     On the section, not on .wedge's bottom, so it matches how every other band
     on this page opens. Being sticky costs nothing here: the rule is only ever
     hidden once .whynow has pinned to top:0, where the nav (z-40, over this
     band's z-0) is already covering that strip anyway. */
  border-top: 1px solid var(--green-line);
}
.whynow-in { width: 100%; }
.whynow-h {
  font-family: var(--serif); font-weight: 700;
  font-size: clamp(28px, 4vw, 46px); line-height: 1.15;
  letter-spacing: -0.5px; color: var(--white);
  margin: 18px 0 46px; max-width: 20ch;
}
.whynow-dim { color: var(--green-muted); }
.whynow-list {
  list-style: none; display: grid; gap: 34px;
  grid-template-columns: repeat(3, 1fr); margin: 0 0 44px; padding: 0;
}
.whynow-list b {
  display: block; font-family: var(--serif); font-weight: 700;
  font-size: clamp(28px, 3.2vw, 40px); color: var(--green);
  line-height: 1; margin-bottom: 12px;
}
.whynow-list span {
  display: block; font-size: 14.5px; line-height: 1.6;
  color: var(--green-muted); max-width: 30ch;
}
.whynow-tail {
  font-family: var(--serif); font-size: clamp(17px, 1.8vw, 21px);
  line-height: 1.45; color: var(--white); max-width: 48ch;
  padding-top: 30px; border-top: 1px solid var(--green-line);
}

/* The quote takes the full viewport as it arrives, which is the moment the nav
   flips to its light treatment. Sized well below the why-now headline so the
   handoff reads as a settling rather than a second shout. */
.pullquote {
  position: relative; z-index: 1;
  background: var(--gray-light);
  min-height: 100vh;
  display: flex; align-items: center;
  padding: 96px 0;
}
.pq-text {
  font-family: var(--serif); font-weight: 700;
  font-size: clamp(26px, 3vw, 42px); line-height: 1.28;
  letter-spacing: -0.3px; color: var(--text);
  text-align: center; margin: 0 auto; max-width: 26ch;
}
.pq-dim { color: #98a394; }
/* The last word carries the accent because the section under this one is the
   scaling ramp: the eye leaves the quote already on the word the curve draws. */
.pq-accent { color: var(--green-bold); }
/* The browser's own blockquote margin is 40px inline, which would knock the
   centred quote off centre. Zeroed here rather than on the children, because
   .pq-text and .pq-by centre themselves with margin auto. */
.pq-quote { margin: 0; }
.pq-by a { color: var(--green-bold); text-decoration-color: rgba(47,110,34,0.4); text-underline-offset: 3px; }
.pq-by a:hover { color: var(--green); }
.pq-by {
  text-align: center; margin: 28px auto 0; max-width: 66ch;   /* the citation sits on one line, so the venue is not split across a break */
  font-size: 13.5px; line-height: 1.6; color: var(--text-muted);
}

/* ============ DOT GRID ============
   The separator idea, borrowed from Attio: instead of alternating off-white
   and white to mark a new band, keep the ground white and change its TEXTURE.
   Two white sections back to back read as distinct because one carries a faint
   matrix and the other does not, and it costs no extra colour in a palette
   that only has one accent. Add the class to any light section. */
.dotgrid {
  /* --rule, not --gray-mid. At #e8e8e4 on a #f2f2f0 ground the matrix was very
     nearly invisible, which meant the texture was not doing the one job it has:
     separating two bands that share a colour. One step darker and 0.25px wider
     is still texture rather than pattern. */

  /* THE MATRIX ENDS AT THE RAILS (Jace, 2026-08-27), for the same reason the
     projection ramp does: the rails claim to bound the content, so texture that
     runs under them reads as the band ignoring its own edges. The ramp got an
     inset box; the dots cannot, because .rails already spends ::before and
     ::after on the two hairlines and these sections have no spare pseudo.

     So the ground paints back OVER the matrix instead. Three layers, first
     listed on top: an opaque strip of the band's own ground out to the rail on
     each side, then the dots underneath. Cheaper than a wrapper div and it
     needs no markup in the four templates that carry the class.

     The dot column is offset half a cell inside the left rail so the matrix
     opens on a whole dot rather than a shaved one. On the right the band width
     decides where the last column lands, so a dot there can be cut flush by the
     strip; at 1.25px in --rule that reads as the texture stopping at the line.

     --rail-inset comes from .rails on the same element. newsletter.html carries
     .final.dotgrid WITHOUT .rails, so the fallback is 0px: no strips, full
     bleed, which is right on a page that draws no rails to stop at. Below
     1239px the rails are hidden and the override further down does the same. */
  background-image:
    linear-gradient(to right, var(--gray-light) var(--rail-inset, 0px), transparent var(--rail-inset, 0px)),
    linear-gradient(to left,  var(--gray-light) var(--rail-inset, 0px), transparent var(--rail-inset, 0px)),
    radial-gradient(var(--rule) 1.25px, transparent 1.25px);
  background-repeat: no-repeat, no-repeat, repeat;
  background-size: auto, auto, 22px 22px;
  background-position: 0 0, 0 0, calc(var(--rail-inset, 0px) + 10px) -1px;
}

/* ============ PROJECTION ============
   Copy sits ON the buildout: hairlines and the target curve are the section
   ground, full bleed, with the trajectory running off the top right corner.
   Stats separate on a top rule, never a left bar. */
.projection {
  position: relative; overflow: hidden;
  background: var(--gray-light);
  border-top: 1px solid var(--rule);
}
/* THE RAMP ENDS AT THE RAILS (Jace, 2026-08-27), rather than bleeding the full
   width of the window under them. It was the one figure on the page that ran
   past the rails, and once the rails exist that reads as the curve escaping the
   column rather than as a deliberate bleed: the rails claim to bound the
   content, and the largest object on the band ignored them.

   Left and right only. The ramp still exits the TOP of the band, which is the
   bleed that matters, because the trajectory is supposed to keep climbing past
   the frame. Bounding it vertically as well would close the curve into a chart.

   The SVG carries preserveAspectRatio="none" and sizes at 100%, so narrowing
   this box refits the whole curve inside the rails instead of cropping its
   right end off in mid air. Reads the same --rail-inset as the rails above, so
   the two land on one line; at 1239px and below the rails are hidden, so the ramp
   goes back to full bleed and nothing looks clipped to an edge that is not drawn. */
.proj-bg {
  position: absolute; z-index: 0;
  top: 0; bottom: 0;
  left: var(--rail-inset);
  right: var(--rail-inset);
}
@media (max-width: 1239px) {
  .proj-bg { left: 0; right: 0; }
}
.proj-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.proj-line { stroke: var(--green-bold); stroke-width: 2.5; vector-effect: non-scaling-stroke; }
/* No display here. The chip is a flex box and that is what centres the label
   inside it; display:block put the text hard against the top of the fill. */
.projection .eyebrow { margin-bottom: 20px; }

/* Left, not centred, and narrower. Centred at 780 the block ran to x=1110 and
   the trajectory crossed the fourth stat and the foot of the note.
   The .wrap keeps its own 1140 and its gutter, so the copy starts on the same
   left edge as every other section on the page; it is the CONTENT that is
   capped at 700, which puts its right edge near x=878 where the curve is still
   well below the stats. Capping .wrap itself instead re-centred the narrow
   block, and zeroing its margin threw it against the viewport edge. */
/* The horizontal padding has to be restated. .proj-in is also .wrap, whose
   `padding: 0 28px` a shorthand here would otherwise zero out. On desktop the
   wrap's own centring hides that; at 390px it put the headline flat against the
   left edge of the screen. */
.proj-in { position: relative; z-index: 1; padding: 104px 28px 124px; }
.proj-h, .proj-stats, .proj-note { max-width: 700px; }
.proj-h {
  font-family: var(--serif); font-weight: 700;
  font-size: clamp(28px, 3.6vw, 44px); line-height: 1.16;
  letter-spacing: -0.5px; color: var(--text); margin: 0 0 38px;
}
.proj-dim { color: var(--text-muted); }
/* One view's headline was the only two-line head on the page whose second line
   was not dimmed. .whynow-h and .proj-h both split claim from consequence by
   colour, so this was the odd one out rather than a deliberate exception
   (Jace, 2026-08-28). Same token, so there is one dim and not three. */
.ov-dim { color: var(--text-muted); }
/* Narrow only: at full measure the head sets on two lines and needs no help. */
.ov-h-br { display: none; }
@media (max-width: 767px) { .ov-h-br { display: inline; } }
.proj-stats {
  list-style: none; margin: 0 0 26px; padding: 0;
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 26px 22px;
}
.proj-stats li { padding-top: 14px; border-top: 1px solid var(--rule); }
/* Buildout runs two stats now, not four: ENTEL's own project count beside the
   federal build number it is a count OF. Two columns, and the second carries
   the accent so the pair reads as ours-then-theirs rather than as a list. */
.proj-stats-2 { grid-template-columns: repeat(2, 1fr); max-width: 560px; }
.proj-stats-2 li.accent b { color: var(--green-bold); }
.proj-stats b {
  display: block; font-family: var(--serif); font-weight: 700;
  font-size: clamp(22px, 2.3vw, 30px); line-height: 1; color: var(--text);
  margin-bottom: 7px;
}
.proj-stats span { display: block; font-size: 13px; line-height: 1.5; color: var(--text-muted); }
.proj-note { font-size: 12.5px; line-height: 1.6; color: var(--text-faint); max-width: 58ch; margin: 0; }

/* THE TWO-UP RUNG.
   Three columns dropping straight to one is the move that turns a tablet into a
   long phone, and it is the thing Jace named: nothing should be squished into a
   single vertical column while the window still has room for two. Each of these
   items is capped at about 30ch anyway, so at 768 two of them fit with measure
   to spare. .proj-stats already graded 4 -> 2 -> 1 and is the model. */
@media (max-width: 1023px) {
  /* THE PIN STAYS. It was being switched off at 900 and the reveal went with
     it: .pullquote only rides UP over .whynow if .whynow is pinned at a
     viewport's height for it to ride over. Without it the two sections are just
     two stacked bands and the page's one structural gesture is a desktop-only
     feature. What was actually wrong was never the pin, it was the content:
     three stacked metric blocks are taller than a phone, so the section
     overflowed its own pin. Fixed below by changing the metric's shape rather
     than by dropping the mechanic. */
  /* NO MINIMUM HEIGHT BELOW THE LAPTOP (Jace, 2026-08-28).
     The band was a viewport tall so the quote had a full screen to ride over.
     On a window this narrow its content is only about two thirds of that, and
     reserving the rest just moved the empty space around: centred it opened a
     hole above the section, top-aligned it opened one below. There is no third
     place to put height nothing is using, so it stops being reserved.

     The reveal does not depend on it. .whynow is still sticky, so it still pins
     and the quote still rides up over it; the band is simply as tall as what is
     in it, and the quote covers it sooner. Landscape and up keep the full
     viewport, where the content genuinely fills it. */
  .whynow { min-height: 0; padding: 84px 0 72px; align-items: flex-start; }
  /* The word goes, the arrow stays (Jace, 2026-08-28). "Resolved" is centred on
     a connector column with no width to spare below the laptop, so it rendered
     clipped against the panel beside it. The arrow carries the direction and
     the panel it points at is headed "One view", so the claim survives. */
  .ov-node { display: none; }
  /* The same height as the band it covers, for the same reason the band is
     pinned: the gesture is one full screen replacing another. At 88px of
     padding around one sentence the quote was 325px tall, so it slid across the
     pinned section without ever covering it and the reveal read as a caption
     going past rather than as an arrival. */
  .pullquote { min-height: 100vh; padding: 88px 0; }
  /* 3vw put the quote at its 26px floor on a 768 window, so the band grew to a
     full viewport around type sized for a phone and the sentence read as a
     caption in an empty room. Sized off the band it is in rather than off the
     viewport width. */
  .pq-text { font-size: clamp(27px, 4.4vw, 38px); max-width: 22ch; }
  .proj-stats { grid-template-columns: 1fr 1fr; gap: 24px; }
  .proj-h, .proj-stats, .proj-note { max-width: none; }
  .proj-note { max-width: 62ch; }
  /* THE CURVE GETS ITS OWN BAND BACK.
     The trajectory is section ground, drawn full bleed and sized so it passes
     under the copy on a 1440 window. Once the copy loses its 700px cap and
     fills a narrow window, the same curve runs straight through the stat labels
     and the note. Two changes, and neither is "make it fainter until you cannot
     read it either way": the curve is anchored to the lower part of the section
     so its rise happens below the text, and the section is given the bottom
     room for that rise to be visible. */
  .proj-in { padding: 76px var(--gut) 172px; }
}
/* THE METRICS GET OUT OF THE RAMP'S WAY (Jace, 2026-08-28).
   The four stats were spanning the full window, so the curve, which enters at
   the top right and falls to the bottom left, had to cross them on the way. Cut
   to half the width and pushed left, they sit under the shallow end of the
   curve and the rise happens in the space they vacate, which is the same
   relationship they have at 1440. The curve moves too: further down, so its
   steep section clears the headline as well. */
@media (min-width: 768px) and (max-width: 1023px) {
  /* NO top offset. Cutting the metrics to half the width was the fix; pushing
     the curve down as well was belt and braces and it cost the ramp the rise
     that makes it read. Landscape has never had an offset and is the version
     Jace signed off, so portrait now matches it exactly. */
  .proj-h { max-width: 26ch; }
  .proj-stats { max-width: 56%; gap: 22px 20px; }
  .proj-stats b { font-size: 26px; }
  .proj-stats span { font-size: 12.5px; line-height: 1.45; }
  .proj-note { max-width: 52%; }
  .proj-in { padding: 84px var(--gut) 132px; }
}
@media (max-width: 599px) {
  .proj-stats { grid-template-columns: 1fr; gap: 18px; }
}

/* ---- THE METRIC RULES STOP AT THE RAMP (Jace, 2026-08-28) ----
 *
 * Below the laptop the copy fills the width, so the four dividers under the
 * metrics run straight through the curve and out into the empty page above it.
 * The hatch under the curve already ends exactly at the curve; the rules should
 * read the same way, as the ground stopping where the ramp starts rather than
 * as four lines laid over a picture.
 *
 * The border stays for LAYOUT and goes transparent, and the visible rule
 * becomes a ::before whose width is set per row. Keeping the 1px border means
 * the rows do not move by 4px when this turns on, so the laptop's geometry is
 * untouched and only the paint changes.
 *
 * --rule-w is written by site.js, which reads the crossing off .proj-line
 * itself with getPointAtLength and getScreenCTM. Not four measured percentages
 * per breakpoint: the curve's position depends on the section's height, which
 * depends on how the copy wraps, so any hard-coded number is right until
 * someone edits a metric label. Absent JS the property is unset and the rules
 * fall back to full width, which is exactly today's behaviour.
 *
 * Armed for every width below the laptop, not just the phone. At 768 and 834
 * the metrics are already capped at 56% and the curve never reaches them, so
 * this measures a crossing that is not there and leaves the rules full width.
 * That is the point: the rule is "stop where the ramp is", and where there is
 * no ramp to stop at it does nothing rather than needing a breakpoint to say
 * so. */
@media (max-width: 1239px) {
  .proj-stats li { border-top-color: transparent; position: relative; }
  .proj-stats li::before {
    content: ""; position: absolute; top: -1px; left: 0; height: 1px;
    width: var(--rule-w, 100%);
    background: var(--rule);
  }
}

/* ---- THE BUILDOUT ON A PHONE, ALL OF IT, IN ONE PLACE ----
 *
 * Every phone-width rule this section needs is here, at the END of the section,
 * and the placement is the point. The padding and the ramp's box were written
 * up in the page-wide phone block with their siblings, where `.proj-in`'s own
 * base rule and the 1023 block both sit further down the file and beat them at
 * equal specificity. FOUR overrides in this pass lost the cascade that way, and
 * the tell was always the same: the stylesheet said 56px and the browser said
 * 172px. Anything phone-specific for this section belongs in this block.
 *
 * WHAT THE NUMBERS ARE FOR. 172px of bottom padding was bought to give the
 * curve room to rise, back when the curve was uncropped and needed the full
 * width to get anywhere. Cropping the peak off the right glass replaced that,
 * so the padding was paying for it twice and the section carried 198px of empty
 * page under its last metric: the buildout appeared to keep going after it had
 * finished. What is left is the seam to the FAQ and nothing else.
 *
 * The ramp starts at 4% rather than 12% for the same reason. The offset existed
 * to keep the curve off the copy, the crop does that now, and every percent of
 * it was pushing the rise down into the space that just got cut.
 *
 * preserveAspectRatio is "none" on the svg, so the wider box renders a
 * horizontal SUBRANGE of the same curve rather than a taller copy of it, which
 * is what cropping the peak means here. */
@media (max-width: 767px) {
  .proj-note { display: none; }
  .proj-in { padding: 52px var(--gut) 40px; }
  .proj-bg { top: 4%; left: 0; right: -26%; }
}

/* THE METRIC TURNS SIDEWAYS.
   Stacked, each metric is a 40px number over three lines of description, and
   three of them are 330px of a 844px phone before the headline has been
   counted. As a row, the number takes a fixed left column and the description
   takes the rest, so each metric is two lines instead of four and the set fits
   inside the pin with room. The rules between them do the separating that 34px
   of gap used to, which also makes the three read as one table of evidence
   rather than three loose blocks. */
@media (max-width: 1023px) {
  .whynow-list { grid-template-columns: 1fr; gap: 0; margin: 0 0 26px; }
  .whynow-list li {
    display: grid; grid-template-columns: 118px minmax(0, 1fr);
    column-gap: 20px; align-items: baseline;
    padding: 14px 0; border-top: 1px solid var(--green-line);
  }
  .whynow-list li:last-child { border-bottom: 1px solid var(--green-line); }
  .whynow-list b { font-size: 27px; margin-bottom: 0; }
  .whynow-list span { font-size: 13.5px; line-height: 1.5; max-width: none; }
  /* The rules between the rows already close the list, so the tail's own would
     be two lines with nothing between them. */
  .whynow-tail { padding-top: 4px; border-top: 0; }
}
@media (max-width: 767px) {
  .whynow { padding: 76px 0 64px; }
  .whynow-h { font-size: clamp(25px, 7vw, 31px); margin: 14px 0 26px; max-width: 19ch; }
  .whynow-list { margin: 0 0 24px; }
  .whynow-list li { grid-template-columns: 108px minmax(0, 1fr); column-gap: 14px; padding: 13px 0; }
  .whynow-list b { font-size: 24px; }
  .whynow-list span { font-size: 12.5px; line-height: 1.46; }
  .whynow-tail { font-size: 16px; }
  .pullquote { padding: 76px 0; }
}

/* ============ TOUR : the two screens in front of /demo/ ============
   A landing, not a document. The section owns the viewport under the nav so the
   step you are on is the whole screen and the footer never competes with it.

   The two steps are stacked in one grid cell rather than shown and hidden in
   flow, so the stage is as tall as the taller step. They are TOP aligned, not
   centred: centring unequal steps inside a shared cell moved the content 44 to
   47px on every advance, which is the exact jump the stack exists to prevent.

   Without JS the stage is plain flow, both steps read top to bottom, the step
   controls are hidden, and step two's "Open the live demo" is still there. The
   front door does not depend on a script. */
.tour {
  background: var(--dark); color: var(--white);
  position: relative; overflow: clip;
  /* The nav is sticky and opaque, so its height is the only part of the
     viewport this section never gets to use. */
  min-height: calc(100svh - 64px);
  display: flex; align-items: center;
  padding: 52px 0 60px;
}
/* Same wash as .page-head, so the interior pages share one ground. */
.tour::before {
  content: ""; position: absolute; inset: -30% -10% auto -10%; height: 620px;
  background: radial-gradient(ellipse 46% 42% at 50% 26%, rgba(122,173,106,0.15), transparent 68%);
  pointer-events: none;
}
.tour-in { position: relative; z-index: 1; max-width: 860px; }

/* ---- the rail: where you are, and the quiet way past ---- */
.tour-rail {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; flex-wrap: wrap;
  padding-bottom: 16px; margin-bottom: 30px;
  border-bottom: 1px solid var(--green-line);
}
.tour-dots { display: flex; gap: 8px; list-style: none; margin: 0; padding: 0; }
/* No step numbers. On a sequence of two, "01" next to a label next to a chip is
   the same fact said three ways, and "Back" already says which one you are on. */
.tour-dots button {
  background: none; border: 1px solid transparent; border-radius: var(--r-sm);
  padding: 7px 12px; cursor: pointer; color: var(--green-muted);
  font-family: var(--sans); font-size: 13px; font-weight: 600;
  transition: color 0.22s var(--ease-out-cubic), border-color 0.22s var(--ease-out-cubic),
              background 0.22s var(--ease-out-cubic);
}
.tour-dots button:hover { color: var(--white); }
.tour-dots button[aria-current="step"] {
  color: var(--white); border-color: #2f5230; background: rgba(122,173,106,0.10);
}
/* The rail's "Skip to the demo" link was removed when step one became a gate:
   an escape hatch past an email capture is not an escape hatch, it is the
   whole capture. The rail is the step chips alone now. */
/* Nothing to advance without a script, so the controls that advance it go. */
.tour:not([data-js]) .tour-dots,
.tour:not([data-js]) [data-go] { display: none; }

/* ---- the stage ---- */
.tour[data-js] .tour-stage { display: grid; }
.tour[data-js] .tour-step { grid-area: 1 / 1; align-self: start; }
/* THE STACK IS A DESKTOP LUXURY.
   Both steps share one grid cell so the stage is as tall as the taller of them
   and advancing never moves the content. That costs the difference between the
   two steps as dead space under the shorter one, which is 44px on a 1440
   window and 638px on a phone: at that size the page is a button followed by
   two thirds of a screen of nothing. Below the tablet line the inactive step
   leaves the flow and the stage takes the height of what is actually on it. The
   jump the stack was preventing now happens on a tap the reader made, which is
   the moment a layout change is expected rather than surprising. */
@media (max-width: 1239px) {
  .tour[data-js] .tour-stage { position: relative; }
  /* Out of FLOW, not display:none. The inactive step is already opacity 0 and
     visibility hidden, so absolute positioning takes its height out of the
     stage without costing the blur-and-travel exit that DESIGN-SYSTEM 5 asks
     for. display:none would have killed the exit, and :not(.leaving) does not
     work at all: .leaving is set on whichever step is not current and is never
     taken off, so it is a synonym for :not(.on) rather than a transient state. */
  .tour[data-js] .tour-step:not(.on) { position: absolute; left: 0; right: 0; top: 0; }
}
/* Blur out, blur in, per DESIGN-SYSTEM 5: fade reads as removal, blur reads as
   distance. Travel is on X and SIGNED by --dir, so Back is visibly the reverse
   of Next rather than pixel-identical to it (motion principle 2). The step
   being left behind exits opposite the one arriving, which is what .leaving is
   for: without it both share one off-state and the new step slides in from the
   side the old one just left through.
   `visibility` is stepped, not eased: it flips at once on the way IN, so a
   .focus() lands, and waits out the fade on the way OUT so the exit is seen. */
.tour[data-js] .tour-step {
  opacity: 0; visibility: hidden; filter: blur(8px);
  transform: translateX(calc(var(--dir, 1) * 18px));
  transition: opacity 0.36s var(--ease-out-cubic), filter 0.42s var(--ease-out-cubic),
              transform 0.46s var(--ease-out-cubic), visibility 0s linear 0.46s;
}
.tour[data-js] .tour-step.leaving { transform: translateX(calc(var(--dir, 1) * -18px)); }
.tour[data-js] .tour-step.on {
  opacity: 1; visibility: visible; filter: none; transform: none;
  transition: opacity 0.36s var(--ease-out-cubic), filter 0.42s var(--ease-out-cubic),
              transform 0.46s var(--ease-out-cubic), visibility 0s linear 0s;
}
/* Both steps read in flow with no script, and the second needs a rule under it
   so it does not run into the first. */
.tour:not([data-js]) .tour-step + .tour-step {
  margin-top: 40px; padding-top: 36px; border-top: 1px solid rgba(122,173,106,0.22);
}

/* Type comes off the scale rather than near it: --fs-issue is the documented
   size for a subpage headline and --fs-body for its deck. */
.tour-h {
  font-family: var(--serif); font-weight: 400;
  font-size: var(--fs-issue); line-height: 1.1; letter-spacing: -0.028em;
  margin-bottom: 18px;
}
.tour-deck { font-size: var(--fs-body); line-height: 1.65; color: #b4c9ad; max-width: 62ch; margin-bottom: 26px; }
.tour-lead { font-size: 13px; color: var(--green-muted); margin-bottom: 6px; }

/* ---- step 2: the five surfaces ---- */
.tour-surfaces { list-style: none; margin: 0 0 22px; padding: 0; }
/* --green-line is 1.37:1 and DESIGN-SYSTEM 9.7 already rejected it for table
   hairlines. These rules carry the row structure, so they need to be seen. */
.tour-surfaces li {
  display: grid; grid-template-columns: 32px 190px 1fr; align-items: baseline; gap: 14px;
  padding: 10px 0; border-top: 1px solid rgba(122,173,106,0.22); font-size: 14px;
}
.tour-surfaces .tnum { font-family: var(--mono); font-size: var(--fs-meta); color: var(--green); letter-spacing: 0.08em; }
.tour-surfaces b { font-weight: 600; }
.tour-surfaces .ts-d { color: var(--green-muted); line-height: 1.5; }

.tour-fine { font-size: 12.5px; line-height: 1.65; color: var(--green-muted); max-width: 66ch; margin-bottom: 26px; }
/* The app's own stamp, not the marketing page's. Provenance spends no colour
   (DESIGN-SYSTEM 7.4): green already means "open to you now" three rows down,
   and this page's job is to teach the object the product actually shows. */
.tour-fine .stamp {
  font-family: var(--mono); font-size: var(--fs-meta); font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--green-muted);
  white-space: nowrap;
}
.tour-cta { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
/* .btn is written for anchors; the step controls are real buttons, which arrive
   with the UA's own font and buttonface background. */
.tour-cta .btn { font-family: var(--sans); cursor: pointer; background: none; }
.tour-cta .btn-primary { background: var(--green); }
.tour-cta .btn-primary:hover { background: var(--green-hi); }
.tour-cta .btn span { display: inline-block; transition: transform 0.24s var(--ease-out-cubic); }
.tour-cta .btn:hover span { transform: translateX(4px); }

/* The UA focus ring is rgb(0,95,204), which measures 2.87:1 on --dark and fails
   WCAG 2.4.11. --green is 6.55:1 on the same ground. */
.tour[data-js] .tour-step:focus { outline: none; }

/* ---- TOUR GATE (step one's email capture) ----
   Field and button are .wl-form, the same object /beta and the hero use, so
   there is one form design on the site and not three. Only what is specific to
   sitting inside a left-aligned dark step is declared here: the width the
   centred .form-shell would otherwise impose, and the locked states of the
   three exits. */
.tour-gate { max-width: 540px; margin: 0 0 26px; }
.tour-gate-h { font-size: 13px; color: var(--green-muted); margin: 0 0 10px; }
.tour-gate-h.gone { display: none; }
.tour-gate .form-fine { margin-top: 11px; }
.tour-gate-ok {
  font-size: 14px; line-height: 1.6; color: var(--green); margin: 0;
}
.tour-gate-ok:empty { display: none; }

/* Locked, not hidden. The reader should see what they are being asked to pay
   for the next screen, not find a control missing and assume the page broke. */
.tour-cta .btn:disabled { opacity: 0.42; cursor: not-allowed; transform: none; }
.tour-cta .btn:disabled:hover span { transform: none; }
.tour-dots button:disabled { opacity: 0.38; cursor: not-allowed; }
.tour-dots button:disabled:hover { color: var(--green-muted); }
.tour-dots button:focus-visible,
.tour-cta .btn:focus-visible {
  outline: 2px solid var(--green); outline-offset: 3px; border-radius: var(--r-sm);
}

/* The forward button has to be ON SCREEN. Measured before this block, "Next"
   ended at y=886 in an 844px viewport and step two's CTA at y=821 in a 768px
   one, so on a phone the only visible forward control was the skip link. Short
   viewports lose the vertical centring and the section's own minimum. */
/* iPad portrait falls through BOTH escape hatches. 768x1024 is neither short
   (max-height: 820) nor narrow (max-width: 760), so .tour held its
   calc(100svh - 64px) minimum and forced a 960px panel around 570px of content:
   450px of empty dark between the forward button and the footer. The minimum
   exists so the step you are on owns the screen, and it only earns that on a
   window wide enough for the two-column stage. */
@media (max-width: 1023px) {
  .tour { min-height: 0; align-items: flex-start; padding: 48px 0 56px; }
}
@media (max-height: 820px) {
  .tour { align-items: flex-start; min-height: 0; padding: 34px 0 40px; }
  .tour-rail { padding-bottom: 13px; margin-bottom: 22px; }
  .tour-h { margin-bottom: 14px; }
  .tour-deck { margin-bottom: 20px; }
  .tour-surfaces li { padding: 8px 0; }
  .tour-surfaces { margin-bottom: 16px; }
  .tour-fine { margin-bottom: 14px; line-height: 1.55; }
}
@media (max-width: 760px) {
  .tour { min-height: 0; padding: 36px 0 48px; }
  .tour-deck { font-size: 15.5px; margin-bottom: 22px; }
  .tour-surfaces li { grid-template-columns: 28px 1fr; row-gap: 3px; padding: 9px 0; }
  .tour-surfaces .ts-d { grid-column: 2; }
  .tour-cta .btn { width: 100%; text-align: center; }
  .tour-cta {
    position: sticky; bottom: 0; z-index: 2;
    flex-wrap: nowrap;
    margin: 0 calc(var(--pad-x-mobile) * -1) calc(var(--pad-x-mobile) * -1);
    padding: 12px var(--pad-x-mobile) calc(12px + env(safe-area-inset-bottom));
    background: var(--dark-deep);
    border-top: 1px solid rgba(122,173,106,0.22);
  }
  .tour-cta .btn-primary { flex: 1; width: auto; }
  .tour-cta .btn-ghost { width: auto; flex: 0 0 auto; }
  /* Step one already fits, so a bar pinned under a button that is on screen
     anyway is chrome for its own sake. */
  .tour-step[data-s="1"] .tour-cta {
    position: static; margin: 0; padding: 0; background: none; border-top: 0;
  }
  .tour-step[data-s="1"] .tour-cta .btn { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .tour[data-js] .tour-step { transition-duration: 0.01ms; filter: none; transform: none; }
}

/* ============ 404 ============
   The only page whose band is mostly empty, so it is the only one that gets a
   field of atom marks behind the copy. Everything here is scoped to .nf-* or
   .page-head-404 and nothing above this comment changes.

   TRANSFORM IS SPLIT ACROSS TWO ELEMENTS ON PURPOSE. .nf-orb carries the
   cursor repulsion, written every frame from JS; .nf-atom carries the idle
   drift, a CSS keyframe. One element cannot hold both, because the rAF loop
   would overwrite the keyframe's matrix on the next tick and the drift would
   stop dead the first time a pointer entered the band. Nesting lets the two
   compose the way a parent and child always do. */
/* The band IS the page here. 65px is the sticky nav, which is in flow and eats
   that much of the viewport, so subtracting it is what makes the atom field end
   exactly at the fold instead of pushing a sliver of footer into view. svh not
   dvh: dvh reflows the whole field every time a mobile browser hides its chrome,
   and svh is the stable one of the two. */
.page-head-404 {
  /* 63px is the sticky nav MEASURED, border included, and it is 63 at every
     breakpoint. It was 65 by guess, which left a 2px sliver of footer peeking
     under the fold. */
  min-height: calc(100vh - 63px);
  min-height: calc(100svh - 63px);
  display: flex; align-items: center; text-align: center;
}
.page-head-404 > .wrap { width: 100%; max-width: 760px; }
.page-head-404 .page-deck { margin-left: auto; margin-right: auto; }

.nf-field {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;   /* the field never eats a click meant for the copy */
  overflow: hidden;
}
.nf-orb { position: absolute; will-change: transform; }
.nf-atom {
  display: block; width: 100%; height: 100%;
  color: var(--green);
  /* Green is a line here, never a fill (DESIGN-SYSTEM 1, accent discipline).
     These are strokes at 10 to 22 percent over --dark, which is the same
     weight as the page rails, so the field reads as chrome and never competes
     with the white serif headline sitting on top of it. */
  opacity: var(--nf-o, 0.16);
  animation: nf-drift var(--nf-dur, 20s) var(--ease-in-out-cubic) var(--nf-delay, 0s) infinite;
}
@keyframes nf-drift {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%      { transform: translate3d(var(--nf-dx, 6px), var(--nf-dy, -10px), 0) rotate(var(--nf-rot, 8deg)); }
}

/* Entrance: blur out to clear, not opacity in (DESIGN-SYSTEM 5). The stagger is
   set per orb inline so the field arrives as one gesture sweeping across the
   band rather than fourteen independent pops. */
.nf-orb { opacity: 0; filter: blur(10px); transform: translate3d(0, 22px, 0) scale(0.82); }
.nf-field.in .nf-orb {
  opacity: 1; filter: blur(0px); transform: translate3d(0, 0, 0) scale(1);
  transition: opacity 0.7s var(--ease-out-cubic), filter 0.7s var(--ease-out-cubic),
              transform 0.9s var(--ease-out-back);
  transition-delay: var(--nf-in, 0s);
}
/* Once the entrance has played, JS owns transform. Dropping the transition
   here is what makes the repulsion feel like an object being shoved rather
   than a UI element easing: the spring in the loop IS the easing, and a CSS
   transition on top of it would add a second, slower one and mush both. */
.nf-field.live .nf-orb { transition: opacity 0.7s var(--ease-out-cubic), filter 0.7s var(--ease-out-cubic); }

.nf-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 34px; }
.nf-fine { margin-top: 22px; font-size: 13.5px; color: var(--green-muted); }
.nf-fine a { color: var(--green); text-decoration-color: rgba(122,173,106,0.45); text-underline-offset: 3px; }
.nf-fine a:hover { color: var(--green-hi); }

@media (max-width: 720px) {
  /* The band stays full height here too. An earlier draft set min-height:0 for
     a short hero and that silently cancelled the whole effect on phones. */
  .nf-actions { flex-direction: column; }
  .nf-actions .btn { width: 100%; justify-content: center; }
}

/* The whole effect is decoration, so it is the first thing to go. No entrance,
   no drift, and the JS loop checks the same query and never starts. */
@media (prefers-reduced-motion: reduce) {
  .nf-orb { opacity: 1; filter: none; transform: none; transition: none; }
  .nf-atom { animation: none; }
}
