/* ─────────────────────────────────────────────────────────────────────────────
   sandbeach — components (BEM, token-fed)
   Layers: base < layout < components < utilities. Every color/size comes from
   tokens.css; context colors via --_bg/--_text/--_muted/--_line so recoloring
   or flipping a slab is a token edit, never a component edit.
   ──────────────────────────────────────────────────────────────────────────── */

@layer base, layout, components, utilities;

/* ═══ base ═══════════════════════════════════════════════════════════════ */
@layer base {

@font-face {
  font-family: "BDO Grotesk";
  src: url("BDOGrotesk-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

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

body {
  margin: 0;
  font-family: var(--font-brand);
  font-weight: 500;
  line-height: var(--lh-body);
  letter-spacing: var(--ls-1);
  color: var(--color-grey-950);
  background-color: var(--bg-under);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6, p, ul, ol, figure {
  margin: 0;
  font-weight: 500;
  overflow-wrap: break-word;
}
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }

img, svg, video { display: block; max-width: 100%; }

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

button, input, textarea, select { font: inherit; letter-spacing: inherit; color: inherit; }

.skip-link {
  position: fixed;
  z-index: 10000;
  top: var(--space-xs);
  left: var(--space-xs);
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--radius-field);
  background: var(--color-white);
  color: var(--color-grey-950);
  transform: translateY(calc(-100% - 2em));
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); }

::selection { background: var(--accent); color: var(--color-grey-950); }

/* One authored focus ring for the whole site. The browser default is a 1px
   system blue that all but disappears on the dark slabs. */
:focus-visible {
  outline: 2px solid var(--_focus, var(--accent));
  outline-offset: 3px;
  border-radius: 2px;
}
:focus:not(:focus-visible) { outline: none; }

} /* base */

/* ═══ layout ═════════════════════════════════════════════════════════════ */
@layer layout {

.container {
  padding-inline: var(--container-padding);
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 0 var(--gutter);
  width: 100%;
}
.grid > * { min-width: 0; }

@media screen and (max-width: 47.94em) {
  .grid { grid-template-columns: repeat(6, 1fr); }
}

} /* layout */

/* ═══ components ═════════════════════════════════════════════════════════ */
@layer components {

/* ── page shell ─────────────────────────────────────────────────────────── */
.page { position: relative; }

/* Viewport-bounded, not page-bounded: as `absolute inset:0` on .page this was a
   5000px+ opaque composited layer being opacity-animated over the hero stagger. */
.page-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background-color: var(--bg-under);
  pointer-events: none;
}

/* ── type roles (Ollef text-style-*) ────────────────────────────────────── */
.t-body      { font-size: var(--text-m); }
.t-heading-s { font-size: var(--h5); line-height: var(--lh-body);    letter-spacing: var(--ls-1); }
.t-heading-m { font-size: var(--h4); line-height: var(--lh-heading); letter-spacing: var(--ls-1); }
.t-heading-l { font-size: var(--h1); line-height: var(--lh-heading); letter-spacing: var(--ls-2); }
.t-heading-xl {
  font-size: var(--display-2);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-3);
  /* SplitText mask breathing room, Ollef does the same */
  margin-inline: -0.1em;
  padding-inline: 0.1em;
}
.t-display { font-size: var(--display); line-height: var(--lh-tight); letter-spacing: var(--ls-4); }

/* ── nav (blend-difference trick: dark ink auto-inverts over any slab) ──── */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 100;
  mix-blend-mode: difference;
}
/* The trick only works if EVERY child is drawn in dark ink. Setting it on the
   wrapper (not just the brand) is what makes the links and CTA invert too —
   without it they inherit --_text and wash out over the light slab. */
.site-nav__invert {
  position: relative;
  z-index: 1;            /* above the scrim: a positioned ::before would
                            otherwise paint over the in-flow nav content */
  filter: invert(1);
  color: var(--color-grey-950);
}
.site-nav__pad { height: var(--container-padding); }
.site-nav__inner { align-items: start; }
.site-nav__brand {
  grid-column: 1 / span 3;
  font-size: var(--text-m);
  padding-block: var(--link-pad);
  margin-block: calc(-1 * var(--link-pad));
}

/* Over the photo band, difference-against-a-photograph is mud. Hand the nav
   over to solid white ink on a short scrim; the handoff also reads as the nav
   belonging to whichever slab it is currently on. */
.site-nav::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 9em;
  /* holds its density across the nav row before falling off — a plain two-stop
     ramp left the brand at ~3:1 over the bright wall in the photo */
  background: linear-gradient(to bottom,
    rgb(0 0 0 / 0.7) 0%, rgb(0 0 0 / 0.55) 45%, rgb(0 0 0 / 0) 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-med) var(--ease-out);
}
.site-nav.is-over-media {
  mix-blend-mode: normal;
}
.site-nav.is-over-media::before { opacity: 1; }
.site-nav.is-over-media .site-nav__invert {
  filter: none;
  color: var(--color-white);
}
.site-nav__links {
  grid-column: 4 / span 5;
  display: flex;
  gap: var(--space-m);
}
/* the right cluster holds the language pair and the CTA on one baseline; with
   one locale the switcher does not render and this collapses to the CTA alone */
.site-nav__cta {
  grid-column: 9 / span 4;
  justify-self: end;
  display: flex;
  align-items: baseline;
  gap: var(--space-s);
}
/* EST · ENG — the inactive language is the muted one, so the pair reads as a
   state, not as two equal links */
.lang-switch { display: flex; align-items: baseline; gap: var(--space-3xs); }
.lang-switch__item { opacity: 0.45; transition: opacity var(--dur-fast) var(--ease-out); }
.lang-switch__item:hover { opacity: 1; }
.lang-switch__item[aria-current] { opacity: 1; }
/* a real element, not a ::before on the link — a pseudo would inherit the
   inactive item's 0.45 and compound to ~0.2 on one side of the pair only */
.lang-switch__sep { font-size: var(--text-m); opacity: 0.45; }
.site-nav__toggle {
  display: none;
  grid-column: 5 / span 2;
  justify-self: end;
  cursor: pointer;
  background: none;
  border: 0;
  padding: var(--container-padding);
  margin: calc(-1 * var(--container-padding));
}
.site-nav__toggle-track { position: relative; display: block; overflow: hidden; }
.site-nav__toggle-open { display: block; transition: transform var(--dur-fast) var(--ease-out); }
.site-nav__toggle-close {
  position: absolute;
  inset: 0;
  display: block;
  transform: translateY(100%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-open { transform: translateY(-100%); }
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-close { transform: translateY(0); }

@media screen and (max-width: 61.94em) {
  .site-nav__links, .site-nav__cta { display: none; }
  .site-nav__toggle { display: block; }
}

/* ── mobile menu (sticky panel under the nav) ───────────────────────────── */
.mobile-menu {
  position: sticky;
  top: 0;
  z-index: 90;
  display: none;
}
@media screen and (max-width: 61.94em) {
  .mobile-menu:not([hidden]) { display: block; }
}
.mobile-menu__panel {
  position: absolute;
  inset: 0;
  z-index: 89;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100svh;
  padding-top: calc(4.13em + var(--space-2xl));
  background-color: var(--_bg);
}
.mobile-menu__link {
  display: block;
  width: 100%;
  padding: var(--space-m) var(--space-l);
  border-top: var(--hairline) solid var(--_line);
  color: var(--_text);
}
.mobile-menu__cta {
  padding: 0 var(--space-l) var(--space-2xl);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-m);
}

/* ── links with hairline underline ──────────────────────────────────────── */
/* Padding (cancelled by an equal negative margin) lifts a 16px text link to a
   ~28px tap target without moving a single pixel of the layout. */
.link-underline {
  position: relative;
  display: inline-block;
  font-size: var(--text-m);
  padding-block: var(--link-pad);
  margin-block: calc(-1 * var(--link-pad));
}
.link-underline::after {
  content: "";
  position: absolute;
  inset: auto 0 var(--link-pad);
  border-top: var(--hairline) solid currentColor;
  transform-origin: right;
  transition: transform var(--dur-fast) var(--ease-out);
}
.link-underline:hover::after { transform: scaleX(0); }
.link-underline--bare::after { transform: scaleX(0); transform-origin: left; }
.link-underline--bare:hover::after { transform: scaleX(1); }
/* the page you are on keeps its underline (nav Tööd on /tood) */
/* "page" = you are on this page (/tood); "location" = the scroll-spy says the
   viewport is inside this anchor's section — one underline language for both */
.link-underline--bare[aria-current]::after { transform: scaleX(1); }
.link-underline--muted { color: var(--_muted); transition: color var(--dur-fast) var(--ease-out); }
.link-underline--muted:hover { color: var(--_text); }
.link-underline--muted::after { display: none; }

/* ── atmosphere: sand haze + film grain on the dark slabs ───────────────── */
/* Beach light on a near-black ground, in two layers on one host:
   ::before — two soft sand blobs, so no dark slab is ever a flat fill;
   ::after  — a tiled fractal-noise film, OVERLAY-blended on purpose. Overlay
              is mean-preserving (mid-grey noise leaves the ground where it
              was), so the grain reads as texture without lifting the black.
              A normal-blend grain at the same strength raised #0d0d0d by ~4%
              and ate the muted-text contrast margin (grey-500 5.6:1 → 4.9:1).
   Both layers sit at z-index -1: painted over the host's own background, under
   its content. Every host below is already a stacking context, which is what
   makes that work — do not add this to an unpositioned element.
   The blob geometry is a variable so each slab gets a different sky; the
   ordering is deliberate, not random (light falls from the hero's top-left,
   the footer answers it from the bottom-right). */
.atmos { position: relative; }
.atmos::before,
.atmos::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.atmos::before {
  --haze-1: 68% 52% at 8% -8%;
  --haze-2: 52% 46% at 92% 104%;
  background-image:
    radial-gradient(var(--haze-1),
      color-mix(in oklab, var(--color-sand) var(--haze-strength), transparent),
      transparent 72%),
    radial-gradient(var(--haze-2),
      color-mix(in oklab, var(--color-sand) calc(var(--haze-strength) * 0.55), transparent),
      transparent 70%);
}
.atmos::after {
  /* inset 1px on the block axis, NOT 0: when a section boundary lands on a
     fractional device pixel, the box's edge row has partial background
     coverage, and overlay-blended grain over a partial backdrop composites
     as raw mid-grey — measured as a full-width 1px light line (lum 17->52)
     at the intro/statement seam, 360x3. One pixel inside, the backdrop is
     always solid. The haze ::before is normal-blend and needs no inset. */
  inset: 1px 0;
  /* Three things in the filter are load-bearing: sRGB interpolation (the
     linearRGB default washes the noise out and shifts its mean), the linear
     transfer that stretches turbulence around 0.5 so overlay has something to
     work with on a 5%-luminance ground, and pinning alpha to 1 — turbulence
     randomises alpha too, which would modulate the layer twice. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3.3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3.3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3.3' intercept='-1'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: var(--grain-tile) var(--grain-tile);
  opacity: var(--grain-strength);
  mix-blend-mode: overlay;
}
/* per-slab skies: hero opens top-left, the statement drifts right, the footer
   closes bottom-left so the page doesn't end where it started */
/* The top blob used to sit at 88% -6%, i.e. its core ABOVE the box, so the
   gradient was at full strength on the statement's very first row. That was
   invisible while the photo band stood between the hero and this section (the
   sticky plate carried a negative margin and the statement slid over it). With
   the plate moved into the hero the statement meets flat ground and the edge
   measured a +31 luminance jump in ONE row at 1440, right side only.
   38% puts the whole falloff inside the box — the same rule .atmos--hero
   already follows for its own low blob. */
.atmos--statement::before { --haze-1: 60% 44% at 88% 38%; --haze-2: 56% 48% at 4% 96%; }
.atmos--footer::before    { --haze-1: 72% 50% at 12% 108%; --haze-2: 48% 42% at 96% -4%; }
/* the CTA band sits between two DARK sections, so unlike the footer (whose
   top edge meets the light FAQ slab) its top blob is tuned to CONTINUE the
   testimonials' bottom-right glow across the boundary: at 96% 4% the edge
   brightness matches the section above (seam delta 6.7, inside grain noise;
   at the footer's -4% it was a hard cut, at 14% a 12-lum step) */
.atmos--ctaband::before   { --haze-1: 72% 50% at 12% 92%; --haze-2: 50% 44% at 96% 4%; }

/* The nav sits IN FLOW above the first slab, so the strip behind it is page
   ground, not section ground, and a slab that starts below the nav draws a line
   along its bottom edge. The fix is NOT to host the sky on .page: a page-tall
   box (5000px+) smears the radials, and capping it at 100vh just moves the
   problem — that version clipped the bottom-right blob mid-hero and measured a
   20-lum step in a single row at EVERY width, mobile and desktop (Sten's phone
   screenshot, 2026-08-08).
   The first slab is the only element that knows how tall the hero actually is,
   so it owns the sky, and it takes the strip over by starting ABOVE the nav and
   paying the height back as padding, so its own background travels with its two
   layers. Reaching only the layers up there (negative `top` on them) does not
   work: the
   overlay-blended grain would have no backdrop to blend into above the section
   box and composites as raw mid-grey — a light band behind the nav. --nav-h is
   generous on purpose; the surplus is cancelled by the padding, so content does
   not move. */
.atmos--hero {
  margin-top: calc(-1 * var(--nav-h));
  padding-top: var(--nav-h);
}
/* and the low blob is pulled inside its own box: anchored below the edge it
   gets clipped by whatever follows, which is the seam all over again */
.atmos--hero::before { --haze-2: 52% 30% at 92% 76%; }

/* ── intro (hero) ───────────────────────────────────────────────────────── */
.intro { position: relative; z-index: 1; background-color: var(--_bg); }
.intro__top { padding-top: var(--space-2xl); }

/* ── the portrait that opens the page ───────────────────────────────────────
   Tom's ask 2026-08-09: his face first, the copy starting inside the photo's
   fade. Three things carry it:
   · z-index 0 puts the plate ABOVE both .atmos pseudos (which sit at -1) and
     below the copy, which follows it in tree order. It must clear the grain:
     the noise is tuned to keep a flat #0d0d0d from banding, and at that
     strength over a face it reads as sandpaper — measured on the first build,
     his skin came out visibly stippled at 1440. The grain still runs the whole
     hero, it just runs UNDER the photograph and through the fade.
   · the bottom is a MASK, not a black gradient painted on top: the photo fades
     to transparent and the section's own ground (haze + grain) comes through,
     so there is no second black to keep matched against --haze-strength.
   · it starts at the section's top edge, which .atmos--hero has already pulled
     up behind the in-flow nav — so the photo owns the nav strip too, and the
     nav hands its ink over (see .site-nav.is-over-media). */
/* Tom 2026-08-19: "pilt natuke rohkem/pikemalt mind näha" — the plate grew
   (76→82svh), the solid stretch of the mask runs deeper (42→52%) and the fade
   ends softer, plus a light contrast lift on the image itself. */
.intro--media { --hero-media-h: min(82svh, 50em); }
.intro__media {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--hero-media-h);
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 0 52%, rgb(0 0 0 / 0.45) 80%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0 52%, rgb(0 0 0 / 0.45) 80%, transparent 100%);
}
/* The crop shows the photo from its very top (0%): the cover overflow all
   crops below, which is what holds the headroom over Tom's head (Sten,
   2026-08-10 — at 14% the head sat right under the nav). Horizontal stays
   centred — he is centred in the photograph. */
.intro__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0%;
  filter: contrast(1.06) brightness(1.03);
}
/* the plate is a POSITIONED z-index:0 child, so it paints after in-flow blocks;
   the copy has to be positioned too or the photograph covers it */
.intro > .container { position: relative; z-index: 1; }
/* Two jobs, one ramp: hold the nav row legible over the bright studio wall at
   the top, and put a floor under the copy where it crosses the photograph.
   The middle stays open so the face is not veiled. */
.intro__media-scrim {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(to bottom,
    rgb(13 13 13 / 0.62) 0%,
    rgb(13 13 13 / 0.12) 24%,
    rgb(13 13 13 / 0.26) 58%,
    rgb(13 13 13 / 0.72) 86%,
    rgb(13 13 13 / 0.86) 100%);
}
/* the copy begins in the fade, ~72% down the plate. The section has already
   paid --nav-h as padding to cover the strip, so that height comes back off
   here or the H1 would sit a whole nav lower than intended. */
.intro--media .intro__top { padding-top: calc(var(--hero-media-h) * 0.72 - var(--nav-h)); }
.intro__statement { grid-column: 1 / span 6; max-width: 24em; }
.intro__heading { font-size: var(--h4); line-height: var(--lh-heading); letter-spacing: var(--ls-1); text-wrap: balance; }
.intro__heading em { font-style: normal; color: var(--_muted); display: block; }
.intro__sub { margin-top: var(--space-m); max-width: 26em; color: var(--_muted); }
.intro__now {
  margin-top: var(--space-m);
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2xs);
  color: var(--_text);
}
.intro__now::before {
  content: "";
  align-self: center;
  width: var(--space-2xs);
  height: var(--space-2xs);
  border-radius: var(--radius-pill);
  background-color: var(--accent);
}
/* The availability line is the page's one live claim. A slow breath makes it
   read as current rather than as printed copy. Runs only while on screen. */
.intro__now.is-live::before { animation: sb-breathe 3.2s ease-in-out infinite; }
@keyframes sb-breathe {
  0%, 100% { opacity: 1;    transform: scale(1); }
  50%      { opacity: 0.45; transform: scale(0.8); }
}

/* the band below the fold is a full viewport of photograph with no other
   affordance saying it moves — the cue is the only hint, so it drifts */
.intro__cue .link-underline.is-live { animation: sb-drift 2.6s ease-in-out infinite; }
.intro__cue .link-underline:hover { animation-play-state: paused; }
@keyframes sb-drift {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, 0.35em, 0); }
}
.intro__form { grid-column: 8 / span 5; align-self: start; }
.intro__cue {
  grid-column: 1 / -1;
  justify-self: end;
  margin-top: var(--space-l);
  overflow: hidden;
  /* was 2xl — half of Tom's "Minust paar nõksu kõrgemale" (08-19) */
  padding-bottom: var(--space-l);
}

/* the giant text logotypes (hero name + footer company) were removed
   2026-08-19 on Tom's ask; --brand-mark-size/--name-mark-size sit unused in
   tokens.css should he ever want the moment back */

@media screen and (max-width: 61.94em) {
  .intro__statement { grid-column: 1 / -1; max-width: 100%; }
  .intro__form { grid-column: 1 / -1; margin-top: var(--space-xl); }
  /* a phone is portrait, so the same svh buys far more photograph than it does
     on a laptop; the plate comes down or the H1 never makes the first screen */
  .intro--media { --hero-media-h: min(66svh, 33em); }
  /* here cover fills the plate's height exactly (the photo is wider than the
     plate, not taller), so object-position has no vertical slack — the image
     itself shifts down for headroom, and the mask gains a top fade so the
     photo emerges from the ground instead of starting on a hard edge */
  .intro__media img { object-position: 50% 0%; transform: translateY(5svh); }
  .intro__media {
    -webkit-mask-image: linear-gradient(to bottom,
      transparent 0, #000 12%, #000 52%, rgb(0 0 0 / 0.45) 80%, transparent 100%);
    mask-image: linear-gradient(to bottom,
      transparent 0, #000 12%, #000 52%, rgb(0 0 0 / 0.45) 80%, transparent 100%);
  }
}

/* ── statement (Minust) ─────────────────────────────────────────────────── */
.statement { position: relative; z-index: 1; background-color: var(--_bg); }
.statement__lead { grid-column: 1 / span 8; max-width: 40em; padding-block: var(--space-3xl); }
.statement__lead p + p { margin-top: var(--space-l); }
.statement__divider { grid-column: 1 / -1; border-top: var(--hairline) solid var(--_line); }
.statement__label { grid-column: 1 / span 2; padding-block: var(--space-xl); }
.statement__stats {
  grid-column: 4 / span 6;
  padding-block: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}
.stat__num { font-size: var(--h1); line-height: var(--lh-tight); letter-spacing: var(--ls-2); }
.stat__text { margin-top: var(--space-2xs); color: var(--_muted); }
.statement__foot { grid-column: 11 / span 2; padding-block: var(--space-xl); justify-self: end; }
/* The section opens with the lead's 6em and has to close on the same 6em. The
   last row already carries 3em of its own padding-bottom, so the spacer only
   completes it — at --section-space-m the tail measured 207px against a 108px
   head and the whole slab sat high in its own space. */
.statement__bottom { grid-column: 1 / -1; height: calc(var(--space-3xl) - var(--space-xl)); }

/* ── home bio (statement--bio): the long first-person Minust + Pilt-2 ──────
   Tom's 2026-08-19 email: the suit portrait sits BESIDE the text, faded into
   the ground. A radial vignette mask means the photograph has no edges at all
   against the dark slab — it emerges from it, the same doctrine as the hero
   plate's bottom mask (a mask, never a painted black gradient). The gentle
   desaturate/darken keeps the daylight greens from floating on #0d0d0d. */
/* dark-on-dark section seams sit on fractional device pixels, and the light
   #e5e5e5 body bled through as a 1px hairline (measured lum 17->60->17 in one
   row at 360x3). A 1px overlap closes the crack at every DPR. */
.statement--bio { margin-top: -1px; }
/* padding-top was 2xl; Tom 08-19: Minust up "paar nõksu" so the page reads
   as continuing under the hero — the cue's padding-bottom came down too */
.statement__toplabel { grid-column: 1 / -1; padding-top: var(--space-l); }
.statement__bio {
  grid-column: 1 / span 7;
  /* share row 2 with the portrait: sparse auto-placement would otherwise drop
     the text a whole row below the photo (the cursor cannot move back left) */
  grid-row: 2;
  max-width: 36em;
  padding-block: var(--space-xl) var(--space-3xl);
}
.statement__p + .statement__p { margin-top: var(--space-m); }
/* the opening paragraph is a whole origin story — one size up keeps it a read,
   not a headline (at --h5 it filled a full 1440 viewport on its own) */
.statement__p--lead { font-size: var(--h6); line-height: var(--lh-body); letter-spacing: var(--ls-1); }
/* Urmas Kamdron via Tom (2026-08-20): the drop from the lead to the rest read
   as too abrupt and the small grey was hard to read on a phone — 95% of his
   clients arrive on mobile. One size up (1em → --text-m) and a mid grey
   (grey-350, 9.2:1) instead of muted: hierarchy stays, the cliff goes. */
.statement__p:not(.statement__p--lead) { font-size: var(--text-m); color: var(--color-grey-350); }
.statement__signoff { margin-top: var(--space-l); }
.statement__media {
  grid-column: 9 / span 4;
  /* rows 2-4 = bio + divider + stats: the sticky portrait keeps Tom in view
     while the long bio scrolls past and is allowed to ride down as far as the
     end of the stat trio (the fades keep the hairline legible through it),
     never into the next slab */
  grid-row: 2 / span 2;
  align-self: start;
  position: sticky;
  top: calc(var(--nav-h) + var(--space-m));
  margin-top: var(--space-xl);
}
/* explicit rows so the media's 2-row area cannot push these down (explicit
   placement may overlap it; auto-placement would flee below it). No divider
   on the bio variant — the hairline read as a leftover (Sten, 08-19). */
.statement--bio .statement__stats   { grid-row: 3; }
.statement--bio .statement__bottom  { grid-row: 4; }
/* the image is a CUTOUT on transparent ground (Tom-Liivrand-About-v2), so it
   needs no edge fades or grading — Sten binned the masked-photo version as
   "weird transparency" (08-19). Only the bottom gets a fade: the cutout ends
   in a hard horizontal cut at the thighs, and that line must dissolve into
   the slab instead of reading as a chop. */
.statement__media img {
  display: block;
  width: 100%;
  height: auto;
  -webkit-mask-image: linear-gradient(to bottom, #000 76%, transparent 98%);
  mask-image: linear-gradient(to bottom, #000 76%, transparent 98%);
}

@media screen and (max-width: 61.94em) {
  .statement__lead { grid-column: 1 / -1; max-width: 100%; }
  .statement__label { grid-column: 1 / span 2; }
  .statement__stats { grid-column: 1 / -1; }
  .statement__foot { grid-column: 1 / -1; justify-self: start; padding-top: 0; }
  /* stacked: label, portrait, bio — the photo introduces the text, capped so a
     phone does not spend two screens on it */
  .statement__bio { grid-column: 1 / -1; grid-row: auto; max-width: 100%; padding-top: var(--space-l); }
  .statement__media {
    grid-column: 1 / -1;
    grid-row: auto;
    position: static;
    max-width: min(100%, 22em);
    justify-self: center;
  }
  .statement--bio .statement__stats,
  .statement--bio .statement__bottom { grid-row: auto; }
}

/* ── pillars (the light slab; service rows, Ollef signature) ────────────── */
.slab { position: relative; z-index: 2; background-color: var(--_bg); color: var(--_text); }
.slab__overlay {
  position: absolute;
  inset: 0;
  z-index: 20;
  background-color: var(--bg-page);
  pointer-events: none;
}

.pillars__head { padding-top: var(--section-space-s); }
.pillars__label { grid-column: 1 / span 3; }
.pillars__title-wrap { grid-column: 1 / -1; overflow: hidden; margin-top: var(--space-2xl); }
.pillars__title { font-size: var(--display-2); }
/* ── the three services, side by side ───────────────────────────────────────
   Tom's ask 2026-08-09: all three on screen at once, nothing to discover by
   scrolling, detail on demand. This replaced a display-size row list plus a
   sticky crossfading media panel — which needed a SECOND markup below 992 and
   so printed every service name twice. One block now serves every width. */
.pillars__grid {
  grid-column: 1 / -1;
  margin-top: var(--space-2xl);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
  align-items: start;
}
.pillar { min-width: 0; }
.pillar__image {
  aspect-ratio: 1;
  overflow: hidden;
}
.pillar__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.04);
  transition: transform var(--dur-med) var(--ease-out);
}
/* the frame settles as the card opens, so the reveal is one move and not a
   panel of text arriving beside a static picture. Keyed on the class, never on
   :hover — app.js owns the state so aria-expanded can never disagree with what
   is on screen. */
.pillar.is-open .pillar__image img { transform: scale(1); }

/* structure only — it must not resize the title, whose em value resolves
   against this element's font-size (same trick as .faq-item__heading) */
.pillar__heading { font-size: inherit; line-height: inherit; }
/* All service copy is permanently visible; this is heading layout, not a
   control. Keeping the class preserves the established visual geometry. */
.pillar__toggle {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
  width: 100%;
  padding: var(--space-s) 0 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
}
.pillar__title {
  font-size: var(--h2);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-2);
  text-wrap: balance;
}
.pillar__icon { position: relative; flex: none; width: 0.7em; height: 0.7em; align-self: center; }
.pillar__line { position: absolute; inset: 0; margin: auto; background-color: currentColor; }
.pillar__line--y { width: 0.1em; height: 0.7em; transition: transform var(--dur-med) var(--ease-out); }
.pillar__line--x { width: 0.7em; height: 0.1em; transition: transform var(--dur-med) var(--ease-out); }
.pillar.is-open .pillar__line--y { transform: rotate(270deg); }
.pillar.is-open .pillar__line--x { transform: rotate(180deg); }

/* 0fr → 1fr, the site's one row-opening move (form status, FAQ). visibility
   keeps a closed body out of the tab order and the accessibility tree. */
.pillar__body {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows var(--dur-med) var(--ease-out), visibility 0s var(--dur-med);
}
.pillar.is-open .pillar__body {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--dur-med) var(--ease-out), visibility 0s;
}
/* nothing may be locked behind a script: with JS off every body is simply open */
html:not(.js) .pillar__body { grid-template-rows: 1fr; visibility: visible; }
html:not(.js) .pillar__icon { display: none; }

/* ALWAYS-OPEN (Tom, 2026-08-19: "las olla kohe näha kogu ulatuses - on
   piisavalt lühike"). The disclosure machinery stays in place, it is simply
   not used: same treatment the ≤992 block already applied, now at every width,
   so the cards read the same everywhere. REVERT = delete this block and set
   PILLARS_ALWAYS_OPEN = false in app.js. */
.pillar__body { grid-template-rows: 1fr; visibility: visible; transition: none; }
.pillar__toggle { cursor: default; }
.pillar__icon { display: none; }
.pillar__image img { transform: none; }
.pillar__body-inner { overflow: hidden; }
.pillar__text { margin-top: var(--space-s); color: var(--_muted); }
.pillar__text + .pillar__text { margin-top: var(--space-xs); }

/* the head padding is --section-space-s; the tail matches it. --section-space-l
   was runway for a longer service list than this page has, and read as a void. */
.pillars__bottom { height: var(--section-space-s); }

@media screen and (max-width: 61.94em) {
  /* hover does not exist here, so nothing may hide behind it: the bodies are
     simply open and the cards stack. The icon would be a lie, so it goes. */
  .pillars__grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  /* stacked, a 4:5 plate is half a phone screen each and the section turns into
     a scroll; a band reads as the same picture without owning the viewport */
  .pillar__image { aspect-ratio: 16 / 10; }
  /* one column means the name is the section's rhythm, not a column head */
  .pillar__title { font-size: var(--display-2); letter-spacing: var(--ls-3); }
  .pillar__toggle { cursor: default; }
  .pillar__icon { display: none; }
  .pillar__body { grid-template-rows: 1fr; visibility: visible; transition: none; }
  .pillar__image img { transform: none; }
}

/* ── references slot (held back; ships hidden until Tom OKs names) ──────── */
.refs { padding-bottom: var(--section-space-s); }
.refs__label { grid-column: 1 / span 3; }
.refs__list {
  grid-column: 4 / span 9;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  color: var(--_muted);
}
@media screen and (max-width: 61.94em) {
  .refs__label { grid-column: 1 / -1; margin-bottom: var(--space-m); }
  .refs__list { grid-column: 1 / -1; }
}

/* ── testimonials (Ollef about-page slider: crossfade + next-slide peek) ── */
.testimonials { position: relative; z-index: 1; background-color: var(--_bg); }
.atmos--testimonials::before { --haze-1: 56% 42% at 10% -6%; --haze-2: 46% 34% at 92% 94%; }
.testimonials__label { grid-column: 1 / span 3; padding-top: var(--section-space-s); }
.testimonials__slider {
  grid-column: 1 / -1;
  margin-top: var(--space-2xl);
  padding-bottom: var(--section-space-s);
}
/* slides stacked in one cell, crossfaded — height follows the tallest quote,
   so the section never jumps when the copy changes length.
   overflow: the transition drifts slides ±8% horizontally; unclipped, a
   mid-crossfade frame showed the incoming quote sliced off at the VIEWPORT
   edge on a phone (Tom's screenshot, 08-19 22:15) — the drift must stay
   inside the slider's own box. */
.testimonials__track { display: grid; overflow: hidden; }
.tst-slide {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-med) var(--ease-out), visibility 0s var(--dur-med);
}
.tst-slide.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-med) var(--ease-out), visibility 0s;
}
/* pretty keeps a quote Tom types later off a one-word last line wherever it is
   supported; the shipped copy carries NBSP joins in the data layer as well,
   because pretty is not everywhere yet. Not a SplitText target, so it is free
   to rebalance the lines. */
.tst-slide__quote { grid-column: 1 / span 8; margin: 0; text-wrap: pretty; }
/* quotes one step down from the t-heading-l default (Sten, 08-19: h2 size).
   Scoped to the slide so the footer heading / page titles / project lead keep
   --h1; covers the preview echo too, which must mirror the quote's metrics or
   the clipped next-slide peek drifts out of line. */
.tst-slide .t-heading-l { font-size: var(--h4); }
.tst-slide__client {
  margin-top: var(--space-xl);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}
.tst-slide__headshot {
  width: 5em;
  height: 5em;
  border-radius: var(--radius-pill);
  object-fit: cover;
}
/* the next quote peeks in at 12%, laid out at container width and clipped to
   its own two columns — the Ollef next-slide-preview trick, verbatim */
.tst-slide__preview { grid-column: 11 / span 2; overflow: hidden; opacity: 0.12; }
.tst-slide__preview-inner { width: calc(100vw - var(--container-padding) * 2); }
.testimonials__nav { display: flex; gap: var(--space-3xs); margin-top: var(--space-xl); }
.tst-arrow {
  display: grid;
  place-items: center;
  width: 2.75em;
  height: 2.75em;
  border: 0;
  background-color: var(--_line);
  color: var(--_text);
  font-size: var(--text-m);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out);
}
.tst-arrow:hover { transform: scale(0.94); }
.tst-arrow:active { transform: scale(0.9); }
/* drag: the quote plane follows the pointer; pan-y leaves vertical scroll
   with the page on touch. While JS owns the move (.is-dragging) the CSS
   crossfade stands down, or the 300ms opacity transition would lag the finger. */
.testimonials__slider { touch-action: pan-y; cursor: grab; }
.testimonials__slider.is-dragging {
  cursor: grabbing;
  user-select: none;
  -webkit-user-select: none;
}
.testimonials__slider.is-dragging .tst-slide { transition: none; }
.tst-slide img { -webkit-user-drag: none; }

@media screen and (max-width: 61.94em) {
  .testimonials__label { grid-column: 1 / -1; }
  .tst-slide__quote { grid-column: 1 / -1; }
  .tst-slide__preview { display: none; }
}

/* ── faq (Ollef about-page dropdowns, on the second light slab) ─────────── */
.faq { padding-block: var(--section-space-s); }
.faq__label { grid-column: 1 / span 3; }
.faq__list { grid-column: 1 / -1; margin-top: var(--space-2xl); }
.faq-item { border-top: var(--hairline) solid var(--_line); }
/* structure only — same trick as .pillar__heading */
.faq-item__heading { font-size: inherit; line-height: inherit; }
.faq-item__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  width: 100%;
  padding: var(--space-m) 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
}
.faq-item__icon { position: relative; flex: none; width: 1em; height: 1em; }
.faq-item__line { position: absolute; inset: 0; margin: auto; background-color: currentColor; }
.faq-item__line--y { width: 0.115em; height: 1em; transition: transform var(--dur-med) var(--ease-out); }
.faq-item__line--x { width: 1em; height: 0.115em; transition: transform var(--dur-med) var(--ease-out); }
/* + becomes −: the vertical line lies down (Ollef rotates 270/180) */
.faq-item.is-open .faq-item__line--y { transform: rotate(270deg); }
.faq-item.is-open .faq-item__line--x { transform: rotate(180deg); }
/* 0fr → 1fr, the same row-opening move the form status uses; visibility keeps
   closed answers out of the tab order and the accessibility tree */
.faq-item__answer {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows var(--dur-med) var(--ease-out), visibility 0s var(--dur-med);
}
.faq-item.is-open .faq-item__answer {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--dur-med) var(--ease-out), visibility 0s;
}
.faq-item__answer-inner { overflow: hidden; }
.faq-item__answer-inner p { max-width: 40em; padding-bottom: var(--space-m); }

/* ── work page (/tood): head + view toggle ──────────────────────────────── */
.work-hero { position: relative; z-index: 1; background-color: var(--_bg); }
/* The work head is a SHORT box: the home hero's low blob (at 92% 76%) parks
   its still-bright core on this section's bottom edge and clips into a hard
   line where .work-index begins (Sten's screenshot, 2026-08-08). Both skies
   keep their glow INSIDE their own box — the head's kiss floats mid-box, the
   list's opener starts below its top edge — so the shared boundary carries
   nothing but flat ground on either side. */
.work-hero.atmos--hero::before { --haze-2: 44% 26% at 92% 40%; }
.work-hero__head { padding-top: var(--space-2xl); align-items: end; }
.work-hero__title-wrap {
  grid-column: 1 / span 9;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
}
.work-hero__count { padding-top: 0.6em; }
.work-hero__views {
  grid-column: 10 / span 3;
  justify-self: end;
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  padding-bottom: 0.5em;
}
.work-hero__views-sep { color: var(--_muted); }
.view-btn {
  position: relative;
  padding: var(--link-pad) 0;
  margin-block: calc(-1 * var(--link-pad));
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--_muted);
  transition: color var(--dur-fast) var(--ease-out);
}
.view-btn::after {
  content: "";
  position: absolute;
  inset: auto 0 var(--link-pad);
  border-top: var(--hairline) solid currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-fast) var(--ease-out);
}
.view-btn:hover { color: var(--_text); }
.view-btn.is-active { color: var(--_text); }
.view-btn.is-active::after { transform: scaleX(1); }

/* ── work page: collection, grid form ───────────────────────────────────── */
.work-index { position: relative; z-index: 1; background-color: var(--_bg); padding-top: var(--space-xl); }
.atmos--worklist::before { --haze-1: 48% 30% at 90% 30%; --haze-2: 52% 32% at 8% 94%; }
.work-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xl) var(--gutter);
}
/* the author display would beat the [hidden] UA rule — make hiding explicit */
.work-grid[hidden], .work-list[hidden] { display: none; }
.work-card__media { aspect-ratio: 1; position: relative; overflow: hidden; }
.work-card__media img { width: 100%; height: 100%; object-fit: cover; }
/* the reveal wipe: covers its image until app.js lifts it (Ollef overlay) */
.work-card__overlay {
  position: absolute;
  inset: 0;
  background-color: var(--_bg);
  pointer-events: none;
}
.work-card__title { margin-top: var(--space-s); }
/* taglines run 1-2 lines; pretty keeps the last line from holding one word
   ("järelvaatamisena" stranded at 1440, home AND /tood share this markup) */
.work-card__tag, .work-card__title { text-wrap: pretty; }
.work-index__bottom { height: var(--section-space-m); }
/* home one-pager form (Tom 2026-08-09): a label opens the section instead of
   /tood's H1 + view toggle, so the section provides its own top air */
.work-index--home { padding-top: var(--section-space-s); }
.work-index__label { grid-column: 1 / span 3; }
.work-index--home .work-grid { margin-top: var(--space-xl); }
.work-index__more { margin-top: var(--space-l); }

/* the card is a link to its case-study page (2026-08-09). The <a> wraps media
   + title + tagline so the whole card is one target; it must be a block or the
   inline box would collapse around the floated grid children. Ollef's grid
   cards carry no hover state by design, so the only affordance added here is
   the keyboard ring — the row form below already has one (its hairline). */
.work-card__link {
  display: block;
  color: inherit;
  text-decoration: none;
}
.work-card__link:focus-visible,
.work-row:focus-visible {
  outline: var(--hairline) solid var(--_text);
  outline-offset: var(--space-3xs);
}

/* ── work page: collection, list form ───────────────────────────────────── */
.work-row {
  display: block;
  color: inherit;
  text-decoration: none;
  border-top: var(--hairline) solid var(--_line);
  transition: border-color var(--dur-fast) var(--ease-out);
  /* scope the thumb hover's per-frame reflow to this row, not the document
     (layout only — NOT paint: the thumb's negative margins overflow the row
     on purpose and paint containment would clip them) */
  contain: layout;
}
.work-row:hover { border-top-color: var(--_text); }
.work-row__grid { align-items: center; padding-block: var(--space-m); }
.work-row__start { grid-column: 1 / span 6; display: flex; align-items: center; }
/* the thumbnail slides open from nothing on hover — 4em tall with negative
   block margins so it contributes no flow height (Ollef list-thumbnail).
   The width/margin animation is the effect: the row text must physically move
   aside, which transform cannot do (Sten approved keeping it, 08-08).
   contain:layout on .work-row scopes each frame's reflow to the hovered row
   instead of the document. */
.work-row__thumb {
  flex: none;
  width: 0;
  height: 4em;
  margin-block: -2em;
  margin-right: 0;
  background-size: cover;
  background-position: 50%;
  transition: width var(--dur-med) var(--ease-out), margin-right var(--dur-med) var(--ease-out);
}
.work-row:hover .work-row__thumb { width: 5.625em; margin-right: var(--space-2xs); }
.work-row__tag { grid-column: 7 / span 4; }
.work-row__year { grid-column: 11 / span 2; justify-self: end; }
.work-list__end { border-top: var(--hairline) solid var(--_line); }

/* ── single project (/tood/<slug>) ──────────────────────────────────────── */
/* Ollef works/rosewood-press: name, the big lead line, then a wide plate. The
   plate is deliberately BELOW the copy, so it never reaches the nav's strip
   and this page needs no ink handoff (see project.php). */
.project-hero { position: relative; z-index: 1; background-color: var(--_bg); }
/* both blobs stay inside this box — a core parked on the bottom edge clips into
   a hard line where .project-body begins (the /tood lesson, 2026-08-08) */
.project-hero.atmos--hero::before { --haze-1: 54% 32% at 10% 30%; --haze-2: 44% 26% at 92% 58%; }
.project-hero__head { padding-top: var(--space-2xl); }
.project-hero__eyebrow { grid-column: 1 / span 3; color: var(--_muted); }
.project-hero__lead { grid-column: 4 / span 8; max-width: 22em; }
.project-hero__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1.6;
  margin-top: var(--space-3xl);
}
.project-hero__media img,
.project-gallery__item img { width: 100%; height: 100%; object-fit: cover; }
/* the reveal wipe, same as the work cards */
.project-hero__overlay {
  position: absolute;
  inset: 0;
  background-color: var(--_bg);
  pointer-events: none;
}

.project-body {
  position: relative;
  z-index: 1;
  background-color: var(--_bg);
  padding-bottom: var(--section-space-s);
}
/* both cores stay well inside the box: this section shares a flat boundary with
   .project-related, and a blob centred at 92% height lights the bottom edge and
   IS the hard line (the .atmos--statement lesson, 2026-08-09) */
.atmos--projectbody::before { --haze-1: 50% 34% at 92% 22%; --haze-2: 54% 36% at 6% 70%; }
.project-body__grid { padding-top: var(--section-space-s); }
.project-meta { grid-column: 1 / span 3; }
.project-meta__item + .project-meta__item { margin-top: var(--space-l); }
.project-meta__list { list-style: none; padding: 0; }
.project-copy { grid-column: 5 / span 7; max-width: 40em; }
.project-copy__label { color: var(--_muted); }
.project-copy__rich { margin-top: var(--space-m); }
.project-copy__rich p + p { margin-top: var(--space-m); }

.project-gallery {
  display: grid;
  gap: var(--space-xl);
  padding-top: var(--section-space-s);
}
.project-gallery__item { position: relative; overflow: hidden; aspect-ratio: 1.6; }

/* the last dark slab before the footer carries the sky like every other one —
   left flat it measured 13 lum against the footer's 33 (its own top-right blob),
   a 20-in-one-row step that /tood does not have because .work-index is hazed */
.project-related { position: relative; z-index: 1; background-color: var(--_bg); }
.atmos--projectrelated::before { --haze-1: 52% 34% at 8% 24%; --haze-2: 46% 30% at 94% 72%; }
.project-related__head { padding-top: var(--section-space-s); align-items: baseline; }
.project-related__label { grid-column: 1 / span 6; color: var(--_muted); }
.project-related__all { grid-column: 10 / span 3; justify-self: end; }
/* three columns holding two cards, so a related card is exactly the size it is
   on /tood — at two columns the 1:1 plates swell to half the viewport and the
   tail of the page outshouts the project it belongs to */
.project-related__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xl) var(--gutter);
  margin-top: var(--space-xl);
}
.project-related__bottom { height: var(--section-space-m); }

@media screen and (max-width: 61.94em) {
  .faq__label { grid-column: 1 / -1; }
  .work-index__label { grid-column: 1 / -1; }
  .faq__list { margin-top: var(--space-xl); }
  .work-grid { grid-template-columns: repeat(2, 1fr); }
  .project-hero__eyebrow { grid-column: 1 / span 4; }
  .project-hero__lead { grid-column: 5 / span 8; }
  .project-meta { grid-column: 1 / span 4; }
  .project-copy { grid-column: 6 / span 7; }
  .project-related__grid { grid-template-columns: repeat(2, 1fr); }
}
@media screen and (max-width: 47.94em) {
  .work-hero__title-wrap { grid-column: 1 / -1; }
  .work-hero__views { grid-column: 1 / -1; justify-self: start; margin-top: var(--space-l); padding-bottom: 0; }
  .work-grid { grid-template-columns: 1fr; }
  .work-row__start { grid-column: 1 / span 4; }
  .work-row__year { grid-column: 5 / span 2; }
  .work-row__tag {
    grid-column: 1 / -1;
    order: 3;
    margin-top: var(--space-3xs);
    color: var(--_muted);
  }
  .work-row__thumb { display: none; }

  /* one column: the plate goes taller than 1.6 so a landscape still reads on a
     phone, and the meta column becomes a block above the description */
  .project-hero__eyebrow,
  .project-hero__lead,
  .project-meta,
  .project-copy,
  .project-related__label,
  .project-related__all { grid-column: 1 / -1; }
  .project-hero__lead { margin-top: var(--space-m); max-width: none; }
  .project-hero__media,
  .project-gallery__item { aspect-ratio: 4 / 3; }
  .project-hero__media { margin-top: var(--space-2xl); }
  .project-copy { margin-top: var(--space-2xl); }
  .project-related__all { justify-self: start; margin-top: var(--space-m); }
  .project-related__grid { grid-template-columns: 1fr; }
}

/* ── lead form ──────────────────────────────────────────────────────────── */
.lead-form__title { margin-bottom: var(--space-m); }
.lead-form__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-m) var(--gutter);
}
.lead-form__field { position: relative; display: flex; flex-direction: column; }
.lead-form__label {
  position: absolute;
  z-index: 1;
  top: 0.55em;
  left: 1.45em;
  color: var(--_muted);
  font-size: max(0.75rem, var(--text-s));
  line-height: 1;
  pointer-events: none;
}
.lead-form__field--full { grid-column: 1 / -1; }
.lead-form__input {
  height: 3.8em;
  padding: 0.9em 1.3em 0;
  border: var(--hairline) solid var(--_line);
  border-radius: var(--radius-field);
  background: transparent;
  color: var(--_text);
  font-size: var(--text-m);
  width: 100%;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.lead-form__input::placeholder { color: var(--_muted); }
/* a border-color nudge is not a focus indicator — WCAG 2.4.11 wants 3:1
   against the adjacent field, which only a real ring delivers here */
.lead-form__input:focus-visible {
  outline: 2px solid var(--_focus);
  outline-offset: 2px;
  border-color: var(--_muted);
}
.lead-form__input:focus { border-color: var(--_muted); }
.lead-form__input[aria-invalid="true"] { border-color: var(--accent); }
textarea.lead-form__input { height: auto; min-height: 6.5em; padding-block: 1em; resize: vertical; }
.lead-form__actions {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-m);
  flex-wrap: wrap;
}
/* fine print, sharing the row with the button — it takes the leftover width
   and wraps inside it instead of dropping under the pill */
.lead-form__note {
  color: var(--_muted);
  font-size: var(--text-s);
  flex: 1 1 0;
  min-width: 12em;
  text-wrap: pretty;
}
/* the reply opens the row it needs instead of snapping into existence */
.lead-form__status {
  grid-column: 1 / -1;
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  color: var(--_muted);
  transition: grid-template-rows var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.lead-form__status > span { overflow: hidden; }
.lead-form__status.is-visible { grid-template-rows: 1fr; opacity: 1; }
.lead-form__status.is-ok { color: var(--_text); }
.lead-form__status.is-error { color: var(--accent); }
.lead-form__privacy {
  grid-column: 1 / -1;
  color: var(--_muted);
  font-size: max(0.75rem, var(--text-s));
  text-wrap: pretty;
}
/* honeypot: visually gone, still in the DOM for bots */
.lead-form__hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

.lead-form--stack .lead-form__grid { grid-template-columns: 1fr; gap: var(--space-s); }

@media screen and (max-width: 47.94em) {
  .lead-form__grid { grid-template-columns: 1fr; }
  .t-body,
  .intro__sub,
  .statement__p,
  .pillar__text,
  .faq-item__answer-inner,
  .lead-form__input,
  .lead-form__note,
  .lead-form__status,
  .site-footer__list,
  .site-footer__copy { font-size: max(1rem, var(--text-m)); }
}

/* ── button (pill) ──────────────────────────────────────────────────────── */
.button {
  display: inline-block;
  padding: 1em 2.5em;
  border: none;
  border-radius: var(--radius-pill);
  background-color: var(--accent);
  color: var(--color-grey-950);
  font-size: var(--text-m);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.button:hover { transform: scale(0.97); }
.button:active { transform: scale(0.94); }
.button:disabled { opacity: 0.5; cursor: wait; transform: none; }
/* in-flight: the button says what is happening, so the wait is legible */
.button[data-pending] { animation: sb-pending 1.2s ease-in-out infinite; }
@keyframes sb-pending {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 0.75; }
}

/* ── CTA band (order test 2026-08-19: the footer head as its own section
   before the KKK slab; reuses the .site-footer__head layout verbatim) ────── */
.cta-band { position: relative; z-index: 1; background-color: var(--_bg); margin-top: -1px; }
.cta-band .site-footer__head { padding-bottom: var(--section-space-s); }

/* ── footer CTA (slide-over reveal) ─────────────────────────────────────── */
.site-footer {
  position: relative;
  z-index: 0;
  background-color: var(--_bg);
}
.site-footer__head { padding-top: var(--section-space-s); }
.site-footer__heading { grid-column: 1 / span 6; max-width: 35em; }
.site-footer__form { grid-column: 8 / span 5; }
.site-footer__divider { grid-column: 1 / -1; margin-top: var(--space-2xl); border-top: var(--hairline) solid var(--_line); }
.site-footer__meta { padding-block: var(--space-2xl); }
.site-footer__col { grid-column: span 3; }
.site-footer__col-label { color: var(--_muted); margin-bottom: var(--space-s); }
/* stacked links: let --link-pad be the spacing so tap areas sit flush, never
   overlapping (a negative margin here would make them intersect) */
.site-footer__list { display: flex; flex-direction: column; gap: 0; align-items: flex-start; }
.site-footer__list .link-underline { margin-block: 0; }
.site-footer__legal {
  margin-top: var(--space-2xl);
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-m);
  flex-wrap: wrap;
  padding-block: var(--space-l);
  color: var(--_muted);
}
/* the belt to sb_no_orphan()'s braces: where it is supported this also balances
   the line above. It is NOT the fix — Firefox ignores it, so the NBSP joins in
   _footer.php are what actually hold. */
.site-footer__copy,
.site-footer__heading .intro__sub { text-wrap: pretty; }

@media screen and (max-width: 61.94em) {
  .site-footer__heading { grid-column: 1 / -1; max-width: 100%; }
  .site-footer__form { grid-column: 1 / -1; margin-top: var(--space-xl); }
  .site-footer__col { grid-column: span 3; margin-bottom: var(--space-l); }
}

/* Below the 767 vw bump the two meta columns go full width, one above the other.
   The contact column holds an email address carrying `u-nowrap` (an address must
   never break, global rule), and at these widths that one token is WIDER than the
   half-column: at 360 the address measures 162px inside a 152px track and pushes
   to within 4px of the "Leht/Page" link beside it. Because both the address and
   the type scale with vw, widening the track does not outrun it at any mobile
   size — the columns have to stop sharing the row. Each column is a 2-3 item list,
   so stacking costs nothing. Triggered by tom@sandbeachmedia.ee at the 2026-08-31
   cutover; the retired tom@sandbeach.ee was four characters shorter and just fit. */
@media screen and (max-width: 47.99em) {
  .site-footer__col { grid-column: 1 / -1; }
}

} /* components */

/* ═══ utilities ══════════════════════════════════════════════════════════ */
@layer utilities {

.u-muted { color: var(--_muted); }
.u-overflow-hidden { overflow: hidden; }
.u-nowrap { white-space: nowrap; }

/* Visually gone, still announced. `hidden` removes a label from the
   accessibility tree entirely, which leaves the input with no name at all. */
.u-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

} /* utilities */

/* ═══ animation initial states (JS only — no-JS shows everything) ════════ */
html.js [data-reveal],
html.js [data-reveal-group] > *,
html.js [data-hero-in],
html.js [data-hero-split],
html.js [data-hero-brand],
html.js [data-title-reveal],
html.js [data-split-reveal] { opacity: 0; }
html.js .page-overlay { opacity: 1; }
/* the footer-dim overlay starts transparent; GSAP scrubs it up. Without this
   it would sit solid over its slab until initGsap registers the tween. */
html.js [data-slab-overlay] { opacity: 0; }
html:not(.js) .page-overlay,
html:not(.js) [data-slab-overlay],
html:not(.js) [data-media-overlay] { display: none; }

/* Reduced motion keeps every state change and its hierarchy — it only removes
   the travel. Loops stop; hover/focus feedback stays legible. */
@media (prefers-reduced-motion: reduce) {
  html.js [data-reveal],
  html.js [data-reveal-group] > *,
  html.js [data-hero-in],
  html.js [data-hero-split],
  html.js [data-hero-brand],
  html.js [data-title-reveal],
  html.js [data-split-reveal] { opacity: 1 !important; transform: none !important; }
  html.js .page-overlay,
  html.js [data-media-overlay] { display: none; }

  .intro__now.is-live::before,
  .intro__cue .link-underline.is-live,
  .button[data-pending] { animation: none; }
  .button[data-pending] { opacity: 0.6; }
  .pillar__image img,
  .pillar.is-open .pillar__image img { transform: none; }
  .button:hover, .button:active { transform: none; opacity: 0.85; }

  /* state changes stay, travel goes */
  .tst-slide,
  .faq-item__answer,
  .faq-item__line--y,
  .faq-item__line--x,
  .work-row,
  .work-row__thumb,
  .view-btn,
  .view-btn::after { transition: none; }
  .tst-arrow:hover, .tst-arrow:active { transform: none; opacity: 0.85; }
}
