/* ============================================================
   FAMOUS REGISTRAR — PAGE PATTERNS
   ============================================================ */

/* ================= HOME HERO ============================ */
.hero {
  position: relative;
  background: var(--grad-ink);
  color: var(--white);
  padding-block: clamp(3rem, 7vw, 6rem) clamp(3.5rem, 8vw, 7rem);
  overflow: hidden;
  isolation: isolate;
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grad-hero-glow);
  z-index: -1;
}

/* THE CODE WALL — the homepage's quiet signature move.
   Lines of the page's own subject matter (HTML, DNS, a domain
   search) set in mono and tinted white, with one amber caret,
   drifting slowly upward behind the hero. It is texture, not
   content: it makes the band feel like something is running.

   WHY IT IS A SIBLING OF .hero__photo AND NOT A BACKGROUND LAYER ON
   IT. The caret is a blinking element, and elements cannot live
   inside a background image — so the wall is markup (`aria-hidden`,
   and gone entirely in print, which sets `typography: none` on
   generated content anyway). Nothing is added to the hero's inner
   markup, which the homepage smoke assertions read.

   STACKING. `.hero` is `isolation: isolate`, so every negative
   z-index in here resolves against this band and not the page. The
   photo and the aurora sit at -1; the wall is pushed to 0 so that it
   paints over the photo rather than under it (both were -1, and two
   equal negatives settle by DOM order — the photo first, which would
   have hidden the wall completely). The hairline grid keeps -1 too,
   so the wall is above the photo AND above the grid, and the only
   things above the wall are the hero content and the glass card,
   which carry their own z-index: 1.

   WHY THE WALL IS AT 0.26 AND NOT BRIGHTER. It sits above the photo's
   ink scrim rather than beneath it, so its own opacity is the only
   thing keeping white text legible. At 0.26 the lightest composite
   any pixel can reach still lands the slate-300 sub-line at ~3.8:1
   and the white headline at ~8:1 — both above the 3:1 floor for
   large text. Anything brighter starts to read as content instead of
   atmosphere.

   The two masks are radial fades on BOTH axes, and the columns are
   spaced with `justify-content: space-around`, so the hard edges of
   the .hero__code element can never be seen — the wall dissolves in
   every direction. The drift is one line-box (22px) on a 34s
   alternate loop, slow enough to read as breathing.

   Desktop only: below 900px the visual column stacks under the
   search box and the band is short, so the wall is dropped rather
   than dimmed. Reduced motion freezes the drift and the caret; the
   wall is correct as a still. */

.hero__code {
  display: none;
}

@media (min-width: 900px) {
  .hero__code {
    display: flex;
    justify-content: space-around;
    gap: clamp(28px, 4vw, 72px);
    position: absolute;
    inset: -12% -2% -6% 32%;
    z-index: 0;
    overflow: hidden;
    color: #cfe0f0;
    opacity: 0.26;
    pointer-events: none;
    user-select: none;
    font-family: var(--font-mono);
    font-size: 11.5px;
    line-height: 22px;
    font-variant-ligatures: none;
    -webkit-mask-image: radial-gradient(
      115% 105% at 74% 42%,
      #000 0%,
      #000 30%,
      transparent 72%
    );
    mask-image: radial-gradient(
      115% 105% at 74% 42%,
      #000 0%,
      #000 30%,
      transparent 72%
    );
    animation: hero-code-drift 34s var(--ease-in-out) infinite alternate;
    will-change: transform;
  }

  /* Each column hangs from its own top offset so no two lines up
     and the wall never reads as a table. */
  .hero__code-col {
    flex: 0 0 auto;
    white-space: pre;
  }

  .hero__code-col--a {
    transform: translateY(3px);
  }

  .hero__code-col--b {
    transform: translateY(-14px);
    opacity: 0.8;
  }

  .hero__code-col--c {
    transform: translateY(-30px);
    opacity: 0.55;
  }

  .hero__code-line {
    display: block;
    white-space: pre;
  }

  /* One line carries a caret — the moment the wall stops being a
     texture and reads as a machine at work. Amber, blinking, and
     about as loud as a cursor ever should be. */
  .hero__code-caret {
    display: inline-block;
    width: 7px;
    height: 13px;
    margin-left: 4px;
    vertical-align: -2px;
    background: var(--amber-400);
    animation: hero-caret 1.1s steps(1, end) infinite;
  }

  @keyframes hero-code-drift {
    from {
      transform: translate3d(0, 0, 0);
    }
    to {
      transform: translate3d(0, -22px, 0);
    }
  }

  @keyframes hero-caret {
    0%,
    49% {
      opacity: 1;
    }
    50%,
    100% {
      opacity: 0.08;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .hero__code {
      animation: none;
    }

    .hero__code-caret {
      animation: none;
    }
  }
}

/* Hairline grid texture — subtle craft detail, not a pattern gimmick */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
      to right,
      rgba(255, 255, 255, 0.035) 1px,
      transparent 1px
    ),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(70% 60% at 60% 40%, #000 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(
    70% 60% at 60% 40%,
    #000 0%,
    transparent 100%
  );
  z-index: -1;
  pointer-events: none;
}

/* Photographic backdrop. A real photo of the work — a keyboard, a desk —
   says more to a plumber or a cafe owner than another abstract shape. It
   is duotoned to the brand palette at build time, so it cannot clash with
   the accent.

   The ink scrim is a gradient rather than a flat opacity because the
   "Popular right now" card on the right is translucent glass: an opacity
   high enough to make the photo visible on the far right would also leave
   bright keytops sitting directly behind white text. This keeps the left
   40% effectively solid behind the headline and the search box, and lets
   the photo through only where nothing has to stay readable. */
.hero__photo {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: linear-gradient(
      100deg,
      var(--ink-900) 0%,
      rgba(10, 18, 32, 0.9) 38%,
      rgba(10, 18, 32, 0.62) 72%,
      rgba(10, 18, 32, 0.45) 100%
    ),
    url("../img/photos/hero-workspace.jpg");
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
  pointer-events: none;
}

/* The aurora — three huge blurred light fields that drift slowly behind the
   hero content. They are what makes the band feel alive instead of a flat
   navy slab with a faded photo. Pure CSS: radial fades on blurred discs,
   zero image bytes, and the global reduced-motion block in base.css stills
   them for anyone who asks for that.

   Shared by BOTH dark heroes: the homepage `.hero` and the product pages'
   `.page-hero--ink`. The class name says "hero" rather than "page-hero"
   because the component predates the product-page adoption and renaming it
   would touch every homepage assertion in _harvest/_smoke.ps1 for no
   behavioural gain. Emit the same markup in either band. */
.hero__aurora {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

.hero__aurora i {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(70px);
  will-change: transform;
}

.hero__aurora .a1 {
  width: 46vw;
  height: 46vw;
  right: -10%;
  top: -24%;
  background: var(--aurora-brand);
  animation: hero-aurora-1 26s ease-in-out infinite alternate;
}

.hero__aurora .a2 {
  width: 36vw;
  height: 36vw;
  left: -8%;
  bottom: -30%;
  background: var(--aurora-teal);
  animation: hero-aurora-2 32s ease-in-out infinite alternate;
}

.hero__aurora .a3 {
  width: 24vw;
  height: 24vw;
  right: 22%;
  bottom: -16%;
  background: var(--aurora-amber);
  animation: hero-aurora-3 38s ease-in-out infinite alternate;
}

@keyframes hero-aurora-1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(-7%, 9%, 0) scale(1.14); }
}

@keyframes hero-aurora-2 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(8%, -7%, 0) scale(1.1); }
}

@keyframes hero-aurora-3 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(-10%, -12%, 0) scale(1.18); }
}

/* ============================================================
   AMBIENT GLOW — the same drifting light, on section bands
   ------------------------------------------------------------
   The homepage has the aurora; every other band on the site is a
   flat fill. That asymmetry is a large part of the "stationary"
   feeling: the first screen moves and then nothing ever does
   again, so the site reads as one animated page followed by
   twenty-six stills.

   This puts a single, very faint glow behind section content,
   so a page breathes as you scroll past it. Deliberately weak —
   a fraction of the hero's opacity, no blur cost worth
   measuring, and no movement large enough to notice as movement.
   The point is that the eye should find the page is not frozen,
   not that anything is animating.

   `will-change` is deliberately NOT set: these are large,
   long-lived elements, and promoting them to their own layer
   costs memory for a 6% opacity gradient that nobody is looking
   at directly. The homepage's smaller discs are the right place
   for that hint.
   ============================================================ */

.section,
.section--cloud {
  position: relative;
}

/* The glow is painted on ::before so it can never intercept a
   click or sit above a card's own background. Sections that
   already use a pseudo-element for their own wash are excluded
   below — a second ::before would silently replace it. */
.section::before,
.section--cloud::before {
  content: "";
  position: absolute;
  top: -10%;
  right: -4%;
  width: 44%;
  max-width: 620px;
  aspect-ratio: 1;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  background: radial-gradient(
    circle at center,
    rgba(18, 166, 111, 0.075) 0%,
    rgba(18, 166, 111, 0) 68%
  );
  animation: ambient-drift 30s var(--ease-in-out) infinite alternate;
}

/* Tinted bands get the teal partner instead, so the glow shifts
   with the surface rather than being the same wash everywhere. */
.section--cloud::before {
  background: radial-gradient(
    circle at center,
    rgba(14, 165, 165, 0.06) 0%,
    rgba(14, 165, 165, 0) 68%
  );
}

@keyframes ambient-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-6%, 7%, 0) scale(1.12); }
}

/* Every direct child that carries content has to sit above the
   glow. Without this the band text and the radial share a
   stacking context and the tint creeps over the copy. */
.section > *,
.section--cloud > * {
  position: relative;
  z-index: 1;
}

/* The dark bands already have a gradient doing this job, at a
   strength that would be wrong doubled up. The wash band is excluded
   for the opposite reason: its emerald tint IS the treatment, and an
   emerald glow on an emerald wash is just noise. */
.section--ink::before,
.section--wash::before,
.cta-band::before {
  content: none;
  animation: none;
}

/* A plain white band (no modifier) has nothing for the glow to tint, so the
   cut-off green disc at the viewport edge read as a rendering error. */
.section:not([class*="section--"])::before {
  content: none;
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .section::before,
  .section--cloud::before {
    animation: none;
  }
}

.hero__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

@media (max-width: 950px) {
  .hero__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-10);
  }
}

.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 5px 14px 5px 5px;
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
  font-size: var(--fs-sm);
  color: var(--slate-300);
  margin-bottom: var(--sp-6);
}

/* The hero badge's "Wholesale" tag is the shared `.flag`, scaled to sit inside
   a taller pill. It is the same claim in the same language as the plan
   ribbons, so it is the same component. */
.hero__badge b {
  background: var(--grad-amber);
  color: var(--ink-950);
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: var(--tracking-flag);
  text-transform: uppercase;
  padding: var(--chip-pad-y) var(--chip-pad-x);
  border-radius: var(--r-full);
  line-height: 1.5;
}

.hero h1 {
  color: var(--white);
  font-size: var(--fs-5xl);
  letter-spacing: var(--tracking-tighter);
  max-width: 17ch;
}

/* THE UNDERLINED PHRASE DOES NOT FIT A PHONE. `.underline` is nowrap — the
   amber stroke under "without the markup." is one unbroken line, and allowing
   the span to wrap would put a decorative bar across a wrapped line instead.
   Measured on the built page: the phrase renders 402px wide at the 44px clamp
   floor, against an h1 box of 273px at a 320 viewport, 343px at 390 and 371px
   at 420 — so on every phone from 320 to about 436px the tail of the headline
   ran off the edge, where `body`'s overflow-x: hidden silently ate it (no
   scrollbar, no warning, just a decapitated headline).

   The fix scales the whole h1, because the phrase is what the size has to
   serve: clamp(1.75rem, 9.2vw, 2.5rem) keeps phrase ≤ box at every width
   down to 320 (269 vs 273, and real phones have overlay scrollbars, so the
   true margin is larger than the headless measurement above). The landing
   range — 29px to 40px — is where every registrar this site is compared
   against sets its phone headline anyway.

   Above 440px the inherited clamp already fits (measured 403px of phrase
   against a 407px box at 441px), so desktop and tablet are untouched. */
@media (max-width: 440px) {
  .hero h1 {
    font-size: clamp(1.75rem, 9.2vw, 2.5rem);
  }
}

.hero h1 .accent {
  color: var(--brand-400);
}

.hero h1 .underline {
  position: relative;
  white-space: nowrap;
}

.hero h1 .underline::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.08em;
  height: 0.12em;
  border-radius: 4px;
  background: var(--grad-amber);
  opacity: 0.9;
}

.hero__sub {
  margin-top: var(--sp-6);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--slate-300);
  max-width: 54ch;
}

.hero__search {
  margin-top: var(--sp-8);
}

/* The hero's search is the page's primary action, and it was the same 44px
   button as every secondary bar on the site — while sitting inside the tallest
   field in the hero, which made the control a visitor is meant to press no
   bigger than the ones they are not. At 52px (the `--btn--lg` height) the
   primary action is also the largest control in the hero. Scoped to `.hero`,
   which only the homepage uses, so the inline search bars on the domains and
   WHOIS pages keep their compact 44px. */
.hero .domain-search .btn {
  height: 52px;
  padding-inline: var(--sp-6);
  font-size: var(--fs-md);
}

/* On a phone the hero search was the only domain bar that did not stack: it
   uses the dark re-skin of the base bar, and only `.domain-search--hero` (the
   domains page) had the narrow-screen rule, so the primary action's field and
   button stayed side by side at 375px and the placeholder truncated. Stack it
   like the hero variant does - full-width field above a full-width button -
   and give the field an explicit height, because in a row it was stretching to
   the 52px button and in a column it would collapse to its line-height. Scoped
   to `.hero` (the homepage only), so the compact inline bars on the domains
   and WHOIS pages are untouched. */
@media (max-width: 520px) {
  .hero .domain-search {
    flex-direction: column;
    align-items: stretch;
  }

  .hero .domain-search input[type="search"] {
    /* `flex: 0 0 auto` is NOT optional here, and its absence was a live bug.
       The base rule is `.domain-search input[type="search"] { flex: 1 }`,
       which is `flex-basis: 0%`. Once this container is a COLUMN, the main
       axis is vertical, so flex-basis — not `height` — decides the field's
       main size. `height: 52px` below was therefore inert and the homepage's
       primary action collapsed to a 21px line box on every phone. Verified
       in headless Edge at 492px: computed height 21px, and 52px the moment
       `flex-basis` stops winning. Do not remove. */
    flex: 0 0 auto;
    height: 52px;
  }

  .hero .domain-search .btn {
    width: 100%;
  }
}

/* The transfer escape hatch. The hero's one job is the search box, but a
   visitor who already owns a name is not here to register a new one, and
   every registrar we are measured against puts Transfer within one click
   of the fold. It sits under the chips, set small, so it reads as an
   option rather than a second call to action competing with the search. */
.hero__alt {
  margin-top: var(--sp-4);
  font-size: var(--fs-sm);
}

.hero__meta {
  margin-top: var(--sp-6);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  font-size: var(--fs-sm);
  color: var(--slate-400);
}

.hero__meta span {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.hero__meta svg {
  width: 15px;
  height: 15px;
  color: var(--brand-400);
  flex-shrink: 0;
}

/* ---------- Hero visual: glass panel of price rows ----------
   NOTE: `.hero__visual`'s own rule lives further down this file, next
   to `.visual-float`, because the padding-bottom there reserves the
   space the floating badge hangs into. Do not re-add it here. */

/* The glass card gets a faint brand-lit rim on its top edge — the classic
   "lit glass" cue — plus a hairline that brightens toward the light. */
.visual-card {
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 45%);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-top-color: rgba(124, 220, 179, 0.38);
  border-radius: var(--r-xl);
  padding: var(--sp-6);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
}

.visual-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.visual-card__title {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--slate-400);
}

.visual-card__rows {
  display: grid;
  gap: 2px;
  margin-top: var(--sp-4);
}

/* ============================================================
   HERO SPOTLIGHT — the card draws the eye down its own rows
   ------------------------------------------------------------
   The hero's right-hand card is the only thing on the page that
   looks like it is doing something, and it is the first thing a
   visitor's eye lands on after the headline. Right now it is a
   static list, so the hero reads as a picture rather than a
   screen.

   This adds a soft highlight that travels down the rows on a
   slow loop, so the card visibly cycles. Three deliberate choices:

   1. **It never moves the rows.** Only a background highlight
      travels. Sliding or fading the rows themselves would make
      the prices hard to read and would repaint text under a
      moving layer — the exact "flicker" the browser is trying to
      avoid. A travelling background is free of both problems.

   2. **Each row gets its own delay, hard-coded below**, rather
      than one shared animation. Five rows, five offsets, one
      shared cycle length, and the highlight is a single
      absolutely-positioned band — so the loop is seamless rather
      than five independent fades that drift apart.

   3. **No JavaScript, no timer, no state.** There is nothing to
      pause, nothing to reset, and it cannot get stuck. Hover and
      keyboard focus still work normally, and the hover rule
      below deliberately wins over the loop.

   Reduced motion stops it dead and leaves the card static.
   ============================================================ */

.visual-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  transition: background var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
  position: relative;
  isolation: isolate;
}

.visual-row::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: var(--r-md);
  background: linear-gradient(
    90deg,
    rgba(52, 196, 138, 0) 0%,
    rgba(52, 196, 138, 0.16) 45%,
    rgba(124, 220, 179, 0.2) 60%,
    rgba(52, 196, 138, 0) 100%
  );
  background-size: 220% 100%;
  background-position: 100% 0;
  opacity: 0;
  pointer-events: none;
  animation: vc-spotlight 9s var(--ease-in-out) infinite;
}

/* Staggered so the band walks down the list. The gaps leave a
   beat between rows, which is what makes it read as a scanning
   eye rather than a strobe. */
.visual-row:nth-child(1)::before { animation-delay: 0s; }
.visual-row:nth-child(2)::before { animation-delay: 1.5s; }
.visual-row:nth-child(3)::before { animation-delay: 3s; }
.visual-row:nth-child(4)::before { animation-delay: 4.5s; }
.visual-row:nth-child(5)::before { animation-delay: 6s; }

@keyframes vc-spotlight {
  0%   { opacity: 0;    background-position: 100% 0; }
  6%   { opacity: 1; }
  22%  { opacity: 1;    background-position: -60% 0; }
  34%  { opacity: 0; }
  100% { opacity: 0;    background-position: -60% 0; }
}

/* Hover and keyboard focus beat the loop. A visitor pointing at a
   row is asking a question about that row, and a moving
   highlight under the cursor answers nothing. */
.visual-row:hover::before,
.visual-row:focus-visible::before {
  animation-play-state: paused;
  opacity: 0;
}

.visual-row:focus-visible {
  background: rgba(255, 255, 255, 0.1);
  outline: 2px solid var(--brand-400);
  outline-offset: -2px;
}

/* A visitor who has asked for less movement gets a static card. */
@media (prefers-reduced-motion: reduce) {
  .visual-row::before {
    animation: none;
    opacity: 0;
  }
}

.visual-row:hover {
  background: rgba(255, 255, 255, 0.08);
  text-decoration: none;
  transform: translateX(3px);
}

.visual-row__name {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--white);
  font-weight: 600;
  font-size: var(--fs-base);
}

.visual-row__name svg {
  width: 17px;
  height: 17px;
  color: var(--brand-400);
  flex-shrink: 0;
}

.visual-row__price {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--brand-300);
  font-size: var(--fs-base);
  white-space: nowrap;
}

/* The pricing disclaimer, and nothing else.

   This used to be a two-up row: the disclaimer on the left and a
   "See all" link into /hosting/ on the right. The link is gone —
   it duplicated a destination the hero's own buttons and the
   category grid already offer one screen away, and in a card that
   narrow it was competing with the one sentence that keeps the
   prices honest. What is left is a single block of small print,
   so the flex and its space-between are gone with it and the
   measure is capped instead: a disclaimer stretched to the full
   card width is a wall of grey text. */
.visual-card__foot {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  /* This line carries the first-term/renewal distinction for every
     row in the card above it, so it is body copy, not a caption:
     15/16px and slate-300 rather than 13px slate-400, which on ink
     measured 5.6:1 and read as decoration. Widened to 52ch to suit
     the larger size (it was set to 46ch for 13px). */
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--slate-300);
  /* Kept at the original 46ch measure. The type is larger now, so 46ch
     is a wider box than it was — enough to stay clear of the floating
     24/7 badge that hangs into this corner of the card. */
  max-width: 46ch;
}

/* Floating accent badge.

   POSITIONING, AND WHY IT MOVED DOWN.

   This badge used to sit at `bottom: -18px`, which parked it on top of
   the card's own footer — the ICANN/pricing disclaimer and the "See
   all" link that has since been removed. The `hero-float-bob`
   animation then lifted it another 7px every cycle, so it drifted
   further over the text as it bobbed. The
   disclaimer is the least skippable text on the page: it is the
   sentence that keeps the prices honest, and a badge sitting on top
   of it is exactly the wrong thing to obscure.

   The fix is to stop the badge overlapping anything:

   - `.hero__visual` reserves space for it via padding-bottom, so the
     card no longer runs to the very bottom of the column.
   - The badge drops below the card (`bottom: -26px`) and shifts right
     (`right: -18px`), so it overlaps the empty margin beside the
     footer rather than the footer's text.
   - The bob moves DOWN, not up. Lifting the badge by 7px was the
     direct cause of the collision; bobbing downward away from the
     card cannot cause it.

   The `hero__visual` padding is the part that actually guarantees
   this: even if the badge is repositioned later, the space for it is
   already reserved, so it cannot land on the disclaimer again. */
.hero__visual {
  position: relative;
  /* Room for the badge to hang below the card without covering the
     card's footer. Zero on narrow screens, where the badge is
     re-laid-out inline below the card instead (see the media query). */
  padding-bottom: var(--sp-10);
}

.visual-float {
  position: absolute;
  right: -18px;
  bottom: -26px;
  background: var(--white);
  color: var(--ink-900);
  border-radius: var(--r-lg);
  padding: var(--sp-4) var(--sp-5);
  box-shadow: var(--sh-xl);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 650;
  max-width: 260px;
  /* Bobs AWAY from the card — downward, into the space reserved for
     it. See the note above. */
  animation: hero-float-bob 7s ease-in-out infinite alternate;
}

@keyframes hero-float-bob {
  from { transform: translateY(0); }
  to   { transform: translateY(7px); }
}

/* Below the two-column breakpoint the hero stacks, and an absolutely
   positioned badge hanging off the right edge of a full-width card
   was overlapping the card's own footer on a phone — the same
   collision, at a smaller size, where there is less room for it.

   So on narrow screens the badge stops floating entirely: it drops
   into the normal flow, becomes a full-width strip under the card,
   and stops bobbing. It is still the last thing in the column, so
   the eye still meets it, and there is now nothing for it to cover
   because the card has already ended. */
/* The hero grid itself stacks at 950px (see `.hero__inner` above), so
   this query is 950px too — deliberately the same number rather than a
   rounder 900. A breakpoint fifty pixels apart would leave a narrow
   band where the card is already full-width but the badge is still
   absolutely positioned, which is the exact collision being fixed.
   The two must change together or the bug returns in the gap. */
@media (max-width: 950px) {
  .hero__visual {
    padding-bottom: 0;
  }

  .visual-float {
    position: static;
    max-width: none;
    margin-top: var(--sp-4);
    animation: none;
  }
}

.visual-float svg {
  width: 20px;
  height: 20px;
  color: var(--brand-600);
  flex-shrink: 0;
}

/* The 24/7 support seal replaces the white pill outright: no plate, no
   padding — the artwork declares its own gold edge — just a drop shadow
   to sit it on the hero's ink, and the requested 15° sticker tilt. The
   tilt lives on the <img> so the container's float bob (a transform)
   never fights it. */
.visual-float--badge {
  background: none;
  padding: 0;
  box-shadow: none;
  border-radius: 0;
  max-width: none;
}

.visual-float--badge img {
  width: 124px;
  height: auto;
  transform: rotate(-15deg);
  filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.35));
}

/* On the stacked layout the badge strip is full-width with no plate —
   centre the seal so it doesn't hug the left edge. */
@media (max-width: 950px) {
  .visual-float--badge {
    justify-content: center;
  }
}

@media (max-width: 500px) {
  .visual-float {
    right: 0;
    bottom: -14px;
  }
}

/* ================= DOMAINS REGISTRATION HERO ============
   Lives in its own file, assets/css/domains-hero.css, loaded by
   domains/index.html only. */

/* ================= PILLAR / PRODUCT TILES =============== */
/* A third-party brand mark sitting above a centred section head — the
   WordPress badge over "Most popular". Centred with margin-inline rather
   than text-align, because an <img> is a block element: the heading's
   centring would leave it hard against the left edge.

   80px is the size it is drawn at, so its file is 160px. It carries real
   alt text ("WordPress") rather than an empty one, because unlike the
   decorative ornament this replaced, it identifies which software the
   plans below run on. The footer's third-party names disclaimer is what
   covers using another company's mark this way. */
.section-head__brand {
  display: block;
  width: 80px;
  height: auto;
  margin: 0 auto var(--sp-5);
}

.pillar {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-6);
  border: 1px solid rgba(226, 232, 240, 0.8);
  border-radius: var(--r-xl);
  background: var(--white);
  position: relative;
  overflow: hidden;
  box-shadow: 0 4px 20px -2px rgba(10, 18, 32, 0.05), 0 1px 3px rgba(10, 18, 32, 0.03);
  transition: border-color var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

.pillar::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--grad-brand);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.pillar:hover {
  border-color: var(--brand-300);
  box-shadow: 0 12px 32px -4px rgba(10, 129, 88, 0.12), 0 4px 12px -2px rgba(10, 18, 32, 0.06);
  transform: translateY(-5px);
}

.pillar:hover::before {
  opacity: 1;
}

.pillar__icon {
  width: 50px;
  height: 50px;
  border-radius: var(--r-lg);
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, var(--cloud-100) 0%, var(--brand-50) 100%);
  color: var(--brand-600);
  border: 1px solid var(--brand-100);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 2px 4px rgba(10, 18, 32, 0.04);
  transition: transform var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out);
}

.pillar:hover .pillar__icon {
  transform: translateY(-2px) scale(1.06);
  background: var(--brand-600);
  color: var(--white);
  border-color: var(--brand-600);
}

.pillar__icon svg {
  width: 23px;
  height: 23px;
}

/* ============================================================
   CATEGORY MARKS — six icons on a line above the pillar grid
   ------------------------------------------------------------
   The homepage heading claims "Six categories", and the six pillar
   cards below it each carry an icon — but between the claim and the
   evidence there is nothing. This row is the connective tissue:
   the same six marks, same order, sitting directly above the grid
   they correspond to.

   Each mark is a glyph plus a short rule beneath it, and the rules
   are what make it read as a counted set rather than a toolbar of
   icons. The glyphs brighten in sequence once on load, then hold —
   the panel is assembled, not looping. A loop here would compete
   with the hero spotlight, which is already the only moving thing
   above the fold.

   Entirely decorative (`aria-hidden`): the six cards underneath
   carry the real content, and a screen reader gains nothing from
   six repeated icons.
   ============================================================ */

.cat-marks {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(var(--sp-4), 4vw, var(--sp-10));
  margin-bottom: var(--sp-12);
  flex-wrap: wrap;
}

.cat-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
}

.cat-mark__glyph {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: var(--r-md);
  background: linear-gradient(140deg, var(--brand-100) 0%, var(--brand-50) 60%, var(--white) 100%);
  border: 1px solid var(--brand-100);
  color: var(--brand-600);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), var(--sh-xs);
  opacity: 0;
  animation: cat-mark-in 520ms var(--ease-out) forwards;
  /* Staggered on the way in — the six arrive left to right, which
     is the direction the eye already travels. */
  animation-delay: calc(var(--i) * 70ms);
}

.cat-mark__glyph svg {
  width: 23px;
  height: 23px;
}

/* The rule grows out from the centre of each mark. It is the
   "tick" that turns six separate icons into a counted set. */
.cat-mark__rule {
  width: 26px;
  height: 3px;
  border-radius: var(--r-full);
  background: var(--brand-400);
  transform: scaleX(0);
  transform-origin: center;
  animation: cat-rule-in 460ms var(--ease-out) forwards;
  animation-delay: calc(var(--i) * 70ms + 180ms);
}

@keyframes cat-mark-in {
  from { opacity: 0; transform: translateY(-6px) scale(0.92); }
  to   { opacity: 1; transform: none; }
}

@keyframes cat-rule-in {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .cat-mark__glyph,
  .cat-mark__rule {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* On a narrow phone six 46px marks plus gaps is a squeeze, so the
   row wraps to three and the pair still reads as a set of six. */
@media (max-width: 520px) {
  .cat-marks {
    gap: var(--sp-4) var(--sp-6);
  }

  .cat-mark__glyph {
    width: 40px;
    height: 40px;
  }

  .cat-mark__glyph svg {
    width: 20px;
    height: 20px;
  }
}

.pillar h3 {
  font-size: var(--fs-lg);
  letter-spacing: var(--tracking-tight);
}

.pillar h3 a {
  color: inherit;
}

.pillar p {
  color: var(--slate-600);
  font-size: var(--fs-sm);
  line-height: 1.55;
  margin: 0;
}

.pillar__price {
  margin-top: auto;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--slate-200);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  /* slate-500 was the only colour in the card that failed 4.5:1
     against the card fill. The term line now carries "first term",
     which is a fact rather than a label, so it has to be readable:
     slate-600 measures 7.5:1 on white. */
  color: var(--slate-600);
}

.pillar__price b {
  font-family: var(--font-display);
  /* The number is what a budget shopper scans the card for, so it
     outranks the card's own h3 (`--fs-xl`) rather than trailing it
     at `--fs-lg` — the previous size made the plan name and the
     price the same height, which is the wrong way round. */
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  color: var(--ink-900);
  font-variant-numeric: tabular-nums;
}

/* ================= SPLIT (text + visual) ================ */
.split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

@media (max-width: 860px) {
  .split {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-10);
  }
}

.split--reverse .split__media {
  order: -1;
}

@media (max-width: 860px) {
  .split--reverse .split__media {
    order: 0;
  }
}

/* The receipt band on /domains/. `start` rather than `.split`'s default
   `center`, so the heading sits level with the top of the bill instead of
   floating down to the middle of a five-row table. The leading heading loses
   its own top margin so the two columns actually start together. */
.split--receipt {
  align-items: start;
}

/* The receipt is capped at 32rem; push it to the right edge of the grid so it
   lines up with the price cards above instead of floating short of them. */
.split--receipt .receipt {
  justify-self: end;
  width: 100%;
}

.split--receipt .prose > :first-child {
  margin-top: 0;
}

.split__media {
  border-radius: var(--r-xl);
  overflow: hidden;
  border: 1px solid var(--slate-200);
  background: var(--cloud-100);
  padding: var(--sp-8);
  box-shadow: var(--sh-lg);
}

.split__media--ink {
  background: var(--grad-ink);
  border-color: transparent;
}

/* A device or product shot that arrives as a transparent PNG — the laptop on
   the website builder page, before that shot moved down to `--bare`. It was
   previously mounted on a gradient plate with a border, padding and a shadow,
   which drew a rectangle around artwork that already has a real edge of its
   own: two competing outlines for one object. Now the panel is switched off
   entirely and the cut-out sits on the page.

   The transparency is what makes this possible, and it is a contract
   rather than a detail. Replace this file with a screenshot that has
   a background baked in — a JPEG, or a PNG that was flattened — and
   that background appears as a hard rectangle against the section.
   The van has the opposite problem and therefore the opposite rule:
   see `.split__media--cutout` below. */
.split__media--screen {
  padding: 0;
  background: none;
  border-color: transparent;
  box-shadow: none;
}

.split__media--screen img {
  display: block;
  width: 100%;
  height: auto;
}

/* Photograph on its own, with its shadow already in the picture. The
   van needs no plate, no frame and no padding — anything around it
   would draw a second, competing edge.

   The background here is not decoration, it is part of a contract.
   The image is a JPEG, so it has no alpha channel: its soft ground
   shadow is baked onto white. This panel has to stay the same white
   the image was composited onto, or a hard rectangle appears where
   the shadow should fade out. See _harvest/_van.mjs. */
.split__media--cutout {
  padding: 0;
  background: var(--white);
  border-color: transparent;
  box-shadow: none;
}

.split__media--cutout img {
  display: block;
  width: 100%;
  height: auto;
}

/* Artwork that is its own edge: a true transparent cut-out or a logo,
   dropped onto the band with nothing around it at all.

   `--screen` and `--cutout` both keep `border-radius` and, more
   importantly, the inherited `overflow: hidden`, because the images
   they were written for are photographs with a soft ground shadow
   and a rounded card is what that shadow fades into. A cut-out has
   no shadow to fade — it has a real silhouette. So this variant
   clears the radius AND the overflow, which matters more than it
   sounds: `overflow: hidden` on a rounded box shaves the artwork's
   own corners. Measured on the builder laptop, 14 opaque pixels sit
   inside the 20px radius and were being clipped — the top-left and
   top-right of the laptop's screen bezel lost a sliver, which reads
   as a rendering fault rather than a crop.

   The `border: 0` is deliberate rather than `border-color:
   transparent`. A transparent 1px border still occupies layout
   space and still rounds, so the image is inset by 1px inside a
   radius that is then clipping it. Removing the border removes the
   box.

   CONTRACT: the file must be a PNG with a real alpha channel and
   genuinely transparent corners. Swap in a JPEG — or a PNG that has
   been flattened — and its background becomes a hard rectangle
   against the band. `dropSpecks`/`clampAlpha` in _harvest/_img.mjs
   are what make the edge safe to sit on a coloured section. */
.split__media--bare {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
}

.split__media--bare img {
  display: block;
  width: 100%;
  height: auto;
}

/* ================= PRODUCT IMAGE MOTION ==================

   Three motions on the artwork, kept together because they share one
   rule: each has to resolve to a static, complete image for a visitor
   who has asked for less movement. These files carry the product, and
   a half-played entrance on the thing you are trying to sell is worse
   than no entrance at all. */

/* 1. THE SETTLE.

   Every product shot now arrives and then stops. The generic reveal
   lifts 16px; artwork wants less travel and a little scale, so this
   overrides the transform for media panels only. Opacity timing is
   inherited, so the two entrances still feel like one system rather
   than two things that happen to fade.

   The `.js` prefix is the same one the reveal system uses: without
   scripting nothing is hidden, so nothing may be hidden by this. */
.js .split__media[data-reveal] {
  transform: translateY(10px) scale(0.965);
}

.js .split__media[data-reveal].is-revealed {
  transform: none;
}

/* 2. THE TILT — bare cut-outs only.

   A laptop and a terminal are the only genuinely three-dimensional
   objects on this site, and a 2 degree tilt toward the pointer is what
   makes them read as objects rather than as pictures of objects. It is
   scoped to `--bare` on purpose: the framed screenshots must stay flat,
   because a flat screenshot that tilts looks like a sticker.

   The transform lives on the `img` while the settle above lives on the
   wrapper, so the two can never fight over the same property. Both
   custom properties default to zero, which means a touch device — or a
   visitor without hover — gets a stationary object and a harmless
   transition rather than a broken one. The pointer tracking that sets
   them is in site.js and is skipped entirely under reduced motion.

   A brand mark is excluded. The cPanel and WordPress wordmarks also
   live in `--bare` panels — they have real alpha channels, so the same
   contract applies to them — but a flat logo pivoting reads as cheap
   rather than dimensional.
   The effect only earns its place on something that has depth.
   `:not(:has(...))` is the filter; where `:has` is unsupported the tilt
   reaches the logo too, which is a cosmetic loss rather than a broken
   layout. */
.split__media--bare:not(:has(.panel-mark)) img {
  transform: perspective(1000px)
    rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg));
  transition: transform 260ms var(--ease-out);
}

/* 3. THE SHEEN — framed screenshots only.

   Glass catches light; a photograph of a van does not, which is why
   this is `--screen` only and not `--cutout`. One pass, on hover, at
   an angle: a viewer should feel the surface catch the light without
   ever catching the animation itself.

   The pseudo-element is wider than the panel and starts off to the
   left, so the sweep is clipped by the inherited `overflow: hidden`
   and never draws outside the rounded card. The base state is
   invisible AND off-panel, so removing the hover state (rather than
   finishing an animation) is what returns it to rest — no `forwards`
   fill, nothing left behind. */
.split__media--screen {
  position: relative;
}

.split__media--screen::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -40%;
  width: 40%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    100deg,
    transparent,
    rgba(255, 255, 255, 0.6),
    transparent
  );
  transform: translateX(-160%) skewX(-12deg);
}

@media (hover: hover) {
  .split__media--screen:hover::after {
    animation: screen-sheen 1000ms var(--ease-out);
  }
}

@keyframes screen-sheen {
  0% {
    transform: translateX(-160%) skewX(-12deg);
    opacity: 0;
  }
  15% {
    opacity: 1;
  }
  100% {
    transform: translateX(360%) skewX(-12deg);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* A sheen IS movement, so there isn't a reduced version of one —
     the surface just stops catching light. The settle and the tilt
     both resolve to a finished, static image on their own: the tilt
     defaults to zero degrees, and the settle's end state is the
     un-transformed image, which is also what it falls back to when
     `.is-revealed` is never added. */
  .split__media--screen::after {
    display: none;
  }
}

/* A third-party brand mark — the cPanel and WordPress logos on their
   hosting pages. A logo is not a photograph: it sits bare on the
   section (the panel carries the `--bare` modifier, so there is no
   plate, frame or shadow around it), keeps its own colours, and is
   drawn at half its file size so it stays sharp on a high-density
   screen. The line underneath credits the owner, because the mark is
   not ours. */
.panel-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  text-align: center;
  min-height: 200px;
}

.panel-mark img {
  display: block;
  width: 200px;
  height: auto;
}

.panel-mark p {
  margin: 0;
  max-width: 34ch;
}

/* ---------- The same mark, drawn inline ----------
   The email page's Titan band. It reuses this component because "a vendor's
   mark, credited" is one idea and should look like one thing wherever it
   appears; what differs is only how the artwork arrives.

   WordPress's and cPanel's marks are files whose wordmark is part of the
   image, so 200px of <img> is the right answer for those. The Titan mark is
   the inline SVG from ICONS.titan — a square icon with no word in it,
   recoloured to the house accent — so it needs a badge of its own and the
   name set under it, or the band reads as an unexplained glyph. Adding a
   file instead would mean shipping a five-kilobyte download, and a registry
   entry, to draw something the page already has. */
.panel-mark__mark {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--r-lg);
  background: var(--brand-50);
  border: 1px solid var(--brand-100);
  color: var(--brand-600);
}

.panel-mark__mark svg {
  width: 32px;
  height: 32px;
}

.panel-mark__name {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink-900);
}

/* Soft radial lighting behind a wash-band heading (added 2026-10-16).

   The blueprint grid and scan-line are even field textures — they
   cover the whole band equally. This is the piece that gives the
   band a focal point: a wide, very low-alpha emerald radial sitting
   behind the section-head, so the heading reads as the lit part of
   the surface rather than as text placed on top of a flat one.

   Scoped to `.section--wash .section-head` only, so the cloud and
   plain bands (which already get the ambient-drift glow on their
   own ::before) do not double up on lighting.

   ::before is free here: .section-head has no pseudo-element of its
   own anywhere in the design system, and the wash band's own
   ::before (see pages.css) is on the SECTION, not on the head, so
   the two never collide.

   position: relative + z-index: 0 on the head creates a stacking
   context so the -1 z-index pseudo paints behind the head's own
   text without falling behind the band's ::before glow layer. The
   head also becomes the containing block for the absolutely
   positioned radial, which is what pins it behind the heading text
   rather than centring it on the whole band. */
.section--wash .section-head {
  position: relative;
  z-index: 0;
}

.section--wash .section-head::before {
  content: "";
  position: absolute;
  /* The vertical inset is generous and the horizontal inset is zero on
     purpose: a wash band does not clip its own children, so a pseudo
     that hangs off the side extends the document's scroll width and
     the page gains a horizontal scrollbar. Measured: -12% horizontal
     added 11px of overflow at 390px; 0% adds none. The radial is
     wider than it is tall for the same reason it is an ellipse — the
     head is a wide block, and a circle would leave the corners dark. */
  inset: -18% 0;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(
    50% 55% at 50% 42%,
    rgba(10, 129, 88, 0.075) 0%,
    rgba(10, 129, 88, 0.03) 44%,
    rgba(10, 129, 88, 0) 72%
  );
}

/* ================= TABLES =============================== */
/* The table card (refined 2026-10-16).

   On a wash band a table used to sit flat: same visual weight as the
   band's own texture, 1px slate border, no lift. The refinement
   gives it three things a data table wants when it rests on a tinted
   surface — a mid-tier shadow so it reads as a card ON the band, a
   slightly warmer border (slate-300 instead of slate-200, which was
   nearly invisible against brand-50), and one step more radius so
   its corners sit at the same family as the notice and the band
   cards.

   Both the shadow and the radius are deliberately restrained: this
   same .table-wrap also renders inside the VPS page's accordion
   (hosting/vps/index.html), where a heavy shadow would read as a
   card floating inside a card. --sh-md and --r-xl are the tokens
   that survive both contexts. */
/* The separation between the band's texture and the card's own
   surface is a single inset hairline rather than a second border: an
   inset box-shadow sits inside the border box, so it cannot change
   the layout box, needs no positioning context (which matters here —
   this element is a scroll container, and adding position: relative
   to it would become the containing block for anything absolutely
   positioned inside), and stays put through the horizontal scroll. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--slate-300);
  border-radius: var(--r-xl);
  background: var(--white);
  box-shadow:
    inset 0 0 0 1px rgba(10, 129, 88, 0.06),
    var(--sh-md);
  -webkit-overflow-scrolling: touch;
}

table.data-table {
  width: 100%;
  border-collapse: collapse;
  min-width: 620px;
  font-size: var(--fs-sm);
}

.data-table caption {
  text-align: left;
  padding: var(--sp-4) var(--sp-5);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--slate-500);
  border-bottom: 1px solid var(--slate-200);
}

.data-table th,
.data-table td {
  text-align: left;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--slate-200);
  vertical-align: top;
}

.data-table thead th {
  background: var(--cloud-100);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--slate-600);
  white-space: nowrap;
}

.data-table tbody tr:last-child td {
  border-bottom: 0;
}

.data-table tbody tr:hover {
  background: var(--cloud-50);
}

.data-table td strong {
  color: var(--ink-900);
}

.data-table .num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.data-table .check {
  color: var(--brand-600);
  font-weight: 700;
  text-align: center;
}

.data-table tbody th {
  font-weight: 600;
  color: var(--ink-900);
}

/* A comparison matrix is read down a column as often as across a row, so the
   tick/dash cells are centred rather than ranged left — a column of ✓ marks
   that do not line up is the one thing that makes this layout look broken.
   Short value cells (`.num`) get the same treatment for the same reason. */
.data-table .dash {
  text-align: center;
}

.data-table td.num,
.data-table th.num {
  text-align: center;
}

/* The highlighted column — the tier the page actually recommends. Tinting the
   column rather than the whole row is what lets an eye run down it, which is
   the entire point of a comparison table. Opt-in per cell via `col-featured`. */
.data-table .col-featured {
  background: var(--brand-50);
  box-shadow: inset 1px 0 0 var(--brand-100), inset -1px 0 0 var(--brand-100);
}

.data-table thead .col-featured {
  background: var(--brand-100);
  color: var(--brand-800);
}

.data-table tbody tr:hover .col-featured {
  background: var(--brand-100);
}

/* A summary row (the price line, usually). Kept visually heavier than the
   feature rows so the table closes rather than just stopping. */
.data-table tfoot td {
  background: var(--cloud-50);
  border-top: 1px solid var(--slate-200);
  border-bottom: 0;
  font-weight: 700;
  color: var(--ink-900);
}

.data-table .dash {
  color: var(--slate-300);
}

/* ================= PROSE (legal / long copy) ============ */
.prose {
  max-width: 72ch;
  color: var(--slate-600);
  line-height: var(--lh-relaxed);
}

.prose > * + * {
  margin-top: var(--sp-5);
}

.prose h2 {
  font-size: var(--fs-2xl);
  margin-top: var(--sp-12);
  color: var(--ink-900);
}

.prose h3 {
  font-size: var(--fs-xl);
  margin-top: var(--sp-8);
  color: var(--ink-900);
}

.prose ul,
.prose ol {
  padding-left: 1.3em;
}

.prose li + li {
  margin-top: var(--sp-3);
}

.prose a {
  font-weight: 600;
}

/* The "authoritative agreements/policy" lists on legal/privacy and
   legal/terms. These bare <a> in <li> measured 21px tall — a real WCAG 2.5.8
   miss, not the inline-in-sentence exception (2026-10-15 visual audit,
   "Still open" list). Same floor and shape as .breadcrumb a /
   .footer__list a / .link-arrow. */
.legal-links a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.prose__meta {
  font-size: var(--fs-sm);
  color: var(--slate-500);
  padding-bottom: var(--sp-6);
  border-bottom: 1px solid var(--slate-200);
}

/* ================= SUPPORT / CONTACT ==================== */
.support-band {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-6);
}

.support-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-6);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  background: var(--white);
  transition: border-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

.support-card:hover {
  border-color: var(--brand-400);
  box-shadow: var(--sh-md);
}

.support-card__icon {
  width: 42px;
  height: 42px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: var(--brand-50);
  color: var(--brand-600);
}

.support-card__icon svg {
  width: 21px;
  height: 21px;
}

.support-card h3 {
  font-size: var(--fs-md);
}

.support-card p {
  font-size: var(--fs-sm);
  color: var(--slate-600);
  margin: 0;
  flex: 1;
}

.support-card__action {
  margin-top: var(--sp-3);
  font-weight: 650;
  font-size: var(--fs-sm);
}

/* ================= TICKER / MARQUEE-FREE LOGO STRIP ===== */
.section--tagline-bridge {
  position: relative;
  z-index: 10;
  padding-block: 0;
  margin-top: -26px;
  margin-bottom: -26px;
  pointer-events: none;
}

.section--tagline-bridge .container {
  pointer-events: auto;
}

.tagline-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4) var(--sp-8);
  padding: var(--sp-3) var(--sp-6);
  font-size: var(--fs-sm);
  color: var(--slate-700);
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(226, 232, 240, 0.85);
  border-radius: var(--r-full);
  box-shadow: 0 10px 25px -3px rgba(10, 18, 32, 0.08), 0 4px 6px -2px rgba(10, 18, 32, 0.04);
  max-width: fit-content;
  margin-inline: auto;
}

.tagline-strip span {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: 500;
}

.tagline-strip svg {
  width: 17px;
  height: 17px;
  color: var(--brand-600);
}

/* ================= REVEAL ON SCROLL =====================
   Keyed off .js on <html> so content never disappears when
   JavaScript is blocked.
   ======================================================== */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 440ms var(--ease-out), transform 440ms var(--ease-out);
  transition-delay: calc(var(--reveal-delay, 0ms) / 2);
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] {
    opacity: 1;
    transform: none;
  }
}

/* ================= MISC UTILITIES ======================= */
.mt-0 { margin-top: 0 !important; }
.mt-4 { margin-top: var(--sp-4); }
.mt-6 { margin-top: var(--sp-6); }
.mt-8 { margin-top: var(--sp-8); }
.mt-10 { margin-top: var(--sp-10); }
.mt-12 { margin-top: var(--sp-12); }
.text-center { text-align: center; }
.text-center .eyebrow { justify-content: center; }
.mx-auto { margin-inline: auto; }
.max-w-prose { max-width: 66ch; }
.max-w-narrow { max-width: 48ch; }

/* Anchor offset so sticky header never covers a jump target */
[id] {
  scroll-margin-top: calc(var(--header-h) + var(--topbar-h) + 1.25rem);
}

/* Keep long unbroken strings (domains, prices) from overflowing */
.break-word {
  overflow-wrap: anywhere;
}

/* ================= TRANSFER PROCESS TIMELINE ============ */
/* Four steps of a domain transfer, drawn as a connected
   progression. Desktop is a horizontal 4-up with a line running
   through the marker row; mobile collapses to a vertical stack
   with the line down the marker column. The markers, the line
   and the step numbers all read `--product-accent`, so the
   component is cyan on the transfer pages and would take any
   other product's colour if reused there — the section carries
   `data-product="transfer"` for exactly that reason.

   The connector is drawn PER STEP (`.transfer-timeline__step::after`),
   from one marker's right edge to the next marker's left edge, so it
   stays aligned no matter how the grid gaps fall; the last step draws
   none. It is a separate element from the marker (which has a solid
   white fill and a higher z-index) so the line passes behind the
   markers instead of through them.

   REVEAL: `.transfer-timeline` gains `.is-in` from
   transfer-timeline.js the first time it scrolls into view (once
   only). The four steps then light in order, ~0.6s apart, over about
   2.6s. Once revealed, a very low-opacity pulse travels through the
   markers on a 6s loop as the "energy flow". Both are transform/
   opacity only. Under `prefers-reduced-motion` the JS marks the
   timeline `.is-in` immediately and the global reduced-motion block
   collapses the animation durations, so the reader sees the finished
   sequence with no motion. */
.transfer-timeline {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-6);
  margin: var(--sp-12) 0 0;
  padding: 0;
  list-style: none;
}

.transfer-timeline::before {
  display: none;
}

/* The connector: one short segment per step, drawn from this
   marker's right edge to the next marker's left edge (`50%` is
   the marker centre in a centred flex column; `30px` is the
   28px marker radius plus the 2px line). The last step draws
   none. Doing it per-step keeps the line aligned no matter how
   the grid gaps fall — a single list-wide line would need to
   guess the column centres and would drift as the gap changes. */
.transfer-timeline__step::after {
  content: "";
  position: absolute;
  z-index: 0;
  top: 27px;
  left: calc(50% + 30px);
  width: calc(100% + var(--sp-6) - 60px);
  height: 2px;
  transform-origin: left center;
  background: linear-gradient(
    90deg,
    var(--product-accent),
    var(--product-accent-soft)
  );
}

.transfer-timeline__step:last-child::after {
  content: none;
}

.transfer-timeline__step {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.transfer-timeline__marker {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--r-full);
  background: var(--white);
  border: 2px solid var(--product-accent-soft);
  color: var(--product-accent-strong);
  box-shadow: 0 6px 16px rgba(var(--product-accent-rgb), 0.18);
}

.transfer-timeline__marker svg {
  width: 24px;
  height: 24px;
}

/* The completion badge: a small accent tick that pops onto the
   marker's lower-right corner as each step activates. It is a
   separate pseudo from the energy-flow halo (`::before`), so the
   one-time pop and the looping pulse never fight over one box. */
.transfer-timeline__marker::after {
  content: "";
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-full);
  background-color: var(--product-accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 12px 12px;
  border: 2px solid var(--white);
  transform: scale(0);
  opacity: 0;
}

.transfer-timeline__count {
  margin-top: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-widest);
  color: var(--product-accent);
}

.transfer-timeline__step h3 {
  margin-top: var(--sp-2);
  font-size: var(--fs-lg);
}

.transfer-timeline__step p {
  margin-top: var(--sp-2);
  max-width: 30ch;
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  color: var(--slate-600);
}

/* Vertical stack: marker on the left, copy on the right, the
   connecting line running down the marker column. */
@media (max-width: 760px) {
  .transfer-timeline {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-8);
    margin-top: var(--sp-10);
  }

  .transfer-timeline::before {
    content: "";
    position: absolute;
    display: block;
    z-index: 0;
    top: 10px;
    bottom: 10px;
    left: 27px;
    right: auto;
    width: 2px;
    height: auto;
    background: linear-gradient(
      180deg,
      var(--product-accent-soft),
      var(--product-accent) 50%,
      var(--product-accent-soft)
    );
  }

  .transfer-timeline__step::after {
    content: none;
  }

  .transfer-timeline__step {
    display: grid;
    grid-template-columns: 56px minmax(0, 1fr);
    column-gap: var(--sp-5);
    align-items: start;
    text-align: left;
  }

  .transfer-timeline__marker {
    grid-column: 1;
    grid-row: 1 / 4;
  }

  .transfer-timeline__count,
  .transfer-timeline__step h3,
  .transfer-timeline__step p {
    grid-column: 2;
  }

  .transfer-timeline__count {
    margin-top: 0;
  }

  .transfer-timeline__step p {
    max-width: none;
  }
}

/* ---------- Reveal (one-time, left to right) ----------
   The pre-reveal state is gated behind `html.js` so that if the
   script is blocked the timeline simply renders in its finished
   state — no permanently blank section. With JS present, the
   timeline starts collapsed and `transfer-timeline.js` adds
   `.is-in` the first time it enters the viewport.

   Every animated property is `opacity` or `transform`. The step
   cadence comes from `--tl-delay`, set per step below, so each
   element's own transition reads the same offset. */
html.js .transfer-timeline:not(.is-in) .transfer-timeline__marker {
  opacity: 0;
  transform: scale(0.55);
}

html.js .transfer-timeline:not(.is-in) .transfer-timeline__marker::after {
  opacity: 0;
  transform: scale(0);
}

html.js .transfer-timeline:not(.is-in) .transfer-timeline__step::after {
  transform: scaleX(0);
}

html.js .transfer-timeline:not(.is-in) .transfer-timeline__count,
html.js .transfer-timeline:not(.is-in) .transfer-timeline__step h3,
html.js .transfer-timeline:not(.is-in) .transfer-timeline__step p {
  opacity: 0;
  transform: translateY(8px);
}

/* Step cadence: 0s / 0.6s / 1.2s / 1.8s. The last transitions
   settle by ~2.6s, inside the 2.5–3.5s target. */
.transfer-timeline__step {
  --tl-delay: 0s;
}

.transfer-timeline.is-in .transfer-timeline__step:nth-child(2) {
  --tl-delay: 0.6s;
}

.transfer-timeline.is-in .transfer-timeline__step:nth-child(3) {
  --tl-delay: 1.2s;
}

.transfer-timeline.is-in .transfer-timeline__step:nth-child(4) {
  --tl-delay: 1.8s;
}

.transfer-timeline.is-in .transfer-timeline__marker {
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 0.4s ease var(--tl-delay),
    transform 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) var(--tl-delay);
}

/* The connector fills just after its marker lands. */
.transfer-timeline.is-in .transfer-timeline__step::after {
  transform: scaleX(1);
  transition: transform 0.5s ease calc(var(--tl-delay) + 0.15s);
}

/* The completion tick pops last, once the step is "done". */
.transfer-timeline.is-in .transfer-timeline__marker::after {
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 0.3s ease calc(var(--tl-delay) + 0.35s),
    transform 0.4s cubic-bezier(0.2, 0.7, 0.3, 1) calc(var(--tl-delay) + 0.35s);
}

.transfer-timeline.is-in .transfer-timeline__count,
.transfer-timeline.is-in .transfer-timeline__step h3,
.transfer-timeline.is-in .transfer-timeline__step p {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.4s ease calc(var(--tl-delay) + 0.1s),
    transform 0.5s ease calc(var(--tl-delay) + 0.1s);
}

/* ---------- Energy flow (after the reveal) ----------
   A faint pulse drifting through the markers on a 6s loop, offset
   per step so it appears to travel left to right through the four
   nodes — the "data moving" cue, not a spinner. It only runs once
   `.is-in` is set, so it never fires before the reveal. transform
   and opacity only; the global reduced-motion block stills it. */
.transfer-timeline.is-in .transfer-timeline__marker::before {
  content: "";
  position: absolute;
  inset: -6px;
  z-index: -1;
  border-radius: var(--r-full);
  /* A hollow ring, not a disc: the centre stays clear so the ring
     reads as light travelling around the node and never tints the
     marker's white face, whatever the paint order ends up being. */
  background: radial-gradient(
    circle,
    rgba(var(--product-accent-rgb), 0) 52%,
    rgba(var(--product-accent-rgb), 0.45) 63%,
    rgba(var(--product-accent-rgb), 0) 78%
  );
  opacity: 0;
  animation: tlFlow 6s ease-in-out infinite;
  animation-delay: calc(var(--tl-delay) + 2.6s);
  pointer-events: none;
}

@keyframes tlFlow {
  0%,
  100% {
    opacity: 0;
    transform: scale(0.9);
  }
  14% {
    opacity: 0.55;
    transform: scale(1.12);
  }
  38% {
    opacity: 0;
    transform: scale(1.28);
  }
}

/* ================= PRINT POLISH ========================= */
@media print {
  .hero,
  .cta-band,
  .section--ink {
    background: #fff !important;
    color: #000 !important;
  }
  .hero h1,
  .hero__sub,
  .cta-band h2,
  .cta-band p {
    color: #000 !important;
  }
}

/* ================= LOCAL LANDING PAGE (/local/) ============ */
/* The QR-code page. Everything here is scoped to `.lp-*` so the
   storefront pages are untouched. The look is deliberately quieter
   than the product pages: a light hero, one highlighted panel for
   the free review, one ink band at the end. */

.lp-hero {
  position: relative;
  background: var(--cloud-100);
  padding-block: clamp(3rem, 7vw, 5.5rem);
  overflow: hidden;
  isolation: isolate;
}

/* The same "graph paper" texture the light product heroes wear, plus a
   soft brand glow in the top-right corner where the photo sits. */
.lp-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--dot-grid);
  background-size: 26px 26px;
  mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
  z-index: -1;
  pointer-events: none;
}

.lp-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grad-hero-glow);
  z-index: -1;
  pointer-events: none;
}

.lp-hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: center;
}

@media (max-width: 860px) {
  .lp-hero__inner {
    grid-template-columns: minmax(0, 1fr);
  }
}

.lp-hero h1 {
  font-family: var(--font-display);
  font-size: var(--fs-5xl);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--ink-900);
  max-width: 16ch;
}

.lp-hero__sub {
  margin-top: var(--sp-5);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--slate-600);
  max-width: 44ch;
}

.lp-hero .btn-row {
  margin-top: var(--sp-8);
}

.lp-hero__tag {
  margin-top: var(--sp-6);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: var(--tracking-flag);
  text-transform: uppercase;
  color: var(--brand-700);
}

/* The hero's photo slot: while it is a placeholder, give the work-order
   panel a portrait-ish frame so the hero keeps its shape; once `src` is
   filled, mediaSlot() emits the standard plated-media markup and this
   rule no longer matches anything. */
.lp-hero .media-slot {
  margin: 0;
  aspect-ratio: 4 / 3;
  justify-content: center;
}

/* ---------- Free Website Review — the one highlighted panel ---------- */
.lp-review {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: center;
  padding: clamp(1.75rem, 4vw, 3rem);
  background: linear-gradient(140deg, var(--brand-50) 0%, var(--white) 70%);
  border: 1px solid var(--brand-100);
  border-radius: var(--r-2xl);
  box-shadow: var(--sh-lg);
}

@media (max-width: 860px) {
  .lp-review {
    grid-template-columns: minmax(0, 1fr);
  }
}

.lp-review__panel {
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-xl);
  padding: var(--sp-6);
  box-shadow: var(--sh-md);
}

.lp-review__panel .label {
  display: block;
  margin-bottom: var(--sp-4);
}

/* ---------- Contact ---------- */
.lp-contact {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: start;
}

@media (max-width: 900px) {
  .lp-contact {
    grid-template-columns: minmax(0, 1fr);
  }
}

.lp-contact__points {
  margin-top: var(--sp-8);
}

.lp-contact__points a {
  color: var(--brand-600);
}

.lp-form-card {
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-lg);
  padding: clamp(1.25rem, 3vw, 2rem);
}

.lp-req {
  color: var(--danger-600);
}

.lp-opt {
  font-weight: 500;
  color: var(--slate-500);
}

.lp-field-error {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--danger-600);
}

.lp-field-error:empty {
  display: none;
}

.lp-invalid {
  border-color: var(--danger-600) !important;
}

.lp-invalid:focus {
  box-shadow: 0 0 0 3px rgba(180, 35, 24, 0.16) !important;
}

/* Honeypot: off-screen but still in the tab order's reach is exactly
   what we don't want — so no display:none (bots test for that), just
   visually removed and tabindex="-1" in the markup. */
.lp-hp {
  position: absolute;
  left: -100vw;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The captcha slot stays out of the layout until a widget renders
   into it (contact.js un-hides it when Turnstile is configured). */
.lp-captcha:empty {
  display: none;
}

.lp-captcha:not(:empty) {
  margin-top: var(--sp-5);
}

.lp-status {
  margin: var(--sp-5) 0;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-sm);
  font-size: var(--fs-base);
  line-height: var(--lh-relaxed);
}

.lp-status:empty {
  display: none;
}

.lp-status--ok {
  background: var(--success-50);
  color: var(--success-600);
  border: 1px solid rgba(6, 118, 71, 0.25);
}

.lp-status--err {
  background: var(--danger-50);
  color: var(--danger-600);
  border: 1px solid rgba(180, 35, 24, 0.25);
}

.lp-status--info {
  background: var(--info-50);
  color: var(--info-600);
  border: 1px solid rgba(23, 92, 211, 0.22);
}

.lp-status a {
  color: inherit;
  font-weight: 650;
  text-decoration: underline;
}

.lp-form__fine {
  margin-top: var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--slate-500);
  text-align: center;
}

.lp-form__fine a {
  color: var(--brand-600);
}

/* ---------- Closing tagline band ---------- */
.lp-tagline {
  text-align: center;
}

.lp-tagline p {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-3xl);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--white);
}
/* ============================================================
   SIX CATEGORIES - making the grid pop
   ------------------------------------------------------------
   The cards were flat white on the cloud band. They now carry a
   soft gradient surface (see .pillar above), and a gradient
   sheen sweeps across each card as it reveals - so the grid
   comes alive while you scroll through it. That sweep is the
   gradient effect as you scroll; the cascade and the warm price
   on hover keep the eye moving across the row. Reusable on any
   card grid on the site.
   ============================================================ */

/* The scroll-sheen: a soft light passes over the card on arrival. It is
   clipped by the card (overflow: hidden on .pillar) so only the sweep is
   seen, and it runs on transform + opacity only. */
.pillar::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(105deg, transparent 32%, rgba(255, 255, 255, 0.6) 50%, transparent 68%);
  transform: translateX(-115%);
  opacity: 0;
  pointer-events: none;
}

.js .pillar[data-reveal].is-revealed::before {
  animation: pillarSheen 1200ms ease-out 150ms both;
}

@keyframes pillarSheen {
  0%   { transform: translateX(-115%); opacity: 0; }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { transform: translateX(115%); opacity: 0; }
}

/* The cascade: the cards arrive in reading order as you scroll in. */
#products .pillar:nth-child(1) { --reveal-delay: 0ms; }
#products .pillar:nth-child(2) { --reveal-delay: 160ms; }
#products .pillar:nth-child(3) { --reveal-delay: 320ms; }
#products .pillar:nth-child(4) { --reveal-delay: 480ms; }
#products .pillar:nth-child(5) { --reveal-delay: 640ms; }
#products .pillar:nth-child(6) { --reveal-delay: 800ms; }

/* The card link's focus ring must stay visible now the card clips (for the
   sheen), so it is drawn just inside the card's edge instead of outside. */
.pillar:focus-within .card__link {
  outline-offset: -4px;
}

/* Hover: the price warms to the accent - no markups is the value prop, so the
   number is the thing to make feel alive. */
.pillar:hover .pillar__price b {
  color: var(--brand-700);
}
/* ============================================================
   THE STAGE behind the category cards (Six categories)
   ------------------------------------------------------------
   A static white faceted wallpaper: the WordPress/cPanel hero mesh
   (_wallpaper-gen.ps1, `products-geo.svg`) in a quiet white colourway.
   It fills the band edge to edge (inset 0, cover) and the bloom is a
   wide, soft falloff centred on the canvas, so tone reads across the
   whole width instead of pooling in a corner. 2026-10-14 rebuild: the
   drifting wallpaper and the aurora discs were removed - a moving,
   oversized layer shifted the crop and read as clumping at the left.
   The cards (white) keep their edge against the pale mesh.
   ============================================================ */
#products {
  isolation: isolate;
  overflow: hidden;
  position: relative;
  width: 100%;
  margin-inline: 0;
  border-radius: 0;
  padding-block: var(--sp-16);
  /* The supply band's file tab overlaps 64px (40px on phones) of this
     padding; the ID selector beats `.section--pre-tab`, so it is set here. */
  padding-bottom: calc(var(--sp-16) + 64px);
}

@media (max-width: 639px) {
  #products {
    padding-bottom: calc(var(--sp-16) + 40px);
  }
}

#products .dot-matrix {
  z-index: -1;
  --dm-opacity: 0.07;
}

#products::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Reset the ambient-glow geometry every `.section::before` inherits
     (44% wide, 620px cap, 1:1, round, drifting). Without this the wallpaper
     was clipped to a 620px circle pinned to the left edge. */
  width: auto;
  max-width: none;
  aspect-ratio: auto;
  border-radius: 0;
  animation: none;
  z-index: -2;
  background-color: var(--cloud-50);
  background-image: url("../img/hero/products-geo.svg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 1;
  pointer-events: none;
}

/* The vignette: a whisper of darkening at the edges so the cards read as
   sitting in light - the depth/contrast half of the stage. */
#products::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(120% 95% at 50% 45%, transparent 52%, rgba(10, 18, 32, 0.06) 100%);
  pointer-events: none;
}

