/* =========================================================================
   Top Rung Marketing — stylesheet
   One hand-written file, passthrough-copied (not processed/bundled).
   Organized: tokens -> base -> layout -> components -> sections -> utilities
   ========================================================================= */

/* -------------------------------------------------------------------------
   TOKENS
   ------------------------------------------------------------------------- */
:root {
  /* Brand colors */
  --orange: #F7641F;
  /* Darkened variant of the brand orange, used only for text set directly on
     white/paper backgrounds. The brand orange itself is ~3:1 against white,
     which fails AA for normal text — this variant clears 4.5:1 while still
     reading as "orange." On dark (ink) backgrounds, use --orange directly. */
  --orange-text: #B94B17;
  --charcoal: #333333;
  --ink: #1C1C1C;
  --white: #FFFFFF;
  --paper: #F4F4F2;

  /* Derived neutrals */
  --paper-dark: #E7E4DD;
  --line: rgba(51, 51, 51, 0.14);
  --line-on-dark: rgba(255, 255, 255, 0.18);
  --text-on-dark: #F4F4F2;
  --text-muted-on-dark: rgba(244, 244, 242, 0.72);
  --text-muted: #5C5C5C;

  /* Type */
  --font-heading: "Oswald", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;

  /* Layout */
  --container-max: 1120px;
  --container-narrow: 680px;
  --radius: 3px;
  --header-height: 72px;

  /* Theme toggle (light defaults — see html[data-theme="dark"] below).
     --bg/--fg drive the page canvas; everything else is header/logo-
     specific. Light values intentionally equal the plain colors already
     used above, so light mode is visually unchanged by this system. */
  --bg: var(--white);
  --fg: var(--charcoal);
  --header-line: var(--line);
  --switch-line: var(--line);
  --logo-ink: var(--charcoal);
  --bar-bg: var(--white);
  --bar-shadow: 0 6px 24px rgba(28, 28, 28, 0.10), 0 1px 3px rgba(28, 28, 28, 0.06);
  --logo-notch: var(--bar-bg);
  --theme-dur: 0.45s;
  --dock-dur: 0.35s;
  --float-gap: 16px;
  --logo-h: 36px;
  --sign-w: 72px;
  --sign-in-at: 1.35s;
  --climb-start: 850ms;
  --rung-step: 70ms;
  --ns-lit: #FFC266;
  --ns-dim: #4A3214;
}

html[data-theme="dark"] {
  --bg: var(--ink);
  --fg: var(--paper);
  --header-line: rgba(255, 255, 255, 0.07);
  --switch-line: var(--line-on-dark);
  --logo-ink: var(--paper);
  --bar-bg: #262626;
  --bar-shadow: 0 8px 28px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.3);
}
@media (max-width: 699px) {
  :root { --float-gap: 10px; }
}

/* -------------------------------------------------------------------------
   BASE
   ------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* Grid/flex items default to min-width:auto, which lets long unbreakable
   content (long headings, buttons, nav rows) force a track wider than its
   container instead of wrapping. Zeroing it out is the standard fix. */
*, *::before, *::after { min-width: 0; }

/* Self-hosted Oswald, variable weight (200-700), Latin subset. Preloaded
   in base.njk — registering it here is what actually makes
   --font-heading resolve to it instead of falling back to the system
   stack. */
@font-face {
  font-family: "Oswald";
  src: url("/fonts/oswald-latin.woff2") format("woff2");
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
}

/* Self-hosted Inter, variable weight (400-800), Latin subset. Same
   self-hosting approach as the heading font above — no runtime CDN
   request. Preloaded in base.njk. */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* Self-hosted Overpass 800, Latin subset — used only by the NIGHT SHIFT
   sign in the header logo (see LOGO / THEME TOGGLE below). Downloaded
   once from Google Fonts and served locally, same as the fonts above; no
   runtime CDN request. Preloaded in base.njk. */
@font-face {
  font-family: "Overpass";
  src: url("/fonts/overpass-latin-800.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-height) + var(--space-4));
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--fg);
  background: var(--bg);
  transition: background-color var(--theme-dur), color var(--theme-dur);
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.12;
  margin: 0 0 var(--space-4);
  color: var(--ink);
}
/* .section--ink / .ladder-section headings already override this to
   --white regardless of theme (see LAYOUT) — unaffected by the rule
   below, which only reaches headings still on their default color. */
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3,
[data-theme="dark"] h4 { color: var(--text-on-dark); }

h1, h2 { text-transform: uppercase; letter-spacing: 0.01em; }

h1 { font-size: clamp(2.25rem, 5vw + 1rem, 3.75rem); }
h2 { font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); }
h3 { font-size: 1.25rem; }
h4 { font-size: 1.05rem; }

p { margin: 0 0 var(--space-4); max-width: 65ch; }

a { color: var(--orange-text); text-decoration-thickness: 1.5px; text-underline-offset: 0.15em; }
a:hover { color: var(--orange); text-decoration-thickness: 2px; }
/* --orange-text is the AA-safe darkened orange for text on light/white —
   it reads too dark on an ink background, where the full-brightness
   --orange already clears AA (see the TOKENS comment above). Scoped to
   main (so header/footer nav links, which have their own color rules,
   are untouched by being outside it entirely) and excludes .btn (an
   element-level selector otherwise outranks a single class, which was
   winning over .btn--primary's ink-on-orange text and making every
   orange button's label invisible in dark mode) and .contact-info a
   (overridden separately, below, to --text-on-dark instead of orange). */
[data-theme="dark"] main a:not(.btn):not(.contact-info a) { color: var(--orange); }

ul, ol { margin: 0 0 var(--space-4); padding-left: 1.2em; }

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

label { font-weight: 500; }

input, select, textarea {
  font: inherit;
  color: inherit;
}

/* Visible keyboard focus everywhere, including inside dark sections */
:focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: 2px;
  border-radius: 1px;
}

/* Skip link */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: -3rem;
  background: var(--ink);
  color: var(--white);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius);
  z-index: 100;
  transition: top 0.15s ease;
}
.skip-link:focus { top: var(--space-4); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* -------------------------------------------------------------------------
   LAYOUT
   ------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

main { display: block; }

.section { padding-block: var(--space-8); }
.section--tight { padding-block: var(--space-7); }
.section--paper { background: var(--paper); }
.section--ink { background: var(--ink); color: var(--text-on-dark); }
.section--ink h2, .section--ink h3, .section--ink h4 { color: var(--white); }
.section--ink p { color: var(--text-muted-on-dark); }

.section__head { max-width: var(--container-narrow); margin-bottom: var(--space-7); }
.section__head--wide { max-width: 48ch; }

/* Thin orange bar — the brand's "top rung" motif, used sparingly as a
   heading underline / section divider. */
.rule {
  display: block;
  width: 56px;
  height: 4px;
  background: var(--orange);
  border: 0;
  margin: 0 0 var(--space-4);
}
.rule--center { margin-inline: auto; }

/* -------------------------------------------------------------------------
   COMPONENTS
   ------------------------------------------------------------------------- */

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-6);
  font-family: var(--font-heading);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.015em;
  text-decoration: none;
  border-radius: var(--radius);
  border: 2px solid transparent;
  cursor: pointer;
}

/* Orange buttons always use dark text — white-on-orange fails AA contrast */
.btn--primary {
  background: var(--orange);
  color: var(--ink);
  border-color: var(--orange);
}
.btn--primary:hover { background: #e2580f; border-color: #e2580f; text-decoration: none; }

.btn--outline {
  background: transparent;
  color: var(--charcoal);
  border-color: var(--charcoal);
}
.btn--outline:hover { background: var(--charcoal); color: var(--white); text-decoration: none; }

.btn--outline-light {
  background: transparent;
  color: var(--white);
  border-color: var(--line-on-dark);
}
.btn--outline-light:hover { background: var(--white); color: var(--ink); text-decoration: none; }

.btn--small { min-height: 40px; padding: var(--space-2) var(--space-5); font-size: 0.9rem; }
.btn--block { width: 100%; }

/* Cards — flat, with a thin orange top edge instead of a drop shadow */
.card {
  background: var(--white);
  border: 1px solid var(--line);
  border-top: 4px solid var(--orange);
  border-radius: var(--radius);
  padding: var(--space-6) var(--space-5);
}
.card h3 { margin-bottom: var(--space-3); }
.card p:last-child { margin-bottom: 0; }

/* -------------------------------------------------------------------------
   HEADER
   Floating -> docked: at the top of the page the header is an inset
   rounded card (.site-header__bar) with a transparent gap around it; once
   scrolled, js/theme-toggle.js adds html.is-docked... actually adds the
   class to .site-header itself (see below), which stretches the bar full
   width and docks it flush to the top. The header's own height never
   changes, so nothing on the page jumps — the bar just moves into the
   gap. Progressive enhancement: without JS the header simply stays in
   its floating resting state (still fully usable, nothing broken).
   ------------------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  padding: var(--float-gap) var(--float-gap) 0;
  pointer-events: none; /* the transparent gap never blocks clicks */
  transition: padding var(--dock-dur) ease;
}

.site-header__bar {
  pointer-events: auto;
  max-width: calc(var(--container-max) + 3rem);
  margin-inline: auto;
  background: var(--bar-bg);
  border: 1px solid var(--header-line);
  border-radius: 14px;
  box-shadow: var(--bar-shadow);
  transition:
    max-width var(--dock-dur) ease, border-radius var(--dock-dur) ease,
    transform var(--dock-dur) ease, box-shadow var(--dock-dur) ease,
    border-color var(--dock-dur) ease, background-color var(--theme-dur);
}
.site-header.is-docked { padding-inline: 0; }
.site-header.is-docked .site-header__bar {
  max-width: 100%;
  border-radius: 0;
  border-color: transparent transparent var(--header-line);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  transform: translateY(calc(var(--float-gap) * -1));
}

/* A faint warm glow under the header once the lights are off — stacked
   as an extra shadow layer on top of each state's existing box-shadow
   rather than replacing it, and picked up by .site-header__bar's own
   box-shadow transition above, so it fades in/out with the rest of the
   theme swap instead of popping in. */
[data-theme="dark"] .site-header__bar { box-shadow: var(--bar-shadow), 0 16px 36px -12px rgba(247, 100, 31, 0.35); }
[data-theme="dark"] .site-header.is-docked .site-header__bar { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06), 0 16px 36px -12px rgba(247, 100, 31, 0.35); }

.site-header__inner {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

.site-header__logo { position: relative; display: flex; align-items: center; flex-shrink: 0; }

.site-nav a, .site-header__phone, .nav-mobile__phone { transition: color var(--theme-dur); }

.site-nav { display: none; margin-inline-end: auto; }
.site-nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--space-6); }
.site-nav a {
  color: var(--charcoal);
  text-decoration: none;
  font-weight: 500;
  padding: var(--space-2) 0;
}
.site-nav a:hover { color: var(--orange); }

.site-header__actions { display: none; align-items: center; gap: var(--space-5); margin-inline-start: auto; }
.site-header__phone { color: var(--charcoal); font-weight: 600; text-decoration: none; white-space: nowrap; }
.site-header__phone:hover { color: var(--orange); }

/* "Start Climbing" CTAs (header + hero), daytime: white text on the same
   dark gray as the logo's resting rail/rung color (--charcoal, i.e.
   --logo-ink in light mode). Hover sweeps in brand orange left-to-right
   via ::after (scaleX, clipped by overflow:hidden) rather than a plain
   crossfade, with the text swapping to the same brand gray once the
   orange has swept in. Dark mode's own white-background treatment (see
   the [data-theme="dark"] rule further down) overrides this entirely
   and is unaffected. */
.site-header .btn--primary,
.hero__actions .btn--primary {
  position: relative;
  z-index: 0;
  overflow: hidden;
  background: var(--charcoal);
  border-color: var(--charcoal);
  color: var(--white);
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  transition: color 0.18s ease, border-color 0.18s ease;
}
.site-header .btn--primary::after,
.hero__actions .btn--primary::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--orange);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.18s ease;
}
.site-header .btn--primary:hover,
.hero__actions .btn--primary:hover { color: var(--charcoal); border-color: var(--orange); }
.site-header .btn--primary:hover::after,
.hero__actions .btn--primary:hover::after { transform: scaleX(1); }
/* The header button's text sits directly in the <a>, so :hover's color
   above reaches it by plain inheritance — the hero version's text is
   wrapped in .hero__cta-text--primary (for the page-load animation
   further down) with its own explicit color, which doesn't inherit a
   new value from its hovering parent, so it needs the same color
   repeated explicitly here. */
.hero__actions .btn--primary:hover .hero__cta-text--primary { color: var(--charcoal); }

/* "View Pricing" text in the exact brand orange — a deliberate AA
   tradeoff, same call already made for the header/hero CTA's white
   text (see above): ~3:1 against the hero's paper background, under
   the 4.5:1 normal-text threshold, in favor of matching the logo and
   buttons exactly rather than using the darkened --orange-text variant
   used elsewhere on the site for text-on-light. No visible box at rest
   (border transparent) — the border and the gray fill both only show
   up on hover, swept in the same way as the primary CTA next to it. */
.hero__actions .btn--outline {
  position: relative;
  z-index: 0;
  overflow: hidden;
  color: var(--orange);
  border-color: transparent;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  transition: color 0.18s ease, border-color 0.18s ease;
}
[data-theme="dark"] .hero__actions .btn--outline { color: var(--text-on-dark); border-color: transparent; }
.hero__actions .btn--outline::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* White, not charcoal — this button's resting (non-hover) label is
     always white once the page-load sweep finishes (see
     .hero__cta-text--outline below), so a white hover box reads as the
     natural "lighter" state next to it, matching "Start Climbing"'s own
     hover treatment right beside it. */
  background: var(--white);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.18s ease;
}
.hero__actions .btn--outline:hover {
  background: transparent;
  color: var(--orange);
  border-color: var(--white);
}
.hero__actions .btn--outline:hover::after { transform: scaleX(1); }
/* :hover on the <a> sets its OWN color, which .hero__cta-text--outline
   doesn't inherit — the span has its own explicit (always-on) color a
   few lines up, and an element's own explicit color always wins over
   whatever its parent's color becomes, hover or not. Same story for
   "Start Climbing" below. */
.hero__actions .btn--outline:hover .hero__cta-text--outline { color: var(--orange); }
/* Dark mode: swipe in orange instead of white, text settles to brand
   gray instead of staying text-on-dark — the specificity here
   ([data-theme="dark"] + both classes) is needed to beat the global
   dark .btn--outline rules further down the file. box-shadow matches
   "Start Climbing"'s brighter dark-mode hover glow (see further down)
   so both hero CTAs give the same noticeable feedback when lit. */
[data-theme="dark"] .hero__actions .btn--outline::after { background: var(--orange); }
[data-theme="dark"] .hero__actions .btn--outline:hover {
  background: transparent;
  color: var(--charcoal);
  border-color: var(--orange);
  box-shadow: 0 0 12px rgba(247, 100, 31, 0.95), 0 0 42px rgba(247, 100, 31, 0.75);
}
[data-theme="dark"] .hero__actions .btn--outline:hover .hero__cta-text--outline { color: var(--charcoal); }

/* Both hero CTA labels play a short one-time reveal as part of the
   page-load sweep (see HERO SCENE further down for the full sequence
   and timing budget) instead of reacting to scroll — and that reveal
   cuts BEHIND the actual letterforms rather than flipping the whole
   label as one solid color: the sweep's edge is still flat/horizontal
   at 950-1100ms (the keyframe
   sequence doesn't settle into its diagonal until 1100-1350ms — see
   HERO SCENE), so as it crosses this row it genuinely passes partway
   through the letters' height for a moment, same as every row above
   it, and the old pre-rewrite scroll-driven version showed that same
   mid-letter cut here too. Each uses its own background-clip:text
   gradient tracking --cover (0-1, registered further up), with the
   colors flipped between the two labels: "View Pricing" goes
   orange-above-the-cut / white-below (the already-covered portion
   reads white-on-orange, the not-yet-covered portion reads orange on
   the still-transparent/paper button, mirroring the headline's "orange
   becomes the background" read), while "Start Climbing" goes the other
   way — orange-above / white-below as well, but meaning white (not yet
   covered, still on its charcoal fill) to orange (its own fill has
   flipped to white underneath by the time the cut passes, see
   .hero__actions .btn--primary's own separate background animation
   right below) — there's no background for the sweep to visually
   "become" on this button the way there is on the outline one, so this
   just gives its label the same "the cut passed over these letters"
   moment against its own fixed fill. Each rule's plain (non-animated)
   --cover is declared as 1 — the animation's END state, not its
   start — because fill-mode is deliberately "backwards", not
   "forwards": backwards holds the keyframes' FROM value (--cover: 0,
   i.e. fully not-yet-covered) during the animation-delay, but once the
   animation finishes playing it stops owning --cover entirely and this
   plain declaration is what's left in effect, so it has to already be
   the correct resting (fully-covered) value — which is also what lets
   :hover actually take over afterward: a running (or finished-but-held)
   CSS animation always outranks a :hover rule in the cascade regardless
   of specificity, so "forwards"/"both" would keep hover from ever
   visibly doing anything once the animation had played once.
   :not([data-theme="dark"]): dark mode has no orange sweep to animate
   against, so its own (separate, unrelated) button treatments are left
   alone entirely — see the plain dark-mode overrides below each. */
html:not([data-theme="dark"]) .hero__cta-text--outline {
  --cover: 1;
  background: linear-gradient(
    to bottom,
    var(--white) 0%,
    var(--white) max(0%, calc(var(--cover) * 100% - 4%)),
    var(--orange) min(100%, calc(var(--cover) * 100% + 4%)),
    var(--orange) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: hero-cta-cover 150ms ease-out 950ms backwards;
}
[data-theme="dark"] .hero__cta-text--outline { color: var(--text-on-dark); }
@keyframes hero-cta-cover {
  from { --cover: 0; }
  to { --cover: 1; }
}
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  html:not([data-theme="dark"]) .hero__cta-text--outline {
    background: none;
    color: var(--white);
    animation: hero-cta-outline-sweep-fallback 150ms ease-out 950ms backwards;
  }
}
@keyframes hero-cta-outline-sweep-fallback {
  from { color: var(--orange); }
  to { color: var(--white); }
}

html:not([data-theme="dark"]) .hero__actions .btn--primary {
  background: var(--white);
  border-color: var(--white);
  animation: hero-cta-primary-bg 150ms ease-out 950ms backwards;
}
@keyframes hero-cta-primary-bg {
  from { background: var(--charcoal); border-color: var(--charcoal); }
  to { background: var(--white); border-color: var(--white); }
}
html:not([data-theme="dark"]) .hero__cta-text--primary {
  --cover: 1;
  background: linear-gradient(
    to bottom,
    var(--orange) 0%,
    var(--orange) max(0%, calc(var(--cover) * 100% - 4%)),
    var(--white) min(100%, calc(var(--cover) * 100% + 4%)),
    var(--white) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: hero-cta-cover 150ms ease-out 950ms backwards;
}
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  html:not([data-theme="dark"]) .hero__cta-text--primary {
    background: none;
    color: var(--orange);
    animation: hero-cta-primary-text-fallback 150ms ease-out 950ms backwards;
  }
}
@keyframes hero-cta-primary-text-fallback {
  from { color: var(--white); }
  to { color: var(--orange); }
}

.nav-mobile__phone {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-inline-start: auto;
  color: var(--charcoal);
  flex-shrink: 0;
}
.nav-mobile__phone svg { width: 22px; height: 22px; }

.nav-mobile { position: relative; flex-shrink: 0; }
.nav-mobile__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  cursor: pointer;
  list-style: none;
}
.nav-mobile__toggle::-webkit-details-marker { display: none; }
.nav-mobile__toggle::marker { content: ""; }

.nav-mobile__icon--menu { display: flex; flex-direction: column; gap: 5px; }
.nav-mobile__icon--menu span { width: 22px; height: 2px; background: var(--charcoal); display: block; }
.nav-mobile__icon--close { display: none; font-size: 1.75rem; line-height: 1; color: var(--charcoal); }

.nav-mobile[open] .nav-mobile__icon--menu { display: none; }
.nav-mobile[open] .nav-mobile__icon--close { display: block; }

.nav-mobile__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 1px);
  width: min(86vw, 320px);
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.nav-mobile__panel ul { list-style: none; margin: 0 0 var(--space-5); padding: 0; }
.nav-mobile__panel a {
  display: block;
  padding: var(--space-3) 0;
  color: var(--charcoal);
  text-decoration: none;
  font-weight: 500;
  border-bottom: 1px solid var(--line);
}
.nav-mobile__panel li:last-child a { border-bottom: 0; }

/* These live in <header>, not <main>, so the general dark link-color rule
   (BASE, above) never reaches them — they need their own dark values,
   same charcoal-with-orange-hover pattern as light mode. :not(:hover) on
   the two that have a :hover rule keeps that rule's orange winning on
   equal specificity instead of being overridden by this unconditional one. */
[data-theme="dark"] .site-nav a:not(:hover),
[data-theme="dark"] .site-header__phone:not(:hover),
[data-theme="dark"] .nav-mobile__phone,
[data-theme="dark"] .nav-mobile__icon--close,
[data-theme="dark"] .nav-mobile__panel a:not(.btn) { color: var(--paper); }
[data-theme="dark"] .nav-mobile__icon--menu span { background: var(--paper); }
[data-theme="dark"] .nav-mobile__panel {
  background: var(--bar-bg);
  border-color: var(--line-on-dark);
}
[data-theme="dark"] .nav-mobile__panel a:not(.btn) { border-bottom-color: var(--line-on-dark); }

/* These CTAs invert to white when the lights go off — same "goes white
   once lit" language as the logo's wordmark swap and the ladder rungs.
   .site-header .btn--primary reaches both the desktop header button and
   its twin in the mobile menu panel, since both live inside <header>;
   .hero__actions .btn--primary is the matching hero CTA. The daytime
   orange-swipe ::after (see above) is switched off here — dark mode
   keeps just the glow brightening on hover, no color change. */
[data-theme="dark"] .site-header .btn--primary,
[data-theme="dark"] .hero__actions .btn--primary {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
  box-shadow: 0 0 13px rgba(247, 100, 31, 0.5);
  transition: box-shadow 0.25s ease, background-color var(--theme-dur);
}
[data-theme="dark"] .site-header .btn--primary::after,
[data-theme="dark"] .hero__actions .btn--primary::after { display: none; }
[data-theme="dark"] .site-header .btn--primary:hover,
[data-theme="dark"] .hero__actions .btn--primary:hover {
  background: var(--white);
  border-color: var(--white);
  box-shadow: 0 0 12px rgba(247, 100, 31, 0.95), 0 0 42px rgba(247, 100, 31, 0.75);
}

/* -------------------------------------------------------------------------
   LIGHT SWITCH
   Wall plate + toggle lever. Up = lights on (light mode), down = lights
   off (dark mode). One instance in .site-header__actions (desktop), one
   (.light-switch--mobile) next to the phone icon on mobile — both driven
   by the same click handler in js/theme-toggle.js.
   ------------------------------------------------------------------------- */
.light-switch {
  position: relative;
  flex-shrink: 0;
  width: 30px;
  height: 44px;
  padding: 0;
  cursor: pointer;
  border: 1px solid var(--switch-line);
  border-radius: 4px;
  background: var(--bar-bg);
  transition: background-color var(--theme-dur), border-color var(--theme-dur);
}
.light-switch::before, .light-switch::after { /* screws */
  content: "";
  position: absolute;
  left: 50%;
  width: 3px;
  height: 3px;
  margin-left: -1.5px;
  border-radius: 50%;
  background: var(--fg);
  opacity: 0.35;
}
.light-switch::before { top: 4px; }
.light-switch::after { bottom: 4px; }
.light-switch__slot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 20px;
  margin: -10px 0 0 -5px;
  border-radius: 2px;
  background: var(--switch-line);
}
.light-switch__lever {
  position: absolute;
  left: 1px;
  right: 1px;
  top: 1px;
  height: 9px;
  border-radius: 1.5px;
  background: var(--charcoal);
  transition: transform 0.18s cubic-bezier(0.3, 1.5, 0.6, 1), background-color var(--theme-dur);
}
[data-theme="dark"] .light-switch__lever { transform: translateY(9px); background: var(--orange); }
.light-switch:hover { border-color: var(--orange); }
.light-switch--mobile { margin-inline-start: var(--space-2); }

@media (min-width: 880px) {
  .site-nav { display: block; }
  .site-header__actions { display: flex; }
  .nav-mobile, .nav-mobile__phone, .light-switch--mobile { display: none; }
  .site-header__logo { --logo-h: 44px; }
}

/* -------------------------------------------------------------------------
   LOGO / THEME TOGGLE — animated lamp, climbing rungs, NIGHT SHIFT sign
   Inline SVG (partials/logo.njk), ported verbatim from
   _reference/top-rung-lights-toggle.html — path data, --up/--down rung
   indices, and all keyframe timings are generator output, not hand-tuned;
   don't edit them directly.

   Resting state (no JS, or JS present but the switch hasn't been clicked
   yet) needs no animation at all: every part's plain (non-keyframe) color/
   opacity rule below is already keyed off [data-theme], so the logo
   renders correct and complete for whichever theme the pre-paint script
   in base.njk resolved (localStorage, else prefers-color-scheme). The
   climb/lamp-in/sign-in keyframes only ever run once html.tr-anim is
   added by js/theme-toggle.js's click handler, as the transition between
   two already-correct resting states — never as a requirement to reach
   one. Without that script the switch is simply inert (not keyboard/
   click-operable), same progressive-enhancement boundary as the rest of
   this site's scoped JS exceptions; see CLAUDE.md.
   ------------------------------------------------------------------------- */
.tr-logo { display: block; height: var(--logo-h); width: auto; aspect-ratio: 973 / 300; overflow: visible; }
.tr-rail, .tr-rung, .tr-ink { fill: var(--logo-ink); transition: fill var(--theme-dur); }
.tr-notch { fill: var(--logo-notch); transition: fill var(--theme-dur); }
.tr-accent, .tr-top { fill: var(--orange); }
/* rails stay dark until the bulb comes on */
.tr-anim[data-theme="dark"] .tr-rail,
.tr-anim[data-theme="dark"] .tr-notch { transition-delay: 0.5s; }

/* Wordmark swap: in dark mode TOP + MARKETING go white and RUNG goes
   orange. The swap is instant when the lights go off; going back to
   light mode it fades. */
.tr-accent { transition: fill var(--theme-dur); }
.tr-ink { transition: fill var(--theme-dur); }
[data-theme="dark"] .tr-accent { fill: var(--white); }
[data-theme="dark"] .tr-ink { fill: var(--orange); }
.tr-anim[data-theme="dark"] .tr-accent,
.tr-anim[data-theme="dark"] .tr-ink { transition: none; } /* instant swap the moment the switch flips */
.tr-top { transform-box: fill-box; transform-origin: center; }

/* The climb. Only runs after the visitor flips the switch (.tr-anim is
   added by JS), so returning visitors don't get an animation on every
   page load. Timeline (dark): lamp drops in 0-550ms, bulb flickers on
   500-850ms, light climbs the rungs from 850ms, words swap when it
   reaches the top. */
.tr-anim[data-theme="dark"] .tr-rung { animation: tr-climb-up 0.5s ease-out calc(var(--climb-start) + var(--up) * var(--rung-step)) both; }
.tr-anim[data-theme="light"] .tr-rung { animation: tr-climb-down 0.45s ease-out calc(var(--down) * 70ms) both; }
.tr-anim[data-theme="dark"] .tr-top { animation: tr-top-pop 0.55s cubic-bezier(0.3, 1.6, 0.5, 1) calc(var(--climb-start) + var(--up) * var(--rung-step)) both; }

/* Same "lights going out" cascade as the click-triggered tr-climb-down/
   tr-top-dip above, reused verbatim, but for the page-load/page-refresh
   entrance rather than a toggle click: paired with the hero's own
   orange sweep-down (see HERO SCENE further down), starting at the same
   400ms beat the hero's headline/CTA sub-animations already key off
   once the sweep starts moving, so the header reads as part of the same
   entrance rather than a separate thing. :not(.tr-anim) is what keeps
   this from ever fighting the click-triggered rules above for the same
   animation property: .tr-anim only gets added by theme-toggle.js's
   click handler and, once added, stays on <html> for the rest of that
   page view (see theme-toggle.js) — so this rule and the click-
   triggered ones are mutually exclusive by construction: this one owns
   the ladder from page load up to the first click, the click-triggered
   rules own it after. :has(.hero): only the home page has the hero
   sweep to pair with, so that's the only page that gets this entrance —
   other pages share this same header/logo but stay static on load.
   Clicking the header logo (always linked to "/") is a real full-page
   navigation/reload, not a client-side toggle, so it naturally replays
   this exactly like any other fresh load, with no extra markup or JS
   needed for that part. */
html:not([data-theme="dark"]):not(.tr-anim):has(.hero) .tr-rung { animation: tr-climb-down 0.45s ease-out calc(400ms + var(--down) * 70ms) both; }
html:not([data-theme="dark"]):not(.tr-anim):has(.hero) .tr-top { animation: tr-top-dip 0.35s ease-out 400ms both; }

/* ---- Hook lamp: only exists in dark mode ---- */
.tr-lamp, .tr-halo { opacity: 0; }
[data-theme="dark"] .tr-lamp, [data-theme="dark"] .tr-halo { opacity: 1; }
.tr-lamp { transform-box: view-box; transform-origin: 2px -6px; } /* pivot = top of the hook */
.tr-lamp-metal { stroke: #B4B2A9; }
.tr-lamp-grip { fill: #5C5C5C; }
.tr-lamp-cord { stroke: var(--orange); }
.tr-bulb { fill: #FFE2A8; }

.tr-anim[data-theme="dark"] .tr-lamp { animation: tr-lamp-in 0.55s ease-out both; }
.tr-anim[data-theme="light"] .tr-lamp { animation: tr-lamp-out 0.3s ease-in both; }
.tr-anim[data-theme="dark"] .tr-bulb { animation: tr-bulb-on 0.35s linear 0.5s both; }
.tr-anim[data-theme="dark"] .tr-halo { animation: tr-halo-on 0.35s linear 0.5s both; }

@keyframes tr-lamp-in { /* drop, then swing to a stop on the hook */
  0% { opacity: 0; transform: translateY(-60px) rotate(0); }
  35% { opacity: 1; transform: translateY(0) rotate(8deg); }
  60% { transform: rotate(-5deg); }
  80% { transform: rotate(2deg); }
  100% { opacity: 1; transform: rotate(0); }
}
@keyframes tr-lamp-out { /* lifted off the ladder */
  0% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(-40px); }
}
@keyframes tr-bulb-on { /* trouble-light flicker */
  0% { fill: #555550; }
  20% { fill: #FFE2A8; }
  35% { fill: #555550; }
  55% { fill: #FFE2A8; }
  70% { fill: #8A7A5A; }
  100% { fill: #FFE2A8; }
}
@keyframes tr-halo-on {
  0% { opacity: 0; }
  20% { opacity: 1; }
  35% { opacity: 0; }
  55% { opacity: 1; }
  70% { opacity: 0.4; }
  100% { opacity: 1; }
}
.tr-anim[data-theme="light"] .tr-top { animation: tr-top-dip 0.35s ease-out both; }

/* a light passes each rung on the way up, leaving it lit */
@keyframes tr-climb-up {
  0% { fill: var(--charcoal); }
  35% { fill: var(--orange); }
  100% { fill: var(--paper); }
}
/* lights go out top-down on the way back */
@keyframes tr-climb-down {
  0% { fill: var(--paper); }
  35% { fill: var(--orange); }
  100% { fill: var(--charcoal); }
}
@keyframes tr-top-pop {
  0% { transform: scale(1, 1); }
  40% { transform: scale(1.08, 1.35); fill: #FF9A66; }
  100% { transform: scale(1, 1); fill: var(--orange); }
}
@keyframes tr-top-dip {
  0%, 100% { transform: scale(1, 1); }
  50% { transform: scale(0.94, 0.8); }
}

/* -------------------------------------------------------------------------
   NIGHT SHIFT sign: horizontal road-sign plate sitting directly under
   MARKETING. Last beat of the sequence: flips down into place after the
   light reaches the top rung.
   ------------------------------------------------------------------------- */
.night-sign {
  position: absolute;
  pointer-events: none;
  width: var(--sign-w);
  /* center under MARKETING (x = 668 of the logo's 973-wide viewBox) */
  left: calc(var(--logo-h) * 668 / 300 - var(--sign-w) / 2);
  /* tucked up under MARKETING, inside the header, with room left below
     it before the header's own edge (see the HEADER comment on
     --header-height's split above/below the logo) */
  top: calc(100% - 2px);
  opacity: 0;
  visibility: hidden;
  transform-origin: 50% 0;
}
.night-sign svg { display: block; width: 100%; height: auto; overflow: visible; }
.night-sign text { font-family: "Overpass", "Highway Gothic", "Arial Narrow", sans-serif; font-weight: 800; }
[data-theme="dark"] .night-sign { opacity: 1; visibility: visible; }
.tr-anim[data-theme="dark"] .night-sign { animation: tr-sign-in 0.55s ease-out var(--sign-in-at) both; }
.tr-anim[data-theme="light"] .night-sign { animation: tr-sign-out 0.25s ease-in both; }
@keyframes tr-sign-in { /* flips down like a placard being set out */
  0% { opacity: 0; transform: perspective(200px) rotateX(-90deg); }
  55% { opacity: 1; transform: perspective(200px) rotateX(18deg); }
  78% { transform: perspective(200px) rotateX(-6deg); }
  100% { opacity: 1; transform: none; }
}
@keyframes tr-sign-out {
  0% { opacity: 1; visibility: visible; transform: none; }
  100% { opacity: 0; visibility: visible; transform: perspective(200px) rotateX(-90deg); }
}

/* Lit letters: amber glow, power-on flicker when the sign lands, then G
   and F keep misfiring like a tired bulb. */
.ns-text { fill: var(--ns-lit); }
.tr-anim[data-theme="dark"] .ns-text { animation: ns-power-on 0.6s steps(1, end) calc(var(--sign-in-at) + 0.4s) both; }
[data-theme="dark"] .ns-flick-1 { animation: ns-flick-a 4.3s steps(1, end) 2.6s infinite; }
[data-theme="dark"] .ns-flick-2 { animation: ns-flick-b 6.7s steps(1, end) 3.4s infinite; }
@keyframes ns-power-on {
  0% { fill: var(--ns-dim); }
  15% { fill: var(--ns-lit); }
  25% { fill: var(--ns-dim); }
  45% { fill: var(--ns-lit); }
  55% { fill: #A0702C; }
  100% { fill: var(--ns-lit); }
}
@keyframes ns-flick-a {
  0%, 70%, 74%, 79%, 100% { fill: var(--ns-lit); }
  72%, 77% { fill: var(--ns-dim); }
}
@keyframes ns-flick-b {
  0%, 40%, 41.5%, 86%, 89%, 100% { fill: var(--ns-lit); }
  40.7% { fill: var(--ns-dim); }
  87%, 88% { fill: #8A6020; }
}

/* The sitewide prefers-reduced-motion block (BASE, above) forces every
   animation/transition DURATION to ~0.01ms, but several of the keyframes
   above are scheduled with an animation-DELAY (the climb waits on
   --climb-start, the lamp/bulb on a flat 0.5s, the sign on --sign-in-at,
   the sign letters' power-on on --sign-in-at + 0.4s) — duration isn't
   delay, so none of that is touched by the blanket rule. Left alone, a
   reduced-motion visitor who clicks the switch would sit staring at each
   part's animation-start (fill:backwards) state — e.g. the sign invisible,
   the rungs unlit — for up to ~1.75s before everything snapped correct.
   Disabling the animations outright (not just speeding them up) skips
   straight to each part's plain, already theme-correct resting rule. */
@media (prefers-reduced-motion: reduce) {
  .tr-anim .tr-rung, .tr-anim .tr-top, .tr-anim .tr-lamp,
  .tr-anim .tr-bulb, .tr-anim .tr-halo, .tr-anim .night-sign,
  .ns-text, .ns-flick,
  html:not([data-theme="dark"]):not(.tr-anim):has(.hero) .tr-rung,
  html:not([data-theme="dark"]):not(.tr-anim):has(.hero) .tr-top { animation: none !important; }
  .tr-anim[data-theme="dark"] .tr-rail,
  .tr-anim[data-theme="dark"] .tr-notch { transition-delay: 0s !important; }
}

/* -------------------------------------------------------------------------
   FOOTER
   ------------------------------------------------------------------------- */
.site-footer { background: var(--ink); color: var(--text-muted-on-dark); padding-block: var(--space-7) var(--space-5); }
.site-footer__inner {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  justify-content: space-between;
  align-items: flex-start;
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--line-on-dark);
}
.site-footer__logo { height: 32px; width: auto; margin-bottom: var(--space-3); }
.site-footer__tagline { margin: 0; color: var(--text-muted-on-dark); }
.site-footer__contact { display: flex; flex-direction: column; gap: var(--space-2); }
.site-footer__contact a, .site-footer__legal a { color: var(--white); text-decoration: none; }
.site-footer__contact a:hover, .site-footer__legal a:hover { color: var(--orange); }
.site-footer__legal { display: flex; flex-direction: column; gap: var(--space-2); }
.site-footer__copyright {
  max-width: var(--container-max);
  margin: var(--space-5) auto 0;
  padding-inline: var(--space-5);
  font-size: 0.875rem;
  color: var(--text-muted-on-dark);
}

/* -------------------------------------------------------------------------
   SECTIONS
   ------------------------------------------------------------------------- */

/* Hero */
/* Pulled up by the header's own floating-gap + height, then padded back
   down by the same amount, so the hero's box (and anything painted in
   it, like .hero__rung below) starts at the true top of the page —
   behind the header — while the visible content's position is
   unchanged from a plain padding-top. */
.hero {
  position: relative;
  margin-top: calc(-1 * (var(--float-gap) + var(--header-height)));
  padding-top: calc(var(--float-gap) + var(--header-height) + var(--space-8));
  /* No padding-bottom here on purpose: .hero__night-photo (an empty
     reserved-height strip, added below — no photo in either theme any
     more) is the last thing in .hero's flow and has to be flush with
     the hero's true bottom edge, with nothing after it. The breathing
     room that padding-bottom used to provide now lives on
     .hero__inner's own margin-bottom instead, so the content above it
     keeps the same spacing it always had. */
  background: var(--paper);
}

/* Plain layout wrapper for the eyebrow/headline/subhead/CTAs — not
   involved in .hero__scene's sizing (see HERO SCENE below for why that
   has to be a direct child of .hero instead, reaching further up and
   down than this wrapper's own box does). */
.hero__content-area {
  position: relative;
}

/* Registered so @keyframes below can genuinely interpolate --cover as a
   number (0 -> 1) instead of just flipping discretely partway through
   the animation — that smooth interpolation is what lets the gradients
   that use it (further down: .hero__cta-text--primary/--outline) slide
   their white/colored split point continuously through the letterforms,
   rather than jumping. Not
   registering this at all would still work, just less smoothly — an
   unregistered custom property doesn't interpolate, so the gradient's
   split point would snap partway through instead of sliding, which is
   the graceful-enough fallback in browsers without @property support. */
@property --cover {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* -------------------------------------------------------------------------
   HERO SCENE — the orange rung plays a one-time, pure-CSS reveal on page
   load (no JS, no scroll dependency — replaces the old scroll-lock/
   --hero-p system this section used to document). Sequence, total
   1350ms:

     0%    - 29.63% (0-400ms):   holds at --hero-initial-edge, a flat
                                  horizontal line just below "Climb to
                                  the Top of Google".
     29.63% - 81.48% (400-1100ms): sweeps down, ease-out, to full
                                  coverage (clip-path's bottom edge
                                  reaches 100%) — flat/horizontal the
                                  whole way, per spec ("keep the moving
                                  edge horizontal").
     81.48% - 100% (1100-1350ms): settles back up into the resting
                                  diagonal (--hero-settle-edge on the
                                  left, --hero-angle degrees shallower
                                  on the right — see .hero__rung below).

   Animated with clip-path only (a polygon, not height/width), so
   nothing here ever triggers layout — the rung is always inset:0 of
   .hero__scene at full size, and clip-path just masks how much of it
   paints. .hero__scene is a direct child of .hero, not of
   .hero__content-area, and deliberately sized wider than that content
   wrapper on both ends: top:0/left:0/right:0 reach .hero's own true
   top — behind the floating header, same "pulled up by the header's
   floating-gap + height" trick .hero itself uses (see the comment on
   .hero above) — rather than stopping below .hero's padding-top the
   way .hero__content-area's normal-flow box does, which is what lets
   the orange sweep bleed up around the header instead of stopping
   right at the content below it; bottom: calc(var(--hero-photo-height) -
   48px) (the same custom property .hero__night-photo's own height uses
   below, so the two always stay roughly in sync — and since it cancels
   out of the scene's own effective height entirely, see the next
   paragraph, its exact value barely matters here) stops the scene 48px
   into where the night photo begins rather than exactly at its top
   edge — that 48px is deliberate overflow room for the settled
   diagonal's low point to reach "48px below the CTA row" (see
   --hero-settle-edge further down), which .hero__content-area's own
   natural bottom (right at the CTAs, plus its small 16px margin-bottom)
   doesn't leave room for on its own. This never actually costs anything
   visible: the orange sweep is light-mode-only and the photo is
   dark-mode-only, so the two never coexist on screen regardless of how
   this 48px plays out geometrically.
   That combined span — true page top through 48px into the
   photo-reserved strip — is also what every clip-path percentage below
   (--hero-initial-edge, --hero-settle-edge, and the keyframes' own
   100%) is a fraction of. Because .hero__scene's own bottom offset
   subtracts the full --hero-photo-height before adding back the fixed
   48px, that variable's value cancels out of the scene's effective
   height algebraically (scene height = header space + content height +
   48px, full stop) — so resizing the reserved photo strip (e.g. for a
   different image) never requires re-tuning --hero-initial-edge or
   --hero-settle-edge, only re-verifying the arithmetic still holds.

   .hero__headline--sweep and the two hero CTA labels run their own
   short color-flip animations timed to roughly when the sweep's edge
   reaches that row (see each rule for exact delays) — they don't read
   the rung's clip-path directly (there's no live link between a
   clip-path animation and other elements in pure CSS), so their delays
   are hand-tuned against this same timeline instead of
   geometry-measured the way the old JS version did it.

   Both this animation and the three text ones are scoped
   html:not([data-theme="dark"]) throughout: dark mode has no orange
   sweep (the night job-site photo + sunset gradient carry dark mode
   instead, see .hero__rung's opacity:0 below and the dark-mode .hero
   background further down) and explicitly keeps a flat resting
   clip-path so there's nothing diagonal left over if a visitor toggles
   the theme. prefers-reduced-motion disables every animation below and
   falls through to each element's own plain (non-animated) base
   style — which is already the fully-swept final look — rendering the
   end state immediately, with no JS fallback needed since none of this
   depends on JS in the first place. */
.hero__scene { position: absolute; top: 0; left: 0; right: 0; bottom: calc(var(--hero-photo-height) - 48px); z-index: 0; pointer-events: none; }
/* Drop shadow for the orange shape, following its animated clip-path
   edge (flat while sweeping, angled once settled) — has to live here,
   one level up from .hero__rung's own clip-path, rather than on
   .hero__rung directly: clip-path and filter on the SAME element would
   have the clip-path also cut off the filter's shadow output (clip-path
   restricts everything the box paints, filtered shadow included), so
   putting the filter on this unclipped wrapper instead lets the shadow
   extend past the clipped edge the way a cast shadow actually would.
   :not([data-theme="dark"]): no separate dark-mode override needed —
   .hero__rung is opacity:0 there, so there's no alpha for drop-shadow
   to cast a shadow from either way, but this still states it
   explicitly rather than relying on that indirectly. */
html:not([data-theme="dark"]) .hero__scene {
  filter: drop-shadow(0 26px 22px rgba(70, 30, 10, 0.28)) drop-shadow(0 6px 6px rgba(70, 30, 10, 0.18));
}

.hero {
  /* Shared with .hero__night-photo's own height below, so .hero__scene
     above can stop exactly where that reserved strip starts without
     hardcoding a second copy of this clamp() that could drift out of
     sync — and, per the comment on .hero__scene above, its exact value
     cancels out of the scene's own effective height anyway, so this is
     purely about keeping one source of truth, not about retuning
     anything else when it changes. */
  --hero-photo-height: clamp(220px, 28vw, 420px);
  /* One knob, per request — tune between 4deg and 5deg. Rise of the
     final diagonal (right edge higher than left) scales with it via
     tan() against the viewport width below, so a larger angle produces
     a larger rise on wide screens and a proportionally smaller one on
     narrow screens (see .hero__rung). */
  --hero-angle: 4.5deg;
  /* Both tuned empirically against the rendered layout (see the build
     notes in index.njk's git history for the Playwright measurements) —
     percentages of .hero__scene's own box height (.hero's full height
     minus the skyline, i.e. true top of the page through the bottom of
     the CTA row — see .hero__scene above for why that's the scope).
     --hero-settle-edge: the LEFT (lower) point of the final resting
     diagonal, landing at least 48px below the CTA row — this one holds
     as a single constant at every width (unlike --hero-initial-edge
     right below) because the CTA row's own position scales with scene
     height closely enough across widths that one value clears it
     everywhere. */
  --hero-settle-edge: 97.79%;
  /* Where the orange's flat bottom edge sits at rest, before the sweep
     starts moving: has to land in the real gap between "...Google"'s
     glyph ink and "& Make Your"'s glyph ink below it (fully covering
     the former — it's unconditionally white — without touching the
     latter, which plays its own color flip instead; see
     .hero__headline--line1's comment further down). Unlike
     --hero-settle-edge above, this can't be one constant: the gap's
     position as a fraction of scene height grows steadily with
     viewport width (measured via a cloned, color-neutral, pixel-scanned
     copy of the headline at a range of widths — the two lines' own
     getBoundingClientRect()s run well past their real glyph ink in both
     directions — the same font-metrics padding that used to force the
     mid-letter cut this whole thing replaces, so they're not
     trustworthy for this on their own) up to where the headline's own
     font-size clamp() plateaus
     (880px — see .hero__headline below), and holds flat from there.
     Fluid clamp() between the two measured endpoints (320px -> 41%,
     1024px -> 45%, flat outside that range) tracks the real
     measurements within a few px at every width in between, biased
     toward the high side of each width's own safe range on purpose:
     undershooting (leaving a sliver of "...Google" uncovered, white on
     the plain background) is a real legibility bug, while overshooting
     (grazing the very top of "&") is just a few px of cosmetic
     imprecision — so ties go to the safer side. */
  --hero-initial-edge: clamp(41%, calc(41% + 4% * (100vw - 320px) / 704px), 45%);
}

.hero__rung {
  position: absolute;
  inset: 0;
  background: var(--orange);
  opacity: 1;
  transition: opacity var(--theme-dur);
}
[data-theme="dark"] .hero__rung { opacity: 0; }
html:not([data-theme="dark"]) .hero__rung {
  /* The animation's own 100% keyframe already matches this exact
     polygon — see the "backwards, not forwards" note on the CTA
     animations above for why that match matters (nothing here needs
     :hover to keep working afterward, but it's the same principle:
     once the animation finishes, this plain rule is what's left
     showing, so it has to already be the correct resting shape). */
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(var(--hero-settle-edge) - 100vw * tan(var(--hero-angle))),
    0 var(--hero-settle-edge)
  );
  animation: hero-sweep 1350ms backwards;
}
@keyframes hero-sweep {
  0% {
    clip-path: polygon(0 0, 100% 0, 100% var(--hero-initial-edge), 0 var(--hero-initial-edge));
  }
  29.63% {
    clip-path: polygon(0 0, 100% 0, 100% var(--hero-initial-edge), 0 var(--hero-initial-edge));
    animation-timing-function: ease-out;
  }
  81.48% {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  }
  100% {
    clip-path: polygon(
      0 0,
      100% 0,
      100% calc(var(--hero-settle-edge) - 100vw * tan(var(--hero-angle))),
      0 var(--hero-settle-edge)
    );
  }
}
/* Dark mode: flat resting edge, no sweep — stated explicitly rather
   than just relying on opacity:0 above, so there's never a stray
   diagonal clip-path left showing through if something (a future
   change, a theme toggle mid-animation) ever makes the rung visible
   in dark mode unexpectedly. */
[data-theme="dark"] .hero__rung {
  clip-path: none;
  animation: none;
}
@media (prefers-reduced-motion: reduce) {
  html:not([data-theme="dark"]) .hero__rung { animation: none; }
}

/* Dusk sky + roofline (dark mode only) — replaces the orange rung's
   job with a flat, illustrated dusk instead of a flat color, clipped to
   the exact same settled diagonal .hero__rung itself rests at (see
   --hero-settle-edge/--hero-angle above), so the two modes' diagonals
   land in the identical place even though dark mode never actually
   runs the rung's own sweep/animation.

   Drawn entirely in CSS rather than a photo, to match the site's flat
   graphic style (logo, goat, widgets, ladder rungs) instead of reading
   as photorealistic next to it: a plain vertical gradient for the sky
   itself, plus a handful of flat decorative children layered on top
   (stars, glow, moon, clouds — each below). Because none of it is a
   baked photo, there's no fixed roofline-angle dependency to keep in
   sync any more — only --hero-angle/--hero-settle-edge above matter,
   and every child here reads from those same two variables rather than
   a second, independent source of truth.

   .hero__dusk is a direct child of .hero__scene, positioned/sized
   exactly like .hero__rung (inset:0 of the same box), clipped with the
   identical polygon, no animation — just a flat opacity toggle on
   theme. The readability gradient a photo sky needed (to keep white
   headline text legible over bright sky pixels) isn't here: the flat
   gradient background is dark almost everywhere text can sit, so
   contrast already clears WCAG AA without it — see the contrast check
   in this change's verification pass before reintroducing one. */
.hero__dusk {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: linear-gradient(#141416, #1c1a1b 40%, #2a1d18);
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(var(--hero-settle-edge) - 100vw * tan(var(--hero-angle))),
    0 var(--hero-settle-edge)
  );
  opacity: 0;
  transition: opacity var(--theme-dur);
}
[data-theme="dark"] .hero__dusk { opacity: 1; }

/* Stars: one inline SVG stretched (preserveAspectRatio="none") to
   .hero__dusk's own box, so the viewBox's fixed coordinates always land
   in the same relative spot regardless of viewport size — the dots
   themselves are small enough (0.9-1.2px) that the non-uniform stretch
   never reads as distortion. Two groups, both measured (not guessed)
   against the real header/eyebrow/subhead/CTA/device-mockup rects at
   375-1920px before being placed:
   - ten spread across the FULL width (x: 80-960 of 1440) but held to a
     narrow y band (122-155 of 900) that real-coordinate testing showed
     clears both the floating header above it and .hero__eyebrow below
     it at every width — "scattered across the top" without needing
     per-breakpoint x tuning, since that band is safe regardless of x.
   - eight more confined to the far-right edge (x: 1385-1430), which
     testing showed stays clear of the text column even at 375px (where
     the column runs nearly full-width) — so this group can safely use
     a much taller y range (125-250) than the first, for a denser
     cluster near the moon. */
.hero__dusk-stars { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Horizon glow: a band that sits exactly on the roofline, rotated by
   the same angle as the diagonal and pivoting on the line's own left
   end (transform-origin: 0 0, the same point --hero-settle-edge
   anchors), so it stays locked to the diagonal at every width and
   tracks it automatically if --hero-angle is ever retuned — no second
   angle value to keep in sync. translateY(-100%) after the rotation
   lifts the band so its bottom edge (not its top) sits on the line,
   since it's meant to glow up into the sky from the roofline, not down
   into the roof. width: 110vw (not 100vw) covers the small horizontal
   shortfall the rotation introduces, same reasoning as .hero__shingles
   used to for the same angle. */
.hero__dusk-glow {
  position: absolute;
  left: 0;
  top: var(--hero-settle-edge);
  width: 110vw;
  height: clamp(220px, 24vw, 360px);
  transform-origin: 0 0;
  transform: rotate(calc(-1 * var(--hero-angle))) translateY(-100%);
  background: linear-gradient(
    rgba(232, 105, 43, 0),
    rgba(122, 52, 20, 0.55) 45%,
    #b84a18 82%,
    var(--orange) 100%
  );
}

/* Moon: a crescent built from one circle plus its own offset
   box-shadow, no background-image/SVG needed. The box itself stays
   fully transparent; the shadow paints a second, solid copy of the
   same circle shifted up-left. Where the two circles overlap, the
   (transparent) box always paints over its own shadow, so that
   overlapping area shows the sky behind it rather than the shadow's
   fill — only the shadow's non-overlapping sliver stays visible,
   which is the crescent. Counter-intuitively, a SMALLER shadow offset
   (relative to the circle's own diameter) makes a THINNER crescent,
   not a wider one: a small offset means the two circles nearly
   coincide, so almost all of the shadow sits inside the occluded
   overlap and only a sliver survives outside it; push the offset much
   closer to the full diameter instead and the circles barely overlap
   at all, so nearly the whole shadow stays visible — which looks like
   a plain full circle, the opposite of a crescent. (An earlier pass
   got this backwards and went the wrong direction looking for a
   thinner sliver.) The low opacity on top is what makes it read as
   "nearly invisible" rather than just "thin." Pinned to the same
   upper-right "safe sky" quadrant as the stars, with a top offset
   (clamp floor 100px) tall enough to clear the floating header's
   measured bottom edge (84-90px depending on --float-gap's own
   breakpoint) with margin to spare at every width down to mobile. */
.hero__dusk-moon {
  position: absolute;
  right: 9%;
  top: clamp(100px, 11vw, 130px);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  box-shadow: -4px 2px 0 0 #f3e9dc;
  transform: rotate(-20deg);
  opacity: 0.5;
}

/* Roofer silhouette, standing on the same diagonal — positioned with
   the same --hero-settle-edge/--hero-angle formula as the edge itself
   (substituting --roofer-x for 100 in the tan() term, since he sits
   partway across rather than at the right edge), so he stays glued to
   the line at every width instead of needing separate per-breakpoint
   top values. translate(-3%, -83%) nudges the image so the drawn
   figure's back foot (not the image's own top-left corner) is the
   point that actually lands on --roofer-x/the line, and so the
   image's bottom ~16% (extra leg/hand length the source PNG includes
   on purpose) sits hidden below the roofline inside .hero__dusk's roof
   area instead of floating. --roofer-x is tuned per breakpoint below
   purely to dodge the headline/CTAs/.hero__visual at that width's own
   layout — not a single value, since "a clear spot on the line" moves
   around as the grid/column layout itself changes. Hidden outright
   under 700px: at phone widths the line sits too close under the CTA
   row for any --roofer-x to clear it. */
.hero__roofer {
  --roofer-x: 60;
  position: absolute;
  left: calc(var(--roofer-x) * 1%);
  top: calc(var(--hero-settle-edge) - var(--roofer-x) * 1vw * tan(var(--hero-angle)));
  height: clamp(110px, 12vw, 192px);
  width: auto;
  transform: translate(-3%, -83%);
  opacity: 0;
  transition: opacity var(--theme-dur);
}
[data-theme="dark"] .hero__roofer { opacity: 1; }
@media (max-width: 699px) {
  [data-theme="dark"] .hero__roofer { opacity: 0; }
}
@media (min-width: 700px) and (max-width: 879px) {
  .hero__roofer { --roofer-x: 74; }
}
@media (min-width: 880px) and (max-width: 1199px) {
  .hero__roofer { --roofer-x: 58; }
}
@media (min-width: 1200px) {
  .hero__roofer { --roofer-x: 60; }
}

/* Empty strip at the bottom of the hero (partials/hero-skyline.njk) —
   no photo in either theme any more: dark mode shows the roofline +
   dusk sky in .hero__scene, light mode's own job-site photo was
   removed on request. Still sits in normal flow as the last child of
   .hero, after .hero__content-area, so it still adds its own height to
   the hero rather than overlapping the content above it (that's also
   what guarantees it never overlaps the CTAs at any width: normal-flow
   elements stack, they don't overlap) — kept for exactly that reason,
   not for anything it visually shows any more: removing it would pull
   --hero-photo-height's reserved space out of the hero entirely, which
   both shifts the page on every load (shorter hero than before) and
   changes .hero__scene's own bottom-offset math the orange sweep's
   geometry depends on (see --hero-photo-height's own comment above).
   With nothing painted inside it, it just shows .hero's own plain
   background through — --paper in light mode, --ink in dark (HERO
   further down) — so both themes already read as a plain color here
   with zero extra CSS needed for that. */
.hero__night-photo { display: block; width: 100%; height: var(--hero-photo-height); }

.hero__inner {
  position: relative;
  z-index: 1;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
  /* Kept deliberately tight (not --space-7 like the grid gap below) so
     the content reads as part of the initial view on page load without
     wasting space; the min(…svh) term shrinks it further on short
     viewports, same short-viewport compression as the eyebrow/headline
     margins below (search this file for "svh" to find the rest) — the
     goal across all of them is that the CTA row stays inside the
     viewport at scrollY:0 even on a short window, each value scaling
     continuously via min()/clamp() rather than snapping at one
     breakpoint. */
  margin-bottom: min(var(--space-4), 2svh);
  display: grid;
  gap: min(var(--space-7), 6.5svh);
  align-items: center;
}
.hero__content { max-width: 46ch; }

/* The H1 is the small kicker line (keyword-targeted copy for SEO); the
   big visual statement below it is a styled <p> instead — overriding
   the global h1 rules' size/case rather than letting the eyebrow render
   at full headline scale. margin-bottom and (below) the headline's own
   font-size/margin fold in a min(…, Nsvh) term alongside the usual
   fixed/fluid-width value — see the short-viewport note on
   .hero__inner's own margin-bottom above for why: on a short window
   these need to shrink along with everything else so the CTA row
   itself stays visible on load. */
.hero__eyebrow {
  font-size: clamp(1.05rem, 0.4vw + 0.95rem, 1.25rem);
  text-transform: none;
  letter-spacing: normal;
  color: var(--charcoal);
  margin-bottom: min(var(--space-2), 2.5svh);
}

.hero__headline {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.12;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  font-size: clamp(1.75rem, min(5vw, 6svh) + 1rem, 3.75rem);
  margin: 0 0 min(var(--space-4), 2svh);
}
/* "Climb to the Top of Google" is already covered by the rung at rest
   (see --hero-initial-edge above), so it's just plain white, no
   animation needed. "& Make Your Phone Ring" starts on the plain
   background (charcoal reads there, white wouldn't) and flips to white
   once the sweep's edge reaches it, timed against the same 1350ms
   budget as .hero__rung's animation: the sweep itself starts at 400ms
   and this row sits near the top of that range, so the flip starts
   shortly after, finishing well before the sweep reaches the CTAs. */
.hero__headline--lead { color: var(--white); }
[data-theme="dark"] .hero__headline--lead { color: var(--text-on-dark); }
/* display:block forces "& Make Your Phone Ring" onto its own line(s),
   never sharing a physical line with the end of "...Google" — without
   this, natural word-wrap can let the "&" trail onto the same line as
   "GOOGLE", and since a word span can't be two colors within one line
   (that was the old per-word gradient-clip trick, removed along with
   the original scroll system), that stray "&" would render in
   whichever color its word-span currently has even on an
   already-orange-covered line, i.e. dark-on-orange right at rest —
   exactly what this animation exists to never do. */
.hero__headline--sweep { display: block; }

/* "& Make Your" (line one of this phrase) used to sit close enough to
   "...Google" above that the two lines' real glyph ink nearly touched,
   forcing --hero-initial-edge to cut mid-letter through this line's own
   top at rest — fixed by making --hero-initial-edge itself responsive
   (see the .hero block below) so it always lands in the real visual gap
   between the two lines' ink instead, at every width. With the edge no
   longer touching this line at rest, it needs nothing fancier than the
   same plain two-state flip "Phone"/"Ring" use below (same keyframe,
   same color pair), just its own delay — it's a separate class rather
   than folded into .hero__headline--word purely because it's not
   actually a single <span> among equal word spans in the markup, it's
   the whole "& Make Your" run; see that rule's own comment just below
   for why nth-child alone is enough to give each of these three a
   distinct delay without per-visitor measurement. */
html:not([data-theme="dark"]) .hero__headline--line1 { color: var(--white); animation: hero-headline-sweep 130ms ease-out 400ms backwards; }

/* Each word inside "& Make Your Phone Ring" gets its own short color
   flip with its own delay — not one flip for the whole phrase — so the
   reveal visibly cascades word by word as the sweep's edge actually
   passes under it. All three sit cleanly below --hero-initial-edge
   (genuinely uncovered at rest, never mid-letter), so a plain two-state
   flip is enough — no gradient needed. The three delays are hand-tuned
   against the sweep's own timeline (hero-sweep starts at 400ms — see
   HERO SCENE above) for where this line typically sits at common
   widths, not measured per-visitor the way the old JS version did it;
   nth-child counts only the <span> elements, ignoring the literal space
   characters between them in the markup. */
html:not([data-theme="dark"]) .hero__headline--word { color: var(--white); }
[data-theme="dark"] .hero__headline--word,
[data-theme="dark"] .hero__headline--line1 { color: var(--text-on-dark); }
@keyframes hero-headline-sweep {
  from { color: var(--charcoal); }
  to { color: var(--white); }
}
html:not([data-theme="dark"]) .hero__headline--word:nth-child(2) { animation: hero-headline-sweep 130ms ease-out 430ms backwards; }
html:not([data-theme="dark"]) .hero__headline--word:nth-child(3) { animation: hero-headline-sweep 130ms ease-out 470ms backwards; }

/* The sitewide prefers-reduced-motion rule (BASE, near the top of this
   file) only zeroes out animation-duration/transition-duration — it
   doesn't touch animation-delay, so on its own these animations
   would still sit at their keyframe-0% ("before") value for the first
   450-950ms before snapping to the end color, which is exactly the
   flash-of-wrong-state reduced-motion is supposed to prevent. animation:
   none here removes the delay along with the animation, falling through
   immediately to each element's plain (non-animated) rule above/further
   up — already the correct final look in every case, so this is a pure
   skip with nothing else to coordinate. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-theme="dark"]) .hero__headline--line1,
  html:not([data-theme="dark"]) .hero__headline--word:nth-child(2),
  html:not([data-theme="dark"]) .hero__headline--word:nth-child(3),
  html:not([data-theme="dark"]) .hero__cta-text--outline,
  html:not([data-theme="dark"]) .hero__actions .btn--primary,
  html:not([data-theme="dark"]) .hero__cta-text--primary {
    animation: none;
  }
  /* No extra --cover override needed here: the two gradient-clip CTA
     text elements above each declare --cover: 1 (the fully-crossed end
     state) as their own plain, non-animated value — see the long
     comment on them above for why that's the correct plain value with
     fill-mode: backwards — so "animation: none" alone already leaves
     them showing the correct final look. */
}

.hero__subhead { font-size: clamp(0.95rem, 2.5svh, 1.125rem); color: var(--charcoal); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: min(var(--space-6), 4.5svh); }

.hero__trust {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
}
.hero__trust li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-muted);
}
.hero__trust svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--orange); }

.hero__visual { display: none; }
.hero__visual img { max-width: 260px; margin-inline: auto; }

@media (min-width: 880px) {
  .hero__inner { grid-template-columns: 1.3fr 1fr; }
  .hero__visual { display: block; }
}

/* The problem */
.problem-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .problem-grid { grid-template-columns: repeat(3, 1fr); } }

/* Keyword research — the differentiator, given a dark panel for weight */
.research__body { max-width: 68ch; font-size: 1.05rem; }
.compare {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  margin-top: var(--space-7);
}
@media (min-width: 700px) { .compare { grid-template-columns: 1fr 1fr; } }

.compare__col {
  border: 1px solid var(--line-on-dark);
  border-radius: var(--radius);
  padding: var(--space-6) var(--space-5);
}
.compare__col--highlight { background: rgba(247, 100, 31, 0.1); border-color: var(--orange); }
.compare__col h3 { font-size: 1rem; text-transform: uppercase; letter-spacing: 0.04em; }
.compare__col ul { list-style: none; padding: 0; margin: 0; }
.compare__col li {
  padding: var(--space-2) 0;
  border-top: 1px solid var(--line-on-dark);
  color: var(--text-muted-on-dark);
}
.compare__col li:first-child { border-top: 0; }
.compare__col--highlight li { color: var(--white); }
.compare__note { margin-top: var(--space-4); font-size: 0.875rem; color: var(--text-muted-on-dark); }

/* Services */
.services-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .services-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .services-grid { grid-template-columns: repeat(3, 1fr); } }

/* Trades */
.trades-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(2, 1fr);
  margin-bottom: var(--space-6);
}
@media (min-width: 700px) { .trades-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .trades-grid { grid-template-columns: repeat(6, 1fr); } }

.trade-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
  padding: var(--space-5) var(--space-3);
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.trade-tile svg { width: 34px; height: 34px; color: var(--orange); }
.trade-tile span { font-weight: 500; font-size: 0.95rem; }
.trades-ask { color: var(--text-muted); }

/* Pricing */
.pricing-grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
  margin-bottom: var(--space-8);
}
@media (min-width: 760px) { .pricing-grid { grid-template-columns: repeat(2, 1fr); } }

.plan-card {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--line);
  border-top: 4px solid var(--orange);
  border-radius: var(--radius);
  padding: var(--space-6);
}
.plan-card--featured { background: var(--ink); border-color: var(--ink); color: var(--text-on-dark); }
.plan-card--featured .plan-card__name { color: var(--white); }
.plan-card--featured .plan-card__price-detail { color: var(--text-muted-on-dark); }
.plan-card--featured .plan-card__features li { border-top-color: var(--line-on-dark); }

.plan-card__badge {
  display: inline-block;
  align-self: flex-start;
  background: var(--orange);
  color: var(--ink);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius);
  margin-bottom: var(--space-4);
}
.plan-card__name { margin-bottom: var(--space-2); }
.plan-card__price { font-family: var(--font-heading); font-size: 1.75rem; font-weight: 600; margin: 0; }
.plan-card__price-detail { color: var(--text-muted); margin-bottom: var(--space-5); }
.plan-card__features { list-style: none; margin: 0 0 var(--space-6); padding: 0; flex-grow: 1; }
.plan-card__features li { padding: var(--space-2) 0; border-top: 1px solid var(--line); }
.plan-card__features li:first-child { border-top: 0; }

.addons-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .addons-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .addons-grid { grid-template-columns: repeat(4, 1fr); } }

.addon-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
}
.addon-card__name { margin-bottom: var(--space-1); }
.addon-card__price { font-weight: 600; color: var(--orange-text); margin-bottom: var(--space-3); }
.addon-card__desc { font-size: 0.925rem; color: var(--text-muted); flex-grow: 1; }

/* How it works — three ladder rungs, top one orange */
.steps { list-style: none; margin: 0; padding: 0; max-width: 640px; }
.step {
  display: flex;
  gap: var(--space-5);
  align-items: flex-start;
  padding: var(--space-5) 0;
  border-top: 4px solid var(--charcoal);
}
.step:first-child { border-top: 4px solid var(--orange); }
.step__number {
  font-family: var(--font-heading);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--text-muted);
  flex-shrink: 0;
  width: 2.5ch;
}
.step:first-child .step__number { color: var(--orange-text); }
.step h3 { margin-bottom: var(--space-2); }
.step p { margin-bottom: 0; }

/* About */
.about__inner {
  display: grid;
  gap: var(--space-6);
  align-items: center;
  grid-template-columns: 1fr;
}
.about__photo { max-width: 240px; margin-inline: auto; }
.about__photo picture, .about__photo img { border-radius: var(--radius); }
@media (min-width: 700px) {
  .about__inner { grid-template-columns: 240px 1fr; }
  .about__photo { margin-inline: 0; }
}

/* Contact */
.contact__inner {
  display: grid;
  gap: var(--space-7);
  grid-template-columns: 1fr;
}
@media (min-width: 880px) { .contact__inner { grid-template-columns: 1.2fr 1fr; } }

.contact-form { display: grid; gap: var(--space-5); }
.form-field { display: grid; gap: var(--space-2); }
.form-field label { font-size: 0.925rem; }
.form-field input, .form-field select, .form-field textarea {
  min-height: 44px;
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--white);
}
.form-field textarea { min-height: 120px; resize: vertical; }
.form-row { display: grid; gap: var(--space-5); grid-template-columns: 1fr; }
@media (min-width: 560px) { .form-row { grid-template-columns: 1fr 1fr; } }

.form-fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-4) var(--space-5); margin: 0; }
.form-fieldset legend { font-weight: 600; font-size: 0.925rem; padding: 0 var(--space-2); }
.form-checkboxes { display: grid; gap: var(--space-2); }
.form-checkboxes label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-weight: 400;
  min-height: 44px;
}
.form-checkboxes input { width: 20px; height: 20px; flex-shrink: 0; }

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

.contact-info { display: flex; flex-direction: column; gap: var(--space-4); align-self: start; }
.contact-info a {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-heading);
  font-size: 1.25rem;
  color: var(--ink);
  text-decoration: none;
}
.contact-info a:hover { color: var(--orange); }

/* -------------------------------------------------------------------------
   RANKING DEMO WIDGET ("Your Business Isn't Broken. It's Buried.")
   Animated before/after comparison: a muted desktop SERP mockup search
   traffic ignores, next to a phone mockup it funnels into instead.
   Pure CSS — hover-driven on desktop, tap-to-toggle on touch
   (`.trm-play-toggle` checkbox), static per prefers-reduced-motion.

   Every selector is scoped under .trm-ranking-widget and every class
   name is prefixed trm- so nothing here can collide with the rest of
   the site's CSS (generic names like "label", "hint", "check", "note"
   would otherwise be a real risk) and nothing from the rest of the
   site's CSS can reach in and restyle it. Reuses this file's own
   tokens (--font-heading, --font-body, --orange, --charcoal,
   --text-muted) directly; only defines new custom properties below
   for colors/shadows that have no existing equivalent (the dark
   device-shell gradient, the phone-screen accent palette).
   ------------------------------------------------------------------------- */

.trm-ranking-widget {
  --panel: #142226;
  --panel-2: #0D171A;
  --mist: rgba(255, 255, 255, 0.14);
  --hint-bg: rgba(255, 255, 255, 0.88);
  --hint-text: #33312B;
  --card-shadow: 0 24px 60px -24px rgba(20, 34, 38, 0.45);

  /* Brand colors, sampled directly from the Top Rung Marketing logo file. */
  --accent-search: #F5F1E8;
  --accent-gold: #F2B33D;
  --accent-gold-soft: #FFDE8A;
  --accent-green: #4FA85A;
  --muted-icon: #6B7178;
  --dull-screen: #454C54;
  --dull-screen-line: #5B636C;
  --charcoal-deep: #1A1A1A;

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  text-align: center;
}

.trm-ranking-widget .trm-bridge {
  font-family: var(--font-heading);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  margin: 0;
  max-width: none;
}

/* ---------- Widget shell (shared sizing/shadow for both widgets) ---------- */

.trm-ranking-widget .trm-phone-widget {
  --travel: 235px;
  --spd-base: 3s;
  position: relative;
  container-type: inline-size;
  width: 660px;
  max-width: 100%;
  aspect-ratio: 660 / 340;
  border-radius: 26px;
  overflow: hidden;
  background: linear-gradient(160deg, var(--panel), var(--panel-2));
  box-shadow: var(--card-shadow);
}

/* Everything inside is laid out on a fixed 660x340 grid, then scaled as a
   unit to whatever width max-width:100% leaves it (phones included). This
   keeps every mover's hand-tuned position proportionally correct instead of
   spilling past the right edge (and being clipped by overflow:hidden) once
   the widget is narrower than its natural 660px. */
.trm-ranking-widget .trm-stage {
  position: absolute;
  top: 0;
  left: 0;
  width: 660px;
  height: 340px;
  transform: scale(calc(100cqw / 660px));
  transform-origin: top left;
}

.trm-ranking-widget .trm-phone-widget::before {
  content: "";
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 60%;
  background: radial-gradient(ellipse at center, rgba(247, 100, 31, 0.12), transparent 70%);
  pointer-events: none;
}

.trm-ranking-widget .trm-play-toggle {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  top: 8px;
  left: 8px;
}

.trm-ranking-widget .trm-play-toggle:focus-visible ~ .trm-tap-target {
  outline: 2px solid var(--orange);
  outline-offset: -2px;
}

.trm-ranking-widget .trm-tap-target {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

.trm-ranking-widget .trm-hint {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  font-weight: 500;
  color: var(--hint-text);
  background: var(--hint-bg);
  padding: 6px 14px;
  border-radius: 999px;
  transition: opacity 0.35s ease;
  z-index: 4;
  white-space: nowrap;
  pointer-events: none;
}

.trm-ranking-widget .trm-phone-widget:hover .trm-hint,
.trm-ranking-widget .trm-phone-widget:focus-within .trm-hint {
  opacity: 0;
}

/* ---------- Phone ---------- */

.trm-ranking-widget .trm-phone {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 112px;
  height: 202px;
  background: linear-gradient(180deg, var(--charcoal), var(--charcoal-deep));
  border-radius: 22px;
  padding: 10px 8px;
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35), inset 0 0 0 1px var(--mist);
  z-index: 3;
}

.trm-ranking-widget .trm-phone .trm-notch {
  width: 30px;
  height: 5px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 4px;
  margin: 0 auto 7px;
}

.trm-ranking-widget .trm-phone .trm-screen {
  position: relative;
  width: 100%;
  height: calc(100% - 26px);
  border-radius: 13px;
  background: linear-gradient(150deg, var(--accent-search) 0%, #E8DFCE 40%, var(--orange) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
}

.trm-ranking-widget .trm-phone .trm-screen::after {
  content: "";
  position: absolute;
  inset: -16px;
  border-radius: 20px;
  background: radial-gradient(circle, rgba(247, 100, 31, 0.55), transparent 70%);
  filter: blur(6px);
  opacity: 0;
  z-index: -1;
}

.trm-ranking-widget .trm-phone .trm-screen .trm-site-label {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--charcoal-deep);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
  transform: translateY(-22px);
}

.trm-ranking-widget .trm-gbp-badge {
  position: absolute;
  top: 8px;
  left: 9px;
  right: 9px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  opacity: 0;
  pointer-events: none;
}

.trm-ranking-widget .trm-gbp-badge .trm-row1 {
  display: flex;
  align-items: center;
  gap: 4px;
}

.trm-ranking-widget .trm-gbp-badge .trm-check {
  flex: 0 0 14px;
  width: 14px;
  height: 14px;
  display: block;
  filter: drop-shadow(0 0 2.5px rgba(66, 133, 244, 0.75));
}

.trm-ranking-widget .trm-gbp-badge .trm-check svg {
  display: block;
  width: 100%;
  height: 100%;
}

.trm-ranking-widget .trm-gbp-badge .trm-label {
  flex: 1;
  height: 4px;
  border-radius: 2px;
  background: rgba(26, 26, 26, 0.55);
}

.trm-ranking-widget .trm-gbp-badge .trm-stars {
  padding-left: 17px;
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0.5px;
  color: var(--accent-gold);
  text-shadow: 0 0 3px rgba(242, 179, 61, 0.55);
}

@keyframes trm-badge-reveal {
  0%, 36%  { opacity: 0; }
  44%      { opacity: 1; }
  82%      { opacity: 1; }
  90%, 100% { opacity: 0; }
}

.trm-ranking-widget .trm-phone .trm-home-bar {
  width: 34px;
  height: 4px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 2px;
  margin: 7px auto 0;
}

@keyframes trm-glow-pulse {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}

@keyframes trm-phone-bob {
  0%, 100% { transform: translate(-50%, -50%) translateY(0); }
  50%      { transform: translate(-50%, -50%) translateY(-9px); }
}

/* ---------- Movers (shared by both streams) ---------- */

.trm-ranking-widget .trm-mover {
  position: absolute;
  width: 34px;
  height: 34px;
  opacity: 0;
  z-index: 2;
  pointer-events: none;
}

.trm-ranking-widget .trm-mover.trm-left  { left: 22px; }
/* Money starts right at the phone's physical right edge (386px — the
   phone sits at left:274px, 112px wide), not in the middle of the
   screen, so it's never drawn over "Your New Website" or the badge —
   it's only ever visible once it's already emerging from the side. */
.trm-ranking-widget .trm-mover.trm-right { left: 386px; top: 153px; z-index: 4; --travel: 201px; }

.trm-ranking-widget .trm-mover.trm-left.trm-lane-1 { top: 20px;  --dy: 128px;  }
.trm-ranking-widget .trm-mover.trm-left.trm-lane-2 { top: 92px;  --dy: 58px;   }
.trm-ranking-widget .trm-mover.trm-left.trm-lane-3 { top: 192px; --dy: -42px;  }
.trm-ranking-widget .trm-mover.trm-left.trm-lane-4 { top: 262px; --dy: -112px; }

.trm-ranking-widget .trm-mover.trm-right.trm-lane-1 { --dy: 128px;  }
.trm-ranking-widget .trm-mover.trm-right.trm-lane-2 { --dy: 58px;   }
.trm-ranking-widget .trm-mover.trm-right.trm-lane-3 { --dy: -42px;  }
.trm-ranking-widget .trm-mover.trm-right.trm-lane-4 { --dy: -112px; }

/* Special pair: once every 10s, the in-screen rungs climb to the
   badge (see .trm-rank-rungs below), and the moment it arrives, a big
   moneybag sprays out of the screen. */
.trm-ranking-widget .trm-mover.trm-special       { width: 46px; height: 46px; top: 147px; }
.trm-ranking-widget .trm-mover.trm-right.trm-special { left: 386px; }

@keyframes trm-special-out {
  0%,
  44.9% { opacity: 0; transform: translateX(0); }
  45%   { opacity: 1; transform: translateX(0); }
  80%   { opacity: 1; }
  86%,
  100%  { opacity: 0; transform: translateX(var(--travel)); }
}

.trm-ranking-widget .trm-mover .trm-bend {
  width: 100%;
  height: 100%;
}

@keyframes trm-drift {
  0%   { opacity: 0; transform: translateX(0); }
  16%  { opacity: 1; }
  64%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(var(--travel)); }
}

@keyframes trm-drift-out {
  0%   { opacity: 1; transform: translateX(0); }
  64%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(var(--travel)); }
}

@keyframes trm-bend {
  0%   { transform: translateY(0); }
  100% { transform: translateY(var(--dy)); }
}

/* ---------- In-screen rungs: climbing the ranks ---------- */

.trm-ranking-widget .trm-rank-rungs {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.trm-ranking-widget .trm-rank-rungs .trm-rung {
  position: absolute;
  left: 27px;
  right: 9px;
  height: 4px;
  border-radius: 2px;
  background: rgba(26, 26, 26, 0.55);
  opacity: 0;
}

.trm-ranking-widget .trm-rank-rungs .trm-rung.trm-r1 { top: 136px; }
.trm-ranking-widget .trm-rank-rungs .trm-rung.trm-r2 { top: 108px; }
.trm-ranking-widget .trm-rank-rungs .trm-rung.trm-r3 { top: 80px;  }

@keyframes trm-rung-flash {
  0%, 1.9%   { opacity: 0; }
  2%, 7.9%   { opacity: 1; }
  8%, 100%   { opacity: 0; }
}

@media (hover: hover) and (pointer: fine) {
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-1,  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-1  { animation: trm-drift var(--spd-base) ease-in-out 0s   infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-2,  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-2  { animation: trm-drift var(--spd-base) ease-in-out 0.9s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-3,  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-3  { animation: trm-drift var(--spd-base) ease-in-out 1.8s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-4,  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-4  { animation: trm-drift var(--spd-base) ease-in-out 2.7s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-1, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-1 { animation: trm-drift-out var(--spd-base) ease-in-out 1.5s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-2, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-2 { animation: trm-drift-out var(--spd-base) ease-in-out 2.4s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-3, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-3 { animation: trm-drift-out var(--spd-base) ease-in-out 3.3s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-4, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-4 { animation: trm-drift-out var(--spd-base) ease-in-out 4.2s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-1  .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-1  .trm-bend { animation: trm-bend var(--spd-base) ease-out 0s   infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-2  .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-2  .trm-bend { animation: trm-bend var(--spd-base) ease-out 0.9s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-3  .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-3  .trm-bend { animation: trm-bend var(--spd-base) ease-out 1.8s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-left.trm-lane-4  .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-left.trm-lane-4  .trm-bend { animation: trm-bend var(--spd-base) ease-out 2.7s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-1 .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-1 .trm-bend { animation: trm-bend var(--spd-base) ease-in  1.5s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-2 .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-2 .trm-bend { animation: trm-bend var(--spd-base) ease-in  2.4s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-3 .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-3 .trm-bend { animation: trm-bend var(--spd-base) ease-in  3.3s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-lane-4 .trm-bend, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-lane-4 .trm-bend { animation: trm-bend var(--spd-base) ease-in  4.2s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-mover.trm-right.trm-special, .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover.trm-right.trm-special { animation: trm-special-out 10s ease-in-out 0s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-rank-rungs .trm-rung.trm-r1, .trm-ranking-widget .trm-phone-widget:focus-within .trm-rank-rungs .trm-rung.trm-r1 { animation: trm-rung-flash 10s linear 0s    infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-rank-rungs .trm-rung.trm-r2, .trm-ranking-widget .trm-phone-widget:focus-within .trm-rank-rungs .trm-rung.trm-r2 { animation: trm-rung-flash 10s linear 1.2s infinite; }
  .trm-ranking-widget .trm-phone-widget:hover .trm-rank-rungs .trm-rung.trm-r3, .trm-ranking-widget .trm-phone-widget:focus-within .trm-rank-rungs .trm-rung.trm-r3 { animation: trm-rung-flash 10s linear 2.4s infinite; }

  .trm-ranking-widget .trm-phone-widget:hover .trm-gbp-badge, .trm-ranking-widget .trm-phone-widget:focus-within .trm-gbp-badge {
    animation: trm-badge-reveal 10s ease-in-out 0s infinite;
  }

  .trm-ranking-widget .trm-phone-widget:hover .trm-screen::after, .trm-ranking-widget .trm-phone-widget:focus-within .trm-screen::after {
    animation: trm-glow-pulse 2.4s ease-in-out infinite;
  }

  .trm-ranking-widget .trm-phone:hover {
    animation: trm-phone-bob 2.6s ease-in-out infinite;
  }
}

/* ---------- Touch fallback: tap to toggle, no hover needed ---------- */

@media (hover: none) {
  .trm-ranking-widget .trm-tap-target { pointer-events: auto; cursor: pointer; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-hint { opacity: 0; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-1  { animation: trm-drift var(--spd-base) ease-in-out 0s   infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-2  { animation: trm-drift var(--spd-base) ease-in-out 0.6s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-3  { animation: trm-drift var(--spd-base) ease-in-out 1.2s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-4  { animation: trm-drift var(--spd-base) ease-in-out 1.8s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-1 { animation: trm-drift-out var(--spd-base) ease-in-out 1.5s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-2 { animation: trm-drift-out var(--spd-base) ease-in-out 2.1s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-3 { animation: trm-drift-out var(--spd-base) ease-in-out 2.7s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-4 { animation: trm-drift-out var(--spd-base) ease-in-out 3.3s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-1  .trm-bend { animation: trm-bend var(--spd-base) ease-out 0s   infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-2  .trm-bend { animation: trm-bend var(--spd-base) ease-out 0.6s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-3  .trm-bend { animation: trm-bend var(--spd-base) ease-out 1.2s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-left.trm-lane-4  .trm-bend { animation: trm-bend var(--spd-base) ease-out 1.8s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-1 .trm-bend { animation: trm-bend var(--spd-base) ease-in  1.5s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-2 .trm-bend { animation: trm-bend var(--spd-base) ease-in  2.1s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-3 .trm-bend { animation: trm-bend var(--spd-base) ease-in  2.7s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-lane-4 .trm-bend { animation: trm-bend var(--spd-base) ease-in  3.3s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover.trm-right.trm-special { animation: trm-special-out 10s ease-in-out 0s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-rank-rungs .trm-rung.trm-r1 { animation: trm-rung-flash 10s linear 0s    infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-rank-rungs .trm-rung.trm-r2 { animation: trm-rung-flash 10s linear 1.2s infinite; }
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-rank-rungs .trm-rung.trm-r3 { animation: trm-rung-flash 10s linear 2.4s infinite; }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-gbp-badge {
    animation: trm-badge-reveal 10s ease-in-out 0s infinite;
  }

  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-screen::after {
    animation: trm-glow-pulse 2s ease-in-out infinite;
  }
}

/* ---------- Magnifying glass (search queries, left stream) ---------- */

.trm-ranking-widget .trm-glass {
  width: 100%;
  height: 100%;
  position: relative;
}

.trm-ranking-widget .trm-glass .trm-lens {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 4px solid var(--accent-search);
  background: rgba(245, 241, 232, 0.14);
}

.trm-ranking-widget .trm-glass .trm-handle {
  position: absolute;
  top: 18px;
  left: 19px;
  width: 13px;
  height: 4px;
  background: var(--accent-search);
  border-radius: 2px;
  transform: rotate(45deg);
}

/* ---------- Question mark (search queries, left stream) ---------- */

.trm-ranking-widget .trm-qmark {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 25px;
  color: var(--accent-search);
  text-shadow: 0 0 8px rgba(245, 241, 232, 0.4);
}

/* ---------- Coin ($, right stream) ---------- */

.trm-ranking-widget .trm-coin {
  width: 30px;
  height: 30px;
  margin: 2px auto;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, var(--accent-gold-soft), var(--accent-gold));
  box-shadow: inset 0 -3px 5px rgba(0, 0, 0, 0.22), inset 0 2px 2px rgba(255, 255, 255, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 16px;
  color: #7A4B00;
}

/* ---------- Dollar bill ($, right stream) ---------- */

.trm-ranking-widget .trm-bill {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.trm-ranking-widget .trm-bill .trm-note {
  width: 32px;
  height: 20px;
  border-radius: 3px;
  background: linear-gradient(160deg, #79CB7E, var(--accent-green));
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.45), inset 0 0 0 4px rgba(255, 255, 255, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: -0.5px;
  color: #F3FFF0;
}

/* ---------- Moneybag (special, right stream) ---------- */

.trm-ranking-widget .trm-moneybag-icon {
  display: block;
  width: 40px;
  height: 40px;
  margin: 3px auto;
}

.trm-ranking-widget .trm-moneybag-icon .trm-bag-ear,
.trm-ranking-widget .trm-moneybag-icon .trm-bag-tie {
  fill: var(--accent-gold);
}

/* .trm-bag-body's fill is set inline per-instance in phone-widget.njk
   (fill="url(#trm-moneybag-grad-{idSuffix})") rather than here, since the
   widget can appear more than once per page and SVG gradient ids must be
   unique — a CSS rule here could only ever point at one fixed id. */

.trm-ranking-widget .trm-moneybag-icon .trm-bag-grad-a { stop-color: var(--accent-gold-soft); }
.trm-ranking-widget .trm-moneybag-icon .trm-bag-grad-b { stop-color: var(--accent-gold); }

.trm-ranking-widget .trm-moneybag-icon .trm-bag-dollar {
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 32px;
  text-anchor: middle;
  fill: var(--accent-green);
}

/* ================= "Before" widget: a typical desktop site ================ */

.trm-ranking-widget .trm-desktop-widget {
  --travel-wide: 700px;
  position: relative;
  container-type: inline-size;
  width: 660px;
  max-width: 100%;
  aspect-ratio: 660 / 340;
  border-radius: 26px;
  overflow: hidden;
  background: linear-gradient(160deg, var(--panel), var(--panel-2));
  box-shadow: var(--card-shadow);
}

.trm-ranking-widget .trm-desktop-widget:hover .trm-hint,
.trm-ranking-widget .trm-desktop-widget:focus-within .trm-hint {
  opacity: 0;
}

.trm-ranking-widget .trm-monitor {
  position: absolute;
  left: 50%;
  top: 47%;
  transform: translate(-50%, -50%);
  z-index: 3;
}

.trm-ranking-widget .trm-monitor-screen {
  width: 194px;
  height: 128px;
  background: var(--dull-screen);
  border: 7px solid var(--charcoal);
  border-radius: 8px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.trm-ranking-widget .trm-serp-bar {
  flex: 0 0 auto;
  height: 12px;
  border-radius: 6px;
  background: var(--dull-screen-line);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 4px;
}

.trm-ranking-widget .trm-serp-bar-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1.5px solid var(--dull-screen);
}

.trm-ranking-widget .trm-serp-body {
  flex: 1;
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

.trm-ranking-widget .trm-serp-results {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 7px;
}

.trm-ranking-widget .trm-serp-row .trm-serp-title {
  height: 5px;
  width: 90%;
  border-radius: 2px;
  background: var(--dull-screen-line);
}

.trm-ranking-widget .trm-serp-row .trm-serp-snippet {
  height: 3px;
  width: 65%;
  margin-top: 3px;
  border-radius: 2px;
  background: var(--dull-screen-line);
}

.trm-ranking-widget .trm-serp-row.trm-r1 { opacity: 1; }
.trm-ranking-widget .trm-serp-row.trm-r2 { opacity: 0.65; }
.trm-ranking-widget .trm-serp-row.trm-r3 { opacity: 0.35; }

.trm-ranking-widget .trm-mini-ladder {
  position: relative;
  flex: 0 0 20px;
  width: 20px;
  height: 62px;
}

.trm-ranking-widget .trm-mini-ladder .trm-rail {
  position: absolute;
  top: 0;
  width: 2px;
  height: 100%;
  background: var(--muted-icon);
  border-radius: 1px;
}

.trm-ranking-widget .trm-mini-ladder .trm-rail.trm-l { left: 1px; }
.trm-ranking-widget .trm-mini-ladder .trm-rail.trm-r { right: 1px; }

.trm-ranking-widget .trm-mini-ladder .trm-rung {
  position: absolute;
  left: 50%;
  height: 2px;
  width: 14px;
  transform: translateX(-50%);
  background: var(--muted-icon);
  border-radius: 1px;
}

.trm-ranking-widget .trm-mini-ladder .trm-rung.trm-r1 { top: 4px; }
.trm-ranking-widget .trm-mini-ladder .trm-rung.trm-r2 { top: 22px; }
.trm-ranking-widget .trm-mini-ladder .trm-rung.trm-r3 { top: 40px; }

.trm-ranking-widget .trm-mini-ladder .trm-rung.trm-bottom {
  top: 56px;
  background: #9C6438;
  box-shadow: 0 0 4px 1px rgba(198, 110, 43, 0.5);
}

.trm-ranking-widget .trm-monitor-neck {
  width: 22px;
  height: 16px;
  background: var(--charcoal);
  margin: 0 auto;
}

.trm-ranking-widget .trm-monitor-base {
  width: 64px;
  height: 8px;
  border-radius: 4px;
  background: var(--charcoal);
  margin: 0 auto;
}

.trm-ranking-widget .trm-mover-wide {
  position: absolute;
  width: 30px;
  height: 30px;
  opacity: 0;
  left: -34px;
  pointer-events: none;
}

.trm-ranking-widget .trm-mover-wide.trm-over  { top: 46px;  z-index: 2; }
.trm-ranking-widget .trm-mover-wide.trm-thru  { top: 152px; z-index: 4; }
.trm-ranking-widget .trm-mover-wide.trm-under { top: 268px; z-index: 2; }

.trm-ranking-widget .trm-desktop-widget .trm-glass .trm-lens   { border-color: var(--muted-icon); background: rgba(107, 113, 120, 0.16); }
.trm-ranking-widget .trm-desktop-widget .trm-glass .trm-handle { background: var(--muted-icon); }
.trm-ranking-widget .trm-desktop-widget .trm-qmark             { color: var(--muted-icon); text-shadow: none; }

@keyframes trm-pass-through {
  0%   { opacity: 0; transform: translateX(0); }
  8%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(var(--travel-wide)); }
}

@media (hover: hover) and (pointer: fine) {
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-over.trm-item-1,  .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-over.trm-item-1  { animation: trm-pass-through 5.2s linear 0s    infinite; }
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-over.trm-item-2,  .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-over.trm-item-2  { animation: trm-pass-through 5.2s linear 2.6s  infinite; }
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-thru.trm-item-1,  .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-thru.trm-item-1  { animation: trm-pass-through 4.6s linear 0.8s  infinite; }
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-thru.trm-item-2,  .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-thru.trm-item-2  { animation: trm-pass-through 4.6s linear 3.4s  infinite; }
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-under.trm-item-1, .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-under.trm-item-1 { animation: trm-pass-through 6s    linear 1.6s  infinite; }
  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide.trm-under.trm-item-2, .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide.trm-under.trm-item-2 { animation: trm-pass-through 6s    linear 4.6s  infinite; }
}

@media (hover: none) {
  .trm-ranking-widget .trm-desktop-widget .trm-tap-target { pointer-events: auto; cursor: pointer; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-hint { opacity: 0; }

  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-over.trm-item-1  { animation: trm-pass-through 5.2s linear 0s   infinite; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-over.trm-item-2  { animation: trm-pass-through 5.2s linear 2.6s infinite; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-thru.trm-item-1  { animation: trm-pass-through 4.6s linear 0.8s infinite; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-thru.trm-item-2  { animation: trm-pass-through 4.6s linear 3.4s infinite; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-under.trm-item-1 { animation: trm-pass-through 6s   linear 1.6s infinite; }
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide.trm-under.trm-item-2 { animation: trm-pass-through 6s   linear 4.6s infinite; }
}

@media (prefers-reduced-motion: reduce) {
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover,
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover .trm-bend,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover .trm-bend,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover .trm-bend,
  .trm-ranking-widget .trm-phone-widget:hover .trm-screen::after,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-screen::after,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-screen::after,
  .trm-ranking-widget .trm-phone-widget:hover .trm-gbp-badge,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-gbp-badge,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-gbp-badge,
  .trm-ranking-widget .trm-phone-widget:hover .trm-rank-rungs .trm-rung,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-rank-rungs .trm-rung,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-rank-rungs .trm-rung,
  .trm-ranking-widget .trm-phone:hover {
    animation: none !important;
  }
  .trm-ranking-widget .trm-phone-widget:hover .trm-mover,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-mover,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-mover {
    opacity: 1;
    transform: none;
  }
  .trm-ranking-widget .trm-phone-widget:hover .trm-gbp-badge,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-gbp-badge,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-gbp-badge {
    opacity: 1;
  }
  .trm-ranking-widget .trm-phone-widget:hover .trm-rank-rungs .trm-rung,
  .trm-ranking-widget .trm-phone-widget:focus-within .trm-rank-rungs .trm-rung,
  .trm-ranking-widget .trm-play-toggle:checked ~ .trm-phone .trm-rank-rungs .trm-rung {
    opacity: 0;
  }

  .trm-ranking-widget .trm-desktop-widget:hover .trm-mover-wide,
  .trm-ranking-widget .trm-desktop-widget:focus-within .trm-mover-wide,
  .trm-ranking-widget #trm-play-toggle-desktop:checked ~ .trm-mover-wide {
    animation: none !important;
    opacity: 1;
    transform: none;
  }
}

/* On narrow screens, let the widget bleed out to the page's edges instead of
   sitting inside the .container gutter. The headline/copy above it stay at
   their normal reading width; only these two demo boxes go edge-to-edge,
   which buys a bit more size for an aspect ratio that's naturally short.
   (Placed after both widgets' own shell rules so it reliably wins the
   cascade regardless of selector order elsewhere in this file.) */
@media (max-width: 699px) {
  .trm-ranking-widget .trm-phone-widget,
  .trm-ranking-widget .trm-desktop-widget {
    width: calc(100% + 2 * var(--space-5));
    max-width: calc(100% + 2 * var(--space-5));
    margin-inline: calc(-1 * var(--space-5));
    border-radius: 0;
  }
}

/* -------------------------------------------------------------------------
   LADDER COMPONENT ("How We Get You Up the Ladder") — text-reveal ladder
   Six rows of large condensed all-caps titles, left-aligned, on a dark
   section (reuses .section--ink, below). Each title starts (once JS
   arms it) covered by a solid orange bar exactly the height of its own
   title line — not the whole row — so the covered state reads as six
   separate thick rungs with gaps between them, not one solid wall. Bar
   height and title font-size both come from one custom property,
   --ladder-bar-h (set per breakpoint/mode on .ladder__rung), so they
   can never drift apart. .ladder__title has a fixed width (in ch, so it
   scales with font-size) rather than sizing to its own text, so every
   bar is the same width regardless of how long each title is.

   At the row's right edge: two thin vertical rails (.ladder__rail,
   children of .ladder, spanning the full rung list) with a solid stub
   per row (.ladder__stub, inside .ladder__rail-col) sitting exactly
   between them — together they read as a small ladder. The stub's
   height also comes from --ladder-bar-h, so it always matches its
   bar. Stubs are always solid/static; only the two rails animate
   (scaleY from transform-origin:bottom, driven by overall scroll
   progress via --rails-p), "drawing" the ladder upward as the visitor
   climbs, complete once row 6 finishes.

   .ladder has max-width: var(--ladder-max-width) and is centered — the
   dark section background it sits in (.section--ink, applied to the
   whole <section>) stays full-bleed regardless.

   TWO REVEAL MECHANISMS, chosen by js/ladder-reveal.js at load:

   1. Pinned / scroll-scrubbed (html.ladder-pinned, the default when the
      viewport is tall enough): .ladder-scroll is a tall wrapper
      (height: var(--ladder-scroll-h)); .ladder-sticky pins inside it at
      top: var(--header-h) for the full scroll distance, sized to
      exactly one viewport (100svh - header height) so all six rows are
      visible at once the whole time. JS turns the wrapper's scroll
      position into six equal 0–1 segments and writes each row's
      progress to --p (plus one overall --rails-p); CSS alone maps
      those to the cover's scaleX, the description's opacity/
      translateY, and the rails' scaleY — no CSS transition involved,
      so it tracks the scrollbar directly with no lag and rewinds
      cleanly on scroll-up.

   2. Stacked / IntersectionObserver (the default, unscoped rules below):
      used when the viewport's too short to fit six rows (~560px
      available height), reduced motion is on, or JS doesn't run. Rows
      stack and scroll normally; see js/ladder-reveal.js for the
      one-shot reveal, staggered with plain setTimeout once row 1
      scrolls fully into view. (An earlier version tried driving this
      via a CSS class-toggle selector instead of direct inline style
      changes; despite correct transition declarations it reliably
      failed to animate at all — confirmed via the browser's Animation
      domain reporting zero animation-start events. Direct inline style
      changes to an explicit value, never cleared back to the
      stylesheet's value, is what actually animates.)

   Default state (no JS, JS still loading, reduced motion, or an edge
   case the active mechanism already resolves to "done") is always the
   FINISHED state: every title already uncovered (cover at its base
   scaleX(0)), rails already fully drawn (scaleY(1)), every stub already
   resting in place, every description already visible. It is never
   required to read the content.
   ------------------------------------------------------------------------- */

.ladder-widget { margin-bottom: var(--space-8); }

/* .section--ink (below, in LAYOUT) covers the h2/h3/p color swap for a
   dark background; the ranking-demo widget's own bridge label has a
   higher-specificity light-mode color that needs overriding here too. */
.ladder-section .trm-bridge { color: var(--text-on-dark); }

/* Tall scroll wrapper + pinned panel — inert (normal block flow) unless
   html.ladder-pinned is present, so no-JS/reduced-motion/short-viewport
   visitors never see a tall empty-feeling scroll region. */
.ladder-scroll {
  --ladder-scroll-h: 260svh; /* <-- adjust this to make the scroll section longer or shorter */
}
.ladder-sticky {
  width: 100%;
}
html.ladder-pinned .ladder-scroll {
  height: var(--ladder-scroll-h);
}
html.ladder-pinned .ladder-sticky {
  position: sticky;
  top: var(--header-h, var(--header-height));
  height: calc(100svh - var(--header-h, var(--header-height)));
  /* display:flex (row) with default align-items:stretch, not center —
     this is what makes .container actually inherit the full height
     below, rather than shrinking to its content's natural height. */
  display: flex;
  overflow: hidden;
}

.ladder {
  --ladder-max-width: 960px; /* <-- adjust this to change the ladder's width */
  --ladder-rail-w: 10px;
  --ladder-mark-w: 48px;
  max-width: var(--ladder-max-width);
  margin-inline: auto;
  position: relative;
  display: flex;
  flex-direction: column;
}
@media (min-width: 768px) {
  .ladder { --ladder-rail-w: 14px; --ladder-mark-w: 72px; }
}
html.ladder-pinned .ladder {
  width: 100%;
  height: 100%;
  justify-content: center;
}

/* One flat rail at the row list's right edge, close enough and wide
   enough to read as the ladder's side rail rather than an abstract line.
   Each row's .ladder__rung-mark is the only other ladder geometry: a
   short fragment jutting in from the rail and fading out, as if cropped
   by how close the "camera" is, rather than a full stub-and-second-rail
   diagram — see the comment on .ladder__rung-mark below. */
/* White along the climbed length, with an orange glow at the current
   leading/top edge — same "the light passes each rung, leaving it lit
   white behind" idea as the header logo's dark-mode climb (see LOGO /
   THEME TOGGLE). The gradient's stops are percentages of the rail's own
   (unscaled) box, so scaling it down with .ladder-pinned's scaleY below
   keeps that orange band pinned to the current visible top edge rather
   than sliding to a fixed pixel position. In the non-pinned default
   (scaleY(1), full height) this same gradient lands the orange glow at
   the top on its own — right where step 6 is — which is also why that
   row doesn't need separate rail styling to match the brief's "step 6
   stays orange" rule, just its own rung-mark below. */
.ladder__rail {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: var(--ladder-rail-w);
  border-radius: 3px;
  background: linear-gradient(to top, var(--paper) 0%, var(--paper) 80%, var(--orange) 100%);
  transform-origin: bottom;
  transform: scaleY(1);
}
html.ladder-pinned .ladder__rail {
  transform: scaleY(var(--rails-p, 0));
}

.ladder__rungs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column-reverse;
}
html.ladder-pinned .ladder__rungs {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
}

.ladder__rung {
  /* Title font-size AND bar/stub height all derive from this one value,
     so they can never drift out of sync with each other. Tuned so six
     rows' bar-height-plus-roughly-equal-gap fills the stacked layout's
     natural rhythm; the pinned override targets ~1/12th of the pinned
     panel's height, since each row gets 1/6th and the bar should take
     about half of that, leaving a gap roughly equal to the bar. */
  --ladder-bar-h: clamp(1.4rem, 1rem + 2.4vw, 2.75rem);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-3);
}
html.ladder-pinned .ladder__rung {
  /* The vw term (not just svh) matters here: at narrow/mobile widths the
     panel is tall but thin, and without a width-aware term this sizes
     titles tall enough to clip past the viewport edge under
     .ladder-sticky's overflow:hidden (confirmed via direct measurement —
     scrollWidth didn't even flag it, since it was genuinely clipped, not
     scrollable). 3.5vw dominates at narrow widths and shrinks the font
     enough for "01 Keyword Research" to fit on one line at 375px; at
     wide viewports the svh term plus the vw term together saturate the
     3.5rem cap well before width is ever a constraint again. */
  --ladder-bar-h: clamp(1.4rem, calc(3.5vw + 2svh), 3.5rem);
  flex: 1 1 0;
  min-height: 0;
  padding-block: 0;
}

.ladder__content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
@media (min-width: 768px) {
  .ladder__content {
    flex-direction: row;
    align-items: baseline;
    gap: var(--space-5);
  }
}

.ladder__title {
  position: relative;
  flex: 0 0 auto;
  width: 18ch;
  margin: 0;
  font-size: var(--ladder-bar-h);
  line-height: 1.05;
  text-transform: uppercase;
  text-align: left;
  color: var(--text-on-dark);
}

.ladder__title-text {
  display: block;
  white-space: nowrap;
}

.ladder__cover {
  position: absolute;
  inset: 0;
  background: var(--orange);
  transform-origin: right;
  transform: scaleX(0);
  transition: transform 0.6s ease-out;
}
html.ladder-pinned .ladder__cover {
  transition: none;
  transform: scaleX(clamp(0, calc(1 - var(--p, 0) / 0.6), 1));
}

/* A short rung fragment per row, jutting in from the rail and fading to
   nothing — reads as a close, cropped glimpse of a rung passing by (the
   brief the client described: "standing on the ladder, climbing it
   yourself") rather than a complete rung spanning to a second rail.
   Height matches the title bar via --ladder-bar-h so it still reads as
   "one rung per row."

   Two layers, same shape: an orange base (this element's own background)
   and a white ::after on top. Default state (no JS / reduced motion /
   stacked fallback) shows both at full opacity — the white layer wins,
   so it reads as already-climbed/lit, matching the rail's own default.
   Pinned mode instead sequences them against --p (the same per-row value
   driving the title cover, which only starts rising once the rail has
   grown up to this row's position): the orange base fades in first as
   the rail reaches the row, then the white layer crossfades in over the
   second half of that same reveal — "appears orange, settles white,"
   mirroring the header logo's dark-mode climb. Step 6 keeps its rung
   orange permanently (see .ladder__rung--top below) by just never
   showing the white layer. */
.ladder__rung-mark {
  position: relative;
  flex: 0 0 var(--ladder-mark-w);
  width: var(--ladder-mark-w);
  height: var(--ladder-bar-h);
  background: linear-gradient(to left, var(--orange) 0%, var(--orange) 28%, transparent 100%);
  opacity: 1;
}
.ladder__rung-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to left, var(--paper) 0%, var(--paper) 28%, transparent 100%);
  opacity: 1;
}
html.ladder-pinned .ladder__rung-mark {
  opacity: var(--p, 0);
}
html.ladder-pinned .ladder__rung-mark::after {
  opacity: clamp(0, calc((var(--p, 0) - 0.5) * 2), 1);
}
.ladder__rung--top .ladder__rung-mark::after,
html.ladder-pinned .ladder__rung--top .ladder__rung-mark::after {
  opacity: 0;
}

.ladder__desc {
  margin: 0;
  max-width: 48ch;
  font-size: 1rem;
  color: var(--text-muted-on-dark);
  opacity: 1;
  transform: none;
  transition: opacity 0.35s ease-out, transform 0.35s ease-out;
}
@media (min-width: 768px) {
  .ladder__desc { flex: 1 1 auto; }
}
html.ladder-pinned .ladder__desc {
  font-size: clamp(0.9rem, calc(0.2vw + 2svh), 1.2rem);
  transition: none;
  opacity: clamp(0, calc((var(--p, 0) - 0.6) / 0.4), 1);
  transform: translateY(calc((1 - clamp(0, calc((var(--p, 0) - 0.6) / 0.4), 1)) * 6px));
}

.ladder__cta {
  margin: var(--space-7) 0 0;
  text-align: center;
}

/* -------------------------------------------------------------------------
   DARK THEME OVERRIDES
   Everything above is written for light mode; html[data-theme="dark"] (see
   TOKENS/BASE for the --bg/--fg/body-level switch and HEADER for the logo
   itself) flips the page canvas and headings automatically. The rules
   below are the remaining section-by-section fixes for surfaces and text
   colors that are hardcoded to a specific light-mode value rather than
   driven by --bg/--fg, so they'd otherwise stay stuck light (or unreadable)
   once the rest of the page goes dark.

   None of this touches a single light-mode rule above — every selector
   here is purely additive, scoped under [data-theme="dark"], so light
   mode is pixel-identical to before this section existed. Two sections
   are deliberately left alone because they're already dark regardless of
   site theme, by original design, not a gap: .section--ink / the ladder
   section (always ink + white/text-on-dark text) and .site-footer
   (always ink + text-muted-on-dark). Two "surface" colors recur a lot
   below: var(--bg) (plain white -> ink) and var(--bar-bg) (paper's dark
   counterpart, #262626 -- a step lighter than --bg so cards/panels still
   read as distinct surfaces instead of vanishing into the page).
   ------------------------------------------------------------------------- */

/* Buttons */
[data-theme="dark"] .btn--outline { color: var(--paper); border-color: var(--line-on-dark); }
[data-theme="dark"] .btn--outline:hover { background: var(--paper); color: var(--ink); }

/* Cards (problem-grid, services-grid both use .card directly) */
[data-theme="dark"] .card { background: var(--bar-bg); border-color: var(--line-on-dark); }

/* Hero */
/* The sunset-at-dusk job used to live here as .hero's own background
   (a star field + a brown-to-orange gradient wash across the whole
   hero) back when the hero was just a flat dark panel with a photo
   strip at the bottom. The roofline redesign replaced that entirely:
   the sunset now lives in .hero__dusk's own gradient/glow/stars (HERO
   SCENE above), visible only above the diagonal, so .hero's own
   background underneath it only ever needs to read as plain roof
   material below that line — flat --ink, not a second, competing
   gradient that would show through/behind .hero__dusk's own clipped
   edge. */
[data-theme="dark"] .hero { background: var(--ink); }
[data-theme="dark"] .hero__eyebrow { color: var(--orange); }
/* .hero__headline--lead and --sweep own their dark-mode colors directly
   (see HERO SCENE above) — nothing needed here for the headline itself. */
[data-theme="dark"] .hero__subhead { color: var(--text-muted-on-dark); }
[data-theme="dark"] .hero__trust li { color: var(--text-muted-on-dark); }

/* The section right after the hero (partials/ranking-widget.njk, no
   own background otherwise — it'd just fall through to body's plain
   --bg/--ink anyway) gets an explicit override here too, stated
   directly rather than left to fall through, so it's obvious at a
   glance that this section is deliberately the same flat --ink as the
   hero's own roof color just above it (HERO further up) — the two meet
   with no seam because they're literally the same value, not because
   of any separate color-matching work. */
[data-theme="dark"] #ranking-demo { background: var(--ink); }
/* Same idea in light mode: without this, .hero (ending in --paper)
   butts directly against this section's own fallback (body's plain
   --bg, i.e. --white) — a real, visible step even after the hero
   photo's own mask fade (above) already brings its bottom all the way
   to --paper, since --paper (#F4F4F2) and --white (#FFFFFF) aren't the
   same color. Matching this section to --paper too removes that last
   step entirely rather than just softening it. */
html:not([data-theme="dark"]) #ranking-demo { background: var(--paper); }

/* .section--paper is used by "Who we work with" and the contact section */
[data-theme="dark"] .section--paper { background: var(--bar-bg); }

/* Trades */
[data-theme="dark"] .trade-tile { background: var(--bar-bg); border-color: var(--line-on-dark); }
[data-theme="dark"] .trades-ask { color: var(--text-muted-on-dark); }

/* Pricing — .plan-card--featured is already ink/text-on-dark regardless
   of site theme, so it needs no override; these rules reach the plain
   .plan-card (and, where the value is identical either way, harmlessly
   also match the featured card's own already-dark-correct elements). */
[data-theme="dark"] .plan-card { background: var(--bar-bg); border-color: var(--line-on-dark); }
[data-theme="dark"] .plan-card__price-detail { color: var(--text-muted-on-dark); }
[data-theme="dark"] .plan-card__features li { border-top-color: var(--line-on-dark); }
[data-theme="dark"] .addon-card { background: var(--bar-bg); }
[data-theme="dark"] .addon-card__desc { color: var(--text-muted-on-dark); }

/* How it works */
[data-theme="dark"] .step { border-top-color: var(--line-on-dark); }
[data-theme="dark"] .step__number { color: var(--text-muted-on-dark); }

/* Contact */
[data-theme="dark"] .form-field input,
[data-theme="dark"] .form-field select,
[data-theme="dark"] .form-field textarea { background: var(--bar-bg); border-color: var(--line-on-dark); }
[data-theme="dark"] .form-fieldset { border-color: var(--line-on-dark); }
[data-theme="dark"] .contact-info a { color: var(--text-on-dark); }

/* Utilities */
[data-theme="dark"] .text-muted { color: var(--text-muted-on-dark); }
[data-theme="dark"] .legal__inner .updated { color: var(--text-muted-on-dark); }

/* -------------------------------------------------------------------------
   LEGAL PAGES (privacy, terms) + 404 / thanks
   ------------------------------------------------------------------------- */
.legal { padding-block: var(--space-8); }
.legal__inner { max-width: var(--container-narrow); margin-inline: auto; padding-inline: var(--space-5); }
.legal__inner h1 { margin-bottom: var(--space-2); }
.legal__inner h2 { font-size: 1.35rem; margin-top: var(--space-7); }
.legal__inner .updated { color: var(--text-muted); font-family: var(--font-body); text-transform: none; margin-bottom: var(--space-6); }

.simple-page { padding-block: var(--space-9); text-align: center; }
.simple-page .container { max-width: var(--container-narrow); }

/* -------------------------------------------------------------------------
   UTILITIES
   ------------------------------------------------------------------------- */
.text-muted { color: var(--text-muted); }
.mt-0 { margin-top: 0; }
