/* ==========================================================================
   Boreal Data Labs — borealdatalabs.com
   Theme: boreal night. Mobile-first. Breakpoints at 720px and 1080px only;
   everything between is handled by clamp() and auto-fit grids.
   ========================================================================== */

/* ---------------------------------------------------------------- tokens */
:root {
  --bg: #0A1220;
  --bg-raised: #101B2E;
  --ink: #E8EEF6;
  --ink-dim: #93A3B8;
  --teal: #2FD4C4;
  --aurora-green: #4ADE80;
  --blue: #3B82F6;
  --line: #1D2C42;

  --accent: linear-gradient(135deg, var(--teal), var(--blue));

  --font-sans: "DM Sans", "DM Sans Fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, Consolas, monospace;

  --shadow: 0 8px 30px rgb(0 0 0 / 0.35);
  --radius-card: 12px;
  --radius-btn: 8px;

  /* Sized to hold the nav mark with breathing room: 44px mark + 8px padding
     each side + the 1px border. The hero subtracts this token from 100svh, so
     changing it here is the only edit needed — the calc picks it up. */
  --header-h: 64px;
  --shell-pad: clamp(1rem, 4vw, 2.5rem);
}

/* ------------------------------------------------------- font fallbacks */
/* Google Fonts load with display=swap (per brief), so the fallback is painted
   first and then replaced. Left to system-ui that swap reflowed the hero and
   cost 0.139 CLS, because Segoe UI is ~4% narrower than DM Sans and the H1
   re-wrapped. Arial and Courier New are near-exact width matches (100.33% and
   99.98%, measured against the loaded webfonts), so with these overrides the
   fallback occupies the same space and the swap shifts nothing. */
@font-face {
  font-family: "DM Sans Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  size-adjust: 100.33%;
  ascent-override: 98.88%;
  descent-override: 30.9%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "JetBrains Mono Fallback";
  src: local("Courier New"), local("Liberation Mono");
  size-adjust: 99.98%;
  ascent-override: 102.02%;
  descent-override: 30%;
  line-gap-override: 0%;
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: clamp(1rem, 0.95rem + 0.2vw, 1.125rem);
  line-height: 1.65;
  /* clip (not hidden) so no descendant can produce a horizontal scrollbar
     while position:sticky on the header keeps working. */
  overflow-x: clip;
}

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

h1, h2, h3 {
  line-height: 1.15;
  margin: 0;
  font-weight: 700;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }

a { color: var(--teal); }

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

/* Sections sit below the sticky header when jumped to via anchor. */
section[id], #top { scroll-margin-top: calc(var(--header-h) + 10px); }

/* ---------------------------------------------------------- shell / grid */
.shell {
  width: 100%;
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--shell-pad);
}

/* ------------------------------------------------------------ skip link */
.skip-link {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 200;
  padding: 0.7rem 1.1rem;
  background: var(--bg-raised);
  color: var(--ink);
  border: 1px solid var(--teal);
  border-radius: 0 0 var(--radius-btn) 0;
  font-weight: 500;
  text-decoration: none;
  transform: translateY(-120%);
}
.skip-link:focus-visible { transform: none; }

/* --------------------------------------------------------- boot veil §5b */
/* Hidden by default: without JS the .booting class is never added, so the
   veil is never rendered and can never trap clicks or a screen reader. */
.veil { display: none; }

html.booting .veil {
  display: grid;
  place-items: center;
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--bg);
  opacity: 1;
  /* The fade is triggered by script.js adding .is-leaving rather than by a
     fixed CSS delay, so the 0.6s minimum hold can be enforced against the
     actual load time instead of guessed at. */
  transition: opacity 0.3s ease;
}
html.booting .veil.is-leaving { opacity: 0; }

.veil__mark {
  font-size: clamp(1.6rem, 6vw, 2.6rem);
  font-weight: 400;
  letter-spacing: -0.02em;
  white-space: nowrap;
  color: var(--teal); /* fallback when background-clip:text is unsupported */
}
.veil__mark strong { font-weight: 700; }

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .veil__mark {
    color: transparent;
    background-image: linear-gradient(
      100deg,
      var(--ink) 0%, var(--ink) 26%,
      var(--aurora-green) 38%, var(--teal) 47%, var(--blue) 56%,
      var(--ink) 68%, var(--ink) 100%
    );
    background-size: 320% 100%;
    /* Must stay within 0%-100%. Because the gradient is wider than the text
       box, any position in that range still covers it completely; outside it
       the image slides off and uncovered glyphs render fully transparent —
       which made "Bore" vanish mid-sweep. */
    background-position: 100% 0;
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    /* Completes inside the 0.6s minimum hold, leaving a beat on the finished
       wordmark before the fade begins. */
    animation: veil-sweep 0.5s ease-out forwards;
  }
}

@keyframes veil-sweep { to { background-position: 0% 0; } }

/* ---------------------------------------------------------------- header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* Less the 1px bottom border on .site-header, so the header's *total* height
     equals --header-h exactly. The hero subtracts that token from 100svh, and
     any mismatch pushes the hero past the fold by the difference. */
  min-height: calc(var(--header-h) - 1px);
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  padding-block: 0.5rem;
}

/* The transparent mark sits on the header surface directly. Sized in CSS with
   a 1:1 intrinsic ratio, so the 28px -> 32px swap at 720px causes no reflow.
   flex:none keeps it from being squeezed when the nav is tight. */
/* 44/48px: 48px is the first size at which this raster mark's mountain and
   network nodes stay genuinely crisp. 48+16 padding = 64 still fits the 67px
   desktop inner box, but 44+16 = 60 did not fit the old 57px mobile one, so
   --header-h rose from 58px to 64px on mobile. Desktop stayed at 68px. */
.wordmark__mark {
  width: 44px;
  height: 44px;
  flex: none;
}

/* Scoped to the name wrapper so the rules don't also catch the mark or the
   wrapper itself. Normal inline flow inside it preserves the word space. */
.wordmark__name strong { font-weight: 700; }
.wordmark__name > span { font-weight: 400; color: var(--ink-dim); }

/* Hamburger. Stays [hidden] until script.js unhides it, so it never appears
   as a dead control when JS is off. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -10px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--radius-btn);
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 20px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
}
.nav-toggle__bars { position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0;
}
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after  { top: 6px; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { top: 0; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after  { top: 0; transform: rotate(-45deg); }

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(0.5rem, 2vw, 1.75rem);
}
.site-nav a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 0.35rem;
  color: var(--ink-dim);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: color 0.2s ease;
}
.site-nav a:hover { color: var(--ink); }

/* ------------------------------------------------------------------ hero */
.hero {
  position: relative;
  display: grid;
  align-content: center;
  /* svh, not vh: mobile browser chrome must not push content out of view.
     The header is subtracted because the hero starts below it — otherwise the
     hero is one screen *plus* the header and the CTAs fall off the fold. */
  min-height: min(calc(100svh - var(--header-h)), 900px);
  padding-block: clamp(3rem, 9vh, 6rem) clamp(3.5rem, 11vh, 7rem);
  isolation: isolate;
  border-bottom: 1px solid var(--line);
}

.hero__inner { position: relative; z-index: 1; }

/* Large mark, occupying the slot the eyebrow used to. Block-level and first in
   flow, so its left edge is the container's content edge — identical to the
   H1's. Width and height use the same clamp so it stays square, and the 1:1
   intrinsic ratio in the HTML attributes means no reservation error. */
.hero__mark {
  width: clamp(80px, 9vw, 128px);
  height: clamp(80px, 9vw, 128px);
  margin-bottom: clamp(0.9rem, 1.6vw, 1.5rem);
}

/* The H1 carries no top margin — the space above it comes from .hero__mark's
   margin, and from the hero's padding plus grid centring above that. */
.hero__title {
  font-size: clamp(2.2rem, 5vw + 1rem, 4.5rem);
  max-width: 20ch;
  margin: 0;
}

/* Accent gradient on part of the headline, with a solid fallback. */
.grad { color: var(--teal); }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .grad {
    color: transparent;
    background-image: var(--accent);
    -webkit-background-clip: text;
    background-clip: text;
  }
}

.hero__sub {
  max-width: 62ch;
  margin-top: clamp(1rem, 2.5vh, 1.6rem);
  color: var(--ink-dim);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: clamp(1.5rem, 4vh, 2.4rem);
}

/* ------------------------------------------------------- aurora §5 */
.aurora {
  position: absolute;
  inset: 0 0 auto 0;
  height: 100%;
  overflow: hidden;
  pointer-events: none; /* never intercept clicks */
  z-index: 0;
  /* Anchored to the top, gone by ~70% of hero height. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 42%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 42%, transparent 74%);
}

.aurora svg {
  position: absolute;
  top: 0;
  left: -12%;
  width: 124%;
  height: 100%;
}

/* Readability scrim, painted between the aurora and the hero content. The
   aurora can therefore be much brighter in the empty regions where it reads as
   sky, while the area behind the text keeps its contrast. An ellipse anchored
   near the headline rather than to the viewport's left edge, so it still tracks
   the text once the shell centres itself on wide screens.

   Ordering: .aurora is z-index 0 and comes first, .hero__inner is z-index 1,
   and this ::after is a positioned z-index:auto box later in DOM order — so it
   lands between them without needing a z-index of its own. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(125% 95% at 26% 40%,
      color-mix(in srgb, var(--bg) 78%, transparent) 0%,
      color-mix(in srgb, var(--bg) 56%, transparent) 38%,
      color-mix(in srgb, var(--bg) 16%, transparent) 66%,
      transparent 82%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--bg) 22%, transparent) 0%,
      color-mix(in srgb, var(--bg) 8%, transparent) 45%,
      transparent 78%);
}

/* Blur lives on the group. Only transform and opacity animate — both are
   compositor properties, so the blurred layer is rasterized once and then
   re-composited, never re-blurred per frame. */
.ribbon {
  filter: blur(44px);
  will-change: transform, opacity;
}
/* Narrow curtains: less blur reads as nearer, and costs less to rasterise. */
.ribbon--e, .ribbon--f, .ribbon--g { filter: blur(30px); }
/* Opacity now breathes inside each keyframe set, which is what reads as a
   living curtain rather than a drifting shape. The value declared here is the
   frozen state used under prefers-reduced-motion, so it sits mid-range.
   Durations are deliberately non-harmonic so the layers never resynchronise. */
/* Negative delays start each layer part-way through its cycle. Without them
   every layer sits at its 0% keyframe on load — the dim end — so the hero
   opened on the flattest frame it ever shows and only lit up ~10s later. */
/* The declared opacity is the frozen prefers-reduced-motion state, so it sits
   mid-range in each keyframe set — a reduced-motion visitor gets an aurora of
   comparable weight, not a washed-out one. All are below the animated peaks
   the contrast report measured against, so they cannot be the worst case. */
.ribbon--a { opacity: 0.36; animation: drift-a 26s ease-in-out infinite; animation-delay: -9s; }
.ribbon--b { opacity: 0.32; animation: drift-b 31s ease-in-out infinite; animation-delay: -12s; }
.ribbon--c { opacity: 0.28; animation: drift-c 29s ease-in-out infinite; animation-delay: -6s; }
.ribbon--d { opacity: 0.21; animation: drift-d 44s ease-in-out infinite; animation-delay: -22s; }
.ribbon--e { opacity: 0.25; animation: drift-e 34s ease-in-out infinite; animation-delay: -13s; }
.ribbon--f { opacity: 0.23; animation: drift-f 38s ease-in-out infinite; animation-delay: -8s; }
.ribbon--g { opacity: 0.19; animation: drift-g 41s ease-in-out infinite; animation-delay: -20s; }

/* Feature check: without blur the ribbons would read as hard-edged shapes
   rather than atmosphere, so drop the aurora entirely. Done in CSS so the
   guard holds with JS disabled too. */
@supports not (filter: blur(4px)) {
  .aurora { display: none; }
}

/* Each layer drifts horizontally, stretches vertically, and breathes in
   intensity on its own period. Three offset phases per layer rather than a
   simple there-and-back, so no single layer reads as a loop. */
/* Peaks calibrated against the measured contrast headroom: the worst-case
   background behind the sub sat at luminance 0.008 against a 0.041 ceiling, so
   there was roughly 5x of room. Re-measure if these move. */
@keyframes drift-a {
  0%, 100% { transform: translate3d(-34px, 0, 0) scaleY(1);    opacity: 0.22; }
  35%      { transform: translate3d(12px, 0, 0)  scaleY(1.09); opacity: 0.50; }
  70%      { transform: translate3d(40px, 0, 0)  scaleY(1.02); opacity: 0.31; }
}
@keyframes drift-b {
  0%, 100% { transform: translate3d(36px, 0, 0)  scaleY(1.06); opacity: 0.20; }
  40%      { transform: translate3d(-14px, 0, 0) scaleY(0.96); opacity: 0.45; }
  75%      { transform: translate3d(-38px, 0, 0) scaleY(1.04); opacity: 0.28; }
}
@keyframes drift-c {
  0%, 100% { transform: translate3d(-22px, 0, 0) scaleY(0.97); opacity: 0.17; }
  45%      { transform: translate3d(18px, 0, 0)  scaleY(1.08); opacity: 0.39; }
  80%      { transform: translate3d(34px, 0, 0)  scaleY(1.01); opacity: 0.23; }
}
@keyframes drift-d {
  0%, 100% { transform: translate3d(26px, 0, 0)  scaleY(1.04); opacity: 0.12; }
  50%      { transform: translate3d(-28px, 0, 0) scaleY(0.95); opacity: 0.30; }
}
@keyframes drift-e {
  0%, 100% { transform: translate3d(16px, 0, 0)  scaleY(0.98); opacity: 0.14; }
  38%      { transform: translate3d(-20px, 0, 0) scaleY(1.10); opacity: 0.36; }
  72%      { transform: translate3d(-6px, 0, 0)  scaleY(1.03); opacity: 0.20; }
}
@keyframes drift-f {
  0%, 100% { transform: translate3d(-18px, 0, 0) scaleY(1.05); opacity: 0.12; }
  42%      { transform: translate3d(22px, 0, 0)  scaleY(0.96); opacity: 0.33; }
  78%      { transform: translate3d(8px, 0, 0)   scaleY(1.07); opacity: 0.19; }
}
@keyframes drift-g {
  0%, 100% { transform: translate3d(20px, 0, 0)  scaleY(1.02); opacity: 0.11; }
  48%      { transform: translate3d(-16px, 0, 0) scaleY(1.09); opacity: 0.28; }
}

/* --------------------------------------------------------------- sections */
.section {
  padding-block: clamp(3.5rem, 9vw, 6.5rem);
  border-bottom: 1px solid var(--line);
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--teal);
  margin: 0;
}

.section__title {
  font-size: clamp(1.75rem, 3vw + 0.5rem, 2.75rem);
  margin-block: 0.5rem clamp(1.75rem, 4vw, 2.75rem);
}

/* ------------------------------------------------------------------ cards */
.grid { display: grid; gap: clamp(0.875rem, 2vw, 1.25rem); }
.grid--services { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
/* min() keeps the track floor from exceeding the container on a 320px phone,
   where a bare minmax(300px,...) would overflow its own gutter. */
.grid--products { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }

.card {
  position: relative;
  padding: clamp(1.25rem, 3vw, 1.75rem);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

/* Gradient hover ring, drawn as a masked outline so the 1px border geometry
   never changes (no reflow, no layout shift). */
.card::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 13px;
  padding: 1px;
  background: var(--accent);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}
.card:hover::after,
.card:focus-within::after { opacity: 1; }

.card h3 {
  font-size: clamp(1.05rem, 0.9rem + 0.4vw, 1.25rem);
  margin-bottom: 0.5rem;
}
.card p { color: var(--ink-dim); font-size: 0.9375rem; }

.icon {
  width: 24px;
  height: 24px;
  margin-bottom: 0.9rem;
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Featured product card */
.card--featured {
  margin-bottom: clamp(0.875rem, 2vw, 1.25rem);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  background:
    linear-gradient(135deg, rgb(47 212 196 / 0.07), rgb(59 130 246 / 0.05)),
    var(--bg-raised);
  box-shadow: var(--shadow);
}
.card--featured__title {
  font-size: clamp(1.5rem, 2.5vw + 0.5rem, 2.25rem);
  margin-bottom: 0.6rem;
}
.card--featured p { font-size: clamp(1rem, 0.95rem + 0.2vw, 1.0625rem); max-width: 68ch; }

.tag {
  display: inline-block;
  margin-bottom: 0.85rem;
  padding: 0.25rem 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--teal);
  background: rgb(47 212 196 / 0.1);
  border: 1px solid rgb(47 212 196 / 0.32);
  border-radius: 999px;
}

.meta {
  margin-top: 0.9rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
}

.card__link { margin-top: 1.15rem; }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 44px;
  font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.link-arrow span { transition: transform 0.2s ease; }
.link-arrow:hover { border-bottom-color: currentColor; }
.link-arrow:hover span { transform: translateX(3px); }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.7rem 1.35rem;
  border-radius: var(--radius-btn);
  border: 1px solid transparent;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 500;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.2s ease, background-color 0.2s ease,
              border-color 0.2s ease, opacity 0.2s ease;
}
.btn:hover { transform: translateY(-1px); }

.btn--primary {
  background-image: var(--accent);
  color: var(--bg); /* dark on teal 10.2:1, on blue 5.1:1 — both pass AA */
  font-weight: 700;
}
.btn--primary:hover { opacity: 0.92; }

.btn--ghost {
  border-color: var(--line);
  color: var(--ink);
  background: transparent;
}
.btn--ghost:hover { border-color: var(--teal); background: rgb(47 212 196 / 0.06); }

.btn--secondary {
  color: var(--teal);
  background: rgb(47 212 196 / 0.12);
  border-color: rgb(47 212 196 / 0.4);
  font-weight: 700;
}
.btn--secondary:hover { background: rgb(47 212 196 / 0.18); border-color: var(--teal); }

/* ---------------------------------------------------------------- contact */
/* Single column since the logo card was removed: a full-width panel in the
   same surface language as the cards above, with the address and the actions
   pushed to opposite ends. That fills the measure instead of leaving a
   half-width column with dead space beside it. */
.contact {
  display: grid;
  gap: clamp(1.5rem, 4vw, 2rem);
  padding: clamp(1.375rem, 3.5vw, 2.25rem);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
}

address {
  font-style: normal;
  color: var(--ink-dim);
  line-height: 1.8;
}
.contact__name {
  display: block;
  color: var(--ink);
  font-weight: 700;
  font-size: 1.0625rem;
}

/* A short label would leave the right-hand column looking thin against the
   five-line address opposite, so the button gets a floor on its width instead.
   Independent of the label text, and 11rem still clears the 244px content box
   at 320px. */
.contact__cta { justify-self: start; }
.contact__cta .btn {
  min-width: 11rem;
  max-width: 100%;
}

/* ----------------------------------------------------------------- footer */
.site-footer { padding-block: clamp(2rem, 5vw, 2.75rem); }
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
}
.site-footer__copy { color: var(--ink-dim); font-size: 0.875rem; }
.site-footer__nav ul { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
.site-footer__nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink-dim);
  font-size: 0.875rem;
  text-decoration: none;
}
.site-footer__nav a:hover { color: var(--teal); }

/* ---------------------------------------------------------- scroll reveal */
/* Only hide content when JS is present to reveal it again. */
.has-js .reveal {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.has-js .reveal.is-visible { opacity: 1; transform: none; }

/* ============================================================ breakpoints */

/* Mobile nav: collapsible dropdown, JS-gated. */
@media (max-width: 719.98px) {
  .has-js .nav-toggle:not([hidden]) { display: inline-flex; }

  .has-js .site-nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow);
  }
  .has-js .site-nav.is-open { display: block; }

  .has-js .site-nav ul { display: block; padding-inline: var(--shell-pad); }
  .has-js .site-nav li + li { border-top: 1px solid var(--line); }
  .has-js .site-nav a { min-height: 52px; padding-inline: 0; font-size: 1rem; }
}

@media (min-width: 720px) {
  :root { --header-h: 68px; }
  .wordmark__mark { width: 48px; height: 48px; }
  .grid--products { grid-template-columns: repeat(2, 1fr); }
  .contact {
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: clamp(2rem, 5vw, 3.5rem);
    padding-inline: clamp(1.75rem, 4vw, 2.75rem);
  }
  .contact__cta { justify-self: end; }
}

@media (min-width: 1080px) {
  .hero__title { max-width: 22ch; }
  .grid--services { grid-template-columns: repeat(4, 1fr); }
}

/* Small phones in portrait (320x568 and similar): reclaim vertical padding so
   the hero CTAs land above the fold. The H1 clamp floor is fixed by the design
   system, so the space has to come from padding rather than type. */
@media (orientation: portrait) and (max-height: 620px) {
  .hero { padding-block: clamp(1.75rem, 5vh, 3rem) clamp(2rem, 6vh, 3.5rem); }
  /* The 80px clamp floor pushed the CTAs ~95px below the fold on a 320x568
     phone. Only short portrait viewports are affected; 390x844 is taller than
     620px and keeps the full 80px. */
  .hero__mark { width: 56px; height: 56px; }
}

/* Phone landscape (and other short viewports): the hero must fit without the
   headline colliding with the sticky nav. */
@media (orientation: landscape) and (max-height: 560px) {
  .hero {
    min-height: 0;
    padding-block: clamp(1.25rem, 5vh, 2rem) clamp(1.5rem, 6vh, 2.25rem);
  }
  /* A short landscape viewport has ~322px for the whole hero, which the 80px
     clamp floor overruns — it pushed the CTAs below the fold. 48px is still
     the smallest size this raster mark stays crisp at. */
  .hero__mark {
    width: 48px;
    height: 48px;
    margin-bottom: 0.5rem;
  }
  .hero__title { font-size: clamp(1.6rem, 3.4vw + 0.5rem, 2.4rem); max-width: 26ch; }
  .hero__sub { margin-top: 0.75rem; font-size: 0.9375rem; max-width: 70ch; }
  .hero__actions { margin-top: 1.1rem; }
  .aurora {
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 40%, transparent 78%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 40%, transparent 78%);
  }
  .section { padding-block: clamp(2.5rem, 12vh, 4rem); }
}

/* ------------------------------------------------------- reduced motion §8 */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* Aurora freezes into a static gradient rather than animating. */
  .ribbon {
    animation: none !important;
    transform: none !important;
    will-change: auto;
  }

  /* Veil: no sweep, just a plain overlay showing a static gradient wordmark.
     The transition kill above collapses the fade, so it disappears the moment
     the 0.6s minimum hold elapses — which script.js still enforces. */
  .veil__mark {
    animation: none !important;
    background-position: 50% 0 !important;
  }

  /* Never leave revealed content stuck invisible. */
  .has-js .reveal { opacity: 1 !important; transform: none !important; }

  .btn:hover { transform: none; }
  .link-arrow:hover span { transform: none; }
}
