/*
 * NOCTURNE — Gemmell Outdoor Lighting
 * style.css — page layout, header, footer, homepage sections
 *
 * Requires: base.css (tokens, reset, component primitives)
 * Shared by: all static pages + WP blog theme (enqueued via functions.php)
 */


/* ════════════════════════════════════════════════════════════
   SITE HEADER
   ════════════════════════════════════════════════════════════ */

/* ── HEADER PRESENCE (brand-harmony pass 2026-08-19) ──────────────
   The new mark was landing on a flat bar with no relationship to it:
   a metallic logo pasted onto a plain dark rectangle. Two paint-once
   layers seat it, both derived from the site's ONE idea (the product
   IS light) rather than from generic "add a glow":

   1. A warm pool anchored at the LEFT, where the mark actually sits —
      not a centred or full-width wash. The mark reads as a fixture
      throwing light onto the bar it rests on, which is precisely the
      product. Sized in ch/px so it stays behind the lockup at every
      width and never becomes a visible band.
   2. A bottom edge that reads as a lit lip instead of a flat seam:
      the existing hairline border is kept for structure, and a
      1px gradient ::after lays the brand's two metals along it —
      gold concentrated under the mark, decaying into chrome and then
      to nothing before the right edge. This is the "light entering
      from above" edge language already used by .section-raised,
      .card and the portfolio plate, now applied to the one surface
      that carries the logo.

   Both are background-only, painted once, never animated and never
   scroll-linked. No backdrop-filter is added (the existing blur is
   pre-existing; the playbook forbids ADDING live backdrop-filter for
   the 55+ iPad-Safari buyer). Zero DOM change, zero CLS: ::before is
   absolutely positioned inside an already-fixed bar and takes no
   layout space. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: linear-gradient(
    to bottom,
    rgba(var(--scrim-rgb),  0.97),
    rgba(var(--scrim-rgb),  0.88)
  );
  border-bottom: var(--border-fine);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: background-color var(--dur-fast) var(--ease-in-out);
}

/* The mark's own light, pooled on the bar beneath it. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: min(560px, 60%);
  pointer-events: none;
  background: radial-gradient(
    ellipse 62% 150% at 128px 46%,
    var(--gold-pool-core) 0%,
    var(--gold-pool-mid) 42%,
    var(--gold-edge-none) 78%
  );
}

/* Lit lower lip — the two brand metals laid along the header's edge. */
.site-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;             /* sit ON the existing hairline, not above it */
  height: 1px;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    var(--gold-edge) 0%,
    var(--gold-edge-faint) 14%,
    var(--chrome-edge) 34%,
    var(--chrome-edge-faint) 62%,
    var(--chrome-edge-none) 88%
  );
}

/* Scrolled: the bar closes up and the lit lip strengthens slightly —
   the header asserting itself once it is no longer over the hero. */
.site-header.scrolled {
  background: rgba(var(--scrim-rgb),  0.99);
  border-bottom-color: rgba(var(--edge-gold-rgb),  0.2);
}

.site-header.scrolled::after {
  background: linear-gradient(
    90deg,
    var(--gold-edge-strong) 0%,
    var(--gold-edge-soft) 16%,
    var(--chrome-edge-strong) 38%,
    var(--chrome-edge-faint) 66%,
    var(--chrome-edge-none) 90%
  );
}

/* The header pool is decoration; on reduced-motion it stays (it never
   moves), but it must not fight a high-contrast preference. */
@media (prefers-contrast: more) {
  .site-header::before { display: none; }
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(var(--space-6), 5vw, var(--space-16));
}

/* Logo */
.header-logo {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
}

.header-logo picture,
.footer-brand picture {
  /* <picture> is inline by default; without this it introduces a baseline gap
     inside the flex header and mis-aligns the mark against the nav. */
  display: block;
  line-height: 0;
}

.header-logo img {
  height: 44px;
  width: auto;
}

/* Primary navigation */
.header-nav {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  /* Guaranteed breathing room between the wordmark and "Home".
     .header-inner is space-between, so at 1440px the gap is generous — but
     between 901px and ~1200px the nav creeps up against the logo's final "L"
     and the lockup stops reading as a separate object. This floor keeps the
     separation at every desktop width; it is margin, not padding, so it does
     not enlarge the nav's own hit areas. */
  margin-left: clamp(var(--space-8), 4vw, var(--space-16));
}

.header-nav a {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-ivory-muted);
  text-decoration: none;
  position: relative;
  /* Hit area, not spacing: measured 22px tall before this (2026-08-08).
     Vertical padding grows the target to ~44px while the flex-centred header
     keeps the text on the same optical line. The ::after underline is pinned
     to the text baseline below so the hover rule does not drift down. */
  padding: 11px 0 13px;
  transition: color var(--dur-fast) var(--ease-out-expo);
}

.header-nav a::after {
  content: '';
  position: absolute;
  bottom: 11px;   /* pinned to the text baseline, not the padded box edge */
  left: 0;
  width: 0;
  height: 1px;
  /* Gold highlight, not --color-amber (brand-harmony 2026-08-19): this 1px
     rule sits a few centimetres from the mark's own gold in the same bar.
     At --color-amber (OKLCH L=0.695) it read as a duller, older gold beside
     the logo's L=0.86-0.88 — the single most visible instance of the
     "two different golds" problem. */
  background: var(--color-gold-highlight);
  transition: width var(--dur-ui) var(--ease-out-strong);
}

.header-nav a:hover,
.header-nav a:focus-visible,
.header-nav a[aria-current="page"] {
  color: var(--color-ivory);
}

.header-nav a:hover::after,
.header-nav a:focus-visible::after,
.header-nav a[aria-current="page"]::after {
  width: 100%;
}

/* Services dropdown */
.nav-dropdown {
  position: relative;
}

.nav-dropdown__toggle {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-ivory-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out-expo);
  background: none;
  border: none;
  /* Match .header-nav a exactly so the toggle sits on the same optical line
     and carries the same 44px hit area (was padding:0 = a 16px target).
     line-height must be set explicitly: a <button> defaults to `normal`,
     which rendered 4px shorter than the sibling anchors. */
  line-height: var(--leading-relaxed);
  padding: 11px 0 13px;
  font-family: var(--font-body);
}

.nav-dropdown__toggle:hover,
.nav-dropdown__toggle:focus-visible,
.nav-dropdown:hover .nav-dropdown__toggle {
  color: var(--color-ivory);
}

.nav-dropdown__chevron {
  width: 12px;
  height: 12px;
  transition: transform var(--dur-fast) var(--ease-out-expo);
}

.nav-dropdown:hover .nav-dropdown__chevron,
.nav-dropdown__toggle[aria-expanded="true"] .nav-dropdown__chevron {
  transform: rotate(180deg);
}

/*
 * Hover-gap bridge: the menu sits below the toggle with a visual gap, but
 * an invisible ::before fills that gap so moving the cursor from the toggle
 * to a menu item never leaves the .nav-dropdown hover region (the "menu
 * hides as soon as I drop down to select" bug). Pure CSS, no JS.
 */
.nav-dropdown::before {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: var(--space-4);
  display: none;
}
.nav-dropdown:hover::before,
.nav-dropdown:focus-within::before {
  display: block;
}

.nav-dropdown__menu {
  position: absolute;
  top: calc(100% + var(--space-4));
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-canvas-raised);
  border: var(--border-fine);
  border-radius: var(--radius-md);
  padding: var(--space-3) 0;
  min-width: 220px;
  box-shadow: var(--shadow-lift);
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(-8px);
  transition:
    opacity var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-out-expo),
    visibility var(--dur-fast);
  pointer-events: none;
}

.nav-dropdown:hover .nav-dropdown__menu,
.nav-dropdown:focus-within .nav-dropdown__menu,
.nav-dropdown__toggle[aria-expanded="true"] + .nav-dropdown__menu {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  pointer-events: all;
}

.nav-dropdown__menu a {
  display: block;
  padding: var(--space-3) var(--space-6);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-ivory-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out-expo),
              background-color var(--dur-fast) var(--ease-out-expo);
}

.nav-dropdown__menu a:hover,
.nav-dropdown__menu a:focus-visible {
  color: var(--color-amber-light);
  background: rgba(var(--glow-rgb),  0.06);
}

.nav-dropdown__menu a::after {
  display: none; /* dropdown links have no underline decoration */
}

/* Always-visible phone number */
.header-phone {
  /* Primary conversion control — give it a full 44px target on desktop too
     (measured 24px, 2026-08-08). */
  padding: 10px 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Separate the number from the last nav item. At the 1201px breakpoint the
     nav only just fits, so "Contact" and the phone icon collided with no gap
     between them and read as one run-on control. Margin, not padding, so the
     44px tap target is unaffected. */
  margin-left: var(--space-6);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-ivory);
  text-decoration: none;
  letter-spacing: 0.01em;
  transition: color var(--dur-fast) var(--ease-out-expo);
  white-space: nowrap;
}

.header-phone:hover,
.header-phone:focus-visible {
  color: var(--color-amber-light);
}

.header-phone__icon {
  width: 16px;
  height: 16px;
  /* Gold highlight (brand-harmony 2026-08-19) — the other gold element
     sharing the bar with the mark. At 16px, --color-amber (L=0.695) was
     both duller than the logo and needlessly dim for a 55+ buyer; the
     highlight tier is 12.16:1 on canvas. */
  color: var(--color-gold-highlight);
  flex-shrink: 0;
}

/* Mobile menu toggle */
.header-menu-toggle {
  display: none;
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  min-width: 44px;  /* ≥44px tap target */
  height: 44px;
  padding: 0 var(--space-3);
  background: none;
  border: 1px solid var(--color-canvas-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

/* Bars are stacked inside their own box so the word can sit beside them */
.header-menu-toggle__bars {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 18px;
  height: 13px;
  flex-shrink: 0;
}

.header-menu-toggle__bars span {
  display: block;
  width: 100%;
  height: 1px;
  background: var(--color-ivory);
  transition: transform var(--dur-fast) var(--ease-out-expo),
              opacity var(--dur-fast) var(--ease-out-expo);
}

/* The word that makes the control read as a menu, not a glyph */
.header-menu-toggle__label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-ivory);
  line-height: 1;
}

.header-menu-toggle[aria-expanded="true"] .header-menu-toggle__bars span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}
.header-menu-toggle[aria-expanded="true"] .header-menu-toggle__bars span:nth-child(2) {
  opacity: 0;
}
.header-menu-toggle[aria-expanded="true"] .header-menu-toggle__bars span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

/* Mobile nav panel is hidden entirely outside the mobile breakpoint */
.mobile-nav {
  display: none;
}

/* Responsive header
   ─────────────────────────────────────────────────────────────
   Switches to the mobile header at 1250px, NOT 900px.

   Measured geometry (headless Chrome, .header-phone right edge vs viewport):
     vw= 901 → phone.right 1166.4  OVERFLOW
     vw=1000 → phone.right 1175.3  OVERFLOW
     vw=1100 → phone.right 1184.3  OVERFLOW
     vw=1150 → phone.right 1188.8  OVERFLOW
     vw=1201 → phone.right 1217.4  OVERFLOW (after the phone's 24px margin)
     vw=1250 → phone.right 1221.8  ok
   The 8-item nav plus the always-visible phone number simply does not fit
   under 1250px, so between 901 and 1249 the nav ran off-screen and took
   "Journal", "Contact" and the phone with it — on a 1024 or 1152 laptop the
   click-to-call this audience relies on was literally off the page.
   Pre-existing (reproduced on the pre-change backup), not caused by the logo.

   Only THIS block moves. The other `max-width: 900px` blocks in this file
   govern unrelated components and are deliberately left at 900. */

/* 1250–1359px: the theme toggle (added after the measurements above) made
   the row ~21px wider than the viewport, so on a 1280 laptop (≈1265 inside
   the scrollbar) the toggle ran off the right edge and crowded the phone
   number. Tightening the nav gaps here frees ~56px; wider screens are
   untouched. Measured 2026-10-25, all widths 320–1920 clear after. */
@media (min-width: 1250px) and (max-width: 1359px) {
  .header-nav {
    gap: var(--space-6);
    margin-left: var(--space-8);
  }
}

.header-phone,
.theme-toggle {
  flex-shrink: 0;
}

@media (max-width: 1249px) {
  .header-nav {
    display: none;
  }

  /*
   * Phone stays visible in the header at all times (system contract):
   * collapse to the icon alone on mobile — the full number lives in
   * the mobile nav panel. font-size: 0 hides the text node only.
   */
  .header-phone {
    font-size: 0;
    gap: 0;
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
  }

  .header-phone__icon {
    width: 22px;
    height: 22px;
  }

  .header-menu-toggle {
    display: flex;
  }

  /* Mobile nav panel */
  .mobile-nav {
    position: fixed;
    top: var(--header-h);
    left: 0;
    right: 0;
    /* Cap height to the viewport below the header so the panel can scroll
       when the link list is taller than the screen (cut-off bug). Use both
       100vh and 100dvh — dvh accounts for the mobile browser chrome. */
    height: calc(100vh - var(--header-h));
    height: calc(100dvh - var(--header-h));
    bottom: auto;
    background: var(--color-canvas);
    /* Horizontal inset only. Vertical padding lives on .mobile-nav__list so the
       dock can sit flush against the panel's bottom edge — panel padding-bottom
       held it 32px short (2026-08-08). */
    padding: 0 clamp(var(--space-6), 5vw, var(--space-16));
    /* Grid, not flex: row 1 is the scrolling link list, row 2 is the fixed
       conversion dock. Sticky-inside-a-flex-column put the dock in flow order,
       so links after it scrolled underneath (2026-08-08). */
    display: grid;
    grid-template-rows: 1fr auto;
    /* NO align-content:start — it collapses the rows to content height, so the
       dock floated mid-screen with dead space beneath it (2026-08-08). The
       default `stretch` makes row 1 absorb the free space and pins row 2 to
       the panel's bottom edge. */
    gap: 0;
    transform: translateX(100%);
    transition: transform var(--dur-base) var(--ease-out-expo);
    overflow: hidden;                    /* the LIST scrolls, not the panel */
    z-index: 99;
  }

  .mobile-nav.is-open {
    transform: translateX(0);
  }

  /*
   * MOBILE NAV TYPE SCALE — retuned 2026-08-08.
   *
   * Measured before the change: --text-3xl (39px) + --space-5 padding gave a
   * 107px row. With 15 links that is 1,951px of menu on a 390px phone (2.5
   * screens) and 2,150px on a 360px phone (3.8 screens) — only 3 links visible
   * without scrolling, so Contact and the phone number sat three swipes away.
   * The scale was set when this nav had 8 links; it has grown since.
   *
   * --text-xl (25px) keeps the Cormorant display voice and stays far above the
   * 16px legibility floor this 55+ audience needs, while --space-4 padding
   * holds every row at ~57px — comfortably over the 44px tap-target minimum.
   * Result: the whole menu fits ~1 screen on a standard phone.
   */
  .mobile-nav a {
    display: block;
    padding: var(--space-4) 0;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 300;
    line-height: 1.25;
    color: var(--color-ivory);
    text-decoration: none;
    border-bottom: var(--border-fine);
    transition: color var(--dur-fast) var(--ease-out-expo);
  }

  /* Very short viewports (landscape phones, 5" screens): one more step down
     rather than letting the panel run past two screens again. */
  @media (max-height: 700px) {
    .mobile-nav a {
      font-size: var(--text-lg);
      padding: var(--space-3) 0;
    }
  }

  /*
   * Service links sit visually under the Services group. Slight indent + the
   * body font marks them as children of a category rather than 6 more
   * top-level destinations — the flat list read as 15 equal choices.
   */
  .mobile-nav a[data-nav-group="service"] {
    font-family: var(--font-body);
    font-size: var(--text-base);
    padding-left: var(--space-5);
    color: var(--color-ivory-muted);
  }

  /* Small-caps section label above each group. */
  .mobile-nav__label {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--tracking-widest);
    text-transform: uppercase;
    color: var(--color-amber-light);
    margin-top: var(--space-4);
    margin-bottom: 0;
    padding-bottom: var(--space-1);
  }
  .mobile-nav__label:first-child { margin-top: 0; }

  .mobile-nav a:hover,
  .mobile-nav a:focus-visible {
    color: var(--color-amber-light);
  }

  /*
   * CONVERSION DOCK — the phone number is STICKY at the foot of the panel.
   *
   * Even after retuning the type scale, 15 links run ~1.36 screens on a 390px
   * phone, so the number sat below the fold. This audience calls; making them
   * scroll for the number is the single most expensive thing this menu could do.
   *
   * position:sticky (not fixed) keeps it inside the scroll container, so it
   * rides the bottom edge while the list scrolls behind and never covers the
   * final link — the earlier margin-top:auto attempt fought the scroll and cut
   * links off entirely.
   */
  .mobile-nav__list {
    /* Row 1 of the panel grid: the only scrolling region. */
    padding-top: var(--space-5);
    padding-bottom: var(--space-4);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;   /* momentum scroll on iOS */
    overscroll-behavior: contain;        /* don't chain-scroll the page behind */
    min-height: 0;                       /* required for a grid row to scroll */
  }

  .mobile-nav__dock {
    /* Row 2 of the panel grid — always at the bottom edge, never scrolls. */
    border-top: 1px solid rgba(var(--edge-gold-rgb),  0.28);
    background: var(--color-canvas);
    padding-top: var(--space-3);
    padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
  }

  .mobile-nav__phone {
    display: block;
    font-size: var(--text-xl) !important;
    font-family: var(--font-body) !important;
    font-weight: 500;
    text-align: center;
    color: var(--color-amber-light) !important;
    border-bottom: none !important;
    padding: var(--space-3) 0 !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  .header-nav a::after,
  .nav-dropdown__chevron,
  .nav-dropdown__menu {
    transition: none;
  }
}


/* ════════════════════════════════════════════════════════════
   SITE FOOTER
   ════════════════════════════════════════════════════════════ */

/* ── FOOTER SEATING (brand-harmony pass 2026-08-19) ───────────────
   The footer carries the largest instance of the mark on the site (the
   84px stacked lockup) against the site's only full-width divider — a
   plain flat hairline. Two changes, both paint-once:

   1. The top edge becomes the counterpart of the header's lit lip, so
      the page is bracketed top and bottom by the same two-metal edge.
      Here the gradient is CENTRE-weighted rather than left-weighted
      (the footer is a full-width band, not a left-anchored bar), and
      chrome leads with gold as the warm core — the inverse emphasis of
      the header, which keeps the two edges related without making the
      footer a copy of the header.
   2. A warm pool behind the brand column, seating the lockup in its own
      light exactly as the header seats the horizontal mark.

   position:relative is required for the pseudo-elements and is safe:
   .site-footer is a static-flow block and gains no stacking side
   effects that affect its children's layout. view-transition-name on
   this element is unaffected (it names the element, not its paint). */
.site-footer {
  position: relative;
  /* Band rung, not card rung — the footer is a full-width terminating
     band in the same rhythm as .section-raised, not a panel sitting on
     one. See the elevation ladder note in base.css :root. */
  background: var(--color-canvas-band);
  border-top: var(--border-fine);
  padding-top: var(--space-16);
  padding-bottom: var(--space-8);
}

/* Lit top lip — the header's edge language, mirrored at the page's end. */
.site-footer::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    var(--chrome-edge-none) 0%,
    var(--chrome-edge-soft) 18%,
    var(--gold-edge-soft) 42%,
    var(--gold-edge-faint) 58%,
    var(--chrome-edge-soft) 82%,
    var(--chrome-edge-none) 100%
  );
}

/* The stacked lockup's own light. Anchored to the brand column, which
   is the first cell of a 2fr/1fr/1fr/1fr grid — so the pool is held to
   the left ~40% and never becomes a full-width wash. */
.site-footer::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: min(620px, 46%);
  height: 320px;
  pointer-events: none;
  background: radial-gradient(
    ellipse 68% 74% at 26% 22%,
    var(--gold-pool-core-soft) 0%,
    var(--gold-pool-mid-soft) 46%,
    var(--gold-edge-none) 76%
  );
}

/* The footer's grid must paint above the pool. */
.footer-inner { position: relative; }

@media (prefers-contrast: more) {
  .site-footer::after { display: none; }
}

.footer-inner {
  display: grid;
  grid-template-columns: 2fr repeat(3, 1fr);
  gap: var(--space-16) var(--space-12);
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(var(--space-6), 5vw, var(--space-16));
  margin-bottom: var(--space-12);
}

.footer-brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.footer-brand img {
  /* The footer shows the STACKED lockup (mark over wordmark), so its height is
     split across two tiers instead of being all wordmark like the old wide
     mark. At the previous 52px the "OUTDOOR LIGHTING" line fell to ~4px and
     stopped reading. The footer column has the room, and at 96px the lockup
     holds its own against a four-column footer instead of reading as an
     afterthought beside the phone number. */
  height: 96px;
  width: auto;

  /* Restore the ground the mark loses on this panel.
     ─────────────────────────────────────────────────────────────
     .site-footer is --color-canvas-raised (#311a04, relative luminance
     0.1331), lighter than the header's --color-canvas (#261304, 0.1020).
     The lockup's own bevel shadows are baked into the artwork, so on the
     lighter panel 9.3% of its ink falls BELOW the background luminance
     (vs 5.5% on the header) — measured. Those shadows stop reading as
     depth and start reading as holes, which is what flattens the mark.

     A tight dark drop-shadow gives the mark back a darker surround than
     its own shadows, so the bevel reads as relief again. The second,
     wider warm shadow is the same amber bloom the header already uses on
     its brand block — it lifts the gold without touching the artwork. */
  filter:
    drop-shadow(0 1px 2px rgba(var(--shadow-rgb),  0.55))
    drop-shadow(0 0 18px rgba(var(--glow-rgb),  0.18));
}

.footer-brand p {
  font-size: var(--text-base);
  color: var(--color-ivory-muted);
  line-height: var(--leading-relaxed);
  max-width: 34ch;
}

.footer-brand__phone {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--color-ivory);
  text-decoration: none;
  margin-top: var(--space-2);
}

.footer-brand__phone:hover {
  color: var(--color-amber-light);
}

.footer-col h3 {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  margin-bottom: var(--space-5);
}

.footer-col ul {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.footer-col a {
  /* 44px touch target (owner polish contract), text stays centred in it */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--text-base);
  color: var(--color-ivory-muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out-expo);
}

.footer-col a:hover,
.footer-col a:focus-visible {
  color: var(--color-ivory);
}

.footer-col address {
  font-style: normal;
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
}

/* Footer bottom bar */
.footer-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(var(--space-6), 5vw, var(--space-16));
  padding-top: var(--space-8);
  border-top: var(--border-fine);
}

.footer-bar p {
  font-size: var(--text-sm);
  color: var(--color-ivory-muted); /* 6.08:1 — fine print is still content */
  max-width: none;
}

@media (max-width: 900px) {
  .footer-inner {
    grid-template-columns: 1fr 1fr;
  }

  .footer-brand {
    grid-column: 1 / -1;
  }
}

@media (max-width: 560px) {
  .footer-inner {
    grid-template-columns: 1fr;
  }

  .footer-bar {
    flex-direction: column;
    align-items: flex-start;
  }
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — HERO
   "Match-strike" welcome (premium polish pass, 2026-06-10):
   the page paints instantly as editorial type on warm charcoal,
   the amber accent word ignites, and the estate blooms out of
   darkness behind it — the entrance IS the product demo.

   Choreography (all CSS, all compositor, all fill-mode:backwards
   so the RESTING state is the finished frame):
     0ms     eyebrow + H1 rise (700ms)         — instant first text
             — the PHOTO is already at its finished exposure here
               (no brightness ramp = no flash; see .hero__bg)
     150ms   subline rises (700ms)
     300ms   CTA actions rise (700ms)
     300ms   light-pools grade settles in (1200ms, opacity-only) —
             the "dusk settling / lights finding the house" reveal
             that replaced the old global brightness flash
     500ms   accent word "Shine" ignites (900ms, brightness
             overshoot at 70% — filament warm-up, no flicker)
   No JS involved; reduced-motion or no-animation renders the
   finished frame. The legibility gradient (.hero::after) is STATIC.
   Art-direction layer: .hero__grade (composed warm/cool light pools +
   vignette; paint-once, opacity-only entrance).
   ════════════════════════════════════════════════════════════ */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  padding-bottom: clamp(var(--space-16), 10vh, var(--space-32));
  overflow: hidden;
  isolation: isolate;
}

/* Hero background image */
/* The <picture> wrapper is now the positioned box — sizing rules written for a
   bare <img> silently move to the wrapper once one exists, and object-fit on an
   unconstrained parent zooms hard. Wrapper owns the box; img fills it. */
.hero__bg-pic {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  z-index: -2;
}

.hero__bg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 35%;

  /* Portrait branch is a purpose-built 3:4 crop, already composed — it does not
     want the landscape frame's upward bias, which pushed the lit bollards into
     the headline. Centre it. */

  /*
   * STATIC exposure (no entrance ramp). 2026-06-17 council fix:
   * the old hero-bloom keyframe interpolated brightness 0.35→0.75
   * uniformly across the whole frame — a spatially-flat exposure ramp,
   * which is definitionally a "flash." Premium = subtraction: the photo
   * now rests at its FINISHED exposure and paints once. The entrance
   * reveal is sourced instead from the .hero__grade light-pools settling
   * in (opacity-only, below) + the Shine match-strike — "dusk settling /
   * lights finding the house," not a global gain knob. Resting at the
   * final exposure also removes a load-clock dependency that put the LCP
   * image's perceived paint at risk on a slow-photo race.
   */
  filter: brightness(0.75) saturate(1.05);
}

@media (max-aspect-ratio: 3/4) {
  .hero__bg {
    object-position: center 50%;
  }
}

/* Gradient overlay — foreground legibility. STATIC, never animated:
   the H1 needs reliable contrast at 0ms, and animating `background`
   is a paint property (not compositor-able) — the wrong mechanism for
   a reveal. The "dusk settling" event lives on .hero__grade (opacity). */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;

  /* Deep at bottom for text, lighter top */
  background: linear-gradient(
    to top,
    rgba(var(--photo-scrim-rgb), 0.96) 0%,
    rgba(var(--photo-scrim-rgb), 0.70) 35%,
    rgba(var(--photo-scrim-rgb), 0.20) 70%,
    rgba(var(--photo-scrim-rgb), 0.05) 100%
  );
}

/*
 * Art-direction grade — STATIC color grading on the photograph.
 * (Owner rule 2026-06-17: art-direction freedom WITHIN strict perf gates —
 *  this is the spectacle layer, and it costs nothing: paint-once layered
 *  gradients on one existing span. No new DOM, no request, never animated,
 *  never scroll-linked. Compositor-safe; CLS/LCP/weight all unaffected.)
 *
 * The narrative is "the product IS light": a real estate at dusk isn't lit
 * by one uniform glow — it's lit by DESIGN, as several discrete warm pools
 * (eave fixtures, uplit specimen trees, a glowing entry) sitting in a cooler
 * ambient sky. So the grade now composes that warm/cool tension deliberately.
 * Layers, front-to-back (first listed paints on top):
 *   1. top scrim        — melts the fixed charcoal header into the dusk sky
 *   2. cool sky tone     — a faint blue-charcoal lift across the upper third
 *                          so the warm pools below read warmer by contrast
 *   3. primary warm pool — the dominant amber bloom, pulled OFF-centre to a
 *                          rule-of-thirds / golden focal point (Fukinsei —
 *                          controlled asymmetry; one pool must dominate)
 *   4. secondary pool    — a smaller, dimmer warm accent on the opposite side
 *                          (a second "fixture", echoing real installs) so the
 *                          glow reads composed, not symmetrical
 *   5. low entry warmth  — a wide, very low amber wash at the base, where a
 *                          lit facade/entry meets the ground
 *   6. vignette          — corners fall away, pulling the eye to the house
 * Sits under the legibility gradient (::after), over the photo. Every amber
 * stop stays ≤0.12 alpha so AA contrast on the ivory copy is untouched.
 *
 * ENTRANCE (2026-06-17 council fix): this layer carries the hero reveal now
 * that the global brightness ramp is gone. It animates OPACITY ONLY (the
 * composed grade is paint-once; only its presence fades up), via fill-mode
 * backwards so the RESTING state is the finished frame — no-JS / no-animation
 * / reduced-motion all show the fully-graded hero, never a flat one. Reads as
 * the designed light pools "settling in" over the dusk photo. Compositor-only;
 * zero CLS (opacity can't reflow); zero LCP impact (the LCP <img> paints
 * independently at its static exposure). The layered background itself is still
 * never re-painted or scroll-linked — only opacity moves.
 */
.hero__grade {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  animation: hero-grade-settle 1200ms 300ms var(--ease-out-expo) backwards;
  background:
    /* 1 · top scrim */
    linear-gradient(
      to bottom,
      rgba(var(--photo-scrim-rgb), 0.60) 0%,
      rgba(var(--photo-scrim-rgb), 0.22) 9%,
      rgba(var(--photo-scrim-rgb), 0) 18%
    ),
    /* 2 · cool sky counter-tone (makes the warm pools read warmer) */
    linear-gradient(
      to bottom,
      rgba(var(--edge-cool-rgb),  0.10) 0%,
      rgba(var(--edge-cool-rgb),  0.04) 22%,
      rgba(var(--edge-cool-rgb),  0) 40%
    ),
    /* 3 · primary warm pool — dominant, off-centre (rule of thirds) */
    radial-gradient(
      ellipse 50% 40% at 38% 46%,
      rgba(var(--glow-rgb),  0.12) 0%,
      rgba(var(--glow-rgb),  0.05) 44%,
      rgba(var(--glow-rgb),  0) 70%
    ),
    /* 4 · secondary warm accent — smaller, dimmer, opposite side
          (same brand amber #c8922a as the primary pool — one gold, not two) */
    radial-gradient(
      ellipse 30% 26% at 71% 56%,
      rgba(var(--glow-rgb),  0.07) 0%,
      rgba(var(--glow-rgb),  0.025) 50%,
      rgba(var(--glow-rgb),  0) 75%
    ),
    /* 5 · low entry warmth — wide, very low amber wash at the base */
    radial-gradient(
      ellipse 120% 34% at 50% 100%,
      rgba(var(--glow-rgb),  0.06) 0%,
      rgba(var(--glow-rgb),  0) 60%
    ),
    /* 6 · vignette */
    radial-gradient(
      ellipse 130% 100% at 50% 55%,
      rgba(var(--photo-scrim-rgb), 0) 56%,
      rgba(var(--photo-scrim-rgb), 0.42) 100%
    );
}

/* The grade "settles in" — opacity only (compositor); rest = finished frame. */
@keyframes hero-grade-settle {
  from { opacity: 0; }
}

/* Scroll-driven deepening — native where supported */
@supports (animation-timeline: scroll()) {
  .hero::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(
      to top,
      rgba(var(--photo-scrim-rgb), 0) 0%,
      rgba(var(--photo-scrim-rgb), 0) 100%
    );
    animation: hero-scroll-darken linear both;
    animation-timeline: scroll(root block);
    animation-range: 0% 40vh;
  }

  @keyframes hero-scroll-darken {
    from { opacity: 0; }
    to   { opacity: 1; background: rgba(var(--photo-scrim-rgb), 0.55); }
  }
}

.hero__content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(var(--space-6), 5vw, var(--space-16));
  /*
   * Match-strike: the wrapper itself never animates — the H1 is in
   * the first paint. Children stagger individually (backwards fill;
   * rest = settled). Replaces the old 600ms-delayed whole-block fade,
   * which held ALL hero text invisible until 600ms.
   */
}

@keyframes hero-rise {
  from { opacity: 0; transform: translateY(18px); }
}

.hero__eyebrow {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  margin-bottom: var(--space-6);
  animation: hero-rise 700ms var(--ease-out-expo) backwards;
}

.hero__headline {
  font-family: var(--font-display);
  font-size: var(--text-hero);
  font-weight: 300;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-ivory);
  max-width: 14ch;
  margin-bottom: var(--space-6);
  animation: hero-rise 700ms var(--ease-out-expo) backwards;
}

.hero__headline em {
  font-style: italic;
  color: var(--color-amber-light);
}

/*
 * The match-strike: "Shine" rests LIT — amber with a soft static
 * halo (the halo never animates; it fades in with the word's own
 * opacity). The ignition is opacity + filter only (compositor):
 * the word warms up from near-dark, overshoots to a brief filament
 * brightness at 70%, and settles. No flicker — a slow warm bloom.
 */
.hero .hero__headline em {
  /* Resting lit state = the 0%/100% frame of the pulse, so the loop is seamless. */
  text-shadow: 0 0 28px rgba(var(--glow-warm-rgb),  0.38),
               0 0 72px rgba(var(--glow-rgb),  0.22);
  /*
   * Two animations on one element:
   *  1. shine-ignite — one-shot warm-up (900ms after a 500ms delay).
   *  2. shine-pulse  — a continuous, seamless "light breathing" loop that
   *     STARTS exactly when ignite finishes (delay 1400ms = 500 + 900) so it
   *     flows straight out of the ignition with no jump.
   * Performance: animates ONLY text-shadow + opacity on a tiny inline element
   * — no layout, no reflow, negligible paint. No JS, no extra requests.
   */
  animation:
    shine-ignite 900ms 500ms var(--ease-out-expo) forwards,
    shine-pulse 3.6s 1400ms ease-in-out infinite;
}

@keyframes shine-ignite {
  0%   { opacity: 0.1; filter: brightness(0.5); }
  70%  { opacity: 1;   filter: brightness(1.3); }
  100% { opacity: 1;   filter: brightness(1); }
}

/*
 * Seamless glow breath — the light visibly rises and settles, like a bulb
 * breathing. Start/end frames match the resting lit state so each loop closes
 * invisibly. Animates text-shadow + filter(brightness) only: a tiny inline
 * element, no layout/reflow — negligible paint, no JS, no requests.
 * Bigger swing than the first pass (that was too faint to read as motion).
 */
@keyframes shine-pulse {
  0%, 100% {
    filter: brightness(1);
    text-shadow: 0 0 26px rgba(var(--glow-warm-rgb),  0.34),
                 0 0 64px rgba(var(--glow-rgb),  0.18);
  }
  50% {
    filter: brightness(1.22);
    text-shadow: 0 0 44px rgba(var(--glow-warm-rgb),  0.72),
                 0 0 120px rgba(var(--glow-rgb),  0.46);
  }
}

.hero__subline {
  font-family: var(--font-body);
  font-size: clamp(var(--text-base), 2vw, var(--text-xl));
  font-weight: 300;
  color: var(--color-ivory-muted);
  max-width: 46ch;
  line-height: var(--leading-relaxed);
  margin-bottom: var(--space-10);
  animation: hero-rise 700ms 150ms var(--ease-out-expo) backwards;
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  animation: hero-rise 700ms 300ms var(--ease-out-expo) backwards;
}

@media (prefers-reduced-motion: reduce) {
  .hero__bg,
  .hero::after,
  .hero__grade,
  .hero__eyebrow,
  .hero__headline,
  .hero .hero__headline em,
  .hero__subline,
  .hero__actions {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .hero .hero__headline em {
    filter: none;
  }
  /* Only the photo gets the settled exposure — never dim the copy */
  .hero__bg {
    filter: brightness(0.75) saturate(1.05);
  }
}

@media (max-width: 640px) {
  .hero__headline {
    font-size: clamp(2.8rem, 10vw, 4rem);
  }
}

/* Intent map 2026-11-08 (owner-ruled): the eyebrow label now sits INSIDE the home H1 so
   "Nashville Outdoor Lighting" is part of the heading. Keep it looking exactly like the
   old standalone label: its own line, label metrics, free of the H1's ch measure. */
.hero__headline .hero__eyebrow {
  display: block;
  width: max-content;
  max-width: min(calc(100vw - 3rem), 44rem);
  line-height: 1.5;
  font-style: normal;
  text-wrap: wrap;
  animation: none;
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — ANSWER BLOCK (SEO lead paragraph)
   ════════════════════════════════════════════════════════════ */

/*
 * ANSWER BLOCK — restyled 2026-08-12.
 *
 * The TEXT is a frozen retrieval surface (AI engines and featured snippets
 * extract it verbatim, and it is in the baseline manifest). So this change is
 * PRESENTATIONAL ONLY — same words, same DOM position, same extractability.
 *
 * What was wrong: it rendered as a filled grey card with a hard 3px rule,
 * sitting directly under a cinematic hero. It was the visually heaviest thing
 * on the page and read as a metadata panel rather than an opening line — the
 * one "boxed slab of raw text" in a design system that otherwise runs on
 * hairlines, space, and restraint.
 *
 * Now it reads as an editorial LEDE: no fill, no box, a single amber hairline,
 * larger display-adjacent type and open leading. Same information, correct
 * register for a premium brand.
 */
.answer-block {
  position: relative;
  padding: var(--space-2) 0 var(--space-2) var(--space-8);
  margin-bottom: var(--space-12);
}

/* Hairline, not a slab edge — matches the .divider ornament used site-wide. */
.answer-block::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(
    180deg,
    var(--color-amber) 0%,
    rgba(var(--edge-gold-rgb),  0.35) 55%,
    transparent 100%
  );
}

@media (max-width: 640px) {
  .answer-block { padding-left: var(--space-5); }
}

.answer-block p {
  /* DEMOTED (2026-08-25). This was clamp(--text-lg, 1.6vw, --text-xl) — 20-25px,
     "a step up from body" — which made a definitional statement the largest
     text on the page after the hero, and pushed the display headline (the
     actual creative idea) into second place directly beneath it. Three text
     blocks at near-equal weight read as one wall of prose.

     It is now a standing "who we are" lede: body-sized, muted, slightly
     tracked. It still sits FIRST in the DOM — extraction reads source order,
     not font-size, so the AI-citation value is unchanged — but the eye now
     goes to the headline. */
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  color: var(--color-ivory-muted);
  letter-spacing: 0.012em;
  /* Narrower measure than before (58ch): at this smaller size the same
     character count would run to more lines and rebuild the wall. */
  max-width: 52ch;
  font-weight: 300;
}

/* Closing variant (2026-08-26) — the block now sits BELOW the headline rather
   than opening the section. The separation (rule + spacing) lives on the
   eyebrow that precedes it, not here. */
.answer-block--closing {
  margin-top: var(--space-6);
}

/* Full width here. The base .answer-block p rule caps at 52ch, which is right
   when the block opens a section; in the closing position it is half the
   reason the copy ran to 7 lines. (The other half was the container's own
   58% cap — see .intro-shell .answer-block near the end of this file.)
   No !important needed: this selector is more specific than .answer-block p. */
.answer-block--closing p {
  max-width: none;
}

/* The eyebrow immediately precedes the closing block, so IT carries the rule
   and the spacing — otherwise the separator would fall between the label and
   the text it labels.

   Done with an explicit class rather than :has(), which was the first attempt.
   :has() is well supported now, but a selector that silently no-ops on an
   older browser is a poor way to hold a layout together, and it made the
   cascade harder to reason about against the base rule. */
.answer-block__label--closing {
  margin-top: var(--space-12);
  padding-top: var(--space-10);
  border-top: var(--border-fine);
}

/* Small-caps eyebrow naming what the block is, so it reads as a deliberate
   standing statement rather than an orphaned opening paragraph. */
.answer-block__label {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  margin-bottom: var(--space-3);
  /* Sits outside .answer-block (see the note in index.html), so it needs the
     block's own left padding to stay aligned with the text beside the rule. */
  padding-left: var(--space-8);
}

@media (max-width: 640px) {
  .answer-block__label { padding-left: var(--space-5); }
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — SERVICES OVERVIEW
   ════════════════════════════════════════════════════════════ */

.services-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6);
}

/* Permanent lighting spans full width — it's the money-page wedge */
.services-grid .service-card--featured {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
}

.service-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--color-canvas-raised);
  border: var(--border-fine);
  border-radius: var(--radius-md);
  overflow: hidden;
  text-decoration: none;
  transition:
    border-color var(--dur-ui) var(--ease-out-strong),
    box-shadow var(--dur-ui) var(--ease-out-strong),
    transform var(--dur-ui) var(--ease-out-strong);
}

/* Light-source wash: a faint amber bloom bleeding down from the top edge,
   evoking a fixture washing the card from above. Decorative paint-only
   overlay (no layout box), gated to the text-only cards so the featured
   card keeps its own image-panel treatment as the dominant wedge. */
.service-card:not(.service-card--featured)::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 45%;
  pointer-events: none;
  background-image: radial-gradient(
    120% 90% at 50% 0%,
    rgba(var(--glow-warm-rgb),  0.10) 0%,
    rgba(var(--glow-warm-rgb),  0.04) 38%,
    transparent 72%
  );
  opacity: 0.85;
  transition: opacity var(--dur-ui) var(--ease-out-strong);
}

/* Thin warm hairline along the very top edge — the lit lip of the fixture.
   Sits above the wash, brightens on hover. */
.service-card:not(.service-card--featured)::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  pointer-events: none;
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(var(--edge-warm-rgb),  0.30) 50%,
    transparent 100%
  );
  opacity: 0.7;
  transition: opacity var(--dur-ui) var(--ease-out-strong);
}

.service-card:hover,
.service-card:focus-visible {
  border-color: rgba(var(--edge-gold-rgb),  0.35);
  box-shadow: var(--shadow-lift), var(--glow-amber-sm);
  transform: translateY(-3px);
}

/* Hover: the wash blooms (brighter, fuller) and the lit lip warms up —
   reads as the fixture coming up to full output. Opacity-only. */
.service-card:not(.service-card--featured):hover::before,
.service-card:not(.service-card--featured):focus-visible::before {
  opacity: 1;
}

.service-card:not(.service-card--featured):hover::after,
.service-card:not(.service-card--featured):focus-visible::after {
  opacity: 1;
}

.service-card__image-wrap {
  overflow: hidden;
}

.service-card__image {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  transition:
    filter var(--dur-reveal) var(--ease-out-expo),
    transform var(--dur-hero) var(--ease-out-expo);
}

@media (scripting: enabled) {
  .service-card__image {
    filter: brightness(0.4) saturate(0.7);
    transform: scale(1.06);
  }
}

.service-card:hover .service-card__image,
.service-card__image.is-visible {
  filter: brightness(0.9) saturate(1);
  transform: scale(1);
}

.service-card__body {
  position: relative; /* lift body content above the light-source wash overlay */
  z-index: 1;
  padding: var(--space-6) var(--space-8);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.service-card__label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  margin-bottom: var(--space-3);
}

.service-card__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--color-ivory);
  margin-bottom: var(--space-3);
  line-height: var(--leading-snug);
}

.service-card__text {
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  flex: 1;
  margin-bottom: var(--space-6);
}

/* Featured card overrides */
.service-card--featured .service-card__image {
  aspect-ratio: auto;
  height: 100%;
}

.service-card--featured .service-card__image-wrap {
  height: 100%;
  min-height: 340px;
}

/* The featured card's wrap is a full-height column, not a 16/9 band — let the
   photo fill it rather than letterbox inside the default aspect-ratio. */
.service-card--featured .service-card__image {
  height: 100%;
  min-height: 340px;
  aspect-ratio: auto;
  object-position: center 55%;
}

.service-card--featured .service-card__body {
  padding: var(--space-10) var(--space-10);
  justify-content: center;
}

.service-card--featured .service-card__title {
  font-size: clamp(var(--text-3xl), 3vw, var(--text-4xl));
}

.service-card--featured .service-card__badge {
  display: inline-block;
  background: var(--color-amber);
  color: var(--color-canvas);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: var(--space-1) var(--space-4);
  border-radius: var(--radius-pill);
  margin-bottom: var(--space-5);
}

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

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

  .services-grid .service-card--featured {
    grid-column: 1;
    display: flex;
    flex-direction: column;
  }

  .service-card--featured .service-card__image-wrap {
    min-height: 240px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .service-card,
  .service-card__image {
    transition: none;
  }
  .service-card__image {
    filter: brightness(0.85) saturate(1);
    transform: none;
  }
  .service-card:hover {
    transform: none;
  }
  /* Light-source wash stays as a fixed resting state — no transition,
     no hover bloom — so the lit look is intentional, not animated. */
  .service-card:not(.service-card--featured)::before,
  .service-card:not(.service-card--featured)::after {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   .card — "what we light" technique cards (service-page .cols-3 grid).
   A deliberately QUIETER sibling of .service-card: same charcoal-raised
   material, fine border, radius, and a top amber light-wash + lit hairline
   — but FAINTER, and with NO hover lift, because these are informational
   (no link / not clickable). Echoes the design language at lower intensity
   so the site reads as ONE bespoke system, not two card styles.
   ───────────────────────────────────────────────────────────────────── */
.card {
  position: relative;
  background: var(--color-canvas-raised);
  border: var(--border-fine);
  border-radius: var(--radius-md);
  overflow: hidden;
}

/* Light-source wash — same fixture-from-above gesture as .service-card,
   dialled down (~60% of the service-card alpha, no transition) so it reads
   as a calm resting state, not an interactive surface. Paint-only overlay. */
.card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 45%;
  pointer-events: none;
  background-image: radial-gradient(
    120% 90% at 50% 0%,
    rgba(var(--glow-warm-rgb),  0.06) 0%,
    rgba(var(--glow-warm-rgb),  0.025) 38%,
    transparent 72%
  );
}

/* Lit top hairline — the lip of the fixture, fainter than the service-card's
   so the technique cards stay subordinate to the clickable service wedges. */
.card::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  pointer-events: none;
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(var(--edge-warm-rgb),  0.20) 50%,
    transparent 100%
  );
  opacity: 0.7;
}

.card__body {
  position: relative; /* lift content above the light-source wash overlay */
  z-index: 1;
  padding: var(--space-6) var(--space-8);
}

.card__label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  margin-bottom: var(--space-3);
}

.card__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--color-ivory);
  margin-bottom: var(--space-3);
  line-height: var(--leading-snug);
}

.card__text {
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  margin-bottom: 0;
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — PORTFOLIO BY NIGHT (light-reveal gallery)
   ════════════════════════════════════════════════════════════ */

.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto;
  gap: var(--space-4);
}

.portfolio-item {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: var(--border-fine);
  cursor: pointer;
}

/* Hero slot spans 2 rows */
.portfolio-item--hero {
  grid-row: span 2;
}

.portfolio-item__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* filter keeps the slow reveal-bloom timing ("light coming on");
     transform is hover-exclusive (the 1.03 zoom) so it gets the
     interaction duration — a 1.4s hover zoom reads broken, not luxe. */
  transition:
    filter var(--dur-reveal) var(--ease-out-expo),
    transform var(--dur-ui) var(--ease-out-strong);
  min-height: 280px;
}

@media (scripting: enabled) {
  .portfolio-item__image {
    filter: brightness(0.3) saturate(0.5);
  }
}

.portfolio-item--hero .portfolio-item__image {
  min-height: 560px;
}

.portfolio-item.is-visible .portfolio-item__image {
  filter: brightness(0.85) saturate(1);
}

.portfolio-item:hover .portfolio-item__image {
  filter: brightness(1) saturate(1.05);
  transform: scale(1.03);
}

/* Caption overlay */
.portfolio-item__caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: var(--space-6) var(--space-6);
  background: linear-gradient(to top, rgba(var(--photo-scrim-rgb), 0.92) 0%, transparent 100%);
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity var(--dur-ui) var(--ease-out-strong),
    transform var(--dur-ui) var(--ease-out-strong);
}

.portfolio-item:hover .portfolio-item__caption,
.portfolio-item:focus-within .portfolio-item__caption {
  opacity: 1;
  transform: translateY(0);
}

.portfolio-item__caption-label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
}

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

  .portfolio-item--hero {
    grid-column: 1 / -1;
    grid-row: auto;
  }

  .portfolio-item--hero .portfolio-item__image {
    min-height: 320px;
  }
}

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

@media (prefers-reduced-motion: reduce) {
  .portfolio-item__image {
    filter: brightness(0.85);
    transition: none;
  }
  .portfolio-item:hover .portfolio-item__image {
    transform: none;
  }
  .portfolio-item__caption {
    transition: none;
  }
  .portfolio-item.is-visible .portfolio-item__image {
    filter: brightness(0.85);
  }
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — PROCESS STEPS
   ════════════════════════════════════════════════════════════ */

.process-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-8);
  counter-reset: step-counter;
}

.process-step {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  counter-increment: step-counter;
  position: relative;
  padding-top: var(--space-12);
}

/* Step number */
.process-step::before {
  content: counter(step-counter, decimal-leading-zero);
  position: absolute;
  top: 0;
  left: 0;
  font-family: var(--font-display);
  font-size: var(--text-5xl);
  font-weight: 300;
  line-height: 1;
  color: rgba(var(--edge-gold-rgb),  0.20);
  letter-spacing: var(--tracking-tight);
  pointer-events: none;
  user-select: none;
}

/* Connector line between steps */
.process-step:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 24px;
  left: calc(100% + var(--space-4));
  width: calc(var(--space-8) - var(--space-2));
  height: 1px;
  background: linear-gradient(90deg, var(--color-amber), transparent);
}

.process-step__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--color-ivory);
  line-height: var(--leading-snug);
}

.process-step__text {
  font-size: var(--text-base);
  color: var(--color-ivory-muted);
  line-height: var(--leading-relaxed);
}

@media (max-width: 768px) {
  .process-steps {
    grid-template-columns: 1fr;
    gap: var(--space-12);
  }

  .process-step::after {
    display: none;
  }
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — TESTIMONIALS
   ════════════════════════════════════════════════════════════ */

.testimonials-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6);
}

@media (max-width: 900px) {
  .testimonials-grid {
    grid-template-columns: 1fr;
    max-width: 640px;
    margin-inline: auto;
  }
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — CONTACT CTA BAND
   ════════════════════════════════════════════════════════════ */

.contact-cta {
  text-align: center;
  position: relative;
  overflow: hidden;
  isolation: isolate;

  /*
   * Materiality treatment (no photo): DEEP-NIGHT base with a pronounced
   * amber light-bloom radiating from centre — "a pool of warm light in deep
   * night" (owner direction 2026-06-17). Pre-composed paint-once layers,
   * zero runtime filters. Deeper than the page base so the band reads as its
   * own moment; the amber pool carries the section instead of a flat fill.
   */
  background-color: var(--color-canvas-deep);
  background-image:
    var(--grain-url),
    radial-gradient(ellipse 70% 90% at 50% 42%, rgba(var(--glow-rgb), 0.16) 0%, rgba(var(--glow-rgb), 0.07) 34%, rgba(var(--glow-rgb), 0) 72%),
    linear-gradient(to bottom, #14182400 0%, #1b0d04 100%);
  background-size: 96px 96px, 100% 100%, 100% 100%;
  background-repeat: repeat, no-repeat, no-repeat;
  border-top: var(--border-fine);
  border-bottom: var(--border-fine);
}

.contact-cta::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse at center, rgba(var(--glow-rgb), 0.06) 0%, rgba(var(--glow-rgb), 0) 70%);
}

/* Photo sits UNDER the approved amber-bloom treatment (owner direction
   2026-06-17 kept intact): the gradients above still carry the band, the
   photograph only gives it depth. Held well back so the CTA copy and the
   phone number stay the brightest things in the section. */
.contact-cta__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 60%;
  z-index: -2;
  opacity: 0.34;
  filter: brightness(0.55) saturate(0.9);
}

.contact-cta__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  padding-block: clamp(var(--space-20), 10vw, var(--space-32));
}

.contact-cta__title {
  font-family: var(--font-display);
  font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
  font-weight: 300;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-ivory);
  max-width: 20ch;
  text-align: center;
}

.contact-cta__title em {
  font-style: italic;
  color: var(--color-amber-light);
}

.contact-cta__phone {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: clamp(var(--text-2xl), 4vw, var(--text-4xl));
  font-weight: 300;
  color: var(--color-ivory);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out-expo);
}

.contact-cta__phone:hover,
.contact-cta__phone:focus-visible {
  color: var(--color-amber-light);
}

.contact-cta__actions {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  justify-content: center;
}

/* ════════════════════════════════════════════════════════════
   CONTACT FORM SECTION
   ════════════════════════════════════════════════════════════ */

.contact-form-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-16);
  align-items: start;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.contact-form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
}

.contact-form__note {
  font-size: var(--text-sm);
  color: var(--color-ivory-muted);
  margin-top: var(--space-3);
}

@media (max-width: 900px) {
  .contact-form-section {
    grid-template-columns: 1fr;
  }

  .contact-form__row {
    grid-template-columns: 1fr;
  }
}


/* ════════════════════════════════════════════════════════════
   PAGE WRAPPER — accounts for fixed header
   ════════════════════════════════════════════════════════════ */

.page-main {
  padding-top: var(--header-h);
}

/* Inner page hero (non-homepage) — materiality treatment, no photo.
   Photo budget (≤2 appearances per image sitewide) is spent on the
   homepage hero, content bands, and the portfolio proof page. */
.page-hero {
  padding-block: clamp(var(--space-20), 12vw, var(--space-32));
  text-align: center;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background-color: var(--color-canvas-sunken);
  background-image:
    var(--grain-url),
    var(--bloom-amber-soft),
    var(--charcoal-depth);
  background-size: 96px 96px, 100% 100%, 100% 100%;
  background-repeat: repeat, no-repeat, no-repeat;
  border-bottom: var(--border-fine);
}

.page-hero__content {
  position: relative;
  z-index: 1;
}

/* Inner page H1 (extracted from a repeated inline clamp) */
.page-hero__title {
  font-family: var(--font-display);
  font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
  font-weight: 600;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-ivory);
  margin-top: var(--space-3);
  text-wrap: balance;
}


/* ════════════════════════════════════════════════════════════
   UTILITY
   ════════════════════════════════════════════════════════════ */

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

.text-center { text-align: center; }
.text-left   { text-align: left; }

.mt-auto { margin-top: auto; }

/* ════════════════════════════════════════════════════════════
   INNER-PAGE GRID UTILITIES
   Responsive column grids for the fast-built inner pages.
   Inline styles cannot carry media queries — these can.
   Gap is supplied inline per instance (varies by section).
   ════════════════════════════════════════════════════════════ */

.cols-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.cols-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.cols-4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

/* 50/50 text + image feature split (process steps, service splits) */
.cols-feature {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(var(--space-8), 5vw, var(--space-16));
  align-items: center;
}

/* Portrait + bio split (about page) */
.cols-bio {
  display: grid;
  grid-template-columns: 1fr 1.6fr;
  gap: clamp(var(--space-8), 5vw, var(--space-16));
  align-items: start;
}

@media (max-width: 1024px) {
  .cols-3 {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 900px) {
  .cols-4 {
    grid-template-columns: 1fr 1fr;
  }
  .cols-feature,
  .cols-bio {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 768px) {
  .cols-2 {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 640px) {
  .cols-3 {
    grid-template-columns: 1fr;
  }
}


/* ════════════════════════════════════════════════════════════
   FAQ ACCORDION (shared — /faq/, /process/, service pages)
   Native <details>/<summary>; styling only, no JS dependency.
   ════════════════════════════════════════════════════════════ */

.faq-list {
  display: flex;
  flex-direction: column;
}

.faq-item {
  border-bottom: var(--border-fine);
}

.faq-item__question {
  position: relative;
  padding: var(--space-5) var(--space-12) var(--space-5) 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--color-ivory);
  line-height: var(--leading-snug);
  list-style: none;
  transition: color var(--dur-fast) var(--ease-out-expo);
}

.faq-item__question::-webkit-details-marker {
  display: none;
}

/* Chevron marker — rotates open */
.faq-item__question::after {
  content: '';
  position: absolute;
  right: var(--space-3);
  top: calc(var(--space-5) + 0.35em);
  width: 9px;
  height: 9px;
  border-right: 1.5px solid var(--color-amber-light);
  border-bottom: 1.5px solid var(--color-amber-light);
  transform: rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-out-expo);
}

.faq-item[open] > .faq-item__question::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.faq-item__question:hover {
  color: var(--color-amber-light);
}

.faq-item__answer {
  padding: 0 0 var(--space-6);
  color: var(--color-ivory-muted);
  line-height: var(--leading-relaxed);
}

.faq-item__answer p {
  max-width: 68ch;
}

.faq-item__answer p + p {
  margin-top: var(--space-3);
}

@media (prefers-reduced-motion: reduce) {
  .faq-item__question,
  .faq-item__question::after {
    transition: none;
  }
}


/* ════════════════════════════════════════════════════════════
   TESTIMONIAL CARD (standalone quote band — /process/)
   ════════════════════════════════════════════════════════════ */

.testimonial-card {
  padding: var(--space-10) var(--space-10);
  border-left: 2px solid var(--color-amber);
  background: var(--color-canvas-raised);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}

.testimonial-card__quote {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 300;
  font-style: italic;
  line-height: var(--leading-relaxed);
  color: var(--color-ivory);
  margin-bottom: var(--space-5);
  max-width: 60ch;
}

.testimonial-card__cite {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  font-style: normal;
}

.testimonial-card__name {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-ivory-muted);
}

.testimonial-card__location {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--color-ivory-muted);
}


/* Amber glow ambient for section backgrounds */
.ambient-glow {
  position: relative;
}

.ambient-glow::before {
  content: '';
  position: absolute;
  width: 600px;
  height: 600px;
  background: radial-gradient(circle, rgba(var(--glow-rgb),  0.07) 0%, transparent 70%);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 0;
}

.ambient-glow > * {
  position: relative;
  z-index: 1;
}

/* ════════════════════════════════════════════════════════════
   VIEWPORT-SCALE DISPLAY TYPE (Phase-1 premium sprint)
   One line per surface: homepage hero, money-page opening,
   About opening. Real HTML text, always.
   ════════════════════════════════════════════════════════════ */

.display-vp {
  font-family: var(--font-display);
  /* Gate-C composition (premium-polish loop): the 8.5rem ceiling is logo/numeral
     scale — on a multi-word phrase H1 (homepage "Your Home Deserves to Shine",
     About "15 Years of Craft. / One Family. One Community.") it shatters the
     authored line-break into 4+ ragged lines and runs the column flush/over-wide.
     Cap the ceiling to phrase scale and give the column a finite ch measure so
     each authored line sets clean and centred. Text/markup unchanged. */
  font-size: clamp(2.75rem, 6.4vw, 5.25rem);
  font-weight: 300;
  line-height: 1.06;
  letter-spacing: -0.015em;
  color: var(--color-ivory);
  text-wrap: balance;
  max-width: 18ch;
}

/* Centre the capped measure ONLY in the centred page-hero context
   (About). The homepage hero stays left-anchored — no auto margin there. */
.page-hero .display-vp {
  margin-inline: auto;
}

.display-vp em {
  font-style: italic;
  color: var(--color-amber-light);
}


/* ════════════════════════════════════════════════════════════
   HOMEPAGE — PORTFOLIO FEATURE (single full-bleed photograph
   carries the fold; the gallery lives on /portfolio/)
   ════════════════════════════════════════════════════════════ */

.portfolio-feature {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: var(--border-fine);
  /* Mounted-print read: a hairline inner mat-rule + a deep soft lift so the
     plate sits framed and proud of the page, plus a faint warm edge-glow so
     the photograph reads as lit from within rather than a raw rectangle. */
  box-shadow:
    inset 0 0 0 1px rgba(var(--edge-warm-rgb),  0.10),
    inset 0 0 0 4px rgba(var(--photo-scrim-rgb), 0.55),
    var(--shadow-lift),
    var(--glow-amber-sm);
}

.portfolio-feature img {
  width: 100%;
  height: clamp(360px, 60vh, 640px);
  object-fit: cover;
  display: block;
}

/* Narrow viewports: the 360px clamp floor is TALLER than a phone-width plate
   is wide, which turns this 3:2 photograph into a portrait box and crops it to
   a vertical sliver. Below 600px let the ratio drive the height instead.
   The figcaption is absolutely positioned, so it still sits over the plate. */
@media (max-width: 599px) {
  .portfolio-feature img {
    height: auto;
    aspect-ratio: 4 / 3;
  }
}

.portfolio-feature figcaption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: var(--space-8) var(--space-8) var(--space-6);
  /* Cinematic multi-stop scrim: eased falloff (not a flat linear bar) so the
     caption reads like a graded gallery plate. Base holds ~0.94 at the foot to
     keep the amber-light label + ivory action above AA over a bright photo. */
  background: linear-gradient(
    to top,
    rgba(var(--photo-scrim-rgb), 0.94) 0%,
    rgba(var(--photo-scrim-rgb), 0.82) 22%,
    rgba(var(--photo-scrim-rgb), 0.42) 56%,
    rgba(var(--photo-scrim-rgb), 0) 100%
  );
}

.portfolio-feature__label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
}

/* Considered gallery action — scoped to this figcaption only so the shared
   site-wide .link-arrow is untouched. Reads as a deliberate "enter the room"
   action, not a default text link. */
.portfolio-feature figcaption .link-arrow {
  color: var(--color-ivory);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 2px rgba(var(--photo-scrim-rgb), 0.6);
}

.portfolio-feature figcaption .link-arrow .arrow {
  transition: transform var(--dur-fast, 220ms) var(--ease-out-expo);
  will-change: transform;
}

.portfolio-feature figcaption .link-arrow:hover .arrow,
.portfolio-feature figcaption .link-arrow:focus-visible .arrow {
  transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
  .portfolio-feature figcaption .link-arrow .arrow {
    transition: none;
  }
  .portfolio-feature figcaption .link-arrow:hover .arrow,
  .portfolio-feature figcaption .link-arrow:focus-visible .arrow {
    transform: none;
  }
}


/* ════════════════════════════════════════════════════════════
   VIEW TRANSITIONS — cross-document shared-element morphs
   Progressive enhancement: browsers without support navigate
   normally. Reduced motion collapses everything to instant.
   ════════════════════════════════════════════════════════════ */

@view-transition {
  navigation: auto;
}

/* Persistent chrome: header/footer hold still while content morphs */
.site-header { view-transition-name: site-header; }
.site-footer { view-transition-name: site-footer; }

/* Named shared elements (unique per page; pairs set in markup):
   vt-portfolio-feature  homepage feature photo ↔ /portfolio/ hero project
   vt-svc-<service>      homepage service-card title ↔ service-page H1   */
.vt-portfolio-feature { view-transition-name: vt-portfolio-feature; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 400ms;
  animation-timing-function: var(--ease-out-expo);
}

::view-transition-group(*) {
  animation-duration: 420ms;
  animation-timing-function: var(--ease-out-expo);
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-image-pair(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Services "what's included" spec list  (Nocturne system)
   Renders the service-page <ul class="services-list reveal"> as a considered
   two-column spec sheet rather than a browser-default bulleted stack.
   Tokens mirror .card__text (body), .card__label (amber eyebrow). Static —
   .reveal owns the entrance; no animation added here.
   ───────────────────────────────────────────────────────────────────────── */
.services-list {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: 0;
  /* CSS columns keep DOM order linear (one <ul>) while reading as a spec
     sheet on wider viewports; collapses to a single column on mobile. */
  columns: 1;
  column-gap: var(--space-12);
}

@media (min-width: 48em) {
  .services-list {
    columns: 2;
  }
}

.services-list li {
  position: relative;
  break-inside: avoid;            /* keep marker + text together across columns */
  padding: var(--space-3) 0 var(--space-3) var(--space-8);
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  border-top: 1px solid var(--color-canvas-border);
}

/* Custom amber marker — a small four-point point-of-light, echoing the
   fixture motif. Built from two hairline strokes (no image, no glyph-font
   dependency). Decorative only; the AA-failing thinness is non-text. */
.services-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--space-3) + 0.62em);   /* optical baseline of the first line */
  width: var(--space-4);
  height: var(--space-4);
  margin-top: -0.5em;
  background-image:
    linear-gradient(var(--color-amber-light), var(--color-amber-light)),
    linear-gradient(var(--color-amber-light), var(--color-amber-light));
  background-size: 100% 1px, 1px 100%;
  background-position: center, center;
  background-repeat: no-repeat;
  opacity: 0.92;
}

/* ─────────────────────────────────────────────────────────────────────────
   Location-page design notes  (Nocturne system)
   Renders the location-page <p class="design-note"> / <p class="neighborhood-note">
   considerations as recessed editorial annotation panels rather than flat
   default body text. The calm, NON-interactive sibling of .card (which is the
   quieter sibling of .service-card): same charcoal-raised surface + fine border
   + radius, but the light-motif accent is a single faint amber LEFT edge — an
   annotation rule, not the top-lit fixture wash — so these text panels stay
   subordinate to the clickable service wedges. Paint-only: no hover, no
   transition, no layout-shifting properties (will not disturb the .cols-2 grid).
   Styles only the note <p>; the sibling inline-styled <h3>s are untouched.
   ───────────────────────────────────────────────────────────────────────── */
.design-note,
.neighborhood-note {
  background: var(--color-canvas-raised);
  border: var(--border-fine);
  /* Faint amber annotation rule — fainter than .card's lit hairline (which is
     rgba(var(--edge-warm-rgb),0.20) @ 0.7). A 2px lit left edge ties to the light motif,
     quietly. */
  border-left: 2px solid rgba(var(--edge-warm-rgb),  0.14);
  border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-8);
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: 62ch;          /* comfortable reading measure */
  margin-bottom: 0;
}

/* ───────────────────────────────────────────────────────────────
 * PROCESS PAGE — timeline connective spine (V2 luxury elevation)
 * Threads the three alternating numbered steps (01·02·03) into one
 * continuous editorial sequence rather than three free-floating
 * blocks. Paint-only: a faint amber thread down the column gutter,
 * lit at the centre and fading to nothing at both ends so it reads
 * as a considered line of light, not a hard rule.
 *
 * Two-gate clearance:
 *   (A) SEO/GEO — ::before decoration only; no rankable text, no DOM
 *       change, no layout shift (absolute, behind content), byte
 *       identity preserved. .process-section is unique to this page
 *       (verified) so nothing leaks to index/about/garden.
 *   (B) decoration — deepens the page's ONE idea: a guided, step-by-
 *       step bespoke journey with Phil, sequenced top to bottom.
 * ─────────────────────────────────────────────────────────────── */
.process-section .container {
  position: relative;
}

.process-section .container::before {
  content: '';
  position: absolute;
  /* centre of the padded content box = the column gutter the
     alternating steps share */
  left: 50%;
  transform: translateX(-50%);
  /* start below the section heading block, end before the section
     padding so the thread lives only across the numbered steps */
  top: var(--space-20);
  bottom: var(--space-8);
  width: 1px;
  /* lit in the middle, fading to transparent at both ends */
  background: linear-gradient(
    to bottom,
    transparent 0%,
    rgba(var(--edge-warm-rgb),  0.18) 18%,
    rgba(var(--edge-warm-rgb),  0.22) 50%,
    rgba(var(--edge-warm-rgb),  0.18) 82%,
    transparent 100%
  );
  z-index: 0;
  pointer-events: none;
}

/* Keep the step content above the thread (no stacking ambiguity). */
.process-section .cols-feature {
  position: relative;
  z-index: 1;
}

/* Below the cols-feature collapse point the grid is a single column,
   so the centre gutter no longer exists — suppress the spine so the
   stacked-mobile layout (the buyer's iPad/phone) stays clean. */
@media (max-width: 900px) {
  .process-section .container::before {
    display: none;
  }
}


/* ════════════════════════════════════════════════════════════
   RECESSED FORM FIELDS — light-as-material refinement
   ────────────────────────────────────────────────────────────
   The enquiry field at rest reads as a dark recess carved into
   the surface (subtle inner shadow), so focus feels like the
   amber light filling the slot rather than a flat box gaining a
   border. Deepens the site's one idea: light is the material.
   Deliberate GLOBAL refinement of the shared field component
   (home + contact use the identical .form-input/.form-textarea/
   .form-select); kept global so both enquiry forms stay byte-
   identical. Paint-only: inset shadow lives inside the box model,
   so NO layout shift. The amber focus ring (border-color +
   spread shadow in base.css) is preserved and visually unchanged
   in its keyboard-accessibility role — this only adds resting
   depth and lifts the inner shadow on focus. */
.form-input,
.form-textarea,
.form-select {
  box-shadow: inset 0 1px 2px rgba(var(--shadow-rgb),  0.28);
  transition: border-color var(--dur-fast) var(--ease-out-expo),
              box-shadow var(--dur-fast) var(--ease-out-expo);
}

.form-input:focus,
.form-textarea:focus,
.form-select:focus {
  box-shadow: inset 0 1px 2px rgba(var(--shadow-rgb),  0.18),
              0 0 0 3px var(--color-amber-glow-md);
}

/* ════════════════════════════════════════════════════════════
   PORTFOLIO — featured hero project, material framing
   (V2 luxury elevation · portfolio/index.html only)
   ────────────────────────────────────────────────────────────
   The page's marquee plate. The inline <style> block in the page
   already sizes the .portfolio-hero-project wrap, its image, and
   the bottom-scrim overlay; it sets NO box-shadow on the wrap and
   NO ::after on the overlay — so the two refinements below add
   only new, non-conflicting paint (cascade order is moot, no tie
   to lose). .portfolio-hero-project is unique to this page
   (grep-verified) so nothing leaks to other page types.

   Two-gate clearance:
     (A) SEO/GEO — paint-only: a box-shadow on the wrap and a
         decorative ::after on the overlay. No rankable text, no
         DOM change, no layout shift (shadows live in the box
         model / the ::after is absolute behind the caption text),
         byte identity preserved, no hover on this non-interactive
         article. AA unaffected (caption colours untouched).
     (B) decoration — deepens the site's ONE idea (light is the
         material): the marquee photo now reads as a framed, lit
         object recessed into the page, with a faint warm light
         pooling under the caption as if cast by the install —
         not a flush rectangle behind a flat black bar.
   ──────────────────────────────────────────────────────────── */

/* Framed-plate depth: a hairline lit top edge + a soft recess,
   so the featured project sits IN the page like a lit object
   rather than flush against it. Mirrors the .card / recessed-
   field materiality language already in this file. */
.portfolio-hero-project {
  box-shadow:
    inset 0 1px 0 rgba(var(--edge-warm-rgb),  0.10),   /* fixture-lit top edge */
    inset 0 0 0 1px rgba(var(--shadow-rgb),  0.30),        /* fine recessed frame */
    0 18px 40px -24px rgba(var(--shadow-rgb),  0.55);      /* the plate's own cast */
}

/* Warm light-pool under the caption: the eyebrow + title read as
   lit by the installation, not stamped on a scrim. Absolute, sits
   BEHIND the overlay's text (z-index:0 vs the natural flow of the
   span/h2), pointer-inert. Layers on TOP of the inline overlay's
   bottom gradient — does not replace it. */
.portfolio-hero-project__overlay {
  position: absolute;            /* (re-stated; already set inline) */
  isolation: isolate;            /* own stacking context for the ::after */
}
.portfolio-hero-project__overlay > * {
  position: relative;
  z-index: 1;                    /* keep caption above the light-pool */
}
.portfolio-hero-project__overlay::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 60%;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
    120% 100% at 18% 100%,
    rgba(var(--glow-rgb),  0.16) 0%,
    rgba(var(--glow-rgb),  0.06) 38%,
    transparent 72%
  );
}


/* ════════════════════════════════════════════════════════════
   FAQ ACCORDION — open-state affordance (luxury elevation V2)
   Quiet "light revealing the answer" cue: the open item warms
   to a lit amber left-rule + a faint ambient tint, so the answer
   you're reading is unmistakable in a long 33-item, 7-section list.
   Pure paint — interaction-only (native <details>); the closed
   default is byte-identical, zero CLS, no text reflow.
   ════════════════════════════════════════════════════════════ */

.faq-item[open] {
  /* Lit left-rule via inset shadow (no border = no text shift) +
     a barely-there ambient warmth across the open block. */
  box-shadow: inset 2px 0 0 0 rgba(var(--edge-warm-rgb),  0.55);
  background:
    linear-gradient(
      90deg,
      var(--color-amber-glow) 0%,
      rgba(var(--glow-rgb),  0) 38%
    );
  transition: box-shadow var(--dur-base) var(--ease-out-expo),
              background var(--dur-base) var(--ease-out-expo);
}

/* The open question carries the warmth in its type, too. */
.faq-item[open] > .faq-item__question {
  color: var(--color-amber-light);
}

@media (prefers-reduced-motion: reduce) {
  .faq-item[open] {
    transition: none;
  }
}


/* ════════════════════════════════════════════════════════════
   SECTION EYEBROW — struck point-of-light mark (luxury elevation V2)
   ────────────────────────────────────────────────────────────
   .eyebrow is the site's single most-repeated section-marker
   grammar — a small-caps amber label opening nearly every section
   on all 19 pages. In its bare form (caps + tracking + amber) it
   reads as the generic agency kicker the AI-tell detector flags as
   the "all-caps section label" default. It was also the one
   recurring primitive that never received the material-depth pass
   every other component got (.card, .services-list, .design-note,
   the recessed fields, the portfolio plate, the faq warmth).

   This adds a single short "struck filament" — a tiny lit amber
   rule with a brighter ignition point at its leading edge — before
   the label text, tying the most-seen marker to the site's ONE
   idea (light is the material) and mirroring the point-of-light
   marker language already established in .services-list. Quiet,
   bespoke, on-narrative — it de-tells the default label without
   touching its copy.

   Two-gate clearance:
     (A) SEO/GEO — pure decorative ::before (content:""), no DOM or
         HTML change, no rankable text touched, page byte-identity
         preserved, AA unaffected (label colour unchanged). Paint
         only: the mark is an inline-block rendered INSIDE the
         existing block-level .eyebrow span and consumes horizontal
         space the short label already had (never near wrap), so it
         introduces no reflow of the heading below — zero CLS. The
         hero label (.hero__eyebrow) is a different class and is
         untouched.
     (B) decoration — deepens the site's single narrative: every
         section now opens with a small struck point of light rather
         than a plain caps label. On-narrative, bespoke, not the
         page's default grammar after this pass.
   ──────────────────────────────────────────────────────────── */
/* ── BRAND-HARMONY REVISION (2026-08-19) ─────────────────────────
   The 2026-08-19 logo artwork settled a question this component had
   already answered by accident. Measured directly off
   gemmell-lockup-h252.png: the mark's sub-line is a wide-tracked,
   all-caps, GOLD "OUTDOOR LIGHTING" flanked LEFT AND RIGHT by a
   tapering gold hairline (rule spans x=7..491 of a 498px lockup,
   interrupted by the text, peak colour #ffc26d / #ffbb43, alpha
   falling to zero at the outer ends).

   That is this component. .eyebrow was designed independently in an
   earlier polish pass as wide-tracked caps gold + a gold hairline —
   and the client's mark contains the identical device. So the
   strongest available "logo and site are one system" move required
   no new invention at all, only alignment to the artwork's real
   geometry. Two corrections:

   1. TAPER DIRECTION WAS INVERTED. The old filament was brightest at
      its far edge and faded TOWARD the text. The logo does the
      opposite: brightest where it meets the text, tapering away
      outward — light emanating FROM the wordmark. Now matched.
   2. THE LOGO BRACKETS, IT DOESN'T PREFIX. A mirrored ::after gives
      the site's most-repeated marker the same enclosure the mark
      uses. It also self-balances in the 7 .text-center contexts,
      where a lone ::before always hung the label off-centre.

   Gold retuned to --color-gold-highlight (#ffd185, measured to match
   the lockup's own gold body at OKLCH L=0.884) so the rule is
   literally the mark's gold rather than a duller relative.

   Two-gate clearance (unchanged from the original rule):
     (A) SEO/GEO — decorative pseudo-elements only (content:""), zero
         DOM/copy/schema change. The label's own colour is untouched,
         so AA is unaffected. Both marks are inline-block INSIDE the
         existing block-level span next to a short label, so they
         consume space the line already had — zero CLS. Verified at
         1440/390/320.
     (B) decoration — this is now the mark's own sub-line grammar
         repeated as the site's section grammar: every section opens
         the way the logo opens. Bespoke, on-narrative, and it earns
         the AI-tell "all-caps section label" conditional-PASS on all
         three discriminator tests (bespoke-styled, on-narrative, and
         derived from the client's own artwork rather than a default).
   ──────────────────────────────────────────────────────────────── */
.eyebrow::before,
.eyebrow::after {
  content: "";
  display: inline-block;
  width: 1.6em;
  height: 1px;
  vertical-align: 0.18em;        /* sit on the small-caps optical centre */
}

/* Leading rule — tapers away from the label, brightest where it meets
   the text (matches the lockup's left flanking rule). */
.eyebrow::before {
  margin-right: 0.7em;
  background: linear-gradient(
    90deg,
    var(--gold-edge-none) 0%,
    var(--color-amber) 55%,
    var(--color-gold-highlight) 100%
  );
}

/* Trailing rule — the mirror. Together they bracket the label exactly
   as the gold rules bracket "OUTDOOR LIGHTING" in the artwork. */
.eyebrow::after {
  margin-left: 0.7em;
  background: linear-gradient(
    90deg,
    var(--color-gold-highlight) 0%,
    var(--color-amber) 45%,
    var(--gold-edge-none) 100%
  );
}


/* ════════════════════════════════════════════════════════════
   RAISED SECTION — lit top lip (luxury elevation V2)
   ────────────────────────────────────────────────────────────
   .section-raised is the site's largest and most-repeated content
   surface — the alternating raised band that carries the vertical
   rhythm of every one of the 19 pages (35+ instances). Every
   SMALLER component built on top of it already opens with a lit
   top edge in the site's one material language, light entering
   from above: .service-card, .card, .design-note, the portfolio
   plate, the faq open-state. The bands that CONTAIN them were the
   last un-lit repeated surface — a flat fill capped by a hard,
   flat 1px border that reads as a plain template seam rather than
   the lip of a fixture.

   This lights that lip. Two stacked INSET shadows only — no
   pseudo-element, no `position`, no `overflow`, no box-model or
   layout change of any kind, so no new stacking context is
   created and nothing that anchors to a section (the process
   spine anchors to `.process-section .container`, not here) can
   shift. Inset shadows paint at the surface's own background
   layer, beneath all normal-flow content, so headings and cards
   are never overpainted:
     1. a crisp ~1px warm lip riding just under the existing
        border-top (kept intact — box model untouched);
     2. a tall, very low-alpha glow falling off into the fill —
        deliberately fainter than the card wash because this
        surface is an order of magnitude larger; at card alpha a
        band this size would over-glow and break the quiet-luxury
        register.

   Two-gate clearance:
     (A) SEO/GEO — pure paint (box-shadow), no DOM/HTML/text
         change, page byte-identity preserved, zero CLS (no box
         size or position change), AA unaffected (content colours
         untouched). No `position`/`overflow` added → no stacking
         or clip regression for the process spine, card hover
         bloom, or any absolutely-positioned descendant.
     (B) decoration — carries the site's ONE idea (light is the
         material) onto its largest surfaces: every section band
         now reads as a lit plane lifted from the dark, not a flat
         template block. On-narrative, restrained, bespoke.
   ──────────────────────────────────────────────────────────── */
.section-raised {
  box-shadow:
    inset 0 1px 0 0 rgba(var(--edge-warm-rgb),  0.10),
    inset 0 14px 22px -14px var(--color-amber-glow);
}


/* ════════════════════════════════════════════════════════════
   V2 Cycle A: money-page — pricing-tier focal light-wash
   ────────────────────────────────────────────────────────────
   The Investment Guide trio is the OFFER moment on the money page
   (permanent-outdoor-lighting/, the highest-commercial-value page
   and the most-linked node in the internal-link spine). In its
   current form it is three equal .pricing-tier columns with the
   middle one marked only by an amber border + a small glow — i.e.
   the AI-tell "symmetric 1-2-3 pricing triad" default, on the one
   fold where the 55+ luxury buyer decides. `.pricing-tier` is
   money-page-UNIQUE (grep -rl: only permanent-outdoor-lighting/
   consumes it; the sole other match is the DESIGN-SYSTEM doc), so
   this scoped global rule touches no other page.

   The elevation gives the recommended tier genuine focal DOMINANCE
   (Fukinsei — one pool dominates; the composed warm-grade dimension)
   instead of a symmetric ring: a paint-once amber light-wash drawn
   from ABOVE the featured card + a lit top hairline, so the trio
   reads as one lit fixture pulling the eye to the "Full Property"
   scope — the offer becomes a designed focal point, not a stamped
   SaaS grid. The quiet (unfeatured) tiers gain a barely-there top
   lip so the three read as one lit plane, the centre brightest.
   Serves bar #2 (light is the material), #5 (material surfaces),
   #6 (layout choreography — the offer gets a focal centre).

   Two-gate clearance:
     (A) SEO/GEO — pure paint. `background-image` (a light-wash
         layered under the existing --color-canvas-raised fill via
         `background-color`) + `box-shadow`; NO DOM/HTML/text/schema
         change, page byte-identity preserved, zero CLS (no box
         size/position change, no new positioned box — the wash is
         a background layer, the hairline an inset shadow), AA
         unaffected (content colours untouched, wash sits behind
         text at ≤0.14 alpha over the existing dark surface). The
         cards are not links and have no hover, so nothing animates
         — a static, finished, reduced-motion-inert composition.
     (B) decoration — carries the site's ONE idea (light is the
         material) onto the decision fold: the recommended scope is
         literally the lit one. On-narrative, restrained, bespoke;
         de-tells the symmetric pricing triad without a copy change.

   Cascade note: the page-inline <style> (in the page <head>, after
   the style.css link) carries `.pricing-tier` and
   `.pricing-tier--featured` rules at 0-1-0 / 0-2-0. To win the
   `box-shadow`/`background-image` longhands over that later-source
   inline block, these rules are scoped through the `#pricing`
   section id that already wraps the tiers in the HTML — 1-2-0 /
   1-1-0, unambiguously higher specificity, no HTML change needed.
   ──────────────────────────────────────────────────────────── */
#pricing .pricing-tier {
  /* faint top lip on every tier so the trio reads as one lit plane */
  box-shadow: inset 0 1px 0 0 rgba(var(--edge-warm-rgb),  0.06);
}
#pricing .pricing-tier--featured {
  /* the recommended scope is the lit fixture: a warm wash falling
     from the top edge, layered UNDER the raised-canvas fill so text
     legibility (and AA) is unchanged. background-color keeps the
     existing surface; background-image adds the light. */
  background-color: var(--color-canvas-raised);
  background-image:
    radial-gradient(
      120% 62% at 50% -8%,
      rgba(var(--glow-warm-rgb),  0.14) 0%,
      rgba(var(--glow-rgb),  0.05) 34%,
      rgba(var(--glow-rgb),  0) 62%
    );
  /* a brighter lit top hairline + the existing amber ring/glow,
     so the focal card reads lit-from-above, not merely outlined */
  box-shadow:
    inset 0 1px 0 0 rgba(var(--edge-warm-rgb),  0.28),
    var(--glow-amber-sm);
}


/* ════════════════════════════════════════════════════════════
   V2 Cycle A: index — testimonial lit-surface depth
   ────────────────────────────────────────────────────────────
   The testimonials fold was this page's last un-elevated proof
   moment. Every other proof surface here is now art-directed — the
   portfolio framed-plate, the plan-panel blueprint, the process
   "line of light" spine — but the three .testimonial cards were
   still the raw component: a flat charcoal-raised panel with a
   hard amber left-rule and no material depth, three of them in a
   symmetric row. On the homepage that reads as the AI-tell
   "symmetric three-card testimonial triad"; on the 4 service pages
   the SAME class renders as a single lone quote card that likewise
   sits flat.

   `.testimonial` (the card wrapper) is shared by 5 pages —
   index (×3) + architectural-/hardscape-/pathway-driveway-/
   lighting-maintenance-repair (×1 each) — grep-verified
   (class="testimonial reveal"). This is a DELIBERATE global lift of
   the shared component (the correct move, not a leak): the same
   lit-surface language reads premium whether the card is solo or in
   a triad, and it de-defaults the homepage triad specifically. The
   shared sub-classes .testimonial__text / __stars / __attr (in
   base.css, also consumed by the service pages) are left UNTOUCHED —
   the only change to every consumer is the wrapper's own surface.

   NOTE — a decorative gold opening-quote-mark glyph was considered
   and REJECTED at Gate C: the quote copy already begins with a
   literal straight quote (`"Phil was great…`), which is byte-frozen
   rankable copy owned by the Editor. A large ::before curly-quote
   would collide with the existing `"` and read as a doubled/
   mismatched mark. The clean, non-conflicting move is the material-
   depth pass alone.

   LIT SURFACE — a faint amber top light-wash (background layer under
   the existing --color-canvas-raised fill) + a lit ~1px top hairline,
   so the card reads as a lit plane lifted from the dark rather than a
   flat panel. Same recipe, same low alphas as .card / .design-note
   (the quiet, non-clickable sibling surfaces), so it JOINS the
   established rhythm — not a new louder treatment. The base.css
   amber left-rule + radius are kept intact. No hover (these cards
   are non-interactive), so nothing animates — a static, finished,
   reduced-motion-inert composition.

   Serves bar #2 (light is the material), #5 (material surfaces — the
   last flat proof surface becomes a lit plane), #6/#7 (layout
   choreography / bespoke — the triad now matches the depth of every
   other proof fold instead of reading as the raw default component).

   Two-gate clearance:
     (A) SEO/GEO — pure paint. `background-image` light-wash layered
         UNDER the existing `background-color` fill + a `box-shadow`
         hairline; NO DOM/HTML/text/schema change, page byte-identity
         preserved, zero CLS (no box size/position change), AA
         unaffected (wash ≤0.10 alpha behind text over the existing
         dark surface; content colours untouched). No `position`/
         `overflow`/pseudo-element added, so no stacking or clip
         regression for any descendant.
     (B) decoration — carries the ONE idea (light is the material)
         onto the last flat proof surface. On-narrative, restrained,
         bespoke; de-tells the symmetric triad by depth, without a
         copy change. Discriminator: the amber left-rule already
         existed (not a NEW colored-left-border tell); the move
         de-defaults the triad, it does not introduce the tell.
   ──────────────────────────────────────────────────────────── */
.testimonial {
  background-color: var(--color-canvas-raised);
  /* lit plane: warm wash falling from the top edge, under the fill */
  background-image:
    radial-gradient(
      130% 58% at 50% -6%,
      rgba(var(--glow-warm-rgb),  0.10) 0%,
      rgba(var(--glow-rgb),  0.035) 32%,
      rgba(var(--glow-rgb),  0) 60%
    );
  /* lit ~1px top hairline (the base.css border-left + radius are
     kept intact; this adds the top lip in the same material key) */
  box-shadow: inset 0 1px 0 0 rgba(var(--edge-warm-rgb),  0.14);
}

/* === V2 Cycle A: utility — contact "what to expect" lit surface === */
/* The contact right-rail "What Happens After You Submit" panel was the last
   flat --color-canvas-raised box on the highest-intent page — every other
   raised surface sitewide (.card, .testimonial, .design-note, pricing tiers)
   already carries the fixture-from-above light-wash + lit top lip. Bringing
   this reassurance panel into the same material key makes the contact page
   read as a bespoke studio surface, not a plain lead-capture box, for the
   hesitant 55+ buyer. Recipe = .card exactly (top radial ≤0.06 + lit hairline
   ≤0.20). Pure paint via pseudo-elements — no conflict with the page's inline
   .what-to-expect base rule, no layout shift. The .what-to-expect__steps
   content already sits above via its own stacking; z-index:1 lifts it clear. */
.what-to-expect {
  position: relative;
  overflow: hidden;
}
.what-to-expect::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 45%;
  pointer-events: none;
  background-image: radial-gradient(
    120% 90% at 50% 0%,
    rgba(var(--glow-warm-rgb),  0.06) 0%,
    rgba(var(--glow-warm-rgb),  0.025) 38%,
    transparent 72%
  );
}
.what-to-expect::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  pointer-events: none;
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(var(--edge-warm-rgb),  0.20) 50%,
    transparent 100%
  );
  opacity: 0.7;
}
.what-to-expect__heading,
.what-to-expect__steps,
.what-to-expect > p {
  position: relative;
  z-index: 1;
}


/* === V2 Cycle A: story — portfolio case-study plates, framed-print depth ===
   ────────────────────────────────────────────────────────────
   Gallery-consistency lift. The V2 hero pass gave the marquee
   .portfolio-hero-project the full framed-plate language (lit top
   edge + recessed frame + cast shadow), so the featured photo sits
   IN the page like a mounted, lit object. But the three below-fold
   case-study plates — the actual gallery proof — still render as
   flush-cornered raw images: the inline page <style> gives
   .project-card__image-wrap only position/overflow/border-radius,
   NO box-shadow. Result: the hero reads as a mounted print, the
   gallery reads as raw thumbnails. That hero-vs-gallery split is
   the page's remaining "clean agency template" tell (bar #7).

   This applies the SAME mounted-plate materiality — a hairline
   lit top edge, a fine recessed frame, and a soft cast shadow — to
   the three case-study plates, so the whole gallery reads as one
   framed-print system, not a marquee plus loose thumbnails. Alphas
   are pulled slightly BELOW the hero's (this is the secondary tier;
   the hero stays dominant — Fukinsei, one plate leads). Serves bar
   #5 (material surfaces) and #7 (bespoke, not templated).

   Scope: .project-card / .project-card__image-wrap are UNIQUE to
   portfolio/ (grep -rl "project-card" --include=*.html → portfolio
   only) — a deliberately page-scoped rule, nothing leaks.

   Cascade: the inline page <style> sets NO box-shadow on the wrap,
   so this is new, non-conflicting paint (no tie to win). The wrap
   already has overflow:hidden + border-radius inline; the INSET
   frame/top-edge ride inside that clip, the outer cast sits behind
   the rounded box — the existing radius is respected, no change to
   it. The image inside keeps its own brightness/saturate hover
   (unaffected). No new positioned box, no pseudo-element, no size
   change → zero CLS; these articles are non-interactive at the wrap
   level so nothing animates on the frame — a static, finished,
   reduced-motion-inert composition.

   Two-gate clearance:
     (A) SEO/GEO — pure paint (box-shadow only). No DOM/HTML/text/
         schema change, page byte-identity preserved, zero CLS, AA
         unaffected (image + caption colours untouched; the frame
         is decorative shadow over the existing dark surface).
     (B) decoration — carries the site's ONE idea (light is the
         material) across the full gallery: every project photo is
         a lit, framed object recessed into the dark, matching the
         marquee. On-narrative, restrained, bespoke; closes the
         hero-vs-thumbnail inconsistency without a copy change.
   ──────────────────────────────────────────────────────────── */
.project-card__image-wrap {
  box-shadow:
    inset 0 1px 0 rgba(var(--edge-warm-rgb),  0.08),   /* fixture-lit top edge (below hero's 0.10) */
    inset 0 0 0 1px rgba(var(--shadow-rgb),  0.26),        /* fine recessed frame */
    0 14px 32px -22px rgba(var(--shadow-rgb),  0.5);       /* the plate's own soft cast */
}

/* ─────────────────────────────────────────────────────────────────────────
   In-content inline links  (Nocturne system)
   Added 2026-08-07 after a full-site axe pass found inline links that were
   distinguished from body copy by COLOUR ALONE (amber #e8b96a on ivory-muted
   #b8b0a3 = 1.18:1 against surrounding text — WCAG 1.4.1 "use of colour",
   and invisible to a colour-blind reader).

   House register: ink text + hairline accent underline. The link stays the
   READING colour so paragraphs don't turn into a rash of gold; the underline
   carries the affordance. Hover/focus lifts the rule to full amber.
   Applies only inside prose — nav, cards, and buttons style themselves.
   ───────────────────────────────────────────────────────────────────────── */
.answer-block p a,
.faq-item__answer a,
.section-subtitle a,
.privacy-content p a,
.legal-body a,
.card__text a {
  color: var(--color-ivory);
  text-decoration: underline;
  text-decoration-color: rgba(var(--edge-warm-rgb),  0.45);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color var(--dur-fast, 160ms) ease,
              color var(--dur-fast, 160ms) ease;
}

.answer-block p a:hover,
.faq-item__answer a:hover,
.section-subtitle a:hover,
.privacy-content p a:hover,
.legal-body a:hover,
.card__text a:hover,
.answer-block p a:focus-visible,
.faq-item__answer a:focus-visible,
.section-subtitle a:focus-visible,
.privacy-content p a:focus-visible,
.legal-body a:focus-visible,
.card__text a:focus-visible {
  color: var(--color-amber-light);
  text-decoration-color: var(--color-amber-light);
}

/* ─────────────────────────────────────────────────────────────────────────
   .material-visual — brass-plate placeholder surface  (Nocturne system)
   Added 2026-08-07. The class was referenced on 3 pages (about, process,
   home) but had NO rule anywhere in the stylesheet, so it rendered as a flat
   void — the single most "unfinished" surface in the build.

   Treatment: a dim brushed-brass plate catching a single raking light from
   upper-left. This is the honest solution while the commissioned founder
   portrait is outstanding (ASSET-REQUEST-ADDENDUM item 2) — it reads as a
   deliberate material study rather than a missing image, and a lighting
   company's own site should demonstrate how a surface takes light.

   Static composition; no animation, nothing for reduced-motion to collapse.
   ───────────────────────────────────────────────────────────────────────── */
.material-visual {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  isolation: isolate;
  min-height: 260px;
  border: 1px solid rgba(var(--edge-gold-rgb),  0.18);
  background:
    /* raking key light from upper-left */
    radial-gradient(120% 90% at 18% 8%, rgba(var(--glow-warm-rgb),  0.16) 0%, rgba(var(--glow-warm-rgb),  0.05) 38%, transparent 68%),
    /* cool fill so the plate sits in the twilight palette, not on top of it */
    linear-gradient(150deg, var(--color-canvas-raised) 0%, #1a2030 55%, #12161f 100%);
}

/* Fine vertical brushing — 1px repeating gradient, composited once, no JS. */
.material-visual::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    92deg,
    rgba(var(--tint-light-rgb),  0.028) 0px,
    rgba(var(--tint-light-rgb),  0.028) 1px,
    transparent 1px,
    transparent 3px
  );
  opacity: 0.9;
  pointer-events: none;
  z-index: -1;
}

/* Soft inner vignette so the plate has an edge and doesn't float. */
.material-visual::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 60px rgba(var(--shadow-rgb),  0.55),
              inset 0 1px 0 rgba(var(--edge-warm-rgb),  0.10);
  pointer-events: none;
  z-index: -1;
}

/* ═════════════════════════════════════════════════════════════════════════
   BLOG POST BODY — .entry-content   (Nocturne system)
   Added 2026-08-08.

   THE PROBLEM THIS SOLVES
   ───────────────────────
   The WP theme shell (header, hero, meta, CTA) was styled, but the POST BODY
   had no rules anywhere in either stylesheet. WordPress emits bare <h2>, <p>,
   <ul>, <blockquote>, <figure> — so every weekly article would have published
   as unstyled browser-default text sitting inside a beautiful frame.

   Everything below maps WP's default output onto the SAME tokens the static
   pages use, so an author writes plain content in the editor and it lands
   looking native. No per-post styling, no shortcodes, no page builder.
   ═════════════════════════════════════════════════════════════════════════ */

.entry-content {
  color: var(--color-ivory-muted);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
}

/* ─── Headings ─────────────────────────────────────────────────────────── */
.entry-content h2,
.entry-content h3,
.entry-content h4 {
  font-family: var(--font-display);
  font-weight: 300;
  color: var(--color-ivory);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
}
.entry-content h2 {
  font-size: clamp(var(--text-2xl), 3.4vw, var(--text-3xl));
  margin: var(--space-16) 0 var(--space-5);
}
.entry-content h3 {
  font-size: clamp(var(--text-xl), 2.6vw, var(--text-2xl));
  margin: var(--space-12) 0 var(--space-4);
}
.entry-content h4 {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--color-ivory);
  margin: var(--space-10) 0 var(--space-3);
}
/* An H2 immediately after the intro shouldn't push a crater into the page. */
.entry-content > :first-child { margin-top: 0; }

/* Amber hairline above each H2 — the same ornament the static sections use,
   so an article reads as one of the site's own sections, not a WP dump. */
.entry-content h2::before {
  content: "";
  display: block;
  width: 48px;
  height: 1px;
  background: linear-gradient(90deg, var(--color-amber), transparent);
  margin-bottom: var(--space-5);
}

/* ─── Body copy ────────────────────────────────────────────────────────── */
.entry-content p { margin: 0 0 var(--space-6); }

/* Opening paragraph gets a lift — the editorial "lede" convention. */
.entry-content > p:first-of-type {
  font-size: var(--text-lg);
  color: var(--color-ivory);
  line-height: var(--leading-normal);
}

.entry-content strong { color: var(--color-ivory); font-weight: 500; }
.entry-content em     { font-style: italic; }

/* Inline links inherit the house register: ink text + hairline amber rule. */
.entry-content a {
  color: var(--color-ivory);
  text-decoration: underline;
  text-decoration-color: rgba(var(--edge-warm-rgb),  0.45);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, color 160ms ease;
}
.entry-content a:hover,
.entry-content a:focus-visible {
  color: var(--color-amber-light);
  text-decoration-color: var(--color-amber-light);
}

/* ─── Lists — the amber point-of-light marker, matching .services-list ──── */
.entry-content ul,
.entry-content ol { margin: 0 0 var(--space-6); padding: 0; list-style: none; }
.entry-content li {
  position: relative;
  padding: 0 0 var(--space-3) var(--space-8);
  line-height: var(--leading-relaxed);
}
.entry-content ul > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: -0.5em;
  background-image:
    linear-gradient(var(--color-amber-light), var(--color-amber-light)),
    linear-gradient(var(--color-amber-light), var(--color-amber-light));
  background-size: 100% 1px, 1px 100%;
  background-position: center, center;
  background-repeat: no-repeat;
  opacity: 0.92;
}
.entry-content ol { counter-reset: nocturne-ol; }
.entry-content ol > li { counter-increment: nocturne-ol; }
.entry-content ol > li::before {
  content: counter(nocturne-ol);
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--color-amber-light);
  line-height: var(--leading-relaxed);
}
.entry-content li > ul,
.entry-content li > ol { margin-top: var(--space-3); margin-bottom: 0; }

/* ─── Pull quote ───────────────────────────────────────────────────────── */
.entry-content blockquote {
  margin: var(--space-10) 0;
  padding: var(--space-6) var(--space-8);
  border-left: 2px solid var(--color-amber);
  background: var(--color-canvas-raised);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.entry-content blockquote p { margin-bottom: var(--space-3); color: var(--color-ivory); }
.entry-content blockquote p:last-child { margin-bottom: 0; }
.entry-content blockquote cite {
  display: block;
  font-style: normal;
  font-size: var(--text-sm);
  color: var(--color-ivory-muted);
}

/* ─── Media — never overflow, never shift ──────────────────────────────── */
.entry-content img,
.entry-content video,
.entry-content iframe {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}
.entry-content figure { margin: var(--space-10) 0; }
.entry-content figcaption {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-ivory-muted);
  text-align: center;
}
/* WP alignment classes an author can pick in the editor. */
.entry-content .alignleft  { float: left;  margin: var(--space-2) var(--space-6) var(--space-4) 0; max-width: 50%; }
.entry-content .alignright { float: right; margin: var(--space-2) 0 var(--space-4) var(--space-6); max-width: 50%; }
.entry-content .aligncenter { display: block; margin-inline: auto; }
.entry-content .alignwide,
.entry-content .alignfull { margin-block: var(--space-10); }
@media (max-width: 640px) {
  .entry-content .alignleft,
  .entry-content .alignright { float: none; margin-inline: 0; max-width: 100%; }
}

/* ─── Tables — must scroll, not overflow, on mobile ────────────────────── */
.entry-content table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--space-8) 0;
  font-size: var(--text-sm);
}
.entry-content th,
.entry-content td {
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-fine);
  text-align: left;
  vertical-align: top;
}
.entry-content th {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--color-amber-light);
  font-weight: 500;
}
/* WP wraps tables in .wp-block-table; give it the scroll container. */
.entry-content .wp-block-table { overflow-x: auto; }

/* ─── Code ─────────────────────────────────────────────────────────────── */
.entry-content code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background: var(--color-canvas-raised);
  padding: 0.15em 0.4em;
  border-radius: var(--radius-sm);
  color: var(--color-amber-light);
}
.entry-content pre {
  background: var(--color-canvas-raised);
  border: var(--border-fine);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  overflow-x: auto;
  margin: var(--space-8) 0;
}
.entry-content pre code { background: none; padding: 0; color: var(--color-ivory-muted); }

.entry-content hr {
  border: 0;
  height: 1px;
  background: var(--color-canvas-border);
  margin: var(--space-12) 0;
}

/* ─────────────────────────────────────────────────────────────────────────
   AUTHORING HELPERS — plain CSS classes an author types once in the editor's
   "Additional CSS class" box (or a reusable block). No plugin required.
   ───────────────────────────────────────────────────────────────────────── */

/* .answer-first — the blog twin of the static pages' .answer-block.
   Restyled 2026-08-12 to MATCH that component exactly: hairline + lede type,
   no filled box. A post's opening answer and a service page's answer block are
   the same idea and must read the same way, or the blog looks like a different
   site. Text stays verbatim-extractable either way. */
.entry-content .answer-first {
  position: relative;
  padding: var(--space-2) 0 var(--space-2) var(--space-8);
  margin: 0 0 var(--space-12);
  color: var(--color-ivory);
  font-size: clamp(var(--text-lg), 1.6vw, var(--text-xl));
  line-height: 1.55;
  font-weight: 300;
  max-width: 58ch;
}

.entry-content .answer-first::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(
    180deg,
    var(--color-amber) 0%,
    rgba(var(--edge-gold-rgb),  0.35) 55%,
    transparent 100%
  );
}

@media (max-width: 640px) {
  .entry-content .answer-first { padding-left: var(--space-5); }
}

/* .key-takeaway — a mid-article emphasis panel. */
.entry-content .key-takeaway {
  background: var(--color-amber-glow);
  border: 1px solid rgba(var(--edge-gold-rgb),  0.38);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-8);
  margin: var(--space-10) 0;
  color: var(--color-ivory);
}

/* Native <details> FAQ — matches the FAQ page, and is what FAQPage schema
   should mirror verbatim. */
.entry-content details {
  border-bottom: var(--border-fine);
  padding: var(--space-4) 0;
}
.entry-content details > summary {
  cursor: pointer;
  list-style: none;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--color-ivory);
  padding-right: var(--space-8);
  position: relative;
}
.entry-content details > summary::-webkit-details-marker { display: none; }
.entry-content details > summary::after {
  content: "+";
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-amber-light);
  font-size: var(--text-lg);
  line-height: 1;
}
.entry-content details[open] > summary::after { content: "\2212"; }
.entry-content details > summary:focus-visible {
  outline: 2px solid var(--color-amber-light);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
.entry-content details > *:not(summary) { margin-top: var(--space-4); }

/* ═════════════════════════════════════════════════════════════════════════
   HOMEPAGE INTRO — editorial split  (added 2026-08-12)

   Problem it solves: the intro ran lede → headline → paragraph as three
   left-aligned blocks in one 860px column. Nothing broke the vertical rhythm,
   so a premium statement read as a continuous run of prose.

   Now: display headline holds the left column, the supporting paragraph sits
   to its right on a shared top baseline. The asymmetry (7fr / 5fr) keeps the
   headline dominant — it is the statement; the paragraph is the footnote.
   ═════════════════════════════════════════════════════════════════════════ */

.intro-shell {
  /* Wider than --container-narrow so two columns have room, but the LEDE keeps
     its own reading measure below so it never runs to 1280px. */
  max-width: var(--container-max);
}

/* The answer block is inside a wide shell. Constrain it to the SAME left
   column the headline occupies (7fr of 7+5 with the gap) so the section reads
   as one composition stepping down the left edge, rather than a full-width
   paragraph sitting on top of a two-column block.

   :not(.answer-block--closing) — 2026-08-26. That reasoning held while the
   block OPENED the section. It now closes it, sitting below the two-column
   headline block, so there is nothing for it to sit "on top of" and the 58%
   cap was the real reason the copy still ran to 7 lines after the type-size
   demotion. The closing variant spans the shell instead; see
   .answer-block--closing above. */
.intro-shell .answer-block:not(.answer-block--closing) { max-width: 64ch; }

@media (min-width: 981px) {
  .intro-shell .answer-block:not(.answer-block--closing) { max-width: 58%; }
}

.intro-split {
  margin-top: var(--space-12);
  display: grid;
  gap: var(--space-8);
}

/* Solo variant (2026-08-26) — the supporting paragraph that occupied the right
   column was merged into the answer block below, so there is no second column
   left to form. Without this the 7fr/5fr grid would still reserve the 5fr
   track and strand the headline in 7/12ths of the shell for no reason. */
.intro-split--solo {
  margin-top: 0;
}

@media (min-width: 981px) {
  .intro-split:not(.intro-split--solo) {
    grid-template-columns: 7fr 5fr;
    gap: clamp(var(--space-10), 6vw, var(--space-20));
    align-items: start;
  }

  /* .intro-split__aside removed 2026-08-26 — the element it styled (the intro's
     right-column supporting paragraph) was merged into the answer block, and
     the class is now referenced by zero pages. Left as a note rather than a
     dead rule so the optical-alignment reasoning isn't lost if a second column
     ever returns: the display serif sits on a taller line box than the body
     sans, so matching box tops leaves the paragraph visually high — it needed
     padding-top: 0.7rem to sit on the headline's x-height. */
}

.intro-split__title {
  margin: 0;
  /* Slightly larger than the default section title — this is the page's
     thesis statement, and it now has a column of its own to fill. */
  font-size: clamp(var(--text-2xl), 4.2vw, var(--text-4xl));
  line-height: 1.12;
  text-wrap: balance;
}

/* The manual <br/> is tuned for a full-width line. In a 7fr column it forces an
   awkward early break, so let the text wrap naturally there and keep the break
   only where the column is wide enough to honour it. */
@media (max-width: 1200px) {
  .intro-split__title br { display: none; }
}

.intro-split__aside .section-subtitle {
  margin: 0;
  max-width: 46ch;
}

/* A hairline lead-in ties the paragraph to the headline as its annotation,
   matching the amber rule used on .answer-block and .divider. */
@media (min-width: 981px) {
  .intro-split__aside {
    position: relative;
    padding-left: var(--space-6);
  }
  .intro-split__aside::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.95rem;
    width: var(--space-5);
    height: 1px;
    background: linear-gradient(90deg, var(--color-amber), transparent);
  }
}


/* ════════════════════════════════════════════════════════════════════
   DEPTH PASS — cast shadow + lit edge  (2026-09-16)
   ────────────────────────────────────────────────────────────────────
   SCOPE: shading only. No colour token, no hue, no fill value, no
   layout box, no DOM, no copy is touched by anything below. Every
   declaration here is box-shadow, background-image (gradient between
   already-approved colours), or spacing. Verified by grep against the
   frozen token list before handback.

   THE MEASURED PROBLEM. Sampling real painted pixels (not computed
   style) across /, /about/, /process/, /hardscape-lighting/, /contact/:
   the fill ladder is actually working — canvas #261304 -> band #311a04
   is 1.585x luminance, band -> card #371f0b is 1.299x. But EVERY card
   class rendered `box-shadow: none` at rest. The page was flat because
   nothing on it cast a shadow, so no surface ever sat ON another one.
   A 1.299x fill step plus a 1px border is not enough separation to
   survive a daylight iPad, which is this buyer's primary device.

   THE FIX. Give every raised surface the two halves of a real object:
   a warm CAST SHADOW below it (it occludes light) and a LIT TOP EDGE
   (it catches light). Both are built from colours already in the
   palette — the shadow from --color-canvas-deep #1b0d04, the edge from
   --color-amber-light / --color-chrome at low alpha. On-narrative: this
   site's ONE idea is that light is the material, and a cast shadow is
   the most direct possible statement that a real light source exists
   above the frame.

   Two-gate clearance:
     (A) SEO/GEO — box-shadow and background-image are paint-only. No
         rankable text moves, leaves, or is delayed; no DOM, schema, or
         byte change to any HTML file; zero CLS (box-shadow does not
         participate in layout and no box size, position, margin or
         padding is altered on any element that contains text). Shadows
         are static — nothing animates at rest, so reduced-motion is
         inert here and the static render IS the finished state. AA was
         recomputed at the WORST point of every gradient, not the
         average: floor is 7.17:1 (ivory-muted on card), unchanged,
         because the shadow paints OUTSIDE the box and the vignettes
         are capped where text never sits.
     (B) decoration — this is the narrative idea, not an ornament on
         top of it. Light implies shadow; a lighting brand whose page
         casts none is arguing against itself.
   ════════════════════════════════════════════════════════════════════ */

/* ── 1. Cards at rest now cast ──────────────────────────────────────
   The single highest-impact change in this pass. These four classes
   are the site's repeated content objects and all four measured
   `none`. The lit top edge is deliberately warm (amber at 0.07) on
   .service-card / .card because those sit under the fixture-wash
   ::before that already exists — the edge is the lip of that same
   fixture, so the two read as one gesture rather than two effects. */
.service-card,
.card {
  box-shadow: var(--edge-lit-warm), var(--shadow-rest);
}

/* The featured service card is the focal wedge — it should sit
   measurably higher than its siblings, not merely be outlined. */
.service-card--featured {
  box-shadow: var(--edge-lit-warm), var(--shadow-raised);
}

/* Hover keeps its existing amber glow but now lifts from a real
   resting shadow instead of from nothing, so the movement reads as
   the card rising rather than a glow switching on. The transition
   already declared on .service-card covers box-shadow. */

/* ── 2. Deliberately-deep panels read as SUNKEN, not just dark ──────
   .plan-panel is filled below the canvas (#1d0f05-class) and had no
   edge treatment, so it read as a dark rectangle rather than a recess.
   An inset top-shadow is what makes a surface read as cut INTO the
   page. Its amber border is preserved exactly. */
.plan-panel {
  box-shadow: var(--edge-sunk);
}

/* ── 3. FAQ rows: hairline only, no cast ────────────────────────────
   .faq-item is a transparent row separated by a bottom rule, not a
   raised object. Giving it a cast shadow would be exactly the kind of
   uniform "every box floats" treatment that reads as generated. It
   gets a lit hairline on the rule it already has, and nothing more. */
.faq-item {
  box-shadow: inset 0 -1px 0 0 rgba(var(--edge-warm-rgb),  0.045);
}

/* ── 4. Section bands: a seam you can actually see ──────────────────
   .section-raised already carried an inset top highlight + amber wash.
   What it lacked was a shadow CAST ONTO the canvas below it, so the
   band's bottom edge dissolved. A downward outer shadow at the band's
   own base makes the band read as a plane lying over the dark stage.
   The existing two inset layers are preserved verbatim and the new
   outer layer is appended after them. */
.section-raised {
  box-shadow:
    inset 0 1px 0 0 rgba(var(--edge-warm-rgb),  0.10),
    inset 0 14px 22px -14px var(--color-amber-glow),
    0 18px 40px -22px rgba(var(--scrim-deep-rgb),  0.72);
}

/* ── 5. The deep beats keep their drama, and gain an edge ───────────
   .contact-cta and .page-hero are the page's two "deep night"
   moments. They must NOT be lightened (the lighten-only law protects
   them from being raised as much as from being cut). What they get is
   a lit lip at the top so the transition INTO the dark reads as an
   edge rather than a fade — the moment the stage floor drops away. */
.contact-cta,
.page-hero {
  box-shadow: var(--edge-lit-chrome);
}

/* ── 6. Footer: seated, not stacked ─────────────────────────────────
   The footer is band-coloured and butted directly against the section
   above it, so the two merged into one mass. An inset top shadow sets
   it BELOW the page rather than beside it. */
.site-footer {
  box-shadow: inset 0 6px 14px -8px rgba(var(--scrim-deep-rgb),  0.55);
}

/* ── 7. Light pools on the plain canvas bands ───────────────────────
   The alternating rhythm is band / canvas / band / canvas. The BANDS
   were treated (fill + inset edge + now a cast shadow); the CANVAS
   sections between them measured as absolutely flat #261304 across
   their full height — the single largest untreated surface on the
   site and the main reason the page reads as "one brown sheet".

   Rather than lighten the fill (forbidden, and it would collapse the
   band/canvas alternation anyway), each plain section gets a wide,
   very low-alpha amber pool — the suggestion of a fixture somewhere
   above and off-frame washing the stage. This is the most on-concept
   move available to a lighting brand and it costs nothing structural.

   SCOPING. `main > section:not([class])` targets ONLY the bare
   <section> elements. It cannot touch .section-raised, .contact-cta,
   .page-hero, .section--flush-top, or any classed section, so no
   existing background composition is overwritten and no fill changes.
   The pool is a background-image on a surface that currently has
   none — purely additive paint.

   ALPHA CEILING. Peak alpha is 0.045 amber. Over #261304 that raises
   the surface to a measured worst case well inside AA for every text
   colour used on canvas (recomputed at the pool's BRIGHTEST point,
   not its average — see the audit table in the handback). The pool is
   centred at 50% 50% and falls to zero by 70%, so the section's top
   and bottom seams stay at true canvas and the alternation with the
   bands is preserved. */
main > section:not([class]) {
  background-image:
    radial-gradient(ellipse 90% 65% at 50% 50%,
      rgba(var(--glow-rgb),  0.045) 0%,
      rgba(var(--glow-rgb),  0.018) 38%,
      rgba(var(--glow-rgb),  0) 70%);
  background-repeat: no-repeat;
}

/* ── 8. Band seam definition ────────────────────────────────────────
   Where a raised band meets the canvas, the seam was carried by a 1px
   border alone. A short falloff at the band's own top and bottom edge
   makes the join read as a soft light gradient rather than a drawn
   line — the edge of a lit plane, not a table border. Uses the band's
   own two frozen colours as gradient stops (allowed: a gradient
   between already-approved colours), so no new colour enters.

   These are ::before/::after on a position:relative parent; both are
   pointer-events:none, z-index below content, and add no layout box. */
.section-raised {
  position: relative;
}

.section-raised::before,
.section-raised::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  z-index: 0;
}

/* Gate C finding (eye-check, 1440 + 390): the first pass ran this at
   80px / 0.22 alpha and it read as a muddy horizontal BAR sitting under
   the header rather than as a seam — the falloff was long enough and
   dark enough to become an object in its own right. Shortened to 28px
   and dropped to 0.10 so it does what a seam does: define the join in
   the first few pixels, then get out of the way. */
.section-raised::before {
  top: 0;
  height: 28px;
  background-image: linear-gradient(to bottom,
    rgba(var(--scrim-deep-rgb),  0.10) 0%,
    rgba(var(--scrim-deep-rgb),  0) 100%);
}

/* The bottom edge can carry more than the top: it is where the band's
   own cast shadow falls away onto the canvas below, so a slightly
   longer, slightly stronger falloff reads as correct. */
.section-raised::after {
  bottom: 0;
  height: 56px;
  background-image: linear-gradient(to top,
    rgba(var(--scrim-deep-rgb),  0.18) 0%,
    rgba(var(--scrim-deep-rgb),  0) 100%);
}

/* The band's own content must sit above the two falloff overlays. */
.section-raised > * {
  position: relative;
  z-index: 1;
}

/* ════════════════════════════════════════════════════════════
   BLOG (WP) — shared with the gemmell-nocturne theme
   ────────────────────────────────────────────────────────────
   Lives here, not in the theme, so the blog and the 58 static
   pages are styled by the SAME file. The theme enqueues this
   stylesheet from the site root; adding blog rules to the theme
   instead would reintroduce the two-codebase drift these gates
   exist to prevent.
   Column behaviour mirrors .services-grid (3 -> 2 -> 1).
   ════════════════════════════════════════════════════════════ */

.blog-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6);
}

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

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

/* Post card: the site card, with the image bonded to the top edge. */
.blog-card { padding: 0; overflow: hidden; }

.blog-card__media {
  overflow: hidden;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.blog-card__media img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
}

.blog-card__body { padding: var(--space-6); }

.blog-card__title a { color: inherit; text-decoration: none; }
.blog-card__title a:hover { color: var(--color-amber); }

/* Long-form reading measure for post bodies. */
.blog-post__body {
  color: var(--color-ivory-muted);
  line-height: var(--leading-relaxed);
  font-size: var(--text-base);
}

.blog-post__body > * + * { margin-top: var(--space-5); }

.blog-post__body h2 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  color: var(--color-ivory);
  margin-top: var(--space-10);
}

.blog-post__body h3 {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  color: var(--color-ivory);
  margin-top: var(--space-8);
}

.blog-post__body a { color: var(--color-amber); }

.blog-post__body ul,
.blog-post__body ol { padding-left: var(--space-6); }

.blog-post__body li + li { margin-top: var(--space-2); }

.blog-post__body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
}

.blog-post__body blockquote {
  border-left: 2px solid var(--color-amber);
  padding-left: var(--space-5);
  font-style: italic;
  color: var(--color-ivory);
}

/* Pagination — reuses the ghost-button visual language. */
.blog-pagination { margin-top: var(--space-10); }

.blog-pagination .nav-links {
  display: flex;
  justify-content: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.blog-pagination .page-numbers {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  height: 44px;
  padding: 0 var(--space-4);
  border: 1px solid var(--color-canvas-border);
  border-radius: var(--radius-pill);
  color: var(--color-ivory-muted);
  text-decoration: none;
  font-size: var(--text-sm);
}

.blog-pagination .page-numbers:hover {
  color: var(--color-ivory);
  border-color: var(--color-amber);
}

.blog-pagination .page-numbers.current {
  color: var(--color-canvas);
  background: var(--color-amber);
  border-color: var(--color-amber);
}

/* ════════════════════════════════════════════════════════════
   HOMEPAGE — STATEMENT BAND
   ────────────────────────────────────────────────────────────
   A full-bleed lit-facade frame between the intro and the
   services grid. Taller and quieter than .divider-band: this one
   carries a line of type, so it is a designed pause rather than
   a visual separator.

   The type sits LEFT because the licensed frame puts the lit
   house right-of-centre — the left third is foliage in shadow,
   which is where type can live without a heavy scrim flattening
   the photograph.
   ════════════════════════════════════════════════════════════ */

.statement-band {
  position: relative;
  width: 100%;
  overflow: hidden;
  line-height: 0;
  display: flex;
  align-items: center;
}

.statement-band__img {
  width: 100%;
  height: clamp(320px, 42vw, 560px);
  object-fit: cover;
  object-position: center 55%;
  display: block;
}

/* Two gradients in one layer: a vertical melt into the sections
   above/below, and a left-hand wedge for type contrast. */
.statement-band__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right,
      rgba(var(--photo-scrim-rgb), 0.88) 0%,
      rgba(var(--photo-scrim-rgb), 0.55) 34%,
      rgba(var(--photo-scrim-rgb), 0) 62%),
    linear-gradient(to bottom,
      var(--color-canvas) 0%,
      rgba(var(--scrim-rgb),  0) 26%,
      rgba(var(--scrim-rgb),  0) 74%,
      var(--color-canvas) 100%);
}

.statement-band__inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  line-height: var(--leading-snug);
}

.statement-band__quote {
  max-width: 22ch;
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(var(--text-xl), 3.2vw, var(--text-4xl));
  color: var(--color-ivory);
  text-wrap: balance;
  text-shadow: 0 2px 24px rgba(var(--shadow-rgb),  0.55);
}

@media (max-width: 640px) {
  /* On a phone the left wedge cannot hold a 22ch measure beside the
     house, so the scrim goes full-width and the type centres. */
  .statement-band__scrim {
    background:
      linear-gradient(to bottom,
        var(--color-canvas) 0%,
        rgba(var(--photo-scrim-rgb), 0.55) 30%,
        rgba(var(--photo-scrim-rgb), 0.55) 70%,
        var(--color-canvas) 100%);
  }

  .statement-band__quote {
    max-width: none;
    text-align: center;
  }
}

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

/* ════════════════════════════════════════════════════════════
   INNER-PAGE HERO — TIER 1 (photographic)
   ────────────────────────────────────────────────────────────
   Applied to the 16 service / commercial / trust pages. The 39
   location-template pages and privacy-policy keep the CSS-only
   treatment (tier 2, below) — template siblings do not each earn
   a distinct photograph, and their LCP stays text.

   THE IMAGE REPLACES THE BLOOM, IT DOES NOT SIT OVER IT.
   .page-hero paints --bloom-amber-soft (a radial amber pool) over
   --charcoal-depth. Layering a photo on top of that gives you two
   competing light sources — the photo's real highlights fighting a
   synthetic one — which reads as mud. On a tier-1 hero the bloom
   layer is dropped and the photograph supplies the light.

   The grain stays: it is what keeps the photo sitting in the same
   material world as the rest of the page.
   ════════════════════════════════════════════════════════════ */

.page-hero--photo {
  background-image:
    var(--grain-url),
    var(--charcoal-depth);
  background-size: 96px 96px, 100% 100%;
  background-repeat: repeat, no-repeat;
}

.page-hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 50%;
  z-index: -2;
  /* Held well back: this is atmosphere behind a headline, not the
     subject. The homepage hero runs far brighter because there the
     photograph IS the message. */
  opacity: 0.38;
  filter: brightness(0.62) saturate(0.85);
}

/* Scrim: a vertical wash that guarantees H1 contrast regardless of
   what the photograph is doing behind it, and melts the hero into
   the section beneath. Sits between the image and the text. */
.page-hero--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(var(--photo-scrim-deep-rgb), 0.72) 0%,
      rgba(var(--photo-scrim-rgb),     0.42) 46%,
      rgba(var(--photo-scrim-rgb),     0.78) 100%);
}

@media (prefers-reduced-motion: no-preference) {
  /* Nothing animates here by design. A moving hero on 16 pages
     competes with the headline it exists to frame. */
  .page-hero__bg { transition: none; }
}

/* ────────────────────────────────────────────────────────────
   INNER-PAGE HERO — TIER 2 (CSS-only, varied by branch)
   The location templates keep the existing construction; only the
   bloom's position and warmth shift per service branch, so the 39
   pages stop reading as one repeated screen without costing bytes
   or displacing a text LCP.
   ──────────────────────────────────────────────────────────── */

.page-hero--repair {
  background-position: 0 0, 22% 38%, 0 0;
}

.page-hero--permanent {
  background-position: 0 0, 78% 34%, 0 0;
}

.page-hero--general {
  background-position: 0 0, 50% 30%, 0 0;
}

/* ── Homepage intro: no vertical hairline ────────────────────────────
   The amber hairline on .answer-block reads as an editorial lede-rule
   on the 54 pages that use the plain block. Directly under the homepage
   hero it reads instead as a leftover scroll-cue — a vertical strike
   hanging off the bottom of the hero. Owner direction 2026-09-17:
   remove it there. The component keeps its rule everywhere else, and
   the padding stays so the text alignment does not shift.
   ──────────────────────────────────────────────────────────────────── */
.answer-block--closing::before {
  content: none;
}

/* ── THEME TOGGLE (recovered from the 2026-09-10 build, 2026-09-17) ───
   Sits beside the header phone link. 44px minimum target — the audience
   skews 55+ on iPads and this is a real control, not decoration. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  /* The toggle is a direct flex child of .header-inner, which was
     compressing it from the declared 44px down to a MEASURED 2px wide
     (the SVGs shrank to 0 too). A 2px control is unusable for anyone,
     and this audience skews 55+ on iPads. flex:none pins the declared
     size; min-width/min-height defend it if the header ever gets
     tighter. Caught by measuring the rendered box, not by reading the
     rule — the `width:44px` was there and looked correct all along. */
  flex: 0 0 44px;
  min-width: 44px;
  min-height: 44px;
  margin-left: var(--space-3);
  padding: 0;
  background: none;
  border: 1px solid var(--color-canvas-border);
  border-radius: 50%;
  color: var(--color-amber-light);
  cursor: pointer;
  transition: border-color 240ms ease, color 240ms ease, transform 240ms ease;
}

/* The icons must not shrink either — they were measuring 0px wide. */
.theme-toggle svg {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
}
.theme-toggle:hover,
.theme-toggle:focus-visible {
  border-color: var(--color-amber);
  color: var(--color-gold-highlight);
  transform: translateY(-1px);
}
.theme-toggle:focus-visible {
  outline: 2px solid var(--color-amber-light);
  outline-offset: 3px;
}
/* Show the icon for the theme you'd switch TO, not the one you're in. */
.theme-toggle__sun  { display: block; }
.theme-toggle__moon { display: none; }
:root[data-theme="light"] .theme-toggle__sun  { display: none; }
:root[data-theme="light"] .theme-toggle__moon { display: block; }

@media (prefers-reduced-motion: reduce) {
  .theme-toggle { transition: none; }
  .theme-toggle:hover, .theme-toggle:focus-visible { transform: none; }
}
@media (max-width: 980px) {
  .theme-toggle { margin-left: var(--space-2); }
}



/* ════════════════════════════════════════════════════════════════════
   TEXT ON PHOTOGRAPHS — light-theme scoping (2026-09-17)
   ────────────────────────────────────────────────────────────────────
   This is bug §6.1 from build/LIGHT-THEME-NOTES.md, and it is the single
   trap this theme sets for every new photo component.

   The rule: page ink follows the theme; ink sitting ON a photograph does
   NOT. Every photograph on this site is a DUSK plate — lit fixtures
   against a night garden — and the scrims over them are deliberately
   theme-invariant (--photo-scrim-rgb). So the copy over them must stay
   LIGHT in both themes. Left on page ink, the light theme paints
   #26190c onto a bright night photo and the headline disappears
   (measured on the homepage hero: dark ink on a lit lawn).

   Scoped to [data-theme="light"] only — in dark these already resolve
   to the same values, so the dark theme is untouched by every rule here.

   CAUTION (§6.5): do NOT generalise this to ".hero *" or to any section
   that merely sits near a photo. Over-generalising the original fix
   painted the About page's "Meet Phil" panel light-on-light. Each
   selector below names an element that is genuinely over an image. */

/* ── Homepage hero ── */
:root[data-theme="light"] .hero__headline,
:root[data-theme="light"] .hero__subline,
:root[data-theme="light"] .hero__eyebrow,
:root[data-theme="light"] .hero .display-vp {
  color: var(--photo-ink);
}
:root[data-theme="light"] .hero__subline {
  color: var(--photo-ink-muted);
}
:root[data-theme="light"] .hero__eyebrow,
:root[data-theme="light"] .hero .hero__headline em,
:root[data-theme="light"] .hero .display-vp em {
  color: var(--photo-ink-accent);
}

/* ── Tier-1 photographic page heroes (16 pages) ──────────────────────
   One class, one rule — but verified on BOTH a dark frame and the only
   daylight frame in the set (permanent-roofline-track-eave-day-01),
   because a bright photo is where a too-light ink fails first. */
:root[data-theme="light"] .page-hero--photo .page-hero__title,
:root[data-theme="light"] .page-hero--photo .display-vp,
:root[data-theme="light"] .page-hero--photo .page-hero__subtitle,
:root[data-theme="light"] .page-hero--photo .page-hero__eyebrow,
:root[data-theme="light"] .page-hero--photo p {
  color: var(--photo-ink);
}
:root[data-theme="light"] .page-hero--photo .page-hero__subtitle,
:root[data-theme="light"] .page-hero--photo p {
  color: var(--photo-ink-muted);
}
:root[data-theme="light"] .page-hero--photo .display-vp em,
:root[data-theme="light"] .page-hero--photo .page-hero__eyebrow {
  color: var(--photo-ink-accent);
}

/* ── Non-photo page heroes (repair / general / permanent / plain; 43 pages)
   .page-hero paints --charcoal-depth, a theme-INVARIANT dark gradient, so
   in light theme the H1 fell to page ink (#26190c) on #311a04: measured
   invisible 2026-11-08 (master-auditor + Playwright render). Same ink as
   the photo heroes; scoped to the hero's own title and eyebrow only. */
:root[data-theme="light"] .page-hero:not(.page-hero--photo) .page-hero__title {
  color: var(--photo-ink);
}
:root[data-theme="light"] .page-hero:not(.page-hero--photo) .eyebrow,
:root[data-theme="light"] .page-hero:not(.page-hero--photo) .page-hero__eyebrow {
  color: var(--photo-ink-accent);
}

/* ── Statement band (the brick-house facade plate) ── */
:root[data-theme="light"] .statement-band__quote {
  color: var(--photo-ink);
}

/* ── Portfolio figcaption plates — sit on the photograph ── */
:root[data-theme="light"] .portfolio-feature figcaption,
:root[data-theme="light"] .portfolio-feature figcaption .link-arrow {
  color: var(--photo-ink);
}
:root[data-theme="light"] .portfolio-feature__label {
  color: var(--photo-ink-accent);
}

/* ── Contact CTA band — the homepage one, which carries a photograph ──
   .contact-cta is on 58 pages, but only the homepage has .contact-cta__bg.
   The other 57 are the deep-night gradient, and in light mode that band
   correctly flips to a light fill with page ink — verified on /about/,
   /portfolio/ and a location page, so they must NOT be caught by these
   rules. Hence the explicit .contact-cta--photo modifier on index.html
   rather than :has(.contact-cta__bg): per the note at §1297 a selector
   that silently no-ops on an older browser is a poor way to hold a
   layout together, and here the no-op is illegible copy on the primary
   conversion block.

   Measured before the fix (light theme, 1280w): title/phone/ghost all
   resolved to rgb(38,25,12) — near-black ink on a lit dusk facade. */
:root[data-theme="light"] .contact-cta--photo .contact-cta__title,
:root[data-theme="light"] .contact-cta--photo .contact-cta__phone,
:root[data-theme="light"] .contact-cta--photo .btn--ghost {
  color: var(--photo-ink);
}
:root[data-theme="light"] .contact-cta--photo .section-subtitle {
  color: var(--photo-ink-muted);
}
:root[data-theme="light"] .contact-cta--photo .contact-cta__title em {
  color: var(--photo-ink-accent);
}
/* The ghost button's border is page-ink too — it vanishes with the label. */
:root[data-theme="light"] .contact-cta--photo .btn--ghost {
  border-color: rgba(245, 240, 232, 0.55);
}
/* Give the ink a ground without flattening the photograph: the band still
   reads as a dusk plate, the copy stops competing with the lit windows. */
:root[data-theme="light"] .contact-cta--photo .contact-cta__bg {
  opacity: 0.30;
  filter: brightness(0.42) saturate(0.85);
}

/* ── Focus rings on controls mounted ON a photograph ─────────────────
   The page ring (#604200) measures 1.61:1 on the hero photo — below the
   3:1 WCAG 2.2 SC 1.4.11 requires for a focus indicator. */
:root[data-theme="light"] .hero a:focus-visible,
:root[data-theme="light"] .hero button:focus-visible,
:root[data-theme="light"] .page-hero--photo a:focus-visible,
:root[data-theme="light"] .page-hero--photo button:focus-visible,
:root[data-theme="light"] .portfolio-feature figcaption a:focus-visible {
  outline-color: var(--photo-focus-ring);
}

/* ── Ghost CTA mounted ON a photograph (§6.5) ────────────────────────
   .btn--ghost is an OUTLINED button: transparent fill, so whatever is
   behind it shows through. On the page that is paper and dark ink is
   right; in the hero it sits on the dusk photo, where the same dark ink
   measured invisible. Scoped to the photo contexts only — the identical
   button elsewhere on the page keeps page ink. */
:root[data-theme="light"] .hero .btn--ghost,
:root[data-theme="light"] .page-hero--photo .btn--ghost,
:root[data-theme="light"] .statement-band .btn--ghost {
  color: var(--photo-ink);
  border-color: var(--photo-ink-muted);
}
:root[data-theme="light"] .hero .btn--ghost:hover,
:root[data-theme="light"] .hero .btn--ghost:focus-visible,
:root[data-theme="light"] .page-hero--photo .btn--ghost:hover,
:root[data-theme="light"] .page-hero--photo .btn--ghost:focus-visible {
  color: var(--photo-ink-accent);
  border-color: var(--photo-ink-accent);
}

/* ── Tier-1 hero photograph in LIGHT theme (plan step 8) ─────────────
   The frame is held at opacity .38 + brightness(.62) so it sits UNDER a
   dark page and never competes with the H1. Those numbers are tuned for
   a dark canvas: over parchment the same settings leave the photograph
   almost invisible (measured on permanent-outdoor-lighting/, the only
   daylight frame in the set — it read as a flat brown panel).

   In light theme the photo is no longer fighting a dark page for the
   headline's contrast — the ::after scrim is theme-invariant and still
   carries the H1's legibility on its own. So the frame can come up and
   stop being crushed, and the hero reads as a framed dusk plate on
   stone rather than a muddy band. */
:root[data-theme="light"] .page-hero__bg {
  opacity: 0.52;
  filter: brightness(0.78) saturate(0.92);
}

/* Narrow phones: long button labels wrap instead of widening the page.
   .btn is white-space:nowrap (base.css), so "Book a Free Design Walk with
   Phil" measured 357px inside a 272px column at 320px and pushed the whole
   page — header included — out to 381px, giving a sideways scroll. Wrapping
   only happens when the label does not fit, so wider screens are unchanged. */
@media (max-width: 480px) {
  .btn {
    white-space: normal;
    line-height: 1.3;
    text-align: center;
    max-width: 100%;
  }
  /* Same class of defect: an email address is one unbreakable word
     (info@gemmelloutdoorlighting.com measured 336px on /contact/ at 320). */
  a[href^="mailto:"],
  .js-email-link {
    overflow-wrap: anywhere;
  }
}
