/* ============================================================
   FAMOUS REGISTRAR — COMPONENTS
   ============================================================ */

/* ================= BUTTONS ============================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-5);
  height: 44px;
  border-radius: var(--r-sm);
  font-size: var(--fs-base);
  font-weight: 650;
  letter-spacing: -0.005em;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn:hover {
  text-decoration: none;
}

.btn:active {
  transform: translateY(1px);
}

.btn svg {
  width: 17px;
  height: 17px;
  flex-shrink: 0;
}

/* Primary — ink. Reads premium, not discount. */
.btn--primary {
  background: var(--ink-900);
  color: var(--white);
  box-shadow: var(--sh-sm);
}

.btn--primary:hover {
  background: var(--ink-700);
  color: var(--white);
  box-shadow: var(--sh-md);
  transform: translateY(-1px);
}

/* Brand — emerald. Reserved for purchase intent. The gradient's stops are
   both at or below the brand-600 contrast floor, so the white label stays
   legal on every pixel of the fill; hover swaps the direction, lifts the
   button and deepens the brand shadow rather than lightening the fill. */
.btn--brand {
  background: var(--grad-btn-brand);
  color: var(--white);
  box-shadow: var(--sh-brand);
}

.btn--brand:hover {
  background: var(--grad-btn-brand-hover);
  color: var(--white);
  box-shadow: var(--sh-brand-lg);
  transform: translateY(-1px);
}

.btn--outline {
  background: var(--white);
  color: var(--ink-900);
  border-color: var(--slate-300);
  box-shadow: var(--sh-xs);
}

.btn--outline:hover {
  border-color: var(--ink-800);
  color: var(--ink-900);
}

.btn--ghost {
  background: transparent;
  color: var(--ink-800);
}

.btn--ghost:hover {
  background: var(--slate-100);
  color: var(--ink-900);
}

.btn--light {
  background: rgba(255, 255, 255, 0.1);
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.22);
}

.btn--light:hover {
  background: rgba(255, 255, 255, 0.18);
  color: var(--white);
}

.btn--white {
  background: var(--white);
  color: var(--ink-900);
}

.btn--white:hover {
  background: var(--cloud-100);
  color: var(--ink-900);
}

.btn--sm {
  height: 36px;
  padding-inline: var(--sp-4);
  font-size: var(--fs-sm);
}

.btn--lg {
  height: 52px;
  padding-inline: var(--sp-8);
  font-size: var(--fs-md);
}

.btn--block {
  display: flex;
  width: 100%;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
}

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 650;
  color: var(--brand-600);
  /* WCAG 2.5.8 target size. Without a minimum the control is only as tall as its
     line box, which measured 20px on the support hub's four card actions
     (308x20) and 17px on the 404's "hosting hub" link — genuine failures, not
     the inline-in-a-sentence exemption. 24px sits inside the existing line box
     for --fs-sm copy, so paragraph rhythm is unchanged. Added 2026-10-15, see
     reviews/visual-audit-2026-10-15.md. */
  min-height: 24px;
}

.link-arrow svg {
  width: 15px;
  height: 15px;
  transition: transform var(--dur-base) var(--ease-out);
}

.link-arrow:hover {
  text-decoration: none;
  color: var(--brand-700);
}

.link-arrow:hover svg {
  transform: translateX(3px);
}

.link-arrow--light {
  color: var(--brand-300);
}

.link-arrow--light:hover {
  color: var(--white);
}

/* ================= BADGES / PILLS ======================= */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--chip-gap);
  padding: var(--chip-pad-y) var(--chip-pad-x);
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  line-height: 1.5;
  white-space: nowrap;
}

.badge--amber {
  background: var(--amber-100);
  color: var(--amber-700);
}

.badge--brand {
  background: var(--brand-100);
  color: var(--brand-800);
}

.badge--slate {
  background: var(--slate-100);
  color: var(--slate-700);
}

.badge--success {
  background: var(--success-50);
  color: var(--success-600);
}

.badge--ink {
  background: var(--ink-900);
  color: var(--white);
}

.badge--on-dark {
  background: rgba(255, 255, 255, 0.12);
  color: var(--white);
}

.badge--outline {
  background: transparent;
  border: 1px solid var(--slate-300);
  color: var(--slate-600);
}

.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* ============================================================
   THE AMBER "FLAG" — one component, four call sites
   ------------------------------------------------------------
   "This is the one most people buy" is said in four places:
   the plan card's ribbon, the hero price card's badge, the
   media-slot tag and the hero badge's <b>. Each of those four
   was a separate 10-line reimplementation of the same amber
   pill — same gradient, same 0.625rem, same 800 weight, same
   uppercase, same radius, same shadow, re-typed by hand each
   time.

   That is the site's most expensive kind of drift. Any future
   change to the flag (a darker amber for contrast, a slightly
   different track) had to be made in four files, and a partial
   change is invisible in review because each copy still looks
   deliberate on its own. It is why the palette has been able to
   say it "uses amber sparingly" while shipping it four times.

   So the pill is defined once, here, and the four call sites
   only say where it sits. The gradient, the type and the shadow
   are a property of the flag, not of the thing it is flagging.
   ============================================================ */
.flag {
  display: inline-block;
  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;
  box-shadow: 0 3px 8px -2px rgba(245, 158, 11, 0.45);
}

/* ================= CARDS =============================== */
.card {
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  padding: var(--sp-6);
  transition: border-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}

.card--hover:hover {
  border-color: var(--slate-300);
  box-shadow: var(--sh-lg);
  transform: translateY(-3px);
}

.card--flat {
  background: var(--cloud-100);
  border-color: transparent;
}

.card--ink {
  background: var(--ink-900);
  border-color: var(--ink-700);
  color: var(--slate-300);
}

.card--ink h3,
.card--ink h4 {
  color: var(--white);
}

/* `.card p` (slate-600) outranks the colour inherited from the card, so body
   copy on an ink surface needs its own rule or it renders dark-on-dark. */
.card--ink p {
  color: var(--slate-300);
}

.card__icon {
  width: 44px;
  height: 44px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: linear-gradient(140deg, var(--brand-100) 0%, var(--brand-50) 60%, var(--white) 100%);
  border: 1px solid var(--brand-100);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), var(--sh-xs);
  color: var(--brand-600);
  margin-bottom: var(--sp-5);
}

.card__icon svg {
  width: 22px;
  height: 22px;
}

.card__icon--amber {
  background: linear-gradient(140deg, var(--amber-100) 0%, var(--amber-50) 60%, var(--white) 100%);
  border-color: var(--amber-100);
  color: var(--amber-600);
}

.card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.card h3 {
  font-size: var(--fs-lg);
}

.card p {
  color: var(--slate-600);
  margin-top: var(--sp-3);
}

.card__foot {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--slate-200);
}

.card--ink .card__foot {
  border-color: var(--ink-700);
}

.card--compact {
  padding: var(--sp-5);
}

.card--compact h3 {
  font-size: var(--fs-md);
}

.card--compact p {
  font-size: var(--fs-sm);
  margin-top: var(--sp-2);
}

.card--linked {
  position: relative;
}

.card__link {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.card__link span {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.card--linked:hover {
  border-color: var(--brand-500);
  box-shadow: var(--sh-lg);
  transform: translateY(-3px);
}

.card--linked:focus-within {
  border-color: var(--brand-500);
  box-shadow: var(--sh-lg);
}

.card--linked:focus-within .card__link {
  outline: 3px solid var(--brand-500);
  outline-offset: 3px;
  border-radius: var(--r-lg);
}

/* ================= GRIDS =============================== */
.grid {
  display: grid;
  gap: var(--sp-6);
}

.grid--2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.grid--3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.grid--4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.grid--auto {
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
}

.grid--tight {
  gap: var(--sp-4);
}

.grid--loose {
  gap: var(--sp-8);
}

/* Five cards as 3 + 2 with no hole: a six-track grid where the first row's
   cards span two tracks and the second row's span three, so both rows fill
   the full width. Below 860px `.grid--3` drops to two columns and the spans
   are reset so the cards flow normally. */
.grid--3-2 {
  grid-template-columns: repeat(6, minmax(0, 1fr));
}

.grid--3-2 > * {
  grid-column: span 2;
}

.grid--3-2 > :nth-child(n + 4) {
  grid-column: span 3;
}

.btn-row--center {
  justify-content: center;
}

@media (max-width: 860px) {
  .grid--3-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .grid--3-2 > *,
  .grid--3-2 > :nth-child(n + 4) {
    grid-column: auto;
  }
}

@media (max-width: 1000px) {
  .grid--4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 860px) {
  .grid--3 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 640px) {
  .grid--2,
  .grid--3,
  .grid--4,
  .grid--auto {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ================= MEDIA SLOT (image placeholder) ==========
   A standing place for an image that has not been made yet.

   It is on the page rather than in a notes file on purpose: a placeholder that
   lives next to the layout it belongs to cannot be forgotten, and it carries
   its own spec. It is also deliberately styled to look like a *slot in a
   design* — dashed edge, quiet tint, monospaced spec table — so it reads as
   work in progress rather than as a broken image or a styling fault.

   Filling in `src` on the `mediaSlot()` call removes it. Nothing else refers
   to it, so there is no list to keep in sync. */
.media-slot {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
  margin: 0;
  padding: var(--sp-8);
  border: 2px dashed var(--slate-300);
  border-radius: var(--r-xl);
  background: var(--cloud-50);
}

/* Geometry only — the amber pill is the shared `.flag`. This one is the quiet
   slate variant used to label a missing image, so it takes the same metrics
   and deliberately not the amber fill. */
.media-slot__tag {
  display: inline-block;
  background: var(--slate-200);
  color: var(--slate-600);
  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;
}

.media-slot__need {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  color: var(--ink-900);
  text-wrap: balance;
}

/* The spec is the work order, so it is set as a small definition list with
   the label column narrow: it should be scannable by whoever is sourcing the
   image, not read as prose. */
.media-slot__spec {
  display: grid;
  gap: var(--sp-2);
  width: 100%;
  margin: 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--slate-200);
}

.media-slot__spec > div {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: baseline;
}

.media-slot__spec dt {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--slate-500);
}

.media-slot__spec dd {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.55;
  color: var(--slate-600);
}

@media (max-width: 560px) {
  .media-slot {
    padding: var(--sp-5);
  }

  .media-slot__spec > div {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

/* ================= PRICE DISPLAY ======================== */
.price {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: var(--sp-4);
}

.price__amount {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: var(--tracking-tighter);
  color: var(--ink-900);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.price__was {
  font-size: var(--fs-base);
  color: var(--slate-500);
  text-decoration: line-through;
}

/* The was/now treatment on a dark surface — the homepage hero card and the
   stat strip both sit on ink, where slate-500 all but disappears. Same
   treatment, re-coloured: not a second implementation. */
.price__was--on-dark {
  color: var(--slate-400);
}

.price__unit {
  font-size: var(--fs-sm);
  color: var(--slate-600);
  font-weight: 550;
}

.price__note {
  font-size: var(--fs-xs);
  color: var(--slate-500);
  margin-top: var(--sp-2);
  line-height: 1.45;
}

.price--inline {
  margin-top: 0;
}

.price--inline .price__amount {
  font-size: var(--fs-lg);
  letter-spacing: var(--tracking-tight);
}

/* ---------- The hero price card's recommended-tier heading ----------
   The hero promotes a specific tier rather than the entry price, so it needs
   three pieces of its own: the badge, the tier name and the entry-price line.

   The badge is the shared `.flag` rather than a second implementation, because
   it is the same claim as `.plan__flag` made twice on one page. Two treatments
   for one fact is how a page starts looking assembled rather than designed.
   Amber stays inside the palette's rule here too: it is a small badge, not a
   full-strength field beside the emerald. */
.price-aside__flag {
  margin-bottom: var(--sp-3);
}

.price-aside__name {
  font-size: var(--fs-xl);
  font-weight: 700;
  color: var(--ink-900);
  letter-spacing: var(--tracking-tight);
}

/* The entry price, kept visible but subordinate. It sits under the button
   because its job is to answer "what is the cheapest way in?" for the reader
   who scrolls back up to look for it — not to be the first number on the page. */
.price-aside__from {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--slate-200);
  font-size: var(--fs-sm);
  color: var(--slate-500);
  text-align: center;
}

.price-aside__from strong {
  color: var(--ink-900);
  font-variant-numeric: tabular-nums;
}

/* ---------- The receipt (B1) ----------
   An itemised bill: paper white, hairline rules, monospace numerals, no
   icon and no illustration. It is a receipt, so the only thing on it
   allowed to catch the eye is the column of figures.

   Deliberately colourless. No emerald, no amber, no shadow, no badge:
   the total is not a "saving" and this is not a promotion. A bill dressed
   as a win is the thing this brand is arguing against, and a green total
   would read as a discount the reader is being sold.

   The dashed rule under the header and the solid rule above the total are
   the two conventions a real receipt uses to separate "what you bought"
   from "what it came to". */
.receipt {
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  padding: var(--sp-6);
  max-width: 32rem;
  margin: 0;
}

.receipt__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px dashed var(--slate-300);
}

.receipt__eyebrow {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--ink-900);
}

.receipt__stamp {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--slate-500);
}

.receipt__table {
  width: 100%;
  border-collapse: collapse;
}

.receipt__table th,
.receipt__table td {
  padding-block: var(--sp-3);
  border-bottom: 1px solid var(--slate-100);
  text-align: left;
  font-weight: 400;
  vertical-align: baseline;
}

.receipt__table th {
  font-size: var(--fs-sm);
  color: var(--slate-700);
}

.receipt__table th small {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--slate-500);
}

/* Right-aligned and monospaced, so the decimal points line up down the
   column the way they do on paper. */
.receipt__table td {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  color: var(--ink-900);
}

.receipt__table tfoot th,
.receipt__table tfoot td {
  border-bottom: 0;
  border-top: 2px solid var(--ink-900);
  padding-top: var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--ink-900);
}

.receipt__note {
  margin-top: var(--sp-5);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--slate-600);
  max-width: 46ch;
}

/* ---------- The supply chain (B3) ----------
   The band explains why we cost less: the argument is in the copy column,
   and the artwork beside it is the Authorized Reseller shield (the hero
   image), not a diagram. See the image brief, sections B3 and the hero art.

   History, because it matters. This slot used to hold a two-lane HTML
   diagram ("The usual way" vs "Our way"). It was HTML rather than a picture
   because the labels had to stay selectable, translatable and correctable,
   and this project has a rule that no number may live in a raster asset.
   The owner replaced that diagram with the authorized-reseller hero art.
   The rule still holds: the replacement is a picture of a shield and four
   product WORD-labels (Domains, SSL Certificates, Professional Email,
   Hosting) with no number in it, and the text at issue lives in the copy
   column. Any future edit that puts a price or a count into this raster
   would break the rule and must not ship.

   Why the background is ink. The band is a change of register mid-page —
   the moment the site stops listing features and explains itself. On white
   it reads as another content band; on ink it reads as the argument, which
   is the job. */
/* The figure is a split at desktop width, like every other two-part band on
   the site. Left: the argument. Right: the artwork that backs it. Below
   1000px it stacks. */
.supply {
  margin: 0;
  color: var(--white);
  display: grid;
  gap: var(--sp-8);
  align-items: center;
}

@media (min-width: 1000px) {
  .supply {
    grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
    gap: var(--sp-12);
    align-items: start;
  }
}

.supply__eyebrow {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brand-300);
}

.supply__lede {
  margin: var(--sp-3) 0 0;
  max-width: 30ch;
  color: var(--white);
  font-size: var(--fs-2xl);
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

.supply__support {
  margin: var(--sp-5) 0 0;
  max-width: 52ch;
  color: var(--slate-300);
}

/* ---------- Band wallpaper (the supply band only) ----------
   A photographic layer behind the dot matrix, inside the band card.
   The photo is only ever seen THROUGH two veils — a dark gradient over
   it and the dot matrix above it — so it reads as depth, not as a
   picture. Contrast holds for that reason: the brightest pixel of the
   composite is still dark ink.

   `z-index: -1` puts it under the dot matrix and under the content
   without a second stacking trick; the band carries `isolation: isolate`
   so the layer cannot slip behind the page background.

   Mobile: no photo. A 390px viewport gets no read out of the corridor
   detail, only weight and bytes, so the band is plain ink there. */
.section--card--wall {
  position: relative;
  isolation: isolate;
}

.section--card--wall .supply__wall {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: linear-gradient(
      180deg,
      rgba(10, 14, 26, 0.82) 0%,
      rgba(10, 14, 26, 0.9) 55%,
      rgba(10, 14, 26, 0.97) 100%
    ),
    url("../img/hero/supply-wall.jpg");
  background-size: cover;
  background-position: center;
  border-radius: inherit;
}

@media (max-width: 640px) {
  .section--card--wall .supply__wall {
    display: none;
  }
}

/* ---------- Lighter wall + brand aurora (domains pricing band only) ----------
   The owner found the full-strength wall too dark here, so this variant cuts
   the overlay's opacity by about 30% and lets a brand-coloured aurora (the
   same blurred-ribbon element the domains hero uses, re-tinted emerald and
   teal) drift over it. Scoped to `.section--wall-light` so the homepage
   supply band keeps its original weight. The lighter overlay lifts the
   surface, so the small print steps up to slate-300 to hold 4.5:1. */
.section--wall-light .supply__wall {
  background-image: linear-gradient(
      180deg,
      rgba(52, 196, 138, 0.12) 0%,
      rgba(23, 92, 211, 0.14) 55%,
      rgba(70, 226, 196, 0.12) 100%
    ),
    linear-gradient(
      180deg,
      rgba(10, 14, 26, 0.52) 0%,
      rgba(10, 14, 26, 0.58) 55%,
      rgba(10, 14, 26, 0.62) 100%
    ),
    url("../img/hero/supply-wall.jpg");
}

.section--wall-light .small.muted {
  color: var(--slate-300);
}

.section--wall-light .domains-hero__aurora::before {
  background: radial-gradient(closest-side, rgba(52, 196, 138, 0.34), transparent 72%);
}

.section--wall-light .domains-hero__aurora::after {
  background: radial-gradient(closest-side, rgba(70, 226, 196, 0.24), transparent 72%);
}

/* ---------- Full-bleed supply band + the SiteGround-style overlap ----------
   The supply band used to be an inset card (`section--card`); the owner asked
   for it to span the viewport with the next band card overlapping upward onto
   its lower edge, the way SiteGround transitions its dark argument band.

   Two rules make that safe:
   - `--pre-overlap` gives the ink band extra bottom padding so its content
     (the fee note) always ends above the region the white card covers —
     the overlap is geometry over padding, never over text (SC 2.4.11).
   - `--overlap-up` pulls the next band card up and raises it (z-index 2),
     so its rounded top corners sit on the ink band. The band keeps its
     `.section--card` side insets, so the house band-card system is unchanged;
     this only changes where the card starts vertically.
   (Owner request 2026-10-14; homepage supply band only.) */
.section--pre-overlap {
  padding-bottom: calc(var(--section-y) + 112px);
}

.section--overlap-up {
  position: relative;
  z-index: 2;
  margin-top: -96px;
}

@media (max-width: 639px) {
  .section--pre-overlap {
    padding-bottom: calc(var(--section-y-sm) + 64px);
  }

  .section--overlap-up {
    margin-top: -48px;
  }
}

/* ---------- White mesh band (`section--mesh`) ----------
   The quiet white faceted wallpaper (products-geo.svg) as a band surface, the
   white sibling of the hosting heroes' meshes. It fades to the page white at
   the top and bottom so neighbouring bands never meet a hard seam.
   `.section.section--mesh::before` out-ranks the ambient-glow rules in
   pages.css, so the glow geometry (44% wide, round, drifting) is reset here -
   forgetting that is what made the Six categories wallpaper clump left. */
.section.section--mesh {
  position: relative;
  isolation: isolate;
}

.section.section--mesh::before {
  content: "";
  position: absolute;
  inset: 0;
  top: 0;
  right: 0;
  width: auto;
  max-width: none;
  aspect-ratio: auto;
  border-radius: 0;
  animation: none;
  z-index: -1;
  background-color: var(--white);
  background-image: linear-gradient(
      180deg,
      var(--white) 0%,
      rgba(255, 255, 255, 0) 10%,
      rgba(255, 255, 255, 0) 78%,
      var(--white) 100%
    ),
    url("../img/hero/products-geo.svg");
  background-size: 100% 100%, cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* ---------- Floating card (`section--float`) + downward overlap ----------
   A white band card that reads as lifted off the page: layered shadow, and
   `--overlap-down` lets it hang 64px (40px on phones) into the band below.
   The band below carries `--post-overlap`, whose extra top padding equals the
   overlap, so the card only ever covers empty surface, never content
   (SC 2.4.11). The card keeps `z-index: 2` from `--overlap-up`; the same
   class set is declared here so `--overlap-down` also works alone. */
.section--float {
  background: var(--white);
  box-shadow: 0 1px 2px rgba(10, 18, 32, 0.06), 0 12px 24px -8px rgba(10, 18, 32, 0.1),
    0 32px 64px -16px rgba(10, 18, 32, 0.14);
}

.section--overlap-down {
  position: relative;
  z-index: 2;
  margin-bottom: -64px;
}

.section--post-overlap {
  padding-top: calc(var(--section-y) + 64px);
}

@media (max-width: 639px) {
  .section--overlap-down {
    margin-bottom: -40px;
  }

  .section--post-overlap {
    padding-top: calc(var(--section-y-sm) + 40px);
  }
}

/* ---------- Full slab (`section--tab-b`) + the shared mesh stage ----------
   `.section--tab` rounds the top corners; `--tab-b` rounds the bottom ones
   too and hangs the slab 64px (40px phones) over whatever follows, so the
   band reads as a dark rounded slab laid on the page. The element that
   follows must be a `.mesh-stage`, whose top padding covers the overlap
   (the slab only ever sits over empty mesh, never content - SC 2.4.11).

   `.mesh-stage` wraps several consecutive bands in ONE surface: the white
   mesh runs unbroken behind all of them (no fade-to-white seam between
   bands). Children that should show it stay transparent; floating cards
   (`.section--float`) sit on top. Same glow-geometry reset as
   `.section--mesh`, for the same reason - it is not a `.section`, but the
   rule is cheap insurance if the class is ever moved onto one. */
.section--tab.section--tab-b {
  border-radius: 28px;
  margin-bottom: -40px;
}

.mesh-stage {
  position: relative;
  isolation: isolate;
  padding-top: calc(40px + var(--sp-6));
}

.mesh-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--white);
  background-image: linear-gradient(
      180deg,
      var(--white) 0%,
      rgba(255, 255, 255, 0) 6%,
      rgba(255, 255, 255, 0) 94%,
      var(--white) 100%
    ),
    url("../img/hero/products-geo.svg");
  background-size: 100% 100%, cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* `--flush`: the stage's first child pulls itself UP over the band above
   (`.section--overlap-up`), so the stage adds no top padding. `flow-root`
   stops that negative margin collapsing through the stage, which would drag
   the whole mesh surface up over the band above. */
.mesh-stage.mesh-stage--flush {
  display: flow-root;
  padding-top: 0;
  /* The stage is its own stacking context (`isolation`), and the band above
     it is `z-index: 2`, so without this the card inside is painted UNDER the
     dark band's padding and the overlap is invisible. */
  z-index: 3;
}

/* ---------- Blue mesh (`mesh-stage--blue`, owner request 2026-10-14) ----------
   Same facets and fades as the neutral mesh, regenerated in a pale steel
   blue (products-geo-blue.svg) so the last band of a page steps toward the
   navy footer instead of grey. Only the image changes. The peaks below take
   the blue base colour so no seam shows. */
.mesh-stage--blue::before {
  background-color: var(--mesh-blue-base);
  background-image: linear-gradient(
      180deg,
      var(--mesh-blue-base) 0%,
      rgba(238, 243, 250, 0) 6%,
      rgba(238, 243, 250, 0) 94%,
      var(--mesh-blue-base) 100%
    ),
    url("../img/hero/products-geo-blue.svg");
}

.mesh-stage--blue.mesh-stage--peaks::after {
  background: var(--mesh-blue-base);
}

/* The same blue for a plain closing band (every subpage's last CTA band). */
.section.section--mesh.section--mesh-blue::before {
  background-color: var(--mesh-blue-base);
  background-image: linear-gradient(
      180deg,
      var(--mesh-blue-base) 0%,
      rgba(238, 243, 250, 0) 10%,
      rgba(238, 243, 250, 0) 78%,
      var(--mesh-blue-base) 100%
    ),
    url("../img/hero/products-geo-blue.svg");
}

/* ---------- Navy mesh (`mesh-stage--navy`, owner request 2026-10-14) ----------
   NOT IN USE: trialled on the hosting hub, owner chose the pale blue. Kept for
   reference; delete it and products-geo-navy.svg if it stays unused. */
   The dark sibling of `--blue`: deep navy facets (products-geo-navy.svg) so
   the last band flows straight into the footer. The white FAQ card stays
   white and pops. The CTA card is itself dark ink, so inside a navy stage it
   gets a 1px light edge and a faint outer glow to keep its silhouette. The
   fades run to the navy base, and the peaks take the same colour. */
.mesh-stage--navy::before {
  background-color: var(--mesh-navy-base);
  background-image: linear-gradient(
      180deg,
      var(--mesh-navy-base) 0%,
      rgba(11, 19, 34, 0) 6%,
      rgba(11, 19, 34, 0) 94%,
      var(--mesh-navy-base) 100%
    ),
    url("../img/hero/products-geo-navy.svg");
}

.mesh-stage--navy.mesh-stage--peaks::after {
  background: var(--mesh-navy-base);
}

.mesh-stage--navy .cta-band {
  border: 1px solid rgba(148, 170, 214, 0.28);
  box-shadow: 0 0 0 1px rgba(6, 11, 20, 0.6), 0 24px 64px -16px rgba(0, 0, 0, 0.55);
}

/* ---------- Triangle skyline (`mesh-stage--peaks`) ----------
   A hand-placed, irregular row of triangular peaks that rise out of the top
   of the mesh into the band above, so the faceted wallpaper seems to climb
   past the seam instead of being cut off by a ruler. The peaks are the mesh's
   own colour (white to cloud) and are 72px tall (40px phones), which sits
   inside the 96px / 48px overlap padding the band above carries
   (`--pre-overlap`), so they only ever cover padding (SC 2.4.11). They sit
   under the floating card (z-index 1 vs the card's 2) and are decorative:
   `pointer-events: none`, no content. */
.mesh-stage--peaks::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: -40px;
  height: 40px;
  /* -1, not 1: the FAQ section is its own stacking layer, so the card's
     z-index cannot out-rank a positive one here. -1 paints after the mesh
     (::before, also -1) but under all section content. */
  z-index: -1;
  background: var(--white);
  clip-path: polygon(
    0 100%, 0 62%, 4% 30%, 8% 70%, 13% 12%, 18% 58%, 22% 38%, 27% 80%, 33% 22%,
    38% 66%, 44% 8%, 49% 52%, 53% 34%, 58% 76%, 64% 18%, 69% 60%, 74% 28%,
    79% 72%, 85% 14%, 90% 56%, 94% 36%, 97% 68%, 100% 40%, 100% 100%
  );
  pointer-events: none;
  /* Own compositing layer: without it the clip-path layer and the band above are rasterised on different pixel grids and a 1px hairline of the dark band shows at its base at some zoom levels (found at 100% in headless Edge, owner saw it at 100% and 125%). */
  will-change: transform;
}

@media (min-width: 640px) {
  .mesh-stage--peaks::after {
    top: -72px;
    height: 72px;
  }
}

/* White cards on a dark band: `.section--ink` turns every h2/h3/h4 white and
   body text light, which makes a plain `.card` (white surface) unreadable.
   Restore the card's own ink colours inside it. */
.section--ink .card:not(.card--ink) h3,
.section--ink .card:not(.card--ink) h4 {
  color: var(--ink-900);
}

.section--ink .card:not(.card--ink) p {
  color: var(--slate-600);
}

/* Body copy that sits directly on an ink band (not inside a white card):
   the section's own slate-300 is right for paragraphs, but `.lead`, `.prose`
   and the small print carry their own lighter-section colours, which fail
   contrast on ink. Scoped here so light bands keep theirs. */
.section--ink .lead,
.section--ink .prose p {
  color: var(--slate-300);
}

.section--ink .small.muted {
  color: var(--slate-400);
}

/* ---------- Skyline on a single band (`section--peaks-top`) ----------
   The same irregular triangle row as `mesh-stage--peaks`, but on the TOP edge
   of any band, in that band's own colour (`--peaks-color`, default cloud), so
   the band rises into the band above as jagged facets instead of a ruler line.
   The band above must have bottom padding >= the peak height (72px / 40px
   phones); the peaks are decorative and sit under the band's content. */
.section--peaks-top {
  position: relative;
  z-index: 2;
}

.section--peaks-top::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: -40px;
  height: 40px;
  z-index: -1;
  background: var(--peaks-color, var(--cloud-100));
  clip-path: polygon(
    0 100%, 0 62%, 4% 30%, 8% 70%, 13% 12%, 18% 58%, 22% 38%, 27% 80%, 33% 22%,
    38% 66%, 44% 8%, 49% 52%, 53% 34%, 58% 76%, 64% 18%, 69% 60%, 74% 28%,
    79% 72%, 85% 14%, 90% 56%, 94% 36%, 97% 68%, 100% 40%, 100% 100%
  );
  pointer-events: none;
  /* Own compositing layer: without it the clip-path layer and the band above are rasterised on different pixel grids and a 1px hairline of the dark band shows at its base at some zoom levels (found at 100% in headless Edge, owner saw it at 100% and 125%). */
  will-change: transform;
}

@media (min-width: 640px) {
  .section--peaks-top::after {
    top: -72px;
    height: 72px;
  }
}

/* A plain mesh band is the layer BELOW whatever precedes it, so that band
   casts a soft shadow down onto the wallpaper: a layered edge instead of a
   hard cut. `:where()` keeps the position/z-index at zero specificity so it
   never overrides a band's own stacking (tabs, floats). */
:where(.section:has(+ .section--mesh)) {
  position: relative;
  z-index: 1;
}

.section:has(+ .section--mesh) {
  box-shadow: 0 20px 32px -20px rgba(10, 18, 32, 0.18);
}

/* Inside a flush stage the FAQ and the band after it each carry ~99px of
   section padding, which stacked into a 198px empty strip around the card. */
.mesh-stage--flush > .section--faq {
  padding-bottom: var(--sp-8);
}

.mesh-stage--flush > .section--faq + .section {
  padding-top: var(--sp-8);
}

/* ---------- Square floating card (`.faq-card`) ----------
   A deliberately hard-edged white card (radius 0) lifted by a layered shadow,
   to break up the rounded-corner rhythm elsewhere on the page. The section
   around it is transparent and has no top padding: the card pulls itself up
   96px (48px phones) over the dark band above, which is padded to cover it
   (`--pre-overlap`), so the overlap only ever sits on padding (SC 2.4.11). */
.section--faq {
  padding-top: 0;
}

/* The mesh is the surface here; the ambient glow disc would only muddy it. */
.section.section--faq::before {
  content: none;
  animation: none;
}

.faq-card {
  position: relative;
  z-index: 2;
  margin-top: -48px;
  padding: var(--sp-8) var(--sp-6);
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: 0;
  box-shadow: 0 1px 2px rgba(10, 18, 32, 0.06), 0 12px 24px -8px rgba(10, 18, 32, 0.1),
    0 32px 64px -16px rgba(10, 18, 32, 0.14);
}

@media (min-width: 640px) {
  .faq-card {
    margin-top: -96px;
    padding: var(--sp-12) var(--sp-12);
  }
}

@media (min-width: 640px) {
  .section--tab.section--tab-b {
    border-radius: 56px;
    margin-bottom: -64px;
  }

  .mesh-stage {
    padding-top: calc(64px + var(--sp-8));
  }
}

/* The featured plan stands a step proud of its neighbours on desktop. Scale
   only (transform), so no layout shift; the hover lift keeps the scale. */
@media (min-width: 960px) {
  .plan--featured {
    transform: scale(1.03);
  }

  .plan--featured:hover {
    transform: scale(1.03) translateY(-5px);
  }
}

/* ---------- The "file tab" transition (owner request 2026-10-14) ----------
   A full-bleed band that slides OVER the bottom of the band above it, with
   large rounded top corners, so the previous section reads as sitting behind
   a tab. Modelled on SiteGround's section transitions; used on some
   boundaries only, so the page keeps its rhythm.

   Pair it: the band ABOVE carries `section--pre-tab` (extra bottom padding, so
   the overlap only ever covers padding, never content - SC 2.4.11) and the
   band that overlaps carries `section--tab`. `overflow: clip` keeps full-bleed
   children (wallpaper, dot matrix) inside the rounded corners.
   `--tab-end` rounds only the top-right corner, the asymmetric variant. */
.section--pre-tab {
  padding-bottom: calc(var(--section-y) + 64px);
}

.section--tab {
  position: relative;
  z-index: 2;
  margin-top: -64px;
  border-radius: 28px 28px 0 0;
  overflow: clip;
}

@media (min-width: 640px) {
  .section--tab {
    border-radius: 56px 56px 0 0;
  }

  .section--tab.section--tab-end {
    border-radius: 0 56px 0 0;
  }
}

@media (max-width: 639px) {
  .section--pre-tab {
    padding-bottom: calc(var(--section-y-sm) + 40px);
  }

  .section--tab {
    margin-top: -40px;
  }
}

/* The artwork that replaced the two-lane diagram: the Authorized Reseller
   shield, centred in its column and capped. The source is only 256px, so it
   is never stretched wider than ~420px — past that the shield's edge softens.
   No number lives in this raster (see the section note above).

   Vertically it centres against the copy column beside it, not against the
   top of the band. `.supply` is `align-items: start` at desktop (the copy
   column is the taller one, so the row is its height); the art column is
   therefore stretched to that row height and its single child is centred
   inside it. Without `align-self: stretch` the wrapper would collapse to the
   image's own height and `align-content: center` would have nothing to centre
   against, leaving the shield pinned to the top as before. */
.supply__art {
  display: grid;
  place-items: center;
  align-content: center;
}

@media (min-width: 1000px) {
  .supply__art {
    align-self: stretch;
  }
}

.supply__art img {
  display: block;
  width: min(100%, 420px);
  height: auto;
}

/* ---------- The proof row + the fee note (B3 tail) ----------
   The artwork and the copy column make the argument; these two blocks close
   it. The proofs are the three things a visitor can hold us to. The fee note
   is the honesty voice this whole site runs on, given its own quiet line
   instead of being buried in the band — the ICANN fee is the one number on
   this page that is not ours to decide, and saying so plainly is worth more
   than another adjective. */
.supply__proofs {
  display: grid;
  gap: var(--sp-6);
  margin-top: var(--sp-12);
  padding-top: var(--sp-8);
  border-top: 1px solid var(--ink-700);
}

@media (min-width: 760px) {
  .supply__proofs {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-10);
  }
}

.supply__proof {
  margin: 0;
}

.supply__proof-ico {
  width: 22px;
  height: 22px;
  color: var(--brand-400);
  margin-bottom: var(--sp-3);
}

.supply__proof-title {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--white);
  letter-spacing: var(--tracking-tight);
}

.supply__proof-body {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
  color: var(--slate-400);
}

.supply__note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-6);
  margin: var(--sp-8) 0 0;
  padding: var(--sp-4) var(--sp-5);
  background: var(--ink-800);
  border: 1px solid var(--ink-700);
  border-radius: var(--r-md);
}

.supply__note-label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--brand-300);
}

.supply__note-text {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--slate-400);
}

/* ---------- Plan card ---------- */
.plan {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  padding: var(--sp-6);
  position: relative;
  transition: border-color var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}

.plan:hover {
  border-color: var(--slate-300);
  box-shadow: var(--sh-xl);
  transform: translateY(-4px);
}

/* The featured tier wears the brand gradient as its outline — a real gradient
   ring (padding-box/border-box trick), not a flat 2px stroke — and it lifts a
   little higher on hover than its neighbours, so the recommendation reads
   before a word of the card does. */
.plan--featured {
  border: 2px solid transparent;
  background:
    linear-gradient(var(--white), var(--white)) padding-box,
    var(--grad-brand) border-box;
  box-shadow: var(--sh-brand);
}

.plan--featured:hover {
  border-color: transparent;
  box-shadow: var(--sh-brand-lg);
  transform: translateY(-5px);
}

/* The ribbon hangs off the top edge, so it only declares its position. The
   pill itself is the shared `.flag`. */
.plan__flag {
  position: absolute;
  top: -12px;
  left: var(--sp-6);
}

.plan__name {
  font-size: var(--fs-lg);
  font-weight: 700;
  color: var(--ink-900);
}

.plan__tagline {
  font-size: var(--fs-sm);
  color: var(--slate-500);
  margin-top: 2px;
  min-height: 2.6em;
}

.plan__features {
  list-style: none;
  padding: 0;
  margin: var(--sp-5) 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  flex: 1;
}

.plan__features li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: 1.45;
  margin: 0;
  color: var(--slate-700);
}

.plan__features li::before {
  content: "";
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  border-radius: var(--r-full);
  background: var(--brand-100)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230A8158' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E")
    center / 10px 10px no-repeat;
}

/* A plan card whose list carries a bold lead and a sentence per point, so a
   short list still fills a card that is stretched to match its neighbours. */
.plan__features--roomy {
  display: grid;
  gap: var(--sp-5);
}

.plan__features--roomy strong {
  color: var(--ink-900);
}

/* A compact price list inside a plan card (the domains page lists first-year
   prices per extension). Name on the left, price on the right, hairline rows. */
.price-list {
  list-style: none;
  margin: var(--sp-5) 0 0;
  padding: 0;
}

.price-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-4);
  padding-block: var(--sp-2);
  border-bottom: 1px solid var(--slate-100);
  font-size: var(--fs-sm);
  color: var(--slate-700);
}

.price-list b {
  font-weight: 700;
  color: var(--ink-900);
}

.price-list small {
  display: block;
  font-size: var(--fs-xs);
  color: var(--slate-500);
}

.price-list__amount {
  font-family: var(--font-display);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ink-900);
}

.plan__foot {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--slate-200);
  display: grid;
  gap: var(--sp-3);
}

/* ================= VALIDATION TABS =========================
   Three mutually-exclusive families (DV / OV / EV) as a segmented
   control built from radio inputs — no JavaScript.

   The inputs are visually hidden but NOT display:none, so they stay in
   the tab order and the native arrow-key movement and the "2 of 3,
   selected" announcement work untouched. The panels are flat siblings
   of the inputs, because the reveal is a `~` selector — which is also
   why the whole thing sits inside one <fieldset>: its <legend> is what
   names the group, and a fieldset does not break the sibling chain.
   With CSS unavailable every panel shows and all sixteen cards stay
   readable, which is the failure mode we want.
   ========================================================== */
.plan-tabs {
  position: relative;
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}

.plan-tabs__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.plan-tabs__bar {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: var(--cloud-100);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  margin-bottom: var(--sp-8);
}

.plan-tabs__tab {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
  text-align: center;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.3;
  color: var(--slate-600);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.plan-tabs__tab:hover {
  color: var(--ink-900);
}

.plan-tabs__sub {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--slate-500);
}

/* The selected tab. The input is invisible, so these rules are the only
   link between the two — the label is styled, the input does the
   deciding. */
#ssl-tab-dv:checked ~ .plan-tabs__bar [for="ssl-tab-dv"],
#ssl-tab-ov:checked ~ .plan-tabs__bar [for="ssl-tab-ov"],
#ssl-tab-ev:checked ~ .plan-tabs__bar [for="ssl-tab-ev"] {
  background: var(--white);
  color: var(--ink-900);
  box-shadow: var(--sh-sm);
}

#ssl-tab-dv:checked ~ .plan-tabs__bar [for="ssl-tab-dv"] .plan-tabs__sub,
#ssl-tab-ov:checked ~ .plan-tabs__bar [for="ssl-tab-ov"] .plan-tabs__sub,
#ssl-tab-ev:checked ~ .plan-tabs__bar [for="ssl-tab-ev"] .plan-tabs__sub {
  color: var(--brand-600);
}

/* The input that actually receives focus cannot be seen, so the ring is
   re-drawn on its label — the same 3px brand-600 as the global
   :focus-visible rule in base.css, for the same contrast reason. */
#ssl-tab-dv:focus-visible ~ .plan-tabs__bar [for="ssl-tab-dv"],
#ssl-tab-ov:focus-visible ~ .plan-tabs__bar [for="ssl-tab-ov"],
#ssl-tab-ev:focus-visible ~ .plan-tabs__bar [for="ssl-tab-ev"] {
  outline: 3px solid var(--brand-600);
  outline-offset: 2px;
}

.plan-tabs__panel {
  display: none;
}

#ssl-tab-dv:checked ~ .plan-tabs__panel[data-family="dv"],
#ssl-tab-ov:checked ~ .plan-tabs__panel[data-family="ov"],
#ssl-tab-ev:checked ~ .plan-tabs__panel[data-family="ev"] {
  display: block;
}

.plan-tabs__blurb {
  max-width: 62ch;
  margin: 0 auto var(--sp-6);
  text-align: center;
  color: var(--slate-600);
}

/* Three labels cannot share a row at 360px, so the control becomes three
   full-width rows: name left, price right, one line each. */
@media (max-width: 640px) {
  .plan-tabs__bar {
    grid-template-columns: minmax(0, 1fr);
  }

  .plan-tabs__tab {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    text-align: left;
  }
}

/* ================= VPS CATEGORY TOGGLE + PLAN GRID =========
   The same no-JavaScript radio pattern as the SSL validation tabs, with its
   own ids (the SSL rules above are keyed to `ssl-tab-*`) and a two-option
   bar. Standard and High performance each hold five purchase cards in
   `.plan-grid`: one column on phones, two from 640px, three from 960px and
   all five across from 1280px. The grid's top padding leaves room for the
   "Most popular" flag that hangs off the featured card.
   ========================================================== */
.plan-tabs--2 .plan-tabs__bar {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  max-width: 560px;
  margin-inline: auto;
}

#vps-tab-std:checked ~ .plan-tabs__bar [for="vps-tab-std"],
#vps-tab-hp:checked ~ .plan-tabs__bar [for="vps-tab-hp"] {
  background: var(--white);
  color: var(--ink-900);
  box-shadow: var(--sh-sm);
}

#vps-tab-std:checked ~ .plan-tabs__bar [for="vps-tab-std"] .plan-tabs__sub,
#vps-tab-hp:checked ~ .plan-tabs__bar [for="vps-tab-hp"] .plan-tabs__sub {
  color: var(--brand-600);
}

#vps-tab-std:focus-visible ~ .plan-tabs__bar [for="vps-tab-std"],
#vps-tab-hp:focus-visible ~ .plan-tabs__bar [for="vps-tab-hp"] {
  outline: 3px solid var(--brand-600);
  outline-offset: 2px;
}

#vps-tab-std:checked ~ .plan-tabs__panel[data-family="std"],
#vps-tab-hp:checked ~ .plan-tabs__panel[data-family="hp"] {
  display: block;
}

/* Two short labels still fit side by side at 360px, so keep the stacked
   name-over-price layout instead of the single-column rows the three-tab
   SSL bar switches to. */
@media (max-width: 640px) {
  .plan-tabs--2 .plan-tabs__tab {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 2px;
  }
}

.plan-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-6);
  padding-top: var(--sp-4);
}

@media (min-width: 640px) {
  .plan-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 960px) {
  .plan-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 1280px) {
  .plan-grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

/* "Add a control panel": three compact cards under the plan grid. */
.addon-head {
  margin: var(--sp-12) 0 var(--sp-6);
  max-width: 62ch;
}

.addon-head h3 {
  margin: 0 0 var(--sp-2);
}

.addon-head p {
  margin: 0;
  color: var(--slate-600);
}

.addon {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.addon h4 {
  margin: 0;
  font-size: var(--fs-lg);
  color: var(--ink-900);
}

.addon .plan__features {
  margin-top: var(--sp-2);
}

/* Indigo marks these as optional add-ons, apart from the emerald plan cards
   above. Scoped to `.addon` so the plan cards keep their green ticks. The
   tile is decorative (`aria-hidden` in the markup); the card name carries
   the meaning. */
/* Vendor logos, shown unmodified in their own colours (the vendors' brand
   rules forbid recolouring). Every slot is the same height so the three card
   names line up; the logo sits on the slot's left edge. The images are
   decorative (empty alt) because the card name beside them names the product. */
.addon__logo-slot {
  display: flex;
  align-items: center;
  height: 44px;
}

.addon__logo {
  display: block;
  width: auto;
  max-width: 100%;
}

.addon__logo--cpanel {
  height: 28px;
}

.addon__logo--plesk {
  height: 40px;
}

.addon .plan__features li::before {
  background: var(--indigo-100)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234F46E5' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E")
    center / 10px 10px no-repeat;
}

/* ================= FEATURE LIST ========================= */
.feature-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--sp-4);
}

.feature-list--2col {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 720px) {
  .feature-list--2col {
    grid-template-columns: minmax(0, 1fr);
  }
}

.feature-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  margin: 0;
  line-height: 1.5;
}

.feature-list .tick {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  border-radius: var(--r-full);
  background: var(--brand-50);
  color: var(--brand-600);
  display: grid;
  place-items: center;
}

.feature-list .tick svg {
  width: 12px;
  height: 12px;
}

.section--ink .feature-list .tick {
  background: rgba(52, 196, 138, 0.16);
  color: var(--brand-400);
}

.feature-list strong {
  display: block;
  color: var(--ink-900);
  font-weight: 650;
}

.section--ink .feature-list strong {
  color: var(--white);
}

.feature-list span {
  font-size: var(--fs-sm);
  color: var(--slate-500);
}

/* ================= FORMS =============================== */
.field {
  display: grid;
  gap: var(--sp-2);
}

.field label,
.label {
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--ink-800);
}

.field__hint {
  font-size: var(--fs-xs);
  color: var(--slate-500);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: 44px;
  padding: 10px var(--sp-4);
  border: 1px solid var(--slate-300);
  border-radius: var(--r-sm);
  background: var(--white);
  font-size: var(--fs-base);
  color: var(--ink-900);
  transition: border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.input::placeholder,
.textarea::placeholder {
  color: var(--slate-400);
}

.input:hover,
.select:hover,
.textarea:hover {
  border-color: var(--slate-400);
}

.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px rgba(18, 166, 111, 0.18);
}

.textarea {
  min-height: 130px;
  resize: vertical;
}

.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23667085' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 15px;
  padding-right: 38px;
}

.form-grid {
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.form-grid .field--full {
  grid-column: 1 / -1;
}

@media (max-width: 640px) {
  .form-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- Domain search bar ---------- */
.domain-search {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: var(--white);
  border: 1px solid var(--slate-300);
  border-radius: var(--r-md);
  box-shadow: var(--sh-lg);
  max-width: 620px;
}

.domain-search:focus-within {
  border-color: var(--brand-500);
  box-shadow: 0 0 0 4px rgba(18, 166, 111, 0.16), var(--sh-lg);
}

.domain-search input[type="search"],
.domain-search input[type="text"] {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 0 var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--ink-900);
}

.domain-search input:focus {
  outline: none;
}

.domain-search input::placeholder {
  color: var(--slate-400);
  font-weight: 450;
}

.domain-search .btn {
  flex-shrink: 0;
}

.domain-search--dark {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.2);
  box-shadow: none;
  backdrop-filter: blur(8px);
}

.domain-search--dark input {
  color: var(--white);
}

/* The base rule is `.domain-search input[type="search"]` (0,2,1).
   This override must match that specificity or it loses, which is
   exactly what made typed text render near-black on the dark glass
   background. */
.domain-search--dark input[type="search"],
.domain-search--dark input[type="text"] {
  color: var(--white);
}

.domain-search--dark input::placeholder {
  color: var(--slate-400);
}

/* ---------- Domain search — the hero variant ----------
   The domains registration hero makes the search the page's PRIMARY
   action, so this variant is deliberately bigger than the inline bars
   elsewhere: a taller field, a larger button, and room INSIDE the
   field for the decorative typed example (`.domain-search__typed`).

   It is the same form contract as the base component — same action,
   same `name="domainToCheck"`, same progressive-enhancement JS — so
   the search behaviour does not change, only its weight. */
.domain-search--hero {
  align-items: stretch;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--r-lg);
  max-width: none;
}

.domain-search__field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
}

/* The hero field is deliberately wide and tall: it is the page's primary
   action and must read as the focal point. */
.domain-search--hero input[type="search"] {
  width: 100%;
  height: 60px;
  padding: 0 var(--sp-5);
  font-size: var(--fs-xl);
  font-weight: 600;
}

/* With JS, the idle text is the typed example overlay, not the native
   placeholder, so the placeholder is made transparent to avoid the two
   sitting on top of each other. Without JS the overlay is hidden and the
   native placeholder shows normally (see the html.js gate below). */
html.js .domain-search--hero input::placeholder {
  color: transparent;
}

.domain-search--hero .btn {
  height: 60px;
  padding-inline: var(--sp-8);
  font-size: var(--fs-md);
}

/* ---------- The typed example overlay ----------
   A light-gray "type and erase" line that lives inside the field,
   cycling example names to inspire a search. It is a decorative overlay
   (`aria-hidden`, `pointer-events:none`) so it never intercepts a
   keystroke, and it is hidden unless JS is present — falling back to the
   real placeholder when the script cannot run. JS stops it the moment
   the field is focused or has a value. */
.domain-search__typed {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  padding-left: var(--sp-5);
  overflow: hidden;
  pointer-events: none;
  color: var(--slate-400);
  font-size: var(--fs-xl);
  font-weight: 600;
  white-space: nowrap;
  text-overflow: ellipsis;
}

html.js .domain-search__typed {
  display: flex;
}

/* Hide the overlay (and its caret) whenever the field is focused or already
   has a value, so it can never sit over the visitor's own text. The filled
   state is set by domain-hero-rotator.js as `.is-filled` — `:has()` with a
   `:placeholder-shown` argument proved unreliable across browsers, so the
   value state is tracked in JS rather than in the selector. */
.domain-search--hero:focus-within .domain-search__typed,
.domain-search--hero.is-filled .domain-search__typed {
  display: none;
}

/* The DARK variant (the homepage hero bar) runs the same overlay contract
   as the hero variant, so it gets the same three guarantees — the native
   placeholder stands in without JS, and hides with JS so it cannot double
   up with the typed example; the overlay drops out the moment the field is
   focused or filled. Its metrics match THIS bar's input (fs-md / 500 /
   sp-4 padding), not the hero variant's 60px row. The WHOIS page keeps its
   own stage treatment in stage-hero.css untouched — that page shows the
   native placeholder and hides the overlay entirely. */
html.js .domain-search--dark input::placeholder {
  color: transparent;
}

.domain-search--dark .domain-search__typed {
  padding-left: var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 500;
}

.domain-search--dark:focus-within .domain-search__typed,
.domain-search--dark.is-filled .domain-search__typed {
  display: none;
}

/* Stretch the field to the bar's row height so the input keeps its full
   tap target now that it sits inside the wrapper (the hero variant sets an
   explicit 60px and is unaffected). */
.domain-search--dark .domain-search__field {
  align-items: stretch;
}

/* A caret: a thin blinking bar, as if someone is typing the example. */
.domain-search__typed::after {
  content: "";
  width: 2px;
  height: 1.1em;
  margin-left: 2px;
  background: var(--slate-400);
  animation: domainTypedCaret 1s steps(1) infinite;
}

@keyframes domainTypedCaret {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* The line under the field and the demoted account link: the search is
   the primary action, account creation is the step AFTER a search. */
.domain-search__note {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--slate-500);
}

.domain-search__account {
  display: inline-flex;
  align-items: center;
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--brand-700);
  /* WCAG 2.5.8 floor — a standalone link, not inline prose. */
  min-height: 24px;
}

/* The launch button: a rocket instead of a magnifier, so the field reads as
   a launchpad ("search" = "launch your idea") without turning gimmicky. A
   soft ring pulses out every few seconds to keep the CTA quietly alive. */
.domain-search__go {
  position: relative;
  box-shadow: 0 6px 26px -6px rgba(10, 129, 88, 0.55);
}

.domain-search__go::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 0 0 rgba(10, 129, 88, 0.5);
  animation: domainLaunchPulse 5s ease-out infinite;
  pointer-events: none;
}

@keyframes domainLaunchPulse {
  0%, 68% {
    box-shadow: 0 0 0 0 rgba(10, 129, 88, 0.45);
    opacity: 1;
  }
  92%, 100% {
    box-shadow: 0 0 0 16px rgba(10, 129, 88, 0);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .domain-search__go::after {
    animation: none;
    opacity: 0;
  }
}

/* Reduced motion: the JS rotator leaves the first example static, and the
   caret does not blink. */
@media (prefers-reduced-motion: reduce) {
  .domain-search__typed::after {
    animation: none;
  }
}

/* On a narrow screen the field and the button stack. */
@media (max-width: 520px) {
  .domain-search--hero {
    flex-direction: column;
  }
  .domain-search--hero .btn {
    width: 100%;
    padding-inline: var(--sp-5);
  }
}

/* ---------- WHOIS lookup ----------
   The WHOIS page's search box and the card it answers with. The form is
   the same .domain-search component the domain pages use, minus its outer
   shadow: it already sits inside the hero's aside card, and a second
   shadow inside a card reads as a mistake rather than as depth.

   Everything here is written by whois-lookup.js with textContent, so the
   result card is data — these rules only decide how that data looks. */
/* The example chips under the bar. They are a STANDALONE control row
   (pick a name, get a lookup), not inline prose, so they carry a real
   hit area: inline-flex + min-height is the shape the other 24px fixes
   on the site use — `min-height` alone is inert on an inline element.
   The stage page re-tints them in stage-hero.css. */
.whois__examples {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.whois__chip {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: var(--chip-pad-y) var(--chip-pad-x);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-full);
  background: var(--white);
  color: var(--slate-600);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-out);
}

.whois__chip:hover {
  color: var(--brand-700);
  border-color: var(--brand-300);
}


.whois__flag {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: var(--r-full);
  background: var(--brand-50);
  border: 1px solid var(--brand-100);
  color: var(--brand-700);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

.whois__sub {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  color: var(--slate-600);
}

.whois__form {
  margin-top: var(--sp-5);
  max-width: none;
  background: var(--cloud-50);
  box-shadow: none;
}

.whois__out {
  margin-top: var(--sp-5);
}

/* The region is empty until a lookup runs, and hidden in the markup so a
   visitor without JavaScript never meets an empty box. */
.whois__out[hidden] {
  display: none;
}

.whois__out[aria-busy="true"] {
  opacity: 0.72;
}

.whois__card {
  padding: var(--sp-4);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-md);
  background: var(--cloud-50);
  font-size: var(--fs-sm);
}

.whois__card--free {
  background: var(--brand-50);
  border-color: var(--brand-100);
}

.whois__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

.whois__domain {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: 650;
  color: var(--ink-900);
  overflow-wrap: anywhere;
}

.whois__msg {
  margin: var(--sp-3) 0 0;
  line-height: var(--lh-normal);
  color: var(--slate-600);
}

.whois__grid {
  display: grid;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 0;
}

.whois__row {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: baseline;
}

.whois__row dt {
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--slate-500);
}

.whois__row dd {
  margin: 0;
  color: var(--ink-800);
  overflow-wrap: anywhere;
}

.whois__missing {
  color: var(--slate-500);
}

.whois__note {
  font-size: var(--fs-xs);
  color: var(--slate-500);
}

.whois__block {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--slate-200);
}

.whois__label {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--slate-500);
}

.whois__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--chip-gap);
}

.whois__pill {
  display: inline-flex;
  align-items: center;
  padding: var(--chip-pad-y-sm) var(--chip-pad-x-sm);
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 650;
  line-height: 1.5;
  white-space: nowrap;
}

.whois__pill--ok {
  background: var(--success-50);
  color: var(--success-600);
}

/* Expiring inside 45 days, already expired, or otherwise worth acting on.
   Amber, not red: it is a signal, not an error. */
.whois__pill--warn {
  background: var(--amber-50);
  color: var(--amber-700);
}

.whois__pill--soft {
  background: var(--white);
  border: 1px solid var(--slate-200);
  color: var(--slate-600);
  font-weight: 550;
}

.whois__links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--slate-200);
}

.whois__links a {
  font-weight: 650;
}

@media (max-width: 420px) {
  .whois__row {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

.tld-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
  align-items: center;
}

.tld-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 5px 11px;
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.13);
  font-size: var(--fs-sm);
  color: var(--slate-300);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.tld-chip b {
  font-weight: 700;
  color: var(--white);
}

.tld-chip:hover {
  background: rgba(18, 166, 111, 0.18);
  border-color: rgba(52, 196, 138, 0.5);
  color: var(--white);
  text-decoration: none;
  transform: translateY(-1px);
}

.tld-chips--light .tld-chip {
  background: var(--cloud-100);
  border-color: var(--slate-200);
  color: var(--slate-600);
}

.tld-chips--light .tld-chip b {
  color: var(--ink-900);
}

.tld-chips--light .tld-chip:hover {
  background: var(--brand-50);
  border-color: var(--brand-300);
  color: var(--brand-700);
  text-decoration: none;
}

/* ---- THE DRIFT ------------------------------------------------

   A slow horizontal travel of the extension row, used once, on the
   domains index. It is here to say "we sell a great many extensions"
   in four lines of CSS, which is a claim an eight-item list cannot
   make on its own.

   The seam is the whole problem with a marquee. The track holds the
   list twice and slides left by exactly one set's width, so the copy
   leaving the left edge is replaced by identical copy entering the
   right — the loop point falls between two identical chips and is
   invisible.

   `--tld-set-w` is the measured width of ONE set, and it is set by
   site.js on load and on resize. It is measured rather than hardcoded
   because the chips are `inline-flex` with real padding and a real
   font: their width depends on the font that loaded and the container
   that wrapped them, and a hardcoded translate would drift out of
   step on the first font swap and show a gap.

   46s for the full traversal is deliberately slow. This runs for as
   long as the page is open and it is the only thing moving on the
   page; anything quicker turns "we have a lot of these" into
   "something is scrolling".

   Pauses on hover AND on focus-within, because a chip that slides out
   from under the pointer mid-click is a bug, and a keyboard user
   tabbing along the row needs the same protection.

   The edges are masked, not faded with a gradient overlay, so the
   chips dissolve into the band rather than sliding under a hard
   edge. `mask-image` needs the `-webkit-` prefix for Safari, which is
   why both are written.

   `min-width: 0` ON THE GRID ITEM, not just here. `.page-hero__aside`
   is a grid child, and a grid child defaults to `min-width: auto` —
   which means it refuses to be narrower than its widest content. A
   `width: max-content` track is very wide content, so without this the
   aside refused to shrink, blew out its `1fr` column, and pushed the
   whole document into horizontal scroll. Measured at 1440px: the aside
   came out 1080px wide inside a 520px column, and `scrollWidth` reached
   1465 against a 1424 viewport.

   `min-width: 0` on `.tld-drift` alone does NOT fix this. That stops
   the drift clipping, but the grid item above it is the thing refusing
   to shrink, and it has to be told directly. The rule is scoped with
   `:has()` so no other hero aside is affected. */
.page-hero__aside:has(.tld-drift) {
  min-width: 0;
}

/* The identical contract for the homepage hero. The drift there sits in a
   bare grid child of `.hero__inner` rather than in `.page-hero__aside`, and
   that child defaults to `min-width: auto` in exactly the same way — the
   marquee's `width: max-content` track is wide content, so without this the
   left column refuses to shrink, blows out its `1.15fr`, and the whole
   document scrolls sideways. Scoped by `:has()` again so no other hero
   column is affected. */
.hero__inner > *:has(.tld-drift) {
  min-width: 0;
}
.tld-chips--drift {
  flex-wrap: nowrap;
  align-items: center;
}

.tld-chips--drift .tld-chips__lead {
  flex-shrink: 0;
  margin-right: var(--sp-2);
}

.tld-drift {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 5%,
    #000 95%,
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 5%,
    #000 95%,
    transparent 100%
  );
}

.tld-drift__track {
  display: flex;
  width: max-content;
  animation: tld-drift 46s linear infinite;
  will-change: transform;
}

.tld-drift__set {
  display: flex;
  gap: var(--sp-2);
  padding-right: var(--sp-2);
}

.tld-drift:hover .tld-drift__track,
.tld-drift:focus-within .tld-drift__track {
  animation-play-state: paused;
}

@keyframes tld-drift {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(calc(var(--tld-set-w, 50%) * -1), 0, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* No drift, and no mask either — the mask exists to hide an edge
     that only exists while the row is travelling. With the row
     stationary and clipped, a legitimate chip could be cut in half at
     the right edge for no reason, so the overflow goes too and the
     list simply sits there. */
  .tld-drift {
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .tld-drift__track {
    animation: none;
    width: auto;
    flex-wrap: wrap;
  }

  /* The duplicated set is invisible under reduced motion, so it does
     not need to be hidden from anyone — but it is still marked
     aria-hidden, and `display: none` keeps the two from being read as
     one list by anything that walks the DOM. */
  .tld-drift__set[aria-hidden="true"] {
    display: none;
  }
}

.form-note {
  font-size: var(--fs-xs);
  color: var(--slate-500);
  margin-top: var(--sp-4);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.form-note svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--brand-500);
}

/* ================= ACCORDION (FAQ) ====================== */
.accordion {
  border-top: 1px solid var(--slate-200);
}

.acc-item {
  border-bottom: 1px solid var(--slate-200);
}

.acc-item > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-5);
  padding: var(--sp-5) 0;
  font-size: var(--fs-lg);
  font-weight: 650;
  color: var(--ink-900);
  line-height: 1.4;
}

.acc-item > summary::-webkit-details-marker {
  display: none;
}

.acc-item > summary::after {
  content: "";
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  border-radius: var(--r-full);
  background: var(--slate-100)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23344054' stroke-width='2.6' stroke-linecap='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E")
    center / 12px 12px no-repeat;
  transition: transform var(--dur-base) var(--ease-out),
    background-color var(--dur-base) var(--ease-out);
}

.acc-item[open] > summary {
  color: var(--brand-700);
}

.acc-item[open] > summary::after {
  transform: rotate(45deg);
  background-color: var(--brand-100);
}

.acc-item > summary:hover {
  color: var(--brand-700);
}

/* The body animates open with the grid-rows technique rather than a
   max-height. `max-height` has to be guessed: too small clips the answer
   mid-sentence, too large makes a two-line answer crawl open through a
   400px void. `grid-template-rows: 0fr -> 1fr` interpolates to whatever
   the content's real height turns out to be, so a short answer and a
   long one both open at the same honest speed.

   The inner wrapper is what makes it work — a grid item at 0fr has zero
   height, and `overflow: hidden` on that item is what actually clips the
   text. `.acc-body` itself cannot clip, because it is the grid container
   and has to be allowed to grow.

   READ THIS BEFORE SIMPLIFYING IT. The closed-state rule below is not
   redundant with the browser's own stylesheet, and deleting it breaks
   the entire component. The UA sheet hides a closed `<details>` with
   `details:not([open]) > *:not(summary) { display: none }`, and an
   AUTHOR `display` declaration beats a UA one no matter how weak its
   selector is. So `.acc-body { display: grid }` on its own silently
   overrides the hiding and renders every answer permanently expanded.

   The fix is an explicit `display: none` on the closed state, which
   restores the UA behaviour deliberately instead of by accident.

   That is also why this is a keyframe animation and not a transition.
   A transition needs a "before" value, and a `display: none` element
   has no rendered "before" — so the open would snap and only the close
   would animate. An animation applied by the `[open]` attribute starts
   from its 0% keyframe with no prior state required. Closing is
   therefore instant, which is what every well-behaved animated
   accordion does anyway. */
.acc-body {
  display: grid;
  grid-template-rows: 1fr;
}

/* READ THIS BEFORE SIMPLIFYING IT.

   Two things about this block are easy to get wrong, and both were
   established by measuring the rendered height rather than by reading
   the CSS. The measuring matters: a headless screenshot FREEZES
   requestAnimationFrame at time zero, so it shows this panel at 24px
   and makes a perfectly good accordion look broken. Do not "fix" what
   a headless screenshot says about an animation.

   THE CLOSED STATE. The UA sheet hides a closed `<details>` with
   `details:not([open]) > *:not(summary) { display: none }`, and an
   AUTHOR `display` declaration beats a UA one no matter how weak its
   selector is. So `.acc-body { display: grid }` on its own silently
   overrides the hiding and renders every answer permanently expanded.
   The explicit `display: none` below restores that deliberately, and it
   is load-bearing — this component has no other closed-state rule.

   WHY THE ROW IS `1fr` AND NOT `auto`. Both resolve to the content
   height when the grid container has no definite height, and the
   textbook `0fr -> 1fr` collapse is the only form that interpolates
   reliably across engines: `fr` is a flexible length, so it has a
   defined starting and ending value to animate between, whereas
   animating to `auto` has no dependable end value to interpolate
   toward. `1fr` here is one share of a free space that does not exist,
   so it resolves to the row's content size — measured at 101px on the
   FAQ page, matching `innerScrollH` exactly.

   The resting value above is `1fr`, the same value the animation lands
   on. That has to be true: a keyframe animation with the default
   `fill-mode: none` releases the element back to its own declared
   value the moment it ends, so a mismatch between the two would snap
   the panel shut at the end of its own opening.

   WHY A KEYFRAME AND NOT A TRANSITION. A transition needs a rendered
   "before" value, and a `display: none` element has none — the open
   would snap and only the close would animate. An animation applied by
   the `[open]` attribute starts from its 0% keyframe with no prior
   state required. Closing is therefore instant, which is what a
   well-behaved accordion does. */
.acc-item:not([open]) > .acc-body {
  display: none;
}

.acc-body__inner {
  overflow: hidden;
  min-height: 0;
  padding: 0 var(--sp-10) var(--sp-6) 0;
  color: var(--slate-600);
  line-height: var(--lh-relaxed);
}

.acc-item[open] > .acc-body {
  animation: acc-open var(--dur-slow) var(--ease-out);
}

@keyframes acc-open {
  from {
    grid-template-rows: 0fr;
  }
  to {
    grid-template-rows: 1fr;
  }
}

.acc-body p + p,
.acc-body p + ul,
.acc-body ul + p {
  margin-top: var(--sp-4);
}

.acc-body ul {
  padding-left: 1.2em;
}

@media (max-width: 640px) {
  .acc-body__inner {
    padding-right: 0;
  }
}

/* Reduced motion still has to OPEN the panel, so the fix is to remove the
   animation while leaving the resting state alone. `animation: none` is
   safe here precisely because the resting state is `1fr` (visible) — the
   same trap the live panel hit does not apply, because nothing about this
   element is hidden by the entrance itself. A visitor who has asked for
   less movement gets a panel that is simply open. */
@media (prefers-reduced-motion: reduce) {
  .acc-item[open] > .acc-body {
    animation: none;
  }
}

/* ================= STEPS ================================ */
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-8);
  counter-reset: step;
}

.step {
  /* The numbered badge is 40px tall and absolutely positioned at
     top:0, so the content needs to clear it plus a real gap —
     otherwise the headline collides with the number. */
  --step-badge: 40px;
  position: relative;
  padding-top: calc(var(--step-badge) + var(--sp-4));
}

.step::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  top: 0;
  left: 0;
  width: var(--step-badge);
  height: var(--step-badge);
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: linear-gradient(140deg, var(--brand-100) 0%, var(--brand-50) 60%, var(--white) 100%);
  border: 1px solid var(--brand-100);
  box-shadow: var(--sh-xs);
  color: var(--brand-700);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 800;
}

/* The dashed connector running badge-to-badge across the row — the "path"
   cue that makes three boxes read as a sequence. Drawn only when the row is
   genuinely horizontal; the auto-fit grid wraps under 860px and a line into
   empty space would be worse than none. */
@media (min-width: 861px) {
  .step::after {
    content: "";
    position: absolute;
    top: calc(var(--step-badge) / 2);
    left: calc(var(--step-badge) + var(--sp-3));
    right: calc(-1 * var(--sp-4));
    border-top: 2px dashed var(--brand-100);
  }

  .step:last-child::after {
    display: none;
  }

  .section--ink .step::after {
    border-color: rgba(52, 196, 138, 0.25);
  }

  /* The connector DRAWS itself in, once, as the band scrolls up — the line
     is pulled out from the first number and extended to the last, which is
     the sequence being asserted rather than merely drawn. It follows the
     number badge rather than leading it, so the 260ms delay lets the badge
     land first; a line racing ahead of its own numbers reads as a progress
     bar for something that has not started.

     Clipped rather than scaled: a dashed border cannot be scaled without
     the dash rhythm stretching, and a stretched dash is the tell that
     something has been animated that should not have been.

     The `.js` prefix matches the reveal system — with no scripting the
     line is simply there, which is exactly what a plain dashed rule would
     have given you. */
  .js .step::after {
    clip-path: inset(0 100% 0 0);
    transition: clip-path 720ms var(--ease-out) 260ms;
  }

  .js .step.is-revealed::after {
    clip-path: inset(0 0 0 0);
  }
}

.step h3 {
  font-size: var(--fs-lg);
}

.step p {
  margin-top: var(--sp-2);
  color: var(--slate-600);
  font-size: var(--fs-base);
}

.section--ink .step::before {
  background: rgba(52, 196, 138, 0.14);
  border-color: rgba(52, 196, 138, 0.3);
  color: var(--brand-400);
}

/* ---------- Sequenced reveal: the transfer-timeline treatment ----------
   `.steps--timeline` (the homepage "How it works" band) runs the same
   staged left-to-right reveal as `.transfer-timeline` on the transfer
   page: each step's number badge pops in, the dashed connector draws
   out toward the next badge a beat later, and the heading and copy
   rise into place — one step every 0.6s, settled in ~1.8s. Once the
   sequence lands, a faint ring pulse travels badge to badge on a 6s
   loop, the same "energy flow" the transfer markers carry.

   `.is-in` is added by transfer-timeline.js the first time the band
   scrolls into view (once only); that script watches `.steps--timeline`
   as well as `.transfer-timeline`, which is why the homepage loads it.
   The collapsed pre-state is gated behind `html.js`: with scripts
   blocked the band simply renders finished. Under
   prefers-reduced-motion the script adds `.is-in` immediately and the
   global reduced-motion block collapses every duration, so the reader
   sees the completed sequence with no motion.

   The step cadence lives in `--tl-delay`, read by every transition
   and by the pulse loop, so one offset choreographs the whole step. */
.steps--timeline .step {
  --tl-delay: 0s;
}

.steps--timeline.is-in .step:nth-child(2) {
  --tl-delay: 0.35s;
}

.steps--timeline.is-in .step:nth-child(3) {
  --tl-delay: 0.7s;
}

/* Collapsed pre-state (JS only — without it the band simply renders finished). */
html.js .steps--timeline:not(.is-in) .step::before {
  opacity: 0;
  transform: scale(0.5);
}

html.js .steps--timeline:not(.is-in) .step h3,
html.js .steps--timeline:not(.is-in) .step p {
  opacity: 0;
  transform: translateY(12px);
  filter: blur(3px);
}

/* The badge lands first, with a spring overshoot... */
.steps--timeline.is-in .step::before {
  animation: stepBadgePop 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) var(--tl-delay) both;
}

@keyframes stepBadgePop {
  0%   { opacity: 0; transform: scale(0.5); }
  60%  { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}

/* ...the copy follows a beat behind, rising and un-blurring. */
.steps--timeline.is-in .step h3,
.steps--timeline.is-in .step p {
  animation: stepCopyIn 0.55s ease-out calc(var(--tl-delay) + 0.12s) both;
}

@keyframes stepCopyIn {
  from { opacity: 0; transform: translateY(12px); filter: blur(3px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* The connector draws itself out toward the next badge, just after its own
   number lands (dashed border, so it is clipped rather than scaled). */
@media (min-width: 861px) {
  .js .steps--timeline .step::after {
    transition: clip-path 640ms var(--ease-out) calc(var(--tl-delay) + 0.25s);
  }

  .js .steps--timeline.is-in .step::after {
    clip-path: inset(0 0 0 0);
  }
}

/* The ring around each number badge. It blooms once as the badge lands — the
   step arriving — then settles into a slow pulse that hops 1 -> 2 -> 3 along
   the row, so energy reads as flowing through the sequence rather than each
   badge twinkling on its own. */
.steps--timeline .step__pulse {
  position: absolute;
  top: -6px;
  left: -6px;
  width: calc(var(--step-badge) + 12px);
  height: calc(var(--step-badge) + 12px);
  border: 2px solid rgba(52, 196, 138, 0.45);
  border-radius: calc(var(--r-md) + 6px);
  opacity: 0;
  pointer-events: none;
}

.steps--timeline.is-in .step__pulse {
  animation:
    stepBloom 0.7s ease-out var(--tl-delay) both,
    stepFlow 7s ease-in-out calc(var(--tl-delay) + 1.1s) infinite;
}

@keyframes stepBloom {
  0%   { opacity: 0; transform: scale(0.7); }
  45%  { opacity: 0.7; transform: scale(1.5); }
  100% { opacity: 0; transform: scale(1.7); }
}

@keyframes stepFlow {
  0%, 100% { opacity: 0; transform: scale(0.9); }
  12% { opacity: 0.55; transform: scale(1.08); }
  34% { opacity: 0; transform: scale(1.28); }
}

/* Hover / focus: a step lifts, its badge brightens, its connector lights up. */
@media (hover: hover) {
  .steps--timeline .step {
    transition: transform 0.35s var(--ease-out);
  }

  .steps--timeline .step:hover {
    transform: translateY(-2px);
  }

  .steps--timeline .step:hover::before {
    border-color: var(--brand-300);
    color: var(--brand-800);
    box-shadow: var(--sh-sm);
  }

  .section--ink .steps--timeline .step:hover::before {
    background: rgba(52, 196, 138, 0.22);
    border-color: rgba(52, 196, 138, 0.6);
    color: var(--brand-300);
    box-shadow: 0 0 0 4px rgba(52, 196, 138, 0.12);
  }

  .section--ink .steps--timeline .step:hover::after {
    border-color: rgba(52, 196, 138, 0.5);
  }
}

/* ================= STAT STRIP =========================== */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-6);
  padding: var(--sp-8) 0;
  border-top: 1px solid var(--slate-200);
  border-bottom: 1px solid var(--slate-200);
}

.stat {
  text-align: center;
}

/* The supply band's copy column variant: four stats at auto-fit left one
   orphaned on a third row (three fit, one wrapped), so the column pins
   a deliberate 2×2 block instead. One per row on a phone — two columns
   of numbers at 390px are unreadable. */
.stat-strip--2x2 {
  grid-template-columns: 1fr;
}

@media (min-width: 560px) {
  .stat-strip--2x2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-8) var(--sp-6);
  }
}

.stat__num {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: var(--tracking-tighter);
  color: var(--ink-900);
  line-height: 1;
}

/* A stat whose heading is a word or a short phrase rather than a figure
   ("Own it", "Up front"). The display face at 2xl is sized for a number
   like "$14.99"; set a phrase at that weight and it reads as a shout and
   wraps badly in the 2x2. This steps it down to the xl size (a sibling of
   the 2xl numerals, not a caption) and eases the tracking, so the words
   sit level with the figures beside them. */
.stat__num--word {
  font-size: var(--fs-xl);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  line-height: 1.2;
}

.stat__label {
  font-size: var(--fs-sm);
  color: var(--slate-500);
  margin-top: var(--sp-2);
}

/* Stat strips sitting on a dark surface. `.split__media--ink` uses
   the dark gradient but is not `.section--ink`, so without this the
   numbers rendered near-black on near-black — effectively invisible. */
.section--ink .stat-strip,
.split__media--ink .stat-strip {
  border-color: rgba(255, 255, 255, 0.1);
}

.section--ink .stat__num,
.split__media--ink .stat__num {
  background: linear-gradient(120deg, var(--white) 30%, var(--brand-300) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.section--ink .stat__label,
.split__media--ink .stat__label {
  color: var(--slate-400);
}

/* ================= CTA BAND ============================= */
.cta-band {
  background: var(--grad-ink);
  border-radius: var(--r-2xl);
  padding: clamp(2.5rem, 5vw, 4.5rem);
  color: var(--white);
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.cta-band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--grad-hero-glow);
  z-index: -1;
}

/* The same drafted texture the page heroes wear, on the one dark band every
   page ends with — it ties the last thing on the page to the first. */
.cta-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
  background-size: 22px 22px;
  mask-image: radial-gradient(70% 110% at 92% 10%, #000 0%, transparent 70%);
  -webkit-mask-image: radial-gradient(70% 110% at 92% 10%, #000 0%, transparent 70%);
}

.cta-band h2 {
  color: var(--white);
  max-width: 20ch;
}

.cta-band p {
  color: var(--slate-300);
  margin-top: var(--sp-4);
  max-width: 52ch;
  font-size: var(--fs-lg);
}

.cta-band .btn-row {
  margin-top: var(--sp-8);
}

.cta-band__inner {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: var(--sp-12);
  align-items: center;
}

@media (max-width: 860px) {
  .cta-band__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-8);
  }
}

.cta-band__contact {
  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-lg);
  padding: var(--sp-6);
  backdrop-filter: blur(8px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}

.cta-band__contact .label {
  color: var(--brand-300);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}

.cta-band__phone {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  color: var(--white);
  margin-top: var(--sp-3);
}

.cta-band__phone:hover {
  color: var(--brand-300);
  text-decoration: none;
}

/* Registered so the perimeter glow's angle and alpha can interpolate
   in keyframes (plain custom properties cannot animate). Both default
   to a hidden arc, so a browser without @property support simply shows
   the finished static border — the graceful fallback. */
@property --notice-glow-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --notice-glow-a {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* One light travelling two slow circuits (9s each = 18s total, inside
   the 8-12s per-circuit budget) and settling out. The alpha ramps in
   and out so the arc never pops; the static border treatment below
   carries the card before, during and after. */
@keyframes notice-perimeter {
  0% {
    --notice-glow-angle: 0deg;
    --notice-glow-a: 0;
  }
  8% {
    --notice-glow-a: 1;
  }
  85% {
    --notice-glow-a: 1;
  }
  100% {
    --notice-glow-angle: 720deg;
    --notice-glow-a: 0;
  }
}

.cta-band__contact p {
  font-size: var(--fs-sm);
  margin-top: var(--sp-3);
  color: var(--slate-400);
}

/* ================= NOTICE / ALERT ======================= */
/* THE NOTICE, AS A CARD. The original was a flat tinted rectangle: one
   background colour, one 1px border, a bare 20px icon. It read as a
   placeholder rather than a designed object. Five cheap token layers
   fix that: a radial surface lift, a hairline ring, a faint blueprint
   grid, the icon on a circular tile, and a mid-tier shadow.

   No markup changed to get here: every live notice is
   `<div class="notice notice--indigo"><svg…/><div>…</div></div>`, so
   these rules ship to all eight at once.

   ::before is the ring and ::after is the surface — BOTH pseudo
   elements are taken. A future layer on this component needs a real
   element; there is no third pseudo to reach for. */
.notice {
  position: relative;
  isolation: isolate;
  display: flex;
  gap: var(--sp-4);
  padding: var(--sp-5);
  border-radius: var(--r-lg);
  background: var(--cloud-100);
  border: 1px solid var(--slate-200);
  box-shadow: var(--sh-md);
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
  color: var(--slate-600);
  /* AN ASIDE, NOT A BAND. The callout used to span its container
     edge to edge, as wide as the sections around it, so it read as
     one more full-width slab. At 85% of the container it steps out
     of the flow like a note in a margin — noticeably narrower, not
     dramatically so. The 1320px ceiling keeps a container--wide
     parent (1360px) from stretching it back out. Widths only: the
     ring, padding and type are untouched. */
  width: min(85%, 1320px);
  margin-inline: auto;
}

/* Wider on tablets (the container is already giving up margin there),
   full-bleed within the page gutters on phones, where 85% would just
   waste the gutter. Padding steps down with it so the text column
   keeps its measure. */
@media (max-width: 1000px) {
  .notice {
    width: 92%;
  }
}

@media (max-width: 639px) {
  .notice {
    width: 100%;
    padding: var(--sp-4);
    gap: var(--sp-3);
  }
}

/* The hairline ring, on ::before so it can carry a gradient (a border
   cannot) and so it never thickens the layout box. The radius is
   copied rather than inherited because a pseudo element does not pick
   it up on its own. */
.notice::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--slate-200);
  border-radius: var(--r-lg);
  pointer-events: none;
  z-index: -1;
}

/* The surface lift: a soft white radial from the top-left corner,
   faded out before the far corner. Brightest stop stays under 0.5
   alpha so body copy contrast does not move — the card sits on
   cloud-100 or indigo-100 and this only lightens it toward the
   origin. */
.notice::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
  background: radial-gradient(
    120% 130% at 0% 0%,
    rgba(255, 255, 255, 0.5) 0%,
    rgba(255, 255, 255, 0) 58%
  );
}

/* The icon is already the notice's first flex child, so the badge is
   a style and not a wrapper: the 20px SVG keeps its size and stroke
   and gains a white circular tile. --sp-2 padding plus the 20px icon
   is 36px, clear of the 24x24 target floor (the icon is decorative
   and aria-hidden, so it is not a target anyway). */
.notice svg {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  padding: var(--sp-2);
  box-sizing: content-box;
  color: var(--brand-600);
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: 50%;
  /* OPTICAL ALIGNMENT, NOT ACCIDENTAL. The tile top-aligns with the
     eyebrow label's line box; the 2px nudge drops it onto the label's
     cap height so the tile, label and heading share one start edge.
     align-self keeps it there even if the text group grows taller. */
  align-self: flex-start;
  margin-top: 2px;
}

.notice--amber {
  background: var(--amber-50);
  border-color: var(--amber-100);
}

.notice--amber::before {
  border-color: var(--amber-100);
}

.notice--amber svg {
  color: var(--amber-600);
}

.notice--brand {
  background: var(--brand-50);
  border-color: var(--brand-100);
}

.notice--brand::before {
  border-color: var(--brand-100);
}

/* "Good to Know" is informational, not a warning: the owner asked for every
   one of them site-wide to sit on a light indigo card, which also separates
   the notice from the emerald wash bands it often sits on. Icon and label
   take indigo-600 — 6.1:1 on the indigo-100 surface. The icon is a circled
   checkmark everywhere (owner's pick over a handshake, which turns to mush
   at 20px). */
.notice--indigo {
  background: var(--indigo-100);
  border-color: var(--indigo-200);
}

/* The accent edge + the perimeter light. A border cannot carry a
   gradient, so the visible frame for this variant is the ::before
   ring. In @supporting browsers it is a 3px band laid outside the
   border (see below); this fallback paints it over the real
   1px border: indigo-200 at the left, fading to near-transparent by
   the right. It reads as a soft glow on the side the reader enters
   from and as nothing on the far side, which is why it stays quiet
   enough for a page that stacks several of these. Indigo only — the
   amber and brand variants keep their plain ring above. */
.notice--indigo::before {
  border-color: transparent;
  background:
    linear-gradient(
      var(--indigo-100),
      var(--indigo-100)
    ) padding-box,
    linear-gradient(
      90deg,
      var(--indigo-200) 0%,
      rgba(79, 70, 229, 0.28) 42%,
      rgba(79, 70, 229, 0) 100%
    ) border-box;
  border: 1px solid transparent;
}

/* THE PERIMETER LIGHT (2026-10-17). The owner asked for one slow
   light to travel the rounded perimeter on first view and then
   settle. Both pseudo elements were already taken, so the light
   shares ::before with the static edge as two stacked gradient
   layers inside one masked ring.

   MEASURED, NOT ASSUMED: a `filter: drop-shadow()` glow on this
   ring does not work -- the mask clips the shadow at the box edge
   (screenshot pixel probes of two candidate builds), so the glow IS
   the band: a 3px ring laid just OUTSIDE the card's border box.
   inset -4px positions the pseudo's box 4px beyond the padding box,
   i.e. 3px clear of the 1px border; padding 3px puts its content
   box exactly on the card's border box, so the ring the mask keeps
   (border-box minus content-box) is the 3px of air around the card.
   The real 1px indigo-200 border is never covered and never
   animates.

   The mask is the border-box/content-box exclusion trick: it keeps
   only the 3px band and drops everything inside it, so no fill
   layer is needed to hide gradient spill across the card face. The
   outer radius is copied as r-lg + 3px so the band hugs the card's
   corners; the inner edge lands back exactly on r-lg.

   Layer order: the travelling conic arc on top (alpha gated by the
   registered --notice-glow-a, so it is invisible before and after
   its run), the static left-edge gradient below it. Without
   @property support the custom props fall back to 0 and the card
   simply shows the finished static border. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .notice--indigo::before {
    inset: -4px;
    padding: 3px;
    border: 0;
    border-radius: calc(var(--r-lg) + 3px);
    background:
      conic-gradient(
        from var(--notice-glow-angle, 0deg) at 50% 50%,
        transparent 0deg,
        rgba(129, 140, 248, calc(0.2 * var(--notice-glow-a, 0))) 17deg,
        rgba(139, 92, 246, calc(0.66 * var(--notice-glow-a, 0))) 27deg,
        rgba(96, 165, 250, calc(0.44 * var(--notice-glow-a, 0))) 35deg,
        transparent 54deg,
        transparent 360deg
      ) border-box,
      linear-gradient(
        90deg,
        var(--indigo-200) 0%,
        rgba(79, 70, 229, 0.28) 42%,
        rgba(79, 70, 229, 0) 100%
      ) border-box;
    -webkit-mask: linear-gradient(#fff 0 0) content-box,
      linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    mask-composite: exclude;
  }

  /* TWO SLOW CIRCUITS (720deg at 9s each = 18s), then stop. `both`
     holds the first frame during any stagger delay -- so the light
     is invisible until its turn -- and holds the last frame after,
     which is alpha 0: the card settles back into the static border
     and stays there. Not a loop. */
  .notice--indigo.is-glowing::before {
    animation: notice-perimeter 18s linear var(--notice-glow-delay, 0ms) 1
      both;
  }

  @media (prefers-reduced-motion: reduce) {
    .notice--indigo.is-glowing::before {
      animation: none;
    }
  }
}

/* THE TEXTURE. A faint blueprint grid — two crossing 1px hairlines at
   a 48px pitch — mirroring the wash band's own grid (base.css
   .section--wash) but tinted indigo so it stays in this card's
   family. It replaced a dot grid here on 2026-10-16: the owner
   compared both rendered side by side and picked the grid because it
   matches the light green comparison bands the notice often sits
   next to.

   Held to 3.5% alpha, far under the point where it would move body
   text contrast, and it shares the mask below so it fades out of the
   text column: the texture lives in the empty right side and behind
   the badge, never under a line of copy. It paints UNDER the card's
   own surface lift (z-index -1), so it can only lighten the surface,
   never darken the text on top of it. The white radial at the end is
   that surface lift, restated because both layers share this one
   pseudo element. */
.notice--indigo::after {
  background:
    repeating-linear-gradient(
      90deg,
      transparent 0 47px,
      rgba(79, 70, 229, 0.035) 47px 48px
    ),
    repeating-linear-gradient(
      180deg,
      transparent 0 47px,
      rgba(79, 70, 229, 0.035) 47px 48px
    ),
    radial-gradient(
      120% 130% at 0% 0%,
      rgba(255, 255, 255, 0.5) 0%,
      rgba(255, 255, 255, 0) 58%
    );
  mask-image: linear-gradient(100deg, transparent 0 34%, #000 100%);
  -webkit-mask-image: linear-gradient(100deg, transparent 0 34%, #000 100%);
}

.notice--indigo svg,
.notice--indigo .notice__label {
  color: var(--indigo-600);
}

/* THE TITLE IS THE ELEMENT WITH A CLASS, NOT THE ELEMENT CALLED <strong>.
   This used to read `.notice strong { display: block; }`, and it was a real
   bug: a notice's title is a <strong>, but so is any emphasis inside the
   sentence under it. So the security page rendered

     It is
     not a backup
     . If a site is taken down...

   — the paragraph torn into three lines around a word that was only ever
   meant to be bold. The rule was written for the title and silently applied
   to every inline <strong> on the site, because they are the same element
   and nothing distinguished them.

   The fix is to stop inferring the title from the tag and name it: the
   title carries `.notice__title`, and inline <strong> keeps only the colour.
   `noticeBand` emits the class; the hand-written notices were given it in
   the same pass. There is no rule left that could hit an inline one. */
.notice__title {
  display: block;
  font-size: var(--fs-md);
  font-weight: 650;
  line-height: var(--lh-tight, 1.25);
  color: var(--ink-900);
  margin-bottom: var(--sp-2);
}

.notice strong {
  color: var(--ink-900);
}

/* THE SAME TWO WORDS OPEN EVERY NOTICE ON THE SITE — "Good to Know:".
   The label is what a visitor learns to recognise, and it is identical
   everywhere so the block is one thing rather than eight; the headline
   under it stays specific to the page, because "the one case where the
   builder is the wrong choice" is the part that is actually worth
   reading.

   So there are two pieces of text here with two different owners: the
   label is emitted by `noticeBand` (and by the hand-written notices,
   from the same constant), and the title is passed in by the page. It
   is set in the eyebrow style — uppercase, tracked — so it reads as a
   label rather than as the start of the sentence. */
.notice__label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--brand-600);
  margin-bottom: var(--sp-2);
}

/* The label takes the notice's own tone, or an amber notice opens with a
   green word on a yellow card and the two halves look unrelated. */
.notice--amber .notice__label {
  color: var(--amber-600);
}

/* THE SECTION AROUND THE CALLOUT (2026-10-17). The owner's note: the
   big white or pale-gray area behind the callout felt empty, and the
   callout itself read as wide as a content band. The card width fix
   is above; this is the room it sits in.

   Layers, all soft and edge-to-edge so nothing reads as a shape:
   1. a gentle vertical wash (cloud-50 to white and back) that
      separates the band from its neighbours without a hard line,
   2. one diffused lavender glow behind the callout, centred and
      feathered to nothing — an atmosphere, not a spotlight,
   3. one extremely subtle mint wash toward the lower edge, tying
      the band to the brand without introducing another accent.

   Alphas are held to 0.10 / 0.06 so nothing here can move text
   contrast: body copy sits on the card, headings sit on the wash's
   lightest stops. The ambient drifting ::before on .section is
   switched off under a callout band so the two glows never stack.

   Scoped by :has() to the section that actually contains a notice —
   all nine live instances are the only match site-wide. */
section:has(.notice) {
  background-color: var(--cloud-50);
  background-image:
    radial-gradient(
      70% 58% at 50% 44%,
      rgba(129, 140, 248, 0.1) 0%,
      rgba(129, 140, 248, 0.045) 42%,
      rgba(129, 140, 248, 0) 74%
    ),
    radial-gradient(
      62% 48% at 50% 100%,
      rgba(10, 129, 88, 0.06) 0%,
      rgba(10, 129, 88, 0) 68%
    ),
    linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.9) 0%,
      rgba(255, 255, 255, 0) 16%,
      rgba(255, 255, 255, 0) 84%,
      rgba(255, 255, 255, 0.9) 100%
    );
  background-repeat: no-repeat;
}

section:has(.notice)::before {
  content: none;
  animation: none;
}

/* ================= BREADCRUMB =========================== */
.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--slate-500);
  margin-bottom: var(--sp-6);
}

.breadcrumb a {
  color: var(--slate-500);
  /* WCAG 2.5.8 floor — a breadcrumb link is a real target, not inline prose. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.breadcrumb a:hover {
  color: var(--brand-600);
  text-decoration: none;
}

.breadcrumb__sep {
  color: var(--slate-300);
}

.breadcrumb [aria-current="page"] {
  color: var(--ink-800);
  font-weight: 600;
}

/* ================= PAGE HERO ============================ */
.page-hero {
  background: var(--cloud-100);
  border-bottom: 1px solid var(--slate-200);
  padding-block: clamp(2.5rem, 5vw, 4rem);
  /* Positioned so the optional backdrop layers can inset to the band rather
     than to the page, and isolated so their negative z-index stays behind
     this section's own content instead of dropping behind the page. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.page-hero--ink {
  background: var(--grad-ink);
  border-bottom-color: transparent;
}

.page-hero--ink h1,
.page-hero--ink .lead {
  color: var(--white);
}

.page-hero--ink .lead {
  color: var(--slate-300);
}

.page-hero--ink .breadcrumb,
.page-hero--ink .breadcrumb a {
  color: var(--slate-400);
}

.page-hero--ink .breadcrumb [aria-current="page"] {
  color: var(--white);
}

.page-hero__inner {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--sp-12);
  align-items: center;
}

@media (max-width: 900px) {
  .page-hero__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-8);
  }
}

.page-hero h1 {
  max-width: 22ch;
}

.page-hero .lead {
  margin-top: var(--sp-4);
}

/* ---------- The hero fact strip ----------
   One line of verifiable facts under the hero buttons: the entry price, one
   inclusion, who answers the phone. It exists so a visitor gets the numbers
   that matter without scrolling, and so every claim in it can be checked —
   which is why the strings come from the page and never from a template
   (see the claims register, §4 of the project brief).

   A dot separator rather than a tick: the tick already means "included" on
   this site (.feature-list, .hero__meta), and a hero opening with a row of
   ticks would compete with the checklists further down the same page. */
.page-hero__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  margin: var(--sp-6) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--slate-600);
}

.page-hero__facts li {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}

.page-hero__facts li::before {
  content: "";
  flex: none;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--slate-400);
}

/* The first item opens the line, so it carries no separator. */
.page-hero__facts li:first-child::before {
  display: none;
}

.page-hero--center .page-hero__facts {
  justify-content: center;
}

.page-hero--ink .page-hero__facts {
  color: var(--slate-300);
}

.page-hero--ink .page-hero__facts li::before {
  background: var(--slate-500);
}

/* ---------- The hero price line ---------
   The SiteGround-style *line*, not a card: "Plans from $12.99/mo · renews at
   $24.99" sits under the buttons and above the fact strip. It answers "what
   does it cost" without letting a price card own the hero; the full per-tier
   grid lives just below. Muted and small so the promise and CTA stay the
   loudest things in the hero, with a tabular figure so the number reads
   steady against the plan grid. */
.page-hero__price {
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--slate-600);
  font-variant-numeric: tabular-nums;
}

.page-hero__price strong {
  color: var(--ink-900);
  font-weight: 700;
}

.page-hero--center .page-hero__price {
  text-align: center;
}

.page-hero--ink .page-hero__price {
  color: var(--slate-300);
}

.page-hero--ink .page-hero__price strong {
  color: var(--white);
}

/* A phone wraps the strip anyway, and a separator stranded at the start of a
   wrapped line reads as a stray bullet. Stack it instead: one fact per line,
   no punctuation. */
@media (max-width: 640px) {
  .page-hero__facts {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-3);
    margin-top: var(--sp-5);
  }

  .page-hero--center .page-hero__facts {
    align-items: center;
  }

  .page-hero__facts li::before {
    display: none;
  }
}

/* ---------- The icon trust row ----------
   Three icon + claim pairs, the treatment the domain registration page
   established. NOT a restyle of `.page-hero__facts` above: that list is
   shared by eight pages (VPS, SEO, email-marketing, SSL, SSL Managed,
   website backup, website security and this one), so changing it would
   re-skin seven heroes nobody asked about. This is a separate component
   so a hero can opt into the richer row.

   The rules are a verbatim copy of `.domains-hero__trust`
   (domains-hero.css) rather than an approximation, so the two rows are
   identical by construction. The 7.5em cap on the label is deliberate:
   each claim wraps to two lines, which is what makes the row read as
   claims and not as a laundry list. */
.hero-trust {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4) clamp(1.25rem, 2.6vw, 2.5rem);
  margin-top: var(--sp-6);
  padding: 0;
  list-style: none;
}

.hero-trust li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  font-size: var(--fs-base);
  line-height: 1.35;
  color: var(--slate-700);
}

.hero-trust li span {
  max-width: 7.5em;
}

.hero-trust svg {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  color: var(--brand-600);
}

/* The domain page's own mobile tuning, carried across so the two rows
   stay matched on a phone as well as on a desktop. */
@media (max-width: 640px) {
  .hero-trust {
    gap: var(--sp-3) var(--sp-5);
  }

  .hero-trust li {
    font-size: var(--fs-sm);
  }
}

/* ---------- The feature checklist ----------
   MOVED HERE 2026-10-08, from product.css, for the same reason as the
   strip below: the Website Builder already borrowed it from a sheet it
   loads for other reasons, and the legacy-hero product pages need it
   without product.css's layout.

   Ticks already mean "included" everywhere on this site, so these wear
   the accent in filled circles — the mock's treatment — and stay a quiet
   one-row list rather than a second feature grid. They wrap to two rows
   naturally on narrow screens; no media query needed.

   THE CAP IS THREE ITEMS. Every hero on the site answers "why should I
   care" in the strip below, and a checklist that restates a strip claim
   is noise — six of them read as clutter, which is what the Website
   Builder hero did. Three is the number this list holds without
   competing with the strip it sits above. */
.product-hero__checks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
  margin: var(--sp-6) 0 0;
  padding: 0;
  list-style: none;
}

.product-hero__checks li {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-base);
  font-weight: 550;
  color: var(--slate-700);
}

.product-hero__check {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--r-full);
  background: var(--product-accent);
  color: var(--white);
}

.product-hero__check svg {
  width: 12px;
  height: 12px;
}

/* The Website Builder hero is centred (page-hero--center) and borrows
   this checklist, so the row centres too; the two-column product
   heroes keep it left-aligned under the copy. */
.page-hero--center .product-hero__checks {
  justify-content: center;
}

/* ---------- The trust strip ----------
   MOVED HERE 2026-10-08, from product.css. The four things every buyer
   checks, closing the band under a hairline so the hero reads as one
   designed header and not as copy floating above whatever section comes
   next. Dividers, not cards: a row of white cards here would echo exactly
   the disconnected white plates this hero exists to replace.

   WHY IT MOVED. It started life in product.css as a product-hero part,
   which quietly limited it to the six pages that load that sheet. Every
   other product page answered the same question with a dot-separated
   `.page-hero__facts` list instead, so "what is this and why should I
   care" had three different answers three different ways. components.css
   is loaded by every page on the site, so the strip is now available to
   the split heroes (§ split-hero.css) and the older `.page-hero` shells
   without dragging product.css's layout onto those pages.

   The class name is kept as `product-hero__strip`: it is the same object
   the shipped pages already carry, and renaming it would mean touching
   five verified heroes to buy nothing.

   The icon rule below asks for `--hero-strip-icon` — bright orange on every page
   — and it now WINS, because it is written as two classes. It did not, once:
   as a single class (0-1-0) it lost to `.product-hero__strip span` (0-1-1)
   further down, which matches the icon's own `<span class="product-hero__strip-icon">`,
   so every glyph on the site painted `--slate-500` and the bar read as grey.
   Specificity is counted, not eyeballed. See PROJECT-BRIEF §4.6. */
.product-hero__strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* A hairline foot, and an explicit stacking context. The split heroes
     paint a wallpaper at z-index -1 and a floating envelope layer at 0,
     so without this the strip would sit UNDER the envelopes on those
     pages. On a product hero it changes nothing. */
  position: relative;
  z-index: 1;
  margin: clamp(2.5rem, 5vw, 3.5rem) 0 0;
  padding: var(--sp-8) 0 0;
  /* The rule is TINTED FROM THE PRODUCT, not slate. At 34% it stays a
     hairline — it never reads as a second border — but it picks up the
     page's own hue, which is most of what stops this bar looking grey and
     plain beside the coloured bands above it. `--product-accent-rgb`
     always resolves (see tokens.css), so a page with no product identity
     gets a green rule rather than no rule. */
  border-top: 1px solid rgba(var(--product-accent-rgb), 0.34);
  list-style: none;
}

.product-hero__strip li {
  display: flex;
  gap: var(--sp-4);
  padding-inline: var(--sp-6);
  min-width: 0;
}

.product-hero__strip li:first-child {
  padding-left: 0;
}

.product-hero__strip li + li {
  border-left: 1px solid rgba(var(--product-accent-rgb), 0.22);
}

/* THE ICON. Two things are deliberate here and both were wrong before.

   1. THE SELECTOR IS TWO CLASSES, not one. As `.product-hero__strip-icon`
      (0-1-0) this rule lost to `.product-hero__strip span` (0-1-1) further
      down — which matches this very element, because the icon IS a
      `<span>` — so `color` here was dead letter and every glyph on the
      site painted `--slate-500` from the text rule instead. Specificity is
      counted, not eyeballed: one class + one type beats one class.
   2. THE COLOUR IS THE COMPONENT'S, not the product's (option A). Orange
      marks the strip as a feature list, the tinted rule above carries the
      page's identity, and the two together are what stop the bar reading
      as flat. `--hero-strip-icon` is overridable per product (tokens.css);
      the literal is only the last-resort fallback. */
.product-hero__strip .product-hero__strip-icon {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  color: var(--hero-strip-icon, #E0450A);
}

.product-hero__strip-icon svg {
  width: 30px;
  height: 30px;
}

.product-hero__strip strong {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  color: var(--ink-900);
}

.product-hero__strip span {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--slate-500);
}

/* The DOMAIN hero's band is pale mint-to-blue, and `--slate-500` measures
   4.05:1 at the blue end — under the 4.5:1 floor for this 13px text, where
   on the white bands it is a comfortable 4.9:1. One step darker fixes it
   (6.25:1) and is within a hair of the original tone, so the row still
   reads as one component on both surfaces. */
.page-hero--sky .product-hero__strip span {
  color: var(--slate-600);
}

/* Two-up, then one-up. The vertical dividers come off with the padding
   because a column divider stranded beside a wrapped row reads as damage.
   These two blocks MUST stay below the base rule above: same single-class
   specificity, so source order is what decides. */
@media (max-width: 980px) {
  .product-hero__strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-6) var(--sp-8);
  }

  .product-hero__strip li {
    padding-inline: 0;
  }

  .product-hero__strip li + li {
    border-left: 0;
  }
}

@media (max-width: 560px) {
  .product-hero__strip {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
  }
}

/* ---------- The headline accent — one shared component ----------
   The two words a headline pays off in colour ("Stop *sending invoices*
   from a personal address"). Every product hero has one and they all come
   from here, so the treatment is one object rather than a rule per sheet.

   WHERE THIS CAME FROM. It used to be `.page-hero--split .accent` over in
   split-hero.css, which quietly limited the idea to the three split pages
   while every other hero had a headline in one flat tone. Un-scoping it
   needed no markup change at all: the span was always just `class="accent"`.

   TWO MEASURED FAULTS FIXED ON THE WAY ACROSS, both in the old stops:
     - `.page-hero--split .accent` started on `--brand-500` (#12A66F) = 2.5:1
       on the email mint band.
     - `.page-hero--teal .accent` started on #0EA5A5 = 2.39:1 on the teal.
   Both are under the 3:1 floor for display type, and both gradients spent
   their first third illegal. The pairs are now supplied per product by
   `--product-accent-from` / `--product-accent-to` (tokens.css), and every
   stop of every pair clears 3:1 on the band it lands on.

   The gradient is a FILL clipped to the glyphs, not a glow, and the solid
   `color` is the fallback for a browser that cannot clip a background to
   text — only a browser that can is allowed to make the fill transparent
   (the same shape as `.page-hero__title-text` further up).

   `background-image`, NEVER the `background` shorthand. The shorthand
   resets every `background-*` property it does not set back to its initial
   value, including `background-clip: text` — that exact mistake painted
   this gradient as a filled rectangle behind invisible glyphs once already,
   and it is why the sibling rule documents it too. */
.accent {
  color: var(--product-accent-from);
}

@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .accent {
    background-image: linear-gradient(
      102deg,
      var(--product-accent-from) 8%,
      var(--product-accent-to) 92%
    );
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
}

/* Forced colours discards author backgrounds, which would leave the clip
   with nothing to paint and the word invisible. */
@media (forced-colors: active) {
  .accent {
    background-image: none;
    color: CanvasText;
  }
}

/* ---------- Product page identity ----------
   Every product page's <h1> is a marketing line — "Stop the attack before it
   reaches your site." — not a product name. So the only thing telling a
   visitor which page they are on was a small uppercase eyebrow, which is
   exactly the complaint this answers.

   The icon plus the promoted label name the page; the h1 stays the punchy
   line. There is no modifier class to remember: the larger title is keyed
   off the label's presence with an adjacent-sibling rule, so a hero cannot
   end up with the big title and no label, or the label and a small title. */
.page-hero__label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--brand-700);
}

.page-hero__icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 50px;
  height: 50px;
  border-radius: var(--r-md);
  background: var(--brand-50);
  border: 1px solid var(--brand-100);
  color: var(--brand-600);
}

.page-hero__icon svg {
  width: 26px;
  height: 26px;
}

/* ---------- Official vendor marks in the badge ----------
   The WordPress and Titan badges carry the vendor's own logo geometry
   instead of a house glyph, because the claim those pages make is
   "this is the software you are buying" and a generic panel icon cannot say
   it. The paths are traced copies of each vendor's mark (WordPress from
   the Simple Icons set, Titan from the vendor's own app asset), filled
   with `currentColor` so a real logo still wears the house accent
   rather than importing a second palette into the badge.

   cPanel is the exception: its hero badge ships the vendor's own cP
   monogram as a transparent WebP on an accent-filled tile instead of a
   traced glyph — the override lives in product.css, next to the label
   retint it builds on. The tile stays either way: a mark in a box is
   still a mark, and dropping the tile on one hero of forty would make
   it look like a different template. */
.page-hero__icon svg.brand-mark {
  width: 30px;
  height: 30px;
}

/* The label earns a title one step above the base h1 (--fs-4xl). It now
   shares --fs-5xl with the centred product title, which is intentional:
   both are product-name lockups, so both speak at the same volume. */
.page-hero__label + h1 {
  font-size: var(--fs-5xl);
}

.page-hero--ink .page-hero__label {
  color: var(--brand-300);
}

.page-hero--ink .page-hero__icon {
  background: rgba(52, 196, 138, 0.14);
  border-color: rgba(52, 196, 138, 0.3);
  color: var(--brand-400);
}

/* A light hero carrying a label gets the house wash: a soft brand glow over a
   white-to-pale-emerald gradient, so the band reads as a deliberately designed
   header rather than a plain grey panel. The ink variant needs no equivalent —
   it already has its own gradient.

   The gradient runs white on the left, where the product name, the <h1> and
   the lead all sit, and only tints at the far right where nothing has to stay
   readable. That is not decoration: it keeps every piece of text on the band's
   lightest surface, so `_build/_contrast.mjs` still measures the worst case
   against cloud-100 and its numbers stay conservative rather than flattering.

   The radial is listed FIRST on purpose. `_build/_contrast.mjs` reads the
   first rgba() out of this block to composite its "washed" worst case, so
   reordering these two layers would silently change what that guard measures
   while still reporting a pass. */
.page-hero--lede:not(.page-hero--ink)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
      92% 130% at 8% 0%,
      rgba(52, 196, 138, 0.11),
      transparent 62%
    ),
    linear-gradient(
      100deg,
      var(--white) 0%,
      var(--brand-50) 58%,
      var(--brand-100) 100%
    );
}

/* The dot-grid texture, painted on ::after so it can sit over every hero
   variant without touching the contracted ::before wash above. It is masked
   hard to the top-right — the corner no copy ever occupies — so the band gets
   a drafted, technical feel while the reading area stays clean paper. */
.page-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--dot-grid);
  background-size: 22px 22px;
  mask-image: radial-gradient(65% 100% at 88% 0%, #000 0%, transparent 72%);
  -webkit-mask-image: radial-gradient(65% 100% at 88% 0%, #000 0%, transparent 72%);
}

.page-hero--ink::after {
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
}

/* ---------- Hero textures — the reusable set ----------
   FIVE backgrounds, chosen per page via a `.page-hero--tex-*` class, so two
   product pages visited one after the other do not open on the same surface.
   The owner asked for this after the SiteGround review: vary the band from
   product to product, keep the type system fixed. The default, with no
   modifier, is still the dot grid above — nothing that ships today moves.

   They are CSS, so they cost zero image bytes. Four of them are static and
   paint on the hero's `::after` layer, which leaves the contracted `::before`
   wash that `_build/_contrast.mjs` measures exactly as it was. The fifth, the
   dither, is the one that moves: it owns a real element of its own further
   down, because two independently animated layers cannot both live on one
   pseudo-element. Two rules keep the static four honest:

   1. EVERY ONE IS ANCHORED TO THE TOP-RIGHT CORNER, the one region no copy
      occupies. A label hero puts its copy in the left column and a centred
      hero puts it in the middle (the title caps at 420px), so the corner
      above and right of both is free. The masks below are what enforce that,
      not the gradient stops alone.
   2. NOTHING HERE IS HEAVIER THAN WHAT IS ALREADY BEHIND A TITLE. The dot
      grid is rgba(10, 18, 32, 0.055) and the wash's radial is emerald at
      0.11; every value below stays at or under those two numbers. That is
      what lets `_contrast.mjs` keep measuring the band at the wash's full
      strength and remain conservative rather than flattering — a texture
      brighter than the wash would make the guard's worst case a lie.

   The dither keeps rule 1 and bends rule 2, and the reason it is allowed to
   is written up beside it: it is kept off every glyph by measurement, so its
   heavier dot weight never lands on type.

   They are light-band only: `:not(.page-hero--ink)` keeps the ink hero's own
   white dots intact if the ink treatment is ever switched back on. */
.page-hero--tex-grid:not(.page-hero--ink)::after {
  background-image: linear-gradient(
      to right,
      rgba(10, 18, 32, 0.045) 1px,
      transparent 1px
    ),
    linear-gradient(to bottom, rgba(10, 18, 32, 0.045) 1px, transparent 1px);
  background-size: 26px 26px;
  mask-image: radial-gradient(56% 76% at 92% 0%, #000 0%, transparent 74%);
  -webkit-mask-image: radial-gradient(56% 76% at 92% 0%, #000 0%, transparent 74%);
}

.page-hero--tex-glow:not(.page-hero--ink)::after {
  background-image: radial-gradient(
      62% 84% at 94% 2%,
      rgba(52, 196, 138, 0.11),
      transparent 70%
    ),
    radial-gradient(44% 60% at 76% 30%, rgba(14, 165, 165, 0.07), transparent 72%);
  background-size: auto, auto;
  /* The stops already fade to nothing, so no mask is needed — and the
     absence of one is why this is the texture used where a band wants to
     feel warm rather than drafted. */
}

.page-hero--tex-rings:not(.page-hero--ink)::after {
  background-image: repeating-radial-gradient(
    circle at 96% 12%,
    rgba(10, 18, 32, 0.05) 0 1px,
    transparent 1px 17px
  );
  background-size: auto;
  mask-image: radial-gradient(54% 72% at 92% 0%, #000 0%, transparent 76%);
  -webkit-mask-image: radial-gradient(54% 72% at 92% 0%, #000 0%, transparent 76%);
}
/* ---------- The hero dither - the moving texture ----------
   The fourth background in the set above, and the only one that moves: a dot
   matrix under a soft emerald ramp, both anchored to a top corner and both
   masked off the copy, which is the same contract the three textures above
   make. It just carries two animated layers as well.

   IT IS ITS OWN ELEMENT, NOT ANOTHER `::after`. The three above overwrite
   `background-image` on the hero's `::after`, because one flat background is
   all they need. This one has two layers that move independently, and a
   pseudo-element cannot hold two animations that must not collide. So
   `pageHero` emits one real element, and the layers hang off its own
   pseudo-elements:

     .page-hero__dither          the clip box and the anchoring corner
     .page-hero__dither::before  the dot matrix  - glides,   `transform`
     .page-hero__dither::after   the emerald ramp - breathes, `opacity`

   ONE MASK PER LAYER rather than one on the wrapper, and that is the whole
   reason the ramp can fill most of the band while the dots stay in the corner.
   The dots are the layer that could land on a glyph, so theirs is the tight
   mask; the ramp is lighter than the wash already behind the copy, so its mask
   is generous. A single shared mask would force one reach on both and throw
   away the safe half of that.

   MOTION IS COMPOSITED, NOT REPAINTED. Both layers move with `transform` and
   `opacity` only, so neither repaints the band nor touches layout. Nothing
   here is a canvas, a filter or a video.

   Hidden by default. The wide-screen gate below is what turns it on, and that
   gate is a real decision rather than a performance tweak - see there. */
/* THE REACH, AND WHY IT IS A FULL-HEIGHT STRIP RATHER THAN A CORNER BLOB.

   The first attempt anchored a small ellipse in the top corner, and the review
   called it out: it read as a patch in one corner rather than as a surface.
   The reason it was so small is arithmetic, and it is worth writing down
   because it explains every number below.

   A centred hero puts its copy in the MIDDLE and the copy is WIDE - around
   700px of title, tagline and lead, however wide the band gets. So the free
   space is not a corner, it is a MARGIN: the strip left of the copy and the
   strip right of it, and those strips run the FULL HEIGHT of the band. At
   901px that margin is about 10% of the band; at 1400px it is about 24%.

   So the texture is the margin strip, full height, and the strip widens in
   three measured steps as the band does. Each step is set to the strip width
   at the NARROWEST width it covers, so the reach is always inside the margin:

     901-1099px   margin ~10%   -> starts fading at 90%
     1100-1299px  margin ~17%   -> 84%
     1300px and up margin ~24%  -> 78%

   MEASURED, NOT CHOSEN. Each of those three was checked on a render with the
   texture switched off: every pixel the texture changed was light in the
   reference, i.e. blank paper. The darkest surface any of them touched was
   208/255, against the 150 a glyph is. Coverage went from 7% of the band (the
   corner blob) to 9-21% (the strip), and none of that growth came from
   approaching the type.

   The mask is on THIS element, not on the layers, so its percentages are
   band-relative. The two layers can be shaped further on top of it - the ramp
   is, below - and because a mask on a parent and a mask on a child compose by
   intersection, the strip is a hard outer bound that neither layer can escape. */
.page-hero__dither {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  mask-image: linear-gradient(to right, transparent 0 90%, #000 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0 90%, #000 100%);
  display: none;
}

/* The strip grows in two more measured steps. `min-width` rather than
   `max-width` so the narrowest value is the one that has no condition to meet
   - a page that somehow missed every rule still gets the safest strip. */
@media (min-width: 1100px) {
  .page-hero__dither {
    mask-image: linear-gradient(to right, transparent 0 84%, #000 96%);
    -webkit-mask-image: linear-gradient(to right, transparent 0 84%, #000 96%);
  }
}

@media (min-width: 1300px) {
  .page-hero__dither {
    mask-image: linear-gradient(to right, transparent 0 78%, #000 90%);
    -webkit-mask-image: linear-gradient(to right, transparent 0 78%, #000 90%);
  }
}

/* THE DOTS.
   0.15 of ink at a 5px pitch lifts a white band to about 217 - a grain you can
   see at arm's length, against the 0.055 at 22px the default dot grid draws.
   It is DARKER than that grid, deliberately, and the mask is what makes that
   safe: the grain is kept in the corner, over plain paper, never over a glyph.

   THE GLIDE IS THE MOTION THAT WAS MISSING. The prototype crawled the matrix
   one pitch over 26s, which is 0.19px a second - invisible, and reported as
   "not animated at all". This travels EIGHT pitches (40px) over 30s, so it
   moves about 1.3px a second and can actually be seen, while staying seamless:
   a whole number of pitches in each axis means the loop restarts mid-matrix,
   so the seam has nowhere to show. The `-48px` overhang is what that 40px of
   travel needs in order never to expose an edge. */
.page-hero__dither::before {
  content: "";
  position: absolute;
  inset: -48px;
  background-image: radial-gradient(circle, rgba(10, 18, 32, 0.15) 1.35px, transparent 1.6px);
  background-size: 5px 5px;
  /* No mask here any more. The strip on the wrapper is the outer bound, and
     the dots fill it edge to edge and full height - which is what makes the
     grain read as a surface rather than as a patch in a corner. */
  animation: heroDitherGlide 30s linear infinite;
}

/* THE RAMP, and the layer whose motion is meant to be caught.
   Two soft radials - the house emerald and a teal - at 0.15 and 0.1 against
   the wash's 0.11. They are still LIGHTER than the band's own gradient stops,
   which is what keeps `_contrast.mjs`'s worst case honest: the darkest surface
   these add is about 234/255 against the 230 of the `--brand-100` whose stop
   already ends the wash, so nothing here costs a reader contrast they did not
   already have.

   The pulse is 0.5 to 1 over 9s. The prototype breathed 0.55 to 1 over 44s,
   and the eye cannot compare two colours it sees 22 seconds apart - so
   "breathe" and "still" looked identical, which is exactly how it was
   reported. Nine seconds is a pulse you can catch, and still slow enough to
   read as the room breathing rather than as a flashing badge.

   `opacity: 0.72` is the RESTING value and is not decoration. `base.css`
   collapses every animation to 0.01ms under `prefers-reduced-motion`, and an
   animation that has ended without `fill-mode: forwards` reverts to the
   element's own value - which would be 1, the strongest the ramp ever gets.
   The resting value is what a reduced-motion visitor gets instead. */
.page-hero__dither::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(
      62% 80% at 96% 2%,
      rgba(52, 196, 138, 0.15),
      transparent 68%
    ),
    radial-gradient(48% 62% at 72% 26%, rgba(14, 165, 165, 0.1), transparent 72%);
  /* A VERTICAL fade on top of the strip the wrapper already applies. Combined
     with that strip it makes a top-anchored glow rather than a flat tinted
     column, because the two masks intersect. The ramp is deliberately given
     the top of the colour and the bottom of the light, which is what reads as
     light falling across the band rather than as a filled shape. */
  mask-image: linear-gradient(to bottom, #000 0%, transparent 92%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 92%);
  opacity: 0.72;
  animation: heroDitherBreath 9s ease-in-out infinite;
}

@keyframes heroDitherGlide {
  to { transform: translate3d(40px, 40px, 0); }
}

@keyframes heroDitherBreath {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

/* THE WIDE-SCREEN GATE. Below 901px the texture is off and the band keeps the
   dot grid every hero has always had, and the reason is not performance.
   A narrow band has no free corner: at 390px a centred hero's copy runs within
   about 60px of the right edge, so ANY corner-anchored texture lands on the
   words. The choice is between a tinted headline and no texture, and no
   texture is obviously correct. */
@media (min-width: 901px) {
  /* The dither REPLACES the default dot grid rather than joining it - a 4px
     matrix over a 22px grid reads as moire, not as paper. The three textures
     above do this by overwriting `background-image` on `::after`; this one
     owns its own element, so it clears the grid explicitly instead. */
  .page-hero--tex-dither:not(.page-hero--ink)::after,
  .page-hero--tex-dither-alt:not(.page-hero--ink)::after {
    background-image: none;
  }

  .page-hero__dither {
    display: block;
  }
}

/* THE VARIANT. The same texture mirrored to the other margin - the left strip
   instead of the right - moved onto the teal end of the palette and gliding
   the other way, so two product pages visited one after the other do not open
   on the same surface.

   IT MIRRORS TO THE OTHER SIDE OF THE SAME BAND, not to a different corner,
   and that is measured rather than chosen. The free space is the margin on
   either side of the centred copy, and both margins are the same width, so
   the mirrored strip is the same strip. What changes is which side it is on
   and which way it drifts. */
.page-hero--tex-dither-alt .page-hero__dither {
  mask-image: linear-gradient(to left, transparent 0 90%, #000 100%);
  -webkit-mask-image: linear-gradient(to left, transparent 0 90%, #000 100%);
}

@media (min-width: 1100px) {
  .page-hero--tex-dither-alt .page-hero__dither {
    mask-image: linear-gradient(to left, transparent 0 84%, #000 96%);
    -webkit-mask-image: linear-gradient(to left, transparent 0 84%, #000 96%);
  }
}

@media (min-width: 1300px) {
  .page-hero--tex-dither-alt .page-hero__dither {
    mask-image: linear-gradient(to left, transparent 0 78%, #000 90%);
    -webkit-mask-image: linear-gradient(to left, transparent 0 78%, #000 90%);
  }
}

/* The mirror crawls the other way, so two pages visited in sequence differ in
   direction as well as in side. Same whole-pitch rule, negated. */
.page-hero--tex-dither-alt .page-hero__dither::before {
  animation-name: heroDitherGlideBack;
}

/* The same top-anchored glow, on the teal end of the palette and mirrored to
   the left margin. The vertical fade is inherited from the base rule - only
   the colour and the side change. */
.page-hero--tex-dither-alt .page-hero__dither::after {
  background-image: radial-gradient(
      62% 80% at 4% 2%,
      rgba(14, 165, 165, 0.12),
      transparent 68%
    ),
    radial-gradient(48% 62% at 28% 26%, rgba(52, 196, 138, 0.1), transparent 72%);
}

@keyframes heroDitherGlideBack {
  to { transform: translate3d(-40px, -40px, 0); }
}

/* ---------- The centred product-title hero ----------
   The treatment the owner picked from the three hero options: the product
   name promoted out of the eyebrow and into the page's <h1>, set large,
   centred, and filled with the brand gradient, with the breadcrumb dropped
   beneath it rather than sitting above it.

   Two consequences of the centring, both deliberate:

   1. The title needed a step above the top of the type scale, so tokens.css
      gains --fs-6xl — 44px to 84px, against --fs-5xl's 44px to 72px.
   2. A centred title cannot share a band with a two-column split, so the
      price card leaves the right-hand slot and sits centred under the
      buttons, capped so it stays a card and not a page-wide panel.

   Opt-in per page via `heroCenter`, the same way the ink hero is. */
.page-hero--center .page-hero__inner {
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  text-align: center;
  gap: var(--sp-8);
}

/* The doubled class is load-bearing: `.page-hero h1` above caps every hero
   heading at 22ch, and a flex row of badge + title must not inherit that. */
.page-hero .page-hero__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4) var(--sp-5);
  max-width: none;
  font-size: var(--fs-5xl);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tighter);
}

/* The badge scales with the title it belongs to. It was 56px against an
   84px title, which read as a small chip bolted onto large type; the owner
   asked for the icon to match the title's presence while the title itself
   comes down a size, so the two converge. 68px against --fs-5xl's 72px cap
   puts the badge just under the cap height instead of well below it. */
.page-hero__title .page-hero__icon {
  width: 68px;
  height: 68px;
  border-radius: var(--r-lg);
}

.page-hero__title .page-hero__icon svg {
  width: 34px;
  height: 34px;
}

/* The gradient is what says "this is the product you are looking at", so it
   has to survive a browser that cannot clip a background to text: the solid
   colour is the fallback, and only a browser that supports the clip is
   allowed to make the fill transparent.

   The endpoints are measured, not eyeballed — against the band and against
   the wash at its strongest: brand-800 8.40 / 7.73, brand-600 4.63 / 4.26,
   teal-600 3.87 / 3.56. brand-500 (2.97 / 2.73) is deliberately not in the
   ramp: it fails even the 3:1 floor for large text. */
.page-hero__title-text {
  color: var(--brand-700);
}

/* Where the clip is unsupported on the dark band, the solid must be a tint
   that reads on ink, not the dark green above. */
.page-hero--ink .page-hero__title-text {
  color: var(--brand-300);
}

@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .page-hero__title-text {
    background-image: linear-gradient(
      96deg,
      var(--brand-800) 0%,
      var(--brand-600) 52%,
      var(--teal-600) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  /* The same idea on the dark band, where the ramp above would be
     dark-on-dark and effectively invisible. This one runs the LIGHT end of
     each hue instead: the gradient is what says "this is the product you
     are looking at", and that has to survive the move to ink.

     Stops are chosen against --grad-ink at its lightest stop (#16213A),
     because the hero photo's scrim reaches only 0.7 alpha at the right edge
     and the aurora lightens parts of the band further. brand-300 and
     brand-400 are the two brand tints that clear 3:1 there; brand-500
     (2.67:1) is deliberately excluded for the same reason it is excluded
     from the light ramp. */
  .page-hero--ink .page-hero__title-text {
    background-image: linear-gradient(
      96deg,
      var(--brand-300) 0%,
      var(--brand-400) 52%,
      var(--brand-300) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* Forced colours discards author backgrounds, which would leave the clip
   with nothing to paint and the title invisible. */
@media (forced-colors: active) {
  .page-hero__title-text,
  .page-hero--ink .page-hero__title-text {
    background-image: none;
    color: CanvasText;
  }
}

/* The marketing line, demoted from the <h1> to a tagline beneath the
   breadcrumb — still the second-largest thing in the band. */
.page-hero__tagline {
  max-width: 38ch;
  margin-top: var(--sp-6);
  margin-inline: auto;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 650;
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--ink-800);
  text-wrap: balance;
}

/* Under the title rather than above it, so the spacing flips.
   CHANGED 2026-10-08: this page now follows the standard order — breadcrumb,
   then the product label, then the h1 — so the breadcrumb is FIRST and the
   `margin-top` that used to push it clear of a title above it is wrong. The
   label below it carries its own bottom margin. */
.page-hero--center .breadcrumb {
  justify-content: center;
  margin-top: 0;
  margin-bottom: 0;
}

/* The label is `inline-flex` and lives inside the copy block on this hero,
   so the inner's own `text-align: center` already puts it on the axis —
   no extra rule. It sits between the breadcrumb and the h1, which is the
   standard order every product hero uses (see PROJECT-BRIEF §4.6). */

/* Both carry a measure, so both need centring within the column as well: a
   max-width on its own leaves them sitting on the left, under a centred
   title. `.lead` comes in from base.css with 62ch and no auto margins. */
.page-hero--center .lead {
  margin-inline: auto;
}

.page-hero--center .btn-row {
  justify-content: center;
}

/* Centred on the page, left-aligned inside it: the ticks and the price line
   up, and only the card itself moves. */
.page-hero--center .page-hero__aside {
  width: 100%;
  max-width: 420px;
  text-align: left;
}

/* Beside a 44px title on a phone, a 68px badge is more noise than signal. */
@media (max-width: 640px) {
  .page-hero__title .page-hero__icon {
    width: 52px;
    height: 52px;
    border-radius: var(--r-md);
  }

  .page-hero__title .page-hero__icon svg {
    width: 26px;
    height: 26px;
  }
}

/* ---------- The faded photo backdrop ----------
   The url lives in the stylesheet rather than in an inline style attribute
   on the element, because _verify.mjs resolves url() inside CSS files — the
   same image set inline would be invisible to every guard, including the
   one that checks the file exists.

   The image itself is a custom property rather than a literal in the stack,
   so there is ONE scrim (the thing that has to stay identical on every page)
   and one tiny rule per photograph. Each page sets --hero-photo on its own
   modifier class, and every url() is still visible to the guard because the
   resolver matches url() wherever it appears.

   The gradient is layered over the photo in a single background stack, the
   way the homepage hero does it, and it is a gradient rather than a flat
   opacity for the same reason: an opacity high enough to keep the headline
   readable across the whole band would erase the photo entirely, and one low
   enough to see the photo would put bright pixels behind white text. This
   keeps the left third effectively solid where the text sits and lets the
   texture through on the right, behind the glass price card. */
.page-hero__photo {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: linear-gradient(
      100deg,
      var(--ink-900) 0%,
      rgba(10, 18, 32, 0.94) 34%,
      rgba(10, 18, 32, 0.78) 72%,
      rgba(10, 18, 32, 0.7) 100%
    ),
    var(--hero-photo, none);
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}

/* The shared stock backdrop — the one _pagehero.mjs writes. Replaced
   per page by the rules below as commissioned photography lands. */
.page-hero--photo-page .page-hero__photo {
  --hero-photo: url("../img/photos/hero-page.jpg");
}

.page-hero--photo-workspace .page-hero__photo {
  --hero-photo: url("../img/photos/hero-workspace.jpg");
}

.page-hero--light {
  background: var(--white);
}

.page-hero__aside {
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-xl);
  padding: var(--sp-8);
  box-shadow: var(--sh-lg);
}

.page-hero--ink .page-hero__aside {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(10px);
}

/* The price card's label. It reads like a heading and used to BE one, which
   put an `h3` ahead of the page's first `h2` on the three pages that carry
   this aside (hosting, security and WHOIS) — an h1 -> h3 jump for anyone
   moving by heading. It labels a price; it is not a section of the document,
   so it is a `<p>` now and the heading type is restated here so the
   rendering is unchanged. `.page-hero__aside h3` is kept as well because a
   future aside may legitimately lead with a heading. */
.page-hero__aside h3,
.page-hero__aside-label {
  font-size: var(--fs-lg);
  color: var(--ink-900);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  line-height: var(--lh-snug);
  text-wrap: balance;
}

.page-hero__aside .price {
  margin-top: var(--sp-3);
}

/* ---------- The hero screenshot ----------
   The Website Builder page carries a screenshot under the hero copy
   instead of the price card. It is a white card, and the white is a
   contract rather than a preference: the file is a JPEG, so its soft
   ground shadow is baked onto white (_harvest/_builder.mjs composites it
   there). On any other colour the shadow ends in a hard rectangle. Same
   rule as `.split__media--cutout` below.

   Drawn at 900px maximum. The source is 1536px wide, so this is where the
   sharpness budget runs out — at 900 the file is printed at about 1.4x,
   and stretching it further is what makes the interface text inside a
   screenshot start to soften. */
.page-hero__shot {
  max-width: 900px;
  margin: var(--sp-12) auto 0;
  padding: clamp(10px, 1.6vw, 22px);
  background: var(--white);
  border: 1px solid var(--slate-200);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-lg);
}

.page-hero__shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: calc(var(--r-xl) - 10px);
}

/* ---------- The same card, holding a transparent cut-out ----------
   The WordPress and cPanel hosting heroes moved into this card on
   2026-10-04, and it is a deliberate reversal of the frameless treatment
   those two were built with — so it is worth writing down why.

   Those files are transparent cut-outs of a person with the product open
   beside them, and a cut-out normally wants no plate at all: padding and a
   border round a silhouette draw a second outline around an object that
   already has one. What changed is what is *inside* the silhouette. The
   WordPress render carries the WordPress mark, a floating status chip and
   two accent strokes; the cPanel one carries two floating feature chips.
   Those are flat, card-shaped graphics with their own edges, and — measured
   rather than assumed — both renders were generated flush to the canvas, so
   the accent strokes were clipped by the left and top edges. On the band
   that clip reads as a mistake, because there is nothing to say where the
   artwork was supposed to end.

   A card answers exactly that. It gives the composition a declared edge and
   real air around it, so the artwork is a framed scene rather than a
   silhouette bleeding off a wash. The registry now also ships both files
   with 28px of transparent margin baked in (_harvest/_bare.mjs), so the
   subject does not sit on the frame either.

   `data-bare` on the <img> is what selects this: the extra padding is for
   artwork, not for a screenshot whose edges are already its own boundary.
   The generated files are placed by hand in the page module rather than
   through mediaSlot(), so a filled slot and this treatment cannot drift
   apart silently — _harvest/_smoke.ps1 asserts the card on both pages. */
.page-hero__shot:has(> img[data-bare]) {
  padding: clamp(18px, 2.8vw, 44px);
}

/* ---------- The hero video, in its own browser window ----------
   The Website Builder hero leads with the recording of the builder rather than
   with a still of it. This block is the window it sits in, and the thing worth
   writing down is what is NOT here: the white card.

   THE CHROME STAYS, THE CARD GOES. The card above exists because its JPEG has
   a soft ground shadow baked onto white - the card IS the file's own plate,
   extended. A recording cropped to a hard edge has no such shadow, so a card
   around it wraps a window in a second window and adds a white margin the
   picture never asked for. The owner asked for exactly this split: keep the
   browser, lose the extra white card.

   So `.page-hero__shot` is stripped back to nothing for this one hero - no
   ground, no border, no radius, no padding - and `.browser-frame` draws the
   window itself, directly on the band. `:has(> .browser-frame)` is what
   selects it, so the two cut-out heroes further down, whose shots are bare
   `<img>`s, are untouched.

   Wider than the 900px card because there is no longer a plate to keep it
   inside, and the toolbar is live DOM rather than a picture of a toolbar, so
   its type is crisp - unlike the recording's own baked chrome, which is
   cropped away below (see `object-position`). */
.page-hero__shot:has(> .browser-frame) {
  max-width: 1040px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.browser-frame {
  margin: 0;
  border-radius: var(--r-xl);
  overflow: hidden;
  background: var(--white);
  border: 1px solid var(--slate-200);
  box-shadow: var(--sh-lg);
}

/* The three stoplight colours are the real values on purpose: a near-miss red
   reads as a printing error rather than as a window, and a window is the whole
   point of the chrome. */
.browser-frame__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 38px;
  padding: 0 14px;
  background: linear-gradient(180deg, #f7f8fa, #eef0f3);
  border-bottom: 1px solid var(--slate-200);
}

.browser-frame__dots {
  display: flex;
  gap: 7px;
  flex: 0 0 auto;
}

.browser-frame__dots i {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: #ff5f57;
}

.browser-frame__dots i:nth-child(2) { background: #febc2e; }
.browser-frame__dots i:nth-child(3) { background: #28c840; }

.browser-frame__url {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-inline: auto;
  max-width: 58%;
  padding: 5px 14px 5px 10px;
  border-radius: 999px;
  background: var(--white);
  border: 1px solid var(--slate-200);
  font-size: 12px;
  line-height: 1;
  color: var(--slate-500);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.browser-frame__url svg {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  color: var(--brand-600);
}

/* Balances the dot cluster so the URL pill is centred rather than pushed left.
   Exactly the cluster's own width: 3 x 11px plus 2 x 7px. */
.browser-frame__tail {
  display: block;
  width: 47px;
  flex: 0 0 auto;
}

.browser-frame__viewport {
  position: relative;
  aspect-ratio: 40 / 21;
  background: var(--white);
  overflow: hidden;
}

.browser-frame__viewport video,
.browser-frame__viewport img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  /* THE CROP. `cover` scales the recording to fill a 40:21 box, so it overflows
     vertically; anchoring to the bottom discards the top. The top is exactly
     the recording's own baked toolbar, which was captured at about 1x and
     scaled with the rest of the frame - the only genuinely blurry type on the
     page, sitting directly above UI that is crisp. The drawn bar above is what
     replaces it. */
  object-fit: cover;
  object-position: 50% 100%;
}

/* ---------- POSTER FIRST, THEN VIDEO ----------
   The markup ships the poster visible and the recording not loaded at all.
   `site.js` adds `.is-live` to the frame only when it has decided the visitor
   can afford the video, and that one class is what swaps them. So there is no
   "fallback" branch anywhere: the poster is not a fallback, it is the default,
   and the video is the upgrade.

   This is also why nothing can flash. A `<video>` that has not decoded a frame
   paints black, and a black rectangle where a picture should be is worse than
   a slightly static one. */
.browser-frame__viewport video { display: none; }
.browser-frame__viewport img { display: block; }

.browser-frame.is-live .browser-frame__viewport video { display: block; }
.browser-frame.is-live .browser-frame__viewport img { display: none; }
/* ---------- The SiteGround split ----------
   Copy on the left, the product composition on the right. This is the
   arrangement the owner asked for after seeing the cPanel and WordPress
   renders: a real person with the product open beside them, which says
   "a business uses this" in a way a feature list cannot.

   Two things about the column:

   1. It is a genuine second grid column, not an absolutely-positioned
      overlay, so the copy keeps its measure and the composition keeps its
      aspect ratio on a phone (where the grid collapses and the art stacks
      under the buttons).

   2. The art is frameless. These files are transparent cut-outs and the
      band behind them is the ink hero with a photo and the aurora running
      through it — a plate or a border would draw a second outline around a
      silhouette that already has a real one. Same rule as
      `.split__media--bare`. The alignment is bottom-right rather than
      centred so the composition sits ON the band instead of floating in it,
      which is how SiteGround grounds theirs. */
.page-hero--art .page-hero__inner {
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: var(--sp-10);
  align-items: end;
}

.page-hero__art {
  display: flex;
  justify-content: flex-end;
  align-items: flex-end;
  min-width: 0;
}

.page-hero__art img {
  display: block;
  width: 100%;
  max-width: 620px;
  height: auto;
}

@media (max-width: 900px) {
  .page-hero--art .page-hero__inner {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--sp-8);
  }

  .page-hero__art {
    justify-content: center;
    order: 2;
  }
}

/* Pillar cards use the .card__link overlay, so mirror the focus ring */
.pillar:focus-within {
  border-color: var(--brand-400);
  box-shadow: var(--sh-lg);
}

.pillar:focus-within .card__link {
  outline: 3px solid var(--brand-500);
  outline-offset: 4px;
  border-radius: var(--r-lg);
}

/* ============================================================
   PRICING CLARITY & TRUST, SURFACED
   ------------------------------------------------------------
   Two things this closes, both about the budget-hosting reader
   rather than about taste.

   1. First-term versus renewal is the single most decision-
      relevant fact on a discounted hosting page, and it lived
      only in a 13px `--slate-500` line at the foot of the grid —
      the quietest type on the page carrying the loudest claim.
      `.price-caveat` is the same sentence at body size and
      `--slate-600`, so it reads as a disclosure a visitor is
      meant to find rather than as fine print that merely exists.

   2. "Who we are" — an independent reseller, supplied through
      one of the world's largest registrars — was stated only in
      the footer disclaimer. On a site whose whole claim is "the
      same products, lower price", the reseller identity IS the
      answer to "then how are you cheaper", so it belongs near
      the hero rather than 3,000 pixels below it.

   Both are plain token-based components. Nothing here invents a
   colour, size or space value; the hairline is opt-in because
   inside a card or a hero aside it would draw a second border
   around content that already has one.
   ============================================================ */

.price-caveat {
  margin-top: var(--sp-5);
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--slate-600);
  max-width: 78ch;
}

.price-caveat--center {
  margin-inline: auto;
  text-align: center;
}

.price-caveat--ruled {
  padding-top: var(--sp-4);
  border-top: 1px solid var(--slate-200);
}

.price-caveat--on-dark {
  color: var(--slate-300);
}

.disclosure-line {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
  max-width: 58ch;
  font-size: var(--fs-base);
  line-height: 1.55;
  color: var(--slate-600);
}

.disclosure-line svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  /* Optical, not geometric: the mark has to sit on the first
     line's cap-height, and a flex start alignment floats it. */
  margin-top: 2px;
  color: var(--brand-600);
}

.disclosure-line strong {
  color: var(--ink-900);
  font-weight: 650;
}

.disclosure-line--center {
  margin-inline: auto;
  justify-content: center;
}

.disclosure-line--on-dark {
  color: var(--slate-300);
}

.disclosure-line--on-dark svg {
  color: var(--brand-400);
}

.disclosure-line--on-dark strong {
  color: var(--white);
}

/* ============================================================
   ENTRANCE — the lede and plain heroes' choreographed arrival
   ------------------------------------------------------------
   The product, domains and split heroes each step their copy in
   with a blur + 22px rise (product.css, domains-hero.css,
   split-hero.css). The text-only heroes — `.page-hero--lede`
   (hosting, contact, FAQ, support) and the bare `.page-hero`
   (legal, 404) — had no arrival at all: the whole column just
   appeared. Their copy column is the first grid child of
   `.page-hero__inner` (label/eyebrow, h1, lead, button row, then
   an optional disclosure), the same shape on every one of them,
   so the same stagger is driven here structurally, with no markup
   change. CSS-only, `both` fill, keyed off `html.js`, and only
   under no-preference for motion. The homepage is deliberately
   NOT included — its hero already runs the aurora, the code
   wallpaper and the TLD drift, so it does not load flat.
   ============================================================ */
@keyframes pageHeroEnter {
  from { opacity: 0; transform: translate3d(0, 22px, 0); filter: blur(6px); }
  to   { opacity: 1; transform: none; filter: none; }
}

@media (prefers-reduced-motion: no-preference) {
  html.js .page-hero--lede .page-hero__inner > div:first-child > *,
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > * {
    animation: pageHeroEnter 900ms var(--ease-out) both;
  }
  html.js .page-hero--lede .page-hero__inner > div:first-child > *:nth-child(2),
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > *:nth-child(2) { animation-delay: 80ms; }
  html.js .page-hero--lede .page-hero__inner > div:first-child > *:nth-child(3),
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > *:nth-child(3) { animation-delay: 220ms; }
  html.js .page-hero--lede .page-hero__inner > div:first-child > *:nth-child(4),
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > *:nth-child(4) { animation-delay: 320ms; }
  html.js .page-hero--lede .page-hero__inner > div:first-child > *:nth-child(5),
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > *:nth-child(5) { animation-delay: 420ms; }
  html.js .page-hero--lede .page-hero__inner > div:first-child > *:nth-child(6),
  html.js .page-hero:not([class*="--"]) .page-hero__inner > div:first-child > *:nth-child(6) { animation-delay: 520ms; }

  /* The lede aside (the hosting price card) rises after the copy, the way the
     product hero's artwork does. Only the hosting hub carries one. */
  html.js .page-hero--lede .page-hero__aside {
    animation: pageHeroEnter 900ms var(--ease-out) 360ms both;
  }
}
/* The customer-care photo in the CTA band: the real people the heading
   promises, framed above the phone card. Below the fold, so lazy-loaded;
   width/height reserve its box to avoid CLS. */
.cta-band__art {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-lg);
  margin-bottom: var(--sp-4);
}
/* A single customer-care rep's face in the phone card - the real person the
   heading promises, kept small and face-only per the owner's call. */
/* The customer-care rep stands in the MIDDLE of the band - a standalone
   figure between the copy and the phone card. The image carries its own
   transparent panel, so there is no container: no circular crop, no ring,
   no clip. It is centred on the band itself (position: absolute takes it
   out of the card's flex row, so the phone card keeps its full width). */
.cta-band__side {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}

.cta-band__avatar {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 128px;
  height: 137px;
  object-fit: contain;
  z-index: 1;
}

/* The phone card keeps its full column width now the figure floats free. */
.cta-band__side > .cta-band__contact {
  flex: 1;
}

@media (max-width: 860px) {
  .cta-band__side {
    flex-direction: column;
    align-items: center;
  }
  .cta-band__avatar {
    position: static;
    transform: none;
    margin-bottom: var(--sp-5);
  }
}

@media (max-width: 640px) {
  .cta-band__side {
    gap: var(--sp-4);
  }
  .cta-band__avatar {
    width: 72px;
    height: 72px;
  }
}