/* ============================================================
   Alpha — KEYNOTE LAYER
   The Apple-register design language: huge centred type, hard
   white/black contrast, product filling the frame, scroll as
   the narrative device.

   RULE: no raw values in here. Everything comes from tokens.css
   (--kn-*, --surface-*, --text-*, --space-*). If a number is
   needed, it becomes a token first.
   ============================================================ */


/* ------------------------------------------------------------
   Type
   ------------------------------------------------------------ */
.kn-display {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--text-kn-display);
  line-height: var(--lh-kn-display);
  letter-spacing: var(--tracking-kn-display);
  color: var(--text-primary);
}
.kn-h2 {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--text-kn-h2);
  line-height: var(--lh-kn-h2);
  letter-spacing: var(--tracking-kn-h2);
  color: var(--text-primary);
}
.kn-sub {
  font-size: var(--text-kn-sub);
  line-height: 1.42;
  font-weight: 400;
  color: var(--text-muted);
}

/* On the black stage */
.kn-invert .kn-display,
.kn-invert .kn-h2 { color: var(--text-on-invert); }
.kn-invert .kn-sub { color: var(--text-on-invert-muted); }


/* ------------------------------------------------------------
   Surfaces
   ------------------------------------------------------------ */
.kn-section {
  padding: var(--kn-section-y) 0;
  text-align: center;
}
.kn-invert { background: var(--surface-invert); }
.kn-subtle { background: var(--surface-subtle); }
.kn-wrap {
  max-width: var(--kn-measure);
  margin: 0 auto;
  padding-inline: var(--space-page-x);
}


/* ------------------------------------------------------------
   Apple-style chevron links
   ------------------------------------------------------------ */
.kn-links {
  display: flex;
  gap: var(--space-xl);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: var(--space-lg);
}
.kn-link {
  color: var(--color-link);
  font-size: var(--text-body-lg);
  text-decoration: none;
}
.kn-link::after { content: " \203A"; }
.kn-link:hover { text-decoration: underline; }
.kn-invert .kn-link { color: var(--kn-link-on-invert); }


/* ------------------------------------------------------------
   Reveal — driven by keynote.js adding .is-in
   ------------------------------------------------------------ */
[data-kn-reveal] {
  opacity: 0;
  transform: translateY(var(--kn-reveal-shift));
  transition: opacity var(--kn-reveal-dur) var(--kn-ease),
              transform var(--kn-reveal-dur) var(--kn-ease);
}
[data-kn-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-kn-reveal] { opacity: 1; transform: none; transition: none; }
}


/* ------------------------------------------------------------
   HERO — pinned; product grows, copy lifts away
   ------------------------------------------------------------ */
.kn-hero {
  height: var(--kn-hero-track);
  position: relative;
  background: var(--color-surface);
}
.kn-hero__stick {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding-top: var(--kn-hero-copy-top);
  overflow: hidden;
}
.kn-hero__copy {
  position: relative;
  z-index: 3;
  text-align: center;
  padding-inline: var(--space-page-x);
  will-change: transform, opacity;
}
.kn-hero__unit {
  position: absolute;
  bottom: var(--kn-hero-unit-bottom);
  left: 50%;
  transform: translateX(-50%) scale(var(--kn-hero-scale-from));
  height: var(--kn-hero-unit-h);
  aspect-ratio: 1 / 1;
  z-index: 2;
  filter: drop-shadow(var(--kn-unit-shadow));
  will-change: transform;
}
/* ------------------------------------------------------------
   CHAPTERS — pinned product on black, beats advance
   ------------------------------------------------------------ */
.kn-chapters {
  height: var(--kn-chapters-track);
  position: relative;
  background: var(--surface-invert);
}
.kn-chapters__stick {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  overflow: hidden;
}
.kn-chapters__glow {
  position: absolute;
  bottom: var(--kn-glow-bottom);
  width: var(--kn-glow-size);
  height: var(--kn-glow-size);
  border-radius: 50%;
  background: radial-gradient(circle, var(--kn-glow-1) 0%, rgba(0, 0, 0, 0) 68%);
  transition: background var(--kn-glow-dur) var(--ease);
}
.kn-chapters__unit {
  height: var(--kn-chapter-unit-h);
  aspect-ratio: 1 / 1;
  margin-bottom: var(--kn-chapter-unit-bottom);
  position: relative;
  z-index: 2;
  filter: drop-shadow(var(--kn-unit-shadow-dark));
  will-change: transform;
}
.kn-beat {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--kn-beat-top);
  z-index: 4;
  text-align: center;
  padding-inline: var(--space-page-x);
  opacity: 0;
  transform: translateY(var(--space-lg));
  transition: opacity var(--kn-beat-dur) var(--ease),
              transform var(--kn-beat-dur) var(--ease);
}
.kn-beat.is-on { opacity: 1; transform: none; }
.kn-beat__eyebrow {
  font-size: var(--text-caption);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-on-invert-muted);
  margin-bottom: var(--space-md);
}
.kn-beat p {
  max-width: var(--kn-beat-measure);
  margin: var(--space-md) auto 0;
  font-size: var(--text-body-lg);
  line-height: 1.45;
  color: var(--text-on-invert-muted);
}
.kn-dots {
  position: absolute;
  bottom: var(--kn-dots-bottom);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: var(--space-sm);
  z-index: 5;
}
.kn-dots i {
  width: var(--kn-dot-size);
  height: var(--kn-dot-size);
  border-radius: var(--r-pill);
  background: var(--kn-dot-off);
  transition: background var(--dur) var(--ease), width var(--dur) var(--ease);
}
.kn-dots i.is-on {
  background: var(--text-on-invert);
  width: var(--kn-dot-size-on);
}

@media (prefers-reduced-motion: reduce) {
  .kn-beat { position: static; opacity: 1; transform: none; }
  .kn-beat + .kn-beat { margin-top: var(--space-2xl); }
  .kn-chapters { height: auto; }
  .kn-chapters__stick { position: static; height: auto; flex-direction: column; padding: var(--kn-section-y) 0; }
}


/* ------------------------------------------------------------
   RUNWAY — the range; vertical scroll drives horizontal travel.
   The transform itself is applied by Alpha.horizontalScroll.
   ------------------------------------------------------------ */
.kn-runway { background: var(--color-surface); }
.kn-runway__head { text-align: center; margin-bottom: var(--space-2xl); }
.kn-track {
  display: flex;
  gap: var(--space-lg);
  padding-inline: var(--space-page-x);
}
.kn-card {
  flex: 0 0 var(--kn-card-w);
  background: var(--color-surface);
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-1);
  text-align: left;
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.kn-card:hover { transform: translateY(calc(var(--space-xs) * -1)); box-shadow: var(--shadow-2); }
.kn-card figure {
  height: var(--kn-card-fig-h);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: var(--color-surface-subtle);
  margin: 0;
}
.kn-card figure img { height: 88%; width: auto; filter: drop-shadow(var(--kn-unit-shadow-sm)); }
.kn-card__body { padding: var(--space-lg); }
.kn-card__eyebrow {
  font-size: var(--text-caption);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}
.kn-card h3 {
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: var(--tracking-tight-sm);
  margin: var(--space-sm) 0;
}
.kn-card p { font-size: var(--text-body-sm); color: var(--text-muted); line-height: 1.5; }


/* ------------------------------------------------------------
   SPECS — big figures on black
   ------------------------------------------------------------ */
.kn-specs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  margin-top: var(--space-3xl);
}
.kn-spec { padding-inline: var(--space-lg); border-left: 1px solid var(--kn-rule-invert); }
.kn-spec:first-child { border-left: 0; }
.kn-spec b {
  display: block;
  font-weight: 700;
  font-size: var(--text-kn-figure);
  letter-spacing: var(--tracking-kn-display);
  line-height: 1;
  color: var(--text-on-invert);
}
.kn-spec b i { font-style: normal; font-size: 0.34em; color: var(--text-on-invert-muted); }
.kn-spec span { display: block; margin-top: var(--space-md); font-size: var(--text-body-sm); color: var(--text-on-invert-muted); }


/* ------------------------------------------------------------
   JOURNEY — the five stages
   ------------------------------------------------------------ */
.kn-journey {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-lg);
  margin-top: var(--space-3xl);
  text-align: left;
}
.kn-step { padding-top: var(--space-lg); border-top: var(--kn-step-rule) solid var(--color-surface-subtle); }
.kn-step:last-child { border-top-color: var(--color-link); }
.kn-step b { display: block; font-size: var(--text-body-lg); font-weight: 600; letter-spacing: var(--tracking-tight-sm); margin-bottom: var(--space-sm); }
.kn-step span { font-size: var(--text-body-sm); color: var(--text-muted); line-height: 1.5; }


/* ------------------------------------------------------------
   TILES — line-up / process
   ------------------------------------------------------------ */
/* auto-fit rather than a fixed three: the rent/own section has TWO tiles, and
   a hard 3-column grid left them hugging the left edge (Master, 2026-08-10).
   justify-content centres whatever number of tracks actually get created. */
.kn-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--kn-tile-min), var(--kn-tile-max)));
  justify-content: center;
  gap: var(--space-lg);
  margin-top: var(--space-2xl);
}
.kn-tile {
  background: var(--surface-subtle);
  border-radius: var(--radius-card);
  padding: var(--space-2xl) var(--space-lg);
  text-align: center;
}
.kn-subtle .kn-tile { background: var(--color-surface); }
.kn-tile figure { height: var(--kn-tile-fig-h); display: flex; align-items: flex-end; justify-content: center; margin: 0 0 var(--space-lg); }
.kn-tile figure img { height: 100%; width: auto; object-fit: contain; filter: drop-shadow(var(--kn-unit-shadow-sm)); }
.kn-tile h3 { font-size: var(--text-h3); font-weight: 600; letter-spacing: var(--tracking-tight-sm); }
.kn-tile p { font-size: var(--text-body-sm); color: var(--text-muted); margin-top: var(--space-sm); line-height: 1.55; }


/* ------------------------------------------------------------
   Responsive
   ------------------------------------------------------------ */
@media (max-width: 900px) {
  .kn-specs, .kn-journey { grid-template-columns: 1fr 1fr; }
  .kn-spec { border-left: 0; padding-block: var(--space-md); }
  .kn-hero { height: var(--kn-hero-track-m); }
  .kn-chapters { height: var(--kn-chapters-track-m); }
  .kn-card { flex-basis: var(--kn-card-w-m); }
}
@media (max-width: 560px) {
  .kn-specs, .kn-journey, .kn-tiles { grid-template-columns: 1fr; }
}


/* ============================================================
   KEYNOTE ON THE INNER PAGES
   The home page carries the concept explicitly. Every other page
   keeps its existing markup and copy (Master's rule), so the
   register is applied by restyling the classes those pages
   ALREADY share: .section-label / .section-title / .section-subtitle,
   plus the card and link primitives.

   Measured before this existed: home h2 = 64px, inner-page h2 =
   19–42px, zero Keynote classes on any inner page. The chrome had
   changed; the bodies had not.
   ============================================================ */

/* --- Section headings take the Keynote scale --- */
.section-title {
  font-size: var(--text-kn-h2);
  line-height: var(--lh-kn-h2);
  letter-spacing: var(--tracking-kn-h2);
  font-weight: 700;
  text-wrap: balance;
}
.section-subtitle {
  font-size: var(--text-kn-sub);
  line-height: 1.42;
  max-width: var(--kn-sub-measure);
  color: var(--text-muted);
}
.section-label {
  font-size: var(--text-caption);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 500;
}

/* Apple centres its section intros; the grids below stay as they are. */
.section-header,
.section-heading {
  text-align: center;
}
.section-header .section-subtitle,
.section-heading .section-subtitle {
  margin-inline: auto;
}

/* --- Section rhythm: more air between ideas --- */
main > section:not([class*="hero"]),
#main > section:not([class*="hero"]) {
  padding-block: var(--kn-section-y);
}

/* Section headings that carry no shared class — CTA blocks, .area-section
   headings, the rental page's steps. Card titles are deliberately excluded:
   they are labels inside a grid, not section headings, and blowing them up
   would wreck the listings. */
main > section > h2,
main > section > .section-container > h2,
main > section > .wrap > h2,
.cta h2,
.area-section h2,
.area-faq h2 {
  font-size: var(--text-kn-h2);
  line-height: var(--lh-kn-h2);
  letter-spacing: var(--tracking-kn-h2);
  font-weight: 700;
  text-wrap: balance;
}
.blog-card-title,
.promo-card-title,
.product-card h2,
.product-card h3,
.brand-heading,
.sr-only { font-size: inherit; letter-spacing: normal; }
.blog-card-title, .promo-card-title { font-size: var(--text-h3); }

/* --- Cards adopt the Keynote card language --- */
.product-card,
.blog-card,
.promo-card {
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-1);
  border: 0;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.product-card:hover,
.blog-card:hover,
.promo-card:hover {
  transform: translateY(calc(var(--space-xs) * -1));
  box-shadow: var(--shadow-2);
}

/* --- Inline prose links pick up the Keynote link colour --- */
.area-section p a,
.blog-post-content a,
.legal-content a {
  color: var(--color-link);
  font-weight: 500;
}

@media (max-width: 768px) {
  main > section:not([class*="hero"]),
  #main > section:not([class*="hero"]) { padding-block: var(--kn-section-y-m); }
}


/* ------------------------------------------------------------
   Runway consistency (Master, 2026-08-10)
   Home's .kn-track was full-bleed while the product-detail feature
   runway sat inside the page container, so the same interaction
   looked like two different components. One inset token now drives
   both, and the CARD WIDTH matches too.
   ------------------------------------------------------------ */
.kn-track,
.pd-features__track,
.hscroll-track {
  padding-inline: var(--kn-runway-inset);
  gap: var(--kn-runway-gap);
}
/* The stacked variant is a deliberate exception — it is a centred column,
   not a runway, and must keep its own measure. */
.pd-features--stacked .pd-features__track {
  padding-inline: var(--space-page-x);
}


/* ------------------------------------------------------------
   CONSISTENCY PASS (Master, 2026-08-10)
   "even if the contents remain the same, we need to apply the
   consistent design to match the home page's design."

   Everything here overrides a component that was designed for the
   OLD blue-hero site and became wrong — sometimes invisible — the
   moment the hero surfaces turned white and black.
   ------------------------------------------------------------ */

/* --- Blog category chips were WHITE-ON-WHITE ---------------
   Same defect Master reported on the products filter, in a class
   the products fix never touched: .blog-filter is styled as a
   glass capsule (--glass-white-70 text on --glass-white-15) built
   to sit on the old blue hero. On the white hero only the active
   chip was legible, so the blog looked like it had one category.
   It survived verification because that sweep queried .filter-btn.
   Repointed to the same --filter-* tokens the products bar uses. */
.blog-filters-inner {
  background: var(--filter-bar-bg);
  border-color: var(--filter-bar-border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.blog-filter { color: var(--filter-fg); }
.blog-filter:hover {
  color: var(--filter-fg-hover);
  background: var(--filter-bg-hover);
}
.blog-filter.active,
.blog-filter.active:hover {
  background: var(--filter-bg-active);
  color: var(--filter-fg-active);
  box-shadow: none;
}

/* --- Section rhythm must not apply to heroes ----------------
   The blanket `main > section` padding above overrode each hero's
   own padding AND stacked against the next section's, measured at
   240px on blog and products — a blank screen between the filter
   bar and the cards it filters. Heroes own their vertical rhythm;
   the section that follows one needs only half a beat, because the
   hero has already provided the air. Every hero class on this site
   contains "hero" and no other section class does. */
main > section[class*="hero"] + section,
#main > section[class*="hero"] + section {
  padding-block-start: var(--kn-section-y-tight);
}
/* ...except a pinned scroll stage, which owns its full height. Giving
   .pd-signature a top padding pushed its sticky canvas down and exposed a band
   of the section's own background above it - the grey strip Master reported
   between the WHP-3000 hero and its scene. */
/* The #main variants matter: an ID selector outranks the class-only exemption,
   so omitting them left the padding in place. */
main > section.pd-signature,
#main > section.pd-signature,
main > section[class*="hero"] + section.pd-signature,
#main > section[class*="hero"] + section.pd-signature {
  padding-block: 0;
}

/* --- The product pricing band was still brand blue ----------
   Master's report #5 named the bottom CTA; .cta was fixed, but the
   product pages carry a SECOND full-width band (.pd-pricing) that
   kept the blue gradient, so product pages still ended on blue.
   Same black stage as every other closing band on the site. */
.pd-pricing {
  background: var(--surface-invert);
  color: var(--text-on-invert);
}

/* --- Product heroes keep their own colour -------------------
   Master (2026-08-10): "we previously had different colour background
   for hero section of each product detail page, i think we can bring
   that back ... i just want to try to make them unique and some can
   be duplicate." Each product page declares its own --theme-tint-*
   and --theme-surface-* pair; product-detail.css composes them. This
   block is intentionally empty of a background override so those win. */

/* --- A filter bar belongs to its grid, not to its hero ------
   The bar is markup-wise the last child of the hero section, so
   the hero's bottom padding and the listing's top padding both
   landed between the chips and the cards they filter (176px).
   :has() scopes this to heroes that actually carry a bar; where
   it is unsupported the page simply keeps the looser spacing. */
main > section[class*="hero"]:has(.blog-filters, .filter-bar),
#main > section[class*="hero"]:has(.blog-filters, .filter-bar) {
  padding-block-end: var(--kn-filterbar-y);
}
main > section[class*="hero"]:has(.blog-filters, .filter-bar) + section,
#main > section[class*="hero"]:has(.blog-filters, .filter-bar) + section {
  padding-block-start: var(--kn-filterbar-y-after);
}


/* ------------------------------------------------------------
   THE RANGE RUNWAY LOOPS ON ITS OWN (Master, 2026-08-10)
   "make six systems section cards to automatically scroll
   repeatedly, so no need to track scrolling for this section ...
   after vitamin shower filter card, it will be whp-3000 card
   again with consistent gap between cards."

   The card set is duplicated in the markup and the row shifts by
   exactly -50%, so the seam is invisible.

   ⚠️ The gap must NOT come from flex `gap` here. With 2N cards and
   gap G the row measures 2N*C + (2N-1)*G, so -50% lands HALF A GAP
   short and the loop visibly stutters once per pass. Each card
   carries its own trailing margin instead, which makes the two
   halves exactly equal and the wrap seamless.

   This section is deliberately no longer wired to
   Alpha.horizontalScroll - it is not scroll-driven at all now. The
   product-detail feature runways still use that scroller.
   ------------------------------------------------------------ */
.kn-marquee {
  /* Full-bleed on purpose: a page inset would scroll away and put
     the seam on screen. */
  overflow: hidden;
}
.kn-marquee__row {
  display: flex;
  width: max-content;
  animation: knMarquee var(--kn-marquee-dur) linear infinite;
  will-change: transform;
}
.kn-marquee .kn-card { margin-right: var(--kn-runway-gap); }
@keyframes knMarquee { to { transform: translateX(-50%); } }

/* A card you are pointing at or tabbing to must hold still long
   enough to read and click. */
.kn-marquee:hover .kn-marquee__row,
.kn-marquee:focus-within .kn-marquee__row { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .kn-marquee { overflow-x: auto; }
  .kn-marquee__row { animation: none; }
}




/* The chapter stage's own product link, below the beat dots. */
.kn-chapters__link {
  position: absolute;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  margin-top: 0;
}


/* ------------------------------------------------------------
   PRODUCT ANGLE STACKS (Master, 2026-08-10)
   "product image animation on home is just the same product image
   growing in size. can we use the assets that we have and actually
   make a cool animation?"

   Each stage now holds several angles of the SAME unit, stacked on a
   common baseline and centred on a common axis. Only opacity changes,
   so a turn costs no layout and stays on the compositor.

   The hero blends continuously (keynote.js writes opacity directly, so
   it reads as the product rotating as it grows). The beat stages settle
   on one angle per beat via .is-on, which is a CSS transition.
   ------------------------------------------------------------ */
.kn-stack { position: relative; }
.kn-stack .kn-frame {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  height: 100%;
  width: auto;
  opacity: 0;
}
.kn-hero__unit .kn-frame { transition: none; }        /* driven per-frame */
.kn-chapters__unit .kn-frame {
  transition: opacity var(--kn-frame-dur) var(--ease);
}
.kn-stack .kn-frame.is-on { opacity: 1; }


@media (prefers-reduced-motion: reduce) {
  .kn-stack .kn-frame { transition: none; }
}


/* ------------------------------------------------------------
   LIGHT CHAPTER STAGE (Master, 2026-08-10)
   "we need vitamin shower filter section to have the same
   background colour as whp-3300 section in home."

   Same pinned mechanics as the black stage, inverted back to the
   hero's surface. Every colour the dark stage takes from the
   --*-on-invert family has to be answered here or the text lands
   white-on-white - the exact failure the filter chips shipped with.
   ------------------------------------------------------------ */
.kn-chapters--light { background: var(--lg-hero-bg); }
.kn-chapters--light .kn-beat__eyebrow { color: var(--text-muted); }
.kn-chapters--light .kn-beat h2 { color: var(--text-primary); }
.kn-chapters--light .kn-beat p { color: var(--text-muted); }
.kn-chapters--light .kn-link { color: var(--color-link); }
.kn-chapters--light .kn-dots i { background: var(--kn-dot-off-light); }
.kn-chapters--light .kn-dots i.is-on { background: var(--text-primary); }
.kn-chapters--light .kn-chapters__unit { filter: drop-shadow(var(--kn-unit-shadow)); }
/* The glow exists to lift a product off black. On white it would read as a
   smudge, so the light stage simply does without it. */
.kn-chapters--light .kn-chapters__glow { display: none; }


@media (max-width: 768px) {
  /* "Explore the Vitamin Shower Filter" wraps at this width, so the link box
     grows upward into the beat dots. Measured -10px before this. */
  .kn-chapters .kn-dots { bottom: var(--kn-dots-bottom-m); }
}


/* ------------------------------------------------------------
   ONE TAG LANGUAGE (Master, 2026-08-10, revised)
   "i noticed there are still tags (e.g., local queensland team
   tag) with different styling ... i think we only need one tag
   token/variable styling."

   The first pass set geometry but not colour, so per-page rules
   still won: .about-hero .hero-badge and .promo-hero .hero-badge
   gave the SAME component a blue-tinted and a neutral ground on
   two pages. Those overrides live in liquid-glass.css,
   promotions.css and product-detail.css.

   ⚠️ THE DOUBLED CLASS IS LOAD-BEARING. Selectors like
   `.about-hero .hero-badge` and `.theme-warm .pd-hero__badge`
   score (0,2,0); a plain `.hero-badge` scores (0,1,0) and loses
   no matter how late it is declared. `.hero-badge.hero-badge`
   matches exactly the same elements at (0,2,0), and source order
   then breaks the tie in this file's favour. No !important needed.

   Every value comes from a --tag-* token: change the token, every
   tag on the site moves together.
   ------------------------------------------------------------ */
.hero-badge.hero-badge,
.pd-hero__badge.pd-hero__badge,
.area-tag.area-tag,
.blog-tag.blog-tag,
.product-tag.product-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs, 0.375rem);
  font-size: var(--tag-size);
  font-weight: var(--tag-weight);
  letter-spacing: var(--tag-tracking);
  text-transform: none;
  padding: var(--tag-pad);
  border-radius: var(--tag-radius);
  line-height: 1.2;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* In-flow and on-card tags run at the smaller size: at 14px a card badge
   competes with the product name beside it. */
.blog-tag.blog-tag,
.product-tag.product-tag {
  font-size: var(--tag-size-sm);
  padding: var(--tag-pad-sm);
}

/* One neutral ground for every tag that is a LABEL. */
.hero-badge.hero-badge,
.pd-hero__badge.pd-hero__badge,
.area-tag.area-tag,
.blog-tag.blog-tag {
  background: var(--tag-bg);
  color: var(--tag-fg);
  border: 1px solid var(--tag-border);
}
.hero-badge.hero-badge svg,
.pd-hero__badge.pd-hero__badge svg {
  width: var(--tag-icon);
  height: var(--tag-icon);
  color: inherit;
}

/* Status badges are the one exception, and deliberately so: on a card the FILL
   is the information (promotion / new / discontinued), not decoration. They
   take the shared geometry and keep their own colour. */
.product-tag.product-tag {
  border: 0;
  color: var(--color-white);
}


/* ------------------------------------------------------------
   SCENT STAGE (Master's sketch, 2026-08-10)
   Black stage, the scent name high in the frame, and the field
   running full-bleed along the BOTTOM.

   The field is a cover-fit band rather than a centred object, so
   it needs different geometry to the product stacks: it pins to
   the bottom edge at full width, and its top is masked into the
   stage so there is no seam between photograph and background.
   ------------------------------------------------------------ */
/* The container spans the WHOLE stage; it is the field PHOTOS that pin to the
   bottom. Sizing the container to the field band meant the closing product
   frame - which shares the container - was pinned to the band's bottom edge
   too, leaving a long void between the copy and the product. */
.kn-scents .kn-scents__field {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
  z-index: 1;
}
.kn-scents .kn-frame {
  position: absolute;
  inset: auto 0 0 0;
  width: 100%;
  height: var(--kn-scent-field-h);
  object-fit: cover;
  transform: none;
  border-radius: 0;
  /* Fade the top edge into the black stage. Without this the photograph ends on
     a hard horizontal line halfway up the screen. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black var(--kn-scent-fade));
          mask-image: linear-gradient(to bottom, transparent 0%, black var(--kn-scent-fade));
  transition: opacity var(--kn-frame-dur) var(--ease);
}
/* The closing frame is the product cut-out, not a field: it keeps its own
   proportions, sits centred, and takes no mask. */
/* The product is centred in the stage, not pinned to the field's edge. */
.kn-scents .kn-frame--unit {
  inset: var(--kn-scent-unit-top) auto auto 50%;
  width: auto;
  height: var(--kn-scent-unit-h);
  transform: translate(-50%, -50%);
  object-fit: contain;
  -webkit-mask-image: none;
          mask-image: none;
  filter: drop-shadow(var(--kn-unit-shadow-dark));
}
/* The beats sit above the field. */
.kn-scents .kn-beat { z-index: 4; }
/* A one-word question needs no measure constraint and reads better large. */
.kn-scents .kn-beat h2 { text-wrap: balance; }




/* ------------------------------------------------------------
   STILL HERO (Master, 2026-08-10)
   "for whp-3300 ... something like 'our slimmest water purifier
   ever'. and it doesn't even need to have animation. just still
   image." Reference supplied: Apple's iPhone Air page - name,
   headline, subhead, one large still, link beneath.

   Nothing here is scroll-driven, and the markup carries no
   data-kn-hero, so keynote.js does not bind to it at all.
   ------------------------------------------------------------ */
.kn-hero--still {
  /* .kn-hero still carries the PINNED hero's track height (--kn-hero-track,
     230vh). Overriding min-height alone left the section a full extra viewport
     tall, which is the white run below the product Master reported. */
  height: auto;
  min-height: auto;
  /* A short bottom: the section below supplies its own top padding, and a full
     beat on both sides read as a void between the hero and the WHP-3700
     (Master, 2026-08-10). */
  padding-block: var(--kn-hero-still-y) var(--kn-hero-still-y-end);
  display: block;
  text-align: center;
  background: var(--lg-hero-bg);
}
.kn-hero--still .kn-eyebrow { margin-bottom: var(--space-md); }
.kn-hero--still .kn-sub {
  max-width: var(--kn-sub-measure);
  margin: var(--kn-title-gap) auto 0;
}
.kn-hero--still .kn-links { margin-top: var(--space-lg); }
.kn-hero__still {
  display: block;
  margin: var(--space-3xl) auto 0;
  height: auto;
  width: min(var(--kn-hero-still-w), 92%);
  filter: drop-shadow(var(--kn-unit-shadow));
}




/* ------------------------------------------------------------
   CONTACT (Master, 2026-08-10)
   "contact page still needs more work on the design
   implementation. especially the contact form."

   Content and fields are unchanged - this is the Keynote register
   applied to the chrome the form was already wearing from the old
   design: a translucent glass panel, 16px/400 labels, 48px fields
   and a small hero.
   ------------------------------------------------------------ */
.contact-form {
  /* Flat white card, not glass. The section behind it is already a flat grey,
     so the translucency bought nothing and read as a leftover. */
  background: var(--color-surface);
  border: 0;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-1);
  padding: var(--form-card-pad);
}

/* Labels become quiet eyebrows - the FIELD is the thing to look at, and at
   16px/600 the labels were competing with the values typed into them. */
.contact-form .form-group label {
  font-size: var(--label-fs);
  font-weight: 500;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--label-color);
  margin-bottom: var(--space-xs);
}
.contact-form .form-group { margin-bottom: var(--form-gap); }

.contact-form input[type="text"],
.contact-form input[type="email"],
.contact-form input[type="tel"],
.contact-form select,
.contact-form textarea {
  width: 100%;
  min-height: var(--field-h);
  padding: var(--space-sm) var(--field-pad-x);
  font-size: var(--field-fs);
  font-family: inherit;
  color: var(--text-primary);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: var(--field-radius);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.contact-form textarea { min-height: var(--field-textarea-h); resize: vertical; }

/* A visible focus ring is the one piece of chrome that must not be quiet. */
.contact-form input:focus,
.contact-form select:focus,
.contact-form textarea:focus {
  outline: none;
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 var(--field-ring-w) var(--field-ring);
}
.contact-form input::placeholder,
.contact-form textarea::placeholder { color: var(--text-muted); }

.form-submit .btn-primary {
  min-height: var(--field-h);
  font-size: var(--text-body);
}

/* The preview panel is an empty box until a product is chosen, so it should sit
   quietly beside the fields rather than competing with them. */
.form-preview-cell .product-preview {
  background: var(--surface-subtle);
  border: 1px solid var(--field-border);
  border-radius: var(--field-radius);
}
.product-preview-placeholder { color: var(--text-muted); }

/* The map is the page's one photograph-scale element; at card size it read as
   an afterthought. */
.map-container {
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-1);
  min-height: var(--contact-map-h);
}
.contact-info-card {
  background: var(--color-surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-card);
  box-shadow: none;
}


/* ------------------------------------------------------------
   EMERGE (Master, 2026-08-10)
   "the apple example i gave you was actually not a scroll driven
   animation. it was automatic reveal of the product from shadow.
   let's do that for ours too."

   So this is NOT scrubbed. The product sits in shadow until the
   section arrives, then lifts into light on its own, once. It
   rides the existing [data-kn-reveal] observer - no new engine,
   and the reduced-motion and never-invisible guarantees in
   keynote.js apply to it for free.
   ------------------------------------------------------------ */
.kn-emerge { text-align: center; }
.kn-emerge__unit {
  margin: var(--space-3xl) auto 0;
  /* Sized against BOTH the viewport and a ceiling: a vh-only size looked right
     at 900px tall and became enormous on an iPad at 1200 (Master: "product
     image sizes ... are too big"). */
  width: min(var(--kn-emerge-unit-w), 92%);
}
.kn-emerge__unit img {
  display: block;
  width: 100%;
  height: auto;
  /* ⚠️ NO opacity fade. Measured off Master's reference recording: the product
     is never absent - it sits at ~13% luminance from the first frame and LIGHTS
     UP. Fading opacity instead makes it appear, which is a different gesture.
     The luminance curve there runs ~2.4s, slow to start and quick through the
     middle, which is what the easing below reproduces. */
  transform: scale(var(--kn-emerge-from)) translateY(var(--kn-emerge-lift));
  /* Blur resolving alongside the brightness is what makes it read as the
     product coming INTO focus out of the dark rather than a lamp being turned
     up (Master wanted it more dramatic, 2026-08-10). */
  filter: brightness(var(--kn-emerge-dark)) blur(var(--kn-emerge-blur))
          drop-shadow(var(--kn-unit-shadow-dark));
  transition: transform var(--kn-emerge-dur) var(--kn-emerge-ease),
              filter var(--kn-emerge-dur) var(--kn-emerge-ease);
}
.kn-emerge__unit.is-in img {
  transform: none;
  filter: brightness(1) blur(0) drop-shadow(var(--kn-unit-shadow-dark));
}
.kn-emerge .kn-specs { margin-top: var(--space-3xl); }
.kn-emerge .kn-links { margin-top: var(--space-2xl); }

@media (prefers-reduced-motion: reduce) {
  .kn-emerge__unit img { transform: none; filter: brightness(1) blur(0); transition: none; }
}


/* The closing section carries a PILL on every page (Master, 2026-08-10:
   "buttons in some bottom hero section is in pill form and others are in plain
   form ... i think the pill form button would be better"). Home was the odd one
   out, using the chevron text link that the mid-page sections use. */
.kn-cta-action {
  display: flex;
  justify-content: center;
  margin-top: var(--space-xl);
}


/* The stage link sits inside the closing beat now (Master: the product link
   goes below the description and above the image), so it inherits the beat's
   fade and needs no positioning of its own. */
.kn-scents .kn-beat .kn-links { margin-top: var(--space-lg); }

/* The closing pill wrapped to three lines at 390px (measured 133px tall)
   because the button had no minimum width to lay its label out in. */
/* The closing pill sizes itself from its label, like the inner pages'. */
.kn-cta-action .btn-white { white-space: nowrap; }


/* The closing section sat on a full --kn-section-y top AND bottom, and the
   footer adds its own padding directly beneath - so the page ended on a long
   run of empty black (Master, 2026-08-10: "bottom hero has too much top and
   bottom padding"). It keeps a full beat above and a shorter one below,
   because the footer supplies the rest. */
/* Doubled class again: `main > section:not([class*="hero"])` scores (0,1,2) and
   a plain `.kn-cta` (0,1,0) never wins, however late it is declared. */
main > .kn-cta.kn-cta,
#main > .kn-cta.kn-cta,
main > .cta.cta,
#main > .cta.cta,
.kn-cta.kn-cta,
.cta.cta { padding-block: var(--kn-cta-y-top) var(--kn-cta-y-bottom); }


/* Card edges (Master, 2026-08-10: "improve border visibility of promotion
   cards"). White cards on the near-white --surface-subtle ground were defined
   by shadow alone, which all but disappears on a bright screen. One token draws
   every card family so they cannot drift apart again. */
.promo-card,
.product-card,
.blog-card,
.kn-card {
  border: 1px solid var(--card-edge);
}
.promo-card:hover,
.product-card:hover,
.blog-card:hover,
.kn-card:hover { border-color: var(--card-edge-hover); }


/* The spotlight scene is the one dark stage; it needs the section behind it to
   be dark too, or the sticky's nav-height padding shows as a band above it. */
.pd-signature--spotlight { background: var(--pd-stage-bg-dark); }


/* --- Contact: the cards sit against the map, not stretched --------------
   The right column stretched to the map's full height, so three cards spread
   down it with air between them instead of reading as a group beside the map
   (Master, 2026-08-10). */
.contact-details-grid { align-items: center; }
.contact-info-cards { align-self: center; }

/* --- Contact form: shorter -------------------------------------------
   Every field ran full width in a single stack, so the form was far taller
   than it needed to be. The short fields pair up; only the ones that genuinely
   need the width (message, and the radio row) stay full. */
.form-col-stack {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--space-lg);
  row-gap: var(--form-gap);
  align-content: start;
}
.form-col-stack > .form-group { margin-bottom: 0; }
/* Only the radio row spans. Letting the product select span too left Location
   half-width with dead space beside it, and the block read as unfinished
   (Master, 2026-08-10). Location and Product pair up instead. */
.form-col-stack > .form-group:has(input[type="radio"]) { grid-column: 1 / -1; }

/* The preview sits at the TOP of its column at the height of the fields beside
   it, rather than stretching into a tall empty panel. */
.form-top-block { align-items: start; }
/* The height goes on the CELL: .product-preview is a position:relative wrapper
   whose contents are absolutely positioned, so it has no intrinsic height and
   collapsed to 2px when sized directly. */
.form-preview-cell { min-height: var(--contact-preview-h); }
.form-preview-cell .product-preview { flex: 1; min-height: var(--contact-preview-h); }

.contact-form textarea { min-height: var(--field-textarea-h-short); }

@media (max-width: 768px) {
  .form-col-stack { grid-template-columns: 1fr; }
}


/* The field pair needs real width: nested inside .form-top-block's 50/50 split
   the two columns measured 184px each. The form column takes the larger share
   and the preview the smaller, and the card itself widens. */
.contact-form-container { max-width: var(--contact-form-max); }
/* ⚠️ DESKTOP ONLY. Unscoped, this loaded after contact.css and overrode its
   mobile rule that stacks .form-top-block to one column - so on a phone the
   preview panel was squeezed into a narrow tall strip beside the fields. */
@media (min-width: 769px) {
  .form-top-block { grid-template-columns: var(--contact-form-split); }
}


/* auto-fit, like .kn-tiles: the figure rows no longer always hold four items
   (the WHP-3700 section carries three), and a hard 4-column grid left an empty
   track and pushed the group off-centre. */
.kn-specs {
  grid-template-columns: repeat(auto-fit, minmax(var(--kn-spec-min), 1fr));
  justify-content: center;
}


/* Air between a beat's headline and its copy. The WHP-3700 section gets this
   from .kn-sub's own margin; the beat stage had the two touching. */
.kn-beat h2 + p { margin-top: var(--space-md); }

@media (max-width: 768px) {
  /* The display-scale closing headline is taller than the h2 it replaced, so
     the product drops to keep clear of it (measured -15px overlap at 390). */
  .kn-scents .kn-frame--unit { top: var(--kn-scent-unit-top-m); }
}


/* A short-but-wide viewport is the hard case: the headline is still at desktop
   display scale while the stage has 900px to work with, and the product ran
   into the copy (measured -34px at 1440x900). Height-based, not width-based -
   the constraint is vertical. */
@media (min-width: 769px) and (max-height: 980px) {
  .kn-scents .kn-frame--unit {
    top: var(--kn-scent-unit-top-short);
    height: var(--kn-scent-unit-h-short);
  }
}


/* The WHP-3700 stage is on the quiet grey now, not black.
   ⚠️ "Emerging from shadow" only reads on BLACK. Carried onto grey at the same
   settings the product became a dark, heavily blurred slab - a smudge, not a
   reveal (Master: "too dramatic now"). On a light ground the same gesture has
   to be a focus-in: dimmed rather than blacked out, less blur, less travel.
   If this section ever goes back to black, restore the deeper values
   (brightness 0.03 / blur 14px / scale 1.24 / 3s) with it. */
.kn-emerge__unit img,
.kn-emerge__unit.is-in img {
  filter: brightness(var(--kn-emerge-dark)) blur(var(--kn-emerge-blur))
          drop-shadow(var(--kn-unit-shadow));
}
.kn-emerge__unit.is-in img {
  filter: brightness(1) blur(0) drop-shadow(var(--kn-unit-shadow));
}


/* ⚠️ Ground-aware text on the quiet grey. Moving a section off black does NOT
   move the colours with it: the eyebrow, spec figures and spec labels all take
   --*-on-invert values, so on grey the figures measured 1.00:1 - white on
   white, invisible - and the eyebrow 2.36:1.
   The muted value is --text-muted-AA, not --text-muted: the plain one is
   4.66:1 on white but only 4.37:1 on --surface-subtle, i.e. under AA purely
   because the ground changed. */
.kn-subtle .kn-beat__eyebrow,
.kn-subtle .kn-spec span,
.kn-subtle .kn-sub { color: var(--text-muted-aa); }
.kn-subtle .kn-spec b,
.kn-subtle h2 { color: var(--text-primary); }
.kn-subtle .kn-link { color: var(--color-link); }
.kn-subtle .kn-spec { border-left-color: var(--card-edge); }

/* Tiles carry their OWN grey fill, so their copy needs the AA-safe muted value
   whatever the section behind them is: --text-muted is 4.66:1 on white but only
   4.37:1 on --surface-subtle, i.e. under AA purely because the ground changed. */
.kn-tile p { color: var(--text-muted-aa); }


/* Title -> description spacing, one value across the three product sections.
   The WHP-3700 measured 0 because the hero's own .kn-sub margin is scoped to
   the hero; the generic .kn-sub carries none. */
.kn-display + .kn-sub,
.kn-h2 + .kn-sub,
.kn-beat h2 + p { margin-top: var(--kn-title-gap); }
.kn-emerge .kn-sub { max-width: var(--kn-sub-measure); margin-inline: auto; }


/* ============================================================
   ONE TYPE SYSTEM (Master, 2026-08-10)
   "look at attached, whp-3300 text and whp-3700 text have
   different font styling ... please thoroughly review the whole
   website and ensure everything is consistent."

   A site-wide audit of computed styles by ROLE found the drift
   below. Each rule here collapses a role onto one treatment.
   ============================================================ */

/* --- EYEBROWS -------------------------------------------------
   Three treatments were live: .section-label in JetBrains MONO at
   600/0.12em (46 uses, 9 pages), .kn-beat__eyebrow in DM Sans at
   400 (14 uses), and "Ruhens WHP-3300" rendering as plain 16px
   body text because .kn-eyebrow had been deleted along with the
   showcase block it happened to live in. That last one is the
   example Master pointed at. */
.kn-eyebrow,
.kn-beat__eyebrow,
.kn-card__eyebrow,
.section-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  line-height: 1.6;
}
/* Colour stays ground-aware: these sit on white, grey and black. */
.kn-eyebrow,
.kn-card__eyebrow,
.section-label { color: var(--text-muted); }
.kn-subtle .kn-eyebrow,
.kn-subtle .kn-card__eyebrow,
.kn-subtle .section-label { color: var(--text-muted-aa); }
.kn-invert .kn-beat__eyebrow,
.kn-invert .kn-eyebrow,
.kn-invert .section-label { color: var(--text-on-invert-muted); }
.kn-eyebrow { margin-bottom: var(--space-md); }

/* --- LEAD PARAGRAPHS ------------------------------------------
   The paragraph directly under a page or section title ran at 23px
   on the Keynote pages and 18px on blog / promotions / areas /
   contact, with two different line-heights. One size. */
.kn-sub,
.section-subtitle,
.promo-hero-sub,
.blog-hero-sub,
.contact-hero-sub,
.pd-hero__sub {
  font-size: var(--text-lead);
  line-height: var(--lh-lead);
  font-weight: 400;
}

/* --- PAGE TITLES ----------------------------------------------
   The 48px tier carried two different letter-spacings (-0.96px on
   most pages, -1.44px on the rental and area pages). */
main > section[class*="hero"] h1,
#main > section[class*="hero"] h1 {
  letter-spacing: var(--tracking-kn-h2);
}

/* --- PAGE TITLE TIER ------------------------------------------
   Four sizes were live for the same role: 48px on ten inner pages,
   67.2px on the product heroes, 32px on 404, and 96px on the home
   hero. Only ONE of those is a different role - the home hero is
   the display piece. Everything else is a page title and takes the
   page-title tier.
   .kn-hero--still is excluded by name so the home hero keeps the
   display scale. */
main > section[class*="hero"]:not(.kn-hero--still) h1,
#main > section[class*="hero"]:not(.kn-hero--still) h1,
.error-page .error-title {
  font-size: var(--text-page-title);
  font-weight: 700;
  letter-spacing: var(--tracking-kn-h2);
  line-height: var(--lh-kn-h2);
}

/* --- Muted copy on the quiet grey ------------------------------
   --text-muted is 4.66:1 on white but 4.37:1 on --surface-subtle,
   so any muted text sitting on the grey needs the AA-safe value.
   These are the remaining places a site-wide sweep found it. */
.contact-form-section .section-subtitle,
.product-preview-placeholder,
.kn-subtle .section-subtitle,
.kn-subtle .product-tagline { color: var(--text-muted-aa); }


@media (max-width: 768px) {
  /* The beat dots sat 7rem up from the bottom, which is where the product now
     is on a phone - they were landing on top of it. Below it instead. */
  .kn-scents .kn-dots { bottom: var(--kn-dots-bottom-scent-m); }
}


@media (max-width: 768px) {
  /* 72px of card padding each side left only 206px of a 350px card for the
     fields themselves. The generous padding is a desktop luxury. */
  .contact-form { padding: var(--form-card-pad-m); }
}

/* --- Out of stock -----------------------------------------------------
   Amber, not grey: this is temporary, unlike `.discontinued`. The doubled
   class is load-bearing here for the same reason as the tag rules above —
   `.pd-hero__badge.pd-hero__badge` scores (0,2,0) and a single class would
   lose to it no matter how late it loads. */
.pd-hero__badge--oos.pd-hero__badge--oos {
  background: var(--color-warning-bg-solid);
  color: var(--color-warning-text);
  border-color: var(--color-warning);
}

/* Home marquee card: the runway is hand-authored HTML, so the JS tag
   renderer never sees it — the marker has to be in the markup. */
.kn-card__eyebrow .kn-oos {
  color: var(--color-warning-text);
  font-weight: 600;
}
