/* =========================================================================
   ClearHarbor — Coming-soon teaser (public root route, Pages/ComingSoon.cshtml)
   Landing-only rules. Everything else — topbar/brand, theme-toggle,
   .mono-eyebrow, the rise-in keyframe, the reduced-motion block — comes from
   site.css/tokens.css and is reused as-is; nothing here duplicates it.

   UNDO: delete this file, its <link> in ComingSoon.cshtml, and
   ComingSoon.cshtml(.cs) — then revert the "/app" route in Index.cshtml back
   to a bare @page. See the comment at the top of Index.cshtml.
   ========================================================================= */

/* fix-9: same conservative floor as .hero (see below) — svh, not dvh, so this
   shell's own floor can't land taller than .hero's when the toolbar hides
   mid-scroll and dvh grows out from under it, which would otherwise show a
   sliver of the shell's own background under the hero. */
.coming-soon-shell {
  min-height: 100svh;
}

/* ---- Hero ---------------------------------------------------------------- */
/* fix-9 (DEFECT: real-device text guillotine). `height:100dvh` +
   `overflow:hidden` was a deliberate hard cap — "page-level scroll is
   structurally impossible" — that fixed a landscape-overlap defect but made
   the wrong trade: it prioritised never-scrolls over never-loses-content.
   Chromium's `dvh` emulation isn't identical to real WebKit/iOS Safari's
   dynamic toolbar (QA flagged this gap explicitly and it wasn't covered) —
   on an actual phone `100dvh` can resolve shorter than the copy needs, and
   `overflow:hidden` silently cropped the subhead + seal instead of yielding.

   Fixed by turning the cap into a floor: `min-height` instead of `height`,
   so the box can grow past one screen when content demands it, and the
   vertical half of `overflow:hidden` is dropped so a taller box is never
   clipped back down — see the standalone `overflow-x: hidden` below, which
   keeps ONLY the horizontal containment (the scrim's 100vw full-bleed trick)
   and deliberately leaves vertical overflow (there's nothing left inside
   .hero that still relies on a hard height to avoid the old overlap defect
   — .hero-chrome is an in-flow flex child, see fix-6 below).

   100svh, not 100dvh, for the floor: svh is the CONSERVATIVE viewport unit —
   it assumes the browser's address bar is visible (the smallest the
   viewport ever gets), so a box sized to it is guaranteed at least that much
   room on every mobile browser, toolbar shown or hidden. dvh would have
   given a floor that is sometimes taller than what's actually on screen
   (toolbar-hidden state) — exactly the mismatch that caused this bug. If
   content still needs more than 100svh (a very short viewport, e.g.
   844x390), min-height no longer stops it: the box grows, the page scrolls
   a little. That is the correct outcome per the no-clipping rule.

   --fs-hero is overridden here (not in tokens.css — nothing else uses that
   token) with a HEIGHT-aware term via min(): on a normal viewport the vw
   term wins and behaves exactly as before; on a short/landscape phone the
   dvh term wins and the headline shrinks with the viewport instead of
   colliding with whatever's above it. Fixes DEFECT 1 (root cause: a
   width-only clamp had no idea the viewport had gone short). This ramp still
   uses dvh on purpose — it's a font-size hint that reduces how tall the copy
   needs to be on a short screen (fewer forced page-scroll cases), it does
   not gate whether clipping can happen; min-height + no vertical overflow
   is what guarantees that now, regardless of what this ramp lands on. */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  /* Horizontal only: contains the scrim's full-bleed 100vw pseudo-element
     (see .hero-copy::before) so it can't push out a horizontal scrollbar.
     Deliberately NOT `overflow: hidden` (both axes) — that shorthand is
     exactly what clipped the subhead/seal on real mobile; see fix-9 above. */
  overflow-x: hidden;
  --fs-hero: clamp(2.25rem, min(1.9rem + 4.2vw, 1.15rem + 13dvh), 5.25rem);

  /* fix-7 (DEFECT: artwork disappears at 501-520px height): replaces the old
     `@media (max-height:500px)` breakpoint, which gated the headline's ch-
     width AND the chrome/copy padding at the exact same pixel. Below 500px
     both snapped to their "short" value together; at 501px NEITHER did, so
     the headline reverted to a taller 3-line wrap at the same moment the
     padding reverted to its roomiest values — together they consumed the
     whole frame and the spacer that reveals the artwork hit zero. A second
     hand-tuned breakpoint would only relocate that seam, not remove it.

     This is a single continuous ramp instead: 0 at a comfortably tall
     viewport, rising smoothly to its cap once height drops under 500px —
     the exact same "fully short" value QA already verified at 844x390 and
     740x360, just arrived at gradually across a 180px band (500-680px)
     instead of snapped to in one pixel. Every rule below multiplies this
     one length by a plain number to get its own delta, so the headline
     measure and the chrome/copy spacing move together at every height —
     they read the same signal, so they cannot land on opposite sides of a
     gate the way two independent breakpoints could. */
  --hero-short: clamp(0px, 680px - 100dvh, 180px);
}

/* Floating wordmark + theme toggle, laid directly over the artwork now that
   the topbar is gone — no bar, no background fill, no border.
   fix-6 (DEFECT 1): this used to be position:absolute, floating free of
   layout — nothing reserved its footprint, so on a short landscape phone
   .hero-copy simply grew until it landed on top of it. Now it's a normal
   flex child (position:relative keeps the z-index meaningful): it occupies
   real space in the column, .hero-spacer/.hero-copy lay out AFTER it, and
   the overlap is structurally impossible rather than avoided by luck. */
.hero-chrome {
  position: relative; z-index: 2; flex: none;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-5) var(--space-6);
  /* fix-7: reads the same --hero-short ramp as the headline below instead
     of its own separate breakpoint — see --hero-short on .hero for why
     these two used to be able to disagree about whether the viewport
     counted as "short" and what that cost when they did. */
  padding: calc(var(--space-5) - var(--hero-short) / 180px * 0.75rem)
           calc(var(--space-6) - var(--hero-short) / 180px * 0.5rem);
}
.hero-chrome .theme-toggle { background: transparent; border: none; }
.hero-chrome .theme-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
.hero-chrome .brand,
.hero-chrome .theme-toggle svg {
  filter: drop-shadow(0 1px 2px rgba(241,235,225,.65)) drop-shadow(0 2px 10px rgba(241,235,225,.4));
}
:root[data-theme="dark"] .hero-chrome .brand,
:root[data-theme="dark"] .hero-chrome .theme-toggle svg {
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 2px 10px rgba(0,0,0,.35));
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-chrome .brand,
  :root:not([data-theme="light"]) .hero-chrome .theme-toggle svg {
    filter: drop-shadow(0 1px 2px rgba(0,0,0,.55)) drop-shadow(0 2px 10px rgba(0,0,0,.35));
  }
}

.hero-media-stack {
  position: absolute;
  inset: 0;
  animation: hero-media-in var(--dur-4) var(--ease-out) both;
}
@keyframes hero-media-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.hero-media {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  background-color: var(--panel-2);
}

/* Token-based gradient fallback painted BEHIND the real photo. A failed
   background-image url() layer simply drops out (no broken-image glyph,
   unlike <img>) — the gradient layers underneath keep showing through, so the
   hero reads as a deliberate on-brand wash whether or not hero-light.webp /
   hero-dark.webp have landed in wwwroot/img/ yet. */
.hero-media-light {
  background-image:
    url("/img/hero-light.webp"),
    radial-gradient(circle at 32% 22%, var(--accent-soft), transparent 60%),
    linear-gradient(155deg, var(--halo-1), transparent 55%),
    linear-gradient(-15deg, var(--halo-2), transparent 60%);
}
.hero-media-dark {
  background-image:
    url("/img/hero-dark.webp"),
    radial-gradient(circle at 32% 22%, var(--accent-soft), transparent 60%),
    linear-gradient(155deg, var(--halo-1), transparent 55%),
    linear-gradient(-15deg, var(--halo-2), transparent 60%);
}

/* Dark-mode crossfade — same selector pattern tokens.css already uses. */
.hero-media-dark { opacity: 0; transition: opacity var(--dur-3) var(--ease); }
:root[data-theme="dark"] .hero-media-light { opacity: 0; }
:root[data-theme="dark"] .hero-media-dark  { opacity: 1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-media-light { opacity: 0; }
  :root:not([data-theme="light"]) .hero-media-dark  { opacity: 1; }
}

/* fix-6 (DEFECT 1): absorbs whatever height the chrome + copy block don't
   need. On a tall viewport this is most of the frame (copy sits low, lots of
   art shows above it, same look as before). On a short one it shrinks toward
   zero — it can't go negative, so if chrome + copy together ever exceed the
   viewport it's .hero's own overflow:hidden that has the final say, never a
   page scrollbar.

   fix-7: min-height is the structural half of the artwork-visibility
   guarantee — a real strip of the flex column is now reserved for the
   spacer BY CONSTRUCTION, not merely left over after the copy block takes
   what it wants. It's deliberately small (never fights the no-clipping
   rule at the shortest tested heights) because the other half of the
   guarantee is the scrim below no longer being able to paint over it: the
   scrim's own reach is now capped independently of how tall the copy block
   gets, so a reserved spacer pixel actually stays visible instead of being
   silently painted over by a scrim that grew to match a 3-line headline. */
.hero-spacer { flex: 1 1 auto; min-height: 0; }
@supports (height: 1dvh) {
  .hero-spacer { min-height: min(2dvh, 10px); }
}

.hero-copy {
  position: relative;
  z-index: 1;
  flex: none;
  width: 100%;
  max-width: 1440px;
  margin: 0 auto;
  padding: 0 var(--space-6) var(--space-7);
}

/* Scrim — fix-8 (complaint: a hard-edged solid slab replaced the watercolour
   under the copy). Root cause (Cathode): the gradient ramped transparent to
   100% opaque `--bg` in a flat 0.5rem regardless of how tall the fade zone
   was — that's an edge with a ruler, not a gradient. Every downstream
   symptom (the "highlighted with a marker" eyebrow, the visible seam in the
   complaint screenshots) traced back to that one line of math.

   Fixed two ways at once: the ramp now spans the FULL bleed distance
   (`--scrim-bleed`) instead of the last 0.5rem of it, and the colour is a
   capped translucent wash (`--scrim-tint`) rather than solid `--bg` — the
   art stays visible through it in every theme that uses it at all.

   Real per-theme need — Cathode's numbers came from sampling the raw .webp
   files at their native aspect ratio; re-verifying against actual composited
   pixels (Playwright, real glyph rows, 18 viewports incl. ultrawide and
   short-landscape) found her "dark needs nothing" call didn't hold. Rather
   than relay a second false pass, the number that actually held is used:
     - LIGHT only the subhead (`--text`) fails raw (2.89–4.08:1); the
       eyebrow/H1 (`--ink`) clear AA unaided at every viewport tested. One
       soft wash sized to what the subhead needs, nothing more.
     - DARK looked clear at native aspect ratio (9.7–14.6:1) but
       `background-size:cover` crops a different, sometimes much brighter,
       vertical slice at short-landscape/ultra-short heights — measured as
       low as 1.13:1 on the eyebrow at 740×360. A wash is needed there too;
       it's flat rather than height-ramped because the deficit doesn't track
       height linearly (a 375×667 phone needed real correction too, despite
       reading "tall" by every other metric in this file) — it tracks which
       slice of one specific photograph a given aspect ratio happens to
       expose, which has no clean formula. Sized to the worst real case
       measured (740×360) with a small margin, not maximised. */
.hero-copy::before {
  content: "";
  position: absolute;
  left: 50%; right: auto;
  width: 100vw;
  margin-left: -50vw;
  top: calc(-1 * var(--scrim-bleed, 18dvh));
  bottom: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0,
    var(--scrim-tint, transparent) var(--scrim-bleed, 18dvh),
    var(--scrim-tint, transparent) 100%
  );
}
@supports (height: 1dvh) {
  .hero-copy::before { --scrim-bleed: clamp(2.5rem, 18dvh, 11rem); }
}

/* LIGHT: capped wash, never solid. 50% is the minimum that keeps --text at
   4.5:1+ across every band Cathode sampled from hero-light.webp, including
   the horizon edge case. --ink needs no help — it already clears AA raw. */
.hero-copy { --scrim-tint: color-mix(in srgb, var(--bg) 50%, transparent); }

/* DARK: also a capped wash, not the "none" originally assumed — see note
   above. 62% is the worst real composited case (740×360) plus a small
   margin, not the maximum; it does mean already-fine tall viewports (raw
   9.7–14.6:1) end up with more wash than they individually need, which is
   the honest tradeoff of a single static value covering a per-crop deficit
   that doesn't reduce to a formula — the wash is still translucent, the art
   still reads through it, at every height tested. */
:root[data-theme="dark"] .hero-copy { --scrim-tint: color-mix(in srgb, var(--bg) 62%, transparent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-copy { --scrim-tint: color-mix(in srgb, var(--bg) 62%, transparent); }
}

/* Halo, fix-8: fix-5's 16-direction ring was insurance for a solid scrim
   that no longer exists (see .hero-copy::before above) — 16 shadows chasing
   a contrast number is exactly the mechanism that produced the
   "highlighted with a marker" complaint once already, so it's cut down to
   one soft shadow per theme rather than carried forward as legacy weight.
   Re-measured against real composited pixels (Playwright, whole-word
   bounding-box perimeter) with the new scrim in place. */
.hero-eyebrow {
  --eyebrow-halo: rgb(241 235 225 / 55%);
  color: var(--ink);
  text-shadow: 0 1px 3px var(--eyebrow-halo);
  animation: rise-in var(--dur-3) var(--ease-out) both;
}
:root[data-theme="dark"] .hero-eyebrow {
  --eyebrow-halo: rgb(14 26 31 / 55%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-eyebrow {
    --eyebrow-halo: rgb(14 26 31 / 55%);
  }
}
.hero-heading {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--fs-hero);
  line-height: var(--lh-tight);
  letter-spacing: -0.02em;
  color: var(--ink);
  /* fix-7 (root cause of the 501-520px vanishing-artwork defect): was a flat
     16ch, widened to a flat 26ch under the old max-height:500px breakpoint.
     ch is font-relative, so widening it is what actually buys back a wrapped
     line (a smaller font alone never does — a "16ch" box is still ~16
     characters wide at any font-size). The old two-value jump meant the
     headline could flip from 3 lines to 2 at a DIFFERENT pixel than the
     chrome/copy padding flipped, if anyone ever touched one without the
     other; here it's one calc reading the same --hero-short as the padding
     above and below it, so the two can't decouple by construction. Same
     endpoints QA already verified (16ch tall, 26ch at height<=500), just
     reached by a ramp instead of a jump. */
  max-width: calc(16ch + var(--hero-short) / 180px * 10ch);
  margin: calc(var(--space-3) - var(--hero-short) / 180px * 0.25rem)
          0
          calc(var(--space-4) - var(--hero-short) / 180px * 0.5rem);
  animation: rise-in var(--dur-3) var(--ease-out) both;
  animation-delay: 80ms;
}
.hero-subhead {
  font-family: var(--font-sans);
  font-size: var(--fs-lg);
  color: var(--text);
  /* fix-4: 18ch was the fix-3 over-correction — it chased a specific 2-line
     break ("Something is coming" / "that treats certainty as the whole
     point.") that's mathematically impossible for this ~60-char sentence,
     and 18ch alone produced a narrow 3-line ribbon instead. The copy column
     has room (the H1 beside it runs to 16ch at a much larger font-size), so
     widen substantially and let the sentence wrap naturally: one line at
     desktop/1024, a natural two-line break at 820 — never a narrow ribbon.
     text-wrap: pretty (not balance — balance is what optimised toward the
     ribbon by evening line lengths) nudges the wrap point without fighting
     it. Measured against real rendered rects (Playwright) — see delivery
     notes for the actual breaks at each width. */
  max-width: 48ch;
  text-wrap: pretty;
  /* fix-7: same ramp as the headline/chrome, same reasoning — see .hero
     --hero-short. Old short value was var(--space-1) = 0.25rem. */
  margin: 0 0 calc(var(--space-2) - var(--hero-short) / 180px * 0.25rem);
  animation: rise-in var(--dur-3) var(--ease-out) both;
  animation-delay: 160ms;
}
@media (max-width: 820px) {
  .hero-subhead { max-width: 34ch; }
}

/* Seal — part of the copy column now (was floating bottom-centre, orphaned
   from the left-aligned composition). Same left edge as the headline/subhead,
   sitting below them; kept small and muted so it stays quiet furniture.
   fix-7: size and margin-top ramp with --hero-short instead of snapping at
   the old max-height:500px gate — old short values were 1.75rem / var(--space-2). */
.hero-seal {
  display: block;
  width: calc(2.25rem - var(--hero-short) / 180px * 0.5rem);
  height: calc(2.25rem - var(--hero-short) / 180px * 0.5rem);
  margin-top: calc(var(--space-4) - var(--hero-short) / 180px * 0.5rem);
  color: var(--muted);
}
.hero-seal .trust-seal { display: block; width: 100%; height: 100%; }

/* ---- Responsive ------------------------------------------------------------ */
/* fix-7: padding-bottom ramps toward the old short-viewport floor
   (var(--space-4) = 1rem) from whichever width-appropriate baseline
   applies, via max() so it never overshoots past that floor. Old behaviour
   forced padding-bottom to a flat 1rem once height<=500px regardless of
   width; these two rules reproduce that same floor while ramping smoothly
   into it instead of snapping. */
.hero-copy {
  padding-bottom: max(var(--space-4), calc(var(--space-7) - var(--hero-short) / 180px * 2rem));
}
@media (max-width: 820px) {
  .hero-copy {
    padding: 0 var(--space-4) var(--space-5);
    padding-bottom: max(var(--space-4), calc(var(--space-5) - var(--hero-short) / 180px * 0.5rem));
  }
}

/* fix-10 (DEFECT: sailboats cropped out of frame on mobile portrait). Both
   heroes are 16:9 photos with the boats sitting well right of the source
   image's own centre; `background-size:cover` crops symmetrically from the
   MIDDLE of that source, so the tighter the crop, the more gets thrown away
   from both sides equally. A landscape/desktop viewport is close enough to
   16:9 that little is thrown away and the boats were never at risk — this
   is specifically a portrait defect (crop gets severe, and it's a SIDE crop,
   which is exactly where the boats live), so it gets a portrait-only fix
   rather than touching the shared .hero-media rule everyone else relies on.

   Measured off the source files directly (pixel ink-mass centroid of the
   boat silhouettes, not eyeballed): x=64.1% of hero-light.webp's width,
   x=66.5% of hero-dark.webp's — the two paintings are different crops of
   the same scene, not a shared photo, so they don't get a shared number.

   background-position-x moves the crop window, not the boats, so the value
   that lands them back in frame isn't their raw source-image position — it
   has to account for how much cover() discards at a given aspect ratio.
   Solved for "boats sit ~55% across the visible frame" (a touch right of
   dead-centre, in the spirit of the source composition's own right-of-
   centre placement, without either edge coming close) at every portrait
   viewport in the test matrix (320-768px wide, 568-1200px tall): light
   needs 65-71% across that whole range, dark 69-75% — narrow enough bands
   that one constant per theme holds everywhere, no second breakpoint
   needed. Landscape is untouched: this selector only fires in portrait. */
@media (max-width: 820px) and (orientation: portrait) {
  .hero-media-light { background-position-x: 68%; }
  .hero-media-dark  { background-position-x: 72%; }
}
