/* Royse Media -- the whole stylesheet, shared by index.html and every
   generated folder page under work/.

   This was inline in index.html until the folder pages arrived. It is one file
   rather than a hero half and a document half because the hero rules are not
   self-sufficient anyway: they read --ink, --ground, --rule and --tile from
   :root, so an inline hero block would still fail completely without this file.
   Splitting would also duplicate the two media blocks at the foot, which
   interleave hero and document selectors line by line.

   First paint is unaffected. The invariant the reveal rests on -- that the
   canvas has drawn its rest frame BEFORE .mark--live is added -- is enforced in
   hero-gpu.js and hero.js, not here. A render-blocking <link> is applied before
   any deferred or module script runs, which is what both hero scripts are. */

  :root{
    /* Dark ground. --ink is the foreground, not the background: the palette
       inverted wholesale, so the names kept their ROLES rather than values. */
    --ground:#0a0a0a;
    --ink:#f4f2ef;        /* warm off-white, 3 degrees off neutral */
    --ink-dim:#c9c6c2;    /* body copy */
    --muted:#8f8f8f;      /* 6.1:1 on --ground; the old #6b6b6b was 3.8:1 */
    --rule:#2a2a2a;
    --tile:#171717;       /* image placeholder, must not flash white */
    /* The footer wordmark is the SAME colour as the ground. It is not drawn, it
       is lit: a soft shadow hugging one edge of every stroke and a soft
       highlight hugging the other, so the page looks pressed rather than
       printed. --ghost-depth is the offset between the two, and going negative
       flips which edge the light comes from. */
    --ghost:13,13,13;
    --ghost-shade:0,0,0;
    --ghost-shade-a:.95;
    --ghost-lite:138,131,119;
    --ghost-lite-a:.82;
    --ghost-depth:5px;
    --ghost-blur:2.5px;
    --maxw:660px;
    --wide:1080px;

    /* Scroll reveal. reveal.js flips .is-in; --dur and --ease are the shared
       feel, overridden per variant below. */
    --ease:cubic-bezier(.22,1,.36,1);
    --dur:.55s;

    /* Flow diagram connector lines (.packs #packages), lit by scrub.js via
       --flow-p. --flow-line is the resting, unlit colour; --flow-hi is the
       lit colour a line settles into once the light has passed; --flow-edge
       is the brighter leading edge riding just ahead of it. */
    --flow-line:var(--rule);
    --flow-hi:rgba(244,242,239,.42);
    --flow-edge:rgba(244,242,239,.7);

    /* Shine. shine.js writes --shine-p per element and reads only --shine-start and
       --shine-end, which are geometry. Everything below is look, and the dev
       bench at dev/shine.html drives all of it live. */
    --shine-band:59%;      /* half width of the glint, as a % of the phrase */
    --shine-core:.69;      /* 0..1 of that half width that is the hard specular core */
    --shine-soft:.15;      /* 0 hard shouldered, 1 fully feathered */
    --shine-peak:.92;      /* opacity of the white layer at the core */
    --shine-bloom:38px;    /* bloom radius */
    --shine-bloom-a:.77;   /* bloom alpha */
    --shine-chroma:.43;    /* 0..1 cool/warm split in the bloom. 0 is neutral white */
    --shine-cool:186,214,255;
    --shine-warm:255,226,188;
    --shine-angle:287deg;
    --shine-start:.8;      /* viewport fraction where the sweep begins */
    --shine-end:.08;       /* and where it completes, above the start line */
    --shine-hold:.44;      /* whiteness left behind once the glint has passed */

    /* CTA glow. Same deal: tuned from the bench, nothing below hardcodes it.
       --glow-travel 0 leaves a pure pulse with no drift. */
    --glow-color:255,255,255;
    --glow-a:.22;
    --glow-size:2.2;       /* glow radius as a multiple of the button height */
    --glow-blur:28px;
    --glow-period:5s;
    --glow-travel:.14;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;}
  html{scroll-behavior:smooth;}
  body{
    background:var(--ground);
    color:var(--ink-dim);
    font-family:"Lexend",system-ui,-apple-system,sans-serif;
    font-weight:300;
    line-height:1.7;
    -webkit-font-smoothing:antialiased;
    text-rendering:optimizeLegibility;
    display:flex;
    flex-direction:column;
    min-height:100vh;
  }
  .vh{
    position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }
  a{color:inherit;}
  :focus-visible{outline:2px solid var(--ink);outline-offset:3px;}

  /* ---- Hero / wordmark reveal ---- */
  .hero{
    position:relative;
    min-height:100svh;
    display:flex;flex-direction:column;
    align-items:center;justify-content:center;
    padding:clamp(1rem,4vh,2rem) 1.5rem clamp(4rem,9vh,6rem);
    overflow:hidden;
  }
  .hero-meta{
    position:absolute;top:0;left:0;right:0;
    display:flex;justify-content:space-between;
    padding:clamp(1rem,3vh,1.6rem) clamp(1rem,3vw,1.8rem);
    font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);pointer-events:none;
  }
  .hero-stage{width:100%;}
  /* The .mark box IS the alignment contract surface: the injected reveal
     canvas is pinned to exactly this box, and the Blender render frames the
     same viewBox, so the two layers cannot drift. Never give it padding. */
  .mark{
    /* hero.js reads --mark-layout to know which artwork and which render to
       use, so the breakpoint below stays the single source of truth. */
    --mark-layout:wide;
    position:relative;margin:0 auto;
    width:min(92vw, calc((100svh - 17rem) * 4.6404494), 1180px);
    aspect-ratio:826/178;
  }
  .mark-flat{
    position:absolute;inset:0;width:100%;height:100%;display:block;
  }
  /* logo.svg has baked black fills, so on a dark ground it is masked rather
     than drawn: the div supplies the colour, the SVG supplies the shape.
     background-size:100% 100% is only safe because the box is locked to the
     exact source aspect ratio above. */
  .mark-flat{
    background:var(--ink);
    -webkit-mask:url(logo.svg) no-repeat center/100% 100%;
            mask:url(logo.svg) no-repeat center/100% 100%;
  }
  /* The reveal canvas is deliberately LARGER than the alignment box. The
     bloom and the star rays travel well past the glyphs, and a canvas that
     stopped at the mark bounding box would slice the halo off square at that
     edge. --bleed is written by hero-gpu.js in CSS pixels, matching the
     padding it left around the mark inside the render buffer, so the glyph
     still lands exactly on the box.

     Sized explicitly rather than with inset + width:auto. A <canvas> is a
     REPLACED element, so width:auto resolves to its INTRINSIC size -- the
     300x150 default until a backing store is assigned -- and `right`/`bottom`
     are then dropped as over-constrained. The canvas rendered 300x150 in the
     top-left corner of the hero, which looks like the shader failing rather
     than like a CSS bug. */
  .mark-reveal{
    position:absolute;
    top:calc(-1 * var(--bleed, 0px));
    left:calc(-1 * var(--bleed, 0px));
    width:calc(100% + 2 * var(--bleed, 0px));
    height:calc(100% + 2 * var(--bleed, 0px));
    opacity:0;pointer-events:none;
  }
  /* THE HANDOFF IS A HARD CUT, AND THAT IS THE WHOLE POINT.
     Deliberately NO transition. With the ink field at zero -- which is what it
     holds until the pointer first moves -- look.wgsl's reveal mask is 0, the
     entire chrome and bloom chain multiplies out, and the canvas renders the
     flat wordmark in --ink over --ground: the same pixels .mark-flat is already
     showing. So the two rules below swap two identical images in one style
     recalc, and there is nothing to fade.

     A fade is what caused the reported flash. .mark-flat has no transition, so
     it SNAPPED to 0 while .mark-reveal ramped up over 120ms -- seven frames in
     which the wordmark was carried by a partly transparent layer and nothing
     else, dropping ~43 L* at the midpoint. It read as the text disappearing and
     flashing back in. Cross-fading both layers only halves that dip
     (R(t) = C - t(1-t)(C-G), worst at t=0.5); removing the fade removes it.

     The invariant this rests on -- that the canvas has ALREADY drawn its rest
     frame before .mark--live is added -- is enforced in hero-gpu.js and
     hero.js, not here. Neither may add the class until one frame is on screen. */
  .mark--live .mark-reveal{opacity:1;}
  .mark--live .mark-flat{opacity:0;}
  .hero-tagline{
    margin:clamp(1.6rem,4.5vh,2.6rem) auto 0;text-align:center;
    font-weight:400;font-size:.78rem;letter-spacing:.28em;line-height:2;
    text-transform:uppercase;color:var(--muted);
    max-width:min(92vw,42rem);
  }
  .hero-tagline span{white-space:nowrap;}
  .hero-tagline span ~ span::before{content:"\00b7";margin:0 .5em 0 .3em;opacity:.7;}
  @media (max-width:640px){
    .hero-tagline span{display:block;}
    .hero-tagline span ~ span::before{content:none;}
  }
  .hero-cue{
    position:absolute;left:50%;bottom:clamp(1.2rem,4vh,2.2rem);
    transform:translateX(-50%);
    width:22px;height:38px;border:1px solid var(--rule);border-radius:12px;
  }
  .hero-cue span{
    position:absolute;left:50%;top:8px;width:2px;height:6px;
    background:var(--muted);border-radius:1px;
    animation:cue 1.9s ease-in-out infinite;
  }
  @keyframes cue{
    0%,100%{transform:translate(-50%,0);opacity:0}
    15%{opacity:1}
    70%{transform:translate(-50%,15px);opacity:0}
  }
  /* The hero's furniture sits ABOVE the reveal canvas.
     On the WebGPU path that canvas is fully opaque -- look.wgsl composites the
     material over --ground and returns alpha 1, so the whole padded rect is
     painted, not just the glyphs. That rect overhangs the .mark box by --bleed
     on every side, which at the wide layout is ~120 CSS px, and a positioned
     element paints above the static content of its ancestors. The tagline sits
     about 42px under the mark, so it was inside the bleed: present on first
     paint and covered the moment the canvas went live. The meta line and the
     scroll cue are in the same danger on a short viewport.
     Ordering the wordmark's glow BEHIND them is also what it should do -- the
     halo belongs to the mark, not to the page. */
  .hero-tagline{position:relative;}
  .hero-meta,.hero-tagline,.hero-cue{z-index:1;}

  /* ---- Section nav ---- */
  /* A slim bar that is not there until the hero is behind you. It is fixed, so
     it cannot take part in the document's one rail; .sitenav-inner does that
     instead, with the same max-width and side padding every section uses.

     visibility, not just the transform: an off-screen bar is still in the tab
     order, and a keyboard reaching it would scroll the page to nothing. The
     transition delays visibility until the slide is over on the way out, and
     drops the delay on the way in. */
  .sitenav{
    position:fixed;top:0;left:0;right:0;z-index:40;
    height:3rem;
    background:color-mix(in srgb,var(--ground) 72%,transparent);
    backdrop-filter:blur(14px) saturate(1.2);
    -webkit-backdrop-filter:blur(14px) saturate(1.2);
    border-bottom:1px solid var(--rule);
    transform:translateY(-100%);visibility:hidden;
    transition:transform .15s ease,visibility 0s linear .15s;
  }
  @supports not (backdrop-filter:blur(1px)){
    .sitenav{background:var(--ground);}
  }
  .sitenav.is-on{
    transform:none;visibility:visible;transition:transform .15s ease;
  }
  .sitenav-inner{
    height:100%;max-width:var(--wide);margin:0 auto;padding:0 1.5rem;
    display:flex;align-items:center;justify-content:space-between;gap:1.5rem;
  }
  .sitenav a{
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);text-decoration:none;white-space:nowrap;
    transition:color .25s ease;
  }
  .sitenav a:hover{color:var(--ink);}
  .sitenav-home{flex:none;}
  .sitenav-links{
    list-style:none;margin:0;padding:0;min-width:0;
    display:flex;align-items:center;gap:1.5rem;
  }
  .sitenav-links li{flex:none;}
  /* The section you are in, in the primary ink. nav.js owns the attribute. */
  .sitenav-links a[aria-current]{color:var(--ink);}
  @media (max-width:719px){
    /* No room for the wordmark and six labels, so the labels win and the row
       scrolls sideways inside the bar. overflow-x on the list, never on the
       body: the page itself must still have nothing to scroll horizontally. */
    .sitenav-home{display:none;}
    .sitenav-inner{padding:0 1rem;}
    .sitenav-links{
      flex:1;gap:1.25rem;overflow-x:auto;overscroll-behavior-x:contain;
      scrollbar-width:none;scroll-snap-type:x proximity;
    }
    .sitenav-links::-webkit-scrollbar{display:none;}
    .sitenav-links li{scroll-snap-align:start;}
  }

  /* ---- Section furniture ---- */
  /* Every anchor target clears the fixed bar rather than landing under it. */
  header[id],main[id],section[id],footer[id]{scroll-margin-top:4rem;}
  main{
    flex:1;width:100%;max-width:var(--wide);margin:0 auto;
    padding:clamp(3.5rem,10vh,7rem) 1.5rem 3rem;text-align:left;
  }
  .eyebrow{
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);margin:0;
  }
  /* Section numbers, as an attribute rather than in the text. Renumbering is
     then one character per section, the label stays the label, and a section
     with nothing to number (the hero, the footer, the folder pages) simply has
     no attribute. Dimmer than the words it prefixes, because it is a position
     in the run, not a word to read. */
  .eyebrow[data-n]::before{content:attr(data-n) " / ";opacity:.62;}
  .section-title{
    font-family:"Playfair Display",Georgia,serif;font-weight:600;
    font-size:clamp(1.5rem,4.2vw,2.35rem);line-height:1.25;
    letter-spacing:.01em;margin:.7rem 0 0;color:var(--ink);
    text-wrap:balance;
  }
  /* Section furniture is eyebrow / title / rule, in that order, in every
     section. The rule is the one that used to vary -- centred here, an
     ::after dash there, absent in the process block. */
  .rule{
    width:64px;height:1px;background:var(--rule);border:0;margin:2.25rem 0;
  }
  .blurb{
    font-size:clamp(1rem,2.4vw,1.075rem);color:var(--ink-dim);
    margin:0;max-width:58ch;
  }
  .blurb + .blurb{margin-top:1.1rem;}

  /* ---- Shine ---- */
  /* Registering --shine-p keeps it a real number inside calc(). An unregistered
     custom property used in a gradient stop is dropped by Safari, and
     inherits:true matters because shine.js sets the value on the span while
     the ::after is what consumes it. */
  @property --shine-p{syntax:"<number>";inherits:true;initial-value:0;}

  /* Atomic inline box on purpose. A wrapped inline box is fragmented into one
     box per line and the mask is then applied to each fragment, so the glint
     would appear twice, once per half of the phrase. */
  .shine{position:relative;display:inline-block;white-space:nowrap;}
  .shine::after{
    content:attr(data-text) / "";  /* alt text form: keeps the copy out of the a11y tree */
    position:absolute;pointer-events:none;
    color:#fff;                    /* set, not inherited: the heading is --ink */
    opacity:var(--shine-peak);
    /* A mask clips to the border box, so without a bleed the bloom would end in
       a straight line just above the caps. The bleed is cancelled by an equal
       padding, which puts the glyphs back exactly over the --ink text beneath.
       It does mean --shine-band is a percentage of the phrase PLUS the bleed,
       so the glint reads a little wider than the token literally says. */
    --shine-bleed:calc(var(--shine-bloom) * 1.8);
    inset:calc(var(--shine-bleed) * -1);padding:var(--shine-bleed);
    /* Chrome, not a soft wash. Four shadows: a tight white specular, a cool
       shadow thrown one way and a warm one the other (the chromatic split is
       what sells polished metal over a torch behind the letters), and a wide
       dim halo carrying the bloom. All four scale off the same two tokens. */
    text-shadow:
      0 0 calc(var(--shine-bloom) * .3) rgba(255,255,255,var(--shine-bloom-a)),
      calc(var(--shine-bloom) * -.1) 0 calc(var(--shine-bloom) * .8)
        rgba(var(--shine-cool),calc(var(--shine-bloom-a) * var(--shine-chroma))),
      calc(var(--shine-bloom) * .1) 0 calc(var(--shine-bloom) * .8)
        rgba(var(--shine-warm),calc(var(--shine-bloom-a) * var(--shine-chroma) * .8)),
      0 0 calc(var(--shine-bloom) * 1.7) rgba(255,255,255,calc(var(--shine-bloom-a) * .45));
    /* --sx is the centre of the glint, travelling from one full band width
       before the phrase to one full band past it as --shine-p goes 0 to 1. --sc is
       the hard core, --sm the shoulder, --sa the shoulder's alpha. */
    --sx:calc((100% + var(--shine-band) * 2) * var(--shine-p) - var(--shine-band));
    --sc:calc(var(--shine-band) * var(--shine-core));
    --sm:calc((var(--shine-band) + var(--sc)) / 2);
    --sa:calc(var(--shine-hold) + (1 - var(--shine-hold)) * (1 - var(--shine-soft) * .8));
    -webkit-mask-image:linear-gradient(var(--shine-angle),
      rgba(0,0,0,var(--shine-hold)) calc(var(--sx) - var(--shine-band)),
      rgba(0,0,0,var(--sa)) calc(var(--sx) - var(--sm)),
      #000 calc(var(--sx) - var(--sc)),
      #000 calc(var(--sx) + var(--sc)),
      rgba(0,0,0,var(--sa)) calc(var(--sx) + var(--sm)),
      rgba(0,0,0,var(--shine-hold)) calc(var(--sx) + var(--shine-band)));
            mask-image:linear-gradient(var(--shine-angle),
      rgba(0,0,0,var(--shine-hold)) calc(var(--sx) - var(--shine-band)),
      rgba(0,0,0,var(--sa)) calc(var(--sx) - var(--sm)),
      #000 calc(var(--sx) - var(--sc)),
      #000 calc(var(--sx) + var(--sc)),
      rgba(0,0,0,var(--sa)) calc(var(--sx) + var(--sm)),
      rgba(0,0,0,var(--shine-hold)) calc(var(--sx) + var(--shine-band)));
  }
  /* .shine-off is set by shine.js, which watches the query itself, so flipping
     the emulation in devtools takes effect without a reload. It mirrors the
     reduced-motion branch below exactly, which is also what lets the dev bench
     preview that state honestly by toggling the class. */
  .shine-off .shine::after{display:none;}
  .shine-off .pill--glow::before{animation:none;transition:none;}
  .offer{margin:0;}
  /* margin:0, not 0 auto: the head has to start on the rail, not sit centred
     in the wide column. The whole document is one rail, and the grid below has
     to line up under the first character of this. */
  .offer-head{max-width:62ch;margin:0;}
  /* The skyline sits beside the line it illustrates rather than under a
     heading of its own. Text first in the source, so it is also what comes
     first when the two stack. */
  .local{
    display:grid;grid-template-columns:1fr;align-items:center;
    gap:clamp(1.25rem,3vw,2.25rem);
    margin:clamp(2.25rem,6vh,3.25rem) 0 clamp(.5rem,2vh,1rem);
  }
  @media (min-width:760px){
    .local{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);}
  }
  .local-line{
    margin:0;max-width:32ch;
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.05rem,2.6vw,1.28rem);line-height:1.4;
    color:var(--ink);text-wrap:balance;
  }
  /* height:auto against the width/height attributes, so the box is reserved at
     the master's ratio before the file arrives and nothing below it jumps. */
  .local-shot{
    width:100%;height:auto;display:block;
    border-radius:4px;background:var(--tile);
  }
  .offer-grid{
    list-style:none;padding:0;margin:clamp(1.5rem,4vh,2.25rem) 0 0;
    display:grid;grid-template-columns:1fr;
    gap:clamp(1.75rem,3.5vw,2.5rem);text-align:left;
  }
  @media (min-width:600px){.offer-grid{grid-template-columns:repeat(2,1fr);}}
  @media (min-width:900px){.offer-grid{grid-template-columns:repeat(3,1fr);}}
  .offer-group{border-top:1px solid var(--rule);padding-top:1.1rem;}
  .offer-t{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:1.08rem;line-height:1.35;color:var(--ink);margin:0 0 .6rem;
  }
  .offer-list{list-style:none;padding:0;margin:0;}
  .offer-list li{
    font-size:.88rem;line-height:1.5;color:var(--muted);padding:.2rem 0;
  }

  /* ---- Process ---- */
  .process{width:100%;padding:clamp(3.5rem,10vh,6rem) 1.5rem;}
  .process-inner{max-width:calc(var(--wide) - 3rem);margin:0 auto;}
  .process-head{margin:0 0 clamp(2rem,5vh,3rem);}
  .steps{
    list-style:none;padding:0;margin:0;display:grid;
    grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
    gap:clamp(1.5rem,3.5vw,2.5rem);
  }
  .step{border-top:1px solid var(--rule);padding-top:1.1rem;}
  .step-n{
    font-size:.72rem;letter-spacing:.18em;color:var(--muted);
    display:block;margin-bottom:.5rem;
  }
  .step-t{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:1.15rem;color:var(--ink);margin:0 0 .45rem;
  }
  .step p{margin:0;font-size:.95rem;color:var(--ink-dim);}

  /* ---- Packages ---- */
  /* Two sections, one run. Only the first carries the full top padding: the
     second is already announced by its own eyebrow, heading and rule, and a
     second helping of section padding between them would break the run into
     two unrelated pages of the document. */
  /* overflow-x:clip, not hidden: hidden would make the section a scroll
     container. clip only stops the lens, which runs past the rail into the
     margin, from adding width to the page. */
  .packs{width:100%;padding:clamp(3.5rem,10vh,6rem) 1.5rem;overflow-x:clip;}
  .packs--next{padding-top:0;}
  .packs-inner{position:relative;isolation:isolate;max-width:calc(var(--wide) - 3rem);margin:0 auto;}
  /* isolation on .packs-inner gives the lens a stacking context of its own to
     sit at the back of, so z-index:-1 puts it behind the copy and the cards
     without dropping it behind the page. */
  /* Registering --lens-fade keeps it a real percentage inside the gradient,
     the same reason --shine-p above is registered. */
  @property --lens-fade{syntax:"<percentage>";inherits:false;initial-value:56%;}

  .packs-lens{
    /* Beside the head, its barrel just past the rail's right edge and the
       photograph's black running out into the page margin, which is why
       .packs clips on x. The bottom fade runs out just below the top of the
       diagram's first box, late enough to show the base of the barrel and no
       later: the box's corner only brushes the last few percent of the fade.
       The still is built
       with its black lifted to --ground, so where the mask leaves a little of
       the photograph at its edges there is nothing to see. */
    position:absolute;top:-1.25rem;right:-6.25rem;z-index:-1;
    width:clamp(320px,60%,620px);height:auto;
    opacity:.65;pointer-events:none;
    /* --lens-fade is scroll-linked: scrub.js writes --scrub-p (0 while the
       lens sits low in the viewport, 1 once it has climbed near the top)
       onto this img via data-scrub, and --lens-fade rides from 56% down to
       30% as it climbs. The fade band itself stays a fixed 28% tall below
       that point and ends before the image does, so the bottom edge is
       never hard. Without JS, --lens-fade falls back to the @property
       initial value, 56%. */
    --lens-fade:calc(56% - 26% * var(--scrub-p, 0));
    -webkit-mask-image:
      radial-gradient(ellipse 65% 70% at 50% 45%,#000 35%,transparent 100%),
      linear-gradient(to bottom,#000 0,#000 var(--lens-fade),transparent calc(var(--lens-fade) + 28%));
            mask-image:
      radial-gradient(ellipse 65% 70% at 50% 45%,#000 35%,transparent 100%),
      linear-gradient(to bottom,#000 0,#000 var(--lens-fade),transparent calc(var(--lens-fade) + 28%));
    -webkit-mask-composite:source-in;
            mask-composite:intersect;
  }
  .packs-head{max-width:62ch;margin:0 0 clamp(1.75rem,4.5vh,2.5rem);}
  .packs-note{
    margin:clamp(1.25rem,3.5vh,1.75rem) 0 0;max-width:68ch;
    font-size:.88rem;line-height:1.6;color:var(--muted);
  }

  /* The session diagram: one box, a stem, a crossbar, two boxes. Borders and
     three pseudo-elements, so there is no image, no icon and nothing to load,
     and it recolours with --rule like the rest of the section furniture.

     --flow-arm is the connector length and --flow-gap the space between the
     branches. The crossbar is positioned from both, so its ends stay under the
     branch centres whatever either is set to: with two equal columns and a gap
     of G, a branch centre sits at (100% - G) / 4 from the container edge.
     --flow-gap now matches the .choice grid gutter below, so the two branch
     boxes sit exactly over the two card columns.

     The four connectors (top stem, crossbar, the two stems bracketing each
     branch box) are scroll-lit in sequence rather than drawn once. --flow-p
     is scrub.js's --scrub-p, read onto .flow via data-scrub; each connector
     turns that single 0..1 into its own local --seg with a clamp() window so
     the light reaches them in order top to bottom instead of all at once. */
  @property --flow-p{syntax:"<number>";inherits:true;initial-value:0;}
  .flow{
    --flow-arm:1.75rem;
    --flow-gap:3.5rem;
    --flow-p:var(--scrub-p, 0);
    margin:clamp(1.75rem,4.5vh,2.5rem) 0 0;
    padding-bottom:var(--flow-arm);
  }
  .flow-top{position:relative;padding-bottom:var(--flow-arm);}
  .flow-top .flow-box{max-width:26rem;margin:0 auto;}
  /* The four connectors light in strict sequence as --flow-p runs 0..1:
     stem, crossbar, the stubs above the branch boxes, then the stubs below
     them. Each rule maps --flow-p onto its own window as --seg, then --t runs
     the lit front from 0 to 110% so the whole soft edge clears the far end.
     With --t at 0 every stop sits at 0 and the line is entirely unlit, which
     is what keeps the first pixel dark before a window opens. */
  .flow-top::after{
    content:"";position:absolute;left:50%;bottom:0;
    width:1px;height:var(--flow-arm);
    --seg:clamp(0,calc(var(--flow-p) / .24),1);
    --t:calc(var(--seg) * 110%);
    background:linear-gradient(to bottom,
      var(--flow-hi) 0,
      var(--flow-hi) calc(var(--t) - 10%),
      var(--flow-edge) calc(var(--t) - 5%),
      var(--flow-line) var(--t),
      var(--flow-line) 100%);
  }
  .flow-branches{
    position:relative;display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:var(--flow-gap);padding-top:var(--flow-arm);
  }
  /* Crossbar, p .27..0.66, the longest window so it fills outward slowly.
     Drawn as two half-width layers that each light from the centre outward;
     a single mirrored gradient cannot do this because its stops cross at
     the start and collapse into a lit blob. */
  .flow-branches::before{
    content:"";position:absolute;top:0;height:1px;
    left:calc(25% - var(--flow-gap) / 4);
    width:calc(50% + var(--flow-gap) / 2);
    --seg:clamp(0,calc((var(--flow-p) - .27) / .39),1);
    --t:calc(var(--seg) * 110%);
    background-image:
      linear-gradient(to left,
        var(--flow-hi) 0,
        var(--flow-hi) calc(var(--t) - 10%),
        var(--flow-edge) calc(var(--t) - 5%),
        var(--flow-line) var(--t),
        var(--flow-line) 100%),
      linear-gradient(to right,
        var(--flow-hi) 0,
        var(--flow-hi) calc(var(--t) - 10%),
        var(--flow-edge) calc(var(--t) - 5%),
        var(--flow-line) var(--t),
        var(--flow-line) 100%);
    background-size:50% 100%;
    background-repeat:no-repeat;
    background-position:left top,right top;
  }
  .flow-branch{position:relative;}
  /* Upper stubs, p .70..0.83: crossbar down into each branch box. */
  .flow-branch::before{
    content:"";position:absolute;left:50%;top:calc(var(--flow-arm) * -1);
    width:1px;height:var(--flow-arm);
    --seg:clamp(0,calc((var(--flow-p) - .70) / .13),1);
    --t:calc(var(--seg) * 110%);
    background:linear-gradient(to bottom,
      var(--flow-hi) 0,
      var(--flow-hi) calc(var(--t) - 10%),
      var(--flow-edge) calc(var(--t) - 5%),
      var(--flow-line) var(--t),
      var(--flow-line) 100%);
  }
  /* Lower stubs, p .87..1: each branch box down into the card beneath it,
     deliberately after the upper stubs have finished. */
  .flow-branch::after{
    content:"";position:absolute;left:50%;bottom:calc(var(--flow-arm) * -1);
    width:1px;height:var(--flow-arm);
    --seg:clamp(0,calc((var(--flow-p) - .87) / .13),1);
    --t:calc(var(--seg) * 110%);
    background:linear-gradient(to bottom,
      var(--flow-hi) 0,
      var(--flow-hi) calc(var(--t) - 10%),
      var(--flow-edge) calc(var(--t) - 5%),
      var(--flow-line) var(--t),
      var(--flow-line) 100%);
  }
  .flow + .choice{margin-top:0;}
  .flow-box{
    border:1px solid var(--rule);text-align:center;
    padding:clamp(1rem,2.5vw,1.4rem) 1.25rem;
  }
  .flow-t{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.05rem,2.4vw,1.2rem);line-height:1.3;
    color:var(--ink);margin:0;
  }
  .flow-s{margin:.4rem 0 0;font-size:.85rem;line-height:1.5;color:var(--muted);}
  @media (max-width:719px){
    /* Stacked, the crossbar has nothing to cross. The gap is set to the arm
       length so each branch's drop line lands exactly where the one above it
       ended, and the whole diagram is one vertical line down the middle. */
    .flow-branches{grid-template-columns:1fr;gap:var(--flow-arm);}
    .flow-branches::before{display:none;}
    .flow-top .flow-box{max-width:none;}
    .flow-branch::after{display:none;}
    .flow{padding-bottom:0;}
    .flow + .choice{margin-top:clamp(1.5rem,3.5vh,2.25rem);}
  }

  /* Package table. Hairline row rules and nothing else: no zebra, no fills,
     no icons, and the price sits in the column head rather than in a row of
     its own, directly over the numbers it buys. */
  .ptable{
    width:100%;margin:clamp(1.75rem,4.5vh,2.5rem) 0 0;
    border-collapse:collapse;table-layout:fixed;text-align:left;
  }
  .ptable-corner{width:27%;border-bottom:1px solid var(--rule);}
  .ptable thead th{
    vertical-align:bottom;font-weight:400;
    padding:0 1rem .9rem 0;border-bottom:1px solid var(--rule);
  }
  .ptable-label,.pack-label{
    display:block;font-size:.7rem;letter-spacing:.2em;
    text-transform:uppercase;color:var(--muted);
  }
  .ptable-price,.pack-price{
    display:block;margin-top:.35rem;
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.75rem,3.2vw,2.25rem);line-height:1.05;
    letter-spacing:.01em;color:var(--ink);font-variant-numeric:tabular-nums;
  }
  /* The currency mark and the "and up" plus. Playfair's dollar is a barred S
     and its plus is a hairline, and at display size both shout. A small sans
     mark raised to the cap line reads as a unit, not a word, and scales with
     whatever price it sits in. */
  .cur,.plus{
    font-family:"Lexend",system-ui,-apple-system,sans-serif;font-weight:400;
    font-size:.46em;position:relative;top:-.72em;margin-right:.08em;
    letter-spacing:0;
  }
  .plus{margin:0 0 0 .06em;}
  .ptable tbody th,.ptable tbody td{
    padding:.85rem 1rem .85rem 0;border-bottom:1px solid var(--rule);
    font-size:.9rem;line-height:1.5;vertical-align:top;
  }
  .ptable tbody th[scope="row"]{font-weight:400;color:var(--muted);}
  .ptable tbody td{color:var(--ink);}
  /* A "no" is a fact about the package, not a warning about it. Muted, so a
     scan down the column reads the numbers first. */
  .ptable td.no{color:var(--muted);}

  /* Content packages. Each package is a box in the same language as the
     diagram above: one hairline, no fill, no icons. The two ways out of a
     session are a choice rather than a ladder, so a pair sits on one row with
     a rule down the gutter and "or" breaking it at the middle. Each box
     carries its own list, so nothing has to be looked up in a footnote. */
  .pack{
    border:1px solid var(--rule);background:var(--ground);
    padding:clamp(1.25rem,2.5vw,1.75rem);margin:0;
  }
  .pack-t{margin:0;}
  .pack-desc{
    margin:.9rem 0 0;font-size:.95rem;line-height:1.55;color:var(--ink-dim);
  }
  /* The list gets a hairline over it so the price and the promise read as two
     things rather than one long block. */
  .pack-inc{
    margin:1.25rem 0 .5rem;padding-top:.9rem;border-top:1px solid var(--rule);
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);
  }
  .pack-items{
    list-style:none;margin:0;padding:0;
    font-size:.95rem;line-height:1.5;color:var(--ink);
  }
  .pack-items li+li{margin-top:.35rem;}
  /* A "no" is a fact about the package, not a warning about it. */
  .pack-items li.no{color:var(--muted);}
  .pack-line{
    margin:1rem 0 0;
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.05rem,2.4vw,1.2rem);line-height:1.3;color:var(--ink);
  }

  .choice,.pack--wide{margin-top:clamp(1.5rem,3.5vh,2.25rem);}
  .choice{display:grid;grid-template-columns:1fr;}
  /* "or" sits on the rule rather than beside it: the hairline is a ::before
     running the full height of the row, and the word carries the ground
     colour, so it punches a gap in the line at the exact centre. */
  .choice-or{
    margin:0;position:relative;
    display:flex;align-items:center;justify-content:center;
  }
  .choice-or::before{
    content:"";position:absolute;left:50%;top:0;bottom:0;
    width:1px;background:var(--rule);transform:translateX(-50%);
  }
  .choice-or span{
    position:relative;background:var(--ground);padding:.55em 0;
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);line-height:1;
  }
  .pack-cols{display:grid;grid-template-columns:1fr;gap:1.5rem;}

  @media (min-width:720px){
    /* The middle track is the gutter the rule sits in. Stretch keeps both
       boxes the same height whatever their lists do. */
    .choice{
      grid-template-columns:minmax(0,1fr) 3.5rem minmax(0,1fr);
      align-items:stretch;
    }
    .pack--wide{
      display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr);
      gap:clamp(1.5rem,4vw,3rem);
    }
    .pack-cols{grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);}
  }

  @media (max-width:719px){
    /* The gutter becomes a band between the stacked boxes, so the rule lies
       flat and "or" still breaks it at the middle. */
    .choice-or{height:3.25rem;}
    .choice-or::before{left:0;right:0;top:50%;bottom:auto;height:1px;width:auto;}
    .choice-or span{padding:0 .8em;}
    .packs-lens{display:none;}
  }

  @media (max-width:719px){
    /* One card per tier instead of a grid of four columns on a phone. The
       table becomes a single-column grid and every cell is placed by `order`,
       so column one's cells land together, then column two's, then column
       three's; tbody and tr go display:contents so the cells are siblings for
       that to work on. The row labels the head column carried come back as
       data-label on each cell. */
    .ptable{display:grid;grid-template-columns:1fr;table-layout:auto;}
    .ptable thead,.ptable tbody,.ptable tr{display:contents;}
    .ptable-corner,
    .ptable tbody th[scope="row"]{display:none;}
    .ptable thead th:nth-child(2),.ptable tbody td:nth-child(2){order:1;}
    .ptable thead th:nth-child(3),.ptable tbody td:nth-child(3){order:2;}
    .ptable thead th:nth-child(4),.ptable tbody td:nth-child(4){order:3;}
    .ptable thead th{
      padding:1.9rem 0 .75rem;border-top:1px solid var(--rule);border-bottom:0;
    }
    .ptable thead th:first-of-type{padding-top:0;border-top:0;}
    .ptable tbody td{
      display:grid;grid-template-columns:auto minmax(0,1fr);
      gap:.2rem 1.25rem;padding:.6rem 0;text-align:right;
    }
    .ptable tbody td::before{
      content:attr(data-label);color:var(--muted);text-align:left;
    }
  }

  /* The two add-ons under the website table: a name and a price on one line,
     one sentence under, on the same ruled-heading pattern as an offer group. */
  .extras{
    list-style:none;padding:0;margin:clamp(1.75rem,4.5vh,2.5rem) 0 0;
    display:grid;grid-template-columns:1fr;gap:clamp(1.5rem,3.5vw,2.5rem);
  }
  @media (min-width:720px){.extras{grid-template-columns:repeat(2,minmax(0,1fr));}}
  .extra{border-top:1px solid var(--rule);padding-top:1.1rem;}
  .extra-head{
    display:flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .7rem;margin:0;
  }
  .extra-t{
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);
  }
  .extra-price{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:1.5rem;line-height:1.1;letter-spacing:.01em;
    color:var(--ink);font-variant-numeric:tabular-nums;
  }
  /* "a month, optional" is a condition on the price, not part of it, so it
     stays on the line at label size rather than being set in Playfair too. */
  .extra-qual{font-size:.82rem;color:var(--muted);}
  .extra-copy{
    margin:.7rem 0 0;font-size:.9rem;line-height:1.55;color:var(--ink-dim);
  }
  /* ---- Portfolio / case studies ---- */
  .work{
    width:100%;max-width:var(--wide);margin:0 auto;
    padding:clamp(3rem,8vh,5rem) 1.5rem clamp(2rem,5vh,3rem);
  }
  .work-head{margin:0 0 clamp(2.5rem,6vh,3.5rem);}

  /* ---- Folder list (index) ----
     Rows on the rail, not a card grid. A grid goes ragged the moment there is
     a fourth folder; rows never do, and the left edge stays on the one rail
     the rest of the document uses. */
  .folders{display:flex;flex-direction:column;}
  .folder{
    display:block;text-decoration:none;color:inherit;
    border-top:1px solid var(--rule);
    padding:clamp(1.5rem,4vh,2.1rem) 0 clamp(1.6rem,4.5vh,2.3rem);
  }
  .folder:last-child{border-bottom:1px solid var(--rule);}
  .folder-top{display:flex;align-items:flex-start;gap:clamp(.9rem,2.5vw,1.35rem);}
  .folder-mark{
    flex:none;color:var(--ink);
    transition:transform .35s ease,color .35s ease;
  }
  .folder-sector{
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);margin:0;
  }
  .folder-title{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.2rem,3vw,1.6rem);line-height:1.25;
    margin:.35rem 0 0;color:var(--ink);
  }
  .peek{display:flex;flex-wrap:wrap;gap:3px;margin:clamp(1.1rem,3vh,1.4rem) 0 0;}
  .peek-img{
    width:44px;height:44px;object-fit:cover;display:block;
    border-radius:2px;background:var(--tile);
  }
  .peek-more{
    width:44px;height:44px;border:1px solid var(--rule);border-radius:2px;
    display:flex;align-items:center;justify-content:center;
    font-size:.64rem;letter-spacing:.04em;color:var(--muted);
  }
  .folder-foot{display:flex;flex-wrap:wrap;gap:.5rem 1.3rem;margin-top:.95rem;}
  .folder-count{
    display:inline-flex;align-items:center;gap:.45rem;
    font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);
  }
  .folder-count svg{flex:none;}
  /* The whole row is the target, so the affordance is the row lifting, not a
     chevron. Keep it to the mark and the title: the site's one loud moment is
     the wordmark, and a list of folders must not compete with it. */
  .folder:hover .folder-mark,.folder:focus-visible .folder-mark{transform:translateY(-2px);}
  .folder:hover .folder-title,.folder:focus-visible .folder-title{opacity:.72;}
  .folder:focus-visible{outline:2px solid var(--ink);outline-offset:4px;}

  /* ---- Folder page ---- */
  .masthead{
    display:flex;justify-content:space-between;align-items:baseline;
    width:100%;max-width:var(--wide);margin:0 auto;
    padding:1.4rem 1.5rem;
    font-size:.7rem;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);
  }
  .masthead-home{
    color:var(--ink);text-decoration:none;
  }
  .masthead-home:hover{opacity:.65;}
  .folder-page .work{padding-top:clamp(1.5rem,5vh,2.5rem);}
  .case-back{margin:0 0 1.4rem;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;}
  .case-back a{color:var(--muted);text-decoration:none;}
  .case-back a:hover{color:var(--ink);}
  .case--page .case-title{font-size:clamp(1.6rem,4.4vw,2.4rem);}
  .case{margin:0 0 clamp(3rem,7vh,4.5rem);}
  .case:last-child{margin-bottom:0;}
  /* Each further case study is ruled off from the one above, so a page of
     them reads as a stack of separate jobs rather than one long feed. */
  .case + .case{
    border-top:1px solid var(--rule);padding-top:clamp(2.5rem,6vh,3.5rem);
  }
  .case-head{max-width:62ch;margin:0 0 clamp(1.5rem,4vh,2.25rem);}
  .case-sector{
    font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
    color:var(--muted);margin:0 0 .55rem;
  }
  .case-title{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.25rem,3.4vw,1.75rem);line-height:1.25;
    margin:0;color:var(--ink);text-wrap:balance;
  }
  .case-summary{color:var(--ink-dim);font-size:.98rem;margin:.85rem 0 0;}
  .case-made{
    list-style:none;padding:0;margin:1.15rem 0 0;
    display:flex;flex-wrap:wrap;gap:.5rem .6rem;
  }
  .case-made li{
    font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;
    color:var(--muted);border:1px solid var(--rule);border-radius:999px;
    padding:.35rem .8rem;
  }
  /* Films sit outside .grid: these are 9:16 social cuts, and a square tile
     centre-crops the top and bottom off a vertical frame. */
  .films{
    display:flex;flex-wrap:wrap;gap:clamp(.75rem,2vw,1.25rem);
    margin:0 0 clamp(1.25rem,3vw,1.75rem);
  }
  .film{margin:0;width:min(240px,44vw);}
  .tile--film{aspect-ratio:9/16;}
  .film-cap{margin:.55rem 0 0;font-size:.78rem;line-height:1.45;color:var(--muted);}
  /* Panorama stills (a side-by-side before/after) take the full row; the
     inline aspect-ratio gallery.js sets overrides .tile's square. */
  .tile--wide{grid-column:1 / -1;}
  .grid{
    display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.6rem;
  }
  .tile{
    position:relative;display:block;width:100%;padding:0;border:0;margin:0;
    background:var(--tile);border-radius:4px;overflow:hidden;cursor:pointer;
    aspect-ratio:1/1;
  }
  .tile-img{
    width:100%;height:100%;object-fit:cover;display:block;
    transition:transform .5s ease,opacity .3s ease;
  }
  .tile:hover .tile-img,.tile:focus-visible .tile-img{transform:scale(1.045);opacity:.92;}
  .tile:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
  .tile-play{
    position:absolute;inset:0;margin:auto;
    width:46px;height:46px;border-radius:50%;
    background:rgba(0,0,0,.45);pointer-events:none;transition:background .3s ease;
  }
  .tile:hover .tile-play{background:rgba(0,0,0,.65);}
  .tile-play::before{
    content:"";position:absolute;left:53%;top:50%;
    transform:translate(-50%,-50%);
    border-style:solid;border-width:8px 0 8px 13px;
    border-color:transparent transparent transparent #fff;
  }
  .tile-dur{
    position:absolute;right:6px;bottom:6px;
    background:rgba(0,0,0,.62);color:#fff;
    font-size:.7rem;letter-spacing:.02em;padding:.1rem .35rem;border-radius:3px;
  }

  /* ---- Closing CTA ---- */
  .close{
    background:var(--ground);color:var(--ink);
    padding:clamp(4rem,12vh,7rem) 1.5rem 0;text-align:center;overflow:hidden;
    border-top:1px solid var(--rule);
  }
  .close-inner{max-width:calc(var(--wide) - 3rem);margin:0 auto;}
  .close-cta{
    font-family:"Playfair Display",Georgia,serif;font-weight:500;
    font-size:clamp(1.85rem,5.4vw,3.9rem);line-height:1.14;
    letter-spacing:-.012em;margin:1.25rem 0 0;color:var(--ink);
    text-wrap:balance;
  }
  /* The <br> is the intended break between the two sentences. Below ~620px
     the first sentence cannot hold one line anyway, so let it wrap freely
     rather than fighting it. The single space after the <br> in the markup is
     load bearing: with the break hidden it is the only thing separating the two
     sentences, and with the break shown it sits at the start of a line and is
     collapsed away, so it costs nothing. */
  @media (max-width:620px){ .close-cta br{display:none;} }
  .close-actions{
    display:flex;flex-wrap:wrap;gap:.75rem;justify-content:center;
    margin:clamp(2rem,5vh,3rem) 0 0;
  }
  .close-note{
    margin:1.1rem auto 0;max-width:44ch;font-size:.88rem;color:var(--muted);
  }
  .pill{
    display:inline-block;padding:.85rem 1.7rem;border-radius:999px;
    border:1px solid var(--ink);background:var(--ink);color:var(--ground);
    font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;
    text-decoration:none;transition:opacity .25s ease,border-color .25s ease;
  }
  .pill:hover{opacity:.75;}
  .pill--ghost{background:transparent;color:var(--ink);border-color:var(--rule);}
  .pill--ghost:hover{border-color:var(--ink);opacity:1;}

  /* A modifier, so .pill and .pill--ghost are untouched and the solid pill
     beside this one stays exactly as it was. */
  .pill--glow{
    position:relative;
    /* isolation, not a bare z-index: the ::before sits at -1 so the label stays
       on top of it, and with no stacking context here that -1 would drop the
       glow behind .close's own background and it would vanish. */
    isolation:isolate;
    /* CSS cannot read the rendered height and every measurement below is a
       multiple of it, so it is rebuilt from .pill: the vertical padding twice,
       plus the line box. If that padding changes, this has to follow. */
    --pill-h:calc(1.7rem + 1.7em);
  }
  .pill--glow::before{
    content:"";position:absolute;z-index:-1;pointer-events:none;
    /* Wider than it is tall: light around a pill should follow the pill rather
       than inflate into a circle. The 16vw floor keeps the field clear of
       .close's overflow:hidden on narrow screens, where .close-actions goes to
       a column and the pills stretch. */
    inset:calc(var(--glow-size) * var(--pill-h) * -.6)
          max(calc(var(--glow-size) * var(--pill-h) * -1),-16vw);
    /* The centre is held well under --glow-a so the ghost pill never reads as
       filled. The peak sits out at 36%, roughly on the button's own edge. */
    background:radial-gradient(closest-side,
      rgba(var(--glow-color),calc(var(--glow-a) * .35)) 0%,
      rgba(var(--glow-color),var(--glow-a)) 36%,
      rgba(var(--glow-color),calc(var(--glow-a) * .28)) 62%,
      rgba(var(--glow-color),0) 80%);
    filter:blur(var(--glow-blur));
    opacity:.72;  /* where the breathe is frozen when motion is off */
    animation:pill-glow var(--glow-period) ease-in-out infinite;
    transition:scale .45s ease,filter .45s ease;
  }
  /* The lift is scale plus a tighter blur, not opacity: opacity belongs to the
     keyframes and a hover rule would only fight them. scale is its own
     property, so it composes with the animated transform instead of replacing it. */
  .pill--glow:hover::before{scale:1.06;filter:blur(calc(var(--glow-blur) * .86));}
  /* Only opacity and transform move, so the blur rasterises once and the
     compositor carries the cycle. Brightness peaks at 50% and the drift a beat
     later, so the two never land together and the light reads as wandering
     rather than as a metronome. */
  @keyframes pill-glow{
    0%,100%{opacity:.62;transform:translate3d(calc(var(--glow-travel) * -14%),0,0);}
    50%    {opacity:1;}
    62%    {transform:translate3d(calc(var(--glow-travel) * 14%),0,0);}
  }
  .close-nav{
    display:flex;flex-wrap:wrap;gap:.6rem 1.6rem;justify-content:center;
    margin:clamp(2.5rem,6vh,3.5rem) 0 0;font-size:.82rem;
  }
  .close-nav a{color:var(--muted);text-decoration:none;transition:color .25s ease;}
  .close-nav a:hover{color:var(--ink);}
  .close-fine{
    margin:1.5rem 0 0;color:var(--muted);font-size:.74rem;letter-spacing:.05em;
  }
  /* Giant wordmark, bled off the bottom edge so it reads as a terminal
     statement rather than a second logo. Masked for the same reason as
     .mark-flat: one SVG, no light-coloured duplicate to keep in sync. */
  /* Pressed, not printed. The fill matches --ground, so the wordmark is
     invisible as a shape and exists only as the two soft edges the filter
     throws. The mask has to live on the pseudo and the filter on the parent:
     within one element a filter is applied BEFORE the mask, so a drop-shadow
     on the masked element would be generated and then clipped straight back
     off. Split across two, the parent filters an already masked child and the
     shadows follow the glyph contours. box-shadow:inset is no use for the same
     reason it never was here, it follows the box and the box is a rectangle. */
  .close-mark{
    position:relative;
    width:calc(100% + 4vw);
    margin:clamp(2.5rem,7vh,4.5rem) -2vw -1.4%;
    aspect-ratio:826/178;
    /* Depth scales with the mark: it is 184px tall at full width and 77px on a
       phone, and a flat offset that carves nicely on a desktop swallows a whole
       stroke on a phone. --ghost-depth is the ceiling, which is what the bench
       drives. */
    --depth:clamp(2px,.55vw,var(--ghost-depth));
    filter:
      drop-shadow(0 calc(var(--depth) * -1) var(--ghost-blur) rgba(var(--ghost-shade),var(--ghost-shade-a)))
      drop-shadow(0 var(--depth) var(--ghost-blur) rgba(var(--ghost-lite),var(--ghost-lite-a)));
  }
  .close-mark::before{
    content:"";position:absolute;inset:0;
    background:rgb(var(--ghost));
    -webkit-mask:url(logo.svg) no-repeat center/100% 100%;
            mask:url(logo.svg) no-repeat center/100% 100%;
  }

  /* ---- Lightbox: content dominates, chrome stays subtle ---- */
  body.lb-open{overflow:hidden;}
  .lb{
    position:fixed;inset:0;z-index:1000;
    display:flex;align-items:center;justify-content:center;
    background:rgba(4,4,4,.985);padding:0;
  }
  .lb[hidden]{display:none;}
  .lb-stage{
    display:flex;align-items:center;justify-content:center;width:100%;height:100%;
  }
  .lb-media{
    max-width:94vw;max-height:92vh;width:auto;height:auto;
    object-fit:contain;display:block;
  }
  video.lb-media{background:#000;width:auto;}
  .lb-nav,.lb-close{
    position:absolute;border:0;background:transparent;cursor:pointer;
    opacity:.55;transition:opacity .25s ease;
    filter:drop-shadow(0 1px 2px rgba(0,0,0,.6));
    -webkit-tap-highlight-color:transparent;
  }
  .lb-nav:hover,.lb-close:hover,.lb-nav:focus-visible,.lb-close:focus-visible{opacity:1;}
  .lb-nav{top:50%;transform:translateY(-50%);width:52px;height:64px;}
  .lb-nav::before{
    content:"";display:block;width:15px;height:15px;
    border-right:2px solid #fff;border-bottom:2px solid #fff;margin:0 auto;
  }
  .lb-prev{left:max(6px,1.5vw);}
  .lb-prev::before{transform:rotate(135deg);}
  .lb-next{right:max(6px,1.5vw);}
  .lb-next::before{transform:rotate(-45deg);}
  .lb-close{top:max(8px,1.2vh);right:max(8px,1.2vw);width:44px;height:44px;}
  .lb-close::before,.lb-close::after{
    content:"";position:absolute;left:50%;top:50%;
    width:22px;height:2px;background:#fff;
  }
  .lb-close::before{transform:translate(-50%,-50%) rotate(45deg);}
  .lb-close::after{transform:translate(-50%,-50%) rotate(-45deg);}
  .lb-count{
    position:absolute;top:max(12px,1.6vh);left:max(12px,1.6vw);
    color:rgba(255,255,255,.8);font-size:.8rem;letter-spacing:.06em;
    text-shadow:0 1px 2px rgba(0,0,0,.6);pointer-events:none;
  }
  .lb-cap{
    position:absolute;left:0;right:0;bottom:max(10px,1.4vh);
    margin:0 auto;max-width:70ch;padding:0 3.5rem;
    color:rgba(255,255,255,.82);font-size:.85rem;text-align:center;
    text-shadow:0 1px 2px rgba(0,0,0,.7);pointer-events:none;
  }
  .lb--idle .lb-nav,.lb--idle .lb-close,.lb--idle .lb-count,.lb--idle .lb-cap{opacity:0;}

  @media (max-width:560px){
    .hero{padding-bottom:5rem;}
    .mark{
      --mark-layout:stack;
      width:min(90vw, calc((100svh - 13rem) * 1.5915493));
      aspect-ratio:452/284;
    }
    .mark-flat{
      -webkit-mask-image:url(logo-stack.svg);
              mask-image:url(logo-stack.svg);
    }
    .hero-meta{font-size:.62rem;letter-spacing:.12em;}
    .hero-tagline{letter-spacing:.2em;font-size:.72rem;}
    .grid{grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.4rem;}
    .folder-mark{width:40px;height:40px;}
    .peek-img,.peek-more{width:38px;height:38px;}
    .close-actions{flex-direction:column;}
    .close-nav{gap:.5rem 1.1rem;font-size:.78rem;}
    .close-mark{width:calc(100% + 2rem);margin-left:-1rem;margin-right:-1rem;}
    .lb-nav{width:40px;height:52px;}
    .lb-close{width:40px;height:40px;}
  }
  /* Scroll reveal. reveal.js adds .is-in on intersect and unobserves; .is-in
     REMOVES the hidden state rather than setting opacity:1, so an element
     with its own resting opacity (the lens, .65) settles there instead of
     flashing fully opaque first. --i is a per-element stagger index written
     by reveal.js when the parent carries data-stagger. */
  .reveal{
    transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease),filter var(--dur) var(--ease);
    transition-delay:calc(var(--i,0) * 70ms);
  }
  html.js .reveal:not(.is-in){opacity:0;transform:translateY(12px);}
  /* Parked is the light state after a first reveal, once an element has
     scrolled fully out of view: it is direction aware, so an element
     re-entering from the top drops down into place and one re-entering
     from the bottom rises. */
  html.js .reveal.is-in.is-parked{opacity:.4;transform:translateY(8px);}
  html.js .reveal.is-in.is-parked[data-exit="top"]{transform:translateY(-8px);}
  .reveal--deep{--dur:.85s;}
  html.js .reveal--deep:not(.is-in){transform:translateY(28px);filter:brightness(.7);}
  .reveal--drift{
    --dur:1s;transition-delay:150ms;
    transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease),filter var(--dur) var(--ease),--lens-fade .18s linear;
  }
  html.js .reveal--drift:not(.is-in){transform:translateX(24px);}
  html.js .reveal--drift.is-in.is-parked{transform:translateX(12px);}
  @media (prefers-reduced-motion:reduce){
    html{scroll-behavior:auto;}
    .tile-img,.lb-nav,.lb-close,.pill,.close-nav a,.folder-mark{transition:none;}
    .reveal{transition:none;}
    html.js .reveal:not(.is-in){opacity:1;transform:none;filter:none;}
    html.js .reveal.is-parked{opacity:1;transform:none;}
    /* The bar still appears and still disappears, it just does it at once. */
    .sitenav,.sitenav.is-on,.sitenav a{transition:none;}
    .shine::after{display:none;}
    .pill--glow::before{animation:none;transition:none;}
    .folder:hover .folder-mark,.folder:focus-visible .folder-mark{transform:none;}
    .hero-cue span{animation:none;opacity:.6;top:14px;}
    .mark-reveal{display:none;}
    .mark--live .mark-flat{opacity:1;}
  }
  @media print{
    .sitenav{display:none;}
    .mark-reveal{display:none;}
    .shine::after{display:none;}
    .pill--glow::before{display:none;}
    .mark--live .mark-flat{opacity:1;}
  }
