/* ==========================================================================
   Affordable Solar
   Decisions are documented in docs/design-direction.md.
   Order: fonts, tokens, reset, primitives, sunray, sections, components.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS  (self-hosted variable woff2, no network requests)
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Barlow';
  src: url('../assets/fonts/barlow-latin-400.woff2') format('woff2');
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow';
  src: url('../assets/fonts/barlow-latin-500.woff2') format('woff2');
  font-style: normal;
  font-weight: 500;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow';
  src: url('../assets/fonts/barlow-latin-600.woff2') format('woff2');
  font-style: normal;
  font-weight: 600;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow';
  src: url('../assets/fonts/barlow-latin-700.woff2') format('woff2');
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow';
  src: url('../assets/fonts/barlow-latin-600-italic.woff2') format('woff2');
  font-style: italic;
  font-weight: 600;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow Condensed';
  src: url('../assets/fonts/barlow-condensed-latin-600.woff2') format('woff2');
  font-style: normal;
  font-weight: 600;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow Condensed';
  src: url('../assets/fonts/barlow-condensed-latin-700.woff2') format('woff2');
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

@font-face {
  font-family: 'Barlow Condensed';
  src: url('../assets/fonts/barlow-condensed-latin-800.woff2') format('woff2');
  font-style: normal;
  font-weight: 800;
  font-display: swap;
}

@font-face {
  font-family: 'Geist Mono';
  src: url('../assets/fonts/geist-mono-latin-var.woff2') format('woff2');
  font-weight: 400 600;
  font-display: swap;
}

@font-face {
  font-family: 'Geist';
  src: url('../assets/fonts/geist-latin-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   2. TOKENS
   One accent (--sun) and one ground, walked from broken white at the top of
   the page to a greyish white at the foot of it.
   -------------------------------------------------------------------------- */

:root {
  /* Opt out of browser-forced dark mode. NOT a style preference.

     Samsung Internet and Chrome on Android will algorithmically re-colour any
     page that does not declare a scheme, and this page is a designed day/night
     gradient: a light paper ground with deliberately dark sections. Forced
     inversion turns the footer into low-contrast grey on charcoal and lets the
     solar-cell texture dominate everything. Reported by the client on 17 Sep
     2026 — broken on his phone, fine on his tablet, because the setting is
     per-device.

     `only light` is the strong form. Plain `light` is advisory and Chromium may
     still force-dark a page that offers no dark theme of its own; `only`
     withdraws consent. Reproduced and verified with
     --enable-features=WebContentsForceDark. */
  color-scheme: only light;

  /*
    The ground. The page used to open in the dark and earn its daylight; it now
    opens light and simply settles, so the arc is carried by tone alone. These
    four stops are the gradient laid over the whole document, not per-section
    fills, which is why there is no banding between sections.
  */
  --paper: #f7f6f0;   /* broken white, shared exactly by the process films */
  --paper-2: #f2f0ea;
  --mist: #eae9e4;
  --mist-2: #e0dfd9;  /* greyish white, the foot of the page */

  --surface: #fffefb; /* raised surfaces: cards, the sticky bar */

  --ink: #16201f;
  --ink-dim: #545c5a;

  /*
    The accent. Locked.
    --sun is a fill and a highlight, never text: on these grounds it measures
    around 1.6:1. --sun-ink is the same accent taken down to a legible value
    for text and icons.
  */
  --sun: #ffb23f;
  --sun-hot: #ffd27a;
  --sun-deep: #e08a1e;
  --sun-ink: #7d4a03;

  /* Radius scale, locked: pill for interactive, 16 for surfaces, 10 for inputs */
  --r-pill: 999px;
  --r-surface: 16px;
  --r-input: 10px;

  /*
    Type scale.
    Display is capped at 3.55rem, not larger: the hero headline is seven
    words, and font size was planned against the copy column width so it
    lands on two lines. A four-line hero headline is a font-size error.
  */
  --t-display: clamp(2.15rem, 4vw, 3.55rem);
  --t-h2: clamp(1.9rem, 3.6vw, 3.1rem);
  --t-h3: clamp(1.25rem, 2vw, 1.6rem);
  --t-body: clamp(1rem, 1.15vw, 1.125rem);
  --t-small: 0.875rem;

  /* Rhythm. VISUAL_DENSITY 3, so sections breathe. */
  --sp-section: clamp(6rem, 13vh, 11rem);
  --shell: 1320px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Layer scale. Documented so nobody reaches for a random z-50. */
  --z-page-light: 0;   /* the solar wash and the cell grid, behind everything */
  --z-glow: 1;
  --z-content: 2;
  --z-nav: 50;
  --z-dock: 60;
  --z-grain: 70;

  /* Resolved once, since there is only one theme now. */
  --bg: transparent;
  --bg-2: var(--surface);
  --fg: var(--ink);
  --fg-dim: var(--ink-dim);
  --rule: rgb(22 32 31 / 0.14);
  --lit-dim: var(--ink-dim);
  --lit-full: var(--ink);
  --accent-text: var(--sun-ink);
}

/* --------------------------------------------------------------------------
   3. RESET + BASE
   -------------------------------------------------------------------------- */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The nav is fixed and ~69px tall. Without this every in-page jump parks the
     target section's own heading underneath the bar — the visitor taps "How it
     works" and lands on the paragraph under a heading they never saw. Slightly
     more than the bar so the heading arrives with air above it. */
  scroll-padding-top: clamp(5rem, 9vh, 6.5rem);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  font-family: 'Barlow', system-ui, -apple-system, sans-serif;
  font-size: var(--t-body);
  line-height: 1.6;
  /* One gradient across the whole document, so the page drifts from broken
     white to greyish white as you read and no section edge shows a seam. */
  background: linear-gradient(
    180deg,
    var(--paper) 0%,
    var(--paper-2) 34%,
    var(--mist) 72%,
    var(--mist-2) 100%
  );
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: `hidden` makes body a scroll container and can disturb
     ScrollTrigger measurements in the process story. */
  overflow-x: clip;
}

/* --------------------------------------------------------------------------
   4b. THE SOLAR GROUND

   Two fixed layers behind the whole document. Fixed rather than scrolling, and
   `pointer-events: none`, so they composite once and never repaint with the
   page. Both sit at z-index 0; every section is `position: relative` and comes
   later in the DOM, so content paints over them without needing its own index.
   -------------------------------------------------------------------------- */

/* The wash. The page gradient underneath still carries the sunrise from broken
   white down to grey; this lays warmth over it, strongest at the top right
   where the hero sun sits, gone by the time you reach the footer.

   Deliberately weak. The amber accent has to stay the loudest warm thing on the
   page - if the ground itself is amber, the CTA stops being a signal and
   becomes the same colour as everywhere else. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-page-light);
  pointer-events: none;
  background:
    radial-gradient(72% 55% at 78% 8%, rgb(255 178 63 / 0.10) 0%, transparent 62%),
    radial-gradient(60% 45% at 12% 34%, rgb(255 200 120 / 0.05) 0%, transparent 68%);
}

/* The cell grid. Invisible until the pointer arrives: the mask is a circle at
   the cursor, so cells only exist where the light is.

   This is why it is not decoration. A hairline grid drawn across a page to make
   it "feel designed" is a tell; a photovoltaic cell that lights up when light
   falls on it is the product doing its job. Nothing shows until you move, and
   nothing is left behind when you leave. */
.solar-grid {
  position: fixed;
  inset: 0;
  z-index: var(--z-page-light);
  pointer-events: none;

  /* A real photovoltaic module: DARK cells, bright metallisation, white
     backsheet between them.

     The first two attempts drew the panel in amber, which is backwards. A panel
     is not made of light, it is the thing light lands on - it sits dark until
     the sun reaches it. Drawing it dark and letting the glow arrive separately
     is both more realistic and a better idea, because now the interaction means
     something: the cells under a block light up when you reach it.

     Cell fill is the darkest element on the layer and therefore the one that
     governs text contrast. See the measurement table in design-direction.md
     before raising it. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56'%3E%3Cpath d='M11,2 L45,2 L54,11 L54,45 L45,54 L11,54 L2,45 L2,11 Z' fill='%23222a2e' fill-opacity='0.055'/%3E%3Crect x='2' y='6' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='11' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='16' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='21' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='26' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='31' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='36' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='41' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='46' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='2' y='51' width='52' height='0.45' fill='%23ffffff' opacity='0.26'/%3E%3Crect x='14.35' y='2' width='1.3' height='52' fill='%23ffffff' opacity='0.55'/%3E%3Crect x='27.35' y='2' width='1.3' height='52' fill='%23ffffff' opacity='0.55'/%3E%3Crect x='40.35' y='2' width='1.3' height='52' fill='%23ffffff' opacity='0.55'/%3E%3C/svg%3E");
  background-size: 56px 56px;

  /* Faded to the bottom. The page gradient already darkens as it descends, so
     holding the panel at full strength there would push body copy under AA at
     exactly the point the ground is greyest. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / 0.45) 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / 0.45) 100%);
}

/* ---- the glow --------------------------------------------------------------
   Light arriving on the cells under a block. The wash is translucent, so the
   dark module underneath shows THROUGH it rather than being covered: the cells
   do not disappear, they warm up. That is the whole effect.

   Only text blocks. Blocks carrying a photograph would hide the panel anyway,
   and lighting one would just tint the image. */
.trust__item,
.faq__item,
.dark-beat__col {
  border-radius: var(--r-surface);
  transition: background-color 0.45s var(--ease), background-image 0.45s var(--ease);
}

/* Lit. Two ways in, depending on what the device can do.

   With a mouse, hovering the block lights it. On touch there is no hover, and
   `:hover` on a touch device only resolves on TAP - so the glow sat there
   waiting to be poked, which nobody does to a paragraph. On touch these light
   on scroll instead: reached, lit, and they stay lit as you pass. Same end
   state, reached the way the device actually works.

   One declaration for both so the two paths can never drift apart. */
.trust__item[data-lit='true'],
.faq__item[data-lit='true'],
.dark-beat__col[data-lit='true'] {
  background-image: radial-gradient(
    120% 130% at 20% 0%,
    rgb(255 178 63 / 0.22) 0%,
    rgb(255 178 63 / 0.10) 45%,
    transparent 78%
  );
}

@media (hover: hover) and (pointer: fine) {
  .trust__item:hover,
  .faq__item:hover,
  .dark-beat__col:hover {
    background-image: radial-gradient(
      120% 130% at 20% 0%,
      rgb(255 178 63 / 0.22) 0%,
      rgb(255 178 63 / 0.10) 45%,
      transparent 78%
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .trust__item,
  .faq__item,
  .dark-beat__col {
    transition: none;
  }
}

/* The panel itself is static, so it stays for touch and for reduced motion.
   Hiding it there was correct when it was a cursor-tracked light; a background
   texture that never moves is not motion and there is nothing to spare anyone
   from. Only the glow is interactive, and that is already hover-gated. */

img,
svg,
video {
  display: block;
  max-width: 100%;
}

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

h1,
h2,
h3 {
  font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  font-weight: 700;
  letter-spacing: -0.018em;
  line-height: 0.98;
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

:focus-visible {
  outline: 2px solid var(--sun);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Premium Keyboard Focus Rings (Emil Kowalski style) */
.btn:focus-visible,
.nav__links a:focus-visible,
.nav__brand:focus-visible,
.faq__q:focus-visible {
  outline: none !important;
}

.btn:focus-visible {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
}

.nav__brand:focus-visible {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
  border-radius: 6px;
}

.nav__links a:focus-visible {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
  border-radius: 4px;
}

.faq__q:focus-visible {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
  border-radius: 6px;
}

::selection {
  background: var(--sun);
  color: #16201f;
}

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

.skip-link {
  position: absolute;
  top: -60px;
  left: var(--gutter);
  z-index: 100;
  padding: 0.7rem 1.2rem;
  background: var(--sun);
  color: #16201f;
  font-weight: 600;
  border-radius: var(--r-pill);
  transition: top 0.2s var(--ease);
}

.skip-link:focus {
  top: 1rem;
}

/* --------------------------------------------------------------------------
   4. LAYOUT PRIMITIVES
   -------------------------------------------------------------------------- */

.sec {
  position: relative;
  background: transparent;
  color: var(--fg);
  padding-block: var(--sp-section);
  overflow: clip;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: var(--z-content);
}

.measure {
  max-width: 62ch;
}

.eyebrow {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-bottom: 1.25rem;
}

.lede {
  font-size: clamp(1.05rem, 1.5vw, 1.35rem);
  color: var(--fg-dim);
  max-width: 54ch;
}

/* --------------------------------------------------------------------------
   5. THE SUNRAY
   Light sweeps across the letters as a block enters the viewport.
   Default state is FULLY LIT so that no-JS and reduced-motion readers get
   readable text. JS pulls it back to dim, then scrubs it to lit on scroll.
   -------------------------------------------------------------------------- */

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .lit {
    background-image: linear-gradient(
      100deg,
      var(--lit-dim) 0%,
      var(--lit-dim) 38%,
      var(--sun-ink) 50%, /* was --sun-hot: a highlight on the old dark
                             ground, but ~1.4:1 on broken white, which put
                             text under AA at the crest of the sweep */
      var(--lit-full) 62%,
      var(--lit-full) 100%
    );
    background-size: 300% 100%;
    background-position: 100% 0; /* lit by default */
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }

  /* Descender clearance, since the clip box crops tight */
  .lit {
    padding-bottom: 0.12em;
  }

  /*
    Accent variant: resolves to the accent rather than the body colour, for
    the one figure on the page that IS the argument. Without this, .lit would
    override the amber and the stat would land plain white.
  */
  .lit--accent {
    /* 3.1:1 at its palest against the greyest stop of the page gradient, so
       the 136px figure clears the large-text floor for its whole travel. */
    --lit-dim: #ad6b18;
    --lit-full: var(--sun-ink);
  }
}

@media (prefers-reduced-motion: reduce) {
  .lit {
    background-position: 100% 0 !important;
  }
}

/* The volumetric beam behind a lit block. Purely atmospheric, never on top
   of text, never intercepting pointer events. */
.godray {
  position: absolute;
  inset: -30% -10%;
  z-index: var(--z-glow);
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    58% 40% at 78% 8%,
    rgb(255 178 63 / 0.12) 0%,
    rgb(255 178 63 / 0.04) 38%,
    transparent 72%
  );
}

/* --------------------------------------------------------------------------
   6. BUTTONS
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 1.6rem;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-weight: 600;
  font-size: 0.975rem;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.18s var(--ease), background-color 0.18s var(--ease),
    border-color 0.18s var(--ease), box-shadow 0.18s var(--ease);
}

.btn svg {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: currentColor;
}

/* Primary: amber on any ground. Text is always near-black for AA. */
.btn--primary {
  background: var(--sun);
  color: #16201f;
  box-shadow: 0 6px 24px -8px rgb(255 178 63 / 0.6);
}

.btn--primary:hover {
  background: var(--sun-hot);
  transform: translateY(-2px);
}

.btn--primary:active {
  transform: translateY(0) scale(0.96);
  transition: transform 0.08s var(--ease);
}

/* Secondary: outlined, inherits the section's foreground */
.btn--ghost {
  background: transparent;
  color: var(--fg);
  border-color: var(--rule);
}

.btn--ghost:hover {
  border-color: var(--sun-deep);
  color: var(--sun-ink);
  transform: translateY(-2px);
}

.btn--ghost:active {
  transform: translateY(0) scale(0.96);
  transition: transform 0.08s var(--ease);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-top: 2.25rem;
}

/* The bar itself.

   These two rules were missing entirely - every child style existed but the
   container did not, so the header rendered as a static block and the brand,
   links and CTA stacked vertically over the top of the hero. The child rules
   still refer to "its 68px height", which is what it was written against. */
.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: var(--z-nav);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease),
              box-shadow 0.35s var(--ease);
}

/* The read-progress line.

   Not decoration: this page is twelve screens on a phone, and the single most
   useful thing a fixed bar can tell someone on a long scroll-story is how much
   of it is left. It sits on the bar's bottom edge, so it doubles as the rule
   that separates the bar from the page once it has a surface.

   Hidden until the bar earns its surface — a progress line over the hero would
   be reporting on a journey nobody has started. */
.nav__progress {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(var(--read, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--sun), var(--sun-deep));
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
}

.nav[data-stuck='true'] .nav__progress {
  opacity: 1;
}

/* Set by scroll-story.js once the visitor is 80px down. The bar starts
   invisible over the hero and only earns a surface once it overlaps content. */
.nav[data-stuck='true'] {
  background: rgb(255 254 251 / 0.88);
  backdrop-filter: saturate(1.4) blur(12px);
  -webkit-backdrop-filter: saturate(1.4) blur(12px);
  border-bottom-color: var(--rule);
  box-shadow: 0 1px 20px rgb(22 32 31 / 0.05);
}

.nav__inner {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
  min-height: 68px;
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
}

/* The links take the slack so the brand sits left and the CTA sits right. */
.nav__inner > nav {
  margin-left: auto;
}

.nav__brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--ink);
  font-size: 1.02rem;
  /* The nav is a single line at every width. A wrapped brand pushes the bar
     past its 68px height and reads as a layout bug. */
  white-space: nowrap;
  transition: color 0.35s var(--ease), transform 0.15s var(--ease);
}

.nav__brand:active {
  transform: scale(0.97);
  transition: transform 0.08s var(--ease);
}

.nav__brand .nav__mark {
  transition: transform 0.6s var(--ease);
}

.nav__brand:hover .nav__mark {
  transform: rotate(45deg);
}

/* The brand mark is the cell, and it is coloured by `currentColor` on the
   polygon so it can sit on paper or on ink without a second copy of the file.
   --brand-cut is the disc and busbar knocked out of it: the page ground, not
   black, so the mark reads as a cell with light behind it rather than a sticker
   with holes. */
/* The client's own sun, with the wordmark set in the site's typeface beside
   it rather than baked into the artwork.

   Their logo is a wide lockup — sun, panel and wordmark together, 2.8:1. Used
   whole in the bar it would be tiny, and its wordmark is a different typeface
   from everything else on the page, so the two would visibly disagree three
   centimetres apart. The sun is the distinctive half and the half that still
   reads at 16px; the words are already handled. So the lockup goes in the
   footer where it has room, and the nav takes the sun alone.

   Full opacity, deliberately. A faded logo reads as a watermark or as a
   placeholder nobody replaced — the one treatment that makes a brand look
   unsure of itself, on a page whose argument is that this company is the
   credible one. If it is too loud the answer is size, not transparency. */
.nav__mark {
  width: 34px;
  height: 34px;
  flex: none;
  object-fit: contain;
}

.nav__links {
  display: flex;
  align-items: center;
  gap: 1.9rem;
  list-style: none;
  padding: 0;
}

.nav__links a {
  display: inline-block;
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9rem;
  opacity: 0.72;
  transition: opacity 0.2s var(--ease), color 0.35s var(--ease), transform 0.15s var(--ease);
}

.nav__links a:hover {
  opacity: 1;
  color: var(--sun-ink);
}

.nav__links a:active {
  transform: scale(0.95);
  transition: transform 0.08s var(--ease);
}

.nav__cta {
  padding: 0.62rem 1.15rem;
  font-size: 0.875rem;
}

/* The glyph rides along at every width but only earns its keep below 400px,
   where it becomes the whole button. */
.nav__cta-icon {
  width: 1.05rem;
  height: 1.05rem;
  fill: currentColor;
  flex: none;
  margin-right: 0.45rem;
}

@media (max-width: 900px) {
  .nav__links {
    display: none;
  }
}

/* Both the brand and the CTA have to share one line on a 390px phone. */
@media (max-width: 560px) {
  .nav__brand {
    font-size: 0.92rem;
    gap: 0.45rem;
  }

  /* 22px was right for a flat geometric mark. The client's sun is a detailed
     render — a face, sunglasses, rays and a panel behind it — and at 22 those
     details turn to mush. 27 is the smallest size at which it still reads as
     what it is, and the bar has room for it. */
  .nav__mark {
    width: 27px;
    height: 27px;
  }

  .nav__cta {
    padding: 0.55rem 0.9rem;
    font-size: 0.8rem;
  }
}

/* Below 400px the two do NOT share a line, and pretending otherwise pushed the
   CTA 36px off the right edge of a 320px screen — the primary conversion
   control, cut in half, on the smallest phone people still carry.

   The label goes and the glyph stays. Shortening the brand instead was the
   other option and it is the wrong one: the company has exactly one name now
   and the top of the page is where it is stated. A WhatsApp mark on a sun-
   yellow pill is not ambiguous. */
@media (max-width: 400px) {
  .nav__cta {
    width: 44px;
    padding: 0;
    justify-content: center;
  }

  .nav__cta-icon {
    margin-right: 0;
    width: 1.25rem;
    height: 1.25rem;
  }

  .nav__cta-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* --------------------------------------------------------------------------
   8. HERO  (asymmetric split, night)
   -------------------------------------------------------------------------- */

.hero {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-top: 6rem; /* cap: never more than pt-24 equivalent */
  padding-bottom: 3rem;
  background: transparent;
  color: var(--ink);
  position: relative;
  overflow: clip;
}

.hero__grid {
  display: grid;
  /* The illustration is the hero's visual argument, and it is a WIDE
     architectural view: the building measures 720 x 299 in its own viewBox, an
     aspect of 2.4:1. Fitted to a column, its height is decided entirely by that
     column's width, which is why the split matters more here than it looks.

     At 1fr / 1.16fr the drawing came out 622 x 326 in a 900px hero - 15.6% of
     the frame, with ~290px of dead space above and below it. It did not read as
     a small illustration, it read as a house a long way away. The column is
     wider now and the stage bleeds past the shell on the right. */
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.32fr);
  align-items: center;
  gap: clamp(1rem, 2.4vw, 2.25rem);
  width: 100%;
}

.hero__title {
  font-weight: 800;
  font-size: var(--t-display);
  margin-bottom: 1.5rem;
}

.hero__title em {
  font-family: 'Barlow', system-ui, sans-serif;
  font-style: italic;
  font-weight: 600;
  letter-spacing: -0.045em;
  line-height: 1.1;
  color: var(--sun-ink);
  /* The h1 carries `.lit`, which sets -webkit-text-fill-color: transparent so
     the gradient shows through the glyphs - and that inherits. Without an
     explicit fill here the accent would be clipped to the parent's sweep and
     lose its amber entirely. Resting state is the accent; the hero timeline
     animates it up from --ink so the colour reads as arriving with the light. */
  -webkit-text-fill-color: var(--sun-ink);
}

/* `.lit` adds descender padding for its tight clip box. On the display-size h1
   that lands as a visible gap above the sub, so it is taken back off here. */
.hero__title.lit {
  padding-bottom: 0;
}

.hero__sub {
  font-size: clamp(1.05rem, 1.4vw, 1.25rem);
  color: var(--ink-dim);
  max-width: 46ch;
}

/* Stage for the SVG choreography */
/* The stage escapes the shell to the right.

   A house that stops neatly inside a container reads as a picture of a house. A
   house that runs past the edge of the frame reads as a house you are standing
   near, because that is what proximity does to a subject: it stops fitting. The
   bleed is the cheapest available depth cue and it costs no artwork.

   Reaching the viewport edge takes TWO distances, and using only the first is
   the easy mistake: the shell's inner `padding-inline` (the gutter) AND the
   outer margin the centred shell leaves once the viewport is wider than its
   1320px cap. Bleeding by the gutter alone stops the drawing exactly where
   every other section's content stops, which reads as alignment, not as a
   bleed. */
.stage {
  --stage-bleed: calc(var(--gutter) + max(0px, (100vw - var(--shell)) / 2));
  position: relative;
  width: calc(100% + var(--stage-bleed));
  margin-right: calc(var(--stage-bleed) * -1);
}

.stage svg {
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Night sky behind the house */
.hero__sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
      120% 80% at 72% 18%,
      rgb(255 255 255 / 0.9) 0%,
      transparent 62%
    ),
    radial-gradient(90% 60% at 20% 100%, rgb(222 220 213 / 0.65) 0%, transparent 70%);
}

/* House parts. The scene is generated from a 3D model; these hold the
   materials and the resting appearance, JS drives the assembly.
   Re-lit for daylight: the page no longer opens in the dark, so the
   house is a plastered wall in sun rather than a silhouette. The night
   sky that went with the old dark hero is gone entirely - markup, CSS
   and tweens - rather than left hidden. */

.h-shadow {
  fill: url(#gShadow);
}

.h-kerb {
  stroke: #1d4553;
  stroke-width: 1.4;
  fill: none;
  opacity: .7;
}

.h-wall {
  stroke: none;
}

.h-plinth {
  fill: #b6b0a5;
  opacity: .95;
}

.h-plinth--s {
  fill: #989287;
}

.h-corner {
  stroke: #8d877c;
  stroke-width: 1;
  fill: none;
  opacity: .7;
}

.h-rebate {
  fill: #6f6a62;
}

.h-rib {
  stroke: #5b574f;
  stroke-width: 1;
  fill: none;
  opacity: .75;
}

.h-lintel {
  fill: #efeae2;
}

.h-doorframe {
  fill: #efeae2;
}

.h-sill {
  fill: #f2ede5;
}

.h-winframe {
  fill: #e8e2d9;
}

.h-glass {
  fill: url(#gGlass);
}

.win-set {
  opacity: 0;
}

.win {
  fill: url(#gLit);
}

.win-spill {
  fill: url(#gSpill);
}

.win-mull {
  stroke: #8a5410;
  stroke-width: .9;
  fill: none;
  opacity: .55;
}

.h-beam {
  stroke: #8a8275;
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
  opacity: .85;
}

.h-roof {
  stroke: none;
}

.h-course {
  stroke: #4e555b;
  stroke-width: 1.1;
  fill: none;
  opacity: .38;
}

.h-pan {
  stroke: #4e555b;
  stroke-width: .6;
  fill: none;
  opacity: .16;
}

.h-fascia {
  fill: url(#gFascia);
}

.h-hip {
  stroke: #9aa1a7;
  stroke-width: 1.5;
  fill: none;
  opacity: .85;
  stroke-linecap: round;
}

.h-ridge {
  fill: #8b9299;
}

.h-rim {
  stroke: #f0ece4;
  stroke-width: 1.1;
  fill: none;
  opacity: .75;
  stroke-linecap: round;
}

.h-eave {
  stroke: #b5aea3;
  stroke-width: 1;
  fill: none;
  opacity: .6;
}

.h-chim {
  fill: #b0aa9f;
}

.h-chim--f {
  fill: #cdc7bd;
}

.h-chimcap {
  fill: #7d848a;
}

.h-chimcap--f {
  fill: #949ba1;
}

.h-shrub {
  fill: #8d9a83;
  opacity: .9;
}

.h-rail {
  stroke: #9aa1a6;
  stroke-width: 1.6;
  fill: none;
  opacity: .7;
}

.p-frame {
  stroke: none;
}

.p-glass {
  stroke: none;
}

.p-cell {
  stroke: #5d6d7d;
  stroke-width: .45;
  fill: none;
  opacity: .5;
}

.p-sheen {
  fill: #ffffff;
  opacity: .07;
}

.p-edge {
  stroke: #ffb23f;
  stroke-width: 1;
  fill: none;
  opacity: .5;
}

.h-inv {
  fill: #eae5dc;
  stroke: #9a948e;
  stroke-width: 1;
}

.h-inv-face {
  fill: #c3bdb2;
}

.h-led {
  fill: #ffb23f;
  opacity: 1;
}

.h-cable {
  stroke: #a4650f;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .9;
}
/* Bloom that spreads when the panel connects */
.hero__bloom {
  position: absolute;
  z-index: 0;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    42% 42% at 66% 34%,
    rgb(255 178 63 / 0.15) 0%,
    transparent 68%
  );
}

@media (max-width: 880px) {
  .hero__grid {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }

  /* No bleed once the grid stacks. At this width the drawing already spans the
     full column, so pushing it past the gutter only crops the house. */
  .stage {
    width: 100%;
    margin-right: 0;
  }

  .hero {
    padding-top: 5.5rem;
  }

  .stage {
    order: -1;
    max-width: 620px;
    margin-inline: auto;
  }
}

/* --------------------------------------------------------------------------
   9. THE DARK  (editorial full-bleed, night)
   -------------------------------------------------------------------------- */

.dark-beat__stat {
  font-family: 'Geist Mono', monospace;
  font-size: clamp(3.4rem, 11vw, 8.5rem);
  font-weight: 500;
  /* Mono earns its place by being mono; -0.05em was closing the counters up
     enough to lose that. Just enough to stop display size feeling loose. */
  letter-spacing: -0.025em;
  line-height: 0.92;
  color: var(--sun-ink);
  display: block;
}

.dark-beat__lead {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
  gap: clamp(2.5rem, 7vw, 7rem);
  align-items: end;
}

.dark-beat__evidence {
  min-width: 0;
}

.dark-beat__answer {
  padding-top: clamp(1.5rem, 3vw, 2.5rem);
  border-top: 1px solid var(--rule);
}

.dark-beat__answer svg {
  width: 2rem;
  height: 2rem;
  margin-bottom: 1.25rem;
  fill: var(--sun-deep);
}

.dark-beat__answer h3 {
  max-width: 11ch;
  margin-bottom: 1rem;
  font-size: clamp(2rem, 4vw, 3.35rem);
  line-height: 0.96;
}

.dark-beat__answer p {
  max-width: 38ch;
  color: var(--fg-dim);
}

.dark-beat__title {
  font-weight: 800;
  font-size: var(--t-h2);
  margin-block: 1.5rem 1.75rem;
  max-width: 20ch;
}

.dark-beat__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(1.75rem, 4vw, 3.5rem);
  margin-top: clamp(3rem, 7vw, 5.5rem);
  padding-top: 2.5rem;
  border-top: 1px solid var(--rule);
}

.dark-beat__col h3 {
  font-size: 1.05rem;
  margin-bottom: 0.6rem;
}

.dark-beat__col p {
  color: var(--fg-dim);
  font-size: 0.975rem;
}

/* --------------------------------------------------------------------------
   10. THE SHIFT  (ownership explanation, day)
   -------------------------------------------------------------------------- */

.shift__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
}

.shift__grid--copy-only {
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  align-items: start;
}

.shift__grid--copy-only .shift__list {
  margin-top: 0;
}

.shift__title {
  font-weight: 700;
  font-size: var(--t-h2);
  margin-bottom: 1.5rem;
}

.shift__list {
  list-style: none;
  padding: 0;
  margin-top: 2.25rem;
  display: grid;
  gap: 1.1rem;
}

.shift__list li {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
}

.shift__list svg {
  width: 1.35rem;
  height: 1.35rem;
  flex: none;
  fill: var(--accent-text);
  margin-top: 0.15rem;
}

/* Fixed-ratio image slots. Swapping a photo never requires touching CSS. */
.slot {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-surface);
  background: color-mix(in srgb, var(--fg) 8%, var(--bg));
}

.slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.slot--4x5 {
  aspect-ratio: 4 / 5;
}

.slot--3x4 {
  aspect-ratio: 3 / 4;
}

.slot--4x3 {
  aspect-ratio: 4 / 3;
}

.slot--16x9 {
  aspect-ratio: 16 / 9;
}

.slot--3x2 {
  aspect-ratio: 3 / 2;
}

/*
  Empty-slot state. Reads as a deliberate reservation rather than a broken
  image, and prints which photo belongs there. Disappears the moment an
  <img> is dropped in, with no CSS change required.
*/
.slot:empty {
  border: 1px dashed var(--rule);
}

.slot:empty::after {
  content: attr(data-slot);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 1rem;
  text-align: center;
  font-family: 'Geist Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

@media (max-width: 880px) {
  .dark-beat__lead,
  .shift__grid {
    grid-template-columns: 1fr;
  }

  .dark-beat__answer h3 {
    max-width: 15ch;
  }
}

/* --------------------------------------------------------------------------
   12. HOW IT WORKS  (cumulative process film)
   Each shot plays once and remains complete. Nothing is replaced or dimmed.
   -------------------------------------------------------------------------- */

/* The steps get a narrower measure than the section header above them, but
   they stay on the page's left edge. Narrowing the whole section instead
   pushed its left margin from 60px to 256px, and a jog in the alignment line
   as you scroll between sections reads as a mistake rather than as a device. */
.process-film {
  margin-top: clamp(3rem, 6vw, 5rem);
  max-width: 1060px;
}

/* The stage is pinned and the four acts are stacked inside it, so only one
   scene occupies the frame at a time. Height is reserved here rather than by
   the tallest act, because acts are absolutely positioned and contribute
   nothing to flow - without this the pinned box would collapse. */
.process-stage {
  position: relative;
  min-height: min(78vh, 700px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.process-stage__acts {
  position: relative;
  flex: 1 1 auto;
  min-height: min(64vh, 560px);
  /* The gate. Scenes travel a long way in and out, and this crops them at the
     frame edge so a leaving scene is clipped rather than left hanging over
     the rail. Without it the travel needed to stop two drawings ghosting
     would push them into the copy and the act rail.

     clip-path rather than overflow, because the gate must close vertically
     and stay open horizontally: overflow:hidden also cropped the left edge,
     which ate the leading "I" of INSPECTION where the word sits flush to the
     shell. Negative inset on the horizontal axis keeps that side unclipped. */
  clip-path: inset(0 -25vw);
}

.process-shot {
  position: absolute;
  inset: 0;
  display: grid;
  /* The visual column is deliberately the smaller of the two, and the gap is
     tight. At 1fr/1.05fr with a 4.5rem gutter the illustration read as a
     portrait of two workers sitting near some text; the subject of each step is
     the ACTION, not the people performing it. Pulled in close, the drawing
     reads as the thing the sentence is describing. */
  grid-template-columns: minmax(0, 0.78fr) minmax(300px, 1.22fr);
  gap: clamp(1rem, 2.2vw, 2.25rem);
  align-items: center;
  /* Every act is painted; the timeline decides which is visible. Opacity
     rather than visibility, so the copy stays in the accessibility tree. */
  opacity: 0;
  will-change: opacity, transform;
}

.process-shot__layer {
  display: contents;
}

/* ---- act rail ---- */
.process-rail {
  position: relative;
  z-index: 6;
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(0.5rem, 1.4vw, 1.1rem);
  margin: clamp(1.5rem, 3vh, 2.75rem) 0 0;
  padding: 0;
  list-style: none;
}

.process-rail__step {
  position: relative;
  padding-top: 0.7rem;
  border-top: 1px solid rgb(22 32 31 / 0.14);
  font-family: 'Geist Mono', monospace;
  font-size: 0.66rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-dim);
  transition: color 0.35s ease;
}

.process-rail__step[data-on='true'] {
  color: var(--sun-ink);
}

.process-rail__fill {
  position: absolute;
  inset: -1px auto auto 0;
  height: 2px;
  width: 0%;
  background: var(--sun-deep);
  border-radius: 2px;
}

/* ---- reduced motion / no-JS: unstack into the original readable flow ---- */
.process-film[data-static='true'] .process-stage {
  display: block;
  min-height: 0;
}

/* ---- the cell's frame, and the light that leaves it ------------------------

   `.process-act` exists for one reason: `.process-shot` is clipped to the
   chamfer, and clip-path clips an element's pseudo-elements too. Anything drawn
   for the rays on the cell itself is cut off at exactly the edge it is supposed
   to be escaping from. So the rays are painted on a wrapper that is not clipped,
   BEHIND the cell — no mask needed to hollow out the middle, because the cell
   covers it. What you see is only the part that reaches past the edges.

   The two driven properties live here rather than on the cell, and that is not
   cosmetic. Custom properties inherit downward only, so a value written to the
   cell is invisible to its own parent. Both are set on the wrapper by
   scroll-story.js and inherited by the cell.

   THE TRAP, and it has already cost this file once: a default declared on a
   descendant BEATS the inherited value. `--light-pos: 0.5` used to sit on the
   ::after that consumed it and silently overrode everything the script wrote,
   pinning the sweep at 50% forever — it looked implemented and never moved.
   Defaults belong here, at the top of the chain, and nowhere below it. */
/* Registered so it can be TRANSITIONED. A plain custom property jumps between
   values, and the gate below switches 0 to 1 the instant the cell lights: the
   rays snapped on while the cell's own glow was still fading up over 0.9s, and
   the two halves of one event visibly disagreed. Registering it as a number
   lets the browser interpolate.
   Where @property is unsupported this still parses as a normal property and the
   rays simply arrive without the fade — correct, just abrupt. */
@property --ray-gate {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.process-film[data-static='true'] .process-act {
  /* Declared here rather than on the cell so the wrapper's own pseudo-elements
     can cut the same corner. They have to trace the identical silhouette or the
     glow sits proud of the chamfer at the corners. */
  --cell-chamfer: clamp(18px, 2.2vw, 30px);
  /* Where the travelling light has reached, 0 to 1 down the cell. */
  --light-pos: 0.5;
  /* How far forward this cell has come: 0 at the edges of its pass through the
     viewport, 1 at the middle. Default 1 so that if the script never runs the
     cells sit square and lit rather than permanently tilted away and dark. */
  --pop: 1;

  /* 0 until the step's film has played. Rays are a lit cell generating; a cell
     the visitor has not reached yet is dark, and dark cells do not shine. */
  --ray-gate: 0;

  position: relative;
  isolation: isolate;
  transition: --ray-gate 0.9s var(--ease);
}

.process-film[data-static='true'] .process-act:has(.process-shot[data-lit='true']) {
  --ray-gate: 1;
}

/* The rim glow. Two chamfered slabs sitting BEHIND the cell, each a little
   larger than it and blurred, so all that shows is the light leaking out around
   the outline. The cell is opaque, so no mask is needed to hollow the middle.

   A ray fan was tried here first and was wrong: rays radiate from a point and
   read as a starburst pinned behind a rectangle, which fought the flat, panel-
   like geometry of the section. Light on the EDGE of a cell reads as the cell
   itself being hot.

   THE CORNERS ARE ROUNDED, NOT CHAMFERED, and that is a fix rather than a
   compromise. These carried the cell's exact chamfer polygon first, and the
   glow vanished: `clip-path` is applied AFTER `filter`, so the blur spread
   outward and was then clipped straight back to the polygon. The layer measured
   0.90 opacity and put zero warm pixels on the page. Same failure as the mask
   before it — the shape was eating the light.

   Without a clip the blur is free to reach. A rounded rectangle behind a
   chamfered cell is a different silhouette, but under a 10px blur and a 30px
   one the difference is not resolvable, and being wrong in a way nobody can see
   beats being right in a way nobody can see either.

   The blur is a constant and only opacity changes with --pop. That is
   deliberate: animating a filter re-rasterizes the layer every frame, animating
   opacity composites. */
.process-film[data-static='true'] .process-act::before,
.process-film[data-static='true'] .process-act::after {
  content: "";
  position: absolute;
  z-index: 0;
  pointer-events: none;
  border-radius: calc(var(--cell-chamfer) * 0.72);
}

/* The hot edge.

   The slab is barely larger than the cell and the blur does the reach, rather
   than the other way about. A wide slab with a small blur has a solid middle
   and lands as a drawn border in a slightly different colour; a tight slab with
   a wide blur is nearly all falloff, which is what light looks like. An early
   attempt at -5px against a 7px blur read as beige piping. */
.process-film[data-static='true'] .process-act::before {
  inset: -1px;
  background: rgb(255 186 60);
  filter: blur(6px);
  opacity: calc(var(--pop) * var(--ray-gate));
}

/* The wider spill, and it does most of the work.

   This was -8px against a 30px blur and it was too polite to see. The page is
   near-white, and a yellow glow on a near-white ground has very little room to
   register — the version that measured correctly and looked like nothing was
   the version that had been tuned by numbers instead of by looking. Rendered
   three strengths side by side and picked this one: wider reach, deeper orange,
   nearly full opacity.

   The rim does not read as an outline because of this layer. An outline stops;
   light keeps going. */
.process-film[data-static='true'] .process-act::after {
  inset: -14px;
  background: rgb(255 132 12);
  filter: blur(46px);
  opacity: calc(var(--pop) * var(--ray-gate) * 0.95);
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the glow — it is the cell generating, not decoration — but hold it at
     a fixed strength instead of pumping with the scroll. */
  .process-film[data-static='true'] .process-act::before {
    opacity: calc(var(--ray-gate) * 0.65);
  }

  .process-film[data-static='true'] .process-act::after {
    opacity: calc(var(--ray-gate) * 0.4);
  }

  .process-film[data-static='true'] .process-act { transition: none; }
}

/* Cells do not touch. On a real module every cell is separated by a strip of
   backsheet, and butted together here the chamfered corners met and pinched
   into each other, which read as a rendering fault rather than as a panel.
   The gap is the backsheet, and it lets the page's own cell field show through
   between them. */
.process-film[data-static='true'] .process-stage__acts {
  display: grid;
  gap: clamp(0.9rem, 2vw, 1.6rem);
  min-height: 0;
  /* The gate is gone with the pinned stage it belonged to. It cropped acts
     travelling out of a fixed frame; there is no fixed frame now, and left in
     place it clips the top and bottom of any cell that pops toward the viewer. */
  clip-path: none;
  /* Room for the pop to happen in. */
  padding-block: 2px;
}

/* Sized to the content rather than to a viewport fraction. The steps used to
   need a full screen each because they were acts in a pinned film; as blocks in
   flow, a 340px drawing beside four lines of text does not fill 620px, and the
   leftover read as four holes in the page. */
.process-film[data-static='true'] .process-shot {
  position: relative;
  inset: auto;
  opacity: 1;
  min-height: 0;
}

.process-film[data-static='true'] .process-rail {
  display: none;
}

/* ---- each step IS one photovoltaic cell -----------------------------------
   The page background is a field of small cells. A step is one of them blown
   up, with its drawing living inside it: the same object at two scales, which
   is why the section belongs to the page rather than sitting on it.

   The chamfer is the same monocrystalline corner cut as the background tile,
   just larger. Busbars and fingers are stretched to the block rather than
   tiled, because this is one cell scaled up and its metallisation scales with
   it. */
.process-film[data-static='true'] .process-shot {
  position: relative;
  z-index: 1;
  padding: clamp(1.5rem, 3vw, 2.75rem) clamp(1.5rem, 3vw, 2.5rem);
  clip-path: polygon(
    var(--cell-chamfer) 0%,
    calc(100% - var(--cell-chamfer)) 0%,
    100% var(--cell-chamfer),
    100% calc(100% - var(--cell-chamfer)),
    calc(100% - var(--cell-chamfer)) 100%,
    var(--cell-chamfer) 100%,
    0% calc(100% - var(--cell-chamfer)),
    0% var(--cell-chamfer)
  );
  /* Dark silicon, not a white card. A photovoltaic cell is near-black with a
     cool cast, and a near-white surface was the one thing stopping these
     reading as cells at all.

     It also fixes the glow. A warm wash over white barely registers; over dark
     silicon it reads as the cell generating, which is the entire metaphor. */
  /* Faded, not solid. At full opacity this was a dark slab dropped on the page;
     letting the page's own cell field read through makes the big cell feel cut
     from the same panel rather than placed on top of it.

     The base colour is DARKER than the solid version was, which is the part
     that is easy to get wrong. Fading composites the light page in behind, so
     keeping the old value and adding transparency lifted the ground under the
     copy from 0.045 to 0.10 luminance and took the step label to 3.86:1. The
     base is pre-darkened so the composited result lands where the solid one
     did, and the fade costs appearance rather than legibility. */
  background-color: rgb(51 58 63 / 0.93);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Crect x='0' y='5' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='12' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='19' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='26' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='33' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='40' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='47' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='54' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='61' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='68' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='75' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='82' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='89' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='0' y='96' width='100' height='0.3' fill='%23ffffff' opacity='0.035'/%3E%3Crect x='24.72' y='0' width='0.56' height='100' fill='%23ffffff' opacity='0.065'/%3E%3Crect x='49.72' y='0' width='0.56' height='100' fill='%23ffffff' opacity='0.065'/%3E%3Crect x='74.72' y='0' width='0.56' height='100' fill='%23ffffff' opacity='0.065'/%3E%3C/svg%3E");
  background-size: 100% 100%;
  background-repeat: no-repeat;

  /* The frontal pop. The cell tips up to face the viewer and comes forward as it
     crosses the middle of the screen, then settles back as it leaves.

     `perspective()` inside the transform, not `perspective` on the parent. The
     parent is a 2 600px column, so a shared perspective origin at its centre
     would give the top and bottom cells a violently different vanishing point
     from the middle one. Per-element perspective gives each cell its own camera
     and they all read the same.

     Transform and opacity only, so this composites and costs nothing. */
  transform:
    perspective(1100px)
    rotateX(calc((1 - var(--pop)) * 6.5deg))
    translateZ(calc(var(--pop) * 78px));
  transform-origin: 50% 62%;
  transition: background-color 0.6s var(--ease), box-shadow 0.6s var(--ease);
}

/* Unlit. A cell with no light on it is a dark square of silicon. */
.process-film[data-static='true'] .process-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Behind the drawing and the copy. As the last child it otherwise paints on
     top, and amber ribbon drawn ACROSS the installers reads as a fault rather
     than as light: the cell glows behind them, it is not printed over them. */
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.9s var(--ease);
  /* The GRID glows, not the cell.

     This layer is the same busbars and fingers as the base metallisation, drawn
     in amber and masked to the lit area. So the silver ribbon turns gold exactly
     where the light reaches it, which is what actually happens on a panel, and
     the cell face between the ribbons stays dark instead of being washed flat.

     A faint ambient bloom sits under it so the light has somewhere to fall off
     to. It is concentrated LEFT, over the drawing: light should fall on the
     panel and the people working on it, not evenly on a block of text. An even
     wash is a highlight; a falloff is illumination. Keeping it off the copy
     column is also what holds the body text and the step label above AA. */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Crect x='0' y='5' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='12' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='19' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='26' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='33' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='40' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='47' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='54' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='61' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='68' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='75' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='82' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='89' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='0' y='96' width='100' height='0.3' fill='%23ffc46b' opacity='0.3'/%3E%3Crect x='24.72' y='0' width='0.56' height='100' fill='%23ffc46b' opacity='0.62'/%3E%3Crect x='49.72' y='0' width='0.56' height='100' fill='%23ffc46b' opacity='0.62'/%3E%3Crect x='74.72' y='0' width='0.56' height='100' fill='%23ffc46b' opacity='0.62'/%3E%3C/svg%3E"),
    radial-gradient(46% 92% at 11% 34%, rgb(255 178 63 / 0.10) 0%, transparent 70%);
  background-size: 100% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  /* Measured, not guessed: the copy column begins at 39% of the cell width, so
     the light has to be gone before it gets there. At 62% reach the amber
     ribbon was glowing under the body text and took it to 3.08:1. */
  /* The light TRAVELS as you scroll. --light-pos runs 0 to 1 across the cell's
     pass through the viewport, driven by scroll-story.js, and the mask centre
     rides it. The sun does not sit still on a roof, and a highlight that never
     moves is a highlight rather than light.

     It travels VERTICALLY and stays in the left column. Measured, not guessed:
     the copy column begins at 39% of the cell width, so the light has to stay
     clear of that horizontally. Sweeping it sideways would drag amber ribbon
     across the body text; sweeping it down the drawing keeps every frame of the
     travel clear of the words. */
  -webkit-mask-image: radial-gradient(25% 58% at 7% calc(var(--light-pos) * 100%), #000 0%, rgb(0 0 0 / 0.5) 46%, transparent 78%);
  mask-image: radial-gradient(25% 58% at 7% calc(var(--light-pos) * 100%), #000 0%, rgb(0 0 0 / 0.5) 46%, transparent 78%);
}

/* ---- everything inside a dark cell inverts ------------------------------
   This is a dark component on a light page, not a theme flip: the section
   around it, its heading and its lede all stay light. Only the four cells are
   dark, the way a dark feature card is dark. */
.process-film[data-static='true'] .process-shot h3 {
  color: var(--paper);
}

/* Lifted with the cell. Raising the surface two shades took the ground under
   this copy from 0.074 to 0.110 luminance and dropped it to 4.09:1, so the text
   pays the difference back. Still a grey rather than white. */
.process-film[data-static='true'] .process-shot .process-shot__copy > p {
  color: #dfe3e2;
}

.process-film[data-static='true'] .process-shot .process-shot__payoff {
  /* beats the !important on the light-ground rule */
  color: var(--paper) !important;
}

/* The pale accent, not --sun. On the two-shades-lighter cell --sun measured
   3.65:1 against a 4.5 floor - amber is a low-luminance colour and it runs out
   of room against a lifted ground faster than grey text does. --sun-hot is the
   same hue with the headroom. Its hairline uses currentColor and follows. */
.process-film[data-static='true'] .process-shot .process-shot__verb {
  color: var(--sun-hot);
}

/* Dust on dark silicon has to be pale. The brown grain that read as dust on a
   light page is invisible here; what comes off a panel under a work light is a
   bright mote, not a dark speck. */
.process-film[data-static='true'] .process-shot .process-shot__dust i {
  background: radial-gradient(circle at 32% 28%, #ffffff 0%, #e4d9c6 55%, #b9a888 100%);
}

/* The drawing's drop shadow was tuned for a light ground and does nothing on
   dark. A soft light halo separates the cut-out from the cell instead. */
.process-film[data-static='true'] .process-shot .process-shot__canvas,
.process-film[data-static='true'] .process-shot .process-shot__frames {
  filter: drop-shadow(0 0.6rem 1.4rem rgb(0 0 0 / 0.35));
}

/* Lit. The glow arrives with the step's own film, so the cell starts generating
   at the moment the work on screen lands, and stays lit after - the same rule
   as the rest of the section: played once, never undone. */
/* The shadow grows with the lift. A cell that comes forward without its shadow
   opening up reads as a scale, not as depth. */
.process-film[data-static='true'] .process-shot {
  box-shadow:
    0 1px 0 rgb(255 255 255 / 0.08) inset,
    0 calc(10px + var(--pop) * 26px) calc(24px + var(--pop) * 44px)
      calc(-18px + var(--pop) * -4px) rgb(17 26 30 / calc(0.18 + var(--pop) * 0.22));
}

.process-film[data-static='true'] .process-shot[data-lit='true'] {
  background-color: rgb(59 67 73 / 0.93);
  box-shadow:
    0 1px 0 rgb(255 255 255 / 0.10) inset,
    0 calc(10px + var(--pop) * 26px) calc(24px + var(--pop) * 44px)
      calc(-18px + var(--pop) * -4px) rgb(17 26 30 / calc(0.20 + var(--pop) * 0.24)),
    0 20px 50px -32px rgb(224 138 30 / 0.75);
}

.process-film[data-static='true'] .process-shot[data-lit='true']::after {
  opacity: 1;
}

@media (hover: hover) and (pointer: fine) {
  .process-film[data-static='true'] .process-shot:hover {
    background-color: rgb(65 74 80 / 0.94);
  }

  .process-film[data-static='true'] .process-shot:hover::after {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .process-film[data-static='true'] .process-shot,
  .process-film[data-static='true'] .process-shot::after {
    transition: none;
  }
}

.process-shot__visual {
  position: relative;
  /* Clears the glowing grid on the cell's ::after. */
  z-index: 2;
  /* Must track the canvas size below. `.process-shot__scene` inside is
     absolutely positioned and contributes no height, so a container shorter
     than the drawing lets it spill over the copy. */
  min-height: clamp(155px, 14vw, 200px);
  display: grid;
  place-items: center;
  isolation: isolate;
  contain: layout;
  overflow: visible;
}

.process-shot__scene {
  position: absolute;
  inset: 2% 4%;
  z-index: 1;
  display: grid;
  place-items: center;
  overflow: visible;
}

.process-shot__frames,
.process-shot__canvas {
  grid-area: 1 / 1;
  /* Smaller on purpose. A large drawing makes the viewer look at the crew; a
     small one tucked against the sentence makes them look at what the crew is
     doing. This is the whole difference between illustration-as-decoration and
     illustration-as-explanation. */
  width: min(76%, 180px);
  height: auto;
  aspect-ratio: 1;
  max-height: 180px;
  object-fit: contain;
  background: transparent;
  filter: drop-shadow(0 0.9rem 1.35rem rgb(17 38 42 / 0.08));
}

.process-shot__canvas {
  position: relative;
  z-index: 2;
  pointer-events: none;
}

.process-shot__canvas:not([data-ready='true']) {
  visibility: hidden;
}

.process-shot__frames[data-canvas-ready='true'] {
  visibility: hidden;
}

.process-shot__copy {
  position: relative;
  z-index: 5;
  align-self: center;
  margin-left: clamp(-2rem, -2.5vw, -0.75rem);
}

/* ---- the last step breaks the pattern ----------------------------------
   Three rows of drawing-beside-text, then this one stacks: the roof is above
   the description, so the dust the brush lifts falls down onto the words.

   It is the only place on the page where an illustration and the copy touch
   each other, and it is the last thing in the section, which is the right
   place to spend a moment like this. It also stops four identical rows from
   reading as a template. */
/* The cleaning step keeps the same left-drawing / right-copy shape as the
   three before it. It earns its distinction from the dust rather than from a
   different grid: this is the only step where the illustration physically
   reaches into the text.

   An earlier version stacked it, drawing above and copy below, so the dust
   could fall straight down. That was generic gravity - it would look the same
   whatever the worker was doing. The brush strokes to the RIGHT, so the dust
   is thrown right, into the words. Same principle as the label wipe: the
   action causes the copy rather than running beside it. */


/* The dust now falls THROUGH the description rather than sitting behind it.
   z-index above the copy, because the charm is watching it drift over the
   words; it is small, low-opacity and gone in under a second, so it never
   costs legibility. Spans the full width of the copy block so particles can
   land anywhere across the sentence. */
/* The emitter. It overhangs LEFT, back into the drawing, so grains leave the
   brush head rather than materialising in the gap beside it, and it reaches
   the full width of the copy so they can carry across the sentence.

   No `contain` here: containment would clip the overhang at the box edge and
   the grains would appear out of nowhere at the left margin, which is the one
   thing this is trying to avoid. */
.process-shot__dust {
  position: absolute;
  z-index: 6;
  inset: -1rem -1rem auto -46%;
  height: 20rem;
  pointer-events: none;
}

/* Generated by scroll-story.js, which sets size and position per grain.
   Origin sits over the brush head, in the left third of the drawing. */
.process-shot__dust i {
  position: absolute;
  left: 16%;
  top: 40%;
  border-radius: 50%;
  /* Two tones in one dot: dust catches light on top and holds shade beneath,
     which is the difference between a grain and a dot. */
  background: radial-gradient(circle at 32% 28%, #c9a878 0%, #9a7445 55%, #7c5a31 100%);
  opacity: 0;
  will-change: transform, opacity;
}

@media (prefers-reduced-motion: reduce) {
  .process-shot__dust {
    display: none;
  }
}

/* The step label, and the only animated piece of type in this section.

   It lives with the description rather than in a card of its own, and it is
   uncovered by the work on screen: the wipe fires on the physical contact
   point and travels in the direction the action moves. Set as a credit -
   Barlow 500, wide-tracked, uppercase - against the heavy condensed h3 below
   it, which is what stops two labels reading as one lump. */
.process-shot__verb {
  position: relative;
  /* inline-block, not block: the rule above sits on this element's box, and
     a full-column rule over a short label reads as a divider rather than a
     credit baseline. Shrinking to the text keeps the two the same width. */
  display: inline-block;
  margin-bottom: 1.15rem;
  padding-top: 0.75rem;
  color: var(--sun-ink);
  font-family: 'Barlow', sans-serif;
  font-weight: 500;
  font-size: clamp(0.8rem, 1.05vw, 0.95rem);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  line-height: 1;
  /* the wipe is a mask driven from JS; without no-repeat it tiles and the
     label reappears outside the swept band */
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

/* The rule sets the baseline before the name arrives, the way a credit does. */
.process-shot__verb::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0.3em;
  top: 0;
  height: 1px;
  background: currentColor;
  opacity: 0.42;
  /* No separate reveal for the rule: it sits inside the masked element, so the
     one wipe uncovers the rule and the label together as a single sweep. */
}

/* These three measures were tuned for the pinned stage, where the eye never
   moved and a narrow poster-like column read as deliberate. In vertical flow
   the step is a row you scan across, and the same measures left roughly 280px
   of the copy column empty on every one of the four steps. Widened to fill the
   row without going past a comfortable reading line. */
.process-shot__copy h3 {
  max-width: 15ch;
  margin-bottom: 1.15rem;
  font-size: clamp(2.1rem, 3.3vw, 3.35rem);
}

.process-shot__copy > p {
  max-width: 48ch;
  color: var(--fg-dim);
  text-wrap: pretty;
}

.process-shot__payoff {
  margin-top: 1.25rem;
  color: var(--ink) !important;
  font-weight: 600;
}

@media (max-width: 880px) {
  .process-stage {
    min-height: min(86vh, 720px);
  }

  .process-stage__acts {
    min-height: min(72vh, 600px);
  }

  .process-shot {
    grid-template-columns: 1fr;
    gap: 1.5rem;
    align-content: center;
    padding-block: 0;
  }

  /* 56px of padding above and below on a phone, which is a desktop figure
     wearing a `vw` costume: at 390px the drawing already sits in its own band
     and the copy has its own margins, so this was air inside air. Halved.
     It is the biggest single reason the cells crowded each other. */
  .process-film[data-static='true'] .process-shot {
    min-height: auto;
    padding-block: clamp(1.6rem, 6vw, 2.6rem);
  }

  /* The cells were 14px apart while the pop grows each one by 17px top and
     bottom and the rim glow reaches roughly 40px past the edge. So a popped
     cell overlapped its neighbour and the two glows merged into one warm
     smear — which is why neither the pop nor the rim could be seen on a phone.
     The gap has to clear the effect, not just the geometry. */
  /* The lift is smaller here than on a desktop, and that is not a compromise.
     translateZ is measured against a fixed 1100px camera, so the same 78px
     grows a 513px phone cell by 39px where it grows a 1074px desktop cell by
     the same 39px — a far larger share of a much smaller object. At the full
     figure two adjacent cells closed to 2px of each other at the crossover and
     their glows merged, which is the exact thing this whole change is fixing.
     54px reads as clearly on a phone and leaves the gap intact. */
  .process-film[data-static='true'] .process-shot {
    transform:
      perspective(1100px)
      rotateX(calc((1 - var(--pop)) * 5deg))
      translateZ(calc(var(--pop) * 54px));
  }

  .process-film[data-static='true'] .process-stage__acts {
    gap: 3rem;
    /* Room for the first and last cell to come forward without being clipped
       by the section edge. */
    padding-block: 1.25rem;
  }

  .process-rail {
    gap: 0.4rem;
  }

  .process-rail__step {
    font-size: 0.56rem;
    letter-spacing: 0.08em;
  }

  .process-shot__visual {
    min-height: clamp(190px, 50vw, 245px);
    margin-inline: 0;
  }

  .process-shot__frames,
  .process-shot__canvas {
    width: min(64%, 180px);
    max-height: 180px;
  }

  /* The travel turns ninety degrees with the layout. At this width the drawing
     is on TOP, so the light sweeps ACROSS the image band rather than down the
     cell. Sweeping vertically here would take it straight through the copy,
     which is the one layout where the text has nowhere else to go. */
  .process-film[data-static='true'] .process-shot::after {
    -webkit-mask-image: radial-gradient(52% 30% at calc(var(--light-pos) * 100%) 15%, #000 0%, rgb(0 0 0 / 0.5) 46%, transparent 80%);
    mask-image: radial-gradient(52% 30% at calc(var(--light-pos) * 100%) 15%, #000 0%, rgb(0 0 0 / 0.5) 46%, transparent 80%);
  }


  .process-shot__copy {
    padding-inline: 0.25rem;
    margin-left: 0;
  }

  /* The dust turns ninety degrees with the layout, exactly as the light sweep
     above does. At this width the copy is BELOW the drawing, so the plume has
     to fall down the page and across the words rather than being thrown to the
     right into a margin that no longer exists.
     The box therefore starts up over the panel being brushed and runs the full
     depth of the description. */
  .process-shot__dust {
    inset: -5.5rem -0.5rem auto 0;
    height: 26rem;
  }

  /* Origin moves with it: on a phone the brush head sits centre-top of the
     drawing band rather than in its left third. */
  .process-shot__dust i {
    left: 46%;
    top: 12%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .process-shot {
    position: relative !important;
    inset: auto !important;
    opacity: 1 !important;
    min-height: min(66vh, 620px);
    padding-block: clamp(3.5rem, 7vh, 5.5rem);
  }

  .process-stage,
  .process-stage__acts {
    display: block !important;
    min-height: 0 !important;
  }

  .process-rail {
    display: none !important;
  }

  .process-shot__copy > * {
    opacity: 1 !important;
    visibility: visible !important;
    transform: none !important;
    clip-path: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
    filter: none !important;
  }


  .process-shot__dust {
    display: none !important;
  }
}

/* --------------------------------------------------------------------------
   13. WHO IT'S FOR  (asymmetric bento, exactly 3 cells)
   -------------------------------------------------------------------------- */

.bento {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
  margin-top: clamp(3rem, 6vw, 5rem);
  perspective: 1400px;
}

.bento__cell {
  position: relative;
  grid-column: span 7;
  border-radius: var(--r-surface);
  border: 1px solid rgb(255 255 255 / 0.24);
  overflow: hidden;
  min-height: clamp(320px, 36vw, 430px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  isolation: isolate;
  color: #f2f6f7;
  box-shadow: 0 24px 64px rgb(22 32 31 / 0.13);
  transform-style: preserve-3d;
  transform-origin: 50% 12%;
  backface-visibility: hidden;
  /* These cells stay dark even though the page is light: they carry white
     type over photographs, and the scrim below only works against a dark
     ground. They read as objects on the page rather than part of it. */
  background: #1b2a28;
}

.bento__cell:nth-child(3) {
  grid-column: span 5;
}

/* The household job leads. Adapted from 21st.dev's Condition Grid pattern:
   one decisive image establishes hierarchy, then unequal supporting spans. */
.bento__cell--feature {
  grid-column: 1 / -1;
  min-height: clamp(420px, 46vw, 590px);
}

.bento__cell img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  backface-visibility: hidden;
}

/* Art direction per slot keeps the useful equipment inside each crop. */
.bento__cell[data-slot='bento-residential'] img {
  object-position: 50% 55%;
}

.bento__cell[data-slot='bento-commercial'] img {
  object-position: 50% 46%;
}

.bento__cell[data-slot='bento-farms'] img {
  object-position: 50% 52%;
}

/* Scrim guarantees AA on the label regardless of what photo lands here */
.bento__cell::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to top,
    rgb(7 19 24 / 0.96) 0%,
    rgb(7 19 24 / 0.74) 38%,
    rgb(7 19 24 / 0.12) 76%,
    transparent 100%
  );
}

.bento__copy {
  position: relative;
  z-index: 1;
  max-width: 44ch;
  transform: translateZ(28px);
}

.bento__cell h3 {
  font-size: clamp(1.65rem, 2.7vw, 2.35rem);
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-bottom: 0.65rem;
  line-height: 1;
}

.bento__cell h3 svg {
  width: 1.3em;
  height: 1.3em;
  fill: var(--sun);
  flex: none;
}

.bento__cell p {
  font-size: clamp(0.95rem, 1.25vw, 1.075rem);
  color: rgb(242 246 247 / 0.94);
  max-width: 38ch;
}

.bento__cell--feature h3 {
  font-size: clamp(2rem, 3.6vw, 3.1rem);
}

.bento__cell--feature p {
  max-width: 46ch;
}

@media (hover: hover) and (pointer: fine) {
  .bento__cell {
    transition:
      border-color 240ms var(--ease),
      box-shadow 420ms var(--ease);
  }

  .bento__cell img {
    transition: filter 420ms var(--ease);
  }

  .bento__cell:hover {
    border-color: rgb(255 178 63 / 0.56);
    box-shadow: 0 32px 76px rgb(22 32 31 / 0.2);
  }

  .bento__cell:hover img {
    filter: saturate(1.08) contrast(1.03);
  }
}

@media (max-width: 780px) {
  .bento {
    grid-template-columns: 1fr;
  }

  .bento__cell,
  .bento__cell:nth-child(3),
  .bento__cell--feature {
    grid-column: auto;
    min-height: 360px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bento__cell,
  .bento__cell img {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   14. PROOF  (trust grid, day)
   No photo and no pull-quote: verifiable paperwork and contact details carry
   the trust argument without adding another visual block.
   -------------------------------------------------------------------------- */

.trust {
  display: grid;
  /* Exactly two columns: four items in an auto-fit grid land three-up with
     an orphan, which reads as a layout accident rather than a set of four. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.75rem, 3.5vw, 2.75rem) clamp(2rem, 6vw, 5rem);
  margin-top: clamp(2.75rem, 5vw, 4rem);
  padding-top: clamp(2.25rem, 4vw, 3rem);
  border-top: 1px solid var(--rule);
}

.trust__item {
  display: flex;
  gap: 0.9rem;
  align-items: flex-start;
}

.trust__item svg {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
  fill: var(--accent-text);
  margin-top: 0.1rem;
}

.trust__item strong {
  display: block;
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.trust__item span {
  color: var(--fg-dim);
  font-size: 0.925rem;
}

@media (max-width: 700px) {
  .trust {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   15. OBJECTIONS  (accordion, day)
   -------------------------------------------------------------------------- */

/* The objection section used to be an 860px column left-aligned inside a 1320px
   shell, which left a 460px void beside it with nothing to balance it. It is now
   a two-column split: the question the section answers stays put on the left
   while the answers scroll past it on the right.

   The sticky header is doing real work, not decoration. This is the longest
   reading block on the page and the one a sceptical visitor works through
   slowly, so keeping "Straight answers, including the ones that cost us sales"
   in view is the promise being held open while they check it. */
.faq-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.faq__head {
  position: sticky;
  /* Clears the 68px nav plus breathing room. */
  top: 7.5rem;
}

.faq {
  max-width: 62ch;
}

@media (max-width: 880px) {
  .faq-grid {
    grid-template-columns: 1fr;
    gap: clamp(1.75rem, 5vw, 2.5rem);
  }

  /* Sticky is wrong once it stacks: the header would pin above the answers and
     eat a third of a phone screen for the whole section. */
  .faq__head {
    position: static;
  }

  .faq {
    max-width: none;
  }
}

.faq__item {
  border-bottom: 1px solid var(--rule);
}

.faq__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.5rem 0;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  font-size: clamp(1.02rem, 1.5vw, 1.2rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  transition: color 0.2s var(--ease), transform 0.15s var(--ease);
  transform-origin: left center;
}

.faq__q:active {
  transform: scale(0.985);
  transition: transform 0.08s var(--ease);
}

.faq__q:hover {
  color: var(--accent-text);
}

.faq__q svg {
  width: 1.2rem;
  height: 1.2rem;
  flex: none;
  fill: var(--accent-text);
  transition: rotate 0.3s var(--ease);
}

.faq__q[aria-expanded='true'] svg {
  rotate: 180deg;
}

.faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.35s var(--ease);
}

.faq__a[data-open='true'] {
  grid-template-rows: 1fr;
}

.faq__a > div {
  overflow: hidden;
}

.faq__a p {
  color: var(--fg-dim);
  max-width: 62ch;
  padding-bottom: 1.5rem;
}

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

/* --------------------------------------------------------------------------
   16. CLOSE + FOOTER
   -------------------------------------------------------------------------- */

.close {
  text-align: center;
  /*
    The preceding section already ends on a full --sp-section of air. A second
    one on top of it measured 234px of nothing, which reads as a gap rather
    than as breathing room.
  */
  padding-top: clamp(3rem, 6vh, 5rem);
}

.close h2 {
  font-size: var(--t-h2);
  max-width: 18ch;
  margin-inline: auto;
  margin-bottom: 1.25rem;
}

.close .lede {
  margin-inline: auto;
  text-align: center;
}

.close .btn-row {
  justify-content: center;
}

.close__reassure {
  margin-top: 1.5rem;
  font-size: var(--t-small);
  color: var(--fg-dim);
}

.footer {
  /* The foot of the gradient. This used to be a dark slab; with one light
     ground the page ends on its greyest tone instead of inverting. */
  background: transparent;
  color: var(--ink);
  padding-block: clamp(3.5rem, 7vw, 5.5rem) 2.5rem;
  border-top: 1px solid var(--rule);
}

/* Reserve the corner the Ask Sunny button lives in.

   The launcher is `position: fixed`, so it does not participate in layout and
   sits over whatever occupies the bottom of the VIEWPORT — which, once you have
   scrolled to the end, is the last line of this footer. It was covering "Tariff
   figures from NERSA and City Power" at every width. This is the second time a
   floating control has landed on the end of this footer; the WhatsApp dock did
   it before it was removed, and was given a bespoke hide-at-the-footer rule
   that went with it. Reserving the space instead is the fix that survives the
   next control.

   Not scoped to a breakpoint, deliberately: a first pass at this fixed phones
   only, and desktop kept the bug because the launcher is fixed there too. */
.footer {
  padding-bottom: calc(2.5rem + 52px + 1.25rem);
}

.footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 2.5rem;
}

/* The full lockup, at the foot, where a 2.8:1 shape has somewhere to sit. */
.footer__logo {
  display: block;
  width: auto;
  height: 52px;
  max-width: 100%;
  margin-bottom: 1rem;
}

.footer__blurb {
  color: var(--ink-dim);
  font-size: 0.95rem;
  max-width: 32ch;
}

/* The coverage answer, stated plainly and above the address rather than as the
   second line of it. "Installing nationwide" was true and present, but buried
   under a street address it read as a qualifier on the address instead of the
   answer to "will you come to me". */
.footer__reach {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 0.9rem;
}

/* The alternative address. Present and findable, and visibly the second one —
   two contact addresses set identically make a visitor choose, and choosing
   between somebody else's inboxes is not a decision they should have to make.
   The company domain leads; this one is offered. */
.footer__alt-mail {
  align-items: flex-start;
}

.footer__alt-mail span {
  display: block;
}

.footer__alt-mail em {
  display: block;
  font-style: normal;
  font-size: 0.8rem;
  color: var(--ink-dim);
}

.footer__addr {
  display: inline-flex;
  gap: 0.5rem;
  align-items: flex-start;
  color: var(--ink-dim);
}

.footer h3 {
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 1rem;
  font-weight: 500;
}

.footer a {
  color: var(--ink);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.footer a:hover {
  color: var(--sun-ink);
}

.footer svg {
  width: 1.05rem;
  height: 1.05rem;
  fill: var(--sun-ink);
  flex: none;
}

.footer ul {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 0.7rem;
  font-size: 0.95rem;
}

.footer__base {
  margin-top: 3rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--rule);
  font-size: 0.82rem;
  color: var(--ink-dim);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
}

/* `space-between` on three items that wrap is a lottery: on a phone it pushed
   "Privacy notice" to the far right of a line it shared with nothing, and left
   the tariff note stranded below. Stacked and left-aligned, they read as what
   they are — three footnotes. */
@media (max-width: 560px) {
  .footer__base {
    display: grid;
    justify-items: start;
    gap: 0.35rem;
  }
}

/* --------------------------------------------------------------------------
   18b. TOUCH  —  what a finger needs that a cursor does not

   Keyed on `pointer: coarse`, not on width. A 1024px tablet is a finger and a
   380px window on a laptop is a mouse; sizing hit areas by viewport gets both
   of those backwards.

   Measured before writing any of it: seven controls on this page were under
   44px tall on a phone, including the nav CTA at 32px and every contact link
   in the footer at 24px. Those footer links ARE the conversion path on mobile —
   the phone number, WhatsApp and the email address — and they were the hardest
   things on the page to hit.

   NOT the place for Sunny's touch rules. The section numbering in this file
   does not match its order — Sunny's block is physically LAST, after both this
   one and the reduced-motion section — so anything for the widget written here
   is overridden by equal-specificity rules further down. It cost one round to
   find. Sunny's touch overrides live with Sunny.
   -------------------------------------------------------------------------- */

@media (pointer: coarse) {
  /* The grey flash a browser paints over a tapped link is the single loudest
     "unfinished" signal on a phone, and this page already answers every tap
     with its own :active state — the buttons scale, the cells light. Two
     acknowledgements of one tap is one too many.
     Only ever safe to remove where a real :active exists, which is why it sits
     here rather than in the reset at the top of the file. */
  a, button, [role='button'], summary, .faq__q {
    -webkit-tap-highlight-color: transparent;
  }

  /* 44px is Apple's floor and 48px is Google's. 44 with real padding around it
     beats 48 achieved by stretching a link across a gap it does not own. */
  .nav__cta,
  .sunny-launch {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .nav__brand {
    padding-block: 0.68rem;
  }

  /* Standalone links in lists: the footer contact column and the base row.
     Inline links inside a paragraph are deliberately NOT touched — a 44px tall
     link in running prose blows the line box apart, and the rule was never
     meant for them. */
  .footer ul a,
  .footer ul .footer__addr {
    min-height: 44px;
    display: flex;
    align-items: center;
  }

  .footer__base a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* The eyebrow was 11.2px, which is a caption size borrowed from desktop where
     it sits beside 60px headlines. On a phone it is just small. */
  .eyebrow {
    font-size: 0.78rem;
  }

  .process-shot__verb {
    font-size: 0.84rem;
  }

  .footer h3 {
    font-size: 0.82rem;
  }

}

/* Vertical rhythm, tightened for a hand.
   --sp-section resolves to 110px top AND bottom on a 844px phone: a quarter of
   the screen is empty between every pair of sections, and the page runs to
   nearly thirteen screens. That much air is generous on a 27-inch display and
   merely long on a phone, where the cost of space is measured in thumb travel.
   The proportions are unchanged, the scale is not. */
@media (max-width: 560px) {
  :root {
    --sp-section: clamp(4.25rem, 9.5vh, 5.5rem);
  }
}

/* --------------------------------------------------------------------------
   19. REDUCED MOTION
   Everything above collapses to its final state. Nothing is lost but time.
   -------------------------------------------------------------------------- */

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

  .godray {
    opacity: 1;
  }

  .win-set {
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   17. LEGAL PAGE  (privacy.html)
   A single narrow measure. No storytelling here on purpose: this is the one
   page on the site a reader arrives at wanting to finish quickly.
   -------------------------------------------------------------------------- */

.legal {
  padding-block: clamp(7rem, 12vh, 9rem) var(--sp-section);
}

.legal__shell {
  max-width: 68ch;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.legal h1 {
  font-family: 'Barlow Condensed', 'Barlow', sans-serif;
  font-size: var(--t-h2);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.01em;
  margin-bottom: 0.6rem;
}

.legal__meta {
  color: var(--ink-dim);
  font-size: var(--t-small);
  margin-bottom: 2.5rem;
}

.legal h3 {
  /* Has to clear the body size by enough to read as a level, not as a bold
     sentence: body resolves to 16px at most widths and 1.02rem landed at 16.3. */
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.3;
  margin-top: 1.9rem;
  margin-bottom: 0.6rem;
}

.legal h2 {
  font-size: var(--t-h3);
  font-weight: 700;
  line-height: 1.2;
  margin-top: 2.75rem;
  margin-bottom: 0.75rem;
}

.legal p,
.legal li {
  color: var(--ink-dim);
  margin-bottom: 0.9rem;
}

.legal strong {
  color: var(--ink);
  font-weight: 600;
}

.legal ul {
  padding-left: 1.15rem;
  display: grid;
  gap: 0.35rem;
  margin-bottom: 0.9rem;
}

.legal a {
  color: var(--sun-ink);
  text-underline-offset: 0.2em;
}

.legal__back {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 3rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}

.legal__back:hover {
  color: var(--sun-ink);
}

/* --------------------------------------------------------------------------
   18. SUNNY  (the assistant widget)

   Sunny is the sole persistent conversation control and sits at bottom-right,
   where visitors expect support widgets. Human handoff stays inside the chat,
   so the page never presents two competing floating calls to action.
   -------------------------------------------------------------------------- */

.sunny-launch {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: var(--z-dock);
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.8rem 1.25rem 0.8rem 1rem;
  border: 1px solid rgb(224 138 30 / 0.34);
  border-radius: var(--r-pill);
  /* Translucent amber glass: the page remains visible through the launcher,
     while the upper-left highlight reads like daylight rather than a flat
     yellow button. */
  background:
    radial-gradient(circle at 18% 0%, rgb(255 255 255 / 0.7), transparent 46%),
    linear-gradient(145deg, rgb(255 224 151 / 0.76), rgb(255 184 68 / 0.64));
  -webkit-backdrop-filter: blur(16px) saturate(145%);
          backdrop-filter: blur(16px) saturate(145%);
  color: var(--ink);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow:
    0 8px 28px rgb(112 66 6 / 0.18),
    inset 0 1px 0 rgb(255 255 255 / 0.64);
  transition: transform 0.15s var(--ease), box-shadow 0.35s var(--ease);
}

.sunny-launch:hover {
  box-shadow: 0 10px 32px rgb(112 66 6 / 0.24);
}

.sunny-launch:active {
  transform: scale(0.97);
}

.sunny-launch:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
}

/* The launcher wears the same sun, so the button and the panel it opens are
   recognisably one thing. Rays here are quieter — this sits over page content,
   not over the panel's own surface. */
.sunny-launch svg {
  position: relative;
  z-index: 1;
  width: 1.15rem;
  height: 1.15rem;
  fill: var(--sun-deep);
  flex: none;
}

.sunny-launch__mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: none;
}

.sunny-launch__mark::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: repeating-conic-gradient(
    from 9deg,
    rgb(255 178 63 / 0.62) 0deg 5.5deg,
    rgb(255 178 63 / 0) 5.5deg 36deg
  );
  -webkit-mask-image: radial-gradient(closest-side, transparent 42%, #000 56%, transparent 84%);
          mask-image: radial-gradient(closest-side, transparent 42%, #000 56%, transparent 84%);
  transition: transform 0.35s var(--ease), opacity 0.35s var(--ease);
}

.sunny-launch:hover .sunny-launch__mark::before {
  transform: scale(1.12);
  opacity: 0.85;
}

.sunny-launch[hidden] { display: none; }

/* The panel */
/* Sizing, and the two things that were wrong with it.
   `100vh` on iOS Safari is the height WITHOUT the address bar, so a panel sized
   against it hangs off the bottom of the screen behind the browser chrome —
   which on a chat widget means the send button. `100dvh` is the visible height
   and it is what this needs. And 600px tall against a 400px width made a column
   that dominated a laptop screen; the panel is a sidecar to the page, not a
   second window. */
.sunny {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: var(--z-dock);
  width: min(376px, calc(100vw - 2rem));
  /* max-height, NOT height.
     A fixed height meant the panel claimed 86% of a phone screen to show a
     three-line greeting, with up to 245px of dead air under it — measured on a
     real device, and it is the difference between a chat that has opened beside
     the page and one that has taken the page over. The panel is now as tall as
     what is in it and stops growing at the cap, which is where the scrolling
     starts. */
  height: auto;
  max-height: min(540px, calc(100dvh - 6rem));
  display: flex;
  flex-direction: column;
  /* Sunlit amber glass. The radial highlight gives the surface one natural
     light source; the diagonal tint behaves like a ray crossing the panel.
     Both layers stay translucent so the page remains visibly behind it. */
  background:
    radial-gradient(circle at 12% 0%, rgb(255 255 255 / 0.5), transparent 38%),
    linear-gradient(145deg, rgb(255 225 153 / 0.62), rgb(255 184 69 / 0.42) 56%, rgb(255 231 174 / 0.56));
  -webkit-backdrop-filter: blur(24px) saturate(150%);
          backdrop-filter: blur(24px) saturate(150%);
  border: 1px solid rgb(255 178 63 / 0.56);
  border-radius: var(--r-surface);
  box-shadow:
    0 24px 64px rgb(70 42 6 / 0.22),
    inset 0 1px 0 rgb(255 255 255 / 0.62);
  overflow: hidden;
}

/* Where the blur is unavailable the translucency has to go with it, or the
   page shows through unblurred and the text sits on whatever happens to be
   behind it. Opaque fallback, same hue. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sunny {
    background: linear-gradient(145deg, #ffe9b8, #ffc86d 58%, #ffe1a4);
  }
}

/* Phones get a bottom sheet instead of a floating card. A 16px gutter around a
   near-full-screen panel is the worst of both: it is not a card any more and it
   wastes the width the keyboard is about to take. Anchored to the bottom edge,
   it also keeps the input above the home indicator. */
@media (max-width: 560px) {
  .sunny {
    right: 0;
    left: 0;
    bottom: 0;
    width: 100%;
    height: auto;
    max-height: min(86dvh, calc(100dvh - 1rem));
    border-width: 1px 0 0;
    border-radius: var(--r-surface) var(--r-surface) 0 0;
  }

  .sunny__legal {
    padding-bottom: calc(0.7rem + env(safe-area-inset-bottom, 0px));
  }
}

/* Short landscape windows: keep the form reachable rather than preserving a
   pretty height. */
/* Short viewports — a phone held sideways, mostly. The cap has to leave room for
   the panel's own bottom offset or it computes a height taller than the space
   below it and pushes its own header off the top of the screen. */
@media (max-height: 460px) {
  .sunny {
    bottom: 0.5rem;
    max-height: calc(100dvh - 1rem);
  }

  .sunny__log {
    /* Never let the conversation collapse to a slot. */
    min-height: 4.5rem;
  }
}

.sunny[hidden] { display: none; }

.sunny__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 0.75rem 0.9rem 1.1rem;
  border-bottom: 1px solid rgb(125 74 3 / 0.16);
  flex: none;
}

/* The sun mark.
   The glyph alone was a small orange shape that read as a generic app icon. The
   rays are what make it a sun, and they are drawn rather than shipped as
   artwork: a repeating conic gradient in translucent --sun, masked to a ring so
   it never touches the disc and fades out before it reaches anything else.
   Translucent on purpose — a solid ray would read as a starburst sticker, and
   light is the one thing this company sells. */
.sunny__mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 2.15rem;
  height: 2.15rem;
  flex: none;
}

.sunny__mark svg {
  position: relative;
  z-index: 1;
  width: 1.3rem;
  height: 1.3rem;
  fill: var(--sun-deep);
}

/* The rays. */
.sunny__mark::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Wider wedges, fewer of them. The first pass used 3.2deg rays every 30deg and
     rendered as a biro scribble — at this size a ray has to be wide enough to
     hold its colour, or it aliases into a scratch. */
  background: repeating-conic-gradient(
    from 9deg,
    rgb(255 178 63 / 0.5) 0deg 5.5deg,
    rgb(255 178 63 / 0) 5.5deg 36deg
  );
  /* Start outside the glyph and stop well before the edge. Rays that run to the
     boundary read as a starburst; rays that stop early read as light. */
  -webkit-mask-image: radial-gradient(closest-side, transparent 40%, #000 54%, transparent 82%);
          mask-image: radial-gradient(closest-side, transparent 40%, #000 54%, transparent 82%);
}

/* The haze the rays sit in. */
.sunny__mark::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(255 178 63 / 0.3), rgb(255 178 63 / 0) 68%);
}

/* Light coming on, once, as the panel renders. Both layers bloom outward from
   nothing rather than fading in flat, which is the difference between a sun
   rising and a graphic appearing. */
.sunny__mark::before,
.sunny__mark::after {
  animation: sunny-rays 620ms var(--ease) both;
}

@keyframes sunny-rays {
  from { opacity: 0; transform: scale(0.55) rotate(-24deg); }
  to   { opacity: 1; transform: scale(1)    rotate(0deg); }
}

.sunny__title {
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1.2;
}

.sunny__sub {
  display: block;
  font-weight: 400;
  font-size: 0.75rem;
  color: #5c4a2c;
}

.sunny__close {
  margin-left: auto;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #5c4a2c;
  cursor: pointer;
  font-size: 1.3rem;
  line-height: 1;
}

.sunny__close:hover { background: rgb(125 74 3 / 0.1); color: var(--ink); }

.sunny__close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
}

.sunny__log {
  /* Sizes to its content and shrinks only once the panel has hit its cap.
     `flex: 1` made it fill whatever height the panel had been given, which is
     what produced the empty middle. `min-height: 0` is required or a flex child
     refuses to shrink below its content and the panel overflows instead of
     scrolling. */
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  font-size: 0.92rem;
  line-height: 1.55;
}

.sunny__msg {
  max-width: 88%;
  padding: 0.7rem 0.9rem;
  border-radius: 14px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Sunny speaks directly on the amber glass. Only visitor messages use a bubble,
   so the conversation does not become a stack of nested cards. */
.sunny__msg--sunny {
  align-self: flex-start;
  max-width: 100%;
  padding: 0.25rem 0;
  background: transparent;
  border: 0;
  color: #111;
  border-radius: 0;
}

.sunny__msg--you {
  align-self: flex-end;
  background: var(--ink);
  color: var(--paper);
  border-bottom-right-radius: 5px;
}

.sunny__msg--note {
  align-self: center;
  max-width: 100%;
  background: none;
  padding: 0.2rem 0;
  font-size: 0.8rem;
  color: var(--ink-dim);
  text-align: center;
}

/* Three dots while the model is thinking. */
.sunny__typing {
  align-self: flex-start;
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.4rem 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}

.sunny__typing i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-dim);
  animation: sunny-blink 1.2s var(--ease) infinite;
}

.sunny__typing i:nth-child(2) { animation-delay: 0.15s; }
.sunny__typing i:nth-child(3) { animation-delay: 0.3s; }

@keyframes sunny-blink {
  0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
  30%           { opacity: 1;    transform: translateY(-3px); }
}

@media (prefers-reduced-motion: reduce) {
  .sunny__typing i { animation: none; opacity: 0.5; }

  /* The rays keep their final state — they are the mark, not decoration on it.
     Only the bloom goes. */
  .sunny__mark::before,
  .sunny__mark::after { animation: none; }

  .sunny-launch__mark::before { transition: none; }
}

/* `hidden` on its own does NOT hide this.
   The attribute works by way of a UA rule, `[hidden] { display: none }`, and any
   author `display` declaration outranks it. So `display: grid` below quietly
   beat `ui.hand.hidden = true` and the handoff block rendered on every open,
   149px of WhatsApp and Email buttons sitting under a greeting before the
   visitor had said anything — a third of a phone screen spent asking someone to
   leave a conversation they had not started.
   Checking `el.hidden` in a test returns true and tells you nothing; it is the
   computed `display` that decides. That is how this survived being "verified"
   twice. `.sunny` and `.sunny-launch` already carried this guard; this one was
   missed. */
.sunny__hand[hidden] {
  display: none;
}

.sunny__hand {
  flex: none;
  padding: 0.85rem 1.1rem;
  border-top: 1px solid rgb(125 74 3 / 0.16);
  background: rgb(255 238 199 / 0.28);
  display: grid;
  gap: 0.5rem;
}

.sunny__hand-lead {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.35;
  /* --ink-dim was tuned against broken white. On the tint it loses contrast, so
     the muted tone here is the accent's own dark value instead. */
  color: var(--sun-ink);
  text-align: center;
}

/* WhatsApp stays the primary route: it is the one the rest of the page pushes,
   and the one that reaches a person fastest. Email sits under it as the quieter
   alternative, for someone who would rather write than message.

   It is deeper orange here than the page's own sun buttons, and it has to be.
   --sun on the panel's yellow tint is two shades of the same thing, and a
   primary action that is nearly the colour of its own background is not a
   primary action. Text stays dark: white on this orange measures about 2.7:1
   and fails, dark ink clears AA comfortably. */
.sunny__hand-wa {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.72rem 1rem;
  border-radius: var(--r-pill);
  background: linear-gradient(135deg, #f0932a, var(--sun-deep) 62%, #cf7a12);
  color: #1b1305;
  font-weight: 700;
  font-size: 0.88rem;
  text-decoration: none;
  box-shadow: 0 6px 18px -6px rgb(176 96 8 / 0.6);
  transition: filter 0.2s var(--ease), transform 0.15s var(--ease);
}

.sunny__hand-wa:hover { filter: brightness(1.07); }
.sunny__hand-wa:active { transform: scale(0.985); }

.sunny__hand-mail {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid rgb(125 74 3 / 0.28);
  border-radius: var(--r-pill);
  background: rgb(255 253 248 / 0.74);
  color: var(--ink);
  font: inherit;
  font-size: 0.83rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.sunny__hand-mail:hover { border-color: var(--sun-deep); color: var(--sun-ink); }

.sunny__hand-wa:focus-visible,
.sunny__hand-mail:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
}

.sunny__hand svg { width: 1.05rem; height: 1.05rem; fill: currentColor; flex: none; }

.sunny__form {
  flex: none;
  display: flex;
  gap: 0.5rem;
  align-items: flex-end;
  padding: 0.75rem;
  border-top: 1px solid rgb(125 74 3 / 0.16);
}

.sunny__input {
  flex: 1;
  resize: none;
  max-height: 7rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid rgb(125 74 3 / 0.26);
  border-radius: var(--r-input);
  background: rgb(255 253 248 / 0.82);
  color: var(--ink);
  font: inherit;
  font-size: 0.92rem;
  line-height: 1.45;
}

/* iOS zooms the whole page in when a focused input is under 16px, then leaves
   the visitor zoomed and hunting for the send button. This measured 14.72px.
   The font size is the fix; the alternative is disabling pinch-zoom for
   everybody, which trades one person's annoyance for another person's access. */
@media (pointer: coarse) {
  .sunny__input {
    font-size: 1rem;
  }

  /* 11.2px of required disclosure on a phone. It has to stay short enough not
     to crowd the input and legible enough to actually be read, and the second
     of those was losing. */
  .sunny__legal {
    font-size: 0.76rem;
    line-height: 1.35;
  }
}

.sunny__input:focus-visible {
  outline: none;
  border-color: var(--sun-deep);
  box-shadow: 0 0 0 3px rgb(224 138 30 / 0.18);
}

.sunny__send {
  flex: none;
  width: 2.4rem;
  height: 2.4rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: linear-gradient(135deg, #f0932a, var(--sun-deep));
  color: #1b1305;
  cursor: pointer;
}

.sunny__send:disabled { opacity: 0.4; cursor: default; }

.sunny__send:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--sun-deep);
}

.sunny__send svg { width: 1.05rem; height: 1.05rem; fill: currentColor; }

.sunny__legal {
  flex: none;
  padding: 0 0.75rem 0.7rem;
  font-size: 0.7rem;
  line-height: 1.4;
  color: #5c4a2c;
  text-align: center;
}

.sunny__legal a { color: var(--sun-ink); }

/* The phone rules for the panel itself live with the panel, up in the sizing
   block. There used to be a second `.sunny` override down here taking it
   `inset: 0` full-screen, and because it came later it silently won — the
   bottom sheet was written and never rendered. Two blocks in one file setting
   the same property on the same breakpoint is how that happens. */
@media (max-width: 560px) {
  .sunny-launch {
    padding: 0.7rem 1rem 0.7rem 0.85rem;
    font-size: 0.85rem;
  }
}

/* ---- 24. The footer map, loaded on demand -------------------------------

   Not an iframe until it is asked for. See the comment on `.footer__map` in
   index.html for why: a Maps embed is 1-2MB and hands Google the visitor's IP
   and a cookie before they have done anything, on a page that otherwise has no
   third-party frame on it at all.

   The facade is sized by `aspect-ratio` so swapping the button for the iframe
   does not move the page - the footer is the one place on this site where a
   late reflow is guaranteed to be seen, because the visitor is already at the
   bottom and nothing below it can absorb the shift. */
.footer__map {
  margin: 2.5rem 0 0;
  border-top: 1px solid var(--rule);
  padding-top: 2rem;
  /* Deliberately small. The office is not the story - the footer says
     "Anywhere in South Africa" a few lines above this, and a map big enough to
     dominate the footer argues the opposite. */
  max-width: 380px;
}

.footer__map-open,
.footer__map iframe {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
  border-radius: 14px;
}

.footer__map-open {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  background: var(--paper-2);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease;
}

.footer__map-open svg {
  width: 26px;
  height: 26px;
  color: var(--ink-dim);
}

.footer__map-label {
  font-weight: 600;
}

.footer__map-note {
  font-size: 0.78rem;
  color: var(--ink-dim);
  text-align: center;
  padding-inline: 1rem;
}

@media (hover: hover) {
  .footer__map-open:hover {
    background: var(--paper);
    border-color: var(--ink-dim);
  }
}

.footer__map-open:focus-visible {
  outline: 3px solid var(--sun);
  outline-offset: 3px;
}

.footer__map iframe {
  background: var(--paper-2);
}

.footer__map-cap {
  margin-top: 0.7rem;
  font-size: 0.82rem;
  color: var(--ink-dim);
}

@media (max-width: 700px) {
  .footer__map { max-width: none; }
}

/* Inline links inside an FAQ answer.

   The cost answer is the first body copy on this site to carry one, so until
   now there was no rule and they rendered as browser-default blue (0,0,238) on
   warm paper — the one colour nowhere else in the palette. They are darker than
   the surrounding text rather than a different hue, because the accent is a
   fill on this site and fails contrast as type. */
.faq__a a {
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-thickness 0.15s ease;
}

@media (hover: hover) {
  .faq__a a:hover { text-decoration-thickness: 2px; }
}

.faq__a a:focus-visible {
  outline: 3px solid var(--sun);
  outline-offset: 2px;
  border-radius: 2px;
}
