/* Plant & Fixings — mapped onto the ht-kit token contract.
   Contract tokens carry the ht-kit names. Every value is unchanged from the
   design system's tokens/*.css; only the names moved. Extras are marked, and
   listed in md/README.md with the reason each one exists. */
/* js/modules/rise.js reads --dur-stagger and parses it as a number. Registering
   it means the computed value is a resolved time, 135ms, rather than the literal
   string "calc(90ms * 1.5)" that an unregistered custom property would hand back
   and parseFloat could not read. If the calc ever fails to parse it falls back
   to the initial value rather than to zero. */
@property --dur-stagger{syntax:"<time>";inherits:true;initial-value:90ms}
/* Same reason: js/modules/scramble.js parses --dur-scramble as a number, and an
   unregistered property would hand back the literal "calc(1.2s * 1.5)". */
@property --dur-scramble{syntax:"<time>";inherits:true;initial-value:1200ms}

:root{
  /* ---- raw palette, the anchors everything else resolves to (extra) ---- */
  --black:#000000;
  --white:#FFFFFF;
  --surface-2:#1E1F23;

  /* ---- colour: contract ---- */
  --ink:#0B0B0C;
  --paper:#E8E9EA;
  --surface:#16171A;
  --accent:#EB0706;
  --accent-ink:var(--black);
  --muted:var(--muted-on-light);
  --rule:var(--rule-on-light);

  /* ---- colour: context pairs and surface tints (extra) ---- */
  --text:var(--ink);
  --text-on-dark:var(--white);
  --muted-on-light:rgba(11,11,12,.62);
  --muted-on-dark:rgba(255,255,255,.62);
  --rule-on-light:rgba(11,11,12,.18);
  --rule-on-dark:rgba(255,255,255,.16);
  --rule-on-accent:rgba(0,0,0,.28);
  --rule-on-accent-strong:rgba(0,0,0,.35);
  --grid-line:rgba(255,255,255,.07);
  --grid-line-soft:rgba(255,255,255,.06);
  --scrim-ink:rgba(11,11,12,.52);
  --scrim-clear:rgba(11,11,12,0);
  --focus-ring:var(--accent);

  /* ---- type: contract ----
     --type-scale is the one dial for the whole ramp, the way --density is the
     one dial for space. Every size below is multiplied by it, so the
     relationships hold and only the overall size moves. 1 is the original. */
  --type-scale:.9;
  --font-display:"Chakra Petch","Archivo",system-ui,sans-serif;
  --font-body:"Inter",system-ui,sans-serif;
  --font-utility:"JetBrains Mono",ui-monospace,monospace;
  --step--1:calc(14px * var(--type-scale));
  --step-0:calc(16px * var(--type-scale));
  --step-1:calc(clamp(22px,2.1vw,30px) * var(--type-scale));
  --step-2:calc(clamp(30px,3.6vw,52px) * var(--type-scale));
  --step-3:calc(clamp(38px,5.4vw,76px) * var(--type-scale));
  --step-4:calc(clamp(44px,11.5vw,196px) * var(--type-scale));
  --lh-tight:.98;
  --lh-body:1.55;
  --track-display:-.015em;

  /* ---- type: roles the contract has no slot for (extra) ---- */
  --fs-figure:calc(clamp(40px,4.4vw,64px) * var(--type-scale));
  --fs-mono:calc(15px * var(--type-scale));
  --fs-mono-sm:calc(13px * var(--type-scale));
  --fs-mono-lg:calc(17px * var(--type-scale));
  --fs-contact:calc(19px * var(--type-scale));
  --fs-arrow:calc(20px * var(--type-scale));
  --tracking-mono:.12em;
  --tracking-mono-wide:.18em;

  /* ---- space: contract. --density scales the whole ramp ---- */
  --density:1.25;
  --space-1:calc(4px * var(--density));
  --space-2:calc(8px * var(--density));
  --space-3:calc(12px * var(--density));
  --space-4:calc(16px * var(--density));
  --space-5:calc(20px * var(--density));
  --space-6:calc(24px * var(--density));
  --space-7:calc(32px * var(--density));
  --space-8:calc(40px * var(--density));
  --space-9:calc(48px * var(--density));   /* extra, ninth rung */
  --gutter:calc(clamp(20px,2.4vw,32px) * var(--density));
  --max-width:1440px;
  --measure:58ch;

  /* ---- space: block padding, also on the density dial (extra) ---- */
  --pad-card:calc(28px * var(--density));
  --pad-panel:calc(clamp(32px,3.6vw,56px) * var(--density));
  --pad-section:calc(clamp(64px,8vw,144px) * var(--density));
  --page-pad:calc(clamp(20px,4vw,64px) * var(--density));

  /* ---- form: contract ---- */
  --radius:0;
  --border:1px;

  /* ---- form: geometry the contract has no slot for (extra) ---- */
  --notch:18px;
  --notch-sm:10px;
  --notch-lg:34px;
  /* The footer's ledge, lifted out so the coloured bands can take the same step
     in their top edge. Deliberately the footer's own numbers, so the step lands
     on the same seam down the page. They are separate tokens rather than the
     --ftr-* ones because --ftr-col is doing double duty in the footer, where it
     is also the grid column the ledge lines up with. A band has no grid to
     answer to, and tying the two together would mean a change to the footer
     layout silently moving the step on two sections. */
  --ledge-col:38%;
  /* Height and horizontal run of the step. Together they set the angle, which
     is the whole character of the cut: --ledge-h down over --ledge-cut across.
     It was 104 over 26, a 4:1 knife edge at about 76 degrees. It is now 80 over
     44, a little under 2:1 and about 61 degrees. Shorter and wider reads as cut
     rather than torn. Keep the two moving together or the angle drifts. */
  --ledge-h:clamp(44px,5.6vw,80px);
  --ledge-cut:clamp(24px,3vw,44px);
  --tap-min:44px;
  --hdr-h:72px;
  --control-h:56px;
  --control-h-lg:68px;
  --control-pad:28px;
  --control-pad-lg:36px;
  --control-gap:10px;
  /* The FX/BORDER edge, and the outline variant's resting ring. Separate from
     --border because that token is the hairline for cards, panels, tables and
     the FX/DRAW rules, where 1px is correct. A control is a 56px block being
     drawn round in four steps, and 1px was too quiet to read. */
  --control-edge:2px;
  --fs-btn:calc(15px * var(--type-scale));
  --bar-w:26px;
  --bar-h:2px;
  --bar-gap:7px;
  --drawer-w:clamp(300px, 45vw, 420px);
  --row-h:56px;
  --faq-mark:24px;   /* the accordion's plus and minus glyph box */
  --stat-h:140px;
  --art-h:210px;
  --art-h-lg:260px;
  --icon:128px;
  --icon-lg:168px;
  --wordmark-fs:calc(19px * var(--type-scale));
  --wordmark-fs-lg:calc(72px * var(--type-scale));

  /* ---- motion: contract ----
     --motion-scale is the one dial for pace, the way --type-scale is for size
     and --density is for space. Every duration and delay below is multiplied by
     it, so the rhythm between them holds and only the speed moves. 1 is the
     original. Raise it to slow everything down. */
  --motion-scale:1.5;
  --ease:cubic-bezier(.2,.7,.2,1);
  --dur-fast:calc(.18s * var(--motion-scale));
  --dur-base:calc(.22s * var(--motion-scale));
  --dur-slow:calc(.28s * var(--motion-scale));

  /* ---- motion: scroll entry and step delays (extra) ---- */
  --dur-entry:calc(.55s * var(--motion-scale));
  --dur-entry-slow:calc(.8s * var(--motion-scale));
  --dur-step:calc(60ms * var(--motion-scale));
  --dur-stagger:calc(90ms * var(--motion-scale));
  /* FX/SCRAMBLE. Deliberately long: 1.2 * 1.5 is 1.8s, where a scramble is
     normally 500 to 800ms. The figures are four characters at most, so the
     resolve is about .45s a character and you can watch each one land. Drop it
     toward .8s if it starts to feel like the page is withholding the number. */
  --dur-scramble:calc(1.2s * var(--motion-scale));
  /* A rule drawing at the same time as its block fades cannot be seen: the line
     is 90% drawn by the time the block is 90% opaque. Hold the rules back until
     the block has arrived, then draw them. */
  /* A line only gets about 1.45s of screen time at a normal 600px/s scroll, so
     the wait plus the draw has to fit inside that or it finishes off screen.
     0.45 + 0.83 = 1.28s. The wait is what makes it readable: without it the
     line is already drawn by the time you have stopped scrolling to look. */
  --delay-rule:calc(.3s * var(--motion-scale));
  /* The house easing is heavily front loaded, which is right for a fade and
     wrong for a line: most of the travel happened in the first few frames and
     read as a snap. Rules get their own near linear curve and a longer run. */
  --ease-rule:cubic-bezier(.35,0,.25,1);
  /* FX/EXPAND. A panel opens and closes on the same curve, so it needs one that
     is symmetrical. The house --ease is heavily front loaded, which is right for
     something arriving and wrong for something that also has to leave: the
     accordion snapped open and crawled shut. This is the standard ease in out. */
  --dur-expand:calc(.3s * var(--motion-scale));
  --ease-expand:cubic-bezier(.4,0,.2,1);
  --dur-rule:calc(.8s * var(--motion-scale));
  /* FX/CURTAIN. --curtain-hold is how long the panel sits while its rule draws,
     --dur-curtain is the lift. The hero intro waits out the sum, or it would
     play behind the panel and be over by the time the page is uncovered. */
  --curtain-hold:calc(.55s * var(--motion-scale));
  --dur-curtain:calc(.6s * var(--motion-scale));
  --curtain-total:calc(var(--curtain-hold) + var(--dur-curtain));
  --rise-y:18px;
}
/* Two text treatments only: black on red, white on near black. */
.on-accent{--text:var(--accent-ink);--muted:var(--accent-ink);--rule:var(--rule-on-accent);color:var(--text)}
.on-dark{--text:var(--text-on-dark);--muted:var(--muted-on-dark);--rule:var(--rule-on-dark);color:var(--text)}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{margin:0;background:var(--paper);color:var(--text);font-family:var(--font-body);font-size:var(--step-0);line-height:var(--lh-body);text-wrap:pretty;-webkit-font-smoothing:antialiased}
h1,h2,h3{margin:0;font-family:var(--font-display);font-weight:700;text-transform:uppercase;letter-spacing:var(--track-display);line-height:var(--lh-tight)}
p{margin:0}
img{display:block;max-width:100%}
/* Buttons and doors carry their own colour pair, so the contextual link rules
   exclude them. Without :not(.btn) an accent button inside .on-dark loses to
   ".on-dark a" and renders white on red at rest, red on red on hover.
   .door is excluded for the same reason: it is an anchor, so this rule caught
   it on hover and flipped its heading and arrow to --ink while
   ".door:hover{--muted:var(--white)}" sent the paragraph to white, leaving
   black and white text on the same red panel. --ink on accent is 4.28:1, which
   the 18px arrow fails. The door keeps its own white throughout. */
a{color:var(--accent);text-decoration:none}
a:not(.btn):not(.door):hover{color:var(--ink)}
.on-dark a:not(.btn){color:var(--white)}.on-dark a:not(.btn):hover{color:var(--accent)}
.on-accent a:not(.btn){color:var(--accent-ink)}.on-accent a:not(.btn):hover{color:var(--accent-ink)}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
.on-accent :focus-visible{outline-color:var(--accent-ink)}
table{border-collapse:collapse;width:100%}

/* skip link: off screen until it takes focus */
.skip{position:absolute;left:var(--space-3);top:var(--space-3);z-index:100;transform:translateY(-200%);
  display:inline-flex;align-items:center;min-height:var(--tap-min);padding:0 var(--space-4);
  background:var(--accent);color:var(--accent-ink);border-radius:var(--radius);
  font-family:var(--font-utility);font-size:var(--fs-mono-sm);letter-spacing:var(--tracking-mono-wide);text-transform:uppercase}
.skip:focus{transform:none}

/* Off screen but still announced. Used by FX/SCRAMBLE, which parks the real
   figure here while the visible one churns through random characters, so a
   screen reader is never handed the noise. */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}

/* type roles */
.d1{font-family:var(--font-display);font-weight:700;text-transform:uppercase;letter-spacing:var(--track-display);line-height:var(--lh-tight);font-size:var(--step-3)}
.d2{font-size:var(--step-2)}
.d3{font-size:var(--step-1);line-height:1.04}
.figure{font-family:var(--font-display);font-weight:700;letter-spacing:-.03em;line-height:.9;font-size:var(--fs-figure)}
.body-sm{font-size:var(--step--1);line-height:1.5}
.mono{font-family:var(--font-utility);font-size:var(--fs-mono);letter-spacing:var(--tracking-mono);text-transform:uppercase;line-height:1.45}
.mono-sm{font-family:var(--font-utility);font-size:var(--fs-mono-sm);letter-spacing:var(--tracking-mono-wide);text-transform:uppercase;line-height:1.4}
.data{font-family:var(--font-utility);font-size:var(--fs-mono);letter-spacing:.06em;font-variant-numeric:tabular-nums}
.data-lg{font-size:var(--fs-mono-lg)}
.data-md{font-size:var(--step-0)}
.muted{color:var(--muted)}
.shell{width:100%;max-width:var(--max-width);margin:0 auto;padding-inline:var(--page-pad)}
.sec{padding-block:var(--pad-section)}

/* notch: 45deg cut, top-left and bottom-right.
   --n is the per-component knob. The angle is fixed by the polygon. */
.notch{clip-path:polygon(var(--n,var(--notch)) 0,100% 0,100% calc(100% - var(--n,var(--notch))),calc(100% - var(--n,var(--notch))) 100%,0 100%,0 var(--n,var(--notch)))}
/* The notch is a clip-path, and 11 of the 16 clipped elements are also animated:
   the doors and cards rise, the hero card rises, every button draws its border.
   Chrome promotes an animating element to its own compositor layer and drops it
   again when the animation ends, and each promotion re-rasters the 45 degree
   diagonal. That is what makes the cut corner appear to blink on and off.
   will-change keeps those elements on a stable layer so the clip is rastered
   once and stays put. The selector targets the ones that animate, and it turns
   out that is all 16: the doors, cards and panels rise, the hero card rises on
   load, and every button animates its border through child elements. Reduced
   motion sets it back to auto, because nothing moves there and a layer would be
   spent for nothing. */
.notch.rise,.notch.fx-border,.hero-card{will-change:transform}
/* The same problem, and the reason the footer kept blinking after the notches
   stopped. .notch is not the only clip on the page: the footer's stepped ledge
   and the two banded sections carry their own clip-path, and each one holds
   blocks that rise. A clipped element whose descendants animate gets promoted
   and dropped exactly like an animated one does, and every drop re-rasters the
   angled edge. These are large elements, so this costs more memory than the
   notches did, which is why it is three named selectors and not a blanket rule
   on everything with a clip-path. */
.ftr,.ledge{will-change:transform}
/* ledge: a step in the top edge, the footer's shape. The corner notch above is
   right for an object with visible sides. A full-bleed band has no sides, so
   cutting its corners just chips the page edges and reads as damage rather than
   as geometry. The ledge leaves the left and right edges flush and puts the cut
   where the band actually has an edge to show, which is why the footer was
   drawn this way in the first place.
   Runs left high, right low, stepping down as it goes right, the same direction
   as the footer, so the three seams line up down the page. */
.ledge{clip-path:polygon(0 0,calc(var(--ledge-col) - var(--ledge-cut)) 0,var(--ledge-col) var(--ledge-h),100% var(--ledge-h),100% 100%,0 100%)}
/* The same step mirrored, so it falls --ledge-col in from the right instead of
   from the left. Same height, same run, same angle, reading the other way. Two
   selectors deep so it wins over .ledge whatever order they are written in. */
.ledge.ledge-right{clip-path:polygon(0 var(--ledge-h),calc(100% - var(--ledge-col)) var(--ledge-h),calc(100% - var(--ledge-col) + var(--ledge-cut)) 0,100% 0,100% 100%,0 100%)}

/* ---------- buttons: FX/BORDER, draws itself one side at a time ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--control-gap);min-height:var(--control-h);padding:0 var(--control-pad);--n:10px;
  font-family:var(--font-utility);font-size:var(--fs-btn);font-weight:500;letter-spacing:var(--tracking-mono);text-transform:uppercase;
  border:0;border-radius:var(--radius);cursor:pointer;text-decoration:none}
.btn>span{position:relative;z-index:1}
/* The arrow is the same glyph the doors use, at the same --fs-arrow, so there is
   one arrow size on the site rather than two. At --fs-btn it was 4.7px of ink
   against 10.2px of cap and read as a tick rather than an arrow.
   It also sat low, and that is the font rather than the layout: measured in
   JetBrains Mono the arrow's ink centres .278em above the baseline where a
   capital centres .368em, so it hung about 1.2px under the label.
   line-height:1 pins the span's box to its own font size. That puts the ink
   .082em below the button's centre line whatever the arrow is sized at, so the
   nudge is one figure in em and holds if --fs-arrow ever moves. */
.btn-arrow{font-size:var(--fs-arrow);line-height:1;transform:translateY(-.082em)}
.btn-accent{background:var(--accent);color:var(--accent-ink)}
.btn-dark{background:var(--surface);color:var(--white)}
.btn-outline{background:transparent;color:inherit;box-shadow:inset 0 0 0 var(--control-edge) currentColor}
.btn-lg{min-height:var(--control-h-lg);padding:0 var(--control-pad-lg);font-size:var(--fs-mono);--n:12px}
/* FX/BORDER — ACTIONS · Quiet · HOVER.
   Carried by .fx-border, so anything that behaves like an action runs the same
   animation: the six .btn, the menu toggle, the mobile call bar and the three
   doors. The edge takes its colour from the element, so a variant only has to
   get its colour pair right. */
.fx-border{position:relative;overflow:hidden}
.fx-border .e{position:absolute;background:currentColor;transform:scale(0);transition:transform var(--dur-fast) var(--ease);z-index:1}
.fx-border .e-t{left:0;top:0;width:100%;height:var(--control-edge);transform-origin:left;transform:scaleX(0)}
.fx-border .e-r{right:0;top:0;width:var(--control-edge);height:100%;transform-origin:top;transform:scaleY(0)}
.fx-border .e-b{left:0;bottom:0;width:100%;height:var(--control-edge);transform-origin:right;transform:scaleX(0)}
.fx-border .e-l{left:0;top:0;width:var(--control-edge);height:100%;transform-origin:bottom;transform:scaleY(0)}
.fx-border:hover .e-t,.fx-border:focus-visible .e-t{transform:scaleX(1);transition-delay:0ms}
.fx-border:hover .e-r,.fx-border:focus-visible .e-r{transform:scaleY(1);transition-delay:var(--dur-step)}
.fx-border:hover .e-b,.fx-border:focus-visible .e-b{transform:scaleX(1);transition-delay:calc(var(--dur-step) * 2)}
.fx-border:hover .e-l,.fx-border:focus-visible .e-l{transform:scaleY(1);transition-delay:calc(var(--dur-step) * 3)}
/* Every live button now draws its edge in currentColor: white on .btn-dark,
   black on .btn-accent. Nothing needs a colour of its own.
   .btn-outline is kept as a defined variant with no consumer on this page. It
   carries a white hairline at rest, so a white edge drawing over it would be
   invisible, and it draws in accent instead. Keep this rule with the variant.
   Delete both together or neither. */
.btn-outline .e{background:var(--accent)}

/* ---------- 1 header ---------- */
.hdr{position:sticky;top:0;z-index:80;background:var(--paper);border-bottom:var(--border) solid var(--rule-on-light)}
.hdr-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-6);min-height:var(--hdr-h);padding-inline:clamp(16px,2vw,28px)}
.wordmark{font-family:var(--font-display);font-weight:700;text-transform:uppercase;letter-spacing:-.02em;line-height:1;font-size:var(--wordmark-fs);color:var(--ink)}
.wordmark-link{display:inline-flex;align-items:center;min-height:var(--tap-min)}
.hdr-nav{display:flex;align-items:center;gap:var(--space-6)}
.hdr-actions{display:flex;align-items:center;gap:var(--space-3)}
/* FX/SWEEP: underline wipes in from the left, out to the right.
   transform-origin flips left on enter, right on leave, so it never reverses.
   The rule is offset from the bottom of the 44px tap box, not from the text, so
   the gap under the word is what is left after the box and the line box are
   centred. At --space-3 the rule cleared the baseline by about 2px and read as
   part of the letterforms. --space-2 puts it about 7px clear, a little under
   the cap height, which is the gap that reads as an underline. */
.nav-link{position:relative;display:inline-flex;align-items:center;min-height:var(--tap-min);color:var(--ink)}
.nav-link::after{content:"";position:absolute;left:0;right:0;bottom:var(--space-2);height:var(--border);background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform var(--dur-base) var(--ease)}
.nav-link:hover::after,.nav-link:focus-visible::after{transform:scaleX(1);transform-origin:left}
/* The toggle is two hairlines and nothing else: no plate, no notch, no drawn
   edge. The 44px tap target comes from padding, so the lines can stay small
   without the target shrinking with them. On open they cross into an X. */
.hdr-toggle{display:none;align-items:center;justify-content:center;
  width:var(--tap-min);height:var(--tap-min);margin-right:calc(var(--space-3) * -1);
  padding:0;background:none;border:0;color:var(--ink);cursor:pointer}
.hdr-bars{display:grid;gap:var(--bar-gap);width:var(--bar-w)}
.hdr-toggle i{display:block;width:100%;height:var(--bar-h);background:currentColor;
  transition:transform var(--dur-base) var(--ease)}
.hdr-toggle[aria-expanded="true"] i:first-child{transform:translateY(calc((var(--bar-gap) + var(--bar-h)) / 2)) rotate(45deg)}
.hdr-toggle[aria-expanded="true"] i:last-child{transform:translateY(calc((var(--bar-gap) + var(--bar-h)) / -2)) rotate(-45deg)}

/* The drawer comes in from the right over the page. It is always in the DOM so
   the slide can transition, and visibility is what takes it out of the tab
   order when closed: transitioned with a delay so it only hides once the panel
   has finished leaving. Transform and opacity only. */
.nav-scrim{position:fixed;inset:0;z-index:65;background:var(--scrim-ink);
  opacity:0;pointer-events:none;transition:opacity var(--dur-slow) var(--ease)}
.nav-scrim[data-open="true"]{opacity:1;pointer-events:auto}
.hdr-panel{position:fixed;top:0;right:0;bottom:0;z-index:70;
  /* The list needs 168px: 68px for the widest link and 100px of padding.
     Everything past that is empty panel, so a single percentage cannot serve
     both ends of the range. A phone drawer should own the screen, a tablet one
     should be a side panel. Fixed floor, gentle scale, hard cap: 83% of a 360px
     phone down to 41% of a 1023px tablet, off one line. */
  width:var(--drawer-w);
  display:grid;align-content:start;gap:var(--space-1);
  padding:calc(var(--hdr-h) + var(--space-6)) var(--space-8) var(--space-8);
  background:var(--surface);color:var(--white);
  --muted:var(--muted-on-dark);--rule:var(--rule-on-dark);
  overflow-y:auto;overscroll-behavior:contain;
  visibility:hidden;transform:translateX(100%);
  transition:transform var(--dur-slow) var(--ease),visibility 0s linear var(--dur-slow)}
.hdr-panel:focus{outline:none}
.hdr-panel[data-open="true"]{visibility:visible;transform:none;
  transition:transform var(--dur-slow) var(--ease),visibility 0s}
.hdr-panel a:not(.btn){display:flex;align-items:center;min-height:var(--row-h);
  border-bottom:var(--border) solid var(--rule-on-dark);color:var(--white)}

/* ---------- FX/CURTAIN — PAGE · Signature · LOAD ----------
   A preloader is a curtain, and the curtain is Signature tier. .agent asks for
   four Quiet effects and no signature, so this is a deliberate exception,
   recorded in md/README.md rather than left to be discovered later.
   Pure CSS. No class is toggled and no script is involved, so there is no state
   in which a failed module leaves the page covered: the lift is an animation
   with both fill, which runs to its end, and an engine that cannot animate at
   all never paints the cover in the first place. Transform only.
   Reduced motion removes it outright rather than showing a panel that sits and
   then jumps away. */
@keyframes curtain-lift{from{transform:translateY(0)}to{transform:translateY(-101%)}}
.curtain{position:fixed;inset:0;z-index:200;pointer-events:none;background:var(--ink);
  display:grid;place-content:center;justify-items:center;gap:var(--space-5);
  animation:curtain-lift var(--dur-curtain) var(--ease) var(--curtain-hold) both}
.curtain-mark{font-family:var(--font-display);font-weight:700;text-transform:uppercase;
  letter-spacing:-.02em;line-height:1;font-size:var(--step-1);color:var(--white)}
.curtain-rule{width:min(260px,42vw);height:var(--border);background:var(--accent);
  transform-origin:left;animation:rule-draw-x var(--curtain-hold) var(--ease-rule) both}
.curtain-note{color:var(--muted-on-dark)}

/* ---------- 2 hero ----------
   Three rows: mark, spacer, foot. min-height not height, so a short landscape
   viewport lets the hero grow instead of stacking the two on top of each other. */
.hero{position:relative;display:grid;grid-template-rows:auto 1fr auto;min-height:calc(100svh - (var(--hdr-h) + var(--border)));overflow:hidden;background:var(--ink);
  --crop:center 58%;--mark-top:clamp(40px,9svh,120px);--mark-fs:var(--step-4)}
.hero-layer{position:absolute;inset:0;z-index:0}
.hero-layer img{width:100%;height:100%;object-fit:cover;object-position:var(--crop);filter:grayscale(1) contrast(1.5) brightness(.74)}
.hero-scrim{position:absolute;left:0;right:0;top:0;height:58%;z-index:1;background:linear-gradient(to bottom,var(--scrim-ink),var(--scrim-clear))}
/* Hero intro. The FX/DRAW gesture on the rules and the FX/RISE gesture on the
   items, fired by load rather than by scroll: the hero is always above the fold,
   so the observers would fire immediately anyway and the result is the same.
   Written as CSS animation instead of the observer modules for two reasons. It
   needs no JavaScript, and animation-fill-mode:both means an engine that cannot
   run it still paints everything in its final state, so the hero can never be
   left blank. Transform and opacity only.
   The rules were a repeating gradient. A gradient cannot be staggered, so each
   line is now its own element. */
@keyframes hero-draw{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@keyframes hero-rise{from{opacity:0;transform:translateY(var(--rise-y))}to{opacity:1;transform:none}}
.hero-rules{position:absolute;inset:0;z-index:2}
.hero-rules span{position:absolute;top:0;bottom:0;width:var(--border);background:var(--rule-on-dark);
  transform-origin:top;animation:hero-draw var(--dur-entry-slow) var(--ease) var(--curtain-total) both}
.hero-rules span:nth-child(1){left:8.333%}
.hero-rules span:nth-child(2){left:25%;animation-delay:calc(var(--curtain-total) + var(--dur-step))}
.hero-rules span:nth-child(3){left:41.666%;animation-delay:calc(var(--curtain-total) + var(--dur-step) * 2)}
.hero-rules span:nth-child(4){left:58.333%;animation-delay:calc(var(--curtain-total) + var(--dur-step) * 3)}
.hero-rules span:nth-child(5){left:75%;animation-delay:calc(var(--curtain-total) + var(--dur-step) * 4)}
.hero-rules span:nth-child(6){left:91.666%;animation-delay:calc(var(--curtain-total) + var(--dur-step) * 5)}
/* the items follow the lines in, in reading order */
.hero-mark,.hero-copy h1,.hero-actions,.hero-card{animation:hero-rise var(--dur-entry) var(--ease) both}
.hero-mark{animation-delay:calc(var(--curtain-total) + var(--dur-stagger) * 2)}
.hero-copy h1{animation-delay:calc(var(--curtain-total) + var(--dur-stagger) * 3)}
.hero-actions{animation-delay:calc(var(--curtain-total) + var(--dur-stagger) * 4)}
.hero-card{animation-delay:calc(var(--curtain-total) + var(--dur-stagger) * 5)}
.hero-mark{position:relative;grid-row:1;z-index:3;display:flex;justify-content:center;padding-top:var(--mark-top);padding-inline:clamp(16px,3vw,48px);pointer-events:none}
.hero-mark span{font-family:var(--font-display);font-weight:700;text-transform:uppercase;letter-spacing:-.03em;line-height:.8;font-size:var(--mark-fs);color:var(--white);white-space:nowrap}
.hero-foot{position:relative;grid-row:3;z-index:4;display:flex;align-items:flex-end;justify-content:space-between;gap:clamp(24px,4vw,64px);padding:clamp(20px,3.4vh,48px) var(--page-pad)}
.hero-copy{display:grid;gap:clamp(12px,1.8vh,22px)}
.hero-copy h1{color:var(--text);max-width:18ch;font-size:var(--step-2)}
.hero-actions{display:flex;flex-wrap:wrap;gap:var(--space-3)}
.hero-card{width:min(370px,40vw);background:var(--surface);color:var(--white);border-radius:var(--radius);--n:16px;--muted:var(--muted-on-dark)}
.hero-card-row{display:flex;justify-content:space-between;gap:var(--space-4);padding:var(--space-3) var(--space-5)}
.hero-card-row-head{align-items:center}
.hero-card-row+.hero-card-row{position:relative;border-top:var(--border) solid var(--rule-on-dark)}

/* ---------- 3 three doors ---------- */
.doors{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gutter)}
.door{position:relative;--edge:var(--rule-on-dark);background:var(--edge);padding:var(--border);border-radius:var(--radius);--n:22px;display:block;color:var(--white);text-decoration:none;--muted:var(--muted-on-dark);--rule:var(--rule-on-dark)}
.door-in{position:relative;isolation:isolate;background:var(--surface);border-radius:var(--radius);height:100%;display:flex;flex-direction:column}
/* Holds only the heading since the 01 / 03 counter came out, so the flex is
   idle. It is kept because this is the head slot of a component and a two item
   head is the reason it exists. Padding is the part doing work today. */
.door-head{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-4);padding:var(--pad-card)}
.door-head h2{max-width:18ch}
.door-art{flex:1 1 auto;min-height:var(--art-h);display:grid;place-items:center;padding:var(--space-7);
  background-image:repeating-linear-gradient(to right,var(--grid-line) 0 var(--border),transparent var(--border) 32px),repeating-linear-gradient(to bottom,var(--grid-line) 0 var(--border),transparent var(--border) 32px);background-position:center}
.door-art svg{width:var(--icon);height:var(--icon);color:var(--white)}
.door-foot{position:relative;border-top:var(--border) solid var(--rule-on-dark);padding:var(--pad-card);display:flex;justify-content:space-between;align-items:stretch;gap:var(--space-4)}
.door-foot p{color:var(--muted);align-self:flex-start}
.door-arrow{align-self:flex-end}
.door-arrow{flex:0 0 auto;font-size:var(--fs-arrow);line-height:1}
/* Hover: the panel goes from near black to accent. Nothing else moves.
   It is an opacity fade on an overlay rather than a background-color
   transition, because the kit animates transform and opacity only. The
   overlay sits at z-index -1 inside the card's own stacking context, so it
   paints above the card background and below everything else: the art panel's
   grid lines and the divider still draw over the red exactly as they do over
   the near black. Muted text goes to solid white for the duration, since 62%
   white over accent measures 2.15:1 and is unreadable. */
.door-in::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--accent);opacity:0;
  transition:opacity var(--dur-base) var(--ease)}
.door:hover .door-in::before{opacity:1}
.door:hover{--muted:var(--white)}
/* The three cards are equal height already, because the grid stretches them.
   What differed was the inside: a one line title or a four line paragraph moved
   each card's art panel and divider to its own height. Subgrid hands the three
   rows to every card so head, art and foot line up across all of them, at any
   width and whatever the copy does. Without subgrid the flex fallback above
   still produces correct, if unaligned, cards.
   Scoped to the three column layout, where the cards form a single row: the
   gutter is a column gap only there, so row-gap goes to 0 and the three rows
   sit flush the way the card was drawn. Below 1000px the cards wrap onto more
   than one row and need that gap back, so the flex fallback takes over. */
/* laptop and up: 1024 and above */
@media (min-width:1024px){
  @supports (grid-template-rows:subgrid){
    .doors{grid-template-rows:auto 1fr auto;row-gap:0}
    .door{display:grid;grid-template-rows:subgrid;grid-row:span 3}
    .door-in{display:grid;grid-template-rows:subgrid;grid-row:span 3}
  }
}

/* FX/RISE: staggered fade and rise on entry.
   .rise on its own is the finished page. The hidden state lives on .armed,
   which js/modules/rise.js adds before it starts observing. That ordering
   matters now the effect is on most of the page: if the module fails to load,
   or scripts are off, every block stays visible instead of the site rendering
   blank below the hero. */
/* The hairline round a card, panel or door is not a border. It is the
   element's own background showing through one pixel of padding, with the
   inner surface laid on top. --edge names that colour so the reveal can lift it
   off the element and onto a pseudo that scales down from the top: the outline
   draws itself in while the block fades up, instead of arriving whole. Only
   while armed, so the resting page, reduced motion and a failed module all keep
   the plain border. */
.rise.armed.card,.rise.armed.panel,.rise.armed.door{background:transparent}
.rise.armed.card::before,.rise.armed.panel::before,.rise.armed.door::before{
  content:"";position:absolute;inset:0;background:var(--edge);
  transform:scaleY(0);transform-origin:top;transition:transform var(--dur-entry-slow) var(--ease) var(--delay-rule)}
.rise.armed.in.card::before,.rise.armed.in.panel::before,.rise.armed.in.door::before{transform:scaleY(1)}
.rise.armed{opacity:0;transform:translateY(var(--rise-y))}
.rise.armed.in{opacity:1;transform:none;transition:opacity var(--dur-entry) var(--ease),transform var(--dur-entry) var(--ease)}

/* ---------- 4 stat block ---------- */
.stats-sec{background:var(--ink)}
.stats{display:grid;grid-template-columns:1.05fr 1fr;gap:var(--gutter)}
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--gutter)}
.card{position:relative;padding:var(--border);border-radius:var(--radius);--n:18px}
.card-in{position:relative;height:100%;padding:var(--pad-card);border-radius:var(--radius)}
.card-accent{--edge:var(--rule-on-accent);background:var(--edge)}.card-accent .card-in{background:var(--accent);color:var(--accent-ink)}
.card-surface{--edge:var(--rule-on-dark);background:var(--edge)}.card-surface .card-in{background:var(--surface);color:var(--white)}
.card-paper{--edge:var(--rule-on-light);background:var(--edge)}.card-paper .card-in{background:var(--paper);color:var(--ink)}
.stat{display:flex;flex-direction:column;justify-content:space-between;gap:var(--space-7);min-height:var(--stat-h)}
.panel{position:relative;padding:var(--border);border-radius:var(--radius);--n:26px;--edge:var(--rule-on-dark);background:var(--edge)}
.panel-in{position:relative;background:var(--surface);color:var(--white);height:100%;padding:var(--pad-panel);border-radius:var(--radius)}
.panel-in-flush{padding:0}
.stats-panel{display:grid;gap:var(--space-6);align-content:space-between;height:100%}
.stats-panel h2{max-width:18ch}

/* ---------- 5 hire ---------- */
.hire{display:grid;grid-template-columns:1fr 1.4fr;gap:clamp(32px,6vw,96px);align-items:start}
/* The head sticks while the rate table scrolls past, so the phone number and
   the full list button stay reachable the whole way down. Two conditions make
   it work and both are already here: .hire carries align-items:start, so the
   head is its own height rather than stretched to the row, and no ancestor
   sets overflow, which would kill sticky silently. The offset clears the
   sticky page header, which is --hdr-h plus its bottom hairline. It is
   released at 1000, where the two columns become one. */
.hire-head{display:grid;gap:clamp(24px,3vw,40px);align-content:start;
  position:sticky;top:calc(var(--hdr-h) + var(--border) + var(--space-7))}
.hire-head h2{max-width:22ch}
.hire-head p{max-width:52ch;color:var(--muted)}
/* The head runs on the section gap, which is right between heading, prose and
   actions and far too wide between two consecutive paragraphs. The prose gets
   its own tighter grid so the section rhythm is not spent inside a paragraph. */
.hire-copy{display:grid;gap:var(--space-5)}
/* Two actions, the same shape as .hero-actions: call, or the full list. */
.hire-actions{display:flex;flex-wrap:wrap;gap:var(--space-3)}
.spec th{position:relative;text-align:left;font-weight:400;color:var(--muted);padding:0 0 var(--space-3);border-bottom:var(--border) solid var(--rule)}
.spec td{position:relative;padding:var(--space-4) 0;border-bottom:var(--border) solid var(--rule)}
.spec th+th,.spec td+td{text-align:right;white-space:nowrap}
.spec-note{padding-top:var(--space-5);color:var(--muted)}

/* ---------- 6 reid ---------- */
.reid-sec{background:var(--black)}
.reid{display:grid;grid-template-columns:1.2fr .8fr;gap:clamp(32px,6vw,96px);align-items:start}
.reid-copy{display:grid;gap:clamp(28px,3.4vw,48px)}
.reid-copy h2{max-width:20ch}
.reid-copy p{color:var(--muted);max-width:var(--measure)}
.reid-mat{position:relative;display:flex;flex-wrap:wrap;gap:var(--space-5);padding-top:var(--space-6);border-top:var(--border) solid var(--rule)}
.reid-art{display:grid;place-items:center;min-height:var(--art-h-lg);padding:var(--space-7);
  background-image:repeating-linear-gradient(to right,var(--grid-line-soft) 0 var(--border),transparent var(--border) 32px),repeating-linear-gradient(to bottom,var(--grid-line-soft) 0 var(--border),transparent var(--border) 32px)}
.reid-art svg{width:var(--icon-lg);height:var(--icon-lg);color:var(--white)}
.reid-cap{position:relative;border-top:var(--border) solid var(--rule-on-dark);padding:var(--space-5);display:flex;justify-content:space-between;gap:var(--space-4)}

/* ---------- 7 faq ----------
   Two columns split by a single hairline. The left column is the heading alone,
   held at the top, and the accordion sets the height of the row. The divider is
   a border on the list, not a separate element, so it stretches with the grid
   for free.
   The section rule is a plain .draw with no --space-8 under it, unlike every
   other section, so the divider starts at the rule and the two make one
   continuous frame. The inset that would have been that margin is inside the
   columns instead, which is why both carry their own top padding. */
.faq{display:grid;grid-template-columns:1fr 1fr}
.faq-head{display:grid;align-content:start;padding:var(--space-8) var(--space-8) var(--space-8) 0}
.faq-head h2{max-width:16ch}
.faq-list{position:relative;padding-top:var(--space-8);border-left:var(--border) solid var(--rule)}
/* Rows are separated by their own bottom border, so the hairlines stop at the
   divider and do not run back under the heading. The section rule closes the
   top of the first row, the last row's own border closes the bottom. */
.faq-item{position:relative;border-bottom:var(--border) solid var(--rule)}
/* The row inset is horizontal only. Putting it on .faq-list instead would pull
   the hairlines in off the divider, which is the one thing holding the column
   together. */
.faq-q{list-style:none;cursor:pointer;padding:var(--space-6) var(--pad-card)}
.faq-q::-webkit-details-marker{display:none}
.faq-q h3{display:flex;align-items:center;justify-content:space-between;gap:var(--space-5)}
/* A row is a control, so it answers the pointer. Instant, like the link rules,
   rather than a transition, because this is a state and not an effect. */
.faq-q:hover{color:var(--accent)}
/* Plus when shut, minus when open, and it is one bar rotating rather than two
   glyphs swapping. Transform only, so it stays inside the kit's rule. */
.faq-mark{position:relative;flex:0 0 auto;width:var(--faq-mark);height:var(--faq-mark)}
.faq-mark::before,.faq-mark::after{content:"";position:absolute;left:0;top:calc(50% - var(--border) / 2);
  width:100%;height:var(--border);background:currentColor}
/* The mark runs on the panel's own timing, so the glyph and the answer are one
   movement rather than two things that happen to overlap. */
.faq-mark::after{transform:rotate(90deg);transition:transform var(--dur-expand) var(--ease-expand)}
[open]>.faq-q .faq-mark::after{transform:rotate(0deg)}
/* One column since the media slot came out. The measure is the page's, so the
   answer is not a full 570px line just because the column can hold one. */
.faq-a{padding:0 var(--pad-card) var(--space-7)}
.faq-a>p{color:var(--muted);max-width:var(--measure)}

/* FX/EXPAND — PANELS · Quiet · CLICK.
   A native <details> snaps. ::details-content is the box the browser already
   wraps the answer in, so the panel animates without a wrapper div and without
   a line of JavaScript, and the accordion keeps working as plain HTML.
   block-size is animated, which the kit otherwise forbids. There is no way to
   open a panel to the height of its own content with transform: scaleY would
   squash the type. FX/EXPAND is on the shelf as "zero to auto" for this reason,
   and it is the third documented exception after FX/BLUR's filter and FX/MASK's
   clip-path. Opacity rides along so the text arrives rather than being revealed
   by a moving edge.
   Three things are load bearing. interpolate-size lets 0 animate to auto at all.
   overflow keeps the answer inside the box while the box is shorter than it is.
   content-visibility has to be transitioned with allow-discrete, because the
   browser sets it to hidden when the panel closes and without that it would
   vanish on frame one and the height would animate against nothing.
   Guarded, so an engine without ::details-content gets the native instant open.
   That is the progressive enhancement floor, not a degraded state. */
@supports (interpolate-size:allow-keywords) and selector(::details-content){
  :root{interpolate-size:allow-keywords}
  .faq-item::details-content{block-size:0;opacity:0;overflow:hidden;
    transition:block-size var(--dur-expand) var(--ease-expand),
      opacity var(--dur-expand) var(--ease-expand),
      content-visibility var(--dur-expand) allow-discrete}
  .faq-item[open]::details-content{block-size:auto;opacity:1}
}

/* ---------- 8 footer ---------- */
/* .addr and .pair were written for the Find Us cards and shared with the
   footer. Find Us is gone, so they live here now, with their only consumer. */
.addr{font-style:normal;display:grid;gap:var(--space-1)}
.pair{display:grid;gap:var(--space-2)}
.pair a,.pair span.v{font-family:var(--font-utility);font-size:var(--fs-contact);letter-spacing:.02em;font-weight:500;color:inherit;display:inline-flex;align-items:center;min-height:var(--tap-min)}

/* The step now takes its height and run from the shared tokens, so tuning the
   angle once moves all three ledges together. --ftr-col stays local: in the
   footer it is also the grid column the step lines up with, which is a footer
   fact, not a shape one. --ftr-ledge still feeds .ftr-right's top padding, so
   that column keeps clearing the step on its own. */
.ftr{background:transparent;color:var(--text);--ftr-col:38%;--ftr-ledge:var(--ledge-h);--ftr-cut:var(--ledge-cut);
  clip-path:polygon(0 0,calc(var(--ftr-col) - var(--ftr-cut)) 0,var(--ftr-col) var(--ftr-ledge),100% var(--ftr-ledge),100% 100%,0 100%)}
.ftr-fill{background:var(--accent)}
.ftr-grid{display:grid;grid-template-columns:var(--ftr-col) 1fr}
.ftr-left{position:relative;border-right:var(--border) solid var(--rule-on-accent);padding:calc(var(--ftr-cut) + var(--space-7)) clamp(24px,3vw,48px) var(--space-9) clamp(20px,3vw,48px);display:grid;gap:var(--space-7);align-content:start}
.ftr-left .wordmark{font-size:var(--wordmark-fs-lg);color:var(--text)}
.ftr-contact{display:grid;gap:var(--space-5)}
/* FX/SWEEP on the two phone numbers. Same rule as the footer nav: in from the
   left, out to the right, origin flipping so it never reverses. The link box is
   44px for the tap target while the number is only about 17px, so the hairline
   is lifted off the bottom to sit under the digits rather than under the box.
   Still scoped to .ftr-contact. .pair no longer has a second consumer now that
   Find Us has gone, but the scope is what stops the sweep arriving on the next
   place .pair is used. */
/* .pair is a grid, so the link would stretch the full column and the sweep
   would run way past the digits. Shrink it to its text. */
.ftr-contact .pair a{position:relative;justify-self:start}
.ftr-contact .pair a::after{content:"";position:absolute;left:0;right:0;bottom:var(--space-2);height:var(--border);background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform var(--dur-slow) var(--ease)}
.ftr-contact .pair a:hover::after,.ftr-contact .pair a:focus-visible::after{transform:scaleX(1);transform-origin:left}
.ftr-right{padding:calc(var(--ftr-ledge) + var(--space-7)) clamp(20px,3vw,48px) var(--space-8) clamp(28px,4vw,64px)}
.ftr-nav{display:grid}
/* FX/SWEEP again: links stay pure black, the hover reads on the row hairline */
.ftr-nav a{position:relative;display:flex;align-items:center;min-height:var(--row-h);color:var(--text);
  border-bottom:var(--border) solid var(--rule-on-accent)}
.ftr-nav a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:var(--border);background:currentColor;
  transform:scaleX(0);transform-origin:right;transition:transform var(--dur-slow) var(--ease)}
.ftr-nav a:hover::after,.ftr-nav a:focus-visible::after{transform:scaleX(1);transform-origin:left}
.ftr-base{position:relative;display:flex;flex-wrap:wrap;justify-content:space-between;gap:var(--space-4);padding:var(--space-6) clamp(20px,3vw,48px) var(--space-7);border-top:var(--border) solid var(--rule-on-accent)}
.ftr-base a{color:var(--text);text-decoration:underline;text-underline-offset:3px}
/* The credit sits on one text line. Growing the box to 44px would push the
   footer down, so the hit area is expanded instead and the line stays put. */
.ftr-credit{position:relative}
.ftr-credit::after{content:"";position:absolute;left:0;right:0;top:50%;height:var(--tap-min);transform:translateY(-50%)}

/* ---------- 9 sticky call bar ---------- */
.callbar{display:none}


/* ---------- every hairline draws itself in ----------
   A border cannot be scaled, so js/modules/rules.js hands each line a pseudo
   laid exactly over the gap the border still occupies, tagged with the side it
   sits on and the colour it was. The border keeps its pixel, so nothing moves.
   Every rule below needs .rule, which only that module adds, so the resting
   page, reduced motion and a failed module all render ordinary borders.
   Each line is observed on its own, so it draws as it reaches the lower edge of
   the screen rather than all of them firing when their block clips the
   viewport. --ease-rule is close to linear on purpose: the house easing is
   heavily front loaded, which made a drawing line look like it simply appeared.
   Named FX/DRAW, not FX/SWEEP: SWEEP is the hover underline on links. */
@keyframes rule-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}

.rule{position:relative;border-color:transparent}
.rule::after{content:"";position:absolute;background:var(--rule-line);
  transition:transform var(--dur-rule) var(--ease-rule) var(--delay-rule)}
.rule-top::after   {left:0;right:0;top:calc(var(--border) * -1);height:var(--border);transform:scaleX(0);transform-origin:left}
.rule-bottom::after{left:0;right:0;bottom:calc(var(--border) * -1);height:var(--border);transform:scaleX(0);transform-origin:left}
.rule-right::after {top:0;bottom:0;right:calc(var(--border) * -1);width:var(--border);transform:scaleY(0);transform-origin:top}
.rule-left::after  {top:0;bottom:0;left:calc(var(--border) * -1);width:var(--border);transform:scaleY(0);transform-origin:top}
.rule.rule-in::after{transform:none}

/* Time is the wrong clock for this. A line gets about 1.45s on screen at a
   600px/s scroll and about 0.35s at 2400px/s, so any duration long enough to
   read is longer than a fast scroll leaves it visible, and the draw finishes
   somewhere above the top of the window. Measured at 2400px/s the whole table
   drew between panelTop -201 and -1210, entirely off screen.
   Where the browser supports it, the draw is driven by scroll position instead:
   progress is where the line sits in the window, not how long ago it arrived,
   so it reads the same whether you creep or throw the page. The range starts at
   8% rather than 0 so a line holds briefly after appearing, which is the delay,
   expressed as distance instead of time.
   Firefox has no view() yet and keeps the timed version above. .hdr is sticky
   and never leaves the window, so it stays timed too. */
@keyframes rule-draw-x{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@keyframes rule-draw-y{from{transform:scaleY(0)}to{transform:scaleY(1)}}
@supports (animation-timeline:view()){
  .rule:not(.hdr):not(.hero-card-row):not(.door-foot):not(.ftr-base):not(.ftr-left)::after{
    transition:none;
    animation:rule-draw-x auto linear both;
    animation-timeline:view();
    animation-range:cover 8% cover 58%}
  .rule-right:not(.hdr):not(.hero-card-row):not(.door-foot):not(.ftr-base):not(.ftr-left)::after,
  .rule-left:not(.hdr):not(.hero-card-row):not(.door-foot):not(.ftr-base):not(.ftr-left)::after{animation-name:rule-draw-y}
  /* Two cannot use a view timeline and stay on the timed path above.
     .door-foot sits inside .door, which .fx-border gives overflow:hidden, and
     that makes the door itself the scrollport: view() then measures the line
     against a box that never scrolls and it sticks part drawn, measured at
     0.75. .ftr-base is the last element on the page, so it never completes a
     pass through the window and caps out, measured at 0.08. */
}


/* ---------- FX/DRAW: section hairlines draw across on scroll ---------- */
.draw{border:0;height:var(--border);background:var(--rule);margin:0}
.draw.armed{transform:scaleX(0);transform-origin:left;transition:transform var(--dur-entry-slow) var(--ease)}
.draw.armed.in{transform:scaleX(1)}
.draw-lead{margin-bottom:var(--space-8)}

/* tablet and below: up to 1023 */
@media (max-width:1023px){
  .stats,.hire,.reid{grid-template-columns:1fr}
  /* One column now, so the head sits above the table rather than beside it and
     there is nothing left for it to stick against. */
  .hire-head{position:static}
  /* One per row, not two then a widow. Three cards in a two column grid leave
     the third alone on its own line at double the width of the two above it,
     which reads as a mistake rather than as a set of three. */
  .doors{grid-template-columns:1fr}
  /* Stacked, so the divider has nothing left to divide. It goes, and the list
     is closed at the top by the heading's own bottom padding instead. The
     pseudo has to go with it or a stray 1px would be left drawing down the
     left edge of the page. */
  .faq{grid-template-columns:1fr}
  .faq-head{padding-right:0}
  .faq-list{border-left:0;padding-top:0}
  .faq-list.rule::after{content:none}
  .faq-q,.faq-a{padding-inline:0}
  .hero{--crop:60% 60%}
  /* three lines instead of six, at the same 8.333 / 41.666 / 75 positions */
  .hero-rules span:nth-child(even){display:none}
  .hero-foot{flex-direction:column;align-items:stretch;gap:var(--space-5)}
  /* The hours card goes from here down, not on phones only. Once the hero foot
     stacks, the card no longer sits beside the headline and the two buttons, it
     sits under them, and the first screen is for those two things.
     It carries the same cost the phone band already carried, now one band
     wider: this is the last place opening hours are readable, so nothing below
     1024 shows them at all. That is recorded in md/README.md and it is worth
     fixing before the pitch rather than after. The postcode goes with it and is
     still in the footer. */
  .hero-card{display:none}
}
/* Also tablet and below. Was its own 860 breakpoint, which left an 861 to
   1000 band running a full desktop header over single column content. Folded
   into the tablet band so nav becomes a menu and the footer stacks together. */
@media (max-width:1023px){
  .hdr-nav{display:none}
  .hdr-toggle{display:inline-flex}
  .hdr-cta{display:none}
  .ftr-grid{grid-template-columns:1fr}
  /* The ledge runs from here all the way down, so the step sits above the
     footer on every band, matching the two section ledges which were never
     switched off. Below desktop it is decorative rather than structural: with
     the grid stacked to one column there is no longer a column edge for it to
     line up with, which is why it used to be dropped.
     The clearance moves with it, and this one line is what makes the whole
     thing work. On desktop the left column is the tall side of the step and
     only has to clear --ftr-cut. Stacked, it spans the full width and sits
     under the low side too, so it has to clear the whole --ftr-ledge. Without
     it the wordmark's top few pixels are sliced off from about 38% across,
     which reads as a rendering fault rather than as a cut.
     It is not free. --ledge-h bottoms out at its 44px floor on a phone, so the
     step is 44px deep across a 148px run at 390, which is most of the top edge
     of a very narrow footer, and the clearance puts 44px of dead space above
     the wordmark. That is the trade for having the step on every screen. To
     flatten it on phones again, put clip-path:none and a plain --space-7
     padding-top back in the phone band. */
  .ftr-left{border-right:0;border-bottom:var(--border) solid var(--rule-on-accent);
    padding:calc(var(--ftr-ledge) + var(--space-7)) var(--page-pad) var(--space-7)}
  .ftr-left{--wordmark-fs-lg:calc(clamp(40px,11vw,64px) * var(--type-scale))}
  .ftr-right{padding:var(--space-7) var(--page-pad)}
  .ftr-base{padding-inline:var(--page-pad)}
}
/* tablet only: 768 to 1023. The first rules this band has had of its own.
   Both tokens are built on a vw term, and vw is exactly what a tablet has least
   of: at 820 the section pad resolved to 8vw, 66px before --density, against
   144px at 1440. The band was not tight because a floor was clamping it, it was
   tight because it was being scaled down by a viewport that is not that small.
   Raising the two floors so they govern the whole band lifts it off the vw
   curve and holds it flat, which is what "designed, not scaled" already means
   further down at phone.
   The floors are chosen to land on the laptop values rather than near them.
   105 and 50 here against 102 and 51 at 1024, so crossing the boundary is a 3px
   settle rather than a step. Nothing else moves: --density and the --space ramp
   are untouched, so card, panel and gap spacing are as they were, and only the
   section block padding and the page gutter open up. */
@media (min-width:768px) and (max-width:1023px){
  :root{
    --pad-section:calc(clamp(84px,8vw,144px) * var(--density));
    --page-pad:calc(clamp(40px,4vw,64px) * var(--density));
  }
}
/* phone: up to 767 */
@media (max-width:767px){
  /* mobile is designed, not scaled */
  /* One screen exactly, and the screen has two pieces of fixed chrome on it.
     The base height already takes away the sticky header. The call bar is
     position:fixed against the bottom of the viewport, so it covers the last
     --row-h whatever the page does, and it has to come off too or the hero runs
     underneath it and the bottom of the hero foot sits behind the bar.
     svh, not vh: vh is the tall viewport, measured as though the browser's URL
     bar were hidden, so on a phone that is showing it the hero would be one URL
     bar taller than the screen and the first thing under it would be a scroll.
     A shorter hero was tried here, to bring the box closer to the image's 1.49:1
     so cover crops less of the sides. 70svh took a phone from 36.5% of the width
     to 44.2%. It was rejected: the full screen photograph is the point, and the
     band of empty paper it opened above the call bar cost more than the extra
     picture was worth. Do not reach for it again. If more of the photograph is
     ever genuinely wanted on a phone, the answer is a second, tighter crop of
     the master served through the <picture> element, not a shorter hero. */
  .hero{min-height:calc(100svh - (var(--hdr-h) + var(--border)) - var(--row-h));
    --mark-top:clamp(28px,6svh,64px);--mark-fs:calc(clamp(38px,11vw,64px) * var(--type-scale))}
  /* The hire pair still goes one per row: those two are wider and the section
     has no fold to fight for. */
  .hire-actions>*{flex:1 1 100%}
  /* The hero pair splits the row in half and fills it, so the two of them reach
     the same page gutters as the headline above.
     flex:1 1 0 rather than a width: equal flex basis is what makes them 50/50
     independently of how long their labels are, which "01463 713737" against
     "Hire list" otherwise would not be.
     They are still stepped down from the shared control size, because at
     --fs-btn 13.5 and --control-pad 28 the phone number alone needs 192px and
     half of a 390 screen is 165px. At 11.7 and 14 it needs 143px and fits.
     --control-h stays 56px: the tap target and the height every other control
     on the site shares are both intact, and width was the only problem.
     nowrap matters more than it looks. Without it a squeezed button can drop to
     its min-content, which for "01463 713737" is the space in the middle, and
     the number wraps onto two lines inside a 56px box. With it, flex refuses to
     shrink either button below its full label: at 320, the narrowest phone worth
     designing for, the two come to 247px inside 270px of usable width, so they
     stay on one line and the split just stops being exactly even. */
  .hero-actions{gap:var(--space-2)}
  .hero-actions>*{flex:1 1 0;white-space:nowrap;--control-pad:14px;--control-gap:6px;
    --fs-btn:calc(13px * var(--type-scale))}
  /* .hero-card is already hidden from the tablet band down. */
  /* The foot goes back to the plain section padding. It used to carry an extra
     --row-h to push the content up off the call bar, which was the right fix
     while the hero was still full height and running underneath it. The height
     above takes that same --row-h off the hero itself, so keeping this as well
     would subtract the bar twice and leave a 56px gap above it. */
  .hero-foot{padding-bottom:clamp(20px,3.4vh,48px)}
  /* .doors is already one column from the tablet band down. */
  .stats-grid{grid-template-columns:1fr}
  /* "Weekly rate" held nowrap, which pushed the table's min-content past the
     column and scrolled the whole page sideways at 375. The rates themselves
     stay nowrap, only the headers wrap. */
  .spec th+th{white-space:normal}
  .door-art{--art-h:170px;--icon:96px}
  .reid-art{--art-h-lg:200px;--icon-lg:120px}
  body{padding-bottom:var(--row-h)}
  .callbar{display:block;position:fixed;left:0;right:0;bottom:0;z-index:60;background:var(--accent);border-top:var(--border) solid var(--rule-on-accent-strong)}
  .callbar a{display:flex;align-items:center;justify-content:center;gap:var(--control-gap);min-height:var(--row-h);color:var(--accent-ink);
    font-family:var(--font-utility);font-size:var(--step-0);letter-spacing:.06em;text-transform:uppercase;font-weight:500}
}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .rise,.rise.armed,.rise.armed.in{opacity:1;transform:none;transition:none}
  .hero-rules span,.hero-mark,.hero-copy h1,.hero-actions,.hero-card{animation:none}
  .draw,.draw.armed,.draw.armed.in{transform:none;transition:none}
  .hero-card-row+.hero-card-row.rule::after{animation:none}
  .curtain{display:none}
  .notch.rise,.notch.fx-border,.hero-card,.ftr,.ledge{will-change:auto}
  .hdr-toggle i,.nav-scrim{transition:none}
  .hdr-panel,.hdr-panel[data-open="true"]{transition:visibility 0s}
  .nav-link::after,.ftr-nav a::after,.ftr-contact .pair a::after{transition:none}
  /* the colour still swaps on hover, it just arrives instantly */
  .door-in::before{transition:none}
  /* the plus still becomes a minus, it just does not turn to get there */
  .faq-mark::after{transition:none}
  /* The panel still opens and closes, it just arrives at its height. */
  .faq-item::details-content{transition:none}
  .fx-border .e{display:none}
}
