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

html {
  height: 100%;
}

html, body {
  /* Guards against occasional edge-to-edge elements (e.g. the "COPIED!"
     badge in .copy-link::after) pushing a horizontal scrollbar on narrow
     phones. `clip` (with a `hidden` fallback for older browsers) rather
     than `auto`/`scroll` — it doesn't create a new scroll container, so
     position:fixed and the GSAP scroll-driven effects are unaffected. */
  overflow-x: hidden;
  overflow-x: clip;
  /* Phase 0: page is no longer scroll-locked — the hero morph div (added
     in a later phase) visually covers this base color while scrolling. */
  background: var(--surface-1);
  /* App-like feel across the whole site (matches designpro.html's tool),
     not just the .copy-link contact spans further down. Doesn't block
     typing/selection inside actual <input>/<textarea> fields — those
     manage their own selection independently of ancestor user-select. */
  -webkit-user-select: none;
  user-select: none;
}

/* Sticky footer: body becomes a flex column so a short page's <main> can
   grow and push .site-footer to the bottom of the viewport instead of
   leaving a dead gap above it. min-height (not height) means a page whose
   content is taller than one viewport just extends past it as normal —
   an explicit height on a flex container instead force-shrinks children
   to fit, which would squeeze the footer on any long page. */
body {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

/* The one content element every page places directly before .site-footer
   (index.html, portfolio.html, solutions.html, freebies.html all use
   <main> this way) — flex-grow absorbs the leftover space on short pages;
   flex-shrink:0 keeps it at its natural content size otherwise. */
body > main {
  flex: 1 0 auto;
}

/* Scroll-locked only while the hero 3D asset is still loading (toggled by
   js/logo3d.js) — keeps the page from scrolling behind #loading-overlay. */
body.is-loading {
  overflow: hidden;
}

/* Scroll-locked while the mobile nav panel is open (toggled by js/main.js) —
   keeps the page from scrolling behind the full-height slide-in menu.
   position:fixed (with `top` set inline by js/main.js to the negative of
   the scroll position at open time) rather than a bare `overflow:hidden`
   — on mobile browsers, releasing a bare overflow lock under a full-
   viewport fixed overlay resets scrollY to 0, so an anchor-link tap would
   always jump to the top of the page first instead of scrolling directly
   from wherever the visitor was. Fixed positioning holds the visual
   scroll position in place until js/main.js explicitly restores it. */
body.nav-open {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}

html {
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* legacy Edge/IE */
}

/* Smooth in-page anchor scrolling (nav links to #section-*, and a fallback
   for anything besides the Home guard's own explicit JS scrollTo in
   js/main.js). Gated on prefers-reduced-motion, same care already taken
   for the GSAP-driven animations throughout js/sections.js. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

html::-webkit-scrollbar {
  display: none; /* Chrome/Safari/Edge Chromium */
}

#logo-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  animation: canvas-in 1.2s ease forwards;
  animation-delay: 0.2s;
}

@keyframes canvas-in {
  to { opacity: 1; }
}

#loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: var(--surface-1);
  transition: opacity 0.5s ease;
}

#loading-overlay.loading-done {
  opacity: 0;
  pointer-events: none;
}

#progress-track {
  position: relative;
  width: 240px;
  height: 3px;
  background: var(--border-strong);
  border-radius: 0;
  overflow: hidden;
}

#progress-fill {
  height: 100%;
  width: 0%;
  background: var(--brand-accent-bright);
  box-shadow: 0 0 8px rgba(253,185,19,0.7), 0 0 16px rgba(253,185,19,0.35);
  transition: width 0.2s ease;
}

#progress-track.indeterminate #progress-fill {
  position: absolute;
  width: 40% !important;
  animation: progress-indeterminate 1.2s ease-in-out infinite;
}

@keyframes progress-indeterminate {
  0%   { left: -40%; }
  100% { left: 100%; }
}

#progress-label {
  font: 12px/1 var(--font-ui);
  color: var(--text-secondary);
  letter-spacing: 0.02em;
}

/* ================================================================
   DESIGN SYSTEM (DSK Media Group 2026, v2.0)

   Everything above this line is the hero (#logo-canvas,
   #loading-overlay + children). Everything below is the shared
   color/type/spacing/shadow token system plus the header/nav/section/
   card/button classes used across all pages. Mobile-first, no build
   tools, vanilla CSS custom properties. Montserrat is loaded via
   Google Fonts <link> tags in each page's <head>, not here.
================================================================ */

:root {
  /* --- brand & surface colors --- */
  --brand-accent:             #FDB913;
  --brand-accent-hover:       #E5A600;
  --brand-accent-bright:      #FFC700;
  --brand-accent-soft-bg:     rgba(253,185,19,0.08);
  --brand-accent-soft-border: rgba(253,185,19,0.5);
  --brand-accent-fade:        #EEE0BF;
  --brand-accent-fade-dark:   #E9DCBB;

  /* Full dark theme (whole site, not just the hero) — surfaces/text
     inverted from the reference's original light palette to dark. Kept
     as a single token set (rather than parallel light/dark variants)
     since every public page is dark now. */
  --surface-0: #0D0D0D;
  --surface-1: #1A1A1A;
  --surface-2: #161616;
  --surface-3: #1F1F1F;
  --surface-inset: rgba(255,255,255,0.04);

  --text-primary:   #FFFFFF;
  --text-secondary: #B3B3B3;
  --text-tertiary:  #808080;
  --text-disabled:  #4D4D4D;
  --text-on-accent: #0D0D0D;

  --border-subtle:  rgba(255,255,255,0.06);
  --border-default: rgba(255,255,255,0.12);
  --border-strong:  rgba(255,255,255,0.2);

  --signal-danger:        #E53935;
  --signal-danger-hover:  #C62828;
  --signal-success:       #25D366;
  --signal-success-hover: #1DA851;

  /* --- radii --- */
  --radius-xs:   0;
  --radius-sm:   0;
  --radius-md:  0;
  --radius-lg:  0;
  --radius-xl:  0;
  --radius-pill: 0;

  /* --- spacing scale --- */
  --space-1:   4px;  --space-2:  8px;  --space-3: 12px;
  --space-4:  16px;  --space-5: 20px;  --space-6: 24px;
  --space-8:  32px;  --space-10: 40px; --space-12: 48px;
  --space-16: 64px;  --space-20: 80px;

  /* --- layout --- */
  --header-h-mobile:  44px;
  --header-h-desktop: 48px;
  --header-height:    var(--header-h-mobile);
  --section-x: clamp(20px, 6vw, 12vw);
  --section-y: clamp(90px, 12vw, 200px);

  /* --- type --- */
  --font-ui:   'Montserrat', system-ui, sans-serif;

  /* Consolidated type scale — 4 named roles used consistently everywhere:
     display (primary topic/heading), display-sm (secondary topic, e.g.
     ticker item names), label (subtopic/caption/UI text), micro
     (disclaimer/tooltip text only), plus body (paragraph reading text). */
  --type-display:    clamp(2.25rem, 3.5vw + 1rem, 6rem);
  --type-display-sm: clamp(1.5rem, 2.5vw + 0.5rem, 2.75rem);
  --type-body:        clamp(1rem, 0.15vw + 0.95rem, 1.15rem);
  --type-label:       0.78rem;
  /* Tracks --type-label rather than its own fixed value, so it picks up
     the same mobile fluid bump (below) instead of staying tiny while
     the regular label text grows. */
  --type-label-sm:    var(--type-label);
  --type-micro:       0.65rem;

  --tracking-tight:  -0.02em;
  --tracking-normal: 0;
  --tracking-wide:    0.05em;
  --tracking-wider:   0.1em;

  --leading-tight:   1.0;
  --leading-snug:    1.2;
  --leading-normal:  1.5;
  --leading-relaxed: 1.7;

  /* --- shadows --- */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.04), 0 2px 8px rgba(0,0,0,0.06);
  --shadow-md: 0 2px 4px rgba(0,0,0,0.06), 0 8px 24px rgba(0,0,0,0.08);
  --shadow-lg: 0 4px 8px rgba(0,0,0,0.08), 0 24px 60px rgba(0,0,0,0.16);

  --shadow-sm-dark: 0 1px 2px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
  --shadow-md-dark: 0 2px 4px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5);

  /* --- motion --- */
  --motion-fast: 150ms;
  --motion-base: 250ms;
  --motion-slow: 400ms;
  --motion-deliberate: 1000ms;

  --ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  --focus-ring:       0 0 0 3px rgba(253,185,19,0.25);
  --focus-ring-tight: 0 0 0 2px rgba(253,185,19,0.30);

  --transition: all var(--motion-base) var(--ease-out);

  /* Hero morph handoff (Phase 1 territory): the fixed #hero-transition-bg
     div below reads this custom property for its background so a later
     phase's rAF scroll listener can drive color-mix(#1a1a1a -> #FFFDF5)
     through it without touching this file again. Left at the hero's
     dark base color so it's a visual no-op until that phase wires it up. */
  --hero-morph-bg: var(--surface-1);
}

@media (min-width: 768px) {
  :root { --header-height: var(--header-h-desktop); }
}

/* Below 768px, --type-display's own vw-based formula (still the same
   slope used up through the 1920px desktop design) bottoms out at its
   2.25rem floor for most phone widths, which reads too small once the
   Experience section's per-stat --stat-scale multiplier (as low as 0.35)
   is applied on top. Raising just the floor — not the formula — keeps
   the same proportional curve as desktop while giving mobile a bigger
   baseline. --type-label (the "Up to"/caption text) is flat at all
   sizes today; give it a small mobile-only fluid bump too. --type-micro
   (back-to-home pill, disclaimers, tooltips) gets the same treatment —
   otherwise it stays the smallest, fixed-size text on the page while
   everything else above it scales up, breaking the consistent mobile
   type scale. */
@media (max-width: 767px) {
  :root {
    --type-display: clamp(2.75rem, 3.5vw + 1rem, 6rem);
    --type-label:   clamp(0.88rem, 1.6vw + 0.5rem, 1.05rem);
    --type-micro:   0.78rem;
  }
}

/* --- base link/typography setup for the new shell (scoped, not a
   blanket `*` reset, so it can't fight the existing hero/reset rules
   above) --- */
.site-header a,
.site-nav a,
.mobile-nav a,
.site-footer a {
  text-decoration: none;
}

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

/* ---- Typography ---- */
.section-eyebrow {
  display: inline-block;
  font-family: var(--font-ui);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  /* 80% opacity look via color-mix, not `opacity` — most .section-eyebrow
     elements carry [data-reveal], whose GSAP fade-in animates `opacity`
     itself and leaves it pinned at inline 1 afterward, which would cancel
     out a plain `opacity: 0.8` here (and can't use !important either,
     since that would also cancel the fade-in transition itself). */
  color: color-mix(in srgb, var(--brand-accent) 80%, black 20%);
  margin-bottom: var(--space-3);
}

.section-title {
  font-family: var(--font-ui);
  text-transform: uppercase;
  font-weight: 300;
  font-variant-caps: titling-caps;
  line-height: var(--leading-snug);
  letter-spacing: 0.15em;
  color: var(--brand-accent);
  font-size: calc(var(--type-display) * 0.9);
  margin-bottom: var(--space-3);
}

/* Same font/caps/color as .section-title, applied together as a modifier
   (e.g. class="section-title section-title--sm") — a second, smaller
   heading size at 90% of the default, for whenever a lighter-weight yellow
   heading is needed. Not used by any element yet. */
.section-title--sm {
  font-size: calc(var(--type-display) * 0.9 * 0.9);
}

.section-subtitle,
.lead-text {
  font-family: var(--font-ui);
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  max-width: 60ch;
  margin-left: auto;
  margin-right: auto;
}

.text-muted { color: var(--text-tertiary); }

/* ---- Buttons ----
   The site's one consistent CTA style everywhere — a solid filled pill
   button (uppercase label type, brand-accent fill). Used for the Services
   and Portfolio section CTAs. A separate boxed .btn/.btn-primary/.btn-outline
   set (originally for Services' CTA before it switched to .text-btn) is
   unused now and was removed. */

/* Solid uppercase "pill" CTA button (e.g. "SEE THE RESULTS ->") */
.text-btn {
  font-family: var(--font-ui);
  font-size: var(--type-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-on-accent);
  background: var(--brand-accent);
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-6);
  cursor: pointer;
  display: inline-block;
  box-shadow: var(--shadow-sm-dark);
  transition: background-color var(--motion-fast) var(--ease-out),
              transform var(--motion-fast) var(--ease-out),
              box-shadow var(--motion-fast) var(--ease-out);
}

.text-btn:hover {
  background: var(--brand-accent-hover);
  /* !important: both .text-btn CTAs carry [data-reveal], whose GSAP scroll
     animation (sections.js) leaves an inline transform on the element after
     it plays, which otherwise outranks this class rule and cancels the lift. */
  transform: translateY(-2px) !important;
  box-shadow: var(--shadow-md-dark);
}

.text-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* No disabled state existed anywhere in the site's button styling before
   — every prior use only disabled a button transiently mid-request. The
   freebies email-gate button is disabled by default (until the consent
   checkbox is ticked), a more prominent case that needs a visible cue. */
.text-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}
.text-btn:disabled:hover {
  background: var(--brand-accent);
  transform: none !important;
}

/* Full-width block variant (e.g. a modal's single confirm action) —
   same pill/brand-accent/hover-lift language as .text-btn, just spanning
   its container instead of sitting inline. */
.text-btn--block {
  display: block;
  width: 100%;
  text-align: center;
}

/* Compact variant (per-card "Get Quote"/"Check Rate Card" buttons) — same
   pill/brand-accent/hover-lift language as .text-btn, just smaller. */
.text-btn--sm {
  padding: var(--space-2) var(--space-4);
  font-size: var(--type-micro);
}

/* ---- Layout: container & section wrappers ---- */
.container {
  width: 100%;
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--section-x);
  padding-right: var(--section-x);
}

@media (min-width: 1024px) {
  .container { max-width: 1100px; }
}

@media (min-width: 1440px) {
  .container { max-width: 1280px; }
}

/* Full-viewport section, matching the reference's one-page-scroll
   homepage rhythm (Services/Portfolio/Experience/Rates-teaser/Contact
   will use this in later phases). */
.section {
  position: relative;
  min-height: 100dvh;
  scroll-margin-top: var(--header-height);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  text-align: center;
  padding: var(--section-y) var(--section-x);
}

/* Deep-page (non-homepage) variant: auto height, tighter vertical padding */
.section--compact {
  min-height: auto;
  padding-top: var(--space-12);
  padding-bottom: var(--space-12);
}

/* Below 768px, full-viewport min-height sections (each stacked with up to
   200px of --section-y padding) read as huge/empty on a phone screen —
   let content set its own height instead, with a shorter vertical rhythm.
   .section-hero keeps its own explicit 100dvh (line ~691) so the WebGL
   hero is unaffected. */
@media (max-width: 767px) {
  :root {
    --section-y: clamp(64px, 16vw, 90px);
  }

  .section {
    min-height: auto;
  }
}

/* ---- Card ---- */
.card {
  background: var(--surface-1);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm-dark);
  transition: transform var(--motion-fast) var(--ease-out),
              box-shadow var(--motion-fast) var(--ease-out);
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md-dark);
}

/* ---- Header / nav shell ----
   Fixed (functionally "sticky") full-width bar — above #logo-canvas
   (z-index 0) and #hero-transition-bg (z-index 1), and above the pinned
   .page-toolbar (z-index 90, solutions/portfolio only) so the mobile menu
   always wins. Transparent + light text by default (sits over the dark
   hero); once JS adds `.scrolled`, it flips to a solid light surface with
   dark text via the transition below. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 95;
  height: var(--header-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 0 max(var(--section-x), env(safe-area-inset-right))
           0 max(var(--section-x), env(safe-area-inset-left));
  background-color: transparent;
  color: var(--text-primary);
  box-shadow: none;
  transition: background-color var(--motion-slow) var(--ease-out),
              color var(--motion-slow) var(--ease-out),
              box-shadow var(--motion-slow) var(--ease-out);
}

.site-header.scrolled {
  background-color: var(--surface-0);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

/* Text wordmark is hidden at all sizes — below 768px the mobile header
   shows only the hamburger (.nav-toggle); at 768px+ the "Home" nav link
   covers the brand/home-mark role instead. */
.site-header .logo {
  display: none;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-ui);
  font-weight: 800;
  letter-spacing: 0.3em;
  color: var(--brand-accent);
}

.site-nav {
  display: none;
  align-items: center;
  gap: var(--space-6);
}

.site-nav ul {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  list-style: none;
}

@media (min-width: 768px) {
  .site-header {
    justify-content: center;
  }

  .site-nav {
    display: flex;
  }
}

.site-nav a {
  font-family: var(--font-ui);
  font-size: var(--type-label);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  /* Dimmed 50% toward black by default (works over both the
     transparent/white-text and .scrolled/dark-text header states),
     full brand yellow on hover. */
  color: color-mix(in srgb, currentColor 50%, black 50%);
  transition: color var(--motion-base) var(--ease-out);
}

.site-nav a:hover {
  color: var(--brand-accent);
}

/* Back-to-home link (portfolio.html / solutions.html only — index.html has
   no matching markup, so this rule is a no-op there). Sits inside the
   pinned .page-toolbar on those two pages. Styled as a solid pill CTA
   button — the same visual language as .text-btn (line 304) — rather
   than a dim nav-style text link, so it reads as a clear way back.
   Padding is tighter than .text-btn's own so it doesn't grow the
   toolbar's height. */
.back-home-link {
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-on-accent);
  background: var(--brand-accent);
  border-radius: var(--radius-pill);
  padding: 0 var(--space-4);
  /* Explicit height + inline-flex centering (rather than relying on
     padding + this element's own line-height) guarantees its text lands
     on the exact same pixel row as .filter-chip/.filter-tally's text,
     which each have their own font-size/border and would otherwise
     compute a very slightly different box height. */
  height: 28px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  box-shadow: var(--shadow-sm-dark);
  transition: background-color var(--motion-fast) var(--ease-out),
              transform var(--motion-fast) var(--ease-out),
              box-shadow var(--motion-fast) var(--ease-out);
}

.back-home-link:hover {
  background: var(--brand-accent-hover);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md-dark);
}

.back-home-link:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Below 768px the logo is hidden and .site-nav isn't rendered (see the
     comment on .site-header .logo above), leaving this the header's only
     flex item — space-between then has nothing to space it from, so it
     collapses to flex-start and the button sits top-left. Push it to the
     right edge explicitly instead. No effect at 768px+, where .nav-toggle
     is display:none anyway. */
  margin-left: auto;
  width: 44px;
  height: 44px;
  background: transparent;
  border: none;
  color: currentColor;
  cursor: pointer;
}

.nav-toggle:hover {
  background-color: var(--border-default);
}

.site-header.scrolled .nav-toggle:hover {
  background-color: var(--border-subtle);
}

.nav-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

@media (min-width: 768px) {
  .nav-toggle {
    display: none;
  }
}

.mobile-nav {
  position: fixed;
  top: var(--header-height);
  left: 0;
  width: 100%;
  height: calc(100dvh - var(--header-height));
  z-index: 94;
  background: var(--surface-0);
  overflow-y: auto;
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform var(--motion-slow) var(--ease-out),
              visibility 0s var(--motion-slow);
  padding: var(--space-8) var(--section-x)
           calc(var(--space-8) + env(safe-area-inset-bottom));
  will-change: transform;
}

.mobile-nav[aria-hidden="false"] {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--motion-slow) var(--ease-out);
}

.mobile-nav ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.mobile-nav a {
  display: block;
  padding: var(--space-2) 0;
  font-family: var(--font-ui);
  font-size: var(--type-display-sm);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-primary);
}

.mobile-nav a:hover {
  color: var(--brand-accent);
}

@media (min-width: 768px) {
  .mobile-nav {
    display: none;
  }
}

/* ---- Hero-transition backdrop ----
   Empty/unused until a later phase inserts the element and drives
   --hero-morph-bg from scroll position. Sits directly above the WebGL
   canvas and below the header/future <main> in the stacking order. */
#hero-transition-bg {
  position: fixed;
  inset: 0;
  z-index: 1;
  background: var(--hero-morph-bg, var(--surface-1));
  opacity: 0;
  pointer-events: none;
}

/* Subtle glow that trails the cursor (position driven by js/main.js via
   a lerp, same cursor-follow convention already used for the hero tilt
   and portfolio hover-preview). Sits in the same layer as the hero
   morph backdrop — above the canvas, below real content. */
#cursor-glow {
  position: fixed;
  top: 0;
  left: 0;
  width: 560px;
  height: 560px;
  z-index: 1;
  border-radius: 0;
  background: radial-gradient(circle, var(--brand-accent) 0%, transparent 70%);
  opacity: 0.15;
  filter: blur(40px);
  transform: translate(-50%, -50%);
  pointer-events: none;
  will-change: transform;
}

/* No cursor to trail on touch devices (js/main.js already skips the rAF
   loop that positions this), so drop the element itself — otherwise it
   just sits static in the top-left corner and adds a needless blurred
   layer to the compositor on phones. */
@media (hover: none), (pointer: coarse) {
  #cursor-glow {
    display: none;
  }
}

/* ---- Main scroll container ----
   Sits above #logo-canvas (0) and #hero-transition-bg (1), below the
   header (20). Normal document flow — this is what makes the page
   scrollable now that html/body's overflow:hidden lock is gone. */
#page {
  position: relative;
  z-index: 2;
}

/* ---- Hero section ----
   Exactly one viewport tall, no background of its own — the WebGL
   canvas and #hero-transition-bg show through underneath it. Hero copy
   fades in only once logo3d.js fires `dmg:hero-reveal-done` (handled in
   main.js by toggling .is-visible), so it doesn't race the 5s reveal. */
.section-hero {
  position: relative;
  height: 100dvh;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: var(--space-16);
}

.hero-copy {
  text-align: center;
  color: var(--text-primary);
  opacity: 0;
  transform: translateY(12px);
  /* Keeps the tagline off both screen edges on narrow phones — on desktop
     the centered text is already far narrower than the viewport, so this
     is a visual no-op there. */
  padding: 0 var(--space-6);
  transition: opacity var(--motion-slow) var(--ease-out),
              transform var(--motion-slow) var(--ease-out);
}

.hero-copy.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.hero-tagline {
  font-family: var(--font-ui);
  font-weight: 300;
  font-size: var(--type-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  margin-bottom: var(--space-6);
}

.tagline-break {
  display: none;
}

/* Desktop keeps the tagline on a single line — natural wrapping at
   in-between widths would otherwise break it mid-phrase. */
@media (min-width: 768px) {
  .hero-tagline {
    white-space: nowrap;
  }
}

@media (max-width: 767px) {
  .tagline-break {
    display: block;
  }
}

.hero-scroll-cue {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  color: inherit;
  font-family: var(--font-ui);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  animation: hero-cue-bob 2.2s ease-in-out infinite;
}

.hero-scroll-cue:hover {
  color: var(--brand-accent);
}

@keyframes hero-cue-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}

/* ---- Footer ---- */
/* Last section before the footer — doesn't need the standard full-
   viewport min-height (that leftover space was the real cause of the
   big gap above the footer, not just padding), and gets less bottom
   padding too so the footer sits right after the content. */
#section-contact {
  min-height: auto;
  padding-bottom: 200px;
}

@media (max-width: 767px) {
  #section-contact {
    padding-bottom: var(--space-20);
  }
}

.site-footer {
  position: relative;
  z-index: 2;
  background: var(--surface-2);
  color: var(--text-tertiary);
  text-align: center;
  padding: var(--space-4) var(--section-x) var(--space-5);
  font-family: var(--font-ui);
  font-size: var(--type-label);
  letter-spacing: 0.03em;
}

.site-footer a {
  color: inherit;
  transition: color var(--motion-base) var(--ease-out);
}

.site-footer a:hover {
  color: var(--brand-accent);
}

/* On narrow screens "Est. 2017 · Loud" and "Ideas Start Here" wrap
   mid-phrase; break cleanly before the tagline instead. Desktop keeps
   the single dotted line. */
@media (max-width: 767px) {
  .site-footer .foot-sep {
    display: none;
  }

  .site-footer .foot-tag {
    display: block;
  }
}

/* ================================================================
   PHASE 2 — HOMEPAGE SECTIONS (Services / Portfolio / Experience / Contact)
   Reuses the .section/.container/.card/.btn/.text-btn/.section-title/
   .section-eyebrow/.section-subtitle utilities from the design system
   above; everything below is new component-level CSS for this phase's
   ticker/grid/card layouts, following the same custom-property-driven
   conventions (var(--space-*), var(--brand-accent), etc.).

   [data-reveal] elements have no special hidden-by-default CSS state:
   js/sections.js's GSAP ScrollTrigger pass sets/animates their opacity
   itself, so if the GSAP CDN fails to load the content is simply
   visible (no FOUC / stuck-invisible risk).
================================================================ */

/* .container used as a direct child of .section only needs its max-width
   cap for wide viewports — .section already supplies the outer gutter via
   its own `padding: var(--section-y) var(--section-x)`, so the container's
   horizontal padding would otherwise double up when nested this way.
   Centering itself is now handled by .section's own `align-items: center`
   (site-wide centering) — no left/right margin override needed here. */
.section > .container {
  padding-left: 0;
  padding-right: 0;
}

/* ---- Services: header + ticker ----
   The ticker rows read best full-bleed (edge-to-edge), so this section
   drops .section's horizontal padding and lets .svc-header apply its own
   gutter instead — same split the reference site used. */
.svc-section {
  justify-content: center;
  gap: var(--space-16);
  overflow: hidden;
  padding-left: 0;
  padding-right: 0;
}

.svc-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  gap: var(--space-3);
  padding-left: var(--section-x);
  padding-right: var(--section-x);
}

.svc-ticker {
  overflow: hidden;
  width: 100%;
}

.svc-ticker:hover .svc-ticker-track {
  animation-play-state: paused;
}

.svc-ticker-track {
  display: flex;
  align-items: baseline;
  white-space: nowrap;
  width: max-content;
  will-change: transform;
  transform: translate3d(0, 0, 0);
  backface-visibility: hidden;
}

.svc-ticker--fwd .svc-ticker-track { animation: svc-ticker-left  110s linear infinite; }
.svc-ticker--rev .svc-ticker-track { animation: svc-ticker-right 130s linear infinite; }

@keyframes svc-ticker-left {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

@keyframes svc-ticker-right {
  from { transform: translate3d(-50%, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .svc-ticker--fwd .svc-ticker-track,
  .svc-ticker--rev .svc-ticker-track {
    animation: none;
  }
}

.svc-item {
  display: inline-flex;
  align-items: baseline;
  flex-shrink: 0;
}

.svc-item-name {
  font-family: var(--font-ui);
  font-weight: 300;
  font-variant-caps: titling-caps;
  text-transform: uppercase;
  color: var(--brand-accent);
  letter-spacing: 0.1em;
  font-size: var(--type-display-sm);
}

.svc-item-sep {
  font-family: var(--font-ui);
  font-size: var(--type-label);
  color: var(--text-tertiary);
  margin: 0 var(--space-2);
}

.svc-item-desc {
  font-family: var(--font-ui);
  font-size: var(--type-label);
  color: var(--text-tertiary);
}

.svc-item-pipe {
  font-family: var(--font-ui);
  color: var(--border-strong);
  margin: 0 var(--space-4);
}

/* ---- Portfolio: header, grid, cursor-follow preview ----
   Matches .svc-header's pattern: eyebrow/title/CTA stacked and centered
   together, grid content below. */
.portfolio-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-10);
}

/* Flexbox (not grid) so an incomplete trailing row centers itself instead
   of leaving empty cells on the right — same reasoning as .stats-tier. */
.portfolio-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
}

.portfolio-item {
  position: relative;
  flex: 0 0 calc((100% - var(--space-4)) / 2);
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: var(--shadow-sm-dark);
  transition: transform var(--motion-fast) var(--ease-out),
              box-shadow var(--motion-fast) var(--ease-out);
}

@media (min-width: 640px) {
  .portfolio-item { flex-basis: calc((100% - 2 * var(--space-4)) / 3); }
}

@media (min-width: 1024px) {
  .portfolio-item { flex-basis: calc((100% - 4 * var(--space-4)) / 5); }
}

.portfolio-item:hover {
  transform: translateY(-4px) scale(1.02);
  box-shadow: var(--shadow-md-dark);
}

.portfolio-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.portfolio-preview {
  position: fixed;
  top: 0;
  left: 0;
  width: clamp(180px, 18vw, 280px);
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-lg);
  overflow: hidden;
  pointer-events: none;
  z-index: 30;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.85) rotate(-4deg);
  box-shadow: var(--shadow-lg);
  transition: opacity var(--motion-slow) var(--ease-out),
              transform var(--motion-slow) var(--ease-out);
}

.portfolio-preview.is-active {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1) rotate(0deg);
}

/* Touch variant — no cursor to follow, so on tap this centers itself as
   a large modal-style preview instead of the desktop cursor-follow
   float (see the touch branch of initHoverPreview in js/sections.js).
   pointer-events re-enabled (only while .is-active — when closed the
   element is invisible but still fixed over the viewport center, and
   must not swallow taps meant for the content behind it). */
.portfolio-preview.is-tap {
  left: 50%;
  top: 50%;
  width: min(90vw, 420px);
  aspect-ratio: 4 / 5;
  z-index: 300;
}

.portfolio-preview.is-tap.is-active {
  pointer-events: auto;
  transform: translate(-50%, -50%) scale(1) rotate(0deg);
}

/* Dim backdrop, a separate fixed sibling (not a pseudo-element of
   .portfolio-preview itself, which has overflow:hidden + border-radius
   for the image tile and shouldn't also clip/shape a full-viewport
   scrim) — see #portfolio-preview-backdrop in index.html, toggled by
   the same .is-active class from js/sections.js. */
.portfolio-preview-backdrop {
  position: fixed;
  inset: 0;
  z-index: 299;
  background: color-mix(in srgb, var(--surface-0) 80%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-slow) var(--ease-out);
}

.portfolio-preview-backdrop.is-active {
  opacity: 1;
  pointer-events: auto;
}

.portfolio-preview-inner {
  width: 100%;
  height: 100%;
  background-color: var(--surface-2);
  background-size: cover;
  background-position: center;
  opacity: 1;
  transition: opacity var(--motion-slow) var(--ease-out);
}

/* Loading shimmer for the desktop cursor-float / mobile tap preview —
   same look as .skeleton-tile on the grid (reuses its skeleton-shimmer
   keyframes) so the blown-up preview image doesn't sit blank while it
   decodes, which was far more noticeable on mobile's much larger .is-tap
   modal than on desktop's small float. Toggled from
   loadPreviewImage() in js/sections.js. */
.portfolio-preview.is-loading {
  background: linear-gradient(100deg, var(--surface-2) 30%, var(--surface-3) 45%, var(--surface-2) 60%);
  background-size: 250% 100%;
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
}
.portfolio-preview.is-loading .portfolio-preview-inner {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .portfolio-preview.is-loading { animation: none; }
}

/* Extra breathing room after "Numbers That Matter" before the stats begin —
   the site-wide .section-title margin was reduced for tighter heading-to-
   button spacing elsewhere, but this section wants more room here. */
.experience-section .section-title {
  margin-bottom: var(--space-8);
}

/* ---- Experience: stat-counter row (built from data/stats.json by
   js/sections.js, animated count-up) ----
   Rows exist purely for line-wrapping (chunked 3/4/3/remainder), not for
   sizing — each item's font size is computed individually in JS via
   log-scale interpolation between the smallest and largest magnitude in
   the dataset, applied through the --stat-scale custom property below.
   This keeps the visual size always proportional to the real number,
   instead of manually-assigned tier buckets that drift out of sync
   whenever a number changes. Flexbox with flex:0 0 auto (no grow/shrink)
   plus justify-content:center means partial trailing rows center
   themselves instead of sitting flush-left. */
.stats-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
}

.stats-tier {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-end;
  gap: var(--space-8) var(--space-6);
  text-align: center;
}

/* Fade each successive tier so the top (biggest-number) row dominates the
   visual hierarchy instead of competing with the rows below it. */
.stats-tier:nth-child(2) { opacity: 0.95; }
.stats-tier:nth-child(3) { opacity: 0.90; }
.stats-tier:nth-child(4) { opacity: 0.85; }

/* On phones, wrapped tiers of side-by-side stats read cramped against
   the per-stat --stat-scale sizing — stack one stat per line instead. */
@media (max-width: 767px) {
  .stats-tier {
    flex-direction: column;
    align-items: center;
  }
}

.stat-block {
  flex: 0 0 auto;
  min-width: 120px;
}

/* Number highlight: the "Upto" prefix reads as a small muted label, so
   the actual counted figure is the dominant, brand-yellow focal point —
   previously both were the same weight/color and blurred together. */
.stat-prefix {
  display: block;
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: var(--space-1);
}

.stat-number {
  font-family: var(--font-ui);
  font-weight: 800;
  color: var(--brand-accent);
  line-height: var(--leading-tight);
  font-size: calc(var(--stat-scale, 1) * var(--type-display));
}

.stat-label {
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: var(--type-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-secondary);
  margin-top: var(--space-1);
}

/* Legal-safe footnote — clarifies "Upto" figures reflect all touched/
   contributed work, not solely original in-house designs. */
.stats-disclaimer {
  font-family: var(--font-ui);
  font-style: italic;
  font-size: var(--type-micro);
  color: var(--text-tertiary);
  max-width: 60ch;
  margin: var(--space-8) auto 0;
}

@media (min-width: 900px) {
  .stats-disclaimer {
    white-space: nowrap;
    max-width: none;
  }
}

/* ---- Contact + About ---- */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-16);
  text-align: left;
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
}

@media (min-width: 768px) {
  .contact-grid { grid-template-columns: repeat(2, 1fr); }
}

.contact-info {
  font-style: normal;
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  margin-top: var(--space-4);
}

.about-text {
  font-family: var(--font-ui);
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  margin-top: var(--space-4);
  max-width: 60ch;
  margin-left: auto;
  margin-right: auto;
}

.about-text--compact {
  font-size: var(--type-label);
}

.copy-link {
  position: relative;
  display: inline-block;
  color: var(--text-primary);
  cursor: pointer;
  user-select: none;
  transition: var(--transition);
}

.copy-link:hover {
  color: var(--brand-accent);
}

.whatsapp-link:hover,
.whatsapp-link.copied {
  color: var(--signal-success);
}

.copy-link::after {
  content: "COPIED!";
  position: absolute;
  left: 100%;
  top: 50%;
  margin-left: var(--space-3);
  transform: translateY(-50%);
  background-color: var(--brand-accent);
  color: var(--text-on-accent);
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  font-weight: 800;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  opacity: 0;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}

.whatsapp-link::after {
  background-color: var(--signal-success);
}

.copy-link.copied::after {
  opacity: 1;
  transform: translateY(-50%) translateX(5px);
}

/* ================================================================
   PHASE 3 — DEEP PAGES (Portfolio / Solutions)
   Reuses .container/.section-eyebrow/.section-title/.text-btn from the
   design system above; everything below is new component-level CSS for
   these two pages' page-hero banner, sticky filter toolbar, masonry/grid
   galleries, HQ badge + lock veil, and the access-key modal — following
   the same token-driven conventions as Phase 2.
================================================================ */

/* ---- Page hero banner ----
   portfolio.html and solutions.html have no .site-header of their own —
   the pinned .page-toolbar IS the page's top bar (back-to-home + filters).
   Extra top padding here keeps the heading clear of the toolbar (the
   toolbar can wrap to two rows on category-heavy pages like
   solutions.html). */
.page-hero {
  /* Stacked above the fixed #logo-canvas/#cursor-glow/#hero-transition-bg
     layers (z-index 0/1/1) the same way index.html's #page (z-index 2)
     does — neither portfolio.html nor solutions.html has a #page wrapper,
     so each top-level content block needs this explicitly or the fixed
     3D logo paints over it. */
  position: relative;
  z-index: 2;
  padding: calc(var(--space-16) + 64px) 0 var(--space-10);
  border-bottom: 1px solid var(--border-subtle);
}

.page-hero .section-eyebrow,
.page-hero .section-title {
  text-align: center;
}

.page-hero-sub {
  font-family: var(--font-ui);
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  max-width: 56ch;
}

/* ---- Filter toolbar: pinned to the very top of the viewport, doubling
   as the page's header. A 3-column grid (side tracks equal 1fr, chip row
   auto-sized in the middle) rather than a plain flex row — that's what
   makes the chip row center on the true toolbar/page center regardless
   of the back-link and tally having different widths; a flex row would
   only center the chips within the leftover space between two unequal
   siblings, which visibly drifts off-page-center. The chip row still
   scrolls internally rather than wrapping, keeping the fixed toolbar's
   height constant regardless of viewport width/chip count. */
.page-toolbar {
  display: grid;
  /* minmax(0, auto), not plain auto — lets the middle column shrink below
     its content's natural width on narrow viewports/many chips (deferring
     to .filter-group's own overflow-x:auto scroll) instead of forcing the
     side 1fr tracks to negative/zero width. */
  grid-template-columns: 1fr minmax(0, auto) 1fr;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--section-x);
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 90;
  background: color-mix(in srgb, var(--surface-0) 92%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border-subtle);
}

.page-toolbar .back-home-link {
  justify-self: start;
}

/* Below 768px there's no .site-header anymore, so the toolbar's own
   back-to-home link is the only way back — it stays visible. The chip
   row (.filter-group) moves behind .filter-menu-toggle instead (see the
   .filter-menu-toggle / .filter-group rules further down), so the
   toolbar becomes a simple 3-slot row: back-link | tally | toggle. */
@media (max-width: 767px) {
  .page-toolbar {
    grid-template-columns: auto 1fr auto;
  }

  .filter-tally {
    justify-self: center;
  }
}

.filter-group {
  display: flex;
  justify-self: center;
  max-width: 100%;
  min-width: 0;
  gap: var(--space-2);
  flex-wrap: nowrap;
  /* "safe" keeps this left-aligned (scrollable from the start) instead of
     centered whenever the chips overflow — plain `center` would center
     the overflow itself, clipping the first/last chips off-screen with
     no way to scroll to them. */
  justify-content: safe center;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.filter-group::-webkit-scrollbar {
  display: none;
}

/* ~75% of the previous size (font-size 0.72rem->0.54rem, padding
   8px/16px->6px/12px). Explicit height + inline-flex centering (see
   .back-home-link above) keeps this pixel-aligned with its siblings. */
.filter-chip {
  font-family: var(--font-ui);
  font-size: 0.54rem;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  padding: 0 9px;
  height: 26px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-out),
              border-color var(--motion-fast) var(--ease-out),
              background-color var(--motion-fast) var(--ease-out);
}

.filter-chip svg { flex-shrink: 0; width: 15px; height: 15px; }

/* Icon-only chips carry their name in aria-label + a hover/focus tooltip
   (see js), which never surfaces on a mobile tap. This span holds the
   same name as visible text; hidden on the desktop inline row, shown
   in the mobile dropdown panel below. */
.chip-label { display: none; }

.filter-chip:hover,
.filter-chip.is-active {
  border-color: var(--brand-accent);
  color: var(--brand-accent);
  background: var(--brand-accent-soft-bg);
}

/* Hamburger-style category toggle — mirrors index.html's .nav-toggle.
   Hidden on desktop, where the chip row stays inline; on mobile it opens
   .filter-group as a dropdown panel instead (see the mobile .filter-group
   override below), because a scrollable inline chip "bus" row reads too
   cramped once the page-toolbar also has to fit the back-home link and
   tally in the same row. */
.filter-menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  justify-self: end;
  background: transparent;
  border: none;
  color: var(--text-primary);
  cursor: pointer;
}

.filter-menu-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

@media (max-width: 767px) {
  .filter-menu-toggle {
    display: inline-flex;
  }

  /* Taken out of the toolbar's grid flow (absolutely-positioned grid
     items don't occupy a track — see the comment on .page-toolbar's
     mobile grid-template-columns above), so it doesn't compete with
     back-home-link/tally/toggle for the 3 mobile columns. Positions as
     a full-width dropdown panel directly under the fixed toolbar. */
  .filter-group {
    display: flex;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    max-height: calc(100dvh - var(--space-16));
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    overflow-x: visible;
    overflow-y: auto;
    gap: var(--space-2);
    padding: var(--space-3) var(--section-x) var(--space-4);
    background: color-mix(in srgb, var(--surface-0) 97%, transparent);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border-subtle);
    /* visibility/opacity (not display:none) so the panel stays in the
       render tree and keeps its scroll position between opens. */
    transition: opacity var(--motion-fast) var(--ease-out),
                visibility 0s var(--motion-fast);
  }

  .filter-group.is-open {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transition: opacity var(--motion-fast) var(--ease-out);
  }

  .filter-chip {
    width: 100%;
    justify-content: flex-start;
  }

  .filter-chip .chip-label {
    display: inline;
    margin-left: var(--space-2);
  }
}

/* position:fixed so this escapes .filter-group's overflow-x:auto clipping
   (see the comment above .filter-group) instead of being cut off. */
.filter-tooltip {
  position: fixed;
  transform: translateX(-50%);
  z-index: 200;
  background: var(--surface-1);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease-out);
}
.filter-tooltip.is-visible {
  opacity: 1;
}
.filter-tooltip-name {
  display: block;
}
.filter-tooltip-desc {
  display: block;
  text-transform: none;
  white-space: normal;
  max-width: 180px;
  margin-top: 2px;
  color: var(--text-secondary);
  font-size: 0.9em;
}

.filter-tally {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: var(--text-tertiary);
  white-space: nowrap;
  justify-self: end;
  height: 26px;
  display: inline-flex;
  align-items: center;
}

/* Coarse-pointer (touch) devices only — bump the toolbar's tap targets
   toward the ~38-44px guideline without changing desktop's tighter,
   mouse-driven density. All three keep sharing one explicit height so
   their text still lands on the same pixel row (see .back-home-link
   above). */
@media (pointer: coarse) {
  .filter-chip,
  .back-home-link,
  .filter-tally {
    height: 38px;
  }

  .filter-chip {
    padding: 0 14px;
  }

  .filter-chip svg {
    width: 17px;
    height: 17px;
  }
}

/* ---- Empty / error state (shared by both galleries) ---- */
.grid-empty {
  text-align: center;
  padding: var(--space-6) 0;
  font-family: var(--font-ui);
  font-size: var(--type-body);
  color: var(--text-tertiary);
  grid-column: 1 / -1;
  column-span: all;
}

@media (max-width: 560px) {
  .grid-empty {
    padding: var(--space-4) 0;
  }
}

.grid-empty span {
  display: block;
  margin-bottom: var(--space-3);
}

.grid-empty span svg {
  width: 32px;
  height: 32px;
}

/* ---- Skeleton tile: per-tile loading placeholder + shimmer. Shared by
   index.html's portfolio preview grid, portfolio.html's full masonry
   gallery, and solutions.html's product grid — each tile reveals
   independently as its own image/video finishes loading, in place of a
   single whole-grid progress bar. Wired up via window.dmgSkeletonMedia
   in js/sections.js. ---- */
.skeleton-tile {
  position: relative;
  overflow: hidden;
  background: linear-gradient(100deg,
    var(--surface-2) 30%,
    var(--surface-3) 45%,
    var(--surface-2) 60%);
  background-size: 250% 100%;
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
  box-shadow: inset 0 0 20px rgba(253,185,19,0.08);
}

@keyframes skeleton-shimmer {
  0%   { background-position: 150% 0; }
  100% { background-position: -150% 0; }
}

.skeleton-tile .skeleton-media {
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease-out);
}

.skeleton-tile.is-loaded {
  animation: none;
  box-shadow: none;
  background: var(--surface-2);
}

.skeleton-tile.is-loaded .skeleton-media {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-tile { animation: none; }
}

/* ---- Portfolio: uniform-tile gallery ----
   Every tile is a fixed 4:5 box (see .card-inner below), cropped via
   object-fit: cover — same mechanism as the homepage preview grid
   (.portfolio-grid/.portfolio-item above). Flexbox (not grid) so an
   incomplete trailing row centers itself instead of leaving empty cells
   on the right — same reasoning as .portfolio-grid/.stats-tier. Because
   every tile is the same size, no JS-computed masonry is needed. */
.port-gallery {
  position: relative;
  z-index: 2;
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-8) var(--section-x) var(--space-20);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
}

.port-card {
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-1);
  display: block;
  box-shadow: var(--shadow-sm-dark);
  transition: box-shadow var(--motion-base) var(--ease-out);
  flex: 0 0 100%;
}

@media (min-width: 561px) {
  .port-card { flex-basis: calc((100% - var(--space-3)) / 2); }
}

@media (min-width: 901px) {
  .port-card { flex-basis: calc((100% - 2 * var(--space-3)) / 3); }
}

@media (min-width: 1281px) {
  .port-card { flex-basis: calc((100% - 3 * var(--space-3)) / 4); }
}

.port-card:hover { box-shadow: var(--shadow-md-dark); }

.card-inner {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  aspect-ratio: 4 / 5;
}

.card-media {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  will-change: transform;
  transition: transform var(--motion-slow) var(--ease-in-out);
}

/* Touch has no real hover — a tap "sticks" the :hover state, so this
   zoom would trigger on tap and stay zoomed until the user taps
   elsewhere. Restrict it to devices with a real pointer, mirroring the
   #cursor-glow touch override above. */
@media (hover: hover) and (pointer: fine) {
  .port-card:hover .card-media { transform: scale(1.07); }
}

.hq-badge {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 5;
  background: var(--brand-accent);
  color: var(--text-on-accent);
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  font-weight: 800;
  letter-spacing: var(--tracking-wider);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-xs);
  text-transform: uppercase;
  opacity: 1;
  transform: none;
  pointer-events: none;
}

/* Locked card — pre-unlock tiles embed no real media at all (files are
   cookie-gated behind portfolio-media.php), so the lock veil sits on this
   dark placeholder panel. Hardcoded tones, like .lock-veil's overlay. */
.card-media--locked {
  background:
    radial-gradient(120% 90% at 30% 20%, rgba(253, 185, 19, 0.07), transparent 60%),
    linear-gradient(160deg, #1b1b1b 0%, #101010 100%);
}

.lock-veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  border-radius: var(--radius-md);
  transition: background var(--motion-base) var(--ease-out);
}

.port-card.locked:hover .lock-veil {
  background: rgba(0,0,0,0.15);
}

.lock-ring {
  width: 46px;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  transition: transform var(--motion-base) var(--ease-out),
              color var(--motion-base) var(--ease-out);
}

.port-card.locked:hover .lock-ring {
  transform: scale(1.12);
  color: var(--brand-accent);
}

.lock-txt {
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/* Unlock glow flash */
@keyframes uglow {
  0%, 100% { box-shadow: none; }
  40% { box-shadow: 0 0 0 3px rgba(253,185,19,0.55), 0 0 50px rgba(253,185,19,0.2); }
}

.port-card.just-unlocked { animation: uglow 0.7s ease; }

/* ---- Portfolio: full-screen preview lightbox ---- */
.port-lightbox {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  backdrop-filter: blur(8px);
  transition: opacity var(--motion-base) var(--ease-out);
}

.port-lightbox.open {
  opacity: 1;
  pointer-events: all;
}

/* Full-screen preview is a desktop-only feature — the card's click
   handler still fires on mobile, it just has nothing visible to open. */
@media (max-width: 767px) {
  .port-lightbox {
    display: none !important;
  }
}

/* Indeterminate loading bar shown while a full-screen image downloads —
   reuses the homepage loader look (accent fill + glow, the shared
   #progress-fill styling and @keyframes progress-indeterminate above). */
.port-lightbox-progress {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80px;
  height: 3px;
  background: var(--border-strong);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-fast) var(--ease-out);
  z-index: 2;
}

.port-lightbox-progress.is-active { opacity: 1; }

.port-lightbox-progress-bar {
  position: absolute;
  display: block;
  width: 40%;
  height: 100%;
  background: var(--brand-accent-bright);
  box-shadow: 0 0 8px rgba(253,185,19,0.7), 0 0 16px rgba(253,185,19,0.35);
  animation: progress-indeterminate 1.2s ease-in-out infinite;
}

.port-lightbox img,
.port-lightbox video {
  max-width: min(92vw, 1400px);
  max-height: 90vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  transform: scale(0.94);
  transition: transform var(--motion-base) var(--ease-out);
}

.port-lightbox.open img,
.port-lightbox.open video {
  transform: scale(1);
}

/* Items without an HQ source render at a fixed 65% of the HQ reference
   box — width/height (not max-width/max-height) so this holds regardless
   of the thumbnail's own native resolution; a max- cap only shrinks
   oversized images and never enlarges undersized ones, which made most
   low-res thumbnails (already smaller than the cap) render at whatever
   tiny native size they happened to have instead of a consistent 65%. */
.port-lightbox.is-lowres img,
.port-lightbox.is-lowres video {
  width: calc(min(92vw, 1400px) * 0.65);
  height: calc(90vh * 0.65);
  max-width: none;
  max-height: none;
}

.port-lightbox-close,
.port-lightbox-prev,
.port-lightbox-next {
  position: absolute;
  width: 42px;
  height: 42px;
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 0;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--motion-fast) var(--ease-out);
}

.port-lightbox-close {
  top: var(--space-5);
  right: var(--space-5);
}

.port-lightbox-prev,
.port-lightbox-next {
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.6rem;
}

.port-lightbox-prev { left: var(--space-5); }
.port-lightbox-next { right: var(--space-5); }

.port-lightbox-close:hover,
.port-lightbox-prev:hover,
.port-lightbox-next:hover {
  background: rgba(255, 255, 255, 0.22);
}

.port-lightbox-close:focus-visible,
.port-lightbox-prev:focus-visible,
.port-lightbox-next:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ---- Freebies: masonry gallery ----
   Unlike .port-gallery (every tile a fixed 4:5 box, see above), freebie
   source images have arbitrary aspect ratios — a real Pinterest-style
   masonry. CSS multi-column (not a JS packing library, keeping the site's
   zero-dependency convention) fills top-to-bottom-then-next-column rather
   than a JS packer's shortest-column-first, an accepted tradeoff for a
   small asset library. Each card's .card-inner gets an inline
   aspect-ratio from the item's own thumbW/thumbH (set in the renderer) so
   its box reserves the correct height before the image loads — no layout
   shift, and .skeleton-tile's shimmer has real space to render into. */
.free-gallery {
  position: relative;
  z-index: 2;
  column-count: 1;
  column-gap: var(--space-3);
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-8) var(--section-x) var(--space-20);
}

@media (min-width: 561px)  { .free-gallery { column-count: 2; } }
@media (min-width: 901px)  { .free-gallery { column-count: 3; } }
@media (min-width: 1281px) { .free-gallery { column-count: 4; } }

.free-card {
  display: block;
  width: 100%;
  break-inside: avoid;
  margin-bottom: var(--space-3);
  border: none;
  padding: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-1);
  box-shadow: var(--shadow-sm-dark);
  cursor: pointer;
  transition: box-shadow var(--motion-base) var(--ease-out);
}

.free-card:hover { box-shadow: var(--shadow-md-dark); }

.free-card .card-inner {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
}

@media (hover: hover) and (pointer: fine) {
  .free-card:hover .card-media { transform: scale(1.04); }
}

.free-card-info {
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.free-card-title {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--type-label-sm);
  color: var(--text-primary);
  margin-bottom: 2px;
}

.free-card-desc {
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ---- Freebies: full-screen lightbox ----
   A parallel copy of .port-lightbox rather than a shared component
   (avoids coupling portfolio.html and freebies.html), extended with a
   title/description/Download info panel and — unlike the desktop-only
   .port-lightbox — available on every viewport, since this gallery is a
   lead-gen conversion surface where gating mobile out would work against
   the feature's purpose. */
.free-lightbox {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  backdrop-filter: blur(8px);
  transition: opacity var(--motion-base) var(--ease-out);
  padding: var(--space-5);
  box-sizing: border-box;
}

.free-lightbox.open {
  opacity: 1;
  pointer-events: all;
}

.free-lightbox-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: min(92vw, 1200px);
  max-height: 92vh;
  transform: scale(0.94);
  transition: transform var(--motion-base) var(--ease-out);
}

.free-lightbox.open .free-lightbox-inner {
  transform: scale(1);
}

.free-lightbox-progress {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80px;
  height: 3px;
  background: var(--border-strong);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-fast) var(--ease-out);
  z-index: 2;
}

.free-lightbox-progress.is-active { opacity: 1; }

.free-lightbox-progress-bar {
  position: absolute;
  display: block;
  width: 40%;
  height: 100%;
  background: var(--brand-accent-bright);
  box-shadow: 0 0 8px rgba(253,185,19,0.7), 0 0 16px rgba(253,185,19,0.35);
  animation: progress-indeterminate 1.2s ease-in-out infinite;
}

.free-lightbox img {
  display: block;
  max-width: min(92vw, 1200px);
  max-height: 66vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.free-lb-info {
  width: 100%;
  max-width: 560px;
  text-align: center;
  margin-top: var(--space-5);
}

.free-lb-title {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--type-body);
  color: #fff;
  margin-bottom: var(--space-2);
}

.free-lb-desc {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: rgba(255,255,255,0.72);
  line-height: var(--leading-relaxed);
  margin-bottom: var(--space-4);
}

.free-lightbox-close,
.free-lightbox-prev,
.free-lightbox-next {
  position: absolute;
  width: 42px;
  height: 42px;
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 0;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--motion-fast) var(--ease-out);
}

.free-lightbox-close {
  top: var(--space-5);
  right: var(--space-5);
}

.free-lightbox-prev,
.free-lightbox-next {
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.6rem;
}

.free-lightbox-prev { left: var(--space-5); }
.free-lightbox-next { right: var(--space-5); }

.free-lightbox-close:hover,
.free-lightbox-prev:hover,
.free-lightbox-next:hover {
  background: rgba(255, 255, 255, 0.22);
}

.free-lightbox-close:focus-visible,
.free-lightbox-prev:focus-visible,
.free-lightbox-next:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

@media (max-width: 600px) {
  .free-lightbox-prev,
  .free-lightbox-next { display: none; }
}

/* ---- Solutions: card grid ---- */
.sol-grid {
  position: relative;
  z-index: 2;
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-8) var(--section-x) var(--space-20);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--space-5);
}

@media (max-width: 900px) {
  .sol-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
}

@media (max-width: 560px) {
  .sol-grid { grid-template-columns: 1fr 1fr; }
}

/* 2 columns gets too narrow for card text/prices under ~380px — drop to
   a single column on the smallest phones. */
@media (max-width: 379px) {
  .sol-grid { grid-template-columns: 1fr; }
}

.sol-card {
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-1);
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-sm-dark);
  transition: transform var(--motion-fast) var(--ease-out),
              box-shadow var(--motion-fast) var(--ease-out);
}

.sol-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md-dark);
}

.sol-card-media-frame {
  width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
}

.sol-card-media {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  will-change: transform;
  transition: transform var(--motion-slow) var(--ease-in-out);
}

/* See the matching .port-card comment above — same sticky-hover-on-tap
   issue applies to the solution cards. */
@media (hover: hover) and (pointer: fine) {
  .sol-card:hover .sol-card-media { transform: scale(1.07); }
}

.sol-card-body {
  padding: var(--space-4) var(--space-5) var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.sol-card-cat {
  font-family: var(--font-ui);
  font-size: var(--type-micro);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.sol-card-title {
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: 700;
  color: var(--text-primary);
  line-height: var(--leading-snug);
}

.sol-card-price {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  letter-spacing: var(--tracking-wide);
  color: var(--brand-accent);
}

.sol-card-desc {
  font-family: var(--font-ui);
  font-size: 0.78rem;
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}

/* Description is a desktop-only detail — keeps mobile solution cards
   short (title/price/actions only). */
@media (max-width: 767px) {
  .sol-card-desc {
    display: none;
  }
}

.sol-card-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}

/* ---- Solutions: Get Quotation section ---- */
.quote-section {
  position: relative;
  z-index: 2;
  padding: var(--space-16) 0 var(--space-20);
  border-top: 1px solid var(--border-subtle);
}

.quote-box {
  text-align: center;
}

/* Narrower than .quote-box itself so the heading above can render at full
   section width (fits on one line) while the form stays a readable column. */
.quote-box-inner {
  max-width: 480px;
  margin: 0 auto;
}

/* Matches the smaller Label-scale treatment used by .about-text--compact
   (e.g. "Loud Ideas Start Here." on the home page) instead of the larger
   Body-scale size .page-hero-sub uses by default elsewhere. */
.quote-box-inner .page-hero-sub {
  font-size: clamp(0.62rem, 1.6vw + 0.45rem, 0.78rem);
  margin-bottom: var(--space-4);
}

/* Single-line only where the ~80ch sentence actually fits — on phones the
   nowrap clips it at the viewport edge, so let it wrap there. */
@media (min-width: 768px) {
  .quote-box-inner .page-hero-sub {
    white-space: nowrap;
  }
}

.quote-box .modal-inp { margin-bottom: var(--space-4); }

/* Same font treatment as .quote-box-inner .page-hero-sub above, so the
   opt-in reads as part of the same subtext voice, not a separate style. */
.quote-news-opt {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-ui);
  font-size: clamp(0.62rem, 1.6vw + 0.45rem, 0.78rem);
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
  cursor: pointer;
}
.quote-news-opt input[type="checkbox"] {
  accent-color: var(--brand-accent);
  cursor: pointer;
}

/* Freebies + Design Pro: news signup is mandatory on those two flows, so
   there is no checkbox to render — just a statement of what requesting
   access signs you up for. Matches .quote-news-opt's type scale and colour
   so the two gates still read as the same family. */
.news-required-note {
  font-family: var(--font-ui);
  font-size: clamp(0.62rem, 1.6vw + 0.45rem, 0.78rem);
  line-height: 1.5;
  color: var(--text-secondary);
  text-align: center;
  margin: 0 0 var(--space-4);
}
.news-required-note a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.news-required-note a:hover,
.news-required-note a:focus-visible {
  color: var(--brand-accent);
}

.quote-context {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: var(--text-secondary);
  margin-bottom: var(--space-3);
}
.quote-context strong { color: var(--text-primary); }
#quote-context-clear {
  background: none;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0 0 4px;
}

/* Stacked, full-width (.text-btn--block) so both buttons share the
   email input's exact left/right edges above them. */
.quote-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.quote-status {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: var(--text-secondary);
  margin-top: var(--space-4);
  min-height: 1.2em;
}

.quote-status.is-error   { color: var(--signal-danger); }
.quote-status.is-success { color: var(--signal-success); }

/* ---- Access-key modal (portfolio.html only) ---- */
.modal-bd {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.84);
  backdrop-filter: blur(12px);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-base) var(--ease-out);
}

.modal-bd.open { opacity: 1; pointer-events: all; }

.modal-box {
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xl);
  padding: var(--space-12) var(--space-8);
  width: min(440px, 92vw);
  text-align: center;
  position: relative;
  transform: translateY(28px) scale(0.96);
  transition: transform var(--motion-slow) var(--ease-spring);
}

.modal-bd.open .modal-box { transform: translateY(0) scale(1); }

.modal-ico {
  display: block;
  margin-bottom: var(--space-4);
  color: var(--brand-accent);
}

.modal-ico svg {
  width: 32px;
  height: 32px;
}

/* Compact dialog heading — intentionally its own scoped rule rather than
   .section-title (that class's huge display size/titling-caps is a
   page-title component, a different intent than a modal's small heading). */
.modal-ttl {
  font-family: var(--font-ui);
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: var(--space-2);
  color: var(--text-primary);
}

.modal-info {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
  margin-bottom: var(--space-8);
}

.modal-inp {
  width: 100%;
  background: var(--surface-inset);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: var(--type-body);
  letter-spacing: var(--tracking-wider);
  text-align: center;
  outline: none;
  transition: border-color var(--motion-fast) var(--ease-out);
  margin-bottom: var(--space-3);
  box-sizing: border-box;
}

.modal-inp:focus { border-color: var(--brand-accent); }

.modal-inp.bad {
  animation: shk 0.3s;
  border-color: var(--signal-danger);
}

@keyframes shk {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-7px); }
  75% { transform: translateX(7px); }
}

.modal-x {
  position: absolute;
  top: var(--space-3);
  right: var(--space-4);
  background: none;
  border: none;
  color: var(--text-tertiary);
  font-size: var(--type-display-sm);
  line-height: 1;
  font-family: inherit;
  cursor: pointer;
  transition: color var(--motion-base) var(--ease-out);
}

.modal-x:hover { color: var(--text-primary); }

.modal-err {
  font-family: var(--font-ui);
  font-size: var(--type-label-sm);
  color: var(--signal-danger);
  margin-top: var(--space-3);
  min-height: var(--space-4);
}

/* Honeypot trap field (see bot-lib.php / js/turnstile-helper.js) — kept out
   of sight and out of the tab order for real visitors, but not display:none
   (some simple form-filler bots skip fields hidden that way, which defeats
   the point of a trap). */
.dmg-hp {
  position: absolute !important;
  left: -9999px !important;
  top: auto;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* Cloudflare Turnstile widget container. */
.cf-turnstile-slot {
  margin: 0 0 var(--space-3);
  min-height: 65px; /* the widget's own rendered height — avoids a layout
                        jump once it loads */
}
