/* ============================================================
   Nexus — site.css
   Dark glass, extended display type, floating glass bubbles.
   Built mobile-first. Desktop composition is layered on at 1024px.
   ============================================================ */

/* ============================================================
   FONTS
   Self-hosted, and that is the fix rather than a preference.

   These were coming from Google Fonts, which on a cold phone means DNS and
   TLS to fonts.googleapis.com, then the CSS, then DNS and TLS again to
   fonts.gstatic.com, then the woff2. That chain regularly outruns the intro,
   which is over in about 4.7s, so the opening wordmark and the nav logo
   painted in the Arial Black fallback and the page had already moved on by
   the time Syncopate arrived. Desktop won the race and hid the problem.

   Served from the origin the files are one hop on a connection the browser
   has already opened for the HTML, and Syncopate 700 is preloaded in the
   head on top of that, so the two places it shows first have it before they
   paint. Same typefaces, same weights, same unicode ranges as the Google
   build: nothing about how the type renders changes, on any breakpoint.

   Space Grotesk is one variable file per subset, not five statics. Google
   serves the identical woff2 for every weight it is asked for and only
   varies the font-weight descriptor; the five downloads were byte for byte
   the same file, so this declares the 300-700 range once. Six files, 84KB.

   font-display:swap everywhere, so a slow font can never hold the page
   blank. site.js's fonts gate is what stops the hero animating mid-swap,
   and it stands down for the intro and has a 1.2s failsafe of its own. */
@font-face{
  font-family:'Syncopate';
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url('../fonts/syncopate-400-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'Syncopate';
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url('../fonts/syncopate-400-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'Syncopate';
  font-style:normal;
  font-weight:700;
  font-display:swap;
  src:url('../fonts/syncopate-700-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'Syncopate';
  font-style:normal;
  font-weight:700;
  font-display:swap;
  src:url('../fonts/syncopate-700-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:'Space Grotesk';
  font-style:normal;
  font-weight:300 700;
  font-display:swap;
  src:url('../fonts/space-grotesk-var-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:'Space Grotesk';
  font-style:normal;
  font-weight:300 700;
  font-display:swap;
  src:url('../fonts/space-grotesk-var-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root{
  /* ground: pure black, so the bubble art (rendered on #000) has no seam */
  --void:#000000;
  --ink:#000000;
  --slate:#17181C;
  --char:#1E2024;

  /* text */
  --bone:#EDEEF0;
  --ash:#9A9DA4;   /* 8.0:1 on black */
  --dim:#85888F;   /* 6.0:1 on black, safe for 11px labels */

  /* single functional accent */
  --cy:#5FE3E0;

  /* Display size. Syncopate is unusually wide: the longest hero line,
     "websites", measures 6.19em, so the ceiling here is set from that
     advance rather than by eye. Raising it makes the headline collide
     with the stat sitting to its right. */
  --dsp:clamp(2.4rem, 10.2vw, 9.5rem);
  --dsp-lh:0.88;

  /* rhythm */
  --gut:clamp(1.25rem, 5vw, 2.5rem);
  --sec-y:clamp(5rem, 13vw, 11rem);
  --maxw:1440px;

  --ease:cubic-bezier(.22,.61,.36,1);

  /* THE GLASS, in one place. The nav pills and the footer are the same
     material and have to stay the same material, so they read as one plane
     with the field passing behind both. They were duplicated literals and
     drifted apart once already; the resting material is now one edit here.
     The nav's scrolled state keeps its own heavier values below, since the
     footer has no scrolled state to match it to.

     The fill is deliberately thin. What makes this read as glass is not the
     wash, it is --glass-lift: the inset hairline on the top edge is a lit
     rim, and the one on the bottom is the light coming back off the far
     side. Those stay put when the fill is lowered, which is why the plates
     keep their definition while getting more see-through.

     saturate() is the liquid part: it pulls the colour out of whatever
     passes underneath, so a prismatic blob drifting behind blooms through
     instead of going grey. */
  --glass-tail:rgba(255,255,255,.042);   /* the gradient's own last stop */
  --glass-fill:linear-gradient(180deg,
    rgba(255,255,255,.075), rgba(255,255,255,.020) 42%, var(--glass-tail));
  --glass-blur:blur(22px) saturate(180%);
  --glass-edge:rgba(255,255,255,.10);
  /* Split three ways because the footer wants only the first of them. A
     pill is a floating object with four edges; the footer's bottom edge is
     not an edge at all, it runs on down .foot-glass. Both bottom treatments
     land inside the bleed's backdrop and are picked up by its blur, which is
     the seam: the hairline draws a line across the join, and the drop shadow
     smudges a dark band under it. Neither is visible anywhere else, since
     the footer's bottom edge is otherwise the end of the document. */
  --glass-lift-top:inset 0 1px 0 rgba(255,255,255,.22);
  --glass-lift-bot:inset 0 -1px 0 rgba(255,255,255,.05);
  --glass-drop:0 10px 28px rgba(0,0,0,.42);
  --glass-lift:var(--glass-lift-top), var(--glass-lift-bot), var(--glass-drop);
}

*,*::before,*::after{box-sizing:border-box}

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

body{
  margin:0;
  background:var(--ink);
  color:var(--bone);
  font-family:'Space Grotesk',Helvetica Neue,Arial,sans-serif;
  font-size:16px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
  /* the field paints its own black ground, so this must stay solid */
}

img{max-width:100%; height:auto; display:block}

/* every content layer rides above the fixed bubble canvas */
main,.foot{position:relative; z-index:1}

.sr{position:absolute; width:0; height:0; overflow:hidden}

.wrap{
  width:100%;
  max-width:var(--maxw);
  margin-inline:auto;
  padding-inline:var(--gut);
}

.ic{width:1em; height:1em; display:block; flex:none}

/* ---------- focus, skip link ---------- */
:where(a,button,[tabindex]):focus-visible{
  outline:2px solid var(--cy);
  outline-offset:3px;
  border-radius:4px;
}

.skip{
  position:absolute; left:var(--gut); top:-100px; z-index:200;
  background:var(--bone); color:var(--ink);
  padding:.7rem 1.1rem; border-radius:999px;
  font-size:.875rem; font-weight:600; text-decoration:none;
  transition:top .2s var(--ease);
}
.skip:focus{top:1rem}

/* ============================================================
   TYPE
   ============================================================ */

.label{
  font-size:.6875rem;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--dim);
  margin:0 0 1.25rem;
  font-weight:500;
}

.sec-h{
  font-family:'Syncopate',Arial Black,sans-serif;
  font-weight:400;
  font-size:clamp(2rem, 7vw, 5rem);
  line-height:.95;
  letter-spacing:-.02em;
  margin:0 0 clamp(2.5rem,6vw,4.5rem);
  color:var(--bone);
}
.sec-h-sm{font-size:clamp(1.75rem, 5vw, 3.5rem)}

/* ============================================================
   BUBBLE FIELD
   A fixed WebGL canvas painted behind every section. One fullscreen quad
   evaluating a metaball distance field, so the blobs fuse and part as they
   pass each other. It paints its own black ground, which is why this layer
   is transparent and composites over the page ground.
   ============================================================ */

.field{
  position:fixed;
  inset:0;
  z-index:0;
  /* no background: the canvas is transparent and the page ground shows
     through the glass, which is what makes the blobs read as clear rather
     than as lit shapes sitting on their own black plate */
  pointer-events:none;   /* the ray is cast from window coords, not hit-testing the DOM */
  opacity:0;
  transition:opacity 1.2s var(--ease);
}
.field.is-ready{opacity:.85}

/* The desktop hero keeps its copy in the corners the composition leaves
   clear. Stacked layouts have no such counter-space, so the field steps back
   a little rather than competing with the text it sits under. It used to step
   back to .42, which was set against the old opaque blobs; the glass is clear
   through the belly now, so it can carry far more without touching legibility,
   and at .42 the phone read as a much darker page than the desktop. */
@media (max-width:1023px){
  .field.is-ready{opacity:.72}
}
.field canvas{display:block; width:100%; height:100%}

/* no WebGL: the page keeps its palette and simply has no field */
.field.is-unavailable{display:none}

/* ============================================================
   GLASS
   Dark fill, not the frosted-white default. The whole trick is the
   top-lit inset hairline: it reads as a lit edge rather than a glow.
   ============================================================ */

.glass{
  position:relative;
  background:linear-gradient(158deg, rgba(255,255,255,.055), rgba(255,255,255,.014));
  -webkit-backdrop-filter:blur(22px) saturate(1.25);
  backdrop-filter:blur(22px) saturate(1.25);
  border:1px solid rgba(255,255,255,.075);
  border-radius:20px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.10),
    0 24px 60px -30px rgba(0,0,0,.9);
}

/* ============================================================
   BUTTONS
   ============================================================ */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:.5rem;
  min-height:48px;
  padding:0 1.5rem;
  border-radius:999px;
  font-size:.9375rem; font-weight:500;
  text-decoration:none;
  white-space:nowrap;
  cursor:pointer;
  border:1px solid transparent;
  transition:background-color .25s var(--ease), color .25s var(--ease),
             border-color .25s var(--ease), transform .25s var(--ease);
}
.btn-white{background:var(--bone); color:var(--ink)}
.btn-white:hover{background:#fff; transform:translateY(-2px)}
.btn-ghost{
  background:rgba(255,255,255,.03);
  color:var(--bone);
  border-color:rgba(255,255,255,.14);
  -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
}
.btn-ghost:hover{background:rgba(255,255,255,.08); border-color:rgba(255,255,255,.26); transform:translateY(-2px)}

/* ============================================================
   NAV
   ============================================================ */

/* No bar. The header is only a positioning frame, so the pills float over
   the field on their own and the bubbles pass behind them uninterrupted.
   It is transparent to the pointer too, or an invisible full width strip
   would sit over the top of the page swallowing clicks. */
.nav-wrap{
  position:fixed; inset:0 0 auto 0; z-index:100;
  padding:clamp(.75rem,2vw,1.25rem) var(--gut);
  transition:opacity .8s var(--ease);
  pointer-events:none;
  background:none;
  border:0;
}
.nav > *{pointer-events:auto}

/* Liquid glass. The fill is a gradient rather than a flat wash so the pill
   is lit from above, and the inset hairline on the top edge is what reads as
   a lit rim rather than a border. saturate() is the liquid part: it pulls the
   colour out of whatever passes underneath, so a prismatic blob drifting
   behind a pill blooms through it instead of going grey. */
.nav-logo,
.nav-links,
.nav-toggle{
  background:var(--glass-fill);
  border:1px solid var(--glass-edge);
  -webkit-backdrop-filter:var(--glass-blur);
  backdrop-filter:var(--glass-blur);
  box-shadow:var(--glass-lift);
  transition:background-color .3s var(--ease), box-shadow .3s var(--ease),
             border-color .3s var(--ease);
}

/* Scrolled, the glass thickens instead of a bar appearing behind it. */
.nav-wrap.stuck .nav-logo,
.nav-wrap.stuck .nav-links,
.nav-wrap.stuck .nav-toggle{
  border-color:rgba(255,255,255,.14);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.28),
    inset 0 -1px 0 rgba(255,255,255,.06),
    0 14px 34px rgba(0,0,0,.55);
}

/* ---- liquid glass ----
   With the lens live (bubbles.js adds .has-lens), the field under every glass
   surface is drawn already bent, so the frosting thins or it would blur the
   bend away, and the hairline rims go: the bend is what draws the edge now.
   The drop shadows stay, since they are what lifts a plate off the page.
   Without WebGL none of this applies and the glass stays frosted. */
.has-lens .nav-logo,
.has-lens .nav-links,
.has-lens .nav-toggle,
.has-lens .glass,
.has-lens .btn-ghost,
.has-lens .wa-float,
.has-lens .foot-glass{
  -webkit-backdrop-filter:blur(1.5px) saturate(160%);
  backdrop-filter:blur(1.5px) saturate(160%);
  border-color:transparent;
}
.has-lens .nav-logo,
.has-lens .nav-links,
.has-lens .nav-toggle{
  box-shadow:var(--glass-drop);
  transition:background-color .3s var(--ease), box-shadow .3s var(--ease),
             -webkit-backdrop-filter .4s var(--ease), backdrop-filter .4s var(--ease);
}
/* The plates that carry paragraphs keep some frost, the "frosty level" of the
   glass: enough that a rim running under a sentence does not cut through it,
   little enough that the bend at the plate's edge still reads. The pills and
   buttons carry a few words each, sit over smoke, and stay nearly clear. */
.has-lens .glass,
.has-lens .foot-glass{
  -webkit-backdrop-filter:blur(6px) saturate(165%);
  backdrop-filter:blur(6px) saturate(165%);
}
.has-lens .glass{box-shadow:0 24px 60px -30px rgba(0,0,0,.9)}
.has-lens .wa-float{box-shadow:0 18px 40px -18px rgba(0,0,0,.9)}
.has-lens .foot-glass{border-top:0; box-shadow:none}
.has-lens .btn-ghost:hover{border-color:transparent}

/* Scrolled, page content passes under the pills as well, and WebGL cannot
   bend that, so a dark tint and a moderate blur come back to keep the links
   legible. The bent field still reads through. */
.has-lens .nav-wrap.stuck .nav-logo,
.has-lens .nav-wrap.stuck .nav-links,
.has-lens .nav-wrap.stuck .nav-toggle{
  background-color:rgba(6,7,9,.46);
  border-color:transparent;
  box-shadow:0 14px 34px rgba(0,0,0,.55);
  -webkit-backdrop-filter:blur(7px) saturate(170%);
  backdrop-filter:blur(7px) saturate(170%);
}

/* the white CTA keeps its fill, per the palette, but joins the same plane */
.nav-cta{box-shadow:0 10px 28px rgba(0,0,0,.42)}

.nav{
  max-width:var(--maxw); margin-inline:auto;
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
}

/* Every item in the bar is 48px tall, the same as the CTA and the menu
   button, so the pills share one top edge and one bottom edge. */
.nav-logo{
  display:inline-flex; align-items:center;
  height:48px;
  font-family:'Syncopate',sans-serif;
  font-size:.8125rem; font-weight:700; letter-spacing:.02em; line-height:1;
  color:var(--bone); text-decoration:none;
  padding:0 1.25rem;
  border-radius:999px;
}

.nav-links{
  display:none;
  position:relative;
  height:48px;
  align-items:center;
  list-style:none; margin:0; padding:0 4px;
  gap:.15rem;
  border-radius:999px;
}
.nav-links li{position:relative; z-index:1}
.nav-links a{
  display:flex; align-items:center;
  height:40px;
  padding:0 1.1rem;
  border-radius:999px;
  color:var(--ash);
  font-size:.875rem; line-height:1; text-decoration:none;
  transition:color .2s var(--ease), background-color .2s var(--ease);
}
.nav-links a:hover{color:var(--bone); background:rgba(255,255,255,.06)}
.nav-links.has-glide a:hover{background:none}
.nav-links a:focus-visible{outline:2px solid var(--cy); outline-offset:2px}

/* The hover pill: one drop of glass that slides between the links. Its box
   is read by bubbles.js, which bends the field inside it; the fill here is
   only the faint body of the drop. The overshoot in the curve is the liquid
   part: it lands a touch past the link and settles back. */
.nav-glide{
  position:absolute; top:4px; left:0;
  width:var(--gw, 0); height:40px;
  border-radius:999px;
  background:rgba(255,255,255,.07);
  box-shadow:0 6px 16px -6px rgba(0,0,0,.6);
  translate:var(--gx, 0) 0;
  opacity:0; scale:.92;
  pointer-events:none;
  transition:translate .5s cubic-bezier(.3,1.35,.45,1), width .5s cubic-bezier(.3,1.35,.45,1),
             opacity .25s var(--ease), scale .3s var(--ease);
}
.nav-glide.on{opacity:1; scale:1}
.nav-glide.snap{transition:none}

.nav-cta{display:none}

.nav-toggle{
  display:inline-flex; align-items:center; justify-content:center;
  width:48px; height:48px;
  font-size:1.25rem;
  color:var(--bone);
  border-radius:999px;
  cursor:pointer;
}

/* mobile sheet */
.sheet{
  position:fixed; inset:0; z-index:150;
  background:rgba(11,12,14,.94);
  -webkit-backdrop-filter:blur(24px); backdrop-filter:blur(24px);
  display:flex; flex-direction:column; justify-content:center;
  padding:var(--gut);
  gap:2rem;
}
.sheet[hidden]{display:none}
.sheet-close{
  position:absolute; top:clamp(.75rem,2vw,1.25rem); right:var(--gut);
  width:48px; height:48px; font-size:1.25rem;
  display:inline-flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.05); color:var(--bone);
  border:1px solid rgba(255,255,255,.08); border-radius:999px; cursor:pointer;
}
.sheet-links{list-style:none; margin:0; padding:0; display:grid; gap:.25rem}
.sheet-links a{
  display:block; padding:.75rem 0;
  font-family:'Syncopate',sans-serif;
  font-size:clamp(1.5rem,8vw,2.25rem);
  color:var(--bone); text-decoration:none;
  letter-spacing:-.02em;
}
.sheet-cta{align-self:flex-start}

/* ============================================================
   HERO
   ============================================================ */

.hero{
  position:relative;
  min-height:100svh;
  display:flex; align-items:center;
  padding-block:clamp(8rem,20vw,11rem) clamp(4rem,10vw,7rem);
  overflow:hidden;
}

.hero-grid{
  position:relative; z-index:1;
  width:100%; max-width:var(--maxw);
  margin-inline:auto; padding-inline:var(--gut);
  display:grid; gap:clamp(1.75rem,4vw,2.75rem);
}

.hero-h1{
  margin:0;
  font-family:'Syncopate',Arial Black,sans-serif;
  font-weight:400;
  font-size:var(--dsp);
  line-height:var(--dsp-lh);
  letter-spacing:-.035em;
  color:var(--bone);
  display:grid;
  gap:0;
}
.hero-h1 span{display:block}
.l2{padding-left:1.5em}
.l3{padding-left:.6em}

.hero-lede{
  margin:0;
  max-width:34ch;
  font-size:clamp(.9375rem,2.2vw,1.0625rem);
  line-height:1.65;
  color:var(--ash);
}

.stat{margin:0}
.stat-n{
  display:block;
  white-space:nowrap;
  font-family:'Syncopate',sans-serif;
  font-size:clamp(1.4rem,3.6vw,2.6rem);
  font-weight:400;
  line-height:1;
  letter-spacing:-.03em;
  color:var(--bone);
}
.stat figcaption{
  margin-top:.6rem;
  font-size:.8125rem;
  line-height:1.5;
  color:var(--dim);
  max-width:24ch;
}

.hero-note{
  margin:0;
  max-width:36ch;
  font-size:.875rem;
  line-height:1.6;
  color:var(--dim);
}

.hero-cta{display:flex; flex-wrap:wrap; gap:.75rem}

/* ------------------------------------------------------------
   The hero, centred on a phone.
   The desktop composition is deliberately asymmetric: the copy sits in the
   corners the artwork leaves clear, so a ragged right edge is balanced by
   the field. Stacked into one column that balance is gone, and left aligned
   text under a full bleed field just leaves a long ragged edge with nothing
   answering it.

   Centring is not only text-align. The staggered indents that give the
   desktop headline its shape read as a broken centre once the lines are
   centred, so they go to zero; and the three blocks with a ch based
   max-width stay pinned left unless their auto margins are set, since a
   max-width narrower than the column is what does the centring, not the
   text alignment inside it.
   ------------------------------------------------------------ */
@media (max-width:767px){
  .hero-grid{text-align:center}

  .hero-h1 .l2,
  .hero-h1 .l3{padding-left:0}

  .hero-lede,
  .hero-note,
  .stat figcaption{margin-inline:auto}

  /* flex children ignore text-align, so the buttons need their own rule */
  .hero-cta{justify-content:center}
}


/* The field roams the whole page, so the small copy carries its own
   contrast rather than relying on whatever happens to drift behind it.
   The display type is large and white enough to hold its own. */
.hero-lede,
.hero-note,
.page-lede,
.stat figcaption,
.contact-p{
  text-shadow:0 1px 14px rgba(0,0,0,.92), 0 0 4px rgba(0,0,0,.75);
}

/* ============================================================
   SECTIONS
   ============================================================ */

.sec{position:relative; padding-block:var(--sec-y)}

/* --- capabilities --- */
.cap .wrap{position:relative; z-index:1}
.cap .sec-h{text-align:center}
.cap .label{text-align:center}

.cards{display:grid; gap:clamp(1rem,2.5vw,1.5rem)}

.card{padding:clamp(1.5rem,4vw,2.25rem)}
.card h3{
  margin:0 0 .875rem;
  font-family:'Syncopate',sans-serif;
  font-weight:400;
  font-size:clamp(1.0625rem,2.6vw,1.375rem);
  line-height:1.2;
  letter-spacing:-.02em;
  color:var(--bone);
}
.card p{margin:0; font-size:.9375rem; line-height:1.65; color:var(--ash)}

.cap-links{
  list-style:none; margin:clamp(2rem,5vw,3rem) 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:1rem clamp(1.5rem,4vw,2.5rem);
}
.cap-links .work-link{min-height:44px; display:inline-flex; align-items:center}

/* --- work ---
   Each card opens on the client's own site: a desktop capture of the live
   hero, the whole width of the card at 16:10 so the site shows at its real
   proportions rather than cropped and blown up, with the client's brand hue
   carrying the copy below it. The hue is set per card as --h and --s on the article; lightness is
   fixed here, so every ground is equally dark and the white copy on it keeps
   the same contrast whatever the brand colour. --h/--s come from each site's
   own :root tokens, so a rebrand on their side means one edit on the card. */
.works{display:grid; gap:clamp(1rem,2.5vw,1.5rem)}

.work-card{
  --h:0; --s:0%;
  --ground:hsl(var(--h) var(--s) 11%);
  --fade:4rem;
  position:relative;
  isolation:isolate;
  overflow:hidden;
  min-height:34rem;
  display:grid; grid-template-rows:1fr auto;
  border-radius:20px;
  border:1px solid rgba(255,255,255,.08);
  background:var(--ground);
}
.work-bg{
  position:absolute; top:0; left:0; z-index:-1;
  width:100%; height:auto; aspect-ratio:16/10;
  object-fit:cover; object-position:center top;
  /* The hover zoom grows down and out from the top edge, so the client's own
     nav along the top of the capture is never pushed out of the card. */
  transform-origin:50% 0;
  transition:transform .6s var(--ease);
}
.work-body{grid-row:2; grid-column:1; padding:clamp(1.25rem,3vw,1.5rem)}

/* The captures are busy, their own headlines included, so the copy cannot
   sit on them through a tint alone. The ground is a grid item sharing the
   copy's row, so it is always exactly as tall as the copy, and reaches
   --fade above it to hand over to the capture. It also blurs what is behind
   it, fading out with the tint so the top of the capture stays sharp. The
   copy itself is not positioned, which keeps the card, not the copy, as the
   box the stretched link fills. */
.work-card::after{
  content:""; grid-row:2; grid-column:1; z-index:-1;
  margin-top:calc(var(--fade) * -1);
  background:linear-gradient(to bottom,
    transparent 0,
    hsl(var(--h) var(--s) 11% / .55) calc(var(--fade) * .5),
    hsl(var(--h) var(--s) 11% / .9) var(--fade),
    hsl(var(--h) var(--s) 11% / .96) 100%);
  -webkit-backdrop-filter:blur(14px);
  backdrop-filter:blur(14px);
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 calc(var(--fade) * .8));
  mask-image:linear-gradient(to bottom, transparent 0, #000 calc(var(--fade) * .8));
}

.work-card h3{
  margin:0;
  font-family:'Syncopate',sans-serif; font-weight:700;
  font-size:clamp(1.125rem,2vw,1.3125rem); line-height:1.15;
  letter-spacing:-.02em; color:#fff;
}
.work-kind{
  margin:.5rem 0 0;
  font-size:.6875rem; letter-spacing:.18em; text-transform:uppercase;
  color:rgba(255,255,255,.78);
}
.work-blurb{margin:.875rem 0 0; font-size:.875rem; line-height:1.6; color:rgba(255,255,255,.82)}

/* One set of lines per row of cards. Each card spans five rows of the grid
   and takes them as a subgrid (capture space, title, kind, blurb, bubble),
   so every title in a row starts on the same line, the kinds and blurbs line
   up under them and the bubbles share one baseline, whatever each client's
   copy runs to. Without it the copy was pinned to each card's foot and a
   longer blurb pushed its title up, so no two titles in a row lined up.
   .work-body steps out of the way (display:contents) so its children are the
   grid items; it was never positioned, so the stretched link still fills the
   card. Browsers without subgrid keep the stacked layout above. */
@supports (grid-template-rows:subgrid){
  .work-card{--pad:clamp(1.25rem,3vw,1.5rem)}
  .works .work-card{
    grid-row:span 5;
    grid-template-rows:subgrid;
    grid-template-columns:minmax(0,1fr);
    row-gap:0;
    min-height:0;
  }
  /* the capture's own height, so the copy starts where the capture ends */
  .work-card::before{content:""; grid-row:1; grid-column:1; aspect-ratio:16/10}
  .work-body{display:contents}
  .work-card h3,
  .work-card .work-kind,
  .work-card .work-blurb,
  .work-card .work-link{grid-column:1; margin-inline:var(--pad); align-self:start}
  .work-card h3{grid-row:2}
  .work-card .work-kind{grid-row:3}
  .work-card .work-blurb{grid-row:4}
  .work-card .work-link{grid-row:5; align-self:end; margin-bottom:var(--pad)}
  .work-card::after{grid-row:2 / 6}
}

.work-link{
  display:inline-flex; align-items:center; gap:.5rem;
  min-height:44px;
  font-size:.875rem; color:var(--bone); text-decoration:none;
  transition:color .2s var(--ease);
}
.work-link .ic{font-size:1rem; transition:transform .25s var(--ease)}
.work-link:hover{color:var(--cy)}
.work-link:hover .ic{transform:translateX(4px)}

/* The bar at the foot of the card is a bubble: a fully round pill of clear
   glass, the way an Apple search field sits on its wallpaper. The fill is
   white, lit from above and a touch brighter again at the foot where the
   light comes back off the far side; the edge is one even line of light all
   the way round, softened by two blurred insets rather than a hard hairline;
   the card's own ground shows through the clear fill underneath. */
.work-card .work-link{
  display:flex; align-items:center; justify-content:space-between;
  margin-top:1.5rem;
  min-height:52px;
  padding:0 1.5rem;
  border-radius:999px;
  font-weight:500; letter-spacing:.02em; color:#fff;
  background:linear-gradient(180deg,
    rgba(255,255,255,.20), rgba(255,255,255,.07) 55%, rgba(255,255,255,.12));
  border:1px solid rgba(255,255,255,.30);
  box-shadow:
    inset 0 1px 3px rgba(255,255,255,.26),
    inset 0 -1px 3px rgba(255,255,255,.10),
    inset 0 0 18px rgba(255,255,255,.05),
    0 10px 24px -10px rgba(0,0,0,.6);
  transition:background .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease);
}
/* Never give the bubble a backdrop-filter, filter or transform. Any of them
   makes it the containing block for its own ::after, and that ::after is the
   stretched link that makes the whole card clickable: with a backdrop blur
   on the bubble, only the bubble itself opened the site. The bubble sits on
   the card's near-solid ground, so a blur there showed nothing anyway. */

/* The whole card is the link: "visit site" is stretched over the card with ::after,
   so there is still exactly one real link per card for keyboards and screen readers.
   The card itself never moves or grows on hover: a card that did stood proud of its
   row and read as misaligned. The hover happens inside it instead (the capture
   zooms behind the ground, the bubble brightens) and around it (a glow in the brand
   hue lifted to mid lightness, and only on hover: at rest the cards carry no shadow
   at all). This selector outranks .reveal so its transition survives. */
.works .work-card{
  cursor:pointer;
  transition:
    opacity .8s var(--ease), transform .8s var(--ease),
    box-shadow .5s var(--ease), border-color .5s var(--ease);
}
.work-card .work-link::after{content:""; position:absolute; inset:0; border-radius:20px; z-index:1}
.work-card .work-link:focus-visible{outline:none}
.work-card:has(.work-link:focus-visible){outline:2px solid var(--cy); outline-offset:3px}
@media (hover:hover){
  .works .work-card:hover{
    border-color:hsl(var(--h) var(--s) 60% / .35);
    box-shadow:0 0 60px -12px hsl(var(--h) var(--s) 48% / .55);
  }
  .work-card:hover .work-bg{transform:scale(1.04)}
  .work-card:hover .work-link{
    background:linear-gradient(180deg,
      rgba(255,255,255,.28), rgba(255,255,255,.11) 55%, rgba(255,255,255,.16));
    border-color:rgba(255,255,255,.46);
  }
  .work-card:hover .work-link .ic{transform:translateX(4px)}
}

/* --- process --- */
.proc-head{display:grid; gap:clamp(1.5rem,4vw,2.5rem); margin-bottom:clamp(3rem,7vw,5rem)}
.proc-head .sec-h{margin-bottom:0}
.proc-intro{
  margin:0; max-width:44ch;
  font-size:clamp(.9375rem,2.2vw,1.0625rem);
  line-height:1.65; color:var(--ash);
}

.steps{
  list-style:none; margin:0; padding:0;
  display:grid; gap:0;
  border-top:1px solid rgba(255,255,255,.08);
}
.step{
  padding-block:clamp(1.75rem,4vw,2.5rem);
  border-bottom:1px solid rgba(255,255,255,.08);
  display:grid; gap:.75rem;
}
.step-n{
  font-family:'Syncopate',sans-serif;
  font-size:.75rem; font-weight:700;
  letter-spacing:.12em;
  color:var(--cy);
}
.step h3{
  margin:0;
  font-family:'Syncopate',sans-serif;
  font-weight:400;
  font-size:clamp(1.25rem,3.5vw,1.75rem);
  letter-spacing:-.02em;
  color:var(--bone);
}
.step p{margin:0; max-width:52ch; font-size:.9375rem; line-height:1.65; color:var(--ash)}

/* --- contact --- */
.contact .wrap{position:relative; z-index:1}
.contact-h{
  margin:0 0 clamp(1.5rem,4vw,2rem);
  font-family:'Syncopate',sans-serif; font-weight:400;
  font-size:clamp(2.25rem,10vw,7rem);
  line-height:.9; letter-spacing:-.035em;
  color:var(--bone);
  display:grid;
}
.contact-h .ind{padding-left:.9em}
.contact-p{
  margin:0 0 clamp(2.5rem,6vw,3.5rem);
  max-width:46ch;
  font-size:clamp(.9375rem,2.2vw,1.0625rem);
  line-height:1.65; color:var(--ash);
}

.contact-rows{display:grid; gap:.5rem; max-width:640px}
.crow{
  display:flex; align-items:center; gap:.875rem;
  min-height:56px;
  margin:0;
  padding:1rem 1.25rem;
  border-radius:14px;
  border:1px solid rgba(255,255,255,.075);
  background:rgba(255,255,255,.025);
  color:var(--bone);
  font-size:.9375rem;
  text-decoration:none;
  transition:background-color .25s var(--ease), border-color .25s var(--ease);
}
a.crow:hover{background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.18)}
.crow .ic{font-size:1.125rem; color:var(--dim)}
.crow-static{color:var(--ash)}
.ic-go{margin-left:auto; transition:transform .25s var(--ease)}
a.crow:hover .ic-go{transform:translateX(4px); color:var(--cy)}

/* ============================================================
   FLOATING WHATSAPP
   ============================================================ */

.wa-float{
  position:fixed;
  right:clamp(1rem,3vw,1.75rem);
  bottom:clamp(1rem,3vw,1.75rem);
  z-index:90;
  width:56px; height:56px;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:1.5rem;
  color:#25D366;
  border-radius:999px;
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.10);
  -webkit-backdrop-filter:blur(18px) saturate(1.2);
  backdrop-filter:blur(18px) saturate(1.2);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.12), 0 18px 40px -18px rgba(0,0,0,.9);
  transition:transform .25s var(--ease), background-color .25s var(--ease),
             color .25s var(--ease), border-color .25s var(--ease);
}
.wa-float:hover{
  transform:translateY(-3px);
  background:#25D366;
  border-color:#25D366;
  color:#07120B;
}

.crow .ic-wa{color:#25D366}

/* ============================================================
   FAQ
   Native <details>, styled as the contact rows. No JS.
   ============================================================ */

.faq{display:grid; gap:.5rem; max-width:760px}
.faq details{
  border:1px solid rgba(255,255,255,.075);
  border-radius:14px;
  background:rgba(255,255,255,.025);
  transition:background-color .25s var(--ease), border-color .25s var(--ease);
}
.faq details:hover,
.faq details[open]{background:rgba(255,255,255,.05); border-color:rgba(255,255,255,.14)}
.faq summary{
  list-style:none;
  cursor:pointer;
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
  min-height:56px;
  padding:1rem 1.25rem;
  border-radius:14px;
  color:var(--bone);
  font-size:.9375rem; font-weight:500;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:'+';
  flex:none;
  color:var(--dim);
  font-size:1.375rem; line-height:1;
  transition:transform .25s var(--ease), color .25s var(--ease);
}
.faq details[open] summary::after{transform:rotate(45deg); color:var(--cy)}
.faq summary:focus-visible{outline:2px solid var(--cy); outline-offset:2px}
.faq details p{
  margin:0;
  padding:0 1.25rem 1.125rem;
  max-width:60ch;
  font-size:.9375rem; line-height:1.65;
  color:var(--ash);
}

/* ============================================================
   SERVICE PAGES
   Same sections as the home page under a smaller, static hero.
   ============================================================ */

.page-hero{padding-top:clamp(8rem,20vw,12rem); padding-bottom:clamp(3rem,8vw,5rem)}
.page-h1{margin-bottom:clamp(1.5rem,4vw,2.25rem)}
.page-lede{
  margin:0 0 clamp(1.75rem,4vw,2.5rem);
  max-width:52ch;
  font-size:clamp(1rem,2.4vw,1.1875rem);
  line-height:1.6;
  color:var(--ash);
}
.page-cta{display:flex; flex-wrap:wrap; gap:.75rem}
.sheet-links a[aria-current="page"],
.foot-nav a[aria-current="page"],
.foot-legal a[aria-current="page"]{color:var(--cy)}

/* ============================================================
   LEGAL PAGES
   Privacy policy and terms. The process section's hairline rows,
   carrying prose instead of steps, beside a contents list that sticks
   on desktop. No bubble field on these pages, as on the 404: long copy
   reads on the plain ground.
   ============================================================ */

.legal-meta{margin:0; font-size:.8125rem; letter-spacing:.04em; color:var(--dim)}

.legal{
  display:grid; gap:clamp(2rem,5vw,3rem);
  padding-bottom:var(--sec-y);
}
.legal-toc ol{
  list-style:none; margin:0; padding:0;
  display:grid; gap:.125rem;
  counter-reset:toc;
}
.legal-toc li{counter-increment:toc}
.legal-toc a{
  display:flex; align-items:center; gap:.875rem;
  min-height:44px;
  color:var(--ash); font-size:.875rem; text-decoration:none;
  transition:color .2s var(--ease);
}
.legal-toc a::before{
  content:counter(toc, decimal-leading-zero);
  flex:none; width:2.75em;   /* Syncopate's 1 is narrow: hold the labels on one edge */
  font-family:'Syncopate',sans-serif; font-size:.625rem; font-weight:700;
  letter-spacing:.12em; color:var(--dim);
}
.legal-toc a:hover{color:var(--bone)}
.legal-toc a:hover::before{color:var(--cy)}

.legal-body{border-top:1px solid rgba(255,255,255,.08)}
.legal-sec{
  padding-block:clamp(1.75rem,4vw,2.5rem);
  border-bottom:1px solid rgba(255,255,255,.08);
  scroll-margin-top:6rem;
}
.legal-sec h2{
  margin:0 0 1.125rem;
  display:flex; align-items:baseline; gap:1rem;
  font-family:'Syncopate',sans-serif; font-weight:400;
  font-size:clamp(1.0625rem,2.6vw,1.375rem);
  line-height:1.25; letter-spacing:-.02em;
  color:var(--bone);
}
.legal-n{
  flex:none;
  font-size:.6875rem; font-weight:700; letter-spacing:.12em;
  color:var(--cy);
}
.legal-sec p,
.legal-sec li{
  max-width:68ch;
  font-size:.9375rem; line-height:1.7;
  color:var(--ash);
}
.legal-sec p{margin:0 0 1rem}
.legal-sec p:last-child{margin-bottom:0}
.legal-sec ul{margin:0 0 1rem; padding-left:1.125rem}
.legal-sec ul:last-child{margin-bottom:0}
.legal-sec li{margin-bottom:.5rem; padding-left:.25rem}
.legal-sec li::marker{color:var(--dim)}
.legal-sec strong{color:var(--bone); font-weight:500}
.legal-sec a{
  color:var(--bone);
  text-decoration:underline;
  text-decoration-color:rgba(255,255,255,.3);
  text-underline-offset:3px;
  transition:text-decoration-color .2s var(--ease);
}
.legal-sec a:hover{text-decoration-color:var(--cy)}

/* the identity block: label and value rows, the contact rows' hairline */
.legal-id{
  margin:0 0 1.25rem;
  display:grid; grid-template-columns:1fr;
  border-top:1px solid rgba(255,255,255,.06);
}
.legal-id dt{
  padding-top:.875rem;
  font-size:.6875rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--dim);
}
.legal-id dd{
  margin:0; padding:.25rem 0 .875rem;
  border-bottom:1px solid rgba(255,255,255,.06);
  font-size:.9375rem; color:var(--bone);
  overflow-wrap:anywhere;
}
@media (min-width:640px){
  .legal-id{grid-template-columns:minmax(9rem,12rem) 1fr; column-gap:1.5rem}
  .legal-id dt{padding-block:.875rem; border-bottom:1px solid rgba(255,255,255,.06)}
  .legal-id dd{padding-block:.875rem}
}

@media (min-width:1024px){
  .legal{
    grid-template-columns:minmax(12rem,16rem) minmax(0,1fr);
    gap:clamp(3rem,7vw,7rem);
    align-items:start;
  }
  .legal-toc{position:sticky; top:7.5rem}
}

/* the line under the contact rows that points at the privacy policy */
.contact-note{
  margin:1.25rem 0 0;
  font-size:.8125rem; color:var(--dim);
}
.contact-note a{
  color:var(--ash);
  text-decoration:underline;
  text-decoration-color:rgba(255,255,255,.3);
  text-underline-offset:3px;
}
.contact-note a:hover{color:var(--bone); text-decoration-color:var(--cy)}

/* ============================================================
   FOOTER
   ============================================================ */

/* The footer carries no glass of its own. All of it, including the part that
   continues past the end of the document, is one plate: .foot-glass.

   THE SEAM, and why this is one element. This was two for a revision, a
   footer plus an abutting stretch below it, each with its own
   backdrop-filter. That cannot be made to join. A backdrop-filter filters the
   backdrop and then clips the result to the element's border box, so each
   plate's blur samples across the other's edge and picks up the other's fill:
   the stretch's top band is contaminated by the brighter glassed pixels above
   it, the footer's bottom band by the unglassed field below. The two land at
   different values either side of the join, which reads exactly as it did on
   the phone, two blocks stuck together showing different depths. Matching the
   colours does not help, because the difference is in the filter and not in
   the fill. One filter, one plate.

   WHY IT IS FIXED, rather than the footer simply growing a viewport of bottom
   padding. Every in-flow version of that was built and measured, and each one
   either lengthens the page or breaks it:

     absolutely positioned ::after at top:100%          +812px of scroll
     position:fixed ::after inside the filtered footer  +812px
     bottom padding + matching negative bottom margin   +812px
     overflow:clip + overflow-clip-margin on .foot      +812px
     the same on body                                   +812px
     the same on html                                   0, but the page no
                                                        longer scrolls at all

   A fixed box contributes nothing to the scrollable overflow, and this one
   can be fixed now precisely because the footer no longer carries a filter:
   a filtered ancestor becomes the containing block for fixed descendants,
   which is what trapped the earlier attempt back into the flow.

   site.js aligns it to the footer's own top edge every frame while the footer
   is anywhere near the viewport, so it tracks through normal scrolling and
   through the bounce alike, and parks below the fold otherwise. With no JS it
   stays parked, which is a footer with no glass rather than a plate stranded
   across the page. */
.foot{
  position:relative;
  padding-block:clamp(2.5rem,6vw,3.5rem);
}

/* The content rides above the plate. Both sit in .foot's stacking context and
   the plate is positioned where a bare .foot-in is not, so without this the
   plate paints over its own footer's type. */
.foot-in{position:relative; z-index:1}

.foot-glass{
  position:fixed;
  left:0; right:0; top:0;
  /* The footer, plus a viewport of stretch beneath it, plus slack for the
     tallest the footer gets before site.js measures its real height.

     lvh, the LARGE viewport height, is the figure with the mobile toolbar
     collapsed, which is the tallest the viewport ever gets and therefore the
     one that guarantees a full screen of stretch no matter what the chrome
     is doing. vh is the same number on iOS and is kept as the fallback for
     engines that do not know lvh. Note this is the only viewport height left
     in the plate, and it is a length rather than a position: nothing about
     where the plate SITS comes from the viewport any more. See below. */
  height:calc(100vh + 420px);
  height:calc(100lvh + 420px);

  /* Hidden until site.js has placed it against the footer's own rect, and
     hidden again the moment the footer is out of range. This used to be a
     parked transform instead, translateY(innerHeight), and that is a
     viewport height baked into a position: iOS captures a small innerHeight
     while the toolbar is expanded, the toolbar collapses on the way down the
     page, the viewport grows, and the plate parked at the old bottom edge is
     left hovering a band of glass over the content. Out of sight has to mean
     out of sight regardless of what the browser chrome does, and only
     visibility gives that. */
  visibility:hidden;

  /* The pills' gradient held to the footer's own height, with --glass-tail
     carrying on below it: the same stop the gradient ends on, so the fill is
     continuous down the plate. One element, so one blur over all of it.

     The tail is a positioned layer that STARTS at --foot-h, not a plain
     background-color. A colour fills the whole box and would sit *under* the
     gradient rather than after it, and two translucent layers composite:
     .042 under .042 is .082 through the footer against .042 below it, a 4%
     step landing exactly on the footer's bottom edge. That was still a
     visible line after the two plates became one, and it is the reason a
     seam can survive having only one blur. The layers have to abut, not
     overlap. linear-gradient() of a single colour is the way to say "solid
     fill, but positionable". */
  background:
    var(--glass-fill) no-repeat 0 0 / 100% var(--foot-h, 200px),
    linear-gradient(var(--glass-tail), var(--glass-tail))
      no-repeat 0 var(--foot-h, 200px) / 100% 100%;
  -webkit-backdrop-filter:var(--glass-blur);
  backdrop-filter:var(--glass-blur);
  border-top:1px solid var(--glass-edge);
  border-radius:22px 22px 0 0;
  box-shadow:var(--glass-lift-top);   /* top edge only, see the token */

  pointer-events:none;
  will-change:transform;
}

/* Placed against the footer and tracking it. site.js adds this only after
   the first placement, so the plate is never shown at a stale position. */
.foot-glass.is-live{visibility:visible}

/* With no scripting the plate never gets placed and the footer would have no
   glass at all, so the glass goes back on the footer itself. No stretch in
   that case, but there is no plate to seam against either. Ignored by any
   engine that does not know the query, which leaves today's behaviour. */
@media (scripting: none){
  .foot{
    background:var(--glass-fill);
    -webkit-backdrop-filter:var(--glass-blur);
    backdrop-filter:var(--glass-blur);
    border-top:1px solid var(--glass-edge);
    border-radius:22px 22px 0 0;
    box-shadow:var(--glass-lift-top);
  }
  .foot-glass{display:none}
}

.foot-in{display:grid; gap:1.5rem; align-items:center}
.foot-logo{
  font-family:'Syncopate',sans-serif; font-size:.8125rem; font-weight:700;
  color:var(--bone);
}
.foot-c{margin:0; font-size:.875rem; color:var(--dim)}
.foot-nav{
  list-style:none; margin:0; padding:0;
  display:flex; flex-wrap:wrap; gap:1.25rem;
}
.foot-nav a{
  color:var(--ash); font-size:.875rem; text-decoration:none;
  transition:color .2s var(--ease);
}
.foot-nav a:hover{color:var(--bone)}

/* legal links: their own row under the rest, smaller and dimmer */
.foot-legal{
  grid-column:1 / -1;
  list-style:none; margin:0; padding:1.25rem 0 0;
  display:flex; flex-wrap:wrap; gap:.25rem 1.25rem;
  border-top:1px solid rgba(255,255,255,.06);
}
.foot-legal a{
  display:inline-flex; align-items:center; min-height:44px;
  color:var(--dim); font-size:.8125rem; text-decoration:none;
  transition:color .2s var(--ease);
}
.foot-legal a:hover{color:var(--bone)}

/* Above ~1200px the footer nav settles onto one row that reaches the right
   edge of .wrap, where the fixed WhatsApp float sits: measured at 1280 and
   1440, the float covered the last link by 44px. Reserve the float's own
   width plus its offset. Narrower viewports wrap the nav to two or three
   left-aligned rows and never reach the float. */
@media (min-width:1200px){
  .foot-nav{padding-right:calc(56px + clamp(1rem,3vw,1.75rem) + 1rem)}
}

/* ============================================================
   HERO ENTRANCE
   One orchestrated moment on load: the headline lines rise first,
   then the satellites around them settle in.
   ============================================================ */

.enter{
  opacity:0;
  transform:translateY(26px);
  will-change:opacity, transform;
}
.hero.in .enter{
  opacity:1;
  transform:none;
  transition:opacity .95s var(--ease) var(--d,0s),
             transform 1.05s var(--ease) var(--d,0s);
}

/* the headline rises out of its own line box rather than sliding as a block */
.hero-h1 span{
  display:block;
  clip-path:inset(-25% -20% -12% -20%);
}
.hero-h1 .enter{transform:translateY(0.16em)}
.hero.in .hero-h1 .enter{transform:none}

/* ============================================================
   REVEAL
   ============================================================ */

.reveal{
  opacity:0;
  transform:translateY(20px);
  transition:opacity .8s var(--ease), transform .8s var(--ease);
}
.reveal.in{opacity:1; transform:none}

/* ============================================================
   RESPONSIVE
   ============================================================ */

@media (min-width:640px){
  .foot-in{grid-template-columns:auto 1fr auto; gap:2rem}
  .foot-c{justify-self:start}
}

@media (min-width:768px){
  .cards{grid-template-columns:repeat(2,1fr)}
  .works{grid-template-columns:repeat(2,1fr)}
  .proc-head{grid-template-columns:1fr 1fr; align-items:end}
  .step{grid-template-columns:auto 1fr; gap:0 clamp(1.5rem,4vw,3rem)}
  .step-n{grid-row:1/3; padding-top:.5rem}
  .step h3{margin-bottom:.5rem}
}

@media (min-width:1024px){
  .nav-links{display:flex}
  .nav-cta{display:inline-flex}
  .nav-toggle{display:none}

  .cards{grid-template-columns:repeat(3,1fr); align-items:start}
  /* stagger the middle card down, as in the reference */
  .card-b{margin-top:clamp(2rem,5vw,4rem)}
  .card-c{margin-top:clamp(1rem,2.5vw,2rem)}

  .works{grid-template-columns:repeat(3,1fr)}
  .work-card{min-height:36rem}

  /* ---- hero: the asymmetric composition ----
     The headline owns rows 1 to 3 and the satellites live in rows below it.
     Tucking the paragraph into the counter-space beside line 2 reads well
     until the paragraph grows taller than a display line, at which point it
     runs into the line underneath, so it is given its own row instead. */
  .hero-grid{
    grid-template-columns:repeat(12,1fr);
    column-gap:clamp(1rem,2vw,1.75rem);
    row-gap:0;
    grid-template-rows:
      calc(var(--dsp) * var(--dsp-lh))
      calc(var(--dsp) * var(--dsp-lh))
      calc(var(--dsp) * var(--dsp-lh))
      auto
      auto;
  }

  .hero-h1{grid-column:1/-1; grid-row:1/4; padding-inline:0}

  .stat-a{
    grid-column:10/13;
    grid-row:1;
    align-self:end;
    text-align:right;
    justify-self:end;
  }
  .stat-a figcaption{margin-left:auto}

  .hero-lede{
    grid-column:1/6;
    grid-row:4;
    padding-top:clamp(1.5rem,4vw,2.75rem);
  }
  .hero-note{
    grid-column:8/13;
    grid-row:4;
    align-self:end;
    text-align:right;
    margin-left:auto;
    padding-top:clamp(1.5rem,4vw,2.75rem);
  }
  .stat-b{
    grid-column:1/5;
    grid-row:5;
    padding-top:clamp(1.75rem,4vw,2.75rem);
  }
  .hero-cta{
    grid-column:5/10;
    grid-row:5;
    justify-content:center;
    align-self:end;
    padding-top:clamp(1.75rem,4vw,2.75rem);
  }

  .l2{padding-left:0; text-align:right; padding-right:.4em}
  .l3{padding-left:1.1em}
}

@media (min-width:1440px){
  .hero-lede{grid-column:1/5}
}

/* subgrid is not required, but where it exists the h1 lines lock
   exactly to the hero rows instead of relying on line-box maths */
@supports (grid-template-rows:subgrid){
  @media (min-width:1024px){
    .hero-h1{
      display:grid;
      grid-template-columns:subgrid;
      grid-template-rows:subgrid;
    }
    .l1{grid-column:1/10; grid-row:1}
    .l2{grid-column:4/13; grid-row:2}
    .l3{grid-column:2/13; grid-row:3}
  }
}

/* ============================================================
   REDUCED MOTION
   Bubbles hold their position at final opacity. Nothing is hidden.
   ============================================================ */

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
  }
  .reveal{opacity:1; transform:none}
  .enter{opacity:1; transform:none}
  .work-card:hover .work-bg{transform:none}
}

/* ============================================================
   INTRO
   The opening bubble owns the screen alone, so every other layer is held
   back until it splits. Nothing here is applied by default: the classes are
   added by bubbles.js only once the field has actually started, so a missing
   WebGL context or a blocked module can never leave the page hidden.
   ============================================================ */

/* The whole page, not just the chrome. The field canvas sits at z-index 0
   and every content layer rides above it, so any content left on screen
   draws OVER the bubble rather than behind it. Hiding only the nav worked
   while the rest happened to be below the fold; scroll during the intro and
   the process section painted straight through the glass. */
body.intro-on main,
body.intro-on .foot,
body.intro-on .nav-wrap,
body.intro-on .wa-float{
  opacity:0;
  pointer-events:none;
}

/* Deliberately NOT transitioned. A fade here bought nothing, since the hero
   runs its own entrance underneath, and it put the whole page behind a
   transition that has to complete for the content to be visible at all: a
   stalled one leaves main at opacity 0 with no rule left setting it and no
   way back. Explicit 1, switched instantly, cannot strand the page. */
main,.foot{opacity:1; transition:none}
.wa-float{transition:opacity .8s var(--ease), transform .3s var(--ease)}

/* and it must not be scrollable while it plays, or you can leave the intro
   behind and arrive at the footer with the bubble still held over it */
body.intro-on{overflow:hidden; touch-action:none}

.intro{
  position:fixed; inset:0;
  z-index:6;                 /* over the field, under the nav that follows it */
  display:grid; place-items:center;
  pointer-events:none;
  visibility:hidden;
  padding:0 var(--gut);
}
/* is-out has to stay visible too: it is removed from is-on in the same
   instant it is added, and a visibility flip is not transitionable, so the
   words would blink out rather than flowing away. */
.intro.is-on,
.intro.is-out{visibility:visible}

.intro-mark{
  margin:0;
  text-align:center;
  font-family:'Syncopate',Arial Black,sans-serif;
  font-weight:700;
  /* Sized against the viewport HEIGHT, because the bubble is: its radius is
     held in half-height units, so it is about 92% of the viewport tall at
     every aspect. Sizing this in vw instead would let the lockup drift wider
     than the glass on a short, wide window and break out of its own rim. */
  font-size:clamp(1rem, 5.2vh, 3.2rem);
  line-height:1.12;
  letter-spacing:.2em;
  text-indent:.2em;          /* the tracking is trailing, so re-centre it */
  color:var(--bone);
}

/* ...and in portrait it is the half-WIDTH that bounds the mass, so the
   lockup has to be measured against the same side or it breaks out of the
   rim it is supposed to be surfacing through. The query is on the aspect and
   not on a pixel width because that is the actual condition: introR() clamps
   to whichever half-extent is smaller, which is the width on any window
   taller than it is wide, phone or not. */
@media (max-aspect-ratio: 1/1){
  .intro-mark{font-size:clamp(.75rem, 8.4vw, 3.2rem)}
}

/* Superposed: two lines locked one above the other, each clipped so its word
   rises out of the mask instead of simply fading up. That is what sells it as
   surfacing through the glass rather than appearing in front of it. */
.intro-word{display:block; overflow:hidden; padding:.06em 0}
.intro-word > span{
  display:block;
  transform:translate3d(0, 118%, 0) scale(.94);
  filter:blur(12px);
  opacity:0;
  transition:transform 1.05s var(--ease),
             filter .9s var(--ease),
             opacity .7s var(--ease);
}
.intro.is-on .intro-word > span{transform:none; filter:blur(0); opacity:1}
.intro.is-on .intro-word:nth-child(2) > span{transition-delay:.16s}

/* and away again, upward and out of focus, as the bubble breaks apart */
.intro.is-out .intro-word > span{
  transform:translate3d(0, -46%, 0) scale(1.06);
  filter:blur(16px);
  opacity:0;
  transition-duration:.7s, .6s, .55s;
  transition-delay:0s;
}
.intro.is-out .intro-word:nth-child(2) > span{transition-delay:.07s}

@media (prefers-reduced-motion: reduce){
  .intro{display:none}
  body.intro-on{overflow:visible; touch-action:auto}
  body.intro-on main,
  body.intro-on .foot,
  body.intro-on .nav-wrap,
  body.intro-on .wa-float{opacity:1; pointer-events:auto}
}
