/* ==========================================================================
   TARESLABS — site stylesheet
   Palette is near-monochrome to sit with the chrome/liquid-metal footage;
   the periwinkle accent is rationed to labels, indices and hairlines.
   ========================================================================== */

/* --- tokens ---------------------------------------------------------- */
:root {
  --bg:        #050506;
  --bg-2:      #0a0a0c;
  --bg-3:      #101015;
  --bg-4:      #16161c;

  --ink:       #f4f4f2;
  --ink-2:     #a6a6ad;
  --ink-3:     #6e6e79;

  --line:      rgba(255, 255, 255, .09);
  --line-2:    rgba(255, 255, 255, .17);

  --accent:    #b9a6ff;
  --accent-2:  #7fe3d4;
  --accent-dim:rgba(185, 166, 255, .13);

  /* A surface that sits ABOVE the page rather than alongside it. The other bg
     tokens are all within a few points of --bg and need a hairline to be read
     as separate; this one is far enough off it to hold its own shape with no
     border at all. Used by the nav CTA and by the loading plate, so the two
     read as the same material.
     Neutral by design: an earlier value carried a little more red than green
     and read as brown once it had a light label sitting on it. */
  --raised:    #161616;

  --font-display: "Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-body:    "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --shell:     min(1240px, 100% - 2 * var(--gutter));
  --gutter:    clamp(20px, 5vw, 64px);
  --nav-h:     72px;

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

  --r-sm:      10px;
  --r:         16px;
  --r-lg:      24px;
}

/* --- reset ----------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img, canvas, svg, video { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -.03em;
  line-height: 1.02;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

a { color: inherit; text-decoration: none; }

::selection { background: var(--accent); color: #0a0a0c; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* --- utilities ------------------------------------------------------- */
.shell { width: var(--shell); margin-inline: auto; }

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0;
}
.eyebrow--muted { color: var(--ink-3); }

.lede {
  font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 62ch;
}

.rule {
  height: 1px;
  border: 0;
  margin: 0;
  background: linear-gradient(90deg, var(--line-2), transparent 62%);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; top: 8px; left: 8px; z-index: 200;
  padding: 10px 16px; background: var(--ink); color: var(--bg);
  border-radius: var(--r-sm); font-weight: 600;
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

/* --- buttons --------------------------------------------------------- */
.btn {
  --btn-bg: var(--ink);
  --btn-fg: #08080a;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 13px 22px;
  border: 1px solid transparent;
  border-radius: 100px;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -.01em;
  white-space: nowrap;
  cursor: pointer;
  transition: transform .35s var(--ease), background .35s var(--ease),
              border-color .35s var(--ease), color .35s var(--ease);
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn svg { width: 14px; height: 14px; transition: transform .35s var(--ease); }
.btn:hover svg { transform: translate(2px, -2px); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  border-color: var(--line-2);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.btn--ghost:hover { border-color: var(--ink); background: rgba(255, 255, 255, .05); }

.btn--lg { padding: 17px 30px; font-size: 15px; }

/* --- interactive hover button ----------------------------------------
   The reference component, replicated literally — this one is self-contained
   (every style is an inline Tailwind utility), so these are its exact values:
   w-32 128px, p-2 8px, border 1px, rounded-full, font-semibold, translate-x-1
   4px, translate-x-12 48px, -translate-x-1 -4px, h-2/w-2 8px, rounded-lg 8px,
   left-[20%] top-[40%], scale-[1.8], duration-300.

   Palette mapping into this site's tokens: bg-background -> --bg-2,
   border -> --line-2, bg-primary -> --ink, text-primary-foreground -> the
   dark ink the site's own light buttons use. So the resting state is a dark
   outlined pill and the hover state is the light pill with dark type that
   .btn already establishes as this site's primary action.

   The dot deliberately carries NO z-index, exactly as in the component: it is
   the only positioned sibling below the z-10 layer, so it paints over the
   resting label (that small dot to the left of the text is the signature)
   and under the hover label it expands into. */
.ihb {
  position: relative;
  display: inline-block;
  width: 128px;
  padding: 8px;
  overflow: hidden;
  cursor: pointer;
  /* No border. The fill is light enough against the page that the pill's own
     shape is what defines it — an outline on top of a surface this distinct
     just draws a second edge around an edge that already reads. */
  border: 0;
  border-radius: 9999px;
  background: var(--raised);
  color: var(--ink);
  text-align: center;
  font-weight: 600;
}

/* The animated liquid-metal rim that used to sit here is gone with the border.
   It was a conic gradient masked to a 1.5px ring, and its whole job was to
   give an otherwise near-invisible pill a visible edge; a raised fill does
   that without a moving highlight competing with the label. The shader
   buttons under the headline still carry the real thing — see widget/. */

/* The component's own escape hatch, used as intended. w-32 is sized for its
   default "Button" label (~48px wide). "Comenzar" plus the 8px gap and the
   24px arrow measures 111.8px against the 112px that w-32 leaves inside p-2 —
   0.2px of slack, which is why the hover row sat flush against both edges.
   w-40 (160px) is the next Tailwind step and leaves 16px a side.
   This is what `<InteractiveHoverButton className="w-40" />` compiles to; the
   component takes className precisely so the width can be set per label. */
.ihb--nav { width: 160px; }
/* left-[20%] is a proportion, so widening the pill to w-40 walked the dot
   32px in from the edge while the longer label starts further left — the gap
   between them closed from the reference's ~10px to ~4px. 16% puts it back
   where it reads in the original. */
.ihb--nav .ihb__dot { left: 16%; }

.ihb__label {
  display: inline-block;
  transform: translateX(4px);
  transition: all .3s;
}
.ihb:hover .ihb__label { transform: translateX(48px); opacity: 0; }

.ihb__hover {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 10;
  display: flex;
  height: 100%;
  width: 100%;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transform: translateX(48px);
  opacity: 0;
  color: #08080a;
  transition: all .3s;
}
.ihb:hover .ihb__hover { transform: translateX(-4px); opacity: 1; }
.ihb__hover svg { width: 24px; height: 24px; flex: none; }

.ihb__dot {
  position: absolute;
  left: 20%;
  top: 40%;
  height: 8px;
  width: 8px;
  transform: scale(1);
  border-radius: 8px;
  background: var(--ink);
  transition: all .3s;
}
.ihb:hover .ihb__dot {
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  transform: scale(1.8);
}

/* text link with a wipe underline */
.tlink {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 14px; font-weight: 600;
  padding-bottom: 3px;
  background-image: linear-gradient(90deg, var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size .45s var(--ease), color .3s var(--ease);
}
.tlink:hover { background-size: 100% 1px; }
.tlink svg { width: 13px; height: 13px; transition: transform .35s var(--ease); }
.tlink:hover svg { transform: translate(2px, -2px); }

/* ==========================================================================
   NAV
   ========================================================================== */
/* The seam this is built to avoid comes from masking a SINGLE blur layer.
   A mask fades that layer's OPACITY, but its blur RADIUS never changes — so
   just below where the mask reaches zero, the page goes from fully blurred to
   perfectly sharp within a few pixels. Against high-contrast content, like the
   headline passing underneath, that reads as a hard line across the page.

   The fix is to taper the RADIUS, which a mask cannot do to one layer but four
   layers can do between them. Each .nav__veil span blurs the result of the
   ones painted before it, so the radii accumulate: near the top all four are
   opaque and the blur is the strongest; further down the wide-radius layers
   have already masked out and only the narrow ones remain; at the bottom edge
   the only thing still fading is a 2px blur, and 2px resolving to 0 is not
   something an eye can find.

   ::after stays a pure colour gradient over a much longer distance. It carries
   the darkening that keeps the nav legible, and a plain alpha fade cannot
   ghost or seam the way a masked blur can. */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--nav-h);
  display: flex;
  align-items: center;
}
.nav__veil {
  position: absolute;
  inset: 0 0 auto 0;
  /* Taller than the bar itself: the ramp needs room. Squeeze it and the
     layers stack up too close together to read as a gradient. */
  height: calc(var(--nav-h) + 96px);
  pointer-events: none;
}
.nav__veil span { position: absolute; inset: 0; }

/* Widest radius, shortest reach — and each step down halves the blur while
   extending further, so the falloff is roughly exponential rather than linear.
   Deliberately gentle: the point is that the bar sits on the page without a
   line under it, not that the page behind it becomes unreadable. Four layers
   accumulate, so the radii here are far lower than a single layer would need.
   saturate rides on the first layer only; applying it four times over would
   compound into a visible colour shift under the bar. */
.nav__veil span:nth-child(1) {
  backdrop-filter: blur(7px) saturate(1.15);
  -webkit-backdrop-filter: blur(7px) saturate(1.15);
  mask-image: linear-gradient(180deg, #000 0%, #000 12%, transparent 34%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 12%, transparent 34%);
}
.nav__veil span:nth-child(2) {
  backdrop-filter: blur(3.5px);
  -webkit-backdrop-filter: blur(3.5px);
  mask-image: linear-gradient(180deg, #000 0%, #000 26%, transparent 52%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 26%, transparent 52%);
}
.nav__veil span:nth-child(3) {
  backdrop-filter: blur(1.8px);
  -webkit-backdrop-filter: blur(1.8px);
  mask-image: linear-gradient(180deg, #000 0%, #000 44%, transparent 72%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 44%, transparent 72%);
}
.nav__veil span:nth-child(4) {
  backdrop-filter: blur(0.9px);
  -webkit-backdrop-filter: blur(0.9px);
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
}
.nav::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: calc(var(--nav-h) + 190px);
  pointer-events: none;
  background: linear-gradient(180deg, rgba(5, 5, 6, .48) 0%, rgba(5, 5, 6, .26) 30%, rgba(5, 5, 6, .1) 60%, transparent 100%);
  transition: opacity .5s var(--ease);
}
.nav.is-stuck::after {
  background: linear-gradient(180deg, rgba(5, 5, 6, .72) 0%, rgba(5, 5, 6, .4) 30%, rgba(5, 5, 6, .16) 60%, transparent 100%);
}

.nav__inner {
  /* positioned WITH an explicit positive z-index (not just position:relative)
     so it paints above BOTH ::before and ::after regardless of DOM order —
     ::after is generated as the LAST child, so without this it would paint
     OVER the real nav content, not just behind it */
  position: relative;
  z-index: 1;
  width: var(--shell);
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: 28px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -.045em;
  margin-right: auto;
}
/* The mark carries its own colour, so it is a background-image on a span
   rather than an <img>: it repeats in the nav and the footer of every page and
   has no meaning a reader would miss — the wordmark beside it already says the
   name, and a second copy of it in an alt attribute is just noise.

   The artwork exactly as supplied, by request. Its second stroke is #1e1c1e
   against a #050506 page, so at this size that half of the mark is essentially
   not there and what reads is the purple diagonal alone. The glow is what
   keeps the dark stroke from vanishing outright — it traces the mark's alpha,
   so it lifts the near-black edge just off the background. */
.brand__mark {
  width: 25px; height: 22px;
  background: url("../img/logo-tareslabs.png") center / contain no-repeat;
  filter: drop-shadow(0 0 14px rgba(185, 166, 255, .34));
}

.nav__links {
  display: flex;
  align-items: center;
  gap: 4px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav__link {
  position: relative;
  display: block;
  padding: 8px 14px;
  border-radius: 100px;
  font-size: 14px;
  color: var(--ink-2);
  transition: color .3s var(--ease), background .3s var(--ease);
}
.nav__link:hover { color: var(--ink); background: rgba(255, 255, 255, .05); }
.nav__link[aria-current="page"] {
  color: var(--ink);
  background: rgba(255, 255, 255, .07);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* shown only inside the mobile panel — see the max-width:900px block below */
.nav__cta-m { display: none; }

.nav__toggle {
  display: none;
  width: 42px; height: 42px;
  align-items: center; justify-content: center;
  background: none; border: 1px solid var(--line-2);
  border-radius: 100px; cursor: pointer;
}
.nav__toggle span {
  display: block; width: 16px; height: 1px; background: var(--ink);
  position: relative;
  transition: background .2s var(--ease);
}
.nav__toggle span::before,
.nav__toggle span::after {
  content: ""; position: absolute; left: 0; width: 16px; height: 1px;
  background: var(--ink); transition: transform .35s var(--ease);
}
.nav__toggle span::before { top: -5px; }
.nav__toggle span::after  { top: 5px; }
.nav.is-open .nav__toggle span { background: transparent; }
.nav.is-open .nav__toggle span::before { transform: translateY(5px) rotate(45deg); }
.nav.is-open .nav__toggle span::after  { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 900px) {
  .nav__toggle { display: inline-flex; }
  /* .nav__cta itself is gone (replaced by the mount span below), but this
     selector is harmless to keep in case anything still carries the class */
  .nav__cta { display: none; }
  /* .lm-btn-mount.lm-btn-mount--nav (not just .lm-btn-mount--nav): the base
     .lm-btn-mount { display: inline-block } rule sits later in the file and
     ties on specificity at one class each — source order would silently win
     for the unconditional rule regardless of this media query matching. */
  .lm-btn-mount.lm-btn-mount--nav { display: none; }
  .nav__links {
    position: fixed;
    inset: var(--nav-h) 0 auto 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 12px var(--gutter) 28px;
    background: rgba(5, 5, 6, .95);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid var(--line);
    transform: translateY(-8px);
    opacity: 0;
    pointer-events: none;
    transition: opacity .3s var(--ease), transform .3s var(--ease);
  }
  .nav.is-open .nav__links { opacity: 1; transform: none; pointer-events: auto; }
  .nav__link { padding: 15px 4px; font-size: 17px; border-radius: 0; }
  .nav__link[aria-current="page"] { background: none; box-shadow: none; color: var(--accent); }
  /* the header CTA is hidden on mobile — reinstate it inside the panel */
  .nav__cta-m { display: block; margin-top: 18px; }
  .nav__cta-m .btn { width: 100%; justify-content: center; }
}

/* ==========================================================================
   SCROLL STAGE — the sticky canvas the page scrolls over
   ========================================================================== */
.stage { position: relative; }

/* the pinned viewport */
.stage__viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  background: var(--bg);
}

/* Pinned hero plate. Stays fixed while the page scrolls over it.
   No transform scale here on purpose — any extra scaling is upscaling, and the
   source is 1751px wide, so it costs sharpness for nothing on a still image. */
/* The image lives here, not in an inline style on the div as it used to. An
   inline background-image outranks every stylesheet rule including media
   queries, so the portrait swap below could never have taken effect while the
   markup carried its own style attribute. */
.stage__still {
  position: absolute;
  inset: 0;
  background: var(--bg) url("../img/hero-tall.webp") center / cover no-repeat;
  pointer-events: none;
}

/* Dedicated portrait plate for phones. The landscape render's iridescent ribbon
   sweeps left-to-right, so `cover` on a tall narrow viewport zooms way in and
   throws most of that sweep off both edges — you get a black frame with a
   fragment of blob. hero-phone.webp is a purpose-made 1290x2521 render with the
   ribbon standing up through the top two thirds and the blob sitting on the
   bottom edge, so it reads at full length without any cropping tricks.
   Swapped by media query rather than by rotating .stage__still with a
   transform: rotating the box means swapping its width/height and correcting
   the origin, and it forces the compositor to resample a full-viewport layer
   on every paint. A second asset costs 333KB and none of that. */
@media (max-width: 760px) and (orientation: portrait) {
  .stage__still { background-image: url("../img/hero-phone.webp?v=60"); }
}

/* keeps type legible over the plate's glossy highlights */
.stage__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(120% 80% at 50% 50%, transparent 30%, rgba(5, 5, 6, .55) 100%),
    linear-gradient(180deg, rgba(5, 5, 6, .8) 0%, transparent 24%, transparent 66%, rgba(5, 5, 6, .92) 100%);
}

/* The hero copy is left-aligned while the subject sits centre-right. Weight the
   scrim toward the text column so the type keeps its contrast without dimming
   the art. Layered onto the scrim itself rather than a pseudo-element so the
   stacking order stays predictable. */
@media (min-width: 900px) {
  .stage__scrim {
    background:
      linear-gradient(90deg, rgba(5, 5, 6, .78) 0%, rgba(5, 5, 6, .42) 32%, transparent 60%),
      radial-gradient(120% 80% at 50% 50%, transparent 30%, rgba(5, 5, 6, .55) 100%),
      linear-gradient(180deg, rgba(5, 5, 6, .8) 0%, transparent 24%, transparent 66%, rgba(5, 5, 6, .92) 100%);
  }
}

.stage__grain {
  position: absolute;
  inset: -50%;
  pointer-events: none;
  opacity: .16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --- content that scrolls over the pinned plate ---------------------- */
.stage__flow {
  position: relative;
  z-index: 2;
  margin-top: -100svh;
  pointer-events: none;
}
.stage__flow a, .stage__flow button { pointer-events: auto; }

/* Block heights set the scroll runway. Total flow height minus one viewport
   is how far the canvas stays pinned, so these are the dial for "how long
   does the scrub last". Tuned to the 10.7s source clip. */
.stage__block {
  min-height: var(--block-h, 100svh);
  display: flex;
  align-items: center;
  padding-block: 8vh;
}
.stage__block--hero  { --block-h: 92svh; }
.stage__block--card  { --block-h: 60svh; }
.stage__block--final { --block-h: 80svh; }
.stage__block > .shell { width: var(--shell); }

/* hero block */
.stage-hero__title {
  font-size: clamp(52px, 11.5vw, 168px);
  line-height: .88;
  letter-spacing: -.055em;
  font-weight: 500;
  margin: 18px 0 0;
}
/* Line break only — the em carries no colour of its own, so "Más" renders in
   the inherited white. */
.stage-hero__title em {
  font-style: normal;
  display: block;
  /* Opens the gap between the two lines without touching line-height. The .88
     above is what keeps each line's own leading tight at display size, and
     raising it would loosen the whole block rather than just the seam between
     the lines. In em, so the gap scales with the clamped font-size instead of
     being generous on a phone and invisible at 168px. */
  margin-top: .12em;
}
/* The tinted word. Kept inside the lavender family end to end rather than
   ramping up from white the way the old full-line gradient did: across one
   short word, a white-to-accent fade reads as a smudge, not colour. */
.stage-hero__title em span {
  background: linear-gradient(96deg, #d7ccff 4%, var(--accent) 54%, #a996ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stage-hero__sub {
  margin-top: clamp(24px, 3.5vw, 40px);
  max-width: 46ch;
  font-size: clamp(16px, 1.4vw, 19px);
  color: var(--ink-2);
}
.stage-hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 32px;
}

/* Reserves the shader button's own default 142x46 footprint (see
   widget/src/components/ui/liquid-metal-button.tsx) so the row doesn't
   reflow once assets/js/vendor/liquid-metal-button.js mounts over it. */
.lm-btn-mount { display: inline-block; width: 142px; height: 46px; }
.lm-btn-mount--nav { flex-shrink: 0; }
/* Wider footprint for the inverted hero button — must stay in step with its
   data-width, which is what the component actually renders at. */
.lm-btn-mount--wide { width: 196px; }

.scroll-hint {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: clamp(36px, 6vh, 64px);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.scroll-hint i {
  display: block; width: 1px; height: 34px;
  background: linear-gradient(180deg, var(--accent), transparent);
  animation: drip 2.4s var(--ease-io) infinite;
  transform-origin: top;
}
@keyframes drip {
  0%, 100% { transform: scaleY(.3); opacity: .4; }
  50%      { transform: scaleY(1);  opacity: 1; }
}

/* narrative cards riding over the canvas */
.stage__block--right { justify-content: flex-end; }
.stage__block--right .stage-card { margin-left: auto; }

.stage-card {
  width: min(460px, 100%);
  padding: 30px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: linear-gradient(165deg, rgba(22, 22, 28, .78), rgba(8, 8, 11, .68));
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .9);
}
.stage-card__idx {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .16em;
  color: var(--accent);
}
.stage-card h2 {
  font-size: clamp(28px, 3.4vw, 42px);
  margin: 14px 0 12px;
}
.stage-card p { color: var(--ink-2); font-size: 15.5px; }

/* closing block of the stage */
.stage-final { text-align: center; margin-inline: auto; max-width: min(860px, 100%); }
.stage-final h2 {
  font-size: clamp(38px, 6.4vw, 90px);
  letter-spacing: -.05em;
  line-height: .96;
}
.stage-final p { margin: 22px auto 0; max-width: 46ch; color: var(--ink-2); }

@media (max-width: 720px) {
  .stage-card { width: 100%; padding: 24px; }
  .stage__block--right { justify-content: flex-start; }
  .stage__block--right .stage-card { margin-left: 0; }
  /* narrow screens put type directly over the subject — hold the contrast */
  .stage__scrim {
    background:
      radial-gradient(105% 62% at 50% 46%, transparent 12%, rgba(5, 5, 6, .72) 100%),
      linear-gradient(180deg, rgba(5, 5, 6, .88) 0%, rgba(5, 5, 6, .34) 30%, rgba(5, 5, 6, .34) 62%, rgba(5, 5, 6, .95) 100%);
  }
}

/* --- compact stage used on the service pages ------------------------- */
/* Service pages scrub a short slice, so the runway is deliberately brief —
   the effect is a garnish here, not the main event. */
.stage--compact .stage__viewport { height: 100svh; }
.stage--compact .stage__block--hero  { --block-h: 100svh; }
.stage--compact .stage__block--final { --block-h: 76svh; }
.stage--compact .stage-hero__title { font-size: clamp(42px, 8.2vw, 108px); }

/* ==========================================================================
   HOME HERO — plate and copy scroll together
   --------------------------------------------------------------------------
   .hero-bg is position:ABSOLUTE, not fixed. It still holds nothing but the
   image and its layers, and still occupies no flow height, but it is anchored
   to the top of the DOCUMENT rather than the viewport — so it travels up with
   the copy as one unit instead of sitting still behind it.

   It was fixed, which pinned the art while the text slid over it. Two things
   pushed it here: Safari refuses to paint fixed content under its bottom
   toolbar (so the plate could never reach the phone's bottom edge), and the
   reveal is now scroll-driven rather than cursor-driven, which reads wrong on
   a plate that does not move.

   #about is an ordinary opaque later-in-DOM section, so it still rises and
   covers the plate: no JS, no transform, just document order.
   ========================================================================== */
.hero-bg {
  position: absolute;
  inset: 0;
  height: 100svh;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
}

/* The art holds still. This element exists only to keep the two plates —
   the render and its white twin — on a single box, so the hover porthole can
   never drift out of register with the art behind it. It fills the fixed
   window exactly, which also means site.js measures a travel distance of
   zero and leaves the transform alone: the image is locked from first paint
   and stays put for the whole scroll, with #about rising to cover it. */
.hero-bg__scroll {
  position: absolute;
  inset: 0;
}

.hero-content {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  align-items: flex-start;
  padding-top: calc(var(--nav-h) + 7vh);
  padding-bottom: 8vh;
}

/* One gutter of inset on the left instead of margin-auto's centered ~100px —
   noticeably closer to the edge without touching it — and a smaller title,
   scoped to the home hero only via this ancestor so the service pages'
   compact stage headers (which share .stage-hero__title) are untouched. */
.hero-content .shell {
  margin-inline: auto;
  width: min(1240px, 100% - 2 * var(--gutter));
  text-align: center;
}
/* The home hero carries no .stage-hero__sub — the headline goes straight to the
   buttons. The service pages still use the class (see websites.html et al), so
   the base rule at the top of this file stays. */
.hero-content .stage-hero__actions {
  justify-content: center;
  gap: 14px;
  margin-top: clamp(26px, 3.4vw, 38px);
}
/* Sized off the viewport rather than a fixed step so the two lines keep
   filling the same share of the screen at any width. The cap is what stops
   it running away on an ultrawide, where 10vw would otherwise put the
   headline past the shell's 1240px and into the gutters. */
.hero-content .stage-hero__title {
  font-size: clamp(41px, 9.2vw, 146px);
}

/* The panel that rises to cover the section before it — then, being sticky
   itself, pins in turn until the NEXT one covers it. Chaining the same trick
   down every section boundary (see index.html: about/services/process/
   pricing/faq/cta all carry this class) is what makes the reveal repeat
   instead of firing once. A section only visibly "holds" if its own content
   is taller than one viewport, true of every section here.
   Rounded top + a lifted shadow read as a stacked card rather than a section
   that merely follows; the hairline glow sells the seam even in a near-mono
   palette. Two background tones (bg-2 / bg-3) alternate down the stack so
   consecutive cards are still visually distinct from one another. */
/* Double-classed selector on purpose: .section's own `position: relative` sits
   later in the file, and single-class specificity ties are broken by source
   order — .section would silently win and cancel the sticky pin. Requiring
   both classes (as every real usage already does) is immune to that. */
/* Not sticky — the cover-reveal is meant to happen once, for the hero-to-about
   handoff, then the rest of the page is plain, ordinary scrolling. #about
   doesn't need to pin itself to cover the (position:fixed) hero underneath;
   it just needs to be later in the document and opaque, which normal flow
   already gives it. Sticky was only ever needed to make #about chain into
   covering the NEXT section too — removed on purpose, not a regression. */
/* Light card on a dark site, on purpose — the panel that covers the hero
   flips from black to white, echoing the hero's own black-to-white reveal.
   Re-declaring the ink/line tokens locally (rather than restyling every
   child selector by hand) means every descendant that reads var(--ink) etc.
   — eyebrow, heading, lede, stat labels — picks up light-appropriate colors
   automatically through normal CSS custom-property inheritance.
   Background is an explicit white, not var(--bg-2): .stat tiles below also
   read --bg-2 for their own fill, so reusing it here would make the tiles
   and the section itself the same flat white with no contrast between them. */
.section.section--cover {
  position: relative;
  z-index: 1;
  background: #ffffff;
  border-radius: 32px 32px 0 0;
  box-shadow: 0 -60px 100px -40px rgba(0, 0, 0, .75);
  /* color:var(--ink) is only declared once, on <body> — inheritance copies
     that ALREADY-RESOLVED rgb() value down to descendants, it doesn't
     re-run var() at each one. Redefining --ink above did nothing for h2
     (no color of its own) even though it correctly reached .lede (which
     declares color:var(--ink-2) on itself). Declaring color here, on the
     element that actually has the new --ink in scope, is what makes the
     re-evaluation — and therefore the inheritance to h2 — happen at all. */
  color: var(--ink);
  --bg-2: #f1f1ee;
  --bg-3: #e7e7e3;
  --bg-4: #dcdcd7;
  --ink: #0a0a0c;
  --ink-2: #46464e;
  --ink-3: #77777f;
  --line: rgba(10, 10, 12, .1);
  --line-2: rgba(10, 10, 12, .18);
}
/* .stat__v's gradient is hardcoded (light-on-dark), not token-based, so the
   variable overrides above don't reach it — flipped by hand to a dark
   gradient so the numbers stay legible on the new white tiles. */
.section--cover .stat__v {
  background: linear-gradient(158deg, #0a0a0c 12%, #55555e 100%);
  -webkit-background-clip: text;
  background-clip: text;
}

/* --- phone hero: edge to edge, no gaps -------------------------------------
   Three separate things leave a strip of page showing on a phone, and all
   three are invisible in a desktop browser:

   1. position: fixed CANNOT reach under Safari's bottom controls. This is the
      big one, and it is WebKit behaviour, not a sizing mistake: Safari refuses
      to paint fixed (and sticky) content below the bottom toolbar, clipping it
      instead of rendering it behind those transparent floating controls. No
      height fixes it — 100lvh, negative bottom, none of it. That is exactly
      why the white #about card could fill that strip while this plate could
      not: the card is in normal flow, the plate was fixed. Reported as an iOS
      26 bug (developer.apple.com/forums/thread/800798) with no official
      workaround. So on the phone the plate is `absolute` instead: still out of
      flow, still pinned to the top of the document, but laid out against the
      page rather than the viewport, which puts it under the toolbar like any
      other in-flow content. The trade is that it scrolls with the page instead
      of staying pinned — no loss here, since #about scrolls up over it within
      one screen anyway.
   2. svh is the SMALL viewport — the height with the browser chrome showing.
      The moment Safari retracts its address bar the visible area grows and a
      100svh plate no longer reaches the bottom. lvh is the LARGE viewport, so
      the plate is sized for the chrome-hidden state and simply overflows (into
      `overflow: hidden`) while the bar is up. Never short, either way.
   3. Without viewport-fit=cover — now set in every page's viewport meta — iOS
      keeps the page inside the safe area, so the notch and home-indicator
      bands stay page-coloured no matter how the plate is sized. With it, the
      layout runs under them and the negative insets below push the plate out
      past the rounded display corners.

   Scoped to the portrait phone so the desktop hero, which is measured against
   svh by site.js's pin logic, is untouched. */
@media (max-width: 760px) and (orientation: portrait) {
  .hero-bg {
    margin: 0;
    top: calc(-1 * env(safe-area-inset-top, 0px));
    left: calc(-1 * env(safe-area-inset-left, 0px));
    right: calc(-1 * env(safe-area-inset-right, 0px));
    width: calc(100% + env(safe-area-inset-left, 0px) + env(safe-area-inset-right, 0px));
    /* NOT a viewport height. The plate is given the render's own ratio, so its
       height follows from its width — at full screen width that is exactly the
       height the image wants to be, and `cover` has nothing left to crop on
       either axis.

       Forcing a viewport height here is what caused the 9.9% side loss: cover
       fills the box, so a 1:1.954 render stretched to fill a 1:2.168 box has to
       scale until it is 436px wide inside 393px, and the surplus comes off both
       edges. Matching the ratio removes the surplus instead of hiding it.

       This number IS the image's aspect ratio — update it whenever the render
       is replaced, or the crop comes back. */
    aspect-ratio: 1290 / 2521;
    height: auto;
  }
  /* The copy has to come back inside the safe area the plate just escaped. */
  .hero-content {
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
    /* Derived from the render, not from the viewport — the same ratio and the
       same width as the plate, so this block ends exactly where the image ends.
       #about is .section--cover, an opaque white panel that starts wherever
       this block stops, so matching them is what puts its top edge ON the
       image's bottom edge: no band of art hidden behind the card, no band of
       card lying over the art.

       Every viewport-height value tried here (100svh, 100lvh, 80lvh) picked a
       height the image was not, which is what kept hiding one edge or the
       other. Keep this in step with the plate's aspect-ratio above; both carry
       the render's dimensions and both need updating when it is replaced. */
    /* --hero-reveal is the share of the image left uncovered. At 1 the card's
       top edge lands exactly on the image's bottom edge; below 1 it rides up
       over the art by the remainder — .88 hides the bottom 12%, which is the
       densest part of the blob and reads as the card sitting on top of it
       rather than after it. The plate keeps its full height either way; this
       only moves where the card starts. */
    --hero-reveal: .97;
    min-height: calc(100vw * 2521 / 1290 * var(--hero-reveal));

    /* Centred, not top-aligned. The base rule stacks the copy under the nav
       with `align-items: flex-start` and a 7vh top pad, which on a phone
       leaves it sitting high with the whole blob empty beneath it.
       The paddings are symmetric ON PURPOSE: making them both var(--nav-h)
       keeps the box's centre the screen's centre, while still guaranteeing the
       copy never runs under the fixed nav. An asymmetric pair would centre the
       content somewhere other than the middle of the screen. */
    align-items: center;
    padding-top: var(--nav-h);
    /* Sits ABOVE centre, not on it. The extra bottom padding is what lifts it:
       with align-items:center the copy settles in the middle of what's left
       after the padding, so padding the bottom more pushes it up. --hero-lift
       is the whole adjustment — 0 is dead centre, larger is higher. */
    --hero-lift: 16vh;
    padding-bottom: calc(var(--nav-h) + var(--hero-lift));
  }

  /* Bigger on the phone than the fluid rule gives. That rule's floor (41px)
     was set for the narrowest phones and leaves a lot of gutter on a modern
     one. The ceiling here is measured, not guessed: "Menos manual." is the
     longest line, and it fills the shell exactly at 50.9px on a 393px screen
     and 55.8px on a 430px one. 12.4vw sits just under that at every width in
     between, and the 52px cap keeps the widest phones off the ceiling.
     Re-measure if the headline copy ever changes — a longer line lowers the
     ceiling and the text would start wrapping to three lines. */
  .hero-content .stage-hero__title {
    font-size: clamp(44px, 12.4vw, 52px);
  }
}

/* ==========================================================================
   GENERIC SECTIONS
   ========================================================================== */
/* Explicit background, not "relies on body showing through" — .hero-bg is
   position:fixed for the entire rest of the page (not just until #about), so
   ANY later section without its own opaque fill lets that fixed image bleed
   through whenever it scrolls under that fixed screen band. Base it here so
   every section is safe by default, not just the ones someone remembered to
   patch — .section--alt overrides to a lighter tone where that's wanted. */
.section { padding-block: clamp(88px, 13vh, 152px); position: relative; background: var(--bg); }
.section--tight { padding-block: clamp(64px, 9vh, 104px); }
.section--alt { background: var(--bg-2); border-block: 1px solid var(--line); }

.section__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(24px, 5vw, 72px);
  align-items: end;
  margin-bottom: clamp(40px, 6vw, 72px);
}
.section__head h2 {
  font-size: clamp(34px, 5.2vw, 64px);
  letter-spacing: -.042em;
}
.section__head .eyebrow { margin-bottom: 18px; }
@media (max-width: 860px) {
  .section__head { grid-template-columns: 1fr; align-items: start; }
}

/* --- reveal on scroll ------------------------------------------------ */
.reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .9s var(--ease), transform .9s var(--ease);
  transition-delay: var(--d, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }

/* ==========================================================================
   HERO CURSOR REVEAL — circular porthole to the white twin, full-size
   --------------------------------------------------------------------------
   Same background treatment as .stage__still (cover, centered) — full-size,
   not a small card. At rest it's entirely clipped out (radius 0). While the
   cursor is over the hero, assets/js/site.js moves and grows a circular
   clip-path to track the pointer, so the white render shows through a
   porthole that follows the mouse instead of appearing all at once. JS
   drives clip-path directly (not CSS custom properties) since the value
   itself, not just one changing part of it, needs to move every frame.
   ========================================================================== */
.hero-bg__alt {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The image is handed to the CELLS, not painted here — if this element
     painted it too, the whole render would already be present underneath the
     dissolve. It travels as a custom property so site.js can read which render
     is in play at this breakpoint and measure its intrinsic size. */
  --alt-img: url("../img/marble-sphere.webp?v=60");
  background-image: none;
}

/* One cell of the dissolve grid. Built by site.js, which also sets each cell's
   box, its slice of the render, and its own scroll range. */
.hero-cell {
  position: absolute;
  background-image: var(--alt-img);
  background-repeat: no-repeat;
  opacity: 0;
}
@supports (animation-timeline: scroll()) {
  .hero-cell {
    /* Longhands: the `animation` shorthand resets duration to 0s and wipes
       animation-timeline, and a scroll-driven animation needs `auto` to span
       its range. Both failures are silent. */
    animation-name: hero-cell-in;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
    animation-range: var(--s) var(--e);
  }
}
@keyframes hero-cell-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}


/* --- phone: scroll dissolves in the white twin -----------------------------
   The desktop reveal is a porthole that follows the cursor; a touch screen has
   nothing to drive it. Scroll drives a DISSOLVE instead — the white render
   arrives as a grid of squares, each appearing at its own point in the scroll
   and staying once it has.

   Why a grid of elements and not a moving mask: a translating mask samples a
   different part of itself as it travels, so a cell that is on at one offset
   can be off at the next — cells flicker instead of staying put. The only
   masks that stay put are ones opaque contiguously to the bottom of each
   column, and those can only ever read as an edge advancing. A grid fixed to
   the plate has no such constraint: every cell owns its own threshold.

   Each cell carries a slice of the same image — background-size is the whole
   plate and background-position is the cell's negative offset — so the grid
   reassembles one render rather than tiling many.

   --s/--e are the cell's own scroll range, set by site.js, and animation-range
   turns them into when it fades. fill-mode both is what makes it stay. */
@media (max-width: 760px) and (orientation: portrait) {
  /* Portrait render for the phone: 1290x2521, the same dimensions as
     hero-phone.webp, so the two plates register pixel for pixel. */
  .hero-bg__alt { --alt-img: url("../img/hero-phone-white.webp?v=60"); }
}

}

/* ==========================================================================
   SERVICE INDEX (home)
   ========================================================================== */
.svc-list {
  display: grid;
  gap: 1px;
  background: var(--line);
  border-block: 1px solid var(--line);
}

.svc {
  display: grid;
  grid-template-columns: 90px minmax(0, 1fr) minmax(0, 1.25fr) auto;
  gap: clamp(20px, 3vw, 48px);
  align-items: start;
  padding: clamp(30px, 4.5vw, 52px) 0;
  background: var(--bg);
  transition: background .5s var(--ease), padding-inline .5s var(--ease);
}
.svc > * { align-self: start; }
.svc__go { align-self: center; }
.svc:hover { background: var(--bg-3); padding-inline: 22px; }

.svc__idx {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: .1em;
  color: var(--ink-3);
  /* nudge onto the cap-height of the service name beside it */
  padding-top: clamp(3px, .85vw, 11px);
  transition: color .4s var(--ease);
}
.svc:hover .svc__idx { color: var(--accent); }

.svc__name {
  font-family: var(--font-display);
  font-size: clamp(26px, 3.6vw, 44px);
  font-weight: 500;
  letter-spacing: -.038em;
  line-height: 1.05;
}
.svc__desc { color: var(--ink-2); font-size: 15.5px; max-width: 52ch; }

.svc__tags {
  display: flex; flex-wrap: wrap; gap: 7px;
  margin-top: 14px; padding: 0; list-style: none;
}
.svc__tags li {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 100px;
}

.svc__go {
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  flex-shrink: 0;
  transition: background .4s var(--ease), border-color .4s var(--ease), transform .4s var(--ease);
}
.svc__go svg { width: 16px; height: 16px; }
.svc:hover .svc__go {
  background: var(--ink); border-color: var(--ink); color: #08080a;
  transform: rotate(45deg);
}

@media (max-width: 980px) {
  .svc { grid-template-columns: 44px minmax(0, 1fr) auto; row-gap: 16px; }
  .svc__desc { grid-column: 2 / -1; }
}
@media (max-width: 620px) {
  .svc { grid-template-columns: 1fr auto; }
  .svc__idx { grid-column: 1 / -1; }
  .svc__desc { grid-column: 1 / -1; }
  .svc__go { width: 44px; height: 44px; }
}

/* ==========================================================================
   CARD GRIDS / PROCESS / STATS
   ========================================================================== */
.grid { display: grid; gap: clamp(16px, 2vw, 24px); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 980px) { .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 860px) { .grid--3 { grid-template-columns: 1fr; } }
@media (max-width: 640px) { .grid--2, .grid--4 { grid-template-columns: 1fr; } }

.card {
  padding: clamp(24px, 2.6vw, 34px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: linear-gradient(168deg, var(--bg-3), var(--bg-2));
  position: relative;
  overflow: hidden;
  transition: border-color .5s var(--ease), transform .5s var(--ease);
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 50% at 50% 0%, var(--accent-dim), transparent 72%);
  opacity: 0;
  transition: opacity .6s var(--ease);
  pointer-events: none;
}
.card:hover { border-color: var(--line-2); transform: translateY(-3px); }
.card:hover::after { opacity: 1; }

.card h3 {
  font-size: clamp(19px, 1.7vw, 23px);
  letter-spacing: -.028em;
  margin-bottom: 10px;
}
.card p { color: var(--ink-2); font-size: 15px; }
.card__idx {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .16em;
  color: var(--ink-3);
  display: block;
  margin-bottom: 20px;
}
.card .icon {
  width: 34px; height: 34px;
  margin-bottom: 20px;
  color: var(--accent);
}

/* process rail */
.steps { counter-reset: step; display: grid; gap: 1px; background: var(--line); }
.step {
  background: var(--bg);
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr) minmax(0, 1.4fr);
  gap: clamp(18px, 3vw, 40px);
  padding: clamp(26px, 3.4vw, 40px) 0;
  align-items: baseline;
}
.step__n {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: .1em;
  color: var(--accent);
}
.step h3 { font-size: clamp(20px, 2.1vw, 27px); }
.step p { color: var(--ink-2); font-size: 15px; }
@media (max-width: 780px) {
  .step { grid-template-columns: 56px minmax(0, 1fr); }
  .step p { grid-column: 2; }
}

/* stats */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.stat { background: var(--bg-2); padding: clamp(24px, 3vw, 36px); }
.stat__v {
  font-family: var(--font-display);
  font-size: clamp(34px, 4.4vw, 56px);
  font-weight: 500;
  letter-spacing: -.05em;
  line-height: 1;
  background: linear-gradient(158deg, #ffffff 12%, #9b9ba6 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.stat__k {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* ==========================================================================
   PANEL ROW — tall vertical boxes (white section)
   --------------------------------------------------------------------------
   Mono-set, deliberately under-filled boxes: the copy occupies the top third
   and the rest stays empty, which is what makes them read as columns rather
   than as cards that happen to be tall. Three across — a fourth column at
   this shell width narrows each box past the point where that works.
   ========================================================================== */
.panels {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(12px, 1.4vw, 19px);
}
@media (max-width: 900px) { .panels { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .panels { grid-template-columns: 1fr; } }

.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  /* The height is the format. These are ~1:1.8 portrait boxes; letting content
     size them collapses the empty lower two-thirds the layout depends on. */
  min-height: clamp(440px, 64vh, 620px);
  padding: clamp(22px, 2.2vw, 30px);
  border-radius: 20px;
  background: #f0f0ed;
  overflow: hidden;
}

/* Mono at display size, tracked tight. --mid is the same figure placed after
   the copy instead of above it, which is the only structural difference
   between the first and second boxes. */
.panel__n {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(30px, 3.2vw, 42px);
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1;
  color: var(--ink);
}
.panel__n--mid { margin-top: clamp(20px, 2.4vw, 30px); }

.panel__body {
  margin: 16px 0 0;
  font-family: var(--font-mono);
  /* 12.5px with 1.65 leading — small and airy is what keeps mono body copy
     legible at this measure instead of turning into a dense slab. */
  font-size: 12.5px;
  line-height: 1.65;
  letter-spacing: -.005em;
  color: var(--ink-2);
  max-width: 34ch;
}
.panel__body + .panel__body { margin-top: 14px; }

/* Inverted centre box. Re-declares the tokens rather than restyling each
   child, so __n and __body flip automatically — the same inheritance move
   .section--cover makes, run in reverse. color: is set here too because
   inheritance copies an already-resolved value; redefining --ink alone would
   not reach children that have no color of their own. */
.panel--dark {
  background: #0a0a0c;
  color: #f4f4f2;
  --ink:   #f4f4f2;
  --ink-2: #a6a6ad;
  --ink-3: #6e6e79;
}

/* Image box: the render sits inset from the panel edge with the caption pill
   beneath it — not a full-bleed background. margin-top:auto on the pill pins
   it under the image and holds the empty space below, matching the other two
   boxes' bottom alignment.

   The box draws no plate of its own: index.html sets the shader's backdrop to
   the panel's own colour, so the sphere floats on the card with no inset
   rectangle around it. The colour below is the fallback's, and has to stay in
   step with .panel's background for the live case to read edgeless. */
.panel--media { padding: clamp(14px, 1.2vw, 17px); }
.panel__media {
  height: clamp(230px, 26vw, 310px);
  border-radius: 12px;
  background: #f0f0ed url("../img/marble-sphere.webp?v=60") center / cover no-repeat;
  /* Crushed to near two-tone, then broken up by the dot grid below — together
     they approximate the screened/halftone register of the reference art
     rather than leaving it a soft photographic grey. */
  filter: grayscale(1) contrast(1.75) brightness(1.06);
  position: relative;
  overflow: hidden;
  /* Centres the label over the sphere. The shader's canvas is absolutely
     positioned, so it stays out of this grid rather than becoming a track. */
  display: grid;
  place-items: center;
}
.panel__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle at center, #000 .9px, transparent 1px);
  background-size: 3.5px 3.5px;
  /* overlay (not a flat alpha) so the dots only bite in the midtones and leave
     the blown-out highlights clean, which is how a real halftone behaves. */
  mix-blend-mode: overlay;
  opacity: .55;
}

/* Set by widget/src/dithering-embed.ts once the live shader has mounted a
   <canvas> into this box. Everything above is the fallback treatment for the
   still render — a fake halftone the shader now does for real — so both the
   crush filter and the dot grid have to come off, or they'd re-process output
   that is already two-tone. filter: applies to descendants, so leaving it
   would flatten the canvas along with the image underneath. */
.panel__media[data-shader-active] { filter: none; }
.panel__media[data-shader-active]::after { content: none; }

/* Sits at the sphere's centre. z-index lifts it over the ::after halftone grid
   above (it clears the canvas either way, which mounts at z-index: -1).

   Sized as a share of the box rather than of the panel: the middle term is
   .26 x the 26vw in .panel__media's own height clamp, so the label holds the
   same proportion of the sphere across the whole responsive range.

   The letters are a MASK over a shader of their own, not type drawn on top of
   the sphere. That's the only way to get the dither running inside them: a
   blend can't do it, because blending a flat colour over a two-tone backdrop
   can only keep, invert, force-black or force-white each pixel. Inverting
   preserves density (50% black inverts to 50% black), so the word comes out
   the same tone as its surroundings and only its antialiased edges survive —
   and forcing a tone reads on one half of a lit sphere and vanishes on the
   other. Both no-op hardest at mid-density, which is exactly the centre, which
   is exactly where the label sits. Tried, verified, discarded.

   Hand-drawn paths rather than a font, for two reasons: an SVG used as
   mask-image can't reach a webfont, and drawing the I as a plain rectangle is
   also what gives it a straight stem instead of JetBrains Mono's crossbarred
   one. Geometry is on a 96x80 grid — 14-wide strokes, 68 tall, so the mask
   scales cleanly to any size. The background is the plate the shader's
   transparent backdrop lets through, so the glyphs stay a solid dark mass
   however sparse the dither over them happens to be, and the light ink lands
   as live pixels inside them. It doubles as the no-WebGL fallback: without a
   canvas the letters are simply solid. */
.panel__media-label {
  position: relative;
  z-index: 1;
  /* 80/68 taller than the cap height it replaces, so the letters land at the
     same size the type did: .26 x the 26vw in .panel__media's height clamp. */
  height: clamp(71px, 8vw, 95px);
  /* 96/80 — the lockup is drawn to the same cap height (68 of 80) as before,
     but the A is narrower and the two letters sit on a normal sidebearing,
     so the pair reads as a word instead of two drifting shapes. */
  aspect-ratio: 96 / 80;
  /* The label's own shader is oversized (data-scale in the markup) rather than
     `contain`-fitted: fitting the sphere to this box's short side leaves the I,
     hard against the left edge, outside the shape, and a pixel outside the
     shape is never repainted — the stem sat as a dead slab while the A
     dithered. Anything that changes this box's aspect has to be checked
     against that. */
  background: #0a0a0c;
  /* Uniform 14-unit stroke on both letters: the I stem and the A's diagonals
     now carry the same weight, and the A has a real counter punched out above
     the crossbar (fill-rule evenodd) rather than being an open chevron. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2080'%3E%3Cpath%20fill-rule='evenodd'%20d='M6%206h14v68H6z%20M52%206h14l24%2068h-14l-4.24-12H46.24L42%2074H28z%20M59%2025.83%2067.53%2050H50.47z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2096%2080'%3E%3Cpath%20fill-rule='evenodd'%20d='M6%206h14v68H6z%20M52%206h14l24%2068h-14l-4.24-12H46.24L42%2074H28z%20M59%2025.83%2067.53%2050H50.47z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Caption pill: sits on the box, one step lighter than it, with a square dark
   affordance on the right. */
.panel__tag {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 12px 0 0;
  padding: 12px 12px 12px 15px;
  border-radius: 12px;
  background: #f7f7f5;
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.45;
  color: #0a0a0c;
}
/* In the two text boxes the pill is the last thing in the flow — auto margin
   drops it to the bottom edge so all three pills land on one line. */
.panel > .panel__tag:last-child { margin-top: auto; }
.panel__go {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  border-radius: 8px;
  background: #0a0a0c;
  color: #f4f4f2;
}

/* checklist */
.checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 13px; }
.checks li {
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 13px;
  align-items: start;
  color: var(--ink-2);
  font-size: 15px;
}
.checks li::before {
  content: "";
  width: 15px; height: 15px;
  margin-top: 5px;
  border-radius: 50%;
  border: 1px solid var(--accent);
  background: radial-gradient(circle at 50% 50%, var(--accent) 0 2.5px, transparent 3px);
}

/* ==========================================================================
   PRICING
   ========================================================================== */
/* stretch, not start — equal-height cards with buttons on a shared baseline */
.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(16px, 2vw, 22px); }
@media (max-width: 940px) { .tiers { grid-template-columns: 1fr; } }

.tier {
  padding: clamp(26px, 2.8vw, 36px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--bg-2);
  display: flex;
  flex-direction: column;
  gap: 22px;
  position: relative;
}
.tier--feature {
  background: linear-gradient(170deg, var(--bg-4), var(--bg-2) 62%);
  border-color: rgba(185, 166, 255, .32);
  box-shadow: 0 40px 90px -50px rgba(185, 166, 255, .55);
}
.tier__flag {
  position: absolute; top: -1px; right: 22px;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  padding: 5px 11px;
  border-radius: 100px;
  background: var(--accent);
  color: #0b0b10;
  font-weight: 500;
}
.tier__name { font-family: var(--font-display); font-size: 15px; letter-spacing: .01em; color: var(--ink-2); }
.tier__price {
  font-family: var(--font-display);
  font-size: clamp(38px, 4.4vw, 52px);
  font-weight: 500;
  letter-spacing: -.05em;
  line-height: 1;
  margin-top: 6px;
}
.tier__price small { font-size: 14px; font-weight: 400; color: var(--ink-3); letter-spacing: 0; margin-left: 4px; }
/* a leading "from" reads as a prefix, and keeps the /mo suffix off its own line */
.tier__price small:first-child { margin-left: 0; margin-right: 7px; }
.tier__note { font-size: 14px; color: var(--ink-2); }
.tier .checks li { font-size: 14.5px; }
.tier .btn { justify-content: center; margin-top: auto; }

/* ==========================================================================
   FAQ
   ========================================================================== */
.faq { border-top: 1px solid var(--line); }
.faq__item { border-bottom: 1px solid var(--line); }
.faq__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 26px 0;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: clamp(17px, 1.8vw, 21px);
  letter-spacing: -.025em;
  color: var(--ink);
  transition: color .3s var(--ease);
}
.faq__q:hover { color: var(--accent); }
.faq__q i {
  flex-shrink: 0;
  width: 13px; height: 13px;
  position: relative;
}
.faq__q i::before, .faq__q i::after {
  content: ""; position: absolute; inset: 50% 0 auto 0;
  height: 1px; background: currentColor;
  transition: transform .4s var(--ease);
}
.faq__q i::after { transform: rotate(90deg); }
.faq__q[aria-expanded="true"] i::after { transform: rotate(0deg); }

.faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .45s var(--ease);
}
.faq__a > div { overflow: hidden; }
.faq__item.is-open .faq__a { grid-template-rows: 1fr; }
.faq__a p { color: var(--ink-2); font-size: 15.5px; padding-bottom: 26px; max-width: 74ch; }

/* ==========================================================================
   CTA + FOOTER
   ========================================================================== */
.cta {
  position: relative;
  overflow: hidden;
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding-block: clamp(96px, 16vh, 190px);
  text-align: center;
}
.cta::before {
  content: "";
  position: absolute;
  left: 50%; top: 0;
  width: min(1000px, 130vw); aspect-ratio: 1;
  transform: translate(-50%, -52%);
  background: radial-gradient(circle, rgba(185, 166, 255, .17), transparent 62%);
  pointer-events: none;
}
.cta h2 {
  position: relative;
  font-size: clamp(40px, 7.4vw, 104px);
  letter-spacing: -.052em;
  max-width: 16ch;
  margin-inline: auto;
}
.cta p { position: relative; margin: 26px auto 0; max-width: 50ch; color: var(--ink-2); }
.cta__actions {
  position: relative;
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
  margin-top: 40px;
}

/* Ported from the reference React/Tailwind component. Every metric below is
   the literal Tailwind value from that class list — max-w-6xl 1152, rounded-t
   32/48, px-6 py-12 lg:py-16, gap-8 32, blur 8px, breakpoints 768/1024/1280 —
   so this is the component's geometry, not an approximation of it.

   ONE addition: a solid --bg-2 plate under the radial. The component declares
   only `bg-[radial-gradient(...white/8%...)]` and otherwise leans on whatever
   the app paints behind it. This site's page is already #050506, so 8% white
   over it is nearly invisible and the footer read as bare page with a stray
   rounded line on top. The plate is what makes it a panel; the radial then
   does its job on top of it.

   The stagger is .reveal rather than motion/react — that helper already takes
   a per-element delay through --d, which is exactly the component's
   `delay: 0.1 + index * 0.1`, without a second animation runtime on a static
   page. The component's blur-in is dropped: .reveal already fades and lifts,
   and animating a filter on top forces a full-width raster every frame. */
.footer {
  position: relative;
  width: 100%;
  max-width: 1152px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-top: 1px solid var(--line);
  border-radius: 32px 32px 0 0;
  padding: 48px 24px;
  background:
    radial-gradient(35% 128px at 50% 0%, rgba(255, 255, 255, .08), transparent),
    var(--bg-2);
}
@media (min-width: 768px)  { .footer { border-radius: 48px 48px 0 0; } }
@media (min-width: 1024px) { .footer { padding-block: 64px; } }

/* The blurred hairline straddling the top border: h-px, w-1/3, centred,
   pulled up by half its own height, foreground/20, blur 8px. */
.footer::before {
  content: "";
  position: absolute;
  top: 0; left: 50%;
  width: 33.333%; height: 1px;
  transform: translate(-50%, -50%);
  border-radius: 100px;
  background: rgba(244, 244, 242, .2);
  filter: blur(8px);
  pointer-events: none;
}

.footer__top { display: grid; width: 100%; gap: 32px; }
.footer__links {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px;
  margin-top: 40px;
}
@media (min-width: 768px) { .footer__links { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* xl:grid-cols-3 with the link block spanning two of the three. */
@media (min-width: 1280px) {
  .footer__top { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footer__links { grid-column: span 2; margin-top: 0; }
}

.footer__blurb { color: var(--ink-3); font-size: 14px; max-width: 34ch; margin-top: 16px; }
/* Copyright lives in the brand block, as in the component — the site's old
   separate bottom bar went with it. */
.footer__copy {
  margin-top: 32px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--ink-3);
}
@media (min-width: 768px) { .footer__copy { margin-top: 32px; } }

.footer__col { margin-bottom: 40px; }
@media (min-width: 768px) { .footer__col { margin-bottom: 0; } }
.footer__col h4 {
  font-family: var(--font-mono);
  font-size: 12px; font-weight: 500;
  letter-spacing: .15em; text-transform: uppercase;
  color: var(--ink-3);
  margin: 0;
}
.footer__col ul { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 8px; }
.footer__col a { font-size: 14px; color: var(--ink-2); transition: color .3s var(--ease); }
.footer__col a:hover { color: var(--ink); }

/* ==========================================================================
   404
   --------------------------------------------------------------------------
   Reuses .stage__still / __grain / __scrim rather than restating the plate:
   those already carry the landscape/portrait swap, so this page picks up the
   phone render for free and can never drift from the real pages when that
   art changes.

   One screen, centred, no scroll — and one action. The second "explore" pill
   the reference design carries was dropped on purpose: on a four-page site
   the nav directly above IS the explore, so a second button would point at
   what's already one tap away. A lost visitor wants one obvious way back.
   ========================================================================== */
.err {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: center;
  overflow: hidden;
  background: var(--bg);
}
/* z-index because the three plate layers above are absolutely positioned
   siblings that would otherwise paint over the copy. */
.err__inner {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: calc(var(--nav-h) + 6vh) 0 8vh;
}
/* .stage__scrim is built for the service pages, where it darkens the EDGES and
   deliberately leaves the middle clear for the art. This page puts its copy in
   exactly that clear middle, over the blob's brightest highlights, so the sub
   and the button lost their contrast. A second scrim weighted the other way —
   dark at the centre, gone by the edges — sits behind the text only.
   z-index:-1 works because .err__inner's own z-index makes it a stacking
   context: the pseudo lands above the parent's background but under its text,
   and never over the art of the page. */
.err__inner::before {
  content: "";
  position: absolute;
  inset: -6% -10%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(58% 52% at 50% 52%, rgba(5, 5, 6, .84) 0%, rgba(5, 5, 6, .6) 42%, transparent 76%);
}
.err__title {
  font-family: var(--font-display);
  font-size: clamp(44px, 9vw, 132px);
  line-height: .9;
  letter-spacing: -.05em;
  font-weight: 500;
  margin: 18px 0 0;
}
/* Same construction as the home headline: <em> is the line break and the tint
   sits on the <span> inside it, so only the last word takes the gradient. */
.err__title em { font-style: normal; display: block; }
.err__title em span {
  background: linear-gradient(96deg, #d7ccff 4%, var(--accent) 54%, #a996ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.err__sub {
  margin: clamp(20px, 3vw, 30px) auto 0;
  max-width: 44ch;
  font-size: clamp(15px, 1.3vw, 18px);
  color: var(--ink-2);
  text-wrap: balance;
}
.err__actions {
  display: flex;
  justify-content: center;
  margin-top: clamp(28px, 4vw, 40px);
}

/* ==========================================================================
   MOTION PREFERENCES
   ========================================================================== */
@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; }
  /* The hero dissolve is scroll POSITION, not autonomous motion — nothing
     moves on its own, it only tracks where the page is, the way a progress
     bar does. The blanket duration override above would drive every cell
     straight to its end state, putting the whole white render on from the
     first frame — louder than the effect it is meant to suppress. */
  .hero-cell { animation-duration: auto !important; }
  /* collapse the pinned runway into a short static banner */
  .stage__flow { margin-top: 0; }
  .stage__viewport { position: relative; height: 62svh; }
  .stage__block { min-height: auto; padding-block: 8vh; }
}

/* ==========================================================================
   OPENING ANIMATION
   A plate over the whole viewport that comes apart as a grid of squares,
   clearing from the bottom up. The plate is a canvas — the mark and wordmark
   are painted INTO it so a square that leaves takes its slice of the logo with
   it, rather than the logo fading out ahead of the plate. The grid, the
   timing and the artwork all live in assets/js/loader.js; what is left here is
   the scroll lock and the backstop that covers the page until the script runs.
   ========================================================================== */

/* The page is not scrollable while the plate is up. Without this a stray
   wheel event during the intro scrolls the content behind it, and the reader
   arrives mid-page for no reason they can see. */
html.is-loading, html.is-loading body { overflow: hidden; }

.site-loader {
  position: fixed;
  inset: 0;
  z-index: 5000;
  /* Covers the page for the frames between first paint and the script's first
     canvas draw. Deliberately NOT var(--bg): the plate sits a step above the
     page it covers, so the squares read as squares against the art from the
     first one that goes.

     It is dropped the instant the dissolve starts. Leaving it on would put an
     opaque sheet behind the canvas, and a cleared square would expose that
     sheet instead of the page — the dissolve would run and nothing would
     appear to happen. loader.js carries the same value as PLATE. */
  background: var(--raised);
}

.site-loader.is-revealing { background: transparent; }

.site-loader__plate {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Without a script the plate is never taken down and the page is a black
   rectangle, so the markup carries a <noscript> that hides it. The rule lives
   there rather than here because it has to apply with no JS at all — nothing
   can be relied on to set a class on <html>. */

@media (prefers-reduced-motion: reduce) {
  /* loader.js removes the plate outright under this preference, so nothing
     here should ever paint. Kept as the belt-and-braces for the window
     between first paint and the script running. */
  .site-loader { display: none; }
}
