/* ============================================================
   Under the Surface - page-specific layout.
   Ported from the approved Claude Design prototype (project/Under The
   Surface.dc.html): a scroll-driven "iceberg" scene showing everything
   that ships with a BowerVault tool beyond the visible lead-magnet
   widget. The prototype drives this via its own DCLogic/React preview
   runtime (support.js) - not shipped here (see CLAUDE.md's "How this
   repo comes together") - so the DOM below is built once by
   initUnderTheSurface() in js/main.js and this file only owns static,
   scale-independent styling (colour, radius, typography, transitions).
   Anything that depends on the live responsive scale factor (position,
   width, the pin/card sizing) is a runtime-computed inline style, set
   by that same JS, for the same reason the prototype computed it
   inline rather than in CSS: the whole scene is one fixed-size canvas
   (1280x2320 desktop / up to 1000 wide mobile) scaled to fit via
   `transform: scale()`, recomputed on resize.

   This page's own "deep blue sea" palette is unique to this
   illustration - distinct from the site's --color-pickled-bluewood/
   -light navy pair, one shade of which (#2C3E50) it still reuses
   verbatim inside the sea gradient below the fixed pixel line where
   the prototype's own colours already matched. Same convention as
   Features' mock client UI carrying its own fictional palette (see
   CLAUDE.md's "The Features page's alternation" section) - scoped to
   this file, never promoted to a shared token.

   This is the design's "Deep blue" sky option (its other option,
   "Midnight", was this page's original near-black palette - replaced
   here since "Deep blue" is the currently-selected one, not exposed
   as a runtime toggle, same as the header's dropdown/chevron/hover
   variants earlier). */

:root {
  --uts-sky-top: #172536;
  --uts-sky-mid: #1F3044;
  --uts-wave: #2D4763;
  --uts-wave-deep: #172A42;
  --uts-bottom: #08132A;
  --uts-star: #E9EBED;
}

/* ---- Scrolled header, this page's own tint ----------------------------
   The shared .navbar (page.css) is already transparent at rest and a
   frosted slate tint once .is-scrolled - sitewide sticky positioning
   plus initStickyHeader() in main.js already gets the "float over the
   hero, go translucent on scroll" behaviour this page's own design
   asks for, so nothing structural changes here (the design's own
   version reaches for position: fixed + a manual spacer div instead,
   only because it isn't building on an existing sticky header the way
   this port is). This page's design just wants a different tint once
   scrolled - its own night colour instead of the sitewide slate, since
   slate would clash sitting over a night-sky background - so this
   overrides just the colour/blur/border, loading after page.css
   (this file's own <link> always comes last, see "Fonts and the local
   CSS stack") so it wins without extra specificity. */
.navbar.is-scrolled {
  background: rgba(13, 18, 24, 0.38);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  box-shadow: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .navbar.is-scrolled { background: var(--uts-sky-top); }
}

/* The footer normally fills with --surface-inverse (see .site-footer
   in page.css); here it goes transparent with just a hairline top
   border instead, same treatment as the header above, so the closing
   section's own --uts-bottom shows straight through instead of
   cutting to a different navy at the footer's edge. .page's own
   white background (the sitewide default, see page.css) would
   otherwise show through that transparency, so it's overridden here
   too - this page is the one place on the site where .page isn't
   white. */
.page { background: var(--uts-bottom); }
.site-footer { background: transparent; border-top: 1px solid rgba(255, 255, 255, 0.16); }

/* Locks background scroll while the item modal is open (see
   openModal()/closeModal() in main.js) - same convention as the
   sitewide mobile nav's own html.menu-open in page.css, kept as its
   own class rather than reusing that one so the two stay independent. */
html.uts-modal-open { overflow: hidden; }

/* ---- Intro + closing: plain centred copy on the night colour ---------
   Not the shared .hero component (no media, no two-column split) or
   .cta (a two-column image+card panel) - both assume layouts this
   design doesn't use, so these are their own small pattern. */
.uts-intro,
.uts-closing {
  display: flex;
  justify-content: center;
}
.uts-intro { position: relative; overflow: hidden; background: var(--uts-sky-top); padding: clamp(160px, 20vw, 240px) var(--page-padding) 8px; }
.uts-closing { background: var(--uts-bottom); padding: 24px var(--page-padding) 88px; }
/* A second, independent star field so the night sky already reads as
   continuous behind the hero copy, rather than starting only once the
   scene below has scrolled into view - see initUnderTheSurface() in
   main.js. Percentage-positioned (not the scene's fixed canvas-space
   px) since this section's height is ordinary content-driven layout,
   not a fixed-size scaled canvas. */
.uts-intro-stars { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.uts-intro__inner { position: relative; z-index: 1; }
.uts-intro__inner,
.uts-closing__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  text-align: center;
}
.uts-intro__inner { max-width: 820px; }
.uts-closing__inner { max-width: 720px; }
.uts-eyebrow {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.72);
}
.uts-intro__title,
.uts-closing__title {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 400;
  letter-spacing: -0.01em;
  color: #fff;
  text-wrap: balance;
}
.uts-intro__title { font-size: clamp(40px, 6vw, 64px); line-height: 1.1; }
.uts-closing__title { font-size: clamp(34px, 4.5vw, 48px); line-height: 1.15; }
.uts-intro__lead,
.uts-closing__lead {
  margin: 0;
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.8);
  text-wrap: pretty;
}
.uts-intro__lead { max-width: 600px; }
.uts-closing__actions { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }

/* ---- The scene itself --------------------------------------------- */
/* Positioning context for .uts-bub-layer below, which needs to sit as
   a sibling in front of .uts-scene rather than inside its scaled
   canvas (see the note on .uts-bub-layer). */
.uts-scene-wrap { position: relative; }
.uts-scene {
  position: relative;
  overflow: hidden;
  background: var(--uts-sky-top);
}
/* The fixed-size canvas everything else below lives inside, scaled to
   fit the scene's actual width - see measure()/layout() in main.js.
   transform-origin stays 0 0 (top-left) to match the JS's own math. */
.uts-canvas {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  background: linear-gradient(180deg, var(--uts-sky-top) 0px, var(--uts-sky-mid) 380px, var(--color-pickled-bluewood) 560px, var(--uts-wave) 561px, var(--uts-wave-deep) 1300px, var(--uts-bottom) 2320px);
}
.uts-stars { position: absolute; inset: 0 0 auto 0; height: 520px; }
.uts-star { position: absolute; border-radius: 50%; background: var(--uts-star); }
.uts-sun {
  position: absolute;
  left: calc(50% + 420px);
  top: 80px;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  background: var(--uts-star);
  box-shadow: 0 0 40px 10px rgba(233, 235, 237, 0.22), 0 0 140px 50px rgba(212, 216, 220, 0.08);
}
.uts-wave-back,
.uts-wave-front {
  position: absolute;
  left: 0;
  top: 500px;
  width: 100%;
  height: 120px;
  overflow: visible;
}
.uts-wave-front { pointer-events: none; }

/* The ice itself: one static SVG (exact facet colours/points from the
   design), bobbing gently via a JS-driven transform on this wrapper:
   see tick() in main.js. */
.uts-ice { position: absolute; left: calc(50% - 640px); top: 0; width: 1280px; height: 2320px; will-change: transform; }

/* Mouse/attention-follow spotlight - a dark scrim whose own visibility
   is cut out by a radial-gradient mask centred on --mx/--my (set by
   JS every frame), so the area right around the torch reads bright
   and everything else dims towards the mask's edge. */
.uts-torch {
  position: absolute;
  left: 0;
  right: 0;
  top: 620px;
  bottom: 0;
  background: linear-gradient(180deg, rgba(30, 52, 78, 0.3) 0px, rgba(8, 19, 42, 0.72) 700px, rgba(8, 19, 42, 0.96) 100%);
  -webkit-mask-image: radial-gradient(circle 300px at var(--mx, 50%) var(--my, 200px), rgba(0, 0, 0, 0.12) 0%, rgba(0, 0, 0, 0.2) 40%, #000 100%);
  mask-image: radial-gradient(circle 300px at var(--mx, 50%) var(--my, 200px), rgba(0, 0, 0, 0.12) 0%, rgba(0, 0, 0, 0.2) 40%, #000 100%);
}
.uts-rays { position: absolute; left: calc(50% - 640px); top: 620px; width: 1280px; height: 1100px; pointer-events: none; }
.uts-ray { position: absolute; top: -40px; transform-origin: top; background: linear-gradient(180deg, rgba(212, 216, 220, 0.09), rgba(212, 216, 220, 0)); }
/* Bubbles moved out of the scaled canvas and into their own real-
   pixel-space layer, a sibling placed in front of (after, in source
   order - painted on top of) .uts-scene rather than a descendant of
   its transformed .uts-canvas - see the note on .uts-scene-wrap and
   layoutBubbles()/tick() in main.js. Unlike everything else in the
   scene, bubbles no longer scale with the canvas's own zoom level, so
   they stay a constant real size/speed regardless of viewport width.
   The mask fades them in below the surface and out again near the
   very bottom, rather than a hard clip. top/height are set in
   main.js (depend on the live scale factor); everything else here is
   static. */
.uts-bub-layer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 4;
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 90px, #000 calc(100% - 560px), transparent calc(100% - 140px));
  mask-image: linear-gradient(180deg, transparent 0, #000 90px, #000 calc(100% - 560px), transparent calc(100% - 140px));
}
.uts-bubble { position: absolute; left: 0; top: 0; border-radius: 50%; border: 1px solid rgba(233, 235, 237, 0.35); pointer-events: none; }

/* A handful of short horizontal bars near the sun, widths/opacities
   animated in tick() - decorative, carried over from the design as-is. */
.uts-mast { position: absolute; left: calc(50% + 456px); top: 556px; width: 0; height: 90px; pointer-events: none; }
.uts-mast__bar { position: absolute; left: 0; height: 2px; border-radius: 2px; background: rgba(233, 235, 237, 0.55); }

/* Pinned cards (desktop) + pin dots (every width) + pills (mobile/
   tablet) all live in one wrapper - no longer bobbing in sync with
   .uts-ice (see tick() in main.js) or will-change: transform here to
   match. This is real content (every item's title/body), unlike the
   ice artwork it used to move with, and a continuously-transformed,
   GPU-promoted layer this large and complex (10 items' worth of cards/
   pins/pills, every frame) intermittently failed to rasterize a tile
   correctly on some Android devices - a pin or pill would occasionally
   render as a blank/wrong-coloured patch instead of its real content,
   different item each time, self-correcting on some reloads and not
   others (reported 26 Sep 2026). Not reproducible in any of this site's
   own testing (fresh loads, taps, simulated slow/jittery networks, all
   clean), consistent with a device-specific compositor bug rather than
   a logic error - removing the per-frame transform removes the
   likeliest trigger. prefers-reduced-motion already skipped this same
   transform with no ill effect, which is what made it safe to drop
   generally rather than just for that case. */
.uts-pins { position: absolute; left: calc(50% - 640px); top: 0; width: 1280px; height: 2320px; }

.uts-line { position: absolute; height: 0; border-top: 1px dashed rgba(233, 235, 237, 0.5); pointer-events: none; }

.uts-card {
  box-sizing: border-box;
  position: absolute;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px 18px;
  border-radius: 8px;
  background: var(--color-pickled-bluewood);
  border: 0;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
  cursor: pointer;
  color: #fff;
  font: inherit;
  transition: box-shadow 600ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.uts-card.is-active { box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); }
.uts-card__eyebrow { font-family: var(--font-ui); font-size: 14px; line-height: 1.4; color: rgba(255, 255, 255, 0.72); }
.uts-card__title { font-family: var(--font-heading); font-size: 24px; line-height: 1.2; color: #fff; }
.uts-card__body { margin: 0; font-family: var(--font-body); font-size: 16px; line-height: 1.45; color: rgba(255, 255, 255, 0.82); text-wrap: pretty; }

.uts-pin { position: absolute; padding: 0; border: 0; background: transparent; cursor: pointer; display: grid; place-items: center; }
.uts-pin__pulse { grid-area: 1 / 1; border-radius: 50%; background: rgba(233, 235, 237, 0.6); animation: utsPulse 2.4s ease-out infinite; }
.uts-pin__dot { grid-area: 1 / 1; border-radius: 50%; background: var(--uts-star); transition: box-shadow var(--duration-base) var(--ease-standard), background var(--duration-base) var(--ease-standard); }
.uts-pin.is-active .uts-pin__dot { background: #fff; }
@keyframes utsPulse {
  0% { transform: scale(1); opacity: 0.7; }
  100% { transform: scale(2.6); opacity: 0; }
}

.uts-pill-wrap { position: absolute; height: 0; display: flex; align-items: center; }
.uts-pill {
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: left;
  box-sizing: border-box;
  white-space: nowrap;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--color-pickled-bluewood);
  color: #fff;
  border: 0;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  cursor: pointer;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
  transition: background var(--duration-base) var(--ease-standard), color var(--duration-base) var(--ease-standard);
}
/* #0D1218 stays a fixed literal here rather than a --uts-* variable:
   the design keeps this specific colour fixed across both its sky
   options, unlike everything else in this file. */
.uts-pill.is-active { background: #fff; color: #0D1218; }

/* Desktop-only depth gauge and mobile/tablet-only bottom sheet - both
   fixed to the viewport (not the canvas), shown only while the scene
   is roughly centred in view (see onScroll() in main.js). */
/* Opacity is a continuous scroll-driven fade set directly by main.js
   (onScroll's fadeIn/fadeOut), not a class toggle - no transition here
   on purpose, the interpolation already comes from that math running
   on every scroll event. display is the only thing toggled in CSS-ish
   fashion (via layout() in main.js, desktop-only). */
.uts-gauge {
  position: fixed;
  right: 28px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
}
.uts-gauge__label { font-family: var(--font-ui); font-size: 13px; color: rgba(255, 255, 255, 0.72); }
.uts-gauge__track { position: relative; width: 2px; height: 220px; background: rgba(233, 235, 237, 0.25); }
.uts-gauge__dot { position: absolute; left: -5px; top: 0; width: 12px; height: 12px; margin-top: -6px; border-radius: 50%; background: #fff; }
.uts-gauge__depth { font-family: var(--font-ui); font-size: 14px; font-weight: 500; color: #fff; min-width: 64px; text-align: center; }

/* Opacity is a continuous scroll-driven fade set directly by main.js
   (onScroll's fadeIn/fadeOut), not a class toggle - no transition here
   on purpose: the interpolation already comes from that math running
   on every scroll event, so a CSS transition on top would only add
   lag against the finger/wheel. */

/* ---- Mobile/tablet: tap-to-open modal, not a fixed bottom sheet ----
   A fixed sheet that auto-tracked scroll position (fading in/out, then
   dropping into flow at the bottom) fought too many things at once -
   overlap with the CTA scrolling up underneath it, a card sometimes
   not rendering on load, and it was fragile to reason about generally.
   Replaced (26 Sep 2026, at the site owner's own request) with a plain
   tap-to-open modal: pills/pins stay exactly as before, but tapping one
   opens this dialog centred on the viewport with a dulling scrim behind
   it, showing that item's content in the same card styling. No scroll
   tracking, no fixed positioning, no fade timing to get right - it only
   ever changes in response to a tap (open, close, prev/next), see
   openModal()/closeModal()/stepModal() in main.js. */
/* [hidden] (not opacity/visibility) is what actually keeps these out
   of the page when closed - openModal()/closeModal() in main.js only
   ever remove/restore the attribute at the start/end of the fade, so
   it's what's on them at page load. This matters beyond "it works":
   initScrollReveal() (see components.css/main.js "Scroll reveal")
   skips revealing any heading inside a [hidden] ancestor, since a
   heading that starts hidden may never be seen as intersecting by its
   IntersectionObserver and would otherwise stay invisible forever the
   first time this modal opens - bit us once already, on .uts-modal__
   title specifically. Don't go back to opacity/visibility alone for
   the closed state. */
.uts-modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 20;
  /* Near-black rather than this page's own navy tones - the scene
     behind it is already dark, so a scrim in the same family of
     colour barely reads as "dimmed" against it; black at high opacity
     is what actually mutes it. */
  background: rgba(0, 0, 0, 0.82);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-standard);
}
.uts-modal-scrim.is-open { opacity: 1; }
.uts-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 21;
  box-sizing: border-box;
  width: min(560px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 24px;
  border-radius: 12px;
  background: var(--color-pickled-bluewood);
  border: 0;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.96);
  transition: opacity var(--duration-base) var(--ease-standard), transform var(--duration-base) var(--ease-standard);
}
.uts-modal.is-open {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
/* This page's own display:flex above beats the browser's default
   [hidden] { display: none } (an author rule always outranks a
   user-agent one, regardless of source order or specificity) - so
   [hidden] alone wouldn't actually hide it without this. */
.uts-modal[hidden] { display: none; }
.uts-modal__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.uts-modal__tag { font-family: var(--font-ui); font-size: 14px; color: rgba(255, 255, 255, 0.72); }
.uts-modal__close { width: 36px; height: 36px; padding: 0; border: 0; border-radius: 999px; background: rgba(255, 255, 255, 0.12); color: #fff; display: grid; place-items: center; cursor: pointer; }
.uts-modal__close svg { width: 20px; height: 20px; }
/* font-weight: 400 overrides the <h3>'s own default bold - nothing
   else on the site sets this heading typeface bold (see .uts-card__
   title, .uts-intro__title, etc., all plain <span>/<h1>/<h2> with no
   UA-default weight to override, so this was the one heading-font
   spot on the page that still needed it stated explicitly). */
.uts-modal__title { margin: 0; font-family: var(--font-heading); font-weight: 400; font-size: 22px; line-height: 1.2; color: #fff; }
.uts-modal__body { margin: 0; font-family: var(--font-body); font-size: 15px; line-height: 1.45; color: rgba(255, 255, 255, 0.82); }
.uts-modal__nav { display: flex; gap: 8px; margin-top: 8px; }
.uts-modal__btn { width: 44px; height: 44px; border-radius: 999px; border: 1px solid rgba(233, 235, 237, 0.35); background: transparent; color: #fff; display: grid; place-items: center; cursor: pointer; }
.uts-modal__btn .icon { width: 18px; height: 18px; }
/* Faded at either end of the list (see updateModal() in main.js) -
   real :disabled, not just a class, so this comes for free rather
   than needing its own selector. */
.uts-modal__btn:disabled { opacity: 0.35; cursor: default; }

/* Only the mode the scene is currently in gets shown - both layers
   exist in the DOM at all times (see main.js), toggled by a single
   attribute on the scene root rather than adding/removing nodes on
   every resize across the 1100px breakpoint. */
.uts-scene[data-mode="mobile"] .uts-cards-layer { display: none; }
.uts-scene[data-mode="desktop"] .uts-pills-layer { display: none; }
/* .uts-gauge lives outside .uts-scene on purpose (it's position: fixed,
   and a transformed ancestor - .uts-canvas's own scale() - would
   otherwise turn that into fixed-to-the-canvas instead of
   fixed-to-the-viewport), so a descendant selector can't reach it for
   the desktop/mobile split. Not needed anyway: layout() in main.js
   already toggles its display directly, desktop-only, and its own
   opacity is the continuous scroll-driven fade above. */

/* No-JS fallback - the scene above is built entirely by JS (see the
   note at the top of this file), so without it there is nothing in
   the accessibility tree at all. Real users of screen readers almost
   always run with JS enabled (VoiceOver/NVDA/JAWS on a modern browser
   render the JS-built cards, which are real <button><h3><p> content,
   same as any other page's cards) - this covers the rare case where
   it's actually off, same idiom as Animate.css's own <noscript> tag
   in every page's <head>. */
.uts-fallback { background: var(--uts-bottom); padding: var(--section-padding-lg) var(--page-padding); display: flex; justify-content: center; }
.uts-fallback__inner { width: 100%; max-width: var(--container-lg); display: flex; flex-direction: column; gap: 32px; }
.uts-fallback__item { display: flex; flex-direction: column; gap: 6px; padding-bottom: 32px; border-bottom: 1px solid rgba(233, 235, 237, 0.2); }
.uts-fallback__item:last-child { border-bottom: 0; padding-bottom: 0; }
.uts-fallback__eyebrow { font-family: var(--font-ui); font-size: 14px; color: rgba(255, 255, 255, 0.72); }
.uts-fallback__item h3 { margin: 0; font-family: var(--font-heading); font-weight: 400; font-size: 24px; line-height: 1.2; color: #fff; }
.uts-fallback__item p { margin: 0; font-family: var(--font-body); font-size: 16px; line-height: 1.5; color: rgba(255, 255, 255, 0.82); }

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

/* Mobile/tablet only (matches the JS mobile breakpoint, see measure()
   in main.js): one more continuous, infinite-loop animation dropped
   for the same reason .uts-pins lost its bobbing transform above -
   every pin's pulse ring was animating (scale + opacity) at once,
   every frame, inside that same large composited layer. Static rings
   still show at their resting state; only the looping animation
   itself is gone here. */
@media (max-width: 1240px) {
  .uts-pin__pulse { animation: none; }
}

@media (max-width: 640px) {
  .uts-eyebrow { font-size: 15px; }
}
