/* ════════════════════════════════════════════════════════════
   AXE — technical / broadcast system
   Pure black · pure white · 12-column grid · everything uppercase
   ════════════════════════════════════════════════════════════ */

:root{
  --black:  #000;
  /* Ground for everything below the hero. Slightly lifted off pure black and
     a touch cool, so the sheet reads as its own surface arriving over the
     hero rather than more of the same void — and so the curve has something
     to be a curve *against*. */
  --black-2:#0b0c0f;
  --white:  #fff;
  --dim:    rgba(255,255,255,.52);
  --faint:  rgba(255,255,255,.30);
  /* Body copy sits brighter than the mono labels. percyjackson.framer.media
     runs its paragraphs at #ABABAB (~67% white); .52 was too dim to read
     several sentences of. */
  --text:   rgba(255,255,255,.68);
  --line:   rgba(255,255,255,.20);
  --hair:   rgba(255,255,255,.10);

  --font-display:'Archivo', 'Arial Black', Impact, sans-serif;
  /* Same variable family as the display face, at normal width instead of
     expanded. Both reference sites set body copy in a grotesk, not a
     monospace — mono is kept for the small uppercase labels only. */
  --font-text:   'Archivo', -apple-system, 'Segoe UI', sans-serif;
  --font-mono:   'Space Mono', ui-monospace, 'SF Mono', Consolas, monospace;

  --margin: clamp(14px, 1.4vw, 18px);
  --gap:    clamp(6px, .7vw, 9px);

  --fs-mono: clamp(10px, .78vw, 11px);
  --fs-txt:  clamp(11px, .92vw, 13px);

  --ease:  cubic-bezier(.22, 1, .36, 1);
  --ease2: cubic-bezier(.65, 0, .35, 1);

  /* Hover sweeps (CTA buttons, the Discord server rows). Entering is slow
     and deliberate; leaving is quicker on purpose. A single duration for
     both meant that flicking the pointer on and off left the fill crawling
     back long after the cursor had gone — the movement stopped matching
     the hand. Every element in one of these hovers shares this pair so no
     part of the inversion can run ahead of the rest. */
  /* The gap between these two is kept moderate on purpose. When a sweep is
     interrupted the browser keeps the current position but adopts the new
     duration, so a large in/out difference shows up as an abrupt change of
     speed mid-flight — which is its own kind of wrong under fast toggling. */
  --hover-in:  .8s;
  --hover-out: .48s;
  /* Hover intent. Without it the sweep fires on the slightest brush, so
     dragging the pointer down the server list set several rows animating at
     once and a flick on-and-off started a full sweep it then had to undo.
     The delay applies to ENTERING only — leaving is immediate, so nothing
     ever feels sticky. Short enough not to read as lag. */
  --hover-delay: .09s;
  /* Picked against two failure modes, both measured:

     - --ease (expo-out) starts at velocity 4.45 and is 67% done by the 20%
       mark: the fill lunges across, then crawls. Reads as fast and jerky.
     - A symmetric in-out starts at velocity 0.02 — effectively stationary.
       Fine uninterrupted, but a transition that is cut short and restarted
       begins its easing again from zero, so every re-entry hesitates before
       moving. That is what felt wrong when flicking the pointer on and off.

     This sits between them: velocity 0.42 at the start (moves immediately,
     without lunging) and 20% done at the 20% mark, so it is close to even
     through the middle and still settles softly at the end. */
  --hover-ease: cubic-bezier(.4, .15, .2, 1);
  /* For small targets. --hover-ease starts at velocity 0.42 and is only 6%
     along at the 10% mark — right for a big panel you settle on, but on a
     tag it reads as a delay before anything happens. This starts at 2.56
     (27% along by the 10% mark) so colour appears under the cursor at once,
     then decelerates into the same soft landing (99% by 85%). */
  --tap-ease: cubic-bezier(.2, .5, .25, 1);

  /* ── BACKGROUND IMAGE ─────────────────────────────────────
     Drop the client's image at  assets/bg.jpg  → applied
     automatically. Until then the wash below shows.          */
  --bg-image: url('../assets/bg.jpg');
  --bg-opacity: .30;
}

*,*::before,*::after{ box-sizing:border-box; margin:0; padding:0; }

html{ scroll-behavior:smooth; -webkit-text-size-adjust:100%; }

body{
  background:var(--black);
  color:var(--white);
  font-family:var(--font-mono);
  font-size:var(--fs-mono);
  line-height:1.5;
  letter-spacing:.02em;
  text-transform:uppercase;          /* system-wide, as per the reference */
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
body.is-locked{ overflow:hidden; }

::selection{ background:var(--white); color:var(--black); }

a{ color:inherit; text-decoration:none; }
ul{ list-style:none; }
img{ max-width:100%; display:block; }
:focus-visible{ outline:1px solid var(--white); outline-offset:4px; }

/* ── Grid system ────────────────────────────────────────────── */
.grid{ display:grid; grid-template-columns:repeat(12,1fr); gap:var(--gap); }

.span-1{grid-column:span 1}   .span-2{grid-column:span 2}
.span-3{grid-column:span 3}   .span-4{grid-column:span 4}
.span-5{grid-column:span 5}   .span-6{grid-column:span 6}
.span-7{grid-column:span 7}   .span-8{grid-column:span 8}
.span-12{grid-column:span 12}
/* `grid-column: span N` expands to `start: span N; end: auto`, so setting
   grid-column-start on top of it left end at auto — the item collapsed to a
   single track. The offset variants therefore state both edges. */
.span-4.span-start-9{ grid-column:9 / span 4; }
.span-5.span-start-9{ grid-column:9 / span 4; }
/* Body copy column: one left edge for every paragraph in a section. */
.span-6.span-start-7{ grid-column:7 / span 6; }
.span-5.span-start-8{ grid-column:8 / span 5; }

.tc{ text-align:center; }
.tr{ text-align:right; }
.dim{ color:var(--dim); }

/* ════════════════════════════════════════════════════════════
   FIXED OVERLAYS
   ════════════════════════════════════════════════════════════ */
.bg{ position:fixed; inset:0; z-index:0; pointer-events:none; }
.bg__image{
  position:absolute; inset:-5%;
  background:var(--bg-image) center/cover no-repeat;
  opacity:var(--bg-opacity);
  filter:grayscale(1) contrast(1.15) brightness(.6);
  will-change:transform;
}
.bg__wash{
  position:absolute; inset:0;
  background:
    radial-gradient(120% 90% at 50% 0%,  rgba(255,255,255,.05) 0%, transparent 55%),
    linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.88) 60%, #000 100%);
}

/* Background guides — 5 evenly spaced vertical rules.
   Drawn with a repeating gradient so it needs no extra DOM.
   The content width is split into 4 equal steps, which puts a line on
   each page margin and three between them: 5 lines in total. */
.colguide{
  position:fixed; inset:0; z-index:1;
  pointer-events:none;
  /* was .55 — the vertical rules crossed behind paragraphs. Still reads as
     structure, no longer as noise sitting on the words. */
  opacity:.3;
  --step:calc((100% - var(--margin) * 2) / 4);
  background-image:linear-gradient(to right, var(--hair) 1px, transparent 1px);
  background-size:var(--step) 100%;
  background-position:var(--margin) 0;
  background-repeat:repeat-x;
}
@media (max-width:860px){ .colguide{ display:none; } }

/* CRT scanline. Runs edge to edge — the header no longer has a glass panel
   to keep clear of, and carving a strip out of the texture behind a
   transparent bar would only draw an untextured band across the top. */
.scanline{
  position:fixed; inset:0; z-index:940; pointer-events:none;
  background:repeating-linear-gradient(
    180deg, rgba(0,0,0,0) 0px, rgba(0,0,0,0) 2px,
    rgba(0,0,0,.34) 3px, rgba(0,0,0,.34) 4px);
  /* was .5 — dark stripes every 4px cut straight through body copy.
     Kept as atmosphere, no longer competing with the text. */
  opacity:.26;
  animation:drift 9s linear infinite;
}
@keyframes drift{ to{ background-position:0 240px; } }

/* Grain. The -150%-inset element drifts past its own edges so the
   animation never reveals a hard boundary; the mask clips that oversized
   box back to the viewport, which clipping .grain itself would not do —
   that would just re-measure against the -150% box. */
.grain-mask{
  position:fixed; inset:0; z-index:945; pointer-events:none;
  overflow:hidden;
}

/* ── Scroll indicator ───────────────────────────────────────
   The native scrollbar is hidden and this replaces it: a hairline down the
   right edge with a white segment growing from the top.

   The handle's length and position come from main.js as --thumb-h and
   --thumb-y. It sits above the film overlays but below the header, and can
   be dragged — see progressBar() in main.js. */
.progress{
  position:fixed; top:0; right:0; bottom:0;
  /* The hit area is far wider than the line. A 3px target is close to
     ungrabbable with a mouse; the extra width is transparent, so what you
     see is still a hairline. */
  width:16px; z-index:930;
  cursor:pointer;
  touch-action:none;            /* the drag handler owns the gesture */
}
.progress__track,
.progress__bar{
  position:absolute; right:0; top:0;
  width:3px;
  transition:width .25s var(--tap-ease);
}
.progress__track{ bottom:0; background:var(--hair); }
/* A handle showing where you are, not a bar filling from the top. Its
   length is the share of the page currently on screen — the same thing a
   native scrollbar thumb tells you — and it is moved with a transform so
   scrolling never triggers layout. Both values come from main.js. */
.progress__bar{
  height:var(--thumb-h, 60px);
  background:var(--white);
  transform:translate3d(0, var(--thumb-y, 0px), 0);
}
/* Thickens under the pointer so it reads as grabbable, and stays thick
   while dragging even if the cursor wanders off the strip. */
.progress:hover .progress__track,
.progress:hover .progress__bar,
.progress.is-dragging .progress__track,
.progress.is-dragging .progress__bar{ width:6px; }
.progress.is-dragging{ cursor:grabbing; }

/* Touch: readout only. The 16px hit strip runs the full height of the right
   edge with touch-action:none, so on a phone a swipe that started anywhere
   near that edge was captured as a drag and the page would not move. There
   is no cursor to widen for either, so the line stays hairline. */
@media (hover: none){
  .progress{ width:3px; pointer-events:none; touch-action:auto; }
  .progress:hover .progress__track,
  .progress:hover .progress__bar{ width:3px; }
}

/* Hidden on both the element and the viewport — the two engines read
   different properties, and `overflow:hidden` here would stop the page
   scrolling at all. */
html{ scrollbar-width:none; -ms-overflow-style:none; }
html::-webkit-scrollbar, body::-webkit-scrollbar{ width:0; height:0; display:none; }

/* Past the hero the page goes flat. Both reference sites set their content
   on a plain ground — no scanlines, no column rules behind the words — and
   the film texture was the main thing making the information hard to read.
   The hero keeps it; everything below gets a calm surface. */
.scanline, .grain-mask, .colguide{ transition:opacity .5s var(--ease); }
:root.is-past-hero .scanline,
:root.is-past-hero .grain-mask,
:root.is-past-hero .colguide{ opacity:0; }
.grain{
  position:absolute; inset:-150%; opacity:.055;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain 5s steps(5) infinite;
}
@keyframes grain{
  0%,100%{transform:translate(0,0)}   20%{transform:translate(-7%,3%)}
  40%{transform:translate(4%,-6%)}    60%{transform:translate(-4%,7%)}
  80%{transform:translate(6%,2%)}
}

/* ════════════════════════════════════════════════════════════
   LOADER
   ════════════════════════════════════════════════════════════ */
.loader{
  position:fixed; inset:0; z-index:2000; background:var(--black);
  display:flex; flex-direction:column; justify-content:flex-end;
  padding:var(--margin);
  transition:opacity .7s var(--ease2), visibility .7s;
}
.loader.is-done{ opacity:0; visibility:hidden; }
.loader__grid{ margin-bottom:10px; }
.loader__tag{ color:var(--dim); }
#loaderPct{ color:var(--white); }
.loader__bar{ height:2px; background:rgba(255,255,255,.14); overflow:hidden; }
.loader__bar i{ display:block; height:100%; width:0; background:var(--white); }

/* ════════════════════════════════════════════════════════════
   HEADER
   ════════════════════════════════════════════════════════════ */
.header{
  position:fixed; top:0; left:0; right:0; z-index:800;
  display:grid;
  grid-template-columns:1fr auto 1fr;   /* explicit columns keep the nav */
  align-items:center; gap:20px;         /* dead-centred on the page      */
  padding:var(--margin);
}

.hd-logo{ grid-column:1; justify-self:start; }
.hd-logo__mark{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900; font-size:15px; line-height:1;
  letter-spacing:-.03em;
}

.hd-nav{
  grid-column:2; justify-self:center;
  display:flex; gap:clamp(12px,1.8vw,30px);
}
.hd-nav a{ position:relative; padding-bottom:2px; }
/* Per-character hover shuffle: each letter is its own hit target, and
   its width is pinned by JS so swapped glyphs cannot shift the word. */
.sc-char{ display:inline-block; text-align:center; }

.hd-nav a::after{
  content:''; position:absolute; left:0; right:100%; bottom:0;
  height:1px; background:var(--white); transition:right .4s var(--ease);
}
.hd-nav a:hover::after,.hd-nav a.is-current::after{ right:0; }
.hd-nav a.is-current{ color:var(--white); }

.burger{
  grid-column:3; justify-self:end;
  display:none; align-items:center; gap:8px;
  background:none; border:0; color:inherit; cursor:pointer;
  font:inherit; text-transform:uppercase; letter-spacing:.02em;
}
.burger i{ display:block; width:18px; height:1px; background:var(--white); transition:transform .4s var(--ease); }
.burger.is-open i:first-of-type{ transform:translateY(1px) rotate(45deg); }
.burger.is-open i:last-of-type{ transform:translateY(0) rotate(-45deg); }

@media (max-width:860px){
  .hd-nav{ display:none; }
  .burger{ display:flex; }
}

/* ════════════════════════════════════════════════════════════
   MENU
   ════════════════════════════════════════════════════════════ */
.menu{
  position:fixed; inset:0; z-index:790; background:var(--black);
  display:flex; flex-direction:column; justify-content:center;
  padding:calc(var(--margin) * 5) var(--margin) var(--margin);
  clip-path:inset(0 0 100% 0);
  transition:clip-path .7s var(--ease);
}
.menu.is-open{ clip-path:inset(0 0 0 0); }
.menu__list a{
  display:flex; align-items:baseline; gap:14px;
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  font-size:clamp(28px,9vw,64px); line-height:1.2;
  letter-spacing:-.045em;
  border-bottom:1px solid var(--hair);
  padding:8px 0;
  opacity:0; transform:translateY(18px);
  transition:opacity .5s var(--ease), transform .5s var(--ease);
}
.menu.is-open .menu__list a{ opacity:1; transform:none; }
.menu.is-open .menu__list a:nth-child(1){transition-delay:.10s}
.menu.is-open .menu__list a:nth-child(2){transition-delay:.16s}
.menu.is-open .menu__list a:nth-child(3){transition-delay:.22s}
.menu.is-open .menu__list a:nth-child(4){transition-delay:.28s}
.menu__list em{ font-family:var(--font-mono); font-style:normal; font-size:11px; font-weight:400; color:var(--faint); letter-spacing:.02em; }
.menu__foot{ margin-top:36px; color:var(--dim); }
.menu__foot a:hover{ color:var(--white); }

/* ════════════════════════════════════════════════════════════
   HERO
   ════════════════════════════════════════════════════════════ */
main{ position:relative; z-index:2; }

/* ── Pinned hero ─────────────────────────────────────────────
   The hero parks at the top and stays there; .stack scrolls up
   over it. Pure sticky, so the browser drives it — no scroll
   listener, and it stays smooth on a phone. */
.hero{
  position:sticky; top:0;
  z-index:0;                    /* .stack sits above and covers it */
  min-height:100svh;
  display:flex; flex-direction:column;
  /* Symmetric on purpose. The header clearance used to live here as a 5×
     top padding, which pushed the CONTENT BOX off centre — so anything
     centred inside it landed low by half that difference. The clearance is
     now carried by .hero__top, which is the thing that actually needs to
     sit clear of the header. */
  padding:var(--margin);
  container-type:inline-size;   /* cqw = content width, scrollbar excluded */
  /* the WarpText canvas is deliberately wider than its text box so the
     warped edges are not cut; clip that bleed here so it cannot widen
     the document. `clip` (not `hidden`) avoids making a scroll container. */
  overflow:clip;
  /* Flat black with a starfield over it. The broad atmospheric glow that
     used to rise from below the fold is gone — the hero is plain black
     now, and the stars are the only thing on it. */
  background-color:#000;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='800'%3E%3Cg fill='%23fff'%3E%3Ccircle cx='38' cy='64' r='.9' opacity='.55'/%3E%3Ccircle cx='142' cy='198' r='.6' opacity='.35'/%3E%3Ccircle cx='96' cy='412' r='1' opacity='.5'/%3E%3Ccircle cx='233' cy='96' r='.7' opacity='.4'/%3E%3Ccircle cx='318' cy='274' r='.5' opacity='.3'/%3E%3Ccircle cx='404' cy='140' r='1.1' opacity='.6'/%3E%3Ccircle cx='476' cy='352' r='.6' opacity='.35'/%3E%3Ccircle cx='560' cy='72' r='.8' opacity='.45'/%3E%3Ccircle cx='642' cy='232' r='.5' opacity='.3'/%3E%3Ccircle cx='716' cy='118' r='1' opacity='.5'/%3E%3Ccircle cx='806' cy='308' r='.7' opacity='.4'/%3E%3Ccircle cx='884' cy='176' r='.6' opacity='.32'/%3E%3Ccircle cx='962' cy='396' r='.9' opacity='.5'/%3E%3Ccircle cx='1044' cy='88' r='.5' opacity='.3'/%3E%3Ccircle cx='1128' cy='248' r='.8' opacity='.45'/%3E%3Ccircle cx='66' cy='286' r='.5' opacity='.28'/%3E%3Ccircle cx='188' cy='438' r='.8' opacity='.42'/%3E%3Ccircle cx='262' cy='524' r='.6' opacity='.3'/%3E%3Ccircle cx='352' cy='466' r='.5' opacity='.26'/%3E%3Ccircle cx='438' cy='596' r='.9' opacity='.4'/%3E%3Ccircle cx='524' cy='488' r='.6' opacity='.3'/%3E%3Ccircle cx='612' cy='564' r='.5' opacity='.25'/%3E%3Ccircle cx='698' cy='452' r='.8' opacity='.38'/%3E%3Ccircle cx='784' cy='620' r='.6' opacity='.28'/%3E%3Ccircle cx='866' cy='498' r='.5' opacity='.24'/%3E%3Ccircle cx='948' cy='578' r='.7' opacity='.32'/%3E%3Ccircle cx='1032' cy='470' r='.5' opacity='.26'/%3E%3Ccircle cx='1116' cy='612' r='.6' opacity='.3'/%3E%3Ccircle cx='170' cy='698' r='.5' opacity='.22'/%3E%3Ccircle cx='396' cy='726' r='.6' opacity='.24'/%3E%3Ccircle cx='648' cy='688' r='.5' opacity='.2'/%3E%3Ccircle cx='892' cy='742' r='.6' opacity='.22'/%3E%3Ccircle cx='1156' cy='356' r='.7' opacity='.34'/%3E%3Ccircle cx='24' cy='512' r='.6' opacity='.3'/%3E%3Ccircle cx='300' cy='196' r='.5' opacity='.28'/%3E%3Ccircle cx='740' cy='36' r='.6' opacity='.32'/%3E%3Ccircle cx='998' cy='224' r='.5' opacity='.26'/%3E%3Ccircle cx='510' cy='268' r='.6' opacity='.3'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-size:cover;
  background-position:center;
}

/* Opaque sheet that rides up over the pinned hero. Without a solid
   background the hero would show straight through it.

   The top edge is concave: an ellipse is cut out of the sheet's
   top-centre, so the edge dips in the middle and rises to full height at
   both sides. border-radius only rounds corners outward, which produced
   the opposite (domed) curve — a mask is the only way to take material
   away. The ellipse is 50% of the width wide, so it meets y=0 exactly at
   both edges and the curve resolves to nothing there. */
.stack{
  /* ── Curve geometry ────────────────────────────────────────
     --sag is the visible depth: how far the centre of the curve hangs
     below where it meets the screen edges. It also sets how long the
     rise reads for, since the centre crosses a given line well before
     the edges do.

     The ellipse is deliberately WIDER than the screen (75% radius, so it
     spans -25%..125%). At exactly 50% its horizontal extreme landed on
     the screen edges, where an ellipse's tangent is vertical — the curve
     hooked straight up in its last few pixels and the ends looked
     snapped off. Using only the gentle flank means the curve simply
     carries on past the edges and has no visible ends at all.

     For a 75%-wide ellipse the edge sits at 0.5/0.75 = 0.667 of the
     horizontal radius, so its boundary there is sqrt(1-0.667²) = 0.745
     of the vertical radius. Hence ry = sag / (1 - 0.745) and the centre
     is lifted by 0.745·ry so the curve still meets y=0 at the edges. */
  --sag-max:clamp(190px, 46vh, 560px);
  /* --arc-t runs 0→1 as the sheet climbs (set by main.js). The curve is
     deepest when the sheet first appears and straightens as it rises, so
     it flattens into a full screen rather than sliding past as a fixed
     shape. Floored at 1px because a zero-radius ellipse is degenerate. */
  --sag:max(1px, calc(var(--sag-max) * (1 - var(--arc-t, 0))));
  --ry:calc(var(--sag) * 3.928);
  --cy:calc(var(--ry) * -0.7454);
  --fade:clamp(420px, 62vh, 900px);
  /* How far the sheet takes to reach its own colour. Longer than --fade on
     purpose: the tone change is the thing most likely to betray the
     boundary, so it is the slowest of the three layers. */
  --ground:clamp(560px, 88vh, 1200px);
  position:relative;
  z-index:1;
  /* Deliberately keyed to --sag-max, not --sag: if the padding shrank along
     with the curve the content would slide up as you scrolled. */
  padding-top:calc(var(--sag-max) * 1.15);
  background-color:var(--black-2);
  /* Layer 1 — the bright leading edge. Same ellipse proportions as the mask
     (95%/1.9 vs 50%/1.0 is the same shape scaled 1.9x), so its 52.6% mark
     falls exactly on the cut. Putting the brightest stop just outside that
     lays a band of light along the curve, which is what makes the sweep
     visible against the dark hero without drawing a hard line.
     Layer 2 — the broad grey wash falling away beneath it. */
  background-image:
    /* Closing fade to pure black, anchored to the BOTTOM of the whole sheet.

       It lived on .footer first, and that was the unnatural part: the
       darkening began exactly at the footer's top edge, so the RATE of
       change jumped from nothing to something on one line and the eye read
       that line as a border even though the colours either side matched.
       Spanning it from well above the footer means there is no point where
       the change starts — it is already under way by the time you get
       there. */
    linear-gradient(180deg,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,.14) 22%,
      rgba(0,0,0,.34) 42%,
      rgba(0,0,0,.58) 60%,
      rgba(0,0,0,.80) 76%,
      rgba(0,0,0,.94) 89%,
      #000 100%),
    radial-gradient(ellipse 142.5% calc(var(--ry) * 1.9) at 50% var(--cy),
      transparent 0 52.6%,
      rgba(255,255,255,.022) 53.6%,
      rgba(255,255,255,.034) 55.5%,
      rgba(255,255,255,.024) 60%,
      rgba(255,255,255,.011) 67%,
      rgba(255,255,255,.004) 76%,
      transparent 88%),
    /* Starts at zero at the sheet's very top edge. It used to peak there,
       and because the mask's soft ramp is a fraction of the arc it collapses
       to nothing as the curve flattens — leaving that wash cut off by a hard
       straight line. Fading in from the edge means there is no step to cut,
       however sharp the mask gets. */
    linear-gradient(180deg,
      rgba(255,255,255,0) 0%,
      rgba(255,255,255,.012) 9%,
      rgba(255,255,255,.021) 20%,
      rgba(255,255,255,.013) 42%,
      rgba(255,255,255,.005) 64%,
      rgba(255,255,255,0) 92%),
    /* Layer 3 — the ground the sheet arrives on.

       background-color alone made the sheet start at full --black-2, so the
       curve met the pure-black hero as a step in lightness: the mask hid the
       EDGE but not the tone change, and the eye reads that as the boundary.
       This lays #000 along the curve — exactly the hero's own colour, so
       there is nothing to see there — and lifts to fully transparent further
       down, letting --black-2 come through as the section's base.

       The stops are eased rather than evenly spaced. A straight ramp across
       this distance banded visibly on a near-black gradient; front-loading
       the change keeps each step under one 8-bit level. */
    linear-gradient(180deg,
      rgb(0,0,0) 0%,
      rgba(0,0,0,.94) 10%,
      rgba(0,0,0,.80) 24%,
      rgba(0,0,0,.60) 40%,
      rgba(0,0,0,.38) 57%,
      rgba(0,0,0,.19) 73%,
      rgba(0,0,0,.06) 88%,
      rgba(0,0,0,0) 100%);
  /* How far up from the bottom the closing fade reaches. Long on purpose —
     it has to start before the footer so the onset is never a line. */
  --closeout:clamp(950px, 135vh, 1900px);
  background-repeat:no-repeat, no-repeat, no-repeat, no-repeat;
  background-size:100% var(--closeout), 100% 100%, 100% var(--fade), 100% var(--ground);
  background-position:0 100%, 0 0, 0 0, 0 0;
  /* The cut ramps in over most of the ellipse instead of switching at its
     edge. A hard stop drew a visible seam against the hero; spreading the
     falloff lets the sheet surface out of the hero with no boundary, while
     the ellipse still shapes it into a curve. */
  /* Four mask layers, intersected:

     1. the ellipse, which carves the curve and shrinks with --arc;
     2. a fade of constant thickness at the very top edge;
     3 + 4. a soft bite out of each top corner, where the curve runs off
     the sides.

     Layer 2 is what fixes the seam. The ellipse's soft ramp is expressed
     as a fraction of its own radius, so as the curve flattens toward 1px
     that ramp collapses with it and the sheet's opaque black lands on the
     lit hero as a hard straight line. Layer 2 is measured in px, so the
     edge keeps the same softness no matter how flat the curve gets.

     Layers 3 + 4 exist because a geometric mask has no falloff at all: the
     arc reaches the screen edges at full strength, so the ends read as cut
     off. (unitedcarriers.com sidesteps this entirely — its arc is a
     pre-blurred image, so the alpha thins out on its own where the shape
     runs off the sides.) These start the ends part-transparent and let the
     sheet gain solidity as it moves inward. Their height is keyed to --sag,
     so the bite shrinks with the curve and is gone by the time the sheet
     lies flat and should be solid black. */
  --soft:clamp(9px, 1.6vh, 20px);
  --end-w:clamp(120px, 20vw, 380px);
  --end-h:calc(var(--sag) * 1.15 + 1px);
  -webkit-mask-image:
    radial-gradient(ellipse 75% var(--ry) at 50% var(--cy),
      rgba(0,0,0,0) 0 97.6%,
      rgba(0,0,0,.22) 98.7%,
      rgba(0,0,0,.68) 99.5%,
      #000 100%),
    linear-gradient(180deg,
      rgba(0,0,0,0) 0px,
      rgba(0,0,0,.30) calc(var(--soft) * .45),
      rgba(0,0,0,.78) calc(var(--soft) * .78),
      #000 var(--soft)),
    radial-gradient(ellipse var(--end-w) var(--end-h) at 0% 0%,
      rgba(0,0,0,.58) 0%,
      rgba(0,0,0,.78) 44%,
      rgba(0,0,0,.94) 74%,
      #000 100%),
    radial-gradient(ellipse var(--end-w) var(--end-h) at 100% 0%,
      rgba(0,0,0,.58) 0%,
      rgba(0,0,0,.78) 44%,
      rgba(0,0,0,.94) 74%,
      #000 100%);
          mask-image:
    radial-gradient(ellipse 75% var(--ry) at 50% var(--cy),
      rgba(0,0,0,0) 0 97.6%,
      rgba(0,0,0,.22) 98.7%,
      rgba(0,0,0,.68) 99.5%,
      #000 100%),
    linear-gradient(180deg,
      rgba(0,0,0,0) 0px,
      rgba(0,0,0,.30) calc(var(--soft) * .45),
      rgba(0,0,0,.78) calc(var(--soft) * .78),
      #000 var(--soft)),
    radial-gradient(ellipse var(--end-w) var(--end-h) at 0% 0%,
      rgba(0,0,0,.58) 0%,
      rgba(0,0,0,.78) 44%,
      rgba(0,0,0,.94) 74%,
      #000 100%),
    radial-gradient(ellipse var(--end-w) var(--end-h) at 100% 0%,
      rgba(0,0,0,.58) 0%,
      rgba(0,0,0,.78) 44%,
      rgba(0,0,0,.94) 74%,
      #000 100%);
  -webkit-mask-repeat:no-repeat, no-repeat, no-repeat, no-repeat;
          mask-repeat:no-repeat, no-repeat, no-repeat, no-repeat;
  -webkit-mask-size:100% 100%, 100% 100%, 100% 100%, 100% 100%;
          mask-size:100% 100%, 100% 100%, 100% 100%, 100% 100%;
  -webkit-mask-composite:source-in;   /* older WebKit spelling of intersect */
          mask-composite:intersect;
}

/* ── The curve's surface ─────────────────────────────────────
   alche.studio does its version of this in a full-screen WebGL canvas —
   there is no CSS to copy, only the read of it: something glassy with a
   fine grain living on it. Built here out of what the curve already is.

   Why the refraction is real and not decoration: .stack's mask feathers
   the top edge, so along the curve the sheet is only PARTLY opaque and
   the hero is genuinely visible through it. A backdrop-filter there has
   the hero to work on, and the same mask shapes it into the curve. Below
   the feather the sheet turns solid and there is nothing left to refract,
   which is why this band is short and fades out. */
.stack__glass{
  position:absolute;
  left:0; right:0; top:0;
  height:calc(var(--sag-max) * 1.25);
  pointer-events:none;
  z-index:0;
  /* The grain inside is oversized so its drift never exposes an edge; without
     this it widened the document by ~500px and produced a horizontal
     scrollbar. `clip` rather than `hidden` so no scroll container is made. */
  overflow:clip;
  /* brightness under 1: the glass darkens what it refracts rather than
     lifting it, so it reads as smoked rather than frosted. */
  -webkit-backdrop-filter:blur(16px) saturate(112%) brightness(74%);
          backdrop-filter:blur(16px) saturate(112%) brightness(74%);
  /* Strongest right at the arc, gone before the sheet goes solid. */
  -webkit-mask-image:linear-gradient(180deg, #000 0%, rgba(0,0,0,.62) 34%, rgba(0,0,0,.22) 62%, transparent 100%);
          mask-image:linear-gradient(180deg, #000 0%, rgba(0,0,0,.62) 34%, rgba(0,0,0,.22) 62%, transparent 100%);
}

/* Grain, scoped to the band. The page already carries a global grain; this
   one is finer and stronger, so the curve reads as a surface with its own
   texture rather than as a clean gradient. `overlay` lets it darken and
   lighten instead of only fogging. */
.stack__grain{
  position:absolute; inset:-40%;
  opacity:.09;
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
  /* Slower and gentler than the page grain — this one sits on a surface
     that is meant to feel still, not to shimmer. */
  animation:grain 7s steps(4) infinite;
}

/* Anything after the glass has to sit above it, or the sheet's own
   background would paint over the refraction. */
.stack > .sec, .stack > footer{ position:relative; z-index:1; }

@media (prefers-reduced-motion: reduce){
  .stack__grain{ animation:none; }
}

/* Once the sheet has fully covered the hero there is nothing to look at,
   so stop rendering it — this also lets the wordmark's WebGL loop idle
   instead of running behind an opaque layer for the whole page. */
:root.is-hero-covered .hero{ content-visibility:hidden; }
/* The hero copy simply fades. Driven by a class, not a scroll-bound value,
   so a small scroll starts it and the transition finishes on its own
   instead of tracking the wheel; the same transition plays in reverse on
   the way back up. Nothing moves and nothing is clipped, which is why the
   per-row clip wrappers this used to need are gone. */
.hero__inner{
  flex:1 1 auto;
  min-height:0;
  /* Three rows, the outer two equal. That is what puts the wordmark dead
     centre: with `1fr auto 1fr` the middle row is centred in the box no
     matter what the meta line above and the lede below happen to measure.

     Auto margins in a flex column could not do this — they only split the
     free space that is LEFT, so the wordmark ended up midway between two
     blocks of unequal height rather than in the middle of the screen. The
     error moved with the layout too (19px at one size, 25px at another),
     so no fixed nudge would have held. */
  display:grid;
  grid-template-rows:1fr auto 1fr;
  transition:opacity 1.15s cubic-bezier(.32, 0, .24, 1);
  will-change:opacity;
}
:root.is-hero-out .hero__inner{
  opacity:0;
  /* the scroll cue is a link — don't leave it clickable once invisible */
  pointer-events:none;
}

/* Each pinned to its own end of the grid, so they stay where they were
   while the middle row does the centring. */
.hero__top{ align-self:start; padding-top:calc(var(--margin) * 4); }
.hero__bottom{ align-self:end; }

.hero__title{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  font-size:clamp(46px, 24.6vw, 470px);   /* fallback for no container-query units */
  font-size:25.4cqw;                      /* snaps the right edge to column 8 of 12 */
  line-height:.86;
  letter-spacing:-.055em;
  /* Centred. Left-aligned it ended around two thirds across and left a large
     dead area on the right of the screen, which is what unbalanced the
     first view. */
  text-align:center;
  /* No margin: the grid row places it. Any margin here would push it back
     off centre. */
  margin:0;
  padding-bottom:.04em;
  white-space:nowrap;
  /* overflow stays visible so the warped glass edges are not clipped */
  position:relative;
  /* Optical centring. The grid puts the text BOX dead centre, but "AXE" is
     all caps and a text box reserves descender space no capital uses, so
     the strokes sit slightly high of it. Measured at two font sizes (-6.5px
     at 312px, -4px at 180px) the discrepancy held at .021em, so the
     correction is expressed in em and holds at any size. Uses `top`, not
     `translate`, which the entrance animation owns. */
  top:.021em;
  /* Entrance uses `translate` rather than `transform` so it stays out of the
     way of anything that may later animate transform on this element. */
  opacity:0; translate:0 30px;
  transition:opacity 1.1s var(--ease), translate 1.1s var(--ease);
  /* the visible glyph is a WebGL canvas, not real text — block drag-select
     and copy on the underlying DOM text so there's nothing to grab */
  user-select:none;
  -webkit-user-select:none;
}
.is-ready .hero__title{ opacity:1; translate:0 0; }

/* WarpText: the canvas paints the wordmark, the real text stays for
   selection + screen readers. Colour is dropped only once WebGL is up
   (.warp-on), so a failure leaves plain white type behind. */
.hero__title.warp-on .warp-src{ color:transparent; }

/* left/top/width/height are set inline by warp-text.js — a canvas is a
   replaced element and will not stretch from left+right alone. */
.warp-canvas{
  position:absolute;
  display:block;
  pointer-events:none;   /* the hero section hosts the pointer instead */
}

/* No fade needed on the rule any more — it sits at the top of this row, so
   the slide-and-cut shaves it off first. */
.hero__bottom{
  align-items:end;
  padding-top:12px;
  border-top:1px solid var(--hair);
}
.hero__lede{ font-size:var(--fs-txt); line-height:1.65; color:var(--dim); }
.hero__idx{ display:flex; flex-direction:column; gap:2px; }
.hero__cue{ display:flex; flex-direction:column; align-items:flex-end; gap:6px; color:var(--dim); }
.hero__cue:hover{ color:var(--white); }
.hero__cue-arrow{ font-size:14px; animation:bob 2s var(--ease2) infinite; }
@keyframes bob{ 0%,100%{transform:translateY(0)} 50%{transform:translateY(5px)} }

/* ════════════════════════════════════════════════════════════
   SECTIONS
   ════════════════════════════════════════════════════════════ */
.sec{
  --sec-pad:clamp(60px,8vw,130px);
  padding:var(--sec-pad) var(--margin);
  border-bottom:1px solid var(--hair);
  container-type:inline-size;
}
.sec--alt{ background:linear-gradient(180deg, rgba(255,255,255,.035), transparent 55%); }

/* The last section's closing rule lands exactly where the footer begins —
   a literal hairline drawn across the full width at the one place the page
   is meant to dissolve into black. The tone ramp behind it was doing its
   job; this line was sitting on top of it. Sections separate from each
   other, not from the end of the page. */
.stack > .sec:last-of-type{ border-bottom:0; }

/* ── About: the two-column intro ─────────────────────────────
   Modelled on unitedcarriers.com's second screen, measured at 1280px:
   a 16-col grid with the sticky headline on cols 2-7 and a 449px text
   column on cols 12-16 — everything between them empty. Reproduced here
   on the 12-col grid as 1-5 and 8-12, so the ratio of column to void is
   the same. Sizes below are that page's px values expressed as vw so
   they hold across widths.

   Kept line-free, as asked earlier: the reference draws a hairline under
   its label and under every stat, and those are the one part not carried
   over. Spacing does that job here instead. */
#about{ border:0; padding-bottom:clamp(74px,9.4vw,150px); }

/* The wrapper is the grid child and defines the scroll distance; the inner
   block is what actually sticks. Splitting them is what lets the headline
   hold while the column beside it keeps moving.

   The wrapper MUST stretch to the row's full height. A sticky element can
   only travel inside its own parent, so with `align-items:start` the
   wrapper collapsed to the headline's own 288px and there was nowhere to
   travel — it read as not sticky at all. Stretching gives it the rail's
   full height to hold across. */
/* One row per right-hand block, with the sticky column spanning all of
   them — the reference's grid is `424px 852px` with its sticky column set
   to `grid-row: 1 / 3`. Rows are declared explicitly so `1 / -1` has a
   real last line to resolve against. */
.intro{ grid-template-rows:auto auto auto; }
.intro__col{ grid-column:8 / span 5; }

.intro__stickwrap{
  position:relative;
  grid-column:1 / span 5;
  grid-row:1 / -1;
  /* Pulled up through the section's top padding so the wrapper's top edge
     IS the section's top edge. A sticky box cannot pin before its wrapper
     reaches the pin line, so any gap above the wrapper is drift the
     headline has to travel through first — that was the residual "it
     follows the scroll a little" movement. With the gap removed it is
     already in place the moment the section arrives. No padding is added
     back inside: the sticky box is a full viewport tall with its contents
     at the bottom, so where it STARTS does not move the headline — it only
     changes when the pin engages. */
  margin-top:calc(var(--sec-pad) * -1);
}
/* Parks the headline LOW on the screen and holds it there for the whole
   section, letting go only where the wrapper ends — which is where
   Graphics begins.

   `bottom:` is the obvious-looking way to do this and does not work: a
   bottom-anchored sticky only engages while the element travels DOWN the
   viewport, i.e. when scrolling up. Scrolling down it just rides along
   (measured: it tracked the rail pixel for pixel).

   So the sticky box itself is a full viewport tall and pinned at top:0,
   with its contents pushed to its own bottom edge. The box is what sticks;
   the headline simply sits at the bottom of it. */
/* Measured off the reference: a sticky box exactly one viewport tall with
   the headline 64px above its bottom edge.

   The reference pins with `bottom:0`; this uses `top:0`, which for a box
   that is itself a full viewport puts the headline in the identical place.
   The swap is not cosmetic — `bottom` never engages here because this
   page's <body> is its own scroll container (`overflow: hidden auto`), so
   a bottom anchor resolves against a scrollport that never moves. Measured
   both ways: with `bottom` the headline tracked the scroll 1:1. */
.intro__stick{
  position:sticky; top:0;
  height:100svh;
  display:flex; flex-direction:column; justify-content:flex-end;
  padding-bottom:64px;
}

/* The first block on the right shares the headline's line. Both boxes are
   a viewport tall with their contents pushed to the bottom and the same
   64px below them, so the two columns start together instead of the copy
   sitting a screen above the headline it belongs to. */
/* Adjacent-sibling, not :first-of-type — every child here is a div, so
   :first-of-type resolves to the sticky wrapper and never matches. */
.intro__stickwrap + .intro__col{
  /* Same pull-up the sticky wrapper gets, so both boxes start on the
     section's top edge. Without it this box begins one --sec-pad lower and
     its bottom line lands that far below the headline's. */
  margin-top:calc(var(--sec-pad) * -1);
  min-height:100svh;
  display:flex; flex-direction:column; justify-content:flex-end;
  padding-bottom:64px;
}

.intro__eyebrow{
  display:block;
  font-family:var(--font-mono);
  font-size:11px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--faint);
  margin-bottom:clamp(26px,3.7vw,48px);
}
.intro__title{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 100;
  font-weight:700;
  /* The reference sets this at 53.3px (4.17vw at 1280); pushed up from
     there on request. Line-height tightens as it grows — at 72px the
     reference's 1.05 leaves the three lines drifting apart. */
  font-size:clamp(38px,5.7vw,88px);
  line-height:.99;
  letter-spacing:-.02em;
}
/* The reference drops its first line to 40% of the text colour — the
   headline reads as one sentence continuing into another, not as a
   heading with a subtitle. */
.intro__title-soft{ color:rgba(255,255,255,.40); }

/* Each block in the right column is a separate beat, spaced far enough
   apart that no rule is needed to separate them. */
.intro__col + .intro__col{ margin-top:clamp(80px,11vw,180px); }

/* The last block finishes level with the headline. The sticky box holds
   its contents 64px above its own bottom edge, and that box runs to the
   bottom of the grid — so the right column needs the same 64px beneath it
   or it overshoots the left by exactly that much. Its top gap is tightened
   at the same time, which is what lifts the block. */
.intro__col:last-child{
  margin-top:clamp(56px,7vw,112px);
  padding-bottom:64px;
}

/* The paragraphs keep their own mixed-serif setting (see "#about .txt"
   below) rather than the reference's flat 20.7px grotesk — that
   alternation of italic serif and grotesk is the section's voice and does
   not survive being flattened. Only the measure is re-cut here, because
   the column is narrower now than it was in the full-width layout. */
#about .intro__col .txt{ max-width:none; }

/* The button is now a line and nothing else — no box, no label. A boxed
   button was the only enclosed shape on the page and read as borrowed UI
   chrome; everything else here is set on open ground and separated by
   space alone. What is left is a single hairline at the same weight as the
   section's other rules, so it belongs to the page rather than sitting on
   top of it.

   With no text inside, the element has no intrinsic size: the width is
   given explicitly and the height is a comfortable hit target rather than
   the 1px the line actually occupies. */
.intro__btn{
  display:block;
  position:relative;
  margin-top:clamp(26px,3.5vw,46px);
  width:clamp(120px,17vw,260px);
  height:22px;
  background-image:linear-gradient(var(--hair), var(--hair));
  background-repeat:no-repeat;
  background-position:0 100%;
  background-size:100% 1px;
}
/* The wipe. A second line over the first, grown from the left edge —
   `transform` rather than `width` so it animates off the compositor. */
.intro__btn::after{
  content:'';
  position:absolute;
  left:0; right:0; bottom:0;
  height:1px;
  background:var(--white);
  transform:scaleX(0);
  transform-origin:left;
  transition:transform .45s var(--ease);
}
.intro__btn:hover::after{ transform:scaleX(1); }
/* Leaving on the right mirrors the entry rather than snapping back. */
.intro__btn:not(:hover)::after{ transform-origin:right; }

.intro__label{
  font-size:12px; font-weight:500;
  color:var(--dim);
  margin-bottom:clamp(30px,4vw,56px);
}

.intro__stat + .intro__stat{ margin-top:clamp(40px,5.4vw,84px); }
.intro__num{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  /* 118.5px at 1280 = 9.26vw. Deliberately LARGER than the section
     headline — the reference leads with its numbers and lets the
     headline sit quiet beside them. */
  font-size:clamp(56px,9.26vw,140px);
  line-height:1.05;
  letter-spacing:-.045em;
  font-variant-numeric:tabular-nums;
}
.intro__statdesc{
  font-size:clamp(13px,1.39vw,19px);
  line-height:1.1;
  font-weight:500;
  color:var(--dim);
  max-width:14ch;
  margin-top:.5em;
}

.intro__list{ list-style:none; }
.intro__list li{
  display:flex; gap:12px; align-items:baseline;
  font-size:var(--fs-txt);
  color:var(--text);
  padding:9px 0;
}
.intro__list em{
  font-family:var(--font-mono); font-style:normal;
  font-size:11px; color:var(--faint); flex:none;
}
.intro__list li > span{ margin-left:auto; color:var(--faint); font-size:12px; text-align:right; }

@media (max-width:900px){
  /* Both columns go full width. Without this the desktop split survived on
     phones and the headline was left in 5 of 12 columns — 141px on a 395px
     screen, three or four characters a line. */
  .intro{ grid-template-rows:auto; }
  .intro__stickwrap{
    grid-column:1 / -1;
    grid-row:auto;
    margin-top:0;                                  /* the desktop pull-up would clip it here */
    margin-bottom:clamp(34px,6vw,60px);
  }
  .intro__col{ grid-column:1 / -1; }

  /* Sticky needs a tall neighbour to hold against; stacked there is nothing
     to hold, so it goes back to being an ordinary heading. The full-viewport
     box and its bottom padding go with it — both existed only to park the
     headline low while it was pinned. */
  .intro__stick{
    position:static;
    min-height:0; height:auto;
    display:block;
    padding-bottom:0;
  }

  /* The first block was a full-viewport box with its contents pushed to the
     bottom, so it would start on the pinned headline's line. Stacked, that
     just leaves most of a screen of white space above the paragraphs. */
  .intro__stickwrap + .intro__col{
    min-height:0;
    display:block;
    margin-top:0;
    padding-bottom:0;
  }

  .intro__col + .intro__col{ margin-top:clamp(52px,9vw,90px); }
  /* The last block's 64px foot only existed to line its end up with the
     pinned headline, which is no longer beside it. */
  .intro__col:last-child{ padding-bottom:0; }
}

/* Label row (number left, meta right) sits above a full-width title.
   Two left edges instead of three competing items on one baseline. */
.sec__head{
  row-gap:10px;
  border-bottom:1px solid var(--line);
  padding-bottom:14px;
  margin-bottom:clamp(30px,4vw,56px);
}
.sec__title{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  /* The reference runs section titles ~5x the body size so the eye lands
     somewhere definite. 78px left them level with the stat numbers, which
     inverted the hierarchy. */
  font-size:clamp(40px, 10.5cqw, 150px);
  line-height:.9;
  letter-spacing:-.05em;
}

.sec__body{ margin-bottom:clamp(34px,4.6vw,64px); }

/* Body copy. The old rule ran ~107 characters per line in uppercase mono;
   percyjackson holds 36–43 and jackelder 54–66, so the measure is capped
   here in `ch` and the text drops to sentence case. Grotesk, not mono —
   uppercase mono is for the labels, not for paragraphs. */
.txt{
  font-family:var(--font-text);
  font-variation-settings:'wdth' 100;
  font-weight:400;
  text-transform:none;
  font-size:clamp(15px, 1.25vw, 18px);
  line-height:1.5;
  letter-spacing:-.011em;
  color:var(--text);
  max-width:56ch;          /* the actual readability fix */
  text-wrap:pretty;        /* avoids orphans where supported */
}
.txt + .txt{ margin-top:1.1em; }

/* Base emphasis for body copy outside About. About overrides both below
   with its serif/grotesk mix; everywhere else the point is only to let a
   claim and a number step forward, so this stays quiet. */
.txt em{ font-style:italic; color:rgba(255,255,255,.92); }
.txt strong{ font-weight:600; font-variation-settings:'wdth' 100; color:#fff; }

/* ── Mixed-type paragraph (About) ────────────────────────────
   Typefaces alternate inside a single sentence flow: an italic
   serif carries the opening claim, the grotesk continues it, and
   a heavier grotesk lands the numbers. Scoped to #about so the
   other sections keep their plain body setting.
   `em` is deliberately re-declared here — .menu__list em styles
   the overlay's 01–04 numerals and must not be touched. */
#about .txt{
  font-size:clamp(17px, 1.55vw, 22px);
  line-height:1.35;          /* tighter than plain body: the mix needs air
                                between phrases, not between lines */
  letter-spacing:-.015em;
  max-width:46ch;            /* larger type, so fewer ch for the same measure */
  color:rgba(255,255,255,.78);
}
#about .txt em{
  font-family:'Instrument Serif', Georgia, serif;
  font-style:italic;
  font-weight:400;
  font-size:1.16em;          /* serif x-height runs small next to the grotesk */
  letter-spacing:-.005em;
  color:#fff;
}
#about .txt strong{
  font-weight:600;
  font-variation-settings:'wdth' 100;
  color:#fff;
}

/* ── Stats ──────────────────────────────────────────────────── */
.stats{ margin-bottom:clamp(34px,4.6vw,60px); }
.stat{ display:flex; flex-direction:column; gap:6px; border-top:1px solid var(--line); padding-top:12px; }
.stat__num{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  /* Was 80px — level with the section title, so nothing led. Now clearly
     subordinate to it. */
  font-size:clamp(30px,3.6vw,48px); line-height:.95; letter-spacing:-.04em;
  font-variant-numeric:tabular-nums;
}

/* ── Roblox ─────────────────────────────────────────────────
   The section used to open with "4B+" set at 42.7cqw — most of the screen
   wide. It shouted, and it said the same thing the copy already says. The
   number now lives in the paragraph and in the screenshot's caption, and
   the image carries the visual weight instead. */
.rbx{ align-items:start; row-gap:clamp(34px,5vw,64px); }
.rbx__text .tags{ margin-top:clamp(24px,3.2vw,40px); }

.rbx__shot{ position:relative; }
/* Hidden until it has decoded — see the note on the img in index.html. */
.rbx__shot img{ display:none; }
.rbx__shot.has-img img{
  display:block; width:100%; height:auto;
  border:1px solid var(--hair);
  /* The source is a bright, saturated game thumbnail dropped into an
     all-greyscale page. Pulled most of the way down so it reads as part of
     the design, and back up on hover so it can still be looked at. */
  filter:grayscale(.72) contrast(1.05) brightness(.82);
  transition:filter .5s var(--ease);
}
.rbx__shot.has-img:hover img{ filter:grayscale(0) contrast(1) brightness(1); }
.rbx__shot figcaption{
  display:flex; justify-content:space-between; gap:12px;
  margin-top:10px;
  font-family:var(--font-mono); font-size:11px;
  letter-spacing:.06em; text-transform:uppercase;
}
/* Stand-in for the artwork, drawn in type. Shown only when there is no
   image file — .no-img is set by the img's onerror. Built to look like a
   deliberate card rather than a placeholder, so the section is finished
   whether or not the screenshot ever arrives. */
.rbx__shot.has-img .rbx__card{ display:none; }
.rbx__card{
  display:flex; flex-direction:column; justify-content:flex-end;
  aspect-ratio:760 / 396;
  padding:clamp(20px,2.6vw,34px);
  border:1px solid var(--hair);
  background:
    radial-gradient(120% 100% at 12% 0%, rgba(255,255,255,.07) 0%, transparent 60%),
    linear-gradient(180deg, #101114 0%, #08090b 100%);
}
.rbx__card-kicker{
  font-family:var(--font-mono); font-size:11px;
  letter-spacing:.1em; text-transform:uppercase; color:var(--faint);
  margin-bottom:auto;
}
.rbx__card-name{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  /* Sized off the card, not the viewport, so it fills the frame at any
     column width. */
  font-size:clamp(38px,7.4vw,104px);
  line-height:.9; letter-spacing:-.045em;
}
.rbx__card-meta{
  margin-top:12px;
  font-family:var(--font-mono); font-size:11px;
  letter-spacing:.06em; text-transform:uppercase; color:var(--dim);
}

@media (max-width:900px){
  /* !important because the grid utility on the element itself
     (.span-6.span-start-7) is two classes deep and outranks a single-class
     selector — without it the screenshot stayed in a 171px half column. */
  .rbx__text,
  .rbx__shot{ grid-column:1 / -1 !important; }
  .rbx__shot{ margin-top:clamp(24px,5vw,40px); }
}

/* ── Rows ───────────────────────────────────────────────────── */
.rows{ margin-bottom:clamp(30px,4vw,52px); }
.row{ border-top:1px solid var(--hair); }
.rows > .row:last-child{ border-bottom:1px solid var(--hair); }

.row,
.row > a{
  display:grid; grid-template-columns:repeat(12,1fr); gap:var(--gap);
  align-items:center;
}
.row{ padding:11px 0; }
.rows--link .row{ padding:0; }
.rows--link .row > a{
  /* The row is itself a 12-column grid, so this link auto-placed into a
     SINGLE column and squeezed its own 12-column grid into ~117px — the
     server names were rendering at 36px wide. It has to span the row. */
  grid-column:1 / -1;
  position:relative; padding:clamp(12px,1.5vw,20px) 0;
  transition:padding-inline var(--hover-out) var(--hover-ease);
}
.rows--link .row > a::before{
  content:''; position:absolute; inset:0;
  background:var(--white); z-index:0;
  transform:scaleX(0); transform-origin:left center;
  transition:transform var(--hover-out) var(--hover-ease);
}
.rows--link .row > a,
.rows--link .row > a::before,
.rows--link .row > a > *{ transition-delay:0s; }
.rows--link .row > a:hover,
.rows--link .row > a:hover::before,
.rows--link .row > a:hover > *{
  transition-duration:var(--hover-in);
  transition-delay:var(--hover-delay);
}
.rows--link .row > a > *{ position:relative; z-index:1; }
.rows--link .row > a:hover{ padding-inline:14px; }
.rows--link .row > a:hover::before{ transform:scaleX(1); }
.rows--link .row > a:hover > *{ color:var(--black); }

.row__n{ grid-column:span 1; color:var(--faint); transition:color var(--hover-out) var(--hover-ease); }
.row__t{
  grid-column:span 5;
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:800;
  font-size:clamp(14px,1.7vw,24px); line-height:1; letter-spacing:-.04em;
  transition:color var(--hover-out) var(--hover-ease);
}
.row__m{ grid-column:span 4; color:var(--dim); transition:color var(--hover-out) var(--hover-ease); }
.row__a{ grid-column:span 2; text-align:right; font-size:13px; transition:color var(--hover-out) var(--hover-ease), transform var(--hover-out) var(--hover-ease); }
.rows--link .row > a:hover .row__a{ transform:translateX(5px); }

@media (max-width:760px){
  .row__n{ grid-column:span 2 }
  .row__t{ grid-column:span 10 }
  .row__m{ grid-column:3 / span 8 }
  .row__a{ grid-column:span 2 }
}

/* ── Spec sheet (Contact) ───────────────────────────────────
   A definition list, not a table: these are label/value pairs, not tabular
   data, and dl carries that relationship to a screen reader for free.
   Hairlines only between rows — a full border would box it off from the
   paragraph it belongs with. */
.spec{ align-self:start; }
.spec__row{
  display:flex; justify-content:space-between; gap:16px;
  padding:11px 0;
  border-bottom:1px solid var(--hair);
}
.spec__row:first-child{ border-top:1px solid var(--hair); }
.spec dt{ color:var(--faint); }
.spec dd{ color:var(--white); text-align:right; }

@media (max-width:860px){
  .spec{ grid-column:span 12 !important; margin-top:clamp(28px,5vw,44px); }
}

/* ── Tags ───────────────────────────────────────────────────── */
.tags{ display:flex; flex-wrap:wrap; gap:6px; margin-bottom:clamp(30px,4vw,52px); }
.tags li{
  border:1px solid var(--line); padding:7px 12px; color:var(--dim);
  /* Much quicker than the big CTAs, and on the fast-start curve. A tag is a
     small target you sweep across, not a destination you settle on — at the
     CTA's pace and curve the colour was still arriving after the pointer
     had moved to the next one. No hover-intent delay here for the same
     reason. */
  transition:background .34s var(--tap-ease), color .34s var(--tap-ease), border-color .34s var(--tap-ease);
}
.tags li:hover{ background:var(--white); color:var(--black); border-color:var(--white); }

/* ── CTA ────────────────────────────────────────────────────── */
.cta{
  position:relative; display:block;
  border-top:1px solid var(--line); border-bottom:1px solid var(--line);
  padding:clamp(16px,2.2vw,26px) 0;
  overflow:hidden;
  transition:padding-inline var(--hover-out) var(--hover-ease);
}
/* The fill is a scaleX, not an animated `right`. `right` is a layout value,
   so every frame re-laid the box out and the sweep could not be handed to
   the compositor; scaleX is smooth at any speed. */
.cta::before{
  content:''; position:absolute; inset:0;
  background:var(--white); z-index:0;
  transform:scaleX(0); transform-origin:left center;
  transition:transform var(--hover-out) var(--hover-ease);
}
.cta > *{
  position:relative; z-index:1; display:block;
  transition:color var(--hover-out) var(--hover-ease);
}
.cta:hover{ padding-inline:14px; }
.cta:hover::before{ transform:scaleX(1); }
.cta:hover .cta__l,.cta:hover .cta__m,.cta:hover .cta__a{ color:var(--black); }
/* Entering is slow and deliberate; leaving is quicker. With one shared
   duration a fast in-out left the fill crawling back long after the pointer
   had gone, which is what felt wrong when flicking on and off. */
.cta,
.cta::before,
.cta > *{ transition-delay:0s; }
.cta:hover,
.cta:hover::before,
.cta:hover > *{
  transition-duration:var(--hover-in);
  transition-delay:var(--hover-delay);
}

/* Duration deliberately not restated here — it inherits the shared
   --hover-* pair from `.cta > *`. Restating it was the bug: this label and
   the arrow were still on .35s, so they flipped well before the fill and
   the rest of the text caught up. */
.cta__l{ margin-bottom:8px; }
.cta__m{
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  font-size:clamp(20px,3.4vw,50px); line-height:1; letter-spacing:-.05em;
}
.cta__a{
  position:absolute !important; right:0; top:50%; transform:translateY(-50%);
  font-size:18px;
  transition:color var(--hover-out) var(--hover-ease), transform var(--hover-out) var(--hover-ease);
}
.cta:hover .cta__a{
  transition-duration:var(--hover-in), var(--hover-in);
  transition-delay:var(--hover-delay), var(--hover-delay);
}
.cta:hover .cta__a{ transform:translateY(-50%) translateX(5px); }

/* ── Works ──────────────────────────────────────────────────── */
.works{ margin-bottom:clamp(30px,4vw,52px); row-gap:clamp(18px,2.4vw,34px); }
.slot__frame{
  position:relative; aspect-ratio:16 / 10;
  border:1px solid var(--hair);
  background:repeating-linear-gradient(-45deg, rgba(255,255,255,.03) 0 1px, transparent 1px 10px);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
  transition:border-color .4s var(--ease);
}
.slot__frame span{ color:var(--faint); letter-spacing:.16em; }
.slot__frame img{ width:100%; height:100%; object-fit:cover; transition:transform .8s var(--ease); }
.slot:hover .slot__frame{ border-color:var(--line); }
.slot:hover .slot__frame img{ transform:scale(1.04); }
.slot figcaption{ display:flex; justify-content:space-between; gap:12px; padding-top:8px; }

@media (max-width:760px){ .slot{ grid-column:span 12; } }

/* ════════════════════════════════════════════════════════════
   FOOTER
   ════════════════════════════════════════════════════════════ */
.footer{
  padding:clamp(50px,7vw,110px) var(--margin) var(--margin);
  container-type:inline-size;
}
.footer__big{
  display:block;
  font-family:var(--font-display);
  font-variation-settings:'wdth' 125;
  font-weight:900;
  /* Re-cut for "axepeak". The old 14.05cqw was measured against "Let's
     talk" — three glyphs longer — so the shorter word only reached 85% of
     the width and left a 183px hole on the right. Scaled by 1/0.851 so the
     word spans the measure edge to edge again. Re-measure this if the text
     ever changes length. */
  font-size:clamp(34px, 15.9vw, 353px);
  font-size:16.51cqw;
  line-height:.9; letter-spacing:-.055em;
  margin:clamp(20px,3vw,44px) 0 clamp(40px,6vw,90px);
  overflow:hidden; padding-bottom:.04em;
  white-space:nowrap;
}
.footer__big .char{ display:inline-block; transition:transform .45s var(--ease); }
.footer__big:hover .char{ transform:translateY(-8px); }
.footer__big .char:nth-child(even){ transition-delay:.05s; }

.footer__bar{ border-top:1px solid var(--hair); padding-top:12px; align-items:baseline; }
.footer__links{ display:flex; justify-content:flex-end; gap:20px; color:var(--dim); }
.footer__links a:hover{ color:var(--white); }

/* ════════════════════════════════════════════════════════════
   REVEAL
   ════════════════════════════════════════════════════════════ */
/* Content surfaces slightly out of focus. The blur is deliberately small —
   at this size it reads as the text resolving rather than as an effect, and
   it keeps glyph edges from being visibly soft for long enough to look
   broken. The hero is untouched: nothing there carries .reveal. */
.reveal{
  opacity:0;
  transform:translateY(18px);
  filter:blur(7px);
  transition:opacity .9s var(--ease), transform .9s var(--ease), filter .9s var(--ease);
}
/* filter:none rather than blur(0) — a finished `none` lets the browser drop
   the filter layer entirely instead of keeping a no-op one alive on every
   revealed element. The transition still animates, because it interpolates
   toward the zero-length equivalent on the way there. */
.reveal.is-in{ opacity:1; transform:none; filter:none; }
.reveal:nth-child(2){transition-delay:.06s}
.reveal:nth-child(3){transition-delay:.12s}
.reveal:nth-child(4){transition-delay:.18s}
.reveal:nth-child(5){transition-delay:.24s}

/* The tag row is five items side by side on ONE line, so they all cross the
   trigger together — a stagger built for stacked items just made the last
   tag wait a quarter second while its neighbours were already solid.
   Tightened to a quick left-to-right ripple. */
.tags .reveal:nth-child(2){transition-delay:.03s}
.tags .reveal:nth-child(3){transition-delay:.06s}
.tags .reveal:nth-child(4){transition-delay:.09s}
.tags .reveal:nth-child(5){transition-delay:.12s}

/* ════════════════════════════════════════════════════════════
   RESPONSIVE COLLAPSE
   ════════════════════════════════════════════════════════════ */
@media (max-width:860px){
  .max-hide{ display:none; }
  .hero__top .span-4{ grid-column:span 6; }

  /* Flexible rows top and bottom, so the wordmark and the lede sit together
     in the middle rather than being pushed to one end. Packing the group to
     the bottom was tried and is worse: on an 812px screen it left a 554px
     void above the meta line and put the wordmark 222px below centre. */
  .hero__top{ align-self:start; }
  /* The wordmark's line-height is below 1, so its ink reaches past the box
     it occupies; at this size that overlapped the lede by 12px. */
  .hero__title{ font-size:38.2cqw; margin:0 0 clamp(14px,3vh,28px); }

  /* The lede moves up under the wordmark; the scroll cue stays at the foot.
     `display:contents` dissolves .hero__bottom so its children become rows
     of .hero__inner directly and can be placed independently — the two were
     locked together in one bottom-pinned block before.
     Its own grid-column values have to be restated as `1`: those span-12
     rules were written for the 12-column .hero__bottom that no longer
     exists here, and spanning 12 tracks of a one-column grid would generate
     eleven implicit columns. */
  .hero__inner{ grid-template-rows:1fr auto auto 1fr; }
  .hero__bottom{ display:contents; }
  .hero__top{ grid-row:1; }
  .hero__title{ grid-row:2; }
  .hero__lede{
    grid-row:3; grid-column:1 !important;
    align-self:start;
    margin-bottom:clamp(20px,4vh,40px);
  }
  .hero__cue{
    grid-row:4; grid-column:1 !important;
    align-self:end;
    flex-direction:row; align-items:center; justify-content:space-between;
    margin-top:16px;
  }
  .sec__head .span-2{ grid-column:span 12; }
  .sec__head .span-7{ grid-column:span 9; }
  .sec__head .span-3{ grid-column:span 3; }
  /* How steep the curve reads is depth ÷ span, and --sag-max is measured in
     vh — fine on a wide screen, wrong on a phone. At 375×812 it produced
     373px of sag across 375px of width: a ratio of 0.996, against 0.29 on a
     1280px desktop. Same shape on paper, three times the pitch on screen.
     Keyed to width here so the ratio matches what the desktop already has.
     Desktop is untouched: this only exists inside the phone breakpoint. */
  .stack{ --sag-max:clamp(88px, 30vw, 210px); }

  /* max-width:56ch still caps the measure, so spanning the grid is safe */
  .txt{ grid-column:span 12 !important; }
  .stat{ grid-column:span 12; }

  /* Every offset column goes full width. These utilities are two classes
     deep, so a single-class override loses to them — hence !important.
     Without this the Discord paragraph and the Contact spec sheet stayed in
     half-width columns on a phone. */
  .sec__body > .span-6.span-start-7,
  .spec.span-4.span-start-9{ grid-column:1 / -1 !important; }
  .spec{ margin-top:clamp(28px,6vw,44px); }
  .footer__links{ grid-column:span 12; justify-content:flex-start; }
  .footer__bar .span-4{ grid-column:span 12; margin-bottom:8px; }
}

/* ════════════════════════════════════════════════════════════
   REDUCED MOTION
   ════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .reveal{ opacity:1; transform:none; }
  /* `translate`, matching the entrance property — using transform here would
     outrank the scroll slide-out and freeze the wordmark in place. */
  .hero__title{ opacity:1; translate:0 0; }
  .grain-mask,.scanline{ display:none; }
}
