/* Brand wordmark face (Orbitron), self-hosted: Google's variable latin file, one file for every
   weight the site uses (700, 800); each page's <head> preloads it. Same weight range and
   unicode-range as the Google Fonts faces it replaces. Used only by the brand X / .vx-wordmark /
   .vx-name — see --font-orbitron. */
@font-face {
  font-family: "Orbitron";
  font-style: normal;
  font-weight: 700 900;
  font-display: swap;
  src: url("fonts/orbitron-latin-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===== Reset & Base ===== */
*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --color-primary: #0e4d6e;
  --color-primary-dark: #093a54;
  --color-accent: #1a8fc4;
  --color-accent-light: #2ab0e8;
  --color-dark: #0c1a24;
  --color-text: #2c3e50;
  --color-text-light: #637383;      /* one step darker than #6b7c8d: 4.53:1 on --color-light, 4.87:1 on white (AA) */
  --color-light: #f4f7f9;
  --color-white: #ffffff;
  --color-border: #dce4ea;
  --color-cyan: #00AEEF;            /* signature brand cyan (logo X, accents, VorteX) */
  --color-accent-btn: #157fa9;      /* primary button fill — deepened so white text passes AA (4.53:1); hover glows instead of lightening */
  --shadow: 0 2px 16px rgba(14, 77, 110, 0.08);
  --shadow-lg: 0 8px 32px rgba(14, 77, 110, 0.12);
  --shadow-xl: 0 20px 56px rgba(14, 77, 110, 0.18);   /* one rung past --shadow-lg: for a tile that already rests on lg */
  --radius: 8px;
  --transition: 0.3s ease;
  --ease-reveal: cubic-bezier(0.16, 1, 0.3, 1);  /* scroll-reveal/entrance curve — same as the VorteX --ease-out */
  /* Spacing scale — adopted verbatim from the app (construction-bidding-app) for
     cross-codebase consistency. Every gap/margin/padding snaps to one of these. */
  --space-1: 4px;   --space-2: 8px;   --space-3: 12px;  --space-4: 16px;
  --space-5: 20px;  --space-6: 24px;  --space-7: 28px;  --space-8: 32px;
  --space-9: 36px;  --space-10: 40px; --space-12: 48px; --space-14: 56px;
  --space-16: 64px; --space-20: 80px; --space-24: 96px;
  --section-y: clamp(64px, 10vw, 96px);   /* vertical padding of every content section, all pages: 64 on phones → 96 on desktop */
  --header-height: 72px;   /* fixed header (.header-inner height) — hero tops derive from this */
  --projects-band-h: 285px; /* Projects title band height, shared with the header's top-state gradient (310px at ≤768) */
  --brand-x-size: 1.5em;   /* enlarged brand-mark X (logo + VorteX nav/footer/badge/headline) */
  --brand-x-drop: 0.14em;  /* small downward nudge so the X peeks slightly below the baseline (was a deep "middle" drop) */
  --brand-x-kern: -0.07em; /* Orbitron-tuned: a bit more tuck than the old -0.05em, since Orbitron's X is wider */
  --font-orbitron: "Orbitron", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

html {
  scroll-behavior: smooth;
  font-size: 16px;
  /* Let the document fill the full screen height so viewport-fit=cover can extend the page
     under the iOS status bar (env(safe-area-inset-top) stays 0 if the page never reaches the
     screen edges). Most effective in Home-Screen / standalone mode (apple-mobile-web-app-*). */
  min-height: 100%;
  /* iOS safe area (Dynamic Island / notch): the ROOT background is what iOS paints
     behind the safe-area inset. With no html background, the WHITE body background
     propagates to the canvas → white strip behind the island. A dark root makes that
     strip ALWAYS navy, independent of safe-area math. Invisible on desktop (no inset;
     body covers the viewport — only an overscroll bounce would ever reveal it). */
  background-color: var(--color-dark);   /* #0c1a24 */
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  color: var(--color-text);
  line-height: 1.6;
  background: var(--color-white);
  min-height: 100vh;   /* fill the screen so cover can engage; vh resolves without an html height chain */
}

/* The light pages' top element is the dark .hero, so a dark body reads correctly up
   top while removing the white that bled into the iOS Dynamic Island inset under
   viewport-fit=cover. vortex already uses a dark body. */
body[data-page="home"],
body[data-page="projects"],
body[data-page="about"] {
  background: var(--color-dark); /* #0c1a24 — removes the Dynamic Island white strip */
}

/* iOS status-bar safe-area filler — VORTEX ONLY. Home/projects let the HEADER itself own
   the notch: .header has padding-top: env(safe-area-inset-top), so its own background —
   transparent over the dark hero at rest, white when scrolled — fills the inset and always
   matches the header, fading with it (no separate strip to fall out of sync). Vortex keeps
   this dark strip because its light-phase header is transparent over a BLUE gradient and the
   status bar must stay dark to match theme-color #0c1a24. z 999 = below the header (1000). */
body[data-page="vortex"]::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top);
  background: var(--color-dark);
  z-index: 999;
  pointer-events: none;
}

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

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

ul {
  list-style: none;
}

.container {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--space-6);
}

/* ===== Buttons ===== */
.btn {
  display: inline-block;
  padding: var(--space-4) var(--space-8);   /* 14→16 / 32 */
  background: var(--color-accent-btn);
  color: var(--color-white);
  font-size: 1rem;
  font-weight: 600;
  font-family: inherit;   /* <button class="btn"> otherwise falls back to the UA font (Arial) */
  border: 2px solid var(--color-accent-btn);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--transition), transform var(--transition), box-shadow var(--transition);
  box-shadow: 0 4px 14px rgba(21, 127, 169, 0.35);   /* = --color-accent-btn @ 35% */
}

/* Hover keeps the AA fill and lights up with a cyan glow instead of a lighter fill.
   Hover effects are limited to real pointers so a tap never leaves them stuck on. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover {
    background: var(--color-accent-btn);
    border-color: var(--color-accent-btn);
    transform: translateY(-2px);
    box-shadow: 0 6px 24px rgba(0, 174, 239, 0.55);   /* = --color-cyan @ 55% */
  }
}

.btn-outline {
  background: transparent;
  color: var(--color-white);
  border-color: var(--color-white);
  box-shadow: none;   /* no glow at rest — the glow belongs to the primary */
}

@media (hover: hover) and (pointer: fine) {
  .btn-outline:hover {
    background: var(--color-white);
    color: var(--color-primary);
    transform: translateY(-2px);          /* match .btn lift for consistent side-by-side behaviour */
    box-shadow: var(--shadow-lg);
  }
}

.btn-sm {
  padding: var(--space-2) var(--space-6);   /* 10→8 / 22→24 */
  font-size: 0.9rem;
}

/* #3 Button sheen: one diagonal highlight sweeps across on hover, layered over the
   existing lift. White for solid buttons; cyan-tinted for .btn-outline (its hover
   background turns white, where a white sheen would vanish). Desktop pointers only. */
.btn {
  position: relative;
  overflow: hidden;   /* clip the sheen to the pill */
}
.btn::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 40%;
  background: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.35) 50%, transparent 100%);
  transform: skewX(-20deg);
  opacity: 0;
  pointer-events: none;
}
.btn-outline::after {
  background: linear-gradient(105deg, transparent 0%, rgba(0, 174, 239, 0.22) 50%, transparent 100%);
}
@keyframes btnSheen {
  from { opacity: 1; transform: skewX(-20deg) translateX(0); }
  to   { opacity: 1; transform: skewX(-20deg) translateX(400%); }
}
@media (hover: hover) and (pointer: fine) {
  .btn:hover::after {
    animation: btnSheen 0.65s ease;
  }
}

/* ===== Header ===== */
.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 1000;
  background: transparent;
  /* No background transition: iOS 26 Safari tints the status bar from this element's
     background-color, and a fade let it latch a half-faded white after scrolling back up.
     The background swaps instantly; only the shadow fades. (VorteX has its own sweep.) */
  transition: box-shadow var(--transition);
}

.header.scrolled {
  background: var(--color-white);
  box-shadow: var(--shadow);
}

/* Home/Projects top state: an opaque navy (the colour Safari reads for the status-bar tint)
   with the hero's own gradient painted over it, sized to the section underneath, so at the
   top of the page the bar still reads as part of the hero. Scrolled = the solid white above. */
body[data-page="home"] .header:not(.scrolled),
body[data-page="projects"] .header:not(.scrolled),
body[data-page="about"] .header:not(.scrolled) {
  background-color: var(--color-dark);
  background-image: linear-gradient(135deg, var(--color-dark) 0%, var(--color-primary) 50%, var(--color-accent) 100%);
  background-repeat: no-repeat;
  background-size: 100% 100vh;   /* = .hero min-height */
}
body[data-page="projects"] .header:not(.scrolled),
body[data-page="about"] .header:not(.scrolled) {
  background-size: 100% var(--projects-band-h);   /* = the title band's height */
}
/* Home also carries the hero's film grain (.hero-noise, 4.5%, opacity baked into the tile)
   and its top-right glow (.hero::before at scrollY 0: a 600px circle at top -50% /
   right -20%, fading out at 70% of its 424px corner radius), so the texture continues
   across the header's bottom edge. Layers, top to bottom: grain, glow, gradient. */
body[data-page="home"] .header:not(.scrolled) {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.045'/%3E%3C/svg%3E"),
    radial-gradient(circle 424.26px at calc(120% - 300px) calc(300px - 50%), rgba(26, 143, 196, 0.15) 0%, transparent 70%),
    linear-gradient(135deg, var(--color-dark) 0%, var(--color-primary) 50%, var(--color-accent) 100%);
  background-repeat: repeat, no-repeat, no-repeat;
  background-size: 200px 200px, 100% 100vh, 100% 100vh;
}

/* Home/Projects: the header's logo, links and hamburger lines swap colour on the same frame
   as the background (a 0.3s colour fade left the logo white-on-white / navy-on-navy for
   ~100ms). The hamburger keeps its transform/opacity morph; the Get a Quote .btn is not a
   link colour and is left alone; VorteX keeps its own sweep transitions (vortex.html). */
body[data-page="home"] .header .logo-text,
body[data-page="projects"] .header .logo-text,
body[data-page="about"] .header .logo-text,
body[data-page="home"] .header .nav a:not(.btn),
body[data-page="projects"] .header .nav a:not(.btn),
body[data-page="about"] .header .nav a:not(.btn) {
  transition: none;
}
body[data-page="home"] .header .nav-toggle span,
body[data-page="projects"] .header .nav-toggle span,
body[data-page="about"] .header .nav-toggle span {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 72px;
}

/* Compact "Quote" button in the phone header; desktop has "Get a Quote" in the nav */
.header-quote {
  display: none;
}

/* ===== Language toggle (EN | ES, i18n.js) =====
   One button, at least 44x44, showing both codes with the active one highlighted: far right on
   desktop, beside Quote on phones. Its colours follow the bar like the nav links (light on the
   dark top state, dark once scrolled, light throughout on VorteX) and swap instantly with it. */
.lang-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-left: var(--space-4);
  padding: 0;
  background: none;
  border: none;
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  color: rgba(255, 255, 255, 0.7);
  cursor: pointer;
}

.lang-sep {
  margin: 0 var(--space-1);
  opacity: 0.6;
}

.lang-toggle .is-active {
  color: var(--color-white);
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
}

.header.scrolled .lang-toggle {
  color: var(--color-text-light);
}

.header.scrolled .lang-toggle .is-active {
  color: var(--color-primary);
}

body[data-page="vortex"] .header .lang-toggle {
  color: rgba(255, 255, 255, 0.7);
}

body[data-page="vortex"] .header .lang-toggle .is-active {
  color: var(--color-white);
}

@media (hover: hover) and (pointer: fine) {
  .lang-toggle [data-lang]:not(.is-active):hover {
    color: var(--color-accent-light);
  }
}

.logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);   /* 10→8 */
  min-height: 44px;      /* tap target (was 32px tall) */
}


.logo-text {
  font-size: 1.85rem;
  font-weight: 800;
  letter-spacing: -0.5px;
  line-height: 1;
  color: var(--color-white);
  /* Crossfade the wordmark white<->dark in sync with the header background on scroll.
     (Vortex overrides this transition with its own more-specific rule, so it's unaffected.) */
  transition: color var(--transition);
}

/* Brand X-motif — static cyan everywhere by default, enlarged and sitting just
   slightly past the baseline (baseline-aligned + a small controlled drop, NOT the
   deep vertical-align:middle drop). The animated tricolor "travelling" X is scoped
   to the VorteX page headline only (see vortex.html). */
.logo-x,
.vortex-badge .vx .x {
  display: inline-block;             /* enables the transform nudge */
  font-family: var(--font-orbitron); /* the X glyph is Orbitron in BOTH names (HydroWrXs + VorteX) */
  color: var(--color-cyan);
  font-size: var(--brand-x-size);
  vertical-align: baseline;
  transform: translateY(var(--brand-x-drop));
  margin: 0 var(--brand-x-kern);     /* compensate the enlarged glyph's wider side-bearings */
}

/* VorteX brand wordmark face — Orbitron (loaded per-page where a VorteX mark
   appears). Scoped to the VorteX wordmark spans ONLY; no other typography. */
.vx-wordmark,
.vortex-badge .vx {
  font-family: "Orbitron", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-weight: 700;
  /* The wordmark never sets the height of the line it sits in. Orbitron's tall ascent and the
     1.5em brand X (which inherits this) would otherwise build a taller line box than the
     surrounding text: the X alone made the footer VorteX row 11px taller than its neighbours,
     dropped VorteX ~2px low in its mobile-menu row and pushed the hero pill's text 3px below
     centre. With 0 the glyphs still paint at full size, but the line is the text's own. */
  line-height: 0;
}

/* "Powered by VorteX" sub-line under a section title */
.title-powered {
  display: block;
  font-size: 0.4em;
  line-height: 1.625; /* its own line-height, not the heading's 1.2: the 1.5em Orbitron X sets the line box, 19.2px × 1.625 = 31.2px tall */
  font-weight: 600;
  letter-spacing: 0.02em;
  opacity: 0.82;
  margin-top: var(--space-2);   /* heading gap: 10→8 */
}
/* Clickable variant — links to vortex.html like the hero badge. Stays visually
   identical (a inherits color + no underline); cursor:pointer is the anchor default. */
a.title-powered { transition: opacity var(--transition); }
a.title-powered:hover { opacity: 1; }

.header.scrolled .logo .logo-text {
  color: var(--color-primary);
}

.nav {
  display: flex;
  margin-left: auto;   /* hugs the right, ahead of the EN | ES toggle */
  /* Align every tab to a shared TEXT BASELINE (the "invisible axis"). This makes
     "VorteX" (Orbitron + enlarged X) line up with Services/About/… automatically,
     regardless of its taller box — no per-item pixel offsets needed. */
  align-items: baseline;
  gap: var(--space-8);   /* 32 */
}
/* The house icon has no text baseline (it'd sit on the line), so keep it centred. */
.nav .nav-home { align-self: center; }

.nav a {
  font-size: 0.95rem;
  font-weight: 500;
  transition: color var(--transition);
}

/* Link colours skip the "Get a Quote" .btn, which keeps .btn's white label in every state */
.nav a:not(.btn) {
  color: rgba(255, 255, 255, 0.9);
}

.nav a:not(.btn):hover {
  color: var(--color-accent-light);
}

/* "VorteX" nav link aligns via the shared baseline above — no offset hack needed. */

.header.scrolled .nav a:not(.btn) {
  color: var(--color-text);
}

.header.scrolled .nav a:not(.btn):hover {
  color: var(--color-accent);
}

.nav-toggle {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 44px;           /* 44×44 tap target (was 32×24) */
  height: 44px;
  margin-right: -6px;    /* keeps the three lines exactly where the old 4px padding put them */
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  z-index: 9999;
  position: relative;
}

.nav-toggle span {
  display: block;
  width: 24px;
  height: 2px;
  background: var(--color-white);
  border-radius: 2px;
  transition: transform 0.3s ease, opacity 0.3s ease, background 0.3s ease;
  transform-origin: center;
}

.header.scrolled .nav-toggle span {
  background: var(--color-text);
}

/* Hamburger to X animation */
.nav-toggle.active span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav-toggle.active span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}

.nav-toggle.active span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* When menu is open, lines are always bright white with glow */
.nav-toggle.active span {
  background: #FFFFFF !important;
  filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.6));
}

/* Override scrolled state when menu is open */
.header.scrolled .nav-toggle.active span {
  background: #FFFFFF !important;
}

/* ===== Hero ===== */
.hero {
  min-height: 100vh;
  display: flex;
  align-items: center;
  background: linear-gradient(135deg, var(--color-dark) 0%, var(--color-primary) 50%, var(--color-accent) 100%);
  padding: calc(var(--header-height) + var(--space-7)) 0 var(--space-20);   /* = 100 / 80 (unchanged) */
  position: relative;
  overflow: hidden;
}

.hero-watermark {
  /* Tunable size — dial this to taste. Capped (not %/vw) so the edge-to-edge
     HD swirl can't balloon. References the app's .bg-swirl (500px). */
  --watermark-size: 700px;
  position: absolute;
  top: 50%;
  right: 4%;
  transform: translateY(-50%);
  width: var(--watermark-size);
  height: var(--watermark-size);
  object-fit: contain;
  opacity: 0.14;
  pointer-events: none;
  z-index: 0;
  filter: brightness(0) invert(1);
  /* #8: deepest parallax layer (rate 0.06 of scroll, downward) — --hero-par is a
     unitless scrollY written by the script.js rAF tick; 0 at rest / when JS is off. */
  transform: translateY(calc(-50% + var(--hero-par, 0) * 0.06px));
}

.hero::before {
  content: "";
  position: absolute;
  top: -50%;
  right: -20%;
  width: 600px;
  height: 600px;
  background: radial-gradient(circle, rgba(26, 143, 196, 0.15) 0%, transparent 70%);
  border-radius: 50%;
  transform: translateY(calc(var(--hero-par, 0) * 0.22px));   /* #8: fastest layer */
}

/* The hero is a flex row, which shrink-wrapped the container and centred it;
   full width puts the hero text on the same left edge as the logo. */
.hero > .container {
  width: 100%;
}

.hero-content {
  max-width: 632px;   /* = the old 680px box minus the container's 24px side padding: same line length as before */
  position: relative;
  z-index: 1;
  transform: translateY(calc(var(--hero-par, 0) * 0.12px));   /* #8: middle layer */
}

.hero h1 {
  font-size: clamp(2.2rem, 5vw, 3.75rem);
  font-weight: 800;
  color: var(--color-white);
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-5);   /* 20 */
  text-align: left;
}

.hero p {
  font-size: 1.15rem;
  color: rgba(255, 255, 255, 0.85);
  margin-bottom: var(--space-6);   /* 24: scope line -> VorteX pill */
  max-width: 540px;
  text-align: left;
}

.hero-actions {
  display: flex;
  gap: var(--space-4);   /* 16 */
  flex-wrap: wrap;
}

/* Film-grain overlay: kills gradient banding. Above the gradient, below text/swirl. */
.hero-noise {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}

/* Hero entrance: staggered fade/rise. Ends on the buttons (hr-4). Ease matches the
   .card/.stat reveals in script.js. Disabled under reduced-motion. */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}
.hero-reveal { opacity: 0; animation: heroRise 0.55s ease forwards; }
.hr-1 { animation-delay: 0s; }
.hr-2 { animation-delay: 0.1s; }
.hr-3 { animation-delay: 0.2s; }
.hr-4 { animation-delay: 0.3s; }

/* #11 word-cascade headline: each word of the h1 rises individually on a tight
   45ms cascade (replaces the old single-block hr-1 rise on the h1 itself; the
   badge/copy/buttons keep their hr-2..4 timing). */
.hero h1 .hw {
  display: inline-block;
  opacity: 0;
  animation: heroRise 0.5s var(--ease-reveal) forwards;
}
.hero h1 .hw:nth-child(2) { animation-delay: 0.045s; }
.hero h1 .hw:nth-child(3) { animation-delay: 0.09s; }
.hero h1 .hw:nth-child(4) { animation-delay: 0.135s; }
.hero h1 .hw:nth-child(5) { animation-delay: 0.18s; }
.hero h1 .hw:nth-child(6) { animation-delay: 0.225s; }
.hero h1 .hw:nth-child(7) { animation-delay: 0.27s; }    /* the Spanish headline runs to seven words */
.hero h1 .hw:nth-child(8) { animation-delay: 0.315s; }

@media (prefers-reduced-motion: reduce) {
  .hero-reveal { opacity: 1; animation: none; transform: none; }
}

/* Scroll-reveal choreography (script.js setupReveals): .reveal-init hides an observed
   item; adding .reveal-in transitions it up, with a per-item inline transition-delay
   supplying the batch cascade. Both classes are removed after settling so component
   hover transitions regain full control. Same travel (24px) as the old JS reveal,
   same ease family as heroRise / the VorteX intro. */
.reveal-init {
  opacity: 0;
  transform: translateY(24px);
}
.reveal-init.reveal-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.6s var(--ease-reveal), transform 0.6s var(--ease-reveal);
}

/* VorteX teaser badge — restrained pill, quiet curiosity hook. No glow/pulse;
   only a border/lift hover. (All app energy lives on the future VorteX page.) */
.vortex-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);             /* 9→8 */
  margin-bottom: var(--space-8);   /* 32: pill -> buttons */
  padding: 7px 16px 7px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.045);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.88);
  white-space: nowrap;
  transition: border-color var(--transition), background var(--transition), color var(--transition), transform var(--transition);
  --badge-vx-kern: -0.06em;   /* fine-tune the "by"->VorteX gap (a small tuck for Orbitron's wide "V") */
}
.vortex-badge .spark {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-cyan);
  flex: none;
  /* No nudge needed: the text below is centred on its cap height, which is where the dot
     belongs. Kept as a hook for fine-tuning. */
  --spark-nudge: 0px;
  transform: translateY(var(--spark-nudge));
}
/* Centre the label on its cap height, not on its font's line metrics. Without this, Orbitron's
   tall ascent and the 1.5em brand X each build their own taller line box, which stretched the
   pill to ~47px and sat every glyph ~6px below the middle. The wordmark's line-height: 0
   (see .vx-wordmark above) lets them paint at full size while the label's own font sets the line. */
.vortex-badge .badge-text { line-height: 1.25; }
.vortex-badge .vx { margin-left: var(--badge-vx-kern); }
/* Where supported, trim the label box to exactly cap-top..baseline, then pad it back to the
   same 1.25em. The cap band is then dead centre in the pill on every platform font (Segoe,
   SF, Roboto), in EN and ES. Browsers without text-box keep the line-height version above. */
@supports (text-box: trim-both cap alphabetic) {
  .vortex-badge .badge-text {
    text-box: trim-both cap alphabetic;
    padding-block: calc((1.25em - 1cap) / 2);
  }
}
/* (.vortex-badge .vx .x is the plain static cyan X — see the .logo-x rule; the travelling
   gradient X lives on the VorteX page headline only) */
@media (hover: hover) and (pointer: fine) {
  .vortex-badge:hover {
    border-color: rgba(0, 174, 239, 0.75);
    background: rgba(0, 174, 239, 0.12);
    color: var(--color-white);
    transform: translateY(-1px);
  }
}

/* ===== Sections ===== */
.section {
  padding: var(--section-y) 0;   /* 64 on phones → 96 on desktop */
  background: var(--color-white);   /* body is now dark for the iOS notch fix; keep light sections light */
}

.section-alt {
  background: var(--color-light);
}

.section-dark {
  background: linear-gradient(135deg, var(--color-dark) 0%, var(--color-primary-dark) 100%);
  color: var(--color-white);
}

.section-title {
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  margin-bottom: var(--space-2);   /* heading gap: 12→8 */
  color: var(--color-primary);
}

.section-dark .section-title {
  color: var(--color-white);
}

.section-title-left {
  text-align: left;
}

/* Cyan underline that draws in from the left once the title scrolls into view
   (script.js adds the persistent .revealed). Clean-site cousin of the VorteX
   pillar-label dash. Static footprint — only transform animates, so no layout shift. */
.section-title::after {
  content: "";
  display: block;
  width: 48px;
  height: 3px;
  margin: var(--space-2) auto 0;
  border-radius: 2px;
  background: var(--color-cyan);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.5s var(--ease-reveal) 0.15s;
}
.section-title.revealed::after {
  transform: scaleX(1);
}
.section-title-left::after {
  margin-left: 0;
}

.section-subtitle {
  text-align: center;
  color: var(--color-text-light);
  max-width: 560px;
  margin: 0 auto var(--space-14);   /* 56 */
}

.section-dark .section-subtitle {
  color: rgba(255, 255, 255, 0.7);
}

/* ===== Services Section ===== */
.services-section {
  position: relative;
  overflow: hidden;
}

.services-section .container {
  position: relative;
  z-index: 1;
}

/* ===== Services Grid ===== */
.services-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--space-7);   /* 28 */
}

.card {
  position: relative;   /* anchors the hover layers: ::before shadow + edge-light, ::after spotlight */
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-8);   /* feature card: 36/28 → 32 */
  transition: transform var(--transition);
}

@media (hover: hover) and (pointer: fine) {
  .card:hover,
  .stat:hover,
  .sketch:hover {
    transform: translateY(-4px);
  }
  /* .stat's and .sketch's resting shadows step off (a 0s animation, never interpolated) once the
     hover layer has fully faded in, so the settled hover shows that layer's shadow alone, as before */
  .stat:hover,
  .sketch:hover {
    animation: restShadowOff 0s linear 0.3s forwards;
  }
}

@keyframes restShadowOff {
  to { box-shadow: none; }
}

/* Hover layer: the larger shadow plus a thin cyan top edge-light (a quiet, single-colour echo
   of the VorteX card's tricolor edge-lit border), faded in together by opacity so a hover never
   animates box-shadow itself. The layer covers the tile's border box and paints only the 2px
   edge inside it; its shadow falls outside. Gated to real pointers below. */
.card::before,
.stat::before,
.sketch::before {
  content: "";
  position: absolute;
  inset: -1px;   /* .card's border box (1px border) */
  border-radius: inherit;
  box-shadow: var(--shadow-lg);
  background: linear-gradient(90deg, transparent 0%, var(--color-cyan) 30%, var(--color-cyan) 70%, transparent 100%) 1px 1px / calc(100% - 2px) 2px no-repeat;
  opacity: 0;
  transition: opacity var(--transition);
  pointer-events: none;
}
.stat::before {
  inset: 0;   /* .stat has no border */
  background-position: 0 0;
  background-size: 100% 2px;
}
/* The framed sketch already rests on --shadow-lg, where .card and .stat only arrive on hover, so
   its hover layer steps up a rung. Geometry is .card's: same 1px border, same inset. */
.sketch::before {
  box-shadow: var(--shadow-xl);
}

/* #7 cursor spotlight, edge-only: the cyan glow follows the pointer (--mx/--my, set per card
   in script.js) along the card's edge as a 1.5px ring. The mask cuts out everything inside
   the ring, so it never tints behind the text. */
.card::after,
.stat::after,
.sketch::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--transition);
  padding: 1.5px;
  background: radial-gradient(160px circle at var(--mx, 50%) var(--my, 50%), rgba(0, 174, 239, 0.9), transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

@media (hover: hover) and (pointer: fine) {
  .card:hover::before,
  .stat:hover::before,
  .sketch:hover::before {
    opacity: 1;
  }
  .card:hover::after,
  .stat:hover::after,
  .sketch:hover::after {
    opacity: 1;
  }
  .card:hover .card-icon svg {
    transform: scale(1.08);
    stroke: var(--color-accent-light);   /* icon "lights up" toward the cyan edge */
  }
}

.card-icon {
  margin-bottom: var(--space-4);   /* 16 */
  color: var(--color-accent);
}

.card-icon svg {
  width: 40px;
  height: 40px;
  stroke: var(--color-accent);
  stroke-width: 1.75;
  transition: transform var(--transition), stroke var(--transition);
}

.card h3 {
  font-size: 1.15rem;
  font-weight: 600;
  margin-bottom: var(--space-2);   /* heading gap: 10→8 */
  color: var(--color-primary);
}

.card p {
  color: var(--color-text-light);
  font-size: 0.95rem;
}

/* ===== About ===== */
.about-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-16);   /* 64 */
  align-items: center;
}

.about-text p {
  color: var(--color-text-light);
  margin-bottom: var(--space-6);   /* 24 */
}

/* VorteX links in the About copy — subtle accent (clean scheme, no neon) */
.about-text a {
  color: var(--color-primary);             /* 9.1:1 text; the cyan lives in the underline */
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--color-cyan);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
  transition: text-decoration-color var(--transition);
}
.about-text a:hover {
  text-decoration-color: var(--color-primary);
}

.about-list li {
  padding: var(--space-2) 0 var(--space-2) var(--space-7);   /* 8 / 28 */
  position: relative;
  font-weight: 500;
}

.about-list li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--color-accent);
  font-weight: 700;
}

.about-stats {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);   /* 32 */
}

.stat {
  position: relative;   /* anchors the hover layers, same as .card */
  background: var(--color-white);
  border-radius: var(--radius);
  padding: var(--space-7);   /* compact card: 28/32 → 28 */
  box-shadow: var(--shadow);
  text-align: center;
  transition: transform var(--transition);
}

.stat-number {
  display: inline-block;   /* so the finish-tick transform applies */
  font-size: 2.5rem;
  font-weight: 800;
  color: var(--color-accent);
}
/* While counting (script.js), fixed-width digits so the changing number doesn't jitter */
.stat-number.is-counting {
  font-variant-numeric: tabular-nums;
}

/* Counter finish tick (script.js adds .stat-done when the count lands): the number
   does a small scale pop and the +/% suffix brightens from dimmed to full. */
@keyframes statPop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.08); }
  100% { transform: scale(1); }
}
.stat-number.stat-done {
  animation: statPop 0.35s var(--ease-reveal);
}
.stat-suffix {
  font-size: 1.5rem;   /* 0.6 × the 2.5rem number */
  opacity: 0.35;
  transition: opacity 0.4s ease;
}
.stat-number.stat-done + .stat-suffix {
  opacity: 1;
}

.stat-label {
  display: block;
  margin-top: var(--space-1);   /* 4 */
  font-size: 0.9rem;
  color: var(--color-text-light);
}

/* ===== Process ===== */
.process-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-8);   /* 32 */
}

.step {
  text-align: center;
  padding: var(--space-6) var(--space-4);   /* 24 / 16 */
  transition: transform var(--transition);
}

.step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: transparent;                 /* ring, not fill — cyan fill is reserved for the primary action */
  border: 2px solid var(--color-cyan);
  color: var(--color-primary);             /* navy numeral for contrast; the cyan stays in the ring */
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: var(--space-4);   /* 18→16 */
  position: relative;   /* anchors the hover glow layer */
}

/* Hover glow on its own layer over the ring's 2px border box, faded in by opacity */
.step-number::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  box-shadow: 0 0 0 4px rgba(0, 174, 239, 0.14), 0 0 18px rgba(0, 174, 239, 0.55);   /* ring glows cyan; the numeral stays navy */
  opacity: 0;
  transition: opacity var(--transition);
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .step:hover {
    transform: translateY(-4px);
  }
  .step:hover .step-number::before {
    opacity: 1;
  }
}

.step h3 {
  font-size: 1.1rem;
  font-weight: 600;
  margin-bottom: var(--space-2);   /* heading gap: 10→8 */
  color: var(--color-primary);
}

.step p {
  font-size: 0.95rem;
  color: var(--color-text-light);
}

/* ===== Contact Form ===== */
.contact-form {
  max-width: 680px;
  margin: 0 auto;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);   /* 20 */
}

.form-group {
  margin-bottom: var(--space-5);   /* 20 */
}

.form-group label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-bottom: var(--space-2);   /* 6→8 */
  color: rgba(255, 255, 255, 0.9);
}

.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: var(--space-3) var(--space-4);   /* 12 / 16 */
  font-size: 1rem;
  font-family: inherit;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.08);
  color: var(--color-white);
  transition: border-color var(--transition), background var(--transition);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
  color: rgba(255, 255, 255, 0.4);
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--color-accent);
  background: rgba(255, 255, 255, 0.12);
}

.form-group select option {
  background: var(--color-dark);
  color: var(--color-white);
}

.contact-form .btn {
  width: 100%;
  margin-top: var(--space-2);   /* 8 */
}

.form-success {
  text-align: center;
  padding: var(--space-10);   /* 40 */
  font-size: 1.1rem;
  color: rgba(255, 255, 255, 0.9);
}

.hidden {
  display: none;
}

/* ===== Footer ===== */
.footer {
  background: var(--color-dark);
  color: rgba(255, 255, 255, 0.7);
  padding-top: var(--space-16);   /* 64 */
}

.footer-inner {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: var(--space-12);            /* 48 */
  padding-bottom: var(--space-12); /* 48 */
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.footer-brand .logo {
  margin-bottom: var(--space-3);   /* 12 */
  display: inline-flex;
}

.footer-brand .logo .logo-text {
  color: var(--color-white);
}

.footer-brand .logo-icon {
  height: 50px;
  width: 50px;
  border-radius: 50%;
  object-fit: contain;
  margin-right: -4px;
  /* Lower the swirl to sit centred on the "HydroWrXs" wordmark line. The enlarged X
     inflates the wordmark's line-box upward, so flex-centring lands the icon too high;
     this transform-only nudge re-centres it without affecting layout. Tune if needed. */
  transform: translateY(0.28rem);
}

.footer-brand p {
  font-size: 0.9rem;
}

.footer-links h4,
.footer-contact h4 {
  color: var(--color-white);
  font-size: 0.95rem;
  margin-bottom: var(--space-4);   /* 16 */
}

.footer-links a {
  display: block;
  font-size: 0.9rem;
  padding: var(--space-1) 0;   /* 4 */
  transition: color var(--transition);
}

.footer-links a:hover {
  color: var(--color-accent-light);
}

.footer-contact p {
  font-size: 0.9rem;
  padding: var(--space-1) 0;   /* 4 */
}

.footer-social {
  display: inline-flex;
  align-items: center;
  min-height: 44px;            /* tap target (was 23px tall) */
  gap: var(--space-2);         /* 10→8 */
  margin-top: var(--space-2);  /* 8 */
  color: rgba(255, 255, 255, 0.7);
  font-size: 0.9rem;
  font-weight: inherit;
  text-decoration: none;
  transition: color var(--transition);
}

.footer-social:hover {
  color: var(--color-accent-light);
}

.footer-social svg {
  width: 18px;
  height: 18px;
  flex: none;
}

.nav-home {
  display: inline-flex;
  align-items: center;
}

.nav-home svg {
  width: 20px;
  height: 20px;
  stroke: currentColor;
}

/* ===== Page title band (Projects, About) ===== */
.projects-hero,
.about-hero {
  /* Fixed band height, shared with the header's top-state gradient so they line up with no
     seam. min-height (not height) so a longer wrap on a very narrow phone grows the band
     instead of spilling the subtitle out of it. */
  min-height: var(--projects-band-h);
  padding-top: calc(var(--header-height) + var(--space-16));   /* 140 → 136 (derived from header) */
  padding-bottom: var(--space-10);                             /* 40 */
  background: linear-gradient(135deg, var(--color-dark) 0%, var(--color-primary) 50%, var(--color-accent) 100%);
}

.projects-hero .section-title,
.about-hero .section-title {
  color: var(--color-white);
}

.projects-hero .section-subtitle,
.about-hero .section-subtitle {
  color: rgba(255, 255, 255, 0.7);
  margin-bottom: 0;   /* the band's own 40px bottom padding is the gap */
}

/* Content sits close under the band (was 96px of empty grey on top of the band's 40) */
.projects-hero + .section,
.about-hero + .section {
  padding-top: var(--space-12);   /* 48 */
}

/* ===== About page ===== */
.about-story {
  display: grid;
  grid-template-columns: minmax(0, 380px) 1fr;
  gap: var(--space-16);   /* 64, same as the Home about grid */
  align-items: start;
}

/* Treatment B: the sketch as drawn, framed like a photo laid on the page */
.sketch {
  position: relative;   /* anchors the hover layers: ::before shadow + edge-light, ::after spotlight */
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-3);
  box-shadow: var(--shadow-lg);
  transition: transform var(--transition);
}

.sketch img {
  width: 100%;
  height: auto;   /* the file's height attribute is a presentational hint; without this it wins over aspect-ratio */
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: center 58%;   /* the drawn area of a tall sheet, without cropping the file */
  border-radius: 4px;
}

.founder {
  margin-bottom: var(--space-6);   /* 24 */
}

/* "Our story" sits under the two bios, so it needs a gap above it that a section's first
   heading does not */
.story-title {
  margin-top: var(--space-10);   /* 40 */
}

.founder h3 {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--color-primary);
}

.founder-role {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-accent);
  margin-bottom: var(--space-2) !important;   /* tighter than .about-text p's 24 */
}

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);   /* 20 */
  /* no content-visibility here: its 400px placeholder let the footer paint inside the first
     screen and then jump (CLS 0.08 at 1440); the images carry width/height instead */
}

.gallery-item {
  position: relative;   /* anchors the hover shadow layer */
  border-radius: var(--radius);
  cursor: pointer;
  box-shadow: var(--shadow);
  aspect-ratio: 1 / 1;
  background: #f0f0f0;
  transition: transform var(--transition);
}

/* The photo's zoom is clipped by this inner frame (formerly the tile's own overflow:hidden),
   so the hover shadow layer can sit outside it */
.gallery-frame {
  display: block;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
}

/* Hover shadow on its own layer, faded in by opacity; the resting shadow steps off once the
   fade has finished (same 0s-animation handoff as .stat) */
.gallery-item::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transition: opacity var(--transition);
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
  .gallery-item:hover {
    transform: translateY(-4px);
    animation: restShadowOff 0s linear 0.3s forwards;
  }
  .gallery-item:hover::before {
    opacity: 1;
  }
}

.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s ease, filter var(--transition);
}

@media (hover: hover) and (pointer: fine) {
  .gallery-item:hover img {
    transform: scale(1.05);
    filter: brightness(1.08);
  }
}

/* Home "Recent Work": the same gallery tiles, as links to the Projects page */
a.gallery-item {
  display: block;
}

.section-cta {
  text-align: center;
  margin-top: var(--space-10);
}

/* End-of-page bid band (Projects, VorteX) */
.cta-band {
  text-align: center;
}

.cta-band .btn {
  margin-top: var(--space-6);
}

/* ===== Lightbox ===== */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s ease, visibility 0.35s ease;
  touch-action: pinch-zoom;   /* a horizontal swipe steps photos (projects.html) instead of panning */
}

.lightbox.active {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s ease, visibility 0s;   /* visible at once on open, so focus can move into it */
}

.lightbox-img {
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
  border-radius: var(--radius);   /* stray 6px → 8px token */
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
  transform: scale(0.92);
  transition: transform 0.35s ease;
}

.lightbox.active .lightbox-img {
  transform: scale(1);
}

.lightbox-close {
  position: absolute;
  top: 20px;
  right: 28px;
  background: none;
  border: none;
  color: #ffffff;
  font-size: 2.5rem;
  font-weight: 300;
  cursor: pointer;
  line-height: 1;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition: background var(--transition);
}

.lightbox-close:hover {
  background: rgba(255, 255, 255, 0.1);
}

/* Previous / next photo (Projects); swipe and the arrow keys do the same (projects.html) */
.lightbox-nav {
  position: absolute;
  top: 50%;
  width: 48px;
  height: 48px;
  margin-top: -24px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.4);
  color: #ffffff;
  cursor: pointer;
  transition: background var(--transition);
}

.lightbox-prev {
  left: 16px;
}

.lightbox-next {
  right: 16px;
}

.lightbox-nav svg {
  width: 28px;
  height: 28px;
}

@media (hover: hover) and (pointer: fine) {
  .lightbox-nav:hover {
    background: rgba(255, 255, 255, 0.14);
  }
}

.footer-bottom {
  padding: var(--space-5) 0;   /* 20 */
  text-align: center;
  font-size: 0.85rem;
}

/* ===== Focus rings (keyboard a11y) =====
   Cyan ring shown only on :focus-visible (keyboard nav), not on mouse click, and
   without disturbing :hover. Placed AFTER the form `outline:none` so it wins for
   keyboard focus on inputs. */
.btn:focus-visible,
.nav a:focus-visible,
.card:focus-visible,
.gallery-item:focus-visible,
.lightbox-close:focus-visible,
.lightbox-nav:focus-visible,
.about-text a:focus-visible,
.footer-links a:focus-visible,
.footer-social:focus-visible,
.vortex-badge:focus-visible,
.title-powered:focus-visible,
.lang-toggle:focus-visible,
.form-group input:focus-visible,
.form-group select:focus-visible,
.form-group textarea:focus-visible {
  outline: 2px solid var(--color-cyan);
  outline-offset: 2px;
}

/* ===== Responsive ===== */
@media (max-width: 768px) {
  :root {
    --projects-band-h: 310px;   /* three-line subtitle on phones */
  }

  /* iOS safe area (viewport-fit=cover): push header content below the status bar.
     At rest the header is transparent, so the dark hero shows through the safe area
     (no white gap); the scroll-to-white state is unaffected. env() is 0 elsewhere. */
  .header {
    padding-top: env(safe-area-inset-top);
  }

  /* The header's padding-top above makes its fixed box CLAIM the safe-area inset, but the
     bar is transparent at rest, so that claimed band painted nothing and iOS rendered it
     WHITE (the dark html root doesn't show through a fixed layer's claimed inset). Paint
     the strip ourselves: a header ::before that fills ONLY the env() band, behind the bar
     content (z-index:-1). Dark at rest (matches hero/theme-color), white when the header
     is .scrolled — so the inset always matches the header. Scoped to home/projects; vortex
     has its own body[data-page="vortex"]::before filler and is left untouched. */
  body[data-page="home"] #header::before,
  body[data-page="projects"] #header::before,
  body[data-page="about"] #header::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: env(safe-area-inset-top);
    background: var(--color-dark);   /* #0c1a24 */
    z-index: -1;
    pointer-events: none;
    /* NO transition: this strip is the topmost layer painted in the safe-area inset, so it
       must swap navy<->white INSTANTLY. A fade lets iOS sample a half-faded grey for the
       status-bar tint after a momentum fling settles. (theme-color is swapped instantly in JS.) */
  }
  body[data-page="home"] #header.scrolled::before,
  body[data-page="projects"] #header.scrolled::before,
  body[data-page="about"] #header.scrolled::before {
    background: var(--color-white);
  }

  /* Balance the wordmark against the hamburger on narrow screens — smaller logo so it
     doesn't dwarf the 24px toggle or crowd the 72px bar. The Orbitron X is em-based,
     so it scales down with this and stays aligned. Tunable. */
  .logo-text {
    --logo-size-mobile: 1.5rem;   /* desktop is 1.85rem */
    font-size: var(--logo-size-mobile);
  }

  .nav-toggle {
    display: flex;
    order: 3;
  }

  .header-quote {
    display: inline-flex;
    align-items: center;
    min-height: 44px;              /* tap target */
    margin-right: var(--space-3);
    order: 2;
  }

  /* EN | ES sits just left of Quote and takes the auto margin that used to push Quote right */
  .lang-toggle {
    order: 1;
    margin-left: auto;
    margin-right: var(--space-1);
  }

  .nav {
    position: fixed;
    top: 0;
    right: 0;
    width: 280px;
    height: 100vh;
    background: var(--color-dark);
    flex-direction: column;
    justify-content: center;
    align-items: center;   /* column menu: keep items centred (baseline is for the desktop row) */
    gap: var(--space-2);   /* 8 — with the 44px rows below this keeps the old 52px item pitch */
    padding: 24px;
    /* Slide via a compositor-only transform (was animating `right`, a layout property
       that re-flowed every frame and made the open feel laggy). Same motion, cheap. */
    transform: translateX(100%);
    /* Closed = visibility:hidden, so the off-canvas links leave the Tab order. The
       flip to hidden waits until the 0.3s slide-out has finished. */
    visibility: hidden;
    transition: transform var(--transition), visibility 0s linear 0.3s;
    will-change: transform;   /* promote just this panel; not site-wide */
  }

  .nav.active {
    transform: translateX(0);
    visibility: visible;
    transition: transform var(--transition), visibility 0s;
  }

  .nav a:not(.btn) {
    color: rgba(255, 255, 255, 0.9) !important;
  }

  .nav a {
    font-size: 1.1rem;
    /* 44px tap rows (links were 28px tall, the house icon 20×20) */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
  }

  /* Menu scrim: dims the page BELOW the header bar. It starts under the bar, so the
     status-bar area and the header itself are untouched. Tapping it closes the menu
     through the existing outside-click handler in script.js. */
  body::after {
    content: "";
    position: fixed;
    top: calc(var(--header-height) + env(safe-area-inset-top));
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 998;   /* below the header (1000), which holds the menu panel */
    background: rgba(12, 26, 36, 0.6);   /* --color-dark @ 60% */
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--transition), visibility 0s linear 0.3s;
  }
  body.menu-open::after {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity var(--transition), visibility 0s;
  }
  /* Scroll lock while the menu is open. Body overflow propagates to the viewport; the
     scroll position itself is untouched, so the header/theme-color loop sees no change. */
  body.menu-open {
    overflow: hidden;
  }

  .hero h1 {
    font-size: 2rem;
    text-wrap: balance;   /* even out wrap; avoid orphan words */
  }

  .hero-watermark {
    --watermark-size: 480px;     /* mobile ≤768 — bolder presence; clipped by .hero overflow:hidden */
    --watermark-right: -12%;     /* shift RIGHT/off-edge so text sits on the clean left. More negative = further right. */
    right: var(--watermark-right);
  }

  .about-grid {
    grid-template-columns: 1fr;
    gap: 40px;
  }

  .about-story {
    grid-template-columns: 1fr;
    gap: var(--space-10);   /* 40 */
  }

  .form-row {
    grid-template-columns: 1fr;
  }

  .footer-inner {
    grid-template-columns: 1fr;
    gap: 32px;
  }

  .footer-links a {
    padding: 10px 0;
    font-size: 1rem;
  }

  .services-grid {
    grid-template-columns: 1fr;
  }

  .gallery-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
  }
}

@media (max-width: 480px) {
  /* Gallery grids (Projects, Home "Recent Work") keep the two ≤768 columns on phones */

  /* Stacked hero buttons at one shared full width (were 224px and 159px) */
  .hero-actions .btn {
    width: 100%;
    text-align: center;
  }

  .hero h1 {
    font-size: 1.75rem;   /* smaller on narrow phones so it doesn't orphan "Right" (balance inherited from ≤768) */
  }

  .logo-text {
    --logo-size-mobile: 1.35rem;   /* step the wordmark down once more for the smallest phones */
  }

  .hero-watermark {
    --watermark-size: 360px;     /* mobile ≤480 — bolder presence; clipped by .hero overflow:hidden */
    --watermark-right: -25%;     /* swirl is ~full-width here, so push it further right to free the left */
    right: var(--watermark-right);
  }
}

@media (max-width: 380px) {
  /* Smallest phones: a slimmer Quote button so EN | ES still fits beside it */
  .header-quote {
    padding: var(--space-2) var(--space-4);
    margin-right: var(--space-2);
  }
}

@media (min-width: 769px) and (max-width: 959px) {
  /* Tablet widths: a tighter nav so the links, the quote button and EN | ES share one row in
     both languages (at these widths the English "Get a Quote" used to wrap onto two lines) */
  .nav {
    gap: 12px;
  }

  .nav .btn-sm {
    padding: var(--space-2) var(--space-4);
  }

  .lang-toggle {
    margin-left: 10px;
  }
}

/* ===== Touch: in-view highlight and press =====
   Touch screens get no hover, so tiles take the hover treatment as they pass the middle of the
   screen (script.js adds .in-view), and a tap presses them with a brief cyan edge flash that
   releases as soon as the finger lifts (script.js adds the touchstart listener iOS needs for
   :active). Everything here is transform and opacity, on the same layers and with the same
   timing the hover uses; :not(.reveal-init) keeps the highlight out of the scroll reveal. */
@media (hover: none) {
  .card.in-view:not(.reveal-init),
  .stat.in-view:not(.reveal-init),
  .step.in-view:not(.reveal-init),
  .sketch.in-view:not(.reveal-init),
  .gallery-item.in-view:not(.reveal-init) {
    transform: translateY(-4px);
  }

  /* the hover layers: shadow + cyan top edge, the step's ring glow, the photo's veil */
  .card.in-view::before,
  .stat.in-view::before,
  .sketch.in-view::before,
  .gallery-item.in-view::before,
  .step.in-view .step-number::before {
    opacity: 1;
  }

  /* the photo lightens by easing its own opacity over the tile's light background — no
     extra layer over the image, and nothing but opacity animates */
  .gallery-item img {
    transition: opacity var(--transition);
  }

  .gallery-item.in-view img {
    opacity: 0.94;
  }

  /* the photo tile's cyan edge rides the shadow layer it already has (touch only, so the
     desktop hover shadow keeps its plain look). The ring shows on press: the lit photo fades
     its own opacity, which makes it a stacking context above this layer, so the layer is raised
     only for :active — a single tile for a moment, never while tiles scroll past. In the lit
     state the photo tile shows what hover gives it: the lift, the shadow and the brightening. */
  .gallery-item::before {
    box-shadow: var(--shadow-lg), inset 0 0 0 2px rgba(0, 174, 239, 0.55);
  }

  .gallery-item:active::before {
    z-index: 1;
  }

  /* Note: unlike the hover path, the lit tile keeps its resting shadow under the layer rather
     than stepping it off with a 0s box-shadow animation — the highlight stays transform and
     opacity only, and nothing starts an animation as tiles scroll past. */

  .card:active,
  .stat:active,
  .step:active,
  .sketch:active,
  .gallery-item:active {
    transform: scale(0.965);
    transition-duration: 0.1s;
  }

  .card.in-view:not(.reveal-init):active,
  .stat.in-view:not(.reveal-init):active,
  .step.in-view:not(.reveal-init):active,
  .sketch.in-view:not(.reveal-init):active,
  .gallery-item.in-view:not(.reveal-init):active {
    transform: translateY(-4px) scale(0.965);
  }

  /* cyan edge flash on press */
  .card:active::before,
  .stat:active::before,
  .sketch:active::before,
  .gallery-item:active::before,
  .step:active .step-number::before {
    opacity: 1;
    transition-duration: 0.1s;
  }
}

/* ===== Reduced motion — Tier-1 + Tier-2 animation batches =====
   script.js skips the reveal system, parallax, and spotlight under reduced motion;
   these rules cover the pure-CSS pieces. .reveal-init override is belt-and-braces
   for a mid-session preference flip. */
@media (prefers-reduced-motion: reduce) {
  .reveal-init {
    opacity: 1;
    transform: none;
  }
  .section-title::after {
    transform: scaleX(1);
    transition: none;
  }
  .stat-number.stat-done {
    animation: none;
  }
  .stat-suffix {
    opacity: 1;
    transition: none;
  }
  /* #3 sheen off */
  .btn:hover::after {
    animation: none;
  }
  /* #7 spotlight overlays off */
  .card::after,
  .stat::after,
  .sketch::after {
    display: none;
  }
  /* Tiles: hover, the touch in-view highlight and press keep their shadow/glow cues but
     never move or scale */
  .card:hover,
  .stat:hover,
  .step:hover,
  .sketch:hover,
  .gallery-item:hover,
  .gallery-item:hover img,
  .card:hover .card-icon svg,
  .card:active,
  .stat:active,
  .step:active,
  .sketch:active,
  .gallery-item:active,
  .card.in-view,
  .stat.in-view,
  .step.in-view,
  .sketch.in-view,
  .gallery-item.in-view {
    transform: none;
  }
  /* the touch-only layers stay dark */
  .card.in-view::before,
  .stat.in-view::before,
  .sketch.in-view::before,
  .gallery-item.in-view::before,
  .step.in-view .step-number::before,
  .card:active::before,
  .stat:active::before,
  .sketch:active::before,
  .gallery-item:active::before,
  .step:active .step-number::before {
    opacity: 0;
  }
  .gallery-item.in-view img {
    opacity: 1;
  }
  /* #10 lightbox: no scale/fade choreography, instant open */
  .lightbox,
  .lightbox.active,
  .lightbox-img {
    transition: none;
  }
  /* #11 words visible immediately */
  .hero h1 .hw {
    opacity: 1;
    animation: none;
    transform: none;
  }
}
