/* ==========================================================================
   NetrAI — Cross-cutting refinements
   --------------------------------------------------------------------------
   Fixes that belong to no single component and have to outrank whatever set
   the value originally, so this file loads LAST on every page.

   It exists because these rules were first parked in css/pages/how-it-works.css,
   which only index.html links — so the Work page fixes below did nothing at
   all until the split. If you add a rule here, it applies site-wide; if it
   belongs to one component, put it in that component's file instead.
   ========================================================================== */

/* ==========================================================================
   Two fixes that have nothing to do with this component, parked here so the
   stylesheet that loads last owns them.
   ========================================================================== */

/* ---- 1. THE MOBILE DASHBOARD WAS DEAD ----------------------------------
   css/hero-scene.css hides the dashboard's pointer events until the scroll
   scene has faded it in:

     .hero-scene .hero-visual            { pointer-events: none; }
     .hero-scene.hero-ui-live .hero-visual { pointer-events: auto; }

   `.hero-ui-live` is written by js/hero-scene.js from the scene's progress
   value — and the scene does not run below 861px or under reduced motion, so
   that class is never added and the class that grants the clicks never
   matches. The panel rendered, looked live, and swallowed every tap.

   Where the scene does not run, the guard has nothing to guard. */
@media (max-width: 860px), (prefers-reduced-motion: reduce) {
  .hero-scene .hero-visual,
  .hero-scene .hero-visual * { pointer-events: auto; }
}

/* ---- 2. MISSION / VISION / VALUES ICONS -------------------------------- */
.mvv-skew-icon { margin-inline: auto; }
.mvv-skew-body { text-align: center; }
.mvv-skew-body p { text-align: center; }


/* ==========================================================================
   Two removals, parked in the file that loads last so nothing outranks them.
   ========================================================================== */

/* ---- 1. THE CHAPTER RAIL ------------------------------------------------
   The column of dots down the right-hand edge that tracked which chapter you
   were in. css/pages/story.css builds it and js/effects/scroll-highlight.js drives it; the
   markup is injected, so hiding it here is the whole removal — delete these
   two rules and it comes back with its behaviour intact.

   `.scroll-cue` goes with it: it is the same system's "keep scrolling" hint
   and reads as an orphan once the rail it belonged to is gone. */
.chapter-rail,
.scroll-cue { display: none !important; }

/* ---- 2. REVIEW ATTRIBUTION ---------------------------------------------
   Name only. The role and company lines are out of the markup on all sixteen
   cards; these two rules are belt and braces in case a card is added later
   from the old pattern. */
.review-role,
.review-company { display: none; }


/* ==========================================================================
   THE HERO DASHBOARD ON A PHONE
   --------------------------------------------------------------------------
   Measured at 390px before this block: the card is 292px tall and its
   contents came to roughly 315px — head 18, thread 150, chips 46, composer
   44, plus 38 of padding. The overflow lands on the composer, which is the
   last child, so "Ask about your data" was sliced in half by the card's
   bottom edge. Above it the tab row ran to x=530 inside a 310px box and the
   chip row to x=603, both cut mid-word with nothing to say they scrolled.
   Together that is what read as everything overlaying everything.

   Nothing here changes the dashboard above 700px.
   ========================================================================== */
@media (max-width: 700px) {

  /* ---- 1. Give the panel room ------------------------------------------
     34em at the panel's 10px base is 340px, and the content needs more than
     that once the composer is counted. This is the cheapest fix for the
     clipping and it also stops the layout feeling cramped. */
  .video-frame:has(.nd) { height: 39em; }

  /* ---- 2. Tighter chrome so the composer fits ---------------------------
     The card's padding was 1.35em/1.45em — nearly 40px of the 292 spent on
     margins inside a panel that is already inset twice. */
  .nd-card { padding: 1em 1.05em; }
  .nd-body { padding-top: 0.6em; }

  /* THE THREAD IS THE ONLY THING ALLOWED TO GIVE WAY, AND IT IS CLAMPED IN
     PIXELS, NOT PER CENT.

     Measured at 390px: the card's content box is 258px and its four children
     plus gaps came to 297. A percentage max-height did not help — it resolves
     against a height the flex chain never made definite, so the thread stayed
     at its content size of 150px and the 39px of overflow fell on the last
     child, which is the composer. Hence the sliced input.

     108px is what is left once the head (18), the chips (46), the composer
     (56) and three 0.5em gaps have taken theirs. The thread scrolls inside
     it, which is what a chat thread is supposed to do. */
  .nd-card.nd-prompt { gap: 0.5em; }
  .nd-thread {
    flex: 0 1 auto;
    min-height: 0;
    max-height: 96px;
    overflow-y: auto;
    justify-content: flex-start;
  }

  /* The composer is the element that was being clipped. It must never be the
     one that gives way. */
  .nd-composer { flex: 0 0 auto; }

  /* ---- 3. The two rows that scroll, made to look like they scroll -------
     Both were already overflowing; neither said so. A mask fading the last
     18px is the whole affordance — the row now ends in a soft edge that
     reads as "there is more", instead of a hard cut through the middle of
     the word "Sources".

     `scroll-padding-inline` keeps a snapped item clear of that fade rather
     than landing underneath it. */
  .nd-nav,
  .nd-chips {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    scroll-padding-inline: 0 22px;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
  }
  .nd-nav::-webkit-scrollbar,
  .nd-chips::-webkit-scrollbar { display: none; }
  .nd-nav button,
  .nd-chip {
    scroll-snap-align: start;
    flex: 0 0 auto;
    white-space: nowrap;
  }
  /* Tighter tabs so more than one is visible before the fade. At the shipped
     0.5em/0.7em only "Prompt" fitted in a 310px row, which made a scroller
     look like a single dead tab. */
  /* `width: 100%` comes from the sidebar layout this nav has at desktop,
     where the tabs are a vertical column. In a horizontal scroller it makes
     every button as wide as the whole row, so only the first one was ever on
     screen and a five-tab scroller looked like one dead tab. */
  .nd-nav button {
    width: auto;
    padding: 0.5em 0.55em;
    font-size: 0.92em;
    gap: 0.45em;
  }
  .nd-nav button svg { width: 1em; height: 1em; }
  /* The row is inside a card with its own padding, so the last item stops
     short of the fade with nothing after it; this restores the visual
     end-stop once you have scrolled to it. */
  .nd-nav::after,
  .nd-chips::after { content: ""; flex: 0 0 10px; }

  /* ---- 4. The empty middle ---------------------------------------------
     The prompt view's card head and its one message sat at the top of a
     296px stage with everything else pinned to the bottom. Closing the gap
     is a matter of letting the stack sit together rather than spreading. */
  .nd-card.nd-prompt { justify-content: flex-start; gap: 0.7em; }
  .nd-card-head { flex: 0 0 auto; }
}


/* ==========================================================================
   WORK PAGE SCREENSHOTS ON A PHONE
   --------------------------------------------------------------------------
   A REGRESSION I INTRODUCED, AND THE FIX.

   The case-study screenshots sit in horizontal scroll rails so a 1048px-wide
   dashboard capture can be panned rather than shrunk to illegibility. That
   rail is built like this in css/components/casestudies.css:

     .shot-scroll .shot-frame     { width: max-content; max-width: none; }
     .shot-scroll .shot-frame img { width: auto; height: auto; max-width: none; }

   `max-content` means the frame takes the image's INTRINSIC width. When the
   screenshots were regenerated at 2x for retina sharpness, that intrinsic
   width doubled — so on a 390px phone the w1048 frame measured 2098px and the
   w887 frame 1776px. Every rail image was displayed at twice its intended
   size. That is the "zoomed up" you saw, and it was my doing.

   Two changes. The image goes back to filling its frame rather than dictating
   its width, and the frame is capped at 168vw — a little under two screens of
   panning instead of the two-and-a-half a 1048px frame needs at 390px. The 2x
   sources mean shrinking costs nothing: a w1048 frame drawn at 655px still
   has 2096 real pixels behind it, which is over 3x density on a 2x screen.
   ========================================================================== */
@media (max-width: 700px) {
  .shot-scroll .shot-frame {
    max-width: 168vw;
  }
  .shot-scroll .shot-frame img {
    width: 100%;
    height: auto;
    max-width: 100%;
  }

  /* The rail needs to say it scrolls, same reasoning as the dashboard's tab
     row: without an edge fade a cropped screenshot reads as a broken image
     rather than as a panel you can push. */
  .shot-scroll {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
    scroll-padding-inline: 0 20px;
  }

  /* Portrait phone captures were the opposite problem — a 640x1353 screen at
     full column width is a very tall object to scroll past. Held to roughly
     one screen so the page keeps moving. */
  .shot-frame:not(.shot-scroll .shot-frame) img { max-height: 74svh; object-fit: contain; }
  .shot-duo { gap: 12px; }
}


/* ==========================================================================
   PASS 26 — alignment, sizing and mobile fixes
   --------------------------------------------------------------------------
   Everything below is a correction to a rule that already exists elsewhere.
   Each block says which rule it is correcting and why, so the next person
   knows whether to fix the source or keep the override.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. About hero — the inline scene

   Replaces `.ill-scene--hero`, whose raster was composed off-centre. Same
   footprint and the same distance from the lead; the geometry now lives in
   the SVG, so it is centred by construction rather than by hoping the
   bitmap was.
   -------------------------------------------------------------------------- */
.ill-hero-scene {
  width: 100%;
  max-width: 720px;
  margin: 38px auto 0;
}

.ill-hero-scene .ill-eye {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  filter: drop-shadow(0 24px 48px rgba(0, 0, 0, 0.14));
}

[data-theme="dark"] .ill-hero-scene .ill-eye {
  filter: drop-shadow(0 24px 54px rgba(0, 0, 0, 0.45));
}

/* The scene sits on the hero's own gradient field, so it takes a frosted
   stage rather than the flat --surface-elevated-wash the card-bound scenes
   use — a solid rectangle on a moving gradient reads as a hole in it. */
.ill-hero-scene .ill-eye > rect:first-of-type {
  fill: color-mix(in srgb, var(--surface-white) 62%, transparent);
}

/* The chart line draws in once and stops. Self-contained rather than borrowing
   illustration.css's [data-ill-draw], which is gated on `.reveal.in-view`. */
.ill-eye-line {
  stroke-dasharray: 340;
  stroke-dashoffset: 340;
  animation: ill-eye-draw 1000ms cubic-bezier(0.22, 1, 0.36, 1) 420ms forwards;
}

.ill-eye-dot {
  opacity: 0;
  animation: ill-eye-pop 380ms cubic-bezier(0.22, 1, 0.36, 1) 1300ms forwards;
}

@keyframes ill-eye-draw { to { stroke-dashoffset: 0; } }
@keyframes ill-eye-pop  { to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .ill-eye-line { stroke-dashoffset: 0; animation: none; }
  .ill-eye-dot  { opacity: 1; animation: none; }
}

/* The dashes are structure, not decoration — at --ill-line they disappeared
   against the frosted stage on the light theme. */
.ill-eye-wires { stroke: color-mix(in srgb, var(--ill-label-dim) 46%, transparent); }

@media (max-width: 700px) {
  .ill-hero-scene { margin-top: 26px; }
}


/* --------------------------------------------------------------------------
   2. Services — card icons, heading scale, and row uniformity

   Three separate faults, one section.

   a) `.service-card-top` is `justify-content: space-between`, which parks the
      icon hard left and the tag hard right inside a card that center.css
      centres everything else in. The icon was the only object on the page
      not on the card's axis.

   b) style.css sizes the featured heading as `.service-featured h3`, but the
      markup is `h2` — every service name fell through to the global h2 and
      rendered at display size, three times the intended 1.4rem.

   c) The grid cards each have a different amount of copy, so the "What's
      included" toggle, the outcome line and the pricing link landed at a
      different height in every card. The cards are the same height (grid
      stretch); it was the contents that were not aligned.
   -------------------------------------------------------------------------- */

/* (a) The icon and its tag stack on the card's centre line. */
.service-card-top {
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 14px;
  margin-bottom: 18px;
}

.service-card-top .card-icon { margin: 0; }

/* (b) Real heading sizes, addressed to the element that is actually there. */
.service-card > h2,
.service-card > h3 {
  font-family: var(--font-display);
  font-size: 1.12rem;
  line-height: 1.3;
  font-weight: 600;
  letter-spacing: -0.014em;
  margin: 0 0 10px;
  text-wrap: balance;
}

.service-featured > h2,
.service-featured > h3 {
  font-size: 1.34rem;
  line-height: 1.24;
  margin-bottom: 12px;
}

.service-card > p {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--mist);
  max-width: 40ch;
  margin: 0 auto 16px;
}

.service-featured > p {
  font-size: 0.95rem;
  max-width: 46ch;
}

/* (c) Uniform rows.

   Each card is a column. The block from the toggle down is pushed to the
   bottom with `margin-top: auto`, so across a row the toggles line up, the
   outcome lines line up and the pricing links line up — regardless of how
   many words the description above them took. */
.services-grid > .service-card,
.services-featured-row > .service-card {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.services-grid > .service-card > .service-toggle { margin-top: auto; }
.services-featured-row > .service-card > .service-cta-row { margin-top: auto; }

/* The compact list is the flexible part of the card, so it absorbs the
   difference in copy length instead of the controls below it. */
.services-grid > .service-card > .service-list-compact {
  margin-bottom: 18px;
}

/* The toggle becomes a divider-with-a-label: one hairline across the card
   at the same height in every card in the row. */
.service-toggle {
  width: 100%;
  justify-content: center;
  gap: 8px;
  padding: 12px 0 0;
  margin-bottom: 0;
  border-top: 1px solid var(--line);
  font-size: 0.79rem;
  letter-spacing: 0.01em;
}

.service-outcome-line {
  justify-content: center;
  gap: 8px;
  margin-top: 16px;
  padding: 11px 14px;
  border-radius: var(--radius-sm, 12px);
  background: var(--surface-canvas);
  border: 1px solid var(--line);
  font-size: 0.83rem;
  line-height: 1.4;
  color: var(--ink);
  text-align: center;
}

.service-outcome-line svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  color: var(--accent);
}

/* The two links at the foot of a featured card were a filled pill next to a
   underlined text link — two different weights of the same invitation. */
/* `margin-top: auto` below pins this row to the foot of the card, but auto
   resolves to ZERO in the card that is already the tallest in its row — so
   the rule's own hairline landed on the descenders of the last list item.
   The guaranteed gap is stated as a bottom margin on the block above, which
   auto cannot eat. */
.service-featured > .service-columns { margin-bottom: 24px; }

.service-cta-row {
  justify-content: center;
  align-items: center;
  gap: 18px;
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

.service-secondary-link {
  text-decoration: none;
  font-size: 0.86rem;
  color: var(--mist);
  transition: color 0.25s ease;
}

.service-secondary-link:hover { color: var(--ink); }

/* The pricing link is a link, not a submit button. As a flex child of the
   card column it was stretching to the full measure, which turned eight
   quiet "see pricing" links into eight full-width black pills — louder than
   the service names above them. */
.services-grid > .service-card > .service-pricing-link {
  margin: 16px auto 0;
  align-self: center;
  width: auto;
}

/* One line or two, the outcome strip is the same height, so the pricing
   links below it stay on one baseline across the row. */
.service-outcome-line { min-height: 62px; align-items: center; }

/* Row rhythm: the featured pair and the grid below it take the same gutter,
   so the section reads as one grid rather than two that nearly line up. */
.services-featured-row { gap: 22px; margin-bottom: 22px; }
.services-grid { gap: 22px; }

@media (max-width: 860px) {
  .service-card > h2,
  .service-card > h3 { font-size: 1.06rem; }
  .service-featured > h2,
  .service-featured > h3 { font-size: 1.2rem; }
  .service-card > p { font-size: 0.9rem; }

  /* mobile-app.css already left-aligns card paragraphs on a phone, for the
     reason stated there: a centred paragraph in a 390px column has a ragged
     left edge and the eye has to hunt for the start of every line. The
     bullet lists are the same body copy at the same measure and were still
     centred, which is why the card read as two different alignments stacked
     on each other. They follow the paragraphs. The icon, tag, heading and
     the controls stay centred — that is where the design's character is. */
  .service-card .service-list,
  .service-card .service-list-compact {
    align-items: flex-start;
    text-align: left;
  }

  .service-card .service-list li,
  .service-card .service-list-compact li { text-align: left; }

  .service-featured > .service-columns { margin-bottom: 20px; }
  .service-cta-row { padding-top: 20px; }
  .services-grid > .service-card > .service-toggle { padding-top: 16px; }
  .services-grid > .service-card > .service-list-compact { margin-bottom: 16px; }
}


/* --------------------------------------------------------------------------
   3. Search overlay — the field on touch devices

   glass.css lists `.site-search-input` with `.form-field input`, and
   mobile.css gives that same group `padding: 13px 15px; min-height: 50px;
   border-radius: var(--radius)`. Both are right for a standalone form field
   and wrong here: the palette input is one row inside a panel that already
   has its own surface and hairline, so the treatment drew a filled, rounded
   box inside another box. The global `input:focus-visible` ring then fired
   on the programmatic focus that opens the palette and outlined that inner
   box in accent blue — which is the "bar" that looked broken.

   The field row takes the focus state instead; the input goes back to being
   type on a surface. 16px is kept, because below it iOS zooms on focus.
   -------------------------------------------------------------------------- */
.site-search-input {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  min-height: 0;
  padding: 2px 0;
  font-size: 16px;
}

.site-search-input:focus,
.site-search-input:focus-visible {
  outline: none;
  box-shadow: none;
}

/* The whole row lights up, which is what the caret is already sitting in. */
.site-search-field {
  gap: 12px;
  padding: 16px 18px;
  transition: border-color 0.25s ease;
}

.site-search-panel:focus-within .site-search-field {
  border-bottom-color: var(--accent);
}

/* Height: `vh` on a phone measures the viewport with the browser chrome
   collapsed, so the palette's last row sat underneath the toolbar. `dvh`
   measures what is actually visible. --app-vh (js/layout/mobile.js) stays as the
   fallback for browsers without dvh. */
@media (max-width: 860px) {
  .site-search {
    padding: calc(var(--safe-t) + 14px) 12px 12px;
    align-items: stretch;
  }
  .site-search-panel {
    height: calc((var(--app-vh, 1vh) * 100) - var(--safe-t) - var(--safe-b) - 26px);
  }
}

@supports (height: 100dvh) {
  @media (max-width: 860px) {
    .site-search-panel {
      height: calc(100dvh - var(--safe-t) - var(--safe-b) - 26px);
    }
  }
}

/* Short landscape — a phone on its side, or a small laptop. The palette was
   taller than the space it had, so the results ran off the bottom with no
   way to reach them. */
@media (max-height: 600px) and (min-width: 861px) {
  .site-search { padding-top: 40px; padding-bottom: 24px; }
  .site-search-panel { max-height: calc(100vh - 64px); }
}

@supports (height: 100dvh) {
  @media (max-height: 600px) and (min-width: 861px) {
    .site-search-panel { max-height: calc(100dvh - 64px); }
  }
}

/* The list is the only part that may scroll; the field and footer are pinned.
   Without an explicit min-height a flex child refuses to shrink below its
   content, which is how the panel outgrew its own max-height. */
.site-search-list { min-height: 0; flex: 1 1 auto; }


/* --------------------------------------------------------------------------
   4. Pricing — the category rail

   Two faults.

   a) glass.css lists `.cat-chip` with `.price-item, .tier-btn, .faq-item …`
      and gives the group a tint, a hairline and a drop shadow. pricing.css
      had deliberately made these tabs plain type on a rule — the glass pass
      put the boxes back, and ten boxed names became the loudest object on a
      page about prices.

   b) center.css sets `justify-content: center` on the rail. On a flex row
      that overflows, centring pushes content off BOTH ends, and scrollLeft
      cannot go below zero — so the first categories were unreachable on any
      screen narrow enough to overflow, which on this list is every phone.
      `safe center` centres only while it fits and falls back to start once
      it does not; the plain fallback is start, which is scrollable.
   -------------------------------------------------------------------------- */

/* (a) Back to tabs. */
.cat-chip {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: 0;
  box-shadow: none;
  isolation: auto;
  border-radius: 0;
  padding: 17px 0 16px;
}

/* (b) Reachable at every width. */
.cat-filter {
  justify-content: flex-start;
  scroll-snap-type: x proximity;
  padding-inline: 2px;
}

@supports (justify-content: safe center) {
  .cat-filter { justify-content: safe center; }
}

.cat-chip { scroll-snap-align: center; }

/* The rail is the page's primary control, so the active tab is stated twice —
   weight and a rule — rather than by weight alone at 0.82rem on a phone. */
.cat-chip.is-active::after { height: 2.5px; bottom: -1px; }

.cat-filter-wrap { gap: 2px; }

/* The bar is sticky over a scrolling catalogue, so it has to actually stop
   it. pricing.css gave it --chrome-bg (0.82 alpha) plus a 20px backdrop
   blur, which is the right recipe for floating chrome and the wrong one
   here: the aisle headings passing underneath stayed legible straight
   through the tab names, and a sticky element you can read the page through
   reads as a rendering fault rather than as glass. Opaque, with the blur
   kept for the moment mid-scroll when a card is halfway under it, and a
   soft drop so the bar sits above the grid rather than in it. */
.cat-filter-wrap {
  background: var(--surface-white);
  box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.5);
}

@media (max-width: 860px) {
  /* Full-bleed, and above the chapter bar.

     js/layout/mobile.js pins `.m-chapter` — the section-progress strip — at
     `top: var(--m-top)`, 34px tall, `z-index: 9200`, full width. The
     category rail sticks to the same 56px line at z-index 40, so the
     chapter bar was landing directly on top of the tab names: on a phone
     the rail showed as a blue underline under a strip of unrelated grey
     text, which is the "not working on mobile" you saw — the tabs were
     live and hit-testable the whole time, just covered.

     The rail wins that band, because on this page it is the wayfinder and
     the chapter bar is only repeating the section title it already implies.
     Full-bleed as well as raised: the chapter bar runs edge to edge, so a
     container-width rail would have left 18px of it showing at each side —
     and the extra width buys two more category names before a swipe. */
  .cat-filter-wrap {
    z-index: 9300;
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    padding-inline: var(--m-gut, 16px);
    background: var(--surface-white);
  }

  /* Touch targets, and enough gap that two names never read as one. */
  .cat-filter { gap: 20px; scroll-padding-inline: 16px; }
  .cat-chip {
    font-size: 0.86rem;
    padding: 14px 0 12px;
    min-height: 46px;
    align-items: center;
  }
  .cat-chip-n { font-size: 0.68rem; }

  /* The rail is swiped, so it has to look swipeable: a fade on whichever
     side still has names behind it. js/pages/pricing.js maintains .can-prev /
     .can-next; without the arrows those classes had nothing to drive. */
  .cat-filter-wrap.can-next:not(.can-prev) .cat-filter,
  .cat-filter-wrap.can-prev:not(.can-next) .cat-filter,
  .cat-filter-wrap.can-prev.can-next .cat-filter {
    -webkit-mask-image: inherit;
  }
}

/* Landing on a filtered category from the search palette or a #hash should
   not park the rail on top of the first card. */
.price-category { scroll-margin-top: 160px; }


/* --------------------------------------------------------------------------
   5. Home hero — the dashboard panel is real glass

   style.css builds `.video-frame` with `backdrop-filter: none` and no
   shadow, and gradient-wave.css then paints it `--gw-panel` — a flat 58%
   white (7% white at night) over the moving gradient behind it. Flat
   translucency over a gradient reads as a wash, not as a pane: the field
   showed straight through the panel at full contrast, so the interface and
   the background were competing at the same depth.

   Frosted properly, the gradient behind is blurred and desaturated by the
   pane, which is what puts the panel in front of it. The ring, the top
   highlight and the drop are the same recipe glass.css uses for every other
   floating surface, so the hero panel is finally the same material as the
   nav capsule and the search palette rather than a one-off.
   -------------------------------------------------------------------------- */
.video-frame:has(.nd),
.hero-scene .video-frame:has(.nd) {
  background: var(--nd-glass-tint);
  -webkit-backdrop-filter: blur(34px) saturate(165%);
  backdrop-filter: blur(34px) saturate(165%);
  border: 1px solid var(--glass-hairline);
  box-shadow:
    inset 0 1px 0 0 var(--glass-edge),
    inset 0 -1px 0 0 var(--glass-edge-dim),
    0 2px 6px -2px rgba(0, 0, 0, 0.10),
    0 26px 60px -22px rgba(0, 0, 0, 0.34),
    0 60px 120px -50px rgba(0, 0, 0, 0.42);
  isolation: isolate;
}

:root { --nd-glass-tint: rgba(255, 255, 255, 0.60); }
[data-theme="dark"] { --nd-glass-tint: rgba(20, 22, 28, 0.56); }

/* A soft bloom sitting just outside the pane. Glass reads as glass partly
   because of what leaks around its edge — without it the panel is a
   rectangle with a blur inside it. Behind the frame, never over it, and
   pointer-transparent so nothing in the demo is shadowed by it. */
.hero-visual:has(.nd) { position: relative; }

.hero-visual:has(.nd)::before {
  content: "";
  position: absolute;
  inset: -8% -5% -12%;
  z-index: 0;
  pointer-events: none;
  border-radius: 50%;
  background:
    radial-gradient(58% 52% at 50% 46%,
      color-mix(in srgb, var(--accent-fill) 26%, transparent) 0%,
      transparent 72%);
  filter: blur(56px);
  opacity: 0.5;
}

[data-theme="dark"] .hero-visual:has(.nd)::before { opacity: 0.65; }

.hero-visual:has(.nd) > * { position: relative; z-index: 1; }

/* The interface sits ON the glass, so its own surfaces go translucent too —
   otherwise the frost only shows in the 1.6em gutter and the panel reads as
   an opaque screenshot with a blurred border. The tokens are re-pointed at
   the frame rather than in hero-dash.css so the demo keeps its solid
   surfaces anywhere else it is used (demos.html, hero-demo.html).

   Held at 74–82%: enough of the field behind to read as glass, opaque enough
   that 12px labels still clear contrast on a moving gradient. */
.video-frame:has(.nd) .nd {
  background: transparent;
  --nd-panel:   color-mix(in srgb, var(--surface-canvas) 76%, transparent);
  --nd-panel-2: color-mix(in srgb, var(--surface-pebble) 82%, transparent);
}

.video-frame:has(.nd) .nd-side,
.video-frame:has(.nd) .nd-card,
.video-frame:has(.nd) .nd-stage > * {
  -webkit-backdrop-filter: blur(10px) saturate(150%);
  backdrop-filter: blur(10px) saturate(150%);
}

/* The demo's own search field had the same nested-box problem the site
   palette did — it is a row on a panel, so it takes a wash and a hairline,
   not a second opaque surface. */
.video-frame:has(.nd) .nd-search {
  background: color-mix(in srgb, var(--surface-white) 52%, transparent);
}

@media (prefers-reduced-transparency: reduce) {
  .video-frame:has(.nd),
  .hero-scene .video-frame:has(.nd) {
    background: var(--surface-white);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .hero-visual:has(.nd)::before { display: none; }
}


/* --------------------------------------------------------------------------
   6. The removed pieces

   `#services-cta` ("Not sure which one fits?") and the `.footer-cta` band
   ("Let's build your next thing.") are deleted from the markup, not hidden.
   These rules only catch a page that has not been re-exported yet, so a
   stale copy does not put the block back.
   -------------------------------------------------------------------------- */
#services-cta,
.footer-cta { display: none; }


/* ==========================================================================
   7. PRESS STATES  (audit finding 02 — apple-design §1)

   The system had 216 :hover rules and 10 :active rules, and not one of the
   ten was a card. On a mouse that is a missing detail; on a touchscreen it
   is the whole feedback story, because the coarse-pointer block in
   mobile.css correctly strips the hover transforms on tap and put nothing
   in their place. Tapping a work card produced no response at all until the
   next page painted.

   §1: "Respond on pointer-down, not on release." A press is the first thing
   the interface can say, and it has to be instant.

   ── WHY SCALE, AND WHY DOWN ────────────────────────────────────────────
   This system's rule is that hierarchy never comes from elevation, so the
   press must not lift. Scaling slightly down is the opposite gesture and
   the honest one: the surface yields under the finger. .tier-btn and
   .nd-btn already did exactly this at 0.96; pressable *surfaces* are bigger
   than buttons, so they take a smaller ratio — 0.985 on a 500px card moves
   its edge ~4px, which reads as give rather than as a jump.

   ── WHY THE `scale` PROPERTY AND NOT `transform` ───────────────────────
   This is the part worth reading before changing anything here.

   Several of these surfaces already own their transform: motion.css builds
   the services and values entrances out of --lift and --scale and puts a
   520ms transition on `transform` to carry the rise. Writing `transform`
   here would win the cascade and still be wrong — the press would inherit
   that 520ms and land half a second after the finger, which is precisely
   the failure §1 describes. Routing the press through a custom property
   does not escape it either: `transform`'s computed value depends on that
   property, so transform re-interpolates over its own 520ms regardless of
   how fast the property itself moves. (That was measured, not assumed:
   0.996 at 110ms, still creeping at 510ms.)

   `scale` is a separate CSS property from `transform`. It composes with it
   — the individual transform properties apply first, then the transform
   matrix — and it carries its own transition. So the entrance keeps its
   520ms rise on `transform` while the press runs at 100ms on `scale`, on
   the same element, with neither touching the other. Nothing in motion.css
   needs editing, and nothing here needs !important.
   -------------------------------------------------------------------------- */

/* Every pressable surface, whether or not it owns a transform. `scale` is
   added to the transition list without disturbing whatever else is in it,
   because these rules only ever name `scale`. */
.card,
.work-card,
.service-card,
.mvv-card,
.review-card,
.price-item,
.combo-card,
.industry-card,
.sv-thumb,
.demo-card,
.case-card {
  transition: background var(--transition),
              scale 100ms var(--ease-apple);
}

/* The two surfaces that compose their own transform need their existing
   transition list restated so `scale` can join it — listing `scale` alone
   would replace the entrance timings. Values are motion.css's, verbatim;
   the only addition is the last line. Same specificity, later in the
   cascade, so it wins by order. */
.card.service-card {
  transition:
    opacity   var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out),
    background var(--transition),
    scale 100ms var(--ease-apple);
}
[data-anim="values"] .mvv-card {
  transition:
    opacity   var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out),
    background var(--transition),
    scale 100ms var(--ease-apple);
}

/* --- The press itself ----------------------------------------------------
   :active for mouse and Android; .is-pressed because iOS Safari does not
   apply :active to a tap on an arbitrary element, and js/core/fluid.js sets the
   class from pointerdown, which fires everywhere. Both carry the same
   declarations so there is one press appearance, not two. */
.card:active,            .card.is-pressed,
.work-card:active,       .work-card.is-pressed,
.service-card:active,    .service-card.is-pressed,
.mvv-card:active,        .mvv-card.is-pressed,
.review-card:active,     .review-card.is-pressed,
.price-item:active,      .price-item.is-pressed,
.combo-card:active,      .combo-card.is-pressed,
.industry-card:active,   .industry-card.is-pressed,
.sv-thumb:active,        .sv-thumb.is-pressed,
.demo-card:active,       .demo-card.is-pressed,
.case-card:active,       .case-card.is-pressed {
  scale: 0.985;
  /* The surface steps at the same moment, so the press still reads on a
     card whose 4px of travel is at the edge of vision. */
  background: var(--glass-bg-hover);
}

/* --- Buttons -------------------------------------------------------------
   Buttons own no entrance to compose with, so they take `scale` directly
   too — one press language across the whole system rather than transform
   here and scale there. */
.btn, .btn-primary, .nav-cta-btn, .tier-btn, .nd-btn, .nav-search-btn {
  transition: background-color var(--transition),
              border-color var(--transition),
              scale 100ms var(--ease-apple);
}
.btn:active,            .btn.is-pressed,
.btn-primary:active,    .btn-primary.is-pressed,
.nav-cta-btn:active,    .nav-cta-btn.is-pressed {
  scale: 0.97;
}

/* Links are too small for a scale — it reads as a wobble. They take the
   colour step instead, still on pointer-down, still instant. */
.nav-links > li > a:not(.btn):active,
.nav-links > li > a:not(.btn).is-pressed,
.footer-grid ul li a:active,
.footer-grid ul li a.is-pressed,
.card-link:active,
.card-link.is-pressed {
  color: var(--accent);
}

/* §14. Reduced motion keeps the feedback and drops the movement — a press
   still answers, it just answers in colour rather than in scale. */
@media (prefers-reduced-motion: reduce) {
  .card:active, .card.is-pressed,
  .work-card:active, .work-card.is-pressed,
  .service-card:active, .service-card.is-pressed,
  .mvv-card:active, .mvv-card.is-pressed,
  .review-card:active, .review-card.is-pressed,
  .price-item:active, .price-item.is-pressed,
  .combo-card:active, .combo-card.is-pressed,
  .industry-card:active, .industry-card.is-pressed,
  .sv-thumb:active, .sv-thumb.is-pressed,
  .demo-card:active, .demo-card.is-pressed,
  .case-card:active, .case-card.is-pressed,
  .btn:active, .btn.is-pressed,
  .btn-primary:active, .btn-primary.is-pressed,
  .nav-cta-btn:active, .nav-cta-btn.is-pressed {
    scale: 1;
  }
}
