/*
Theme Name: NorthCo Classic v2
Theme URI: https://northcoservices.com/
Author: NorthCo Services
Description: A classic (non-FSE) WordPress theme for NorthCo Services — clean PHP templates, native Customizer for site-wide settings, hardcoded homepage content. Zero plugin dependencies.
Version: 2.0.0
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 8.1
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: northco-classic
Tags: business, custom-menu, featured-images, threaded-comments
*/




*{box-sizing:border-box;margin:0;padding:0;}
/* ── Display face: Poppins SemiBold ────────────────────────────────────────────
   LICENSING IS WHY THIS FONT. The brand's display face is Mont (Fontfabric, commercial) and
   NorthCo does not hold a licence for it, so it cannot be served. Mont was briefly wired in here
   and has been removed — the file is gone from the theme.

   Poppins is the closest free geometric sans: SIL Open Font License, so commercial use, webfont
   use and self-hosting are all permitted outright. Compared side by side at 46px it set to the
   SAME width as Mont Bold to the pixel, so nothing reflows; the tells are in the a, g, y, R and J.

   SemiBold 600, not Bold 700, because 600 is what matches Mont Bold's weight on screen.

   font-weight:600 900 is the same trick the Mont face used, and for the same reason: this theme
   asks for 700 in 77 places, 800 in 19 and 900 in 6, but there is ONE file. Declaring the range
   makes every one of those use the real SemiBold instead of the browser faking a heavier weight.
   It also means headings render at a consistent weight, which is what they already did under
   Mont — so this is a like-for-like swap, not a restyle.

   Self-hosted rather than Google-served: 8KB, one fewer third-party connection, and no
   render-blocking dependency on fonts.gstatic.com.

   ⚠ Poppins' latin subset has no ← or → glyph. Not used anywhere in the careers pages, but
   .nav-viewall and the footer links DO use → and are set in var(--heading) — so on the V2 site
   those arrows will fall back to a system font. Replace them with the inline SVG the role cards
   already use, or accept the fallback. Flagged, not silently ignored. */
@font-face{
  font-family:'Poppins';
  font-weight:600 900;
  font-style:normal;
  font-display:swap;
  src:url('assets/fonts/Poppins-SemiBold.woff2') format('woff2');
}

:root{
  /* Brand */
  --navy:#161044;
  --navy-dark:#0D0A26;       /* footer / deepest */
  --navy-700:#241C5A;        /* raised / hover */
  --brown:#a77845;           /* gold-600 */
  --brown-dark:#8C6236;      /* gold-700, for text on cream */
  --brown-light:#BB8C57;     /* gold-500 hover */
  --brown-pale:#F3E8D6;      /* gold-100 wash */
  --white:#ffffff;

  /* Conversion CTA — carried over from the production site, where Postmedia
     measured this button as a significant driver of quote requests. Loud on
     purpose; reserved for the primary "Free Estimate" action only. */
  --cta:#ff6b00;
  --cta-hover:#E85F00;

  /* Surfaces — cream-first */
  --cream:#FAF7F2;           /* page */
  --cream-raised:#F2EDE4;    /* raised block */
  --off:#F7F3EC;             /* warmer off-cream */
  --grey:#F1EFF3;            /* inset */
  --border:#E3E0E6;          /* hairline */
  --border-warm:#E8E0D2;     /* warm hairline on cream */

  /* Ink (warm neutrals) */
  --text:#0B082A;            /* navy-900 — default body text */
  --text-2:#3A3744;          /* secondary body */
  --muted:#6B6676;           /* caption */
  --light:#8F8B99;            /* tertiary */

  /* Type */
  --heading:'Poppins','Montserrat',sans-serif;
  --sans:'DM Sans',system-ui,sans-serif;
  --ease:cubic-bezier(.2,.8,.2,1);
  /* ── Page widths ──────────────────────────────────────────────────────────
     --max      the reading-content container. Every .inner-style wrapper uses it
                (10 rules), so this single value sets the page's text column.
     --hero-max bounds the full-bleed hero on very wide screens.

     WHY TWO VALUES. The nav and the hero are full-bleed by design — .nav is
     position:fixed with padding:0 48px, and .hero is a viewport-height panel grid
     with padding:12px. Neither reads --max. So on a wide monitor the page had two
     competing widths: chrome/imagery running edge to edge, and content stopping at
     1200px. At 1900px that was a 338px-per-side discrepancy, most visible at the
     trust strip where content snaps inward directly under the hero.

     1200 → 1440 closes it from the content side; --hero-max stops the hero running
     away past ~1720px. Previous values were --max:1200px and no hero bound at all;
     a copy of the stylesheet from before this change is in
     dev/snapshots/style.css.pre-width-experiment.bak.

     Tune both live in the preview with ?widths — see nc_width_switcher() in
     dev/render.php. Nothing else in the CSS hardcodes a page width, so these two
     numbers are the whole control surface. */
  --max:1440px;
  --hero-max:1720px;

  /* ── THE NAV'S GEOMETRY, DEFINED ONCE ──
     --nav-h      the bar's own height.
     --nav-clear  what page content must clear to sit below it. Equal to the height while the
                  bar is flush to the top; larger the moment it floats.

     ⚠ THESE EXIST BECAUSE 68px WAS HARDCODED IN TWENTY-TWO PLACES. Every hero, head and
     sticky offset carried its own copy — .pf-head, .jn-head, .fqe-head, .post-head,
     .svcb-hero, .svc-body, three kitchen heroes, a 100vh calculation and a sticky sidebar at
     top:104px. A number the whole site depends on, that nothing declared.

     It has already produced one visible bug: a clearance 12px short sliced an h1 underneath
     the bar, and it survived review because desktop looked fine. Change the nav's shape here
     and every clearance follows instead of needing to be found. */
  --nav-h:68px;
  --nav-clear:var(--nav-h);

  /* ── THE TRUST STRIP'S HEIGHT, FOR THE HERO TO SUBTRACT ──
     Jesse: "shrink the vertical height of the hero three buttons a bit, so we can see the
     trust bar. I think this'll frame the hero buttons nicely."

     The hero was exactly 100vh - --nav-clear, so its bottom edge landed ON the fold and the
     strip sat wholly below it: measured 832px of hero ending at 900 on a 900px viewport,
     strip 900 → 951. Taking --trust-h off the hero puts the strip's bottom on the fold
     instead, so it reads as the base of the hero rather than the top of the next screen.

     ⚠ 52px IS MEASURED, NOT CHOSEN: 16 padding + a 19px row + 16 padding + a 1px border.
     If .trust's padding, font-size or border change, this changes with them — and it just
     did: the 11px -> 12px nudge took the row from 18 to 19, so this went 51 -> 52.

     ⚠ AND IT IS NOT ONE NUMBER — the strip WRAPS. Its five phrases need 1048px on one row,
     plus .trust's 48+48 gutter = 1144px of viewport. Below that they take two rows and 77px
     (77 = 52 + a 19px row + the 6px row-gap 12px/6px sets). Confirmed to the pixel: 1144 is
     one row, 1143 is two, and 800 is still two rather than three. Subtracting the one-row
     value everywhere would leave the second row sliced 25px below the fold, which looks
     worse than not shrinking at all.

     ⚠ THE BOUNDARY MOVES WITH THE COPY, AND IT HAS MOVED TWICE NOW. 896 when the fourth item
     read "Hundreds of Homes Transformed"; 1020 once the longer rating line replaced it; 1144
     now that "Home of the Kitchen Update" is a fifth item. Re-measure whenever the wording
     changes — never assume it held.

     ⚠ WHAT THE FIFTH ITEM COSTS, since Jesse is watching hero height: between 1024 and 1143
     the strip is two rows, so the hero is 25px shorter there than it would be with four
     items. Every common desktop width is above that — 1280, 1440, 1512, 1728 all stay one
     row and lose nothing. Mobile went 79px to 88px on its own 2-up grid, below the fold
     either way.

     Below 769px none of this applies — the hero drops to height:auto and the strip regrids. */
  --trust-h:52px;

  /* ── HOW MUCH SHORTER THAN THE FOLD THE HERO SITS ──
     Jesse, after seeing the strip land exactly on the fold: "not quite there yet, we can
     revert back to the huge layout we had before, but maybe shorten them a bit more so I
     can see how this would look you know?"

     THIS IS THE KNOB. --trust-h above is measured and should not be tuned; this one is pure
     taste and exists to be tuned. 0 restores the full-height hero exactly as it was, so the
     revert is one number rather than an edit to the formula.

     ⚠ min-height:580px on .hero outranks this on short viewports. Below roughly 700px of
     viewport height the hero is already clamped, so turning this up changes nothing there —
     which is correct, not a bug: there is no room to give away on a 660px screen.

     40 → 12, 2026-09-25 (Jesse: "Elongate the hero buttons a bit so the tagline thing sits
     closer to the bottom of the viewport on first load"). 12 is the hero's own gutter, so
     the strip keeps the same breathing room below it as the panels have between them.
     Measured before the change: 40px above the fold at 1440×790, 1512×870, 1728×1005,
     1920×950 and 2000×910. */
  --hero-trim:12px;
}

/* The wrap band. Bounded at BOTH ends on purpose: 769 because the hero has no fixed height
   below it, 1143 because that is the measured last width where the strip needs two rows. */
@media(min-width:769px) and (max-width:1143px){
  :root{ --trust-h:77px; }
}

body{
  font-family:var(--sans);
  color:var(--text);
  background:var(--cream);
  font-size:16px;
  line-height:1.6;
  overflow-x:hidden;
}
/* height:auto, or an <img> with a height attribute keeps that height when max-width narrows it (the mobile review,
   2026-10-07): every blog post's featured image rendered 342x533 on a phone, its 3:2 aspect-ratio overridden by the
   attribute, and V1's step photos in the cabinet pages' folds were stretched. Measured on all 194 pages at 390, 768 and
   1440: those are the only images it changes. */
img{max-width:100%;display:block;height:auto;}
a{color:var(--brown);text-decoration:none;}

/* ── FADE UP ANIMATION ──
   CONTENT IS VISIBLE BY DEFAULT. The animation is the enhancement; it is never the thing
   that makes the content appear.

   This used to set `opacity:0` on the element itself and rely on the animation to bring it
   back. Two problems with that, found in a design audit 2026-09-01: someone who asks for
   reduced motion got the animation anyway, and in any environment where the animation does
   not run, the copy stays invisible permanently. Content that depends on an animation to
   become readable is one blocked stylesheet away from being gone.

   Gated on `no-preference`, NOT disabled under `reduce`. The difference matters: written the
   other way round, the hiding half applies unconditionally and is only switched off by a
   query the browser has to understand. Anything that does not evaluate that query — or that
   loads the CSS but never runs the animation — is left with invisible copy. Written this
   way the animation is purely additive: when the rule does not apply, for any reason at all,
   the element is simply visible.

   `both` rather than `forwards` supplies the `from` state during animation-delay, so the
   .delay-* stagger reads exactly as it did before. */
@keyframes fadeUp{
  from{opacity:0;transform:translateY(20px);}
  to{opacity:1;transform:translateY(0);}
}
@media(prefers-reduced-motion:no-preference){
  .fade-up{
    animation:fadeUp 0.7s var(--ease) both;
  }
}
.delay-1{animation-delay:0.1s;}
.delay-2{animation-delay:0.2s;}
.delay-3{animation-delay:0.3s;}
.delay-4{animation-delay:0.4s;}

/* ════════════════════
   NAV
════════════════════ */
/* ── Anchor offset ─────────────────────────────────────────
   The nav below is position:fixed at 68px, so without this every
   in-page anchor (#quote, #estimator, #services) scrolls its
   target's top underneath the header — the section heading ends
   up hidden. scroll-padding-top on the scroll container fixes
   every anchor site-wide in one place, including the primary
   Free Estimate CTA which all four buttons now point at.
   68px nav + 16px breathing room.
─────────────────────────────────────────────────────────── */
html{
  scroll-padding-top:84px;
  /* An EXPLICIT canvas background, not just body's.
     A background on <body> only reaches the canvas by propagation, and that stops being
     dependable the moment body gets overflow:hidden — which it does while the nav drawer
     is open. With html transparent the browser then paints its default WHITE beyond the
     body box, which showed as a white band at the bottom of the screen on iOS whenever
     Safari's toolbar shifted the visual viewport. Setting it here means the canvas is
     cream no matter what body is doing. */
  background:var(--cream);
}

/* ── Responsive line breaks ────────────────────────────────
   Several headings use a hardcoded <br> to control where they
   wrap on desktop. On a phone that forced break usually just
   costs a line. Add class="br-md" to any such <br> to collapse
   it below 768px — and put the space AFTER the tag, e.g.
     Get Started<br class="br-md"> Today!
   Leading whitespace is ignored at the start of a wrapped line,
   so desktop is unaffected, and the gap appears once it's hidden.

   Candidates not yet converted (each is a judgement call about
   whether one long line beats two on mobile):
     feature-kitchen, feature-bathroom, feature-exterior,
     estimator, intro h1, hero-panels headings
─────────────────────────────────────────────────────────── */
@media(max-width:768px){
  br.br-md{display:none;}
}

.nav{
  position:fixed;top:0;left:0;right:0;z-index:500;
  background:rgba(250,247,242,0.88);
  backdrop-filter:blur(12px);
  -webkit-backdrop-filter:blur(12px);
  border-bottom:1px solid var(--border-warm);
  height:var(--nav-h);
  display:flex;align-items:center;
  padding:0 48px;
  justify-content:space-between;
}

/* ── Logged-in WordPress admin bar ───────────────────────────────────────────────
   WordPress adds html{margin-top:32px !important} for the toolbar, which moves the
   document but NOT position:fixed elements — so this nav and the drawer tuck underneath
   it and look broken to anyone browsing while logged in. Visitors never see this; the
   people reviewing the site do, which is worse, because it reads as a broken theme.

   The offsets track WordPress's own breakpoints: 32px toolbar on desktop, 46px at
   ≤782px. Below 601px WP switches the bar to position:absolute so it scrolls away with
   the page — offsetting there would leave a permanent 46px gap once it scrolled off, so
   that case goes back to 0. Only exists under real WordPress; dev/render.php has no
   admin bar, so this is untestable in the preview. */
body.admin-bar .nav{top:32px;}
body.admin-bar .nav-drawer{top:42px;}
@media screen and (max-width:782px){
  body.admin-bar .nav{top:46px;}
  body.admin-bar .nav-drawer{top:56px;}
}
@media screen and (max-width:600px){
  body.admin-bar .nav{top:0;}
  body.admin-bar .nav-drawer{top:10px;}
}
.nav-logo{
  font-family:var(--heading);
  font-size:17px;font-weight:800;
  color:var(--navy);letter-spacing:0.02em;
  display:flex;align-items:center;gap:3px;
}
/* Bound the logo at EVERY width, for both branches of header.php — the Customizer
   logo and the hardcoded fallback. Without this base rule the only height rule was
   the one inside @media(max-width:900px), so on desktop a Customizer logo rendered
   at its natural size (720×486 on the staging clone, covering the hero). The mobile
   block overrides this to 52px. Do not rely on an inline style on the fallback <img>:
   the two branches must be styled by the same rule or only one of them gets tested. */
.nav-logo img{height:44px;width:auto;display:block;}
.nav-logo-co{color:var(--brown);}
.nav-logo-tag{
  font-size:9px;font-weight:500;
  color:var(--light);letter-spacing:0.14em;
  text-transform:uppercase;
  margin-left:8px;padding-left:8px;
  border-left:1px solid var(--border);
  line-height:1;
}
.nav-links{
  display:flex;align-items:center;gap:2px;
}
.nav-link{
  font-family:var(--heading);font-size:11px;font-weight:600;
  letter-spacing:0.07em;text-transform:uppercase;
  color:var(--muted);padding:8px 13px;border-radius:6px;
  transition:color 0.2s,background 0.2s;
}
.nav-link:hover{color:var(--navy);background:var(--grey);}

/* ══════════════════════════════════════════════════════════════════════════════
   MEGA MENU

   ONE DOM, TWO PRESENTATIONS. The same markup is a hover dropdown in the desktop
   bar and a slide-out child panel in the mobile drawer. That is deliberate: these
   submenu links exist to restore the site-wide internal links that ten service
   pages lost, and duplicating the markup per breakpoint would either double every
   link or hide half of them from crawlers.

   ⚠ Panels are hidden with visibility/opacity/transform — NEVER display:none on a
   container whose links matter, and never rendered on demand. See the warning in
   northco_nav_item().
   ══════════════════════════════════════════════════════════════════════════════ */

.nav-has-panel{display:contents;}   /* overridden per breakpoint below */
.nav-trigger{display:inline-flex;align-items:center;gap:5px;}
.nav-caret{flex:0 0 auto;transition:transform .22s var(--ease);}

/* ── Desktop: full-bleed dropdown under the bar ───────────────────────────────── */
@media(min-width:901px){
  /* ⚠ DO NOT set position:relative on .nav here.
     It was, briefly, "to make it the containing block for the panels" — but .nav is
     already position:fixed (line ~122), and fixed ALSO establishes a containing block
     for absolutely-positioned descendants, so relative bought nothing and cost a lot:
     it un-fixed the nav, which stopped it sticking on scroll AND left .hero's
     compensating margin-top:68px double-counting, opening a 68px dead band between the
     nav and the hero. */
  .nav-has-panel{display:block;position:static;}

  /* The shared surface. Owns the background, border and shadow that the panels used to
     carry individually, and animates its HEIGHT between panels so switching nav items
     morphs one container instead of closing a box and opening another.
     height is animated from an explicit px value set by nav.js — auto is not animatable,
     and this is the one case where measuring in JS is the correct answer rather than a
     workaround. */
  .nav-mega-bg{
    /* ATTACHED (Jesse, 2026-09-23): flush to the bar's bottom edge, top corners square,
       bottom corners rounded, so it reads as the bar pulled downward. That is what the Kal
       grow animation implies, and Kal's own dropdown is its bar extended: same width, same
       colour. Ours cannot take the bar's width, because the bar is full-bleed and the content
       only spans Kitchens → Contact (a full-width sheet was mocked and left half of it empty),
       so it is the same card, attached. Jesse: "try option 3… we can revert if not what i
       want". The detached version is the commit before this one.

       It used to be DETACHED: 10px below the bar, all four corners rounded, "a card belonging
       to the nav rather than an extension of it". That predates the grow animation, which is
       what made it read as disconnected. The card grew out of the bar and then sat apart from
       it, with a strip of frosted page showing through the gap.

       top:100% is the bar's padding edge, so the card covers the bar's 1px border-bottom
       across its own width. There is no seam under the card, and the line carries on either
       side of it. */
    position:absolute;top:100%;height:0;

    /* left and width are set by nav.js from the .nav-links box, so the card spans
       exactly Kitchens → Contact. It cannot be done in CSS: .nav-links is a flex item
       whose offset depends on the logo width, and there is no static value to hardcode.
       These are the fallbacks for the no-JS case. */
    left:0;right:0;max-width:var(--max);margin:0 auto;

    /* SOLID CREAM, THE BAR'S OWN COLOUR (2026-09-23). Jesse, on Kal's: the dropdown is "same
       colour as the bar". Attached, any translucency shows up as a seam. At the same 0.94 alpha
       as the bar, the card sat over the frosted, navy-tinted hero while the bar sat over the
       page's plain cream, and a 2x close-up of the join showed the card a shade greyer. Solid
       on both, while a menu is open, is the only way they stay one colour whatever is behind
       them. The page around the card is still frosted by .nav-mega-veil, which is where the
       frost reads.

       History: it was rgba(…,0.94) with blur(24px) saturate(140%). 0.82 had read as
       see-through, with the hero competing with the 12.5px --muted sub-labels. The blur went
       with the translucency: behind an opaque fill it does nothing but cost a layer. */
    background:var(--cream);

    /* Sides and bottom only: the bar is the card's top edge. */
    border:1px solid var(--border-warm);border-top:0;
    border-radius:0 0 18px 18px;
    /* Cast downward only. The old 0 20px 44px reached ~24px ABOVE the card's top edge, and the
       card is inside .nav, painted over the bar's own background, so attached it would have
       smudged the bar. A -20px spread pulls the blur's top reach back to the edge exactly. */
    box-shadow:0 24px 44px -20px rgba(11,8,42,0.24);

    opacity:0;visibility:hidden;pointer-events:none;
    transition:height .34s var(--ease),opacity .22s var(--ease),visibility .34s var(--ease);
    z-index:59;                                  /* directly beneath the panels */
  }
  body.nav-mega-open .nav-mega-bg{opacity:1;visibility:visible;}

  /* Same colour as the bar (Jesse, 2026-09-23, on Kal's). While a menu is open, the bar goes
     solid cream like the card, so the two read as one surface rather than two tints meeting at
     a seam (see the note on the card's background). The bar goes solid while scrolling too
     (html.nc-scrolling, far below), so this is a state it already has. The transitions are
     scoped to the open and close states on purpose: a transition on .nav itself would also ease
     that scrolling swap, and during the fade the bar would sit unblurred over moving content. */
  body.nav-mega-open .nav{
    background:var(--cream);
    transition:background-color .6s cubic-bezier(.85,0,.24,1);
  }
  body.nav-mega-shrink .nav{ transition:background-color .5s cubic-bezier(.85,0,.24,1); }

  /* Height animation OFF for this state — note `height` is absent from the list below.
     The .34s height transition exists to MORPH the card when moving between nav items.
     On the FIRST open there is nothing to morph from: the card grows 0 → full height over
     .34s while .nav-panel cross-fades its content in over only .2s, so for ~140ms the
     featured card at the bottom of the panel is painted BELOW the card's bottom edge,
     hanging in mid-air. Delaying the content instead would just trade the bug for a
     sluggish menu, and would slow every switch too.
     So: first open snaps to full height and fades in as one surface (nothing can spill),
     and switching between items still morphs. nav.js adds this class, commits the height
     with a forced reflow, then removes it — see setOpen(). Applied on close for the same
     reason: a collapsing card can outrun its own fading content on the way down. */
  body.nav-mega-instant .nav-mega-bg{
    transition:opacity .22s var(--ease),visibility .22s var(--ease);
  }

  /* ── FIRST OPEN GROWS — after kalstore.com's mega menu (Jesse, 2026-09-22) ──
     Measured on their live site: panel height over .6s on cubic-bezier(.85,0,.24,1), a
     near-flat start and a very soft landing, which is the "sooo smooth" part. Contents fade in
     over .3s after .2s, and the page frosts on the same curve (.nav-mega-veil below). nav.js
     adds nav-mega-grow for the first open only and removes it after 650ms. Switching items
     keeps the .34s morph above.

     ⚠ THE CLIP IS WHAT MAKES GROWING SAFE HERE. Our panel content floats over the card rather
     than sitting inside it, which is why the first open used to snap (see the note on
     nav-mega-instant). Revealing the panel top-down on the same curve keeps the content inside
     the growing card, exactly as Kal's max-height container does by construction. */
  body.nav-mega-grow .nav-mega-bg{
    transition:height .6s cubic-bezier(.85,0,.24,1),opacity .2s var(--ease),visibility .2s var(--ease);
  }
  body.nav-mega-grow .nav-has-panel.is-open > .nav-panel{
    animation:ncMegaReveal .6s cubic-bezier(.85,0,.24,1) both;
  }
  body.nav-mega-grow .nav-has-panel.is-open > .nav-panel .nav-panel-inner{
    animation:ncMegaFade .3s var(--ease) .2s both;
  }
  @keyframes ncMegaReveal{ from{ clip-path:inset(0 0 100% 0); } to{ clip-path:inset(0 0 0 0); } }
  @keyframes ncMegaFade{ from{ opacity:0; } to{ opacity:1; } }

  /* ── CLOSE RISES (Jesse, 2026-09-22): "it smoothly goes back up and the frost blur goes away
     nicely". The card animates back to 0 on the open's curve instead of snapping. Its own fade
     waits until the rise is nearly done (.36s), so it reads as the card lifting away, not
     vanishing. The content fades out in its usual .2s, so the rising edge never cuts through
     visible links. The veil clears on the same curve and duration (below). */
  body.nav-mega-shrink .nav-mega-bg{
    transition:height .5s cubic-bezier(.85,0,.24,1),
               opacity .16s var(--ease) .36s,
               visibility 0s linear .52s;
  }

  /* The page frost. Kal uses blur(7px) under a 70% black; that is right for their dark
     storefront and far too heavy here. "Frosted out slightly" is the brief, so this is the same
     7px blur under a light navy tint. It sits between the page and the nav (z 499 < 500),
     click-through, so hover-out still closes the menu exactly as before.
     ⚠ A FULL-VIEWPORT BLUR IS THE EXPENSIVE KIND (see blur-cost-is-area), which is fine on a
     desktop GPU and is why this lives inside the desktop block: the drawer never shows it. */
  .nav-mega-veil{
    position:fixed;left:0;right:0;bottom:0;top:var(--nav-h,68px);
    background:rgba(22,16,68,.14);
    backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
    opacity:0;visibility:hidden;pointer-events:none;
    /* .5s on close, matching the card's rise (nav-mega-shrink), so frost and card leave together. */
    transition:opacity .5s cubic-bezier(.85,0,.24,1),visibility .5s;
    z-index:499;
  }
  body.nav-mega-open .nav-mega-veil{
    opacity:1;visibility:visible;
    transition:opacity .6s cubic-bezier(.85,0,.24,1),visibility .6s;
  }
  /* ── THE MENU CLOSES WHEN THE CURSOR LEAVES THE CARD (Jesse, 2026-09-22) ──
     "when the cursor goes outside the area it goes away? reasonable grace area". It did not:
     .nav-panel spans the whole nav width (left:0; right:0), invisible beyond the card, and
     while open it took the pointer. So a cursor 150px beside the card was still "inside" and
     the page stayed frosted. Measured: card 220–900px, panel 0–1440px at a 1440 viewport.

     Now only the card's own area takes the pointer: .nav-panel-inner, which nav.js aligns to
     the card. Around it is a 40px grace margin on three sides, and CLOSE_DELAY (320ms, nav.js)
     is the time grace on top. The inner starts at the nav's bottom edge, so the bridge down
     from the trigger is still covered.
     ⚠ NOT UPWARD. A margin into the bar would sit over the neighbouring triggers and stop the
     menu switching from Kitchens to Bathrooms. */
  /* `body` on the front: the is-open rule further down sets pointer-events:auto at the same
     specificity and wins on source order, which is exactly what the first deploy of this did.
     Measured still open with the cursor 150px beside the card. */
  body .nav-has-panel.is-open > .nav-panel{ pointer-events:none; }
  body .nav-has-panel.is-open > .nav-panel .nav-panel-inner{ pointer-events:auto; position:relative; }
  /* The card is sized from the inner's offsetHeight (nav.js), which this margin does not reach.
     It briefly used scrollHeight, which the margin DID reach, and the card grew by its depth. */
  body .nav-has-panel.is-open > .nav-panel .nav-panel-inner::after{
    content:''; position:absolute; top:0; left:-40px; right:-40px; bottom:-40px; z-index:-1;
  }

  @media (prefers-reduced-motion: reduce){
    body.nav-mega-grow .nav-mega-bg,.nav-mega-veil,body.nav-mega-open .nav-mega-veil{ transition:none; }
    body.nav-mega-grow .nav-has-panel.is-open > .nav-panel,
    body.nav-mega-grow .nav-has-panel.is-open > .nav-panel .nav-panel-inner{ animation:none; }
  }

  .nav-panel{
    position:absolute;top:100%;left:0;right:0;
    /* No chrome of its own — .nav-mega-bg provides it. This is what makes the switch
       between nav items read as one surface changing rather than two boxes blinking. */
    background:none;border:0;box-shadow:none;
    /* Hidden without display:none so the links stay in the accessibility tree. */
    opacity:0;visibility:hidden;pointer-events:none;
    /* Opacity only. The translateY(-8px) slide that used to be here made every switch
       look like a fresh drop-in; a straight cross-fade lets the outgoing and incoming
       content pass through each other while the backdrop holds still. */
    transition:opacity .2s var(--ease),visibility .2s var(--ease);
    z-index:60;
  }
  .nav-has-panel.is-open > .nav-panel{
    opacity:1;visibility:visible;pointer-events:auto;
  }
  .nav-has-panel.is-open > .nav-trigger .nav-caret{transform:rotate(180deg);}
  .nav-has-panel.is-open > .nav-trigger{color:var(--navy);}

  .nav-panel-inner{
    /* Aligned to .nav-mega-bg by nav.js, same as the card — see the note there.
       padding-top 36 was 26 plus the floating card's 10px offset. When the card was attached
       to the bar (2026-09-23) the content stayed exactly where it was, which is what the
       mockup Jesse chose showed, so the 36 stayed too. It is now simply the top padding. */
    max-width:var(--max);margin:0 auto;
    padding:36px 28px 30px;
    /* 26px row gap, was 40px. The "View all" row plus a 40px gap plus its own 22px
       margin stacked to ~62px of empty band across the panel, which read as a broken
       layout rather than breathing room. */
    /* SINGLE column: view-all, then the link columns, then the featured card as a full
       width band beneath them.
       It was `1fr 300px` with the featured pinned right, which worked at 1200px wide but
       not once the card was narrowed to the nav-links span (~698px). At that width
       642px of content minus a 300px card left 342px for two 190px columns, so they
       wrapped and the panel grew to 567px with a large void beside the featured card.
       Stacking gives the columns the full width and turns the featured card into a
       short horizontal banner — shorter overall, and no dead space at any width. */
    display:grid;grid-template-columns:1fr;gap:22px;align-items:start;
  }
  .nav-panel-head{display:none;}    /* drawer-only */

  /* justify-self:start is load-bearing.
     grid-column:1/-1 makes this a full-width grid item, and grid items default to
     justify-self:stretch — so the inline-flex box expanded to the whole panel and its
     border-bottom drew a rule right across it, reading as a divider instead of an
     underlined link. start makes it hug its own text again.
     margin-bottom drops to 0 because the grid row gap now does that job. */
  /* .nav-panel-inner .nav-viewall — two classes, because the base .nav-viewall rule
     with margin-bottom:22px is declared LATER in this file and would otherwise win.
     dev/nc test caught exactly this and failed the build, which is the check earning
     its keep for the third time in this stylesheet. */
  .nav-panel-inner .nav-viewall{grid-column:1 / -1;justify-self:start;margin-bottom:0;}

  .nav-panel-cols{display:flex;gap:48px;flex-wrap:wrap;}
  .nav-col{min-width:190px;}

  /* Featured card as a horizontal banner across the foot of the panel. Image left at a
     fixed 150px, copy right — keeps it to ~110px tall instead of the ~260px the vertical
     version cost, which matters because this now sits BELOW the links rather than beside
     them and adds to the panel's height rather than filling space next to it.

     Every selector here is prefixed with .nav-panel-inner or .nav-featured to raise
     specificity: the base .nav-featured / .nav-feat-* rules are declared LATER in this
     file, so single-class selectors here lost silently. dev/nc test failed the build with
     6 dead declarations and caught it — fourth time in this stylesheet. */
  .nav-panel-inner .nav-featured{
    display:grid;grid-template-columns:150px 1fr;align-items:center;
    gap:0 18px;padding:0 18px 0 0;overflow:hidden;
  }
  .nav-featured .nav-feat-img{
    grid-row:1 / span 3;
    height:100%;min-height:104px;margin:0;
  }
  .nav-featured .nav-feat-eyebrow{padding:14px 0 0;}
  .nav-featured .nav-feat-title{padding:3px 0 0;}
  .nav-featured .nav-feat-body{padding:4px 0 14px;}

  /* ⚠ THE IMAGE-LESS CARD, WHICH IS A REAL STATE AND NOT A TEMPORARY ONE.
     The rule above is a 150px image column plus a copy column, with the <img> spanning all
     three text rows. Take the image away and the three spans auto-place into that same grid:
     eyebrow top-left, TITLE top-right, body wrapped four lines deep inside the empty 150px
     column. That is what the About panel was doing — it is the only panel with no photo, so
     it was the only one showing it, and nothing errored.

     One column, and the copy gets the horizontal padding the image column used to provide.
     northco_nav_featured_image_src() decides which class applies, so this state ends by
     itself the day assets/images/services/svc-about.jpg lands. */
  .nav-panel-inner .nav-featured--noimg{
    grid-template-columns:1fr;
    padding:16px 18px;
  }
  .nav-featured--noimg .nav-feat-eyebrow{padding:0;}
  .nav-featured--noimg .nav-feat-title{padding:4px 0 0;}
  .nav-featured--noimg .nav-feat-body{padding:4px 0 0;max-width:62ch;}
}

/* ── Column headings + links (shared) ────────────────────────────────────────── */
.nav-col-label{
  font-family:var(--sans);font-size:10px;font-weight:700;
  letter-spacing:0.12em;text-transform:uppercase;
  color:var(--muted);margin-bottom:12px;
}
.nav-sub{
  display:block;padding:7px 0;text-decoration:none;
}
.nav-sub-label{
  display:block;
  font-family:var(--heading);font-size:14px;font-weight:700;
  color:var(--navy);line-height:1.3;
}
.nav-sub-desc{
  display:block;margin-top:2px;
  font-family:var(--sans);font-size:12.5px;color:var(--muted);line-height:1.35;
}
.nav-sub:hover .nav-sub-label{color:var(--brown-dark);}

.nav-viewall{
  display:inline-flex;align-items:center;gap:7px;
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.05em;text-transform:uppercase;
  color:var(--brown-dark);text-decoration:none;
  padding-bottom:3px;border-bottom:1px solid var(--border-warm);
  margin-bottom:22px;align-self:start;
}
.nav-viewall:hover{color:var(--navy);border-color:var(--navy);}
.nav-viewall-arr{transition:transform .2s var(--ease);}
.nav-viewall:hover .nav-viewall-arr{transform:translateX(3px);}

/* ── Featured card ───────────────────────────────────────────────────────────── */
.nav-featured{
  display:block;text-decoration:none;
  background:var(--white);border:1px solid var(--border-warm);
  border-radius:14px;overflow:hidden;padding-bottom:16px;
}
/* height is mandatory alongside the width/height attributes — see .v2-photo-img. */
.nav-feat-img{
  display:block;width:100%;height:130px;object-fit:cover;margin-bottom:14px;
}
.nav-feat-eyebrow{
  display:block;padding:0 16px;
  font-family:var(--sans);font-size:10px;font-weight:700;
  letter-spacing:0.1em;text-transform:uppercase;color:var(--brown);
}
.nav-feat-title{
  display:block;padding:4px 16px 0;
  font-family:var(--heading);font-size:15px;font-weight:800;
  color:var(--navy);line-height:1.25;
  letter-spacing:var(--track-heading, normal);
}
.nav-feat-body{
  display:block;padding:5px 16px 0;
  font-family:var(--sans);font-size:12.5px;color:var(--muted);line-height:1.4;
}
.nav-featured:hover .nav-feat-title{color:var(--brown-dark);}

/* ── Mobile drawer: chevron rows + slide-out child panels ────────────────────── */
@media(max-width:900px){
  .nav-has-panel{display:block;}

  /* The trigger becomes a full-width row matching .nav-drawer .nav-link, with the
     chevron on the right. The whole row opens the panel (JS preventDefaults the
     href) — one tap target, no label-vs-chevron ambiguity. */
  /* .nav-link.nav-trigger — THREE classes, and the third one is load-bearing.
     The trigger carries both classes, and `.nav-drawer .nav-link{padding:11px 0}` is
     declared LATER in this file at equal specificity, so a two-class selector here
     silently lost and the rows stayed 64px. Adding .nav-link raises specificity so
     order stops mattering.
     Note dev/nc test does NOT catch this: its cascade check compares identical
     selectors, and these two differ. Different-selector collisions on the same element
     still need to be reasoned about by hand.

     7px rather than 11px because the 34px chevron disc already supplies the row height.
     At 11px these rows were 64px against 48px for the flat links, which both looked
     inconsistent and pushed the top-level list 6px past the 714px drawer. */
  .nav-drawer .nav-link.nav-trigger{
    display:flex;align-items:center;justify-content:space-between;width:100%;
    /* 6px keeps the chevron rows level with the 9px flat rows above: the 34px disc plus
       12px equals the 16px text plus 18px+lineheight. Revert to 7px with the rollback. */
    padding:6px 0;
  }
  /* Chevron in a bordered gold disc, not bare.
     A bare chevron is too quiet to read as "tap to expand" for a 40–65 audience —
     especially when the rest of the row looks exactly like the flat links above it,
     which navigate. The disc gives it an unmistakable button affordance, and gold
     separates "opens something" from navy "goes somewhere". 34px, and the whole row
     is the tap target regardless, so the disc is a signal rather than a small target. */
  .nav-drawer .nav-caret{
    transform:rotate(-90deg);                                /* chevron points right */
    width:34px;height:34px;flex:0 0 34px;padding:10px;
    box-sizing:border-box;
    border:1px solid rgba(167,120,69,0.45);border-radius:11px;
    color:var(--brown-dark);
    transition:background .18s var(--ease),border-color .18s var(--ease);
  }
  .nav-drawer .nav-trigger:hover .nav-caret,
  .nav-drawer .nav-trigger:active .nav-caret{
    background:var(--brown-pale);border-color:var(--brown);
  }

  /* Child panel covers the whole drawer. .nav-drawer is position:fixed, so that is
     the containing block — inset:0 fills it, including over the buttons and socials. */
  .nav-drawer .nav-panel{
    position:absolute;inset:0;z-index:5;
    background:var(--cream);
    transform:translateX(100%);
    transition:transform .3s var(--ease);
    overflow-y:auto;-webkit-overflow-scrolling:touch;
    visibility:hidden;
  }
  .nav-drawer .nav-has-panel.is-open > .nav-panel{
    transform:translateX(0);visibility:visible;
  }
  .nav-drawer .nav-panel-inner{padding:14px 22px 30px;}

  .nav-panel-head{
    display:flex;align-items:center;gap:6px;
    margin:0 0 14px;padding-bottom:12px;
    border-bottom:1px solid var(--border-warm);
  }
  /* Back button: 44px TAP TARGET wrapping a 34px gold disc that matches the forward
     chevrons exactly. Two sizes on purpose — the disc is the visual affordance, the
     button is the hit area, and the hit area must stay 44px because this is the only
     way out of a child panel.
     A bare arrow was too quiet to read as a control at all, which is the same reason
     the forward chevrons got discs. Same border, radius and colour so "goes deeper" and
     "comes back" are visibly the same kind of thing. */
  .nav-back{
    width:44px;height:44px;flex:0 0 44px;margin-left:-10px;
    display:inline-flex;align-items:center;justify-content:center;
    background:none;border:0;padding:0;cursor:pointer;
  }
  .nav-back svg{
    width:34px;height:34px;padding:8px;box-sizing:border-box;
    border:1px solid rgba(167,120,69,0.45);border-radius:11px;
    color:var(--brown-dark);
    /* 2.6, not the tag's 2. This icon uses a 24-unit viewBox against the chevron's 12,
       so an identical stroke-width attribute renders visibly thinner once both are
       scaled into a 34px box. Matched by eye against .nav-caret. */
    stroke-width:2.6;
    transition:background .18s var(--ease),border-color .18s var(--ease);
  }
  .nav-back:hover svg,
  .nav-back:active svg{background:var(--brown-pale);border-color:var(--brown);}
  .nav-panel-title{
    font-family:var(--heading);font-size:17px;font-weight:800;color:var(--navy);
    letter-spacing:var(--track-heading, normal);
  }

  .nav-panel-cols{display:flex;flex-direction:column;gap:20px;}
  .nav-col-label{margin-bottom:8px;}
  /* 44px rows — these are the only small tap targets in the panel. */
  .nav-sub{padding:10px 0;min-height:44px;}
  .nav-viewall{margin-bottom:18px;}

  .nav-featured{margin-top:22px;}
  .nav-feat-img{height:110px;}
}
.nav-actions{display:flex;align-items:center;gap:10px;}
/* ── Desktop header actions ──
   ⚠ SINCE 2026-10-15: ALL THREE SOLID AGAIN, REORDERED Free Estimate / phone / Join the Team (header.php has the why).
   For one day the phone was outlined and Join the Team was out of the bar; Jesse undid both ("alex just said to
   reorganize them"). Phones are unchanged throughout: their header keeps the solid orange call pill (its own rule).
   The original note still holds:
   All three solid, matching the live site: phone and Free Estimate in CTA orange, Join
   the Team in brand brown with white text. V1's own values are #FF6B00 and #B0763C —
   --cta is already an exact match for the orange, and --brown is within a few points
   of the gold, so both come from the design system rather than one-off hex.
   An earlier version tinted these as outlines to keep the orange CTA visually dominant.
   Jesse's call is parity with the live site, so they are filled. Don't revert to
   outlines on hierarchy grounds without asking.
   radius is 8px on all three — .nav-call used to be 6px for no reason. */
.nav-call{
  font-family:var(--heading);font-size:11px;font-weight:700;
  letter-spacing:0.06em;
  background:var(--cta);color:#fff;
  padding:8px 16px;border-radius:8px;
  border:0;
  transition:all 0.2s;
}
.nav-call:hover{background:var(--cta-hover);}
.nav-join{
  font-family:var(--heading);font-size:11px;font-weight:600;
  letter-spacing:0.06em;text-transform:uppercase;
  background:var(--brown);color:#fff;
  padding:9px 16px;border-radius:8px;
  border:0;
  transition:all 0.2s;
  /* Without this the label wraps to "JOIN THE / TEAM" and the button becomes 56px
     tall against its 36px neighbours, which knocks the whole bar out of alignment. */
  white-space:nowrap;
}
.nav-join:hover{background:var(--brown-dark);}
.nav-quote{
  font-family:var(--heading);font-size:11px;font-weight:700;
  letter-spacing:0.06em;text-transform:uppercase;
  background:var(--cta);color:white;
  padding:9px 20px;border-radius:8px;
  transition:all 0.2s;
  white-space:nowrap;
}
.nav-quote:hover{background:var(--cta-hover);}

/* Handset icon — hidden on desktop where the number itself is shown. */
.nav-call-ico{display:none;}
.nav-call{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;}

/* One explicit height for all three. Their paddings differ (8/9/9px vertical, and
   16/16/20px horizontal to suit three different label lengths), so without this they
   render as three slightly different boxes. Flex centring keeps the labels optically
   centred inside the fixed box. Must stay AFTER the rules above so it wins on the
   display property. */
.nav-join,.nav-call,.nav-quote{
  display:inline-flex;align-items:center;justify-content:center;
  height:38px;box-sizing:border-box;
}

/* ⚠ THE NARROW DESKTOP BAR, 901–1279px (iPads held sideways, small laptops). With logo, seven links and three buttons
   the bar needs about 1,270px, so until 2026-10-15 it overflowed below that: the logo squeezed to nothing (0px wide at
   1024 and 1100, 19px at 1180) and the last button ran off the right edge. Measured before Join the Team returned, and
   the same after; the order doesn't change the total. Here the gutters and link padding tighten, the phone button
   becomes its handset icon (the aria-label still says "Call …", and it's still a tap to call), and the logo stops
   shrinking. */
.nav-logo{flex-shrink:0;}
@media(min-width:901px) and (max-width:1279px){
  .nav{padding:0 24px;}
  .nav-link{padding:8px 9px;}
  .nav-call{width:38px;padding:0;}
  .nav-call-num{display:none;}
  .nav-call-ico{display:block;}
}
/* Below 1024 even that is ~100px too wide, so Join the Team steps out of the bar (it stays under About, and in the
   drawer below 900). */
@media(min-width:901px) and (max-width:1023px){
  .nav-join{display:none;}
}

/* Orange button variant for use anywhere .btn is used (mobile drawer, etc.) */
.btn-cta{background:var(--cta);color:#fff;border-color:var(--cta);}
.btn-cta:hover{background:var(--cta-hover);border-color:var(--cta-hover);}
/* ══════════════════════════════════════════════
   MOBILE NAV — burger + right-hand drawer
   Below 900px the desktop .nav-links bar is hidden.
   Before the drawer existed there was NO way to reach
   any page on a phone — and 85% of traffic is mobile.
════════════════════════════════════════════════ */
.nav-burger{display:none;}

@media(max-width:900px){
  .nav{padding:0 14px;}
  .nav-links{display:none;}

  /* Mobile header = logo + orange click-to-call + hamburger. Three items, which
     is exactly the production layout.
     The orange pill is the CALL button, not Free Estimate — that is the element
     Postmedia measured as a conversion driver, so it is reproduced as-is rather
     than repurposed. Free Estimate is carried by the sticky bar instead, so the
     two intents (call now / enquire later) both have a one-tap path. */
  .nav-join{display:none;}
  .nav-quote{display:none;}

  .nav-call{
    background:var(--cta);color:#fff;border:0;
    font-family:var(--heading);font-size:13px;font-weight:700;
    padding:10px 18px;border-radius:24px;
    letter-spacing:0.01em;
  }
  .nav-call:hover,.nav-call:active{background:var(--cta-hover);color:#fff;}
  .nav-call-ico{display:none;}   /* number is legible at this size — no icon needed */
  .nav-call-num{display:inline;}

  .nav-actions{gap:8px;}
  /* The logo is a vertical lockup (icon + NORTHCO + SERVICES, ~222×150), so at a small
     height the wordmark reads tiny next to the 128px call pill. 52px gives it presence
     and makes it the header's anchor without crowding the call button + burger. */
  .nav-logo img{height:52px !important;}

  /* The box already matched the orange pill at 44px, but three bare 2px lines
     read far lighter than a solid 127×44 block beside them — the mismatch is
     optical, not dimensional. A circular container gives it comparable mass and
     makes it look like a button, which matters for an audience that skews older.
     Fully rounded to echo the pill's stadium shape.
     gap stays 5px: the animated X below is built on translateY(7px), which only
     lands correctly at that spacing. */
  /* Two buttons, one visual. #navBurger sits in the header; #navDrawerClose sits
     inside the drawer that slides over it, on exactly the same 44x44 box. Styling
     and timing are shared so the element on top always shows the same frame as the
     one beneath — the morph looks continuous instead of a snap when the drawer lands. */
  .nav-burger,
  .nav-drawer-close{
    display:inline-flex;flex-direction:column;justify-content:center;align-items:center;
    gap:5px;width:44px;height:44px;padding:0 10px;   /* 44px = min touch target */
    background:var(--white);
    border:1px solid var(--border-warm);
    border-radius:50%;
    cursor:pointer;margin-left:2px;
    transition:background .28s var(--ease),border-color .28s var(--ease);
  }
  .nav-burger[aria-expanded="true"],
  .nav-drawer.is-open .nav-drawer-close{
    background:var(--navy);
    border-color:var(--navy);
  }
  .nav-burger[aria-expanded="true"] span,
  .nav-drawer.is-open .nav-drawer-close span{background:#fff;}

  .nav-burger span,
  .nav-drawer-close span{
    display:block;width:100%;height:2px;border-radius:2px;
    background:var(--navy);
    /* .28s matches the drawer's .32s slide closely enough that the bars finish
       crossing as the drawer settles. No transition-delay on purpose — a delay on
       one button and not the other would desync them mid-slide, which is exactly
       the snap this is meant to remove.
       gap stays 5px above: translateY(7px) only lands a clean X at that spacing. */
    transition:transform .28s var(--ease),opacity .18s linear,background .28s var(--ease);
  }
  .nav-burger[aria-expanded="true"] span:nth-child(1),
  .nav-drawer.is-open .nav-drawer-close span:nth-child(1){transform:translateY(7px) rotate(45deg);}
  .nav-burger[aria-expanded="true"] span:nth-child(2),
  .nav-drawer.is-open .nav-drawer-close span:nth-child(2){opacity:0;}
  .nav-burger[aria-expanded="true"] span:nth-child(3),
  .nav-drawer.is-open .nav-drawer-close span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}

  /* Respect a reduced-motion preference: land on the X, don't animate to it. */
  @media(prefers-reduced-motion:reduce){
    .nav-burger,.nav-drawer-close,
    .nav-burger span,.nav-drawer-close span{transition:none;}
  }
}

/* Scrim behind the drawer. visibility (not display) so it can transition. */
.nav-scrim{
  position:fixed;inset:0;z-index:998;
  background:rgba(11,8,42,.45);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .3s var(--ease),visibility .3s var(--ease);
}
.nav-scrim.is-open{opacity:1;visibility:visible;pointer-events:auto;}

/* ── EXPERIMENT 2026-08-11: fully detached floating drawer ──
   Matches the desktop mega card — inset from every edge, all corners rounded, reading as
   an object over the page rather than a panel welded to its edge.

   HUGS ITS CONTENT (updated 2026-08-21). Pinned at the top only + height:auto, so the card
   ends just below the social row instead of stretching to the bottom of a tall phone and
   leaving a pool of empty cream. It still never exceeds the screen: the height cap and the
   scrolling both live on .nav-drawer-inner, so a menu taller than the viewport scrolls
   inside the card rather than pushing the social icons off-screen.

   TO ROLL BACK to the full-height panel: set `top:0;right:0;bottom:0;height:100%` and
   `border-radius:20px 0 0 20px` here, and restore .nav-drawer-inner's `min-height:100%;
   height:100%` in place of its max-height. */
.nav-drawer{
  position:fixed;z-index:999;
  top:10px;right:10px;bottom:auto;
  width:min(86vw,360px);height:auto;
  background:var(--cream);
  border-radius:20px;
  box-shadow:0 18px 48px rgba(11,8,42,.22);
  transform:translateX(100%);visibility:hidden;
  transition:transform .32s var(--ease),visibility .32s var(--ease);

  /* overflow:HIDDEN, and this element must NOT be the scroll container.
     Scrolling moved to .nav-drawer-inner — see the note there.

     Why: the closed mega panels are position:absolute to this element at
     transform:translateX(100%), i.e. 346px off to the right, and a transformed child
     still contributes to scrollable overflow. While this element scrolled, that made
     the drawer 691px wide internally against a 346px viewport, so you could swipe
     sideways and find the hidden panels parked out there.
     overflow-x:hidden alone did NOT fix it — it suppresses the scrollbar but the
     overflow area remains and momentum scrolling still reaches it. Separating the
     clipping container (this) from the scrolling container (.nav-drawer-inner) removes
     the scrollable area entirely. */
  overflow:hidden;
}
/*
 * ── OPENING AND CLOSING ON DIFFERENT CLOCKS ──
 * Jesse: "when in mobile view and tapping the hamburger, the nav bar pops out - it's nice -
 * can you smooth the animation there? it comes out so quickly."
 *
 * The transition on .nav-drawer above governs CLOSING (the state being returned to); this
 * one governs OPENING (the state being entered). They were both .32s/--ease, which is right
 * for getting out of the way and too quick for a 360px panel arriving — it read as a pop.
 *
 * .5s on --ease-reveal (easeOutQuart), the same pairing the sticky pill and the section
 * reveals use: fast off the mark, long settle. Closing stays .32s, because dismissing
 * something should feel immediate.
 *
 * visibility 0s, NOT .5s: visibility interpolates as a step, so on the way IN it must flip
 * at once or the panel animates while still unrendered. The base rule keeps it transitioned
 * on the way OUT, which is what holds the drawer visible for the whole slide away.
 */
.nav-drawer.is-open{
  transform:translateX(0);
  visibility:visible;
  transition:transform .5s var(--ease-reveal), visibility 0s;
}

.nav-drawer-inner{
  display:flex;flex-direction:column;
  /* THIS is the scroll container, AND the element that caps the card's height. .nav-drawer
     is height:auto and hugs this element, so the cap has to live here: it grows with its
     content until it hits max-height, then scrolls. The parent clips (overflow:hidden) so
     the off-canvas mega panels cannot create a sideways scroll area.
     max-height mirrors the old top:10 / bottom:max(10,safe-area) pin — dvh tracks Safari's
     toolbar; the plain-vh line is the fallback for browsers without dvh. box-sizing keeps
     the padding inside that cap. */
  max-height:calc(100vh - 20px);
  max-height:calc(100dvh - 10px - max(10px, env(safe-area-inset-bottom)));
  box-sizing:border-box;
  overflow-y:auto;overflow-x:hidden;
  -webkit-overflow-scrolling:touch;
  /* Deliberately NOT justify-content:center — if the menu outgrows the card on a short
     phone, a centred flex column would push the overflow off BOTH ends and make the first
     link unreachable by scrolling. Top-aligned stays recoverable.
     70px top (not 56) clears the close X: at 56 the X (12–56px) sat 7px above and
     overlapping the first row's chevron disc (63–97px), reading as one broken control;
     70px opens that gap to 21px. Bottom uses max() so the social icons clear the iOS home
     indicator when Safari's bottom toolbar is hidden and env() becomes non-zero. */
  padding:70px 26px max(24px, env(safe-area-inset-bottom));
}

/* Position only. The circle, the bars and the ≡→✕ animation are defined with
   .nav-burger inside the max-width:900px block ABOVE, because the two buttons share
   one visual. Do not re-declare background / border / width / height here: this rule
   comes later in the file, so anything set here silently beats the shared rules and
   the morph breaks. (The drawer is hidden above 900px anyway, so there is no desktop
   case that needs its own styling.) */
.nav-drawer-close{
  position:absolute;top:12px;right:14px;
}

/* Reuse of .nav-link markup — override the compact bar styling for the drawer.
   11px padding gives a 48px row: still over the 44px minimum tap target, but 8px
   per row shorter than the old 15px. Across 7 menu items that's 56px reclaimed,
   which is the difference between the social icons sitting below the fold and the
   whole stack fitting on a 714px iPhone viewport. */
.nav-drawer .nav-link{
  /* 9px, was 11px. Funds the floating drawer's 20px vertical inset: ~46px rows instead of
     ~49px, which is ~21px reclaimed across seven rows and still clear of the 44px
     tap-target floor. Revert to 11px if the detached drawer is rolled back. */
  display:block;padding:9px 0;
  border-bottom:1px solid var(--border-warm);
  font-family:var(--heading);font-size:16px;font-weight:600;
  letter-spacing:0;text-transform:none;
  color:var(--navy);text-decoration:none;
}
.nav-drawer .nav-link:last-child{border-bottom:0;}
.nav-drawer .nav-link[aria-current="page"]{color:var(--brown-dark);}

.nav-drawer-actions{margin-top:12px;display:flex;flex-direction:column;gap:9px;}   /* 12px, was 20px — 8 of the 14px reclaimed for the drawer's top padding. */
.nav-drawer-actions .btn{width:100%;text-align:center;justify-content:center;}

/* ── Drawer social row ──
   Four icons under the buttons: Instagram, Facebook, Google reviews, email.
   Fixed margin-top, NOT auto. Auto absorbed all the leftover height into one gap
   above this row, which pinned the icons to the very bottom and left a hole in the
   middle of the drawer. A fixed gap keeps the whole stack together near the top.
   44px targets: this is the one place in the drawer with small tappable things. */
.nav-drawer-social{
  margin-top:14px;padding-top:16px;   /* was 18/18 — the other 6px of the reclaim. */
  /* space-between, not gap: the row is the same 294px as the buttons above it, so
     spreading the four 44px circles edge to edge lines the first and last up with
     the button edges and reads as part of the same stack. A fixed gap would leave
     them bunched at the left. */
  display:flex;align-items:center;justify-content:space-between;
  border-top:1px solid var(--border-warm);
}
.nav-drawer-social .nds-link{
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;border-radius:50%;
  color:var(--muted);
  background:var(--white);
  border:1px solid var(--border-warm);
  text-decoration:none;
  transition:color .18s var(--ease),border-color .18s var(--ease),background .18s var(--ease);
}
.nav-drawer-social .nds-link:hover{
  color:var(--navy);border-color:var(--navy);background:var(--off);
}
/* BBB has no glyph mark, so it's set as type. 11px/800 with tightened tracking is
   the largest "BBB" that clears the 44px circle's inner width without crowding it. */
.nav-drawer-social .nds-word{
  font-family:var(--heading);font-size:11px;font-weight:800;
  letter-spacing:-.02em;line-height:1;color:inherit;
}
/* No per-icon colour overrides here on purpose — all four glyphs are single-path
   fill:currentColor, so they take their colour from .nds-link and change together
   on hover. An earlier rule tried to force the four-colour Google mark to inherit
   currentColor and just rendered it flat black; the mark itself is monochrome now. */

/* Never render either on desktop, regardless of leftover state classes. */
@media(min-width:901px){
  .nav-drawer,.nav-scrim,.nav-burger{display:none !important;}
}

/* Freeze the page behind an open drawer. */
/* Scroll lock while the drawer is open.
   position:fixed, NOT overflow:hidden alone. On iOS Safari overflow:hidden on <body>
   does not reliably stop touch scrolling, and it leaves the body clipped to the viewport
   while the document stays full height — which is what exposed the bare canvas as a
   white band at the bottom of the screen.
   `top` is set inline by nav.js to the negative of the current scroll offset, so the page
   stays visually put instead of snapping to the top; nav.js restores the offset on close.
   width:100% is required — a fixed body collapses to its content width otherwise. */
body.nav-open{
  position:fixed;
  width:100%;
  overflow:hidden;
}


/* ══════════════════════════════════════════════
   ESTIMATOR SCROLL HINT
   Sits just above the sticky pill while the current step has options below the
   fold. Light on purpose — the pill below it is the primary action (navy, or gold
   in step mode), so this has to read as secondary or it competes.
   Shown by nav.js; mobile only.
════════════════════════════════════════════════ */
.est-scroll-hint{display:none;}

@media(max-width:900px){
  .est-scroll-hint{
    position:fixed;z-index:991;
    left:50%;bottom:calc(88px + env(safe-area-inset-bottom, 0px));
    display:inline-flex;align-items:center;gap:7px;
    padding:8px 14px;
    background:rgba(255,255,255,.95);
    -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
    color:var(--navy);
    border:1px solid var(--border-warm);border-radius:999px;
    font-family:var(--heading);font-size:10.5px;font-weight:700;
    letter-spacing:0.08em;text-transform:uppercase;
    box-shadow:0 6px 18px rgba(11,8,42,.16);
    cursor:pointer;

    /* Hidden by default; nav.js adds .is-on. visibility so it can transition. */
    opacity:0;visibility:hidden;pointer-events:none;
    transform:translateX(-50%) translateY(6px);
    transition:opacity .25s var(--ease),transform .25s var(--ease),visibility .25s;
  }

  .est-scroll-hint.is-on{
    opacity:1;visibility:visible;pointer-events:auto;
    transform:translateX(-50%) translateY(0);
    animation:est-nudge 2.1s ease-in-out 1s infinite;
  }
}

/* Gentle vertical nudge — draws the eye without becoming a distraction. */
@keyframes est-nudge{
  0%,100%{transform:translateX(-50%) translateY(0);}
  50%    {transform:translateX(-50%) translateY(5px);}
}

@media(prefers-reduced-motion:reduce){
  .est-scroll-hint.is-on{animation:none;}
}


/* ══════════════════════════════════════════════
   STICKY MOBILE CTA BAR
   Keeps Call and Free Estimate one tap away with the header
   still clean. Better than a header button on a tall phone:
   always visible while scrolling, and in the thumb zone rather
   than 800px up the screen. Opt out with body.no-sticky-cta.
════════════════════════════════════════════════ */
.sticky-cta{display:none;}

@media(max-width:900px){
  /* Floating frosted pill rather than an edge-to-edge bar — it reads as hovering
     over the page instead of chopping the bottom off it, and the translucency
     keeps the content underneath visible so it feels less like lost screen. */
  .sticky-cta{
    display:flex; gap:8px; align-items:center;
    position:fixed; z-index:990;
    left:14px; right:14px;
    bottom:calc(12px + env(safe-area-inset-bottom, 0px));
    padding:8px;
    border-radius:999px;
    background:rgba(250,247,242,.72);
    backdrop-filter:saturate(180%) blur(18px);
    -webkit-backdrop-filter:saturate(180%) blur(18px);
    border:1px solid rgba(255,255,255,.55);
    box-shadow:0 10px 30px rgba(11,8,42,.16), 0 2px 6px rgba(11,8,42,.06);
    /*
     * ── ARRIVING AND LEAVING ARE DIFFERENT MOVES ────────────────────────────────────────
     * Jesse: "I see you nuked the hovering estimate on first load - but it comes back as you
     * scroll down - cool idea. Please have it's animation of coming back on frame be a bit
     * smoother."
     *
     * This rule is now the RETURN only; .is-hidden below carries the exit. They were one
     * .28s/--ease transition serving both, which is what made the return feel abrupt: .28s
     * is right for getting out of the way and far too quick for 140% of travel arriving.
     *
     * --ease-reveal is easeOutQuart, the same curve the section reveals use, and the note in
     * this file already says why: "the right shape for something ARRIVING: fast off the mark,
     * long settle." Transform runs 100ms longer than opacity on purpose — it fades in early
     * and keeps settling after, so it stops rather than lands.
     */
    transition:opacity .38s var(--ease-reveal),
               transform .48s var(--ease-reveal);
  }

  /* Slides away while #quote is on screen — the pill points AT that form, so
     keeping it visible is both redundant and liable to sit over the form's own
     submit button. Toggled by nav.js via IntersectionObserver. */
  /* Leaving stays brisk — see the return's note above. Getting out of the way should not be
     something you watch, and a slow exit over content is the thing being fixed elsewhere. */
  .sticky-cta.is-hidden{
    opacity:0;transform:translateY(140%);pointer-events:none;
    transition:opacity .24s var(--ease),transform .28s var(--ease);
  }
  .sticky-cta a{
    flex:1 1 0; display:inline-flex; align-items:center; justify-content:center;
    gap:8px; height:46px; border-radius:999px; text-decoration:none;
    font-family:var(--heading); font-size:12px; font-weight:700;
    letter-spacing:0.06em; text-transform:uppercase;
  }
  /* Call already has an orange pill in the header, so it is not repeated here —
     and a second orange element would split attention. The sticky bar carries
     the other intent only, in navy so it supports rather than competes. */
  .sticky-cta .sc-call{display:none;}
  .sticky-cta .sc-quote{
    background:var(--navy);color:#fff;flex:1 1 100%;
    transition:box-shadow .25s var(--ease), background .2s var(--ease);
  }
  /* Concept test (2026-08-21): very faint orange glow on hover — a thin ring + soft outer
     halo in the brand CTA orange. Plain :hover (not @media(hover:hover)) so it's visible in
     the device preview; real touch devices have no hover, so nothing shows there. */
  .sticky-cta .sc-quote:hover{
    box-shadow:0 0 0 2px rgba(255,107,0,.5), 0 0 16px rgba(255,107,0,.4);
  }
  .sticky-cta .sc-quote:active{background:var(--navy-700);}

  /* Inside the estimator the pill proxies the step's Next button (see nav.js).
     Gold marks it as "advance the builder" rather than "leave and enquire", and
     the disabled state mirrors the real button so it can't be tapped before the
     step's requirement is met. */
  .sticky-cta.is-step .sc-quote{background:var(--brown);}
  .sticky-cta.is-step .sc-quote:active{background:var(--brown-dark);}
  .sticky-cta.is-step-disabled .sc-quote{
    background:var(--border);color:var(--light);
    cursor:not-allowed;pointer-events:auto;   /* keep it tappable so the tap is a no-op, not a dead zone */
  }

  /* Stop the floating pill covering the last of the page content or the footer.
     Pill occupies 8 + 46 + 8 = 62px, sitting 12px off the bottom. */
  body{padding-bottom:calc(86px + env(safe-area-inset-bottom, 0px));}

  /* Hide it while the drawer is open — the drawer has its own CTAs. */
  body.nav-open .sticky-cta{display:none;}

  /* Opt-out */
  body.no-sticky-cta .sticky-cta{display:none;}
  body.no-sticky-cta{padding-bottom:0;}
}

/* Tablet — full-bleed makes sense on a phone, but stretching one button across
   716px of iPad reads as a banner rather than a floating action. Cap the width
   and centre it: left+right+max-width with auto margins splits the leftover
   space evenly, which works on a fixed-position element. */
@media(min-width:701px) and (max-width:900px){
  .sticky-cta{
    max-width:360px;
    margin-left:auto;
    margin-right:auto;
    bottom:calc(18px + env(safe-area-inset-bottom, 0px));
  }
}

/* ════════════════════
   HERO — 3 panels
   White bg shows between panels.
   Images are the panels themselves.
   Text overlays on bottom of each.
════════════════════ */
.hero{
  /* margin, not margin-top: the `auto` sides centre the hero once --hero-max starts
     biting. Written longhand rather than as margin-inline so it works on the older
     Safari versions this audience is genuinely on. 68px clears the fixed nav. */
  margin:var(--nav-clear) auto 0;
  max-width:var(--hero-max);
  display:grid;
  grid-template-columns:1fr 1fr 1fr;
  /* - --trust-h so the trust strip finishes ON the fold instead of starting there, then
     - --hero-trim for the cream below it. See both notes in :root; --trust-h is measured,
     --hero-trim is the taste knob and 0 restores the original full-height hero. */
  height:calc(100vh - var(--nav-clear) - var(--trust-h) - var(--hero-trim));
  min-height:580px;
  max-height:900px;
  gap:12px;
  /* --cream, not --white. The hero is a 3-column grid with a 12px gap, so its own
     background shows through between the panels and anywhere the panels do not reach —
     and at pure white that read as a pale box against the cream page, most visibly at
     the bottom of the screen with the drawer open: body{overflow:hidden} locks the
     scroll, iOS shifts the visual viewport as the Safari toolbar changes, and the strip
     of exposed hero background appeared as a white rectangle under the navy scrim.
     Matching the page background makes any exposed area invisible whatever causes it. */
  background:var(--cream);
  padding:12px;
}
@media(max-width:768px){
  .hero{
    grid-template-columns:1fr;
    height:auto;
    /* min-height:580px was NOT being reset here — three stacked panels were
       sharing 580px (~193px each), far too short for label + heading + sub +
       pills + CTA, so .panel-content (absolute, bottom:0) overflowed upward
       and collided with .panel-label. */
    min-height:0;
    max-height:none;
    gap:10px;padding:10px;
  }

}

.panel{
  position:relative;overflow:hidden;
  cursor:pointer;
  border-radius:16px;
}
.panel-img{
  position:absolute;inset:0;
  background-size:cover;
  background-position:center;
  transition:transform 0.7s var(--ease);
  /* Lifted, with the shade below kept to the lower half. Guillaume, 2026-10-26: all three photos "a little too shaded",
     keep the pictures and brighten them; "they were darkened so that the text pops, but there might be a way to do the
     lower half". In CSS, not in the files: hero-kitchen/-bathroom/-exterior.jpg are also the portfolio's and Exterior
     Services' fallbacks. */
  filter:brightness(1.1) saturate(1.05);
}
.panel:hover .panel-img{transform:scale(1.04);}

/* Gradient: clear down to 40%, then heavier where the text sits (from ~64% at 1440). It started at 20% and was 0.32 by
   the middle of the photograph (Guillaume, 2026-10-26, above). */
.panel-grad{
  position:absolute;inset:0;
  background:linear-gradient(
    to bottom,
    rgba(10,8,30,0.0) 40%,
    rgba(10,8,30,0.30) 62%,
    rgba(10,8,30,0.72) 100%
  );
  transition:background 0.4s;
}
.panel:hover .panel-grad{
  background:linear-gradient(
    to bottom,
    rgba(10,8,30,0.0) 40%,
    rgba(10,8,30,0.40) 62%,
    rgba(10,8,30,0.78) 100%
  );
}

/* Service label top-left */
.panel-label{
  position:absolute;top:28px;left:28px;
  font-family:var(--heading);font-size:9px;font-weight:700;
  letter-spacing:0.22em;text-transform:uppercase;
  color:rgba(255,255,255,0.92);
  display:flex;align-items:center;gap:8px;
  text-shadow:0 1px 8px rgba(0,0,0,0.4);
}
.panel-label::before{
  content:'';width:20px;height:1.5px;
  background:var(--brown-light);flex-shrink:0;
}

/* NEW badge */
.panel-new{
  position:absolute;top:26px;right:26px;
  font-family:var(--heading);font-size:8px;font-weight:700;
  letter-spacing:0.14em;text-transform:uppercase;
  color:white;
  border:1px solid rgba(167,120,69,0.7);
  padding:4px 10px;border-radius:20px;
  background:rgba(167,120,69,0.3);
  backdrop-filter:blur(4px);
}

/* Bottom content */
.panel-content{
  position:absolute;bottom:0;left:0;right:0;
  padding:32px 32px 36px;
  z-index:1;
}

/* Brown underline — animates in on hover */
.panel-content::before{
  content:'';
  display:block;
  width:32px;height:2px;
  background:var(--brown);
  margin-bottom:14px;
  transform:scaleX(0);transform-origin:left;
  transition:transform 0.4s var(--ease);
}
.panel:hover .panel-content::before{transform:scaleX(1);}

.panel-heading{
  font-family:var(--heading);
  font-size:clamp(20px,2.2vw,28px);
  font-weight:800;
  color:white;
  line-height:1.15;
  letter-spacing:-0.02em;
  margin-bottom:8px;
}
.panel-sub{
  font-size:13px;
  color:rgba(255,255,255,0.6);
  line-height:1.5;
  margin-bottom:18px;
  font-weight:300;
  max-width:260px;
}
/* A soft shade under the words themselves, since the gradient behind them was lightened (2026-10-26, .panel-img). */
.panel-heading,.panel-sub{ text-shadow:0 1px 14px rgba(10,8,30,0.45); }
.panel-pills{
  display:flex;flex-wrap:wrap;gap:5px;
  margin-bottom:18px;
}
.panel-pill{
  font-family:var(--heading);font-size:9px;font-weight:600;
  letter-spacing:0.07em;text-transform:uppercase;
  color:rgba(255,255,255,0.6);
  border:1px solid rgba(255,255,255,0.2);
  padding:3px 9px;border-radius:20px;
  transition:all 0.3s;
}
.panel:hover .panel-pill{
  color:rgba(255,255,255,0.85);
  border-color:rgba(255,255,255,0.35);
}
.panel-cta{
  font-family:var(--heading);font-size:11px;font-weight:700;
  letter-spacing:0.1em;text-transform:uppercase;
  color:white;
  display:inline-flex;align-items:center;gap:7px;
  opacity:0;transform:translateY(6px);
  transition:opacity 0.3s,transform 0.3s;
  text-decoration:none;
}
.panel:hover .panel-cta{opacity:1;transform:translateY(0);}
.panel-cta .arr{transition:transform 0.2s;}
.panel:hover .panel-cta .arr{transform:translateX(4px);}

/* ── Hero panels · mobile ──────────────────────────────────
   MUST live here, AFTER the base .panel-* rules above. A media
   query adds no specificity, so an override placed before the
   base rule loses the cascade on equal specificity — which is
   exactly what happened on the first attempt at this fix.
─────────────────────────────────────────────────────────── */
/* COMPACT is the mobile default.
   All three panels must fit inside a 393×852 viewport *above the sticky CTA bar*
   — at 248px the bar covered the Exterior panel's Explore link, which defeated
   the whole point of the compact layout:
     852 − 68 nav − 74 floating pill − 40 padding/gaps = 670 ÷ 3 ≈ 220
   This matters because siding is 25% of all site conversions and it sits third
   in the stack — at the original 430px it was entirely below the fold on the
   device carrying 85% of traffic. Sub-copy is dropped (it repeats in
   services-trio and services-detail); label, heading, pills and CTA stay. */
@media(max-width:768px){
  /* .panel has no intrinsic height — every child is position:absolute — so a
     stacked layout needs an explicit one or the content overflows the box. */
  .panel{min-height:220px;}

  .panel-sub{display:none;}
  .panel-content{padding:18px 20px 20px;}

  /*
   * PER-PHOTOGRAPH CROP, BECAUSE ONE background-position CANNOT SERVE BOTH SHAPES.
   * Jesse: "for mobile pls move the bathrooms image up a bit since the crop isn't working."
   *
   * hero-bathroom.jpg is 1026x1536 — a 2:3 portrait. Scaled to cover a 370x220 stacked
   * panel it becomes 370x554, so 334px overflows vertically and the shared
   * background-position:center showed only the middle band: marble wall and shower glass,
   * with the vanity cut off entirely. At 100% the frame lands on the faucet and basin, which
   * actually reads as a bathroom at 220px tall. Compared against 75% before choosing — 75%
   * was still abstract marble.
   *
   * Desktop is untouched and must stay that way: there the panel is ~717px tall, the overflow
   * is small, and centre is correct. This is a consequence of the SHORT panel, not of the photo.
   */
  .panel--bathrooms .panel-img{background-position:center 100%;}
  .panel-label{top:16px;left:20px;}
  .panel-new{top:14px;right:16px;}
  .panel-heading{font-size:clamp(22px,5.6vw,28px);margin-bottom:10px;}
  .panel-pills{margin-bottom:12px;}

  /* Weight the gradient higher so short panels keep their text legible: here the heading starts about a third of the way
     down. Lighter than it was (0.05 / 0.42 / 0.80) for the same note, with the text shadow carrying the difference. */
  .panel-grad{
    background:linear-gradient(to bottom,
      rgba(10,8,30,0.0) 0%, rgba(10,8,30,0.30) 40%, rgba(10,8,30,0.72) 100%);
  }

  /* Touch devices have no :hover, so this hover-revealed CTA was permanently
     invisible to the 85% of traffic that is mobile. */
  .panel-cta{opacity:1;transform:none;}
}

/* ── Hero variant · TALL (body.hero-tall) ──────────────────
   The original 430px treatment, kept as an opt-out so the two
   can be compared from the preview switcher. Not the default:
   at 430px the Exterior panel falls below the fold on a phone.
─────────────────────────────────────────────────────────── */
@media(max-width:768px){
  body.hero-tall .panel{min-height:430px;}
  body.hero-tall .panel-sub{display:block;font-size:14px;margin-bottom:14px;}
  body.hero-tall .panel-content{padding:24px 22px 26px;}
  body.hero-tall .panel-label{top:22px;left:22px;}
  body.hero-tall .panel-new{top:20px;right:20px;}
  body.hero-tall .panel-heading{font-size:clamp(26px,6.6vw,34px);margin-bottom:14px;}
  body.hero-tall .panel-grad{
    background:linear-gradient(to bottom,
      rgba(10,8,30,0.0) 20%, rgba(10,8,30,0.32) 55%, rgba(10,8,30,0.72) 100%);
  }
}

/* ════════════════════
   TRUST BAR
════════════════════ */
/* Four supporting facts as plain text with dot separators.
   Previously five pills, one per row on a phone, 257px total — 36% of the viewport.
   Two things caused that and BOTH had to go: the pill chrome (34px tall, 201–258px
   wide each) and 96px of horizontal padding, which left only 306px of a 402px screen,
   narrower than the widest pill. flex-wrap was already set and could not help. */
.trust{
  border-bottom:1px solid var(--border-warm);
  padding:16px 48px;
  background:var(--cream);
}
.trust-inner{
  max-width:var(--max);margin:0 auto;
  display:flex;align-items:center;justify-content:center;
  gap:6px 12px;flex-wrap:wrap;
}
/* 12/600, up from 11/500. Jesse's read once the strip reached the first screen: it was the
   quietest text on the page while carrying the most conversion-relevant words on it. The
   nudge is deliberately one step — the three panel headings have to stay the loudest thing
   in the hero, and a strip that competes with them costs the "epic" the hero is there for.
   Colour is UNCHANGED on purpose: contrast-chasing is what wrecked an approved treatment
   over three rounds once, and legibility was never the complaint here. */
.trust-item{
  font-family:var(--sans);font-size:12px;font-weight:600;
  color:var(--text-2);letter-spacing:0.06em;text-transform:uppercase;
  white-space:nowrap;   /* a phrase breaking mid-way reads as two separate facts */
}

/* The flex, kept to one glyph. --brown and ★ (U+2605) are exactly what .rv-star already
   uses in the reviews section, so this is the same star the page shows later, not a second
   convention. Sized a shade above the text so it reads as a mark rather than a character. */
.trust-item--rating{ display:inline-flex; align-items:center; gap:5px; color:var(--text); }
.trust-star{ color:var(--brown); font-size:13px; line-height:1; }
.trust-sep{color:var(--brown);font-size:13px;line-height:1;}

/* Legacy: .trust-dot / .trust-divider are no longer emitted by trust-bar.php, kept
   only so any other template still using the old markup does not lose its styling. */
.trust-dot{
  width:5px;height:5px;border-radius:50%;
  background:var(--brown);flex-shrink:0;
}
.trust-divider{width:1px;height:20px;background:var(--border);}

@media(max-width:700px){
  /* 20px, not 48px. The wide desktop gutter was costing 56px of usable width on a
     phone, which is most of the reason nothing could share a row. */
  .trust{padding:14px 20px;}
  .trust-divider{display:none;}

  /* 2x2 grid, and the separators GO.
     A dot only reads as a divider when the items sit on one line. Once they wrap to
     their own rows the dots strand at the end of each line and read as stray
     punctuation — which is the "crunched" look. Desktop keeps them because everything
     fits on one row there. */
  .trust-inner{
    display:grid;grid-template-columns:1fr 1fr;
    gap:9px 14px;
    justify-content:initial;align-items:start;
  }
  .trust-sep{display:none;}
  .trust-item--wide{display:none;}   /* a clean 2x2 on phones; see trust-bar.php */
  .trust-item--rating{display:block;}   /* the star rides the first line, not beside a wrapped phrase */
  .trust-item{
    /* nowrap has to be released: the longest fact is ~202px and a grid cell here is
       ~174px, so it must be allowed to break inside its own cell instead of
       overflowing the row. */
    white-space:normal;
    text-align:center;
    font-size:10px;letter-spacing:0.04em;line-height:1.35;
  }
}

/* ════════════════════
   SECTION WRAPPER
════════════════════ */
.section{padding:96px 48px;}
.section.tight{padding:64px 48px;}
.inner{max-width:var(--max);margin:0 auto;}
@media(max-width:768px){
  .section{padding:56px 24px;}
  .section.tight{padding:44px 24px;}
}

/* ════════════════════
   TYPE
════════════════════ */
/* Eyebrow — 12px DM Sans Bold, 0.14em track, gold, with leading 32×2 gold dash */
/* ── Eyebrow ───────────────────────────────────────────────────────────────────────
   NOT WRAPPING IS THE DEFAULT. It used to be opt-in via .nowrap, on the reasoning that
   longer eyebrows still need to break on narrow screens — but of the ~20 eyebrows in this
   theme only five ever opted in, and every one of the rest is a one-to-three-word label:
   "Our work", "The process", "Why NorthCo", "404". Those break into "OUR / WORK" and read
   as a rendering fault.

   A default that fifteen call sites forget is the wrong default. Long eyebrows can opt OUT
   with .eyebrow.wrap; nothing in the theme currently needs to.

   The ::before override is what makes this SAFE. Without it, nowrap in a container too
   narrow for the text pushes overflow off the page and gives you sideways scroll — a worse
   bug than the wrap it fixed. Letting the decorative dash shrink (flex:0 1 32px, floor of
   14px) means the RULE gives way under pressure instead of the words. Worst case the dash
   is stubby and the text still reads on one line. Budget at the floor: 14px dash + 12px
   gap + ~236px text ≈ 262px, inside a 320px viewport's content column. The longest eyebrow
   in the theme is "Every service, in detail" at 24 characters, well under that. */
.eyebrow{
  font-family:var(--sans);font-size:12px;font-weight:700;
  letter-spacing:0.14em;text-transform:uppercase;
  color:var(--brown);margin-bottom:18px;
  display:flex;align-items:center;gap:12px;
  line-height:1.4;
  white-space:nowrap;
}
/*
 * ⚠ flex:0 0 32px, NOT 0 1 32px WITH A min-width FLOOR — and this was a real bug, not a
 * preference. MEASURED across four rendered pages before changing it:
 *
 *     home                8 eyebrows at 14px   2 at 32px
 *     kitchen-renovation  8 at 14px            0 at 32px
 *     careers             1 at 14px            1 at 32px
 *     interior-services   2 at 14px            1 at 32px
 *
 * Roughly 19 against 4. The 32px basis was shrinkable, and .eyebrow is width:fit-content — so
 * the container shrink-wraps its content, the flex line has no free space, and the rule
 * collapsed to its own min-width of 14px. A 14px stub, not the 32px dash that was written.
 *
 * ⚠ AND IT WAS NOT EVEN CONSISTENTLY 14px, which is what makes it a bug rather than a quirk.
 * The trigger is whether the CONTAINER gives the eyebrow more width than its content needs,
 * not which class it carries: .eyebrow.center sets width:auto, so those rendered 32px, and one
 * plain .eyebrow on the careers page rendered 32px purely because of its parent. The same class
 * drew two different rule lengths depending on where it was placed.
 *
 * 32px is the author's evident intent: both places it was controlled explicitly —
 * .eyebrow.center::after and .eyebrow.wrap::before — say 0 0 32px. The min-width was a squeeze
 * floor that became the normal state.
 *
 * Overflow was the thing to check, since .eyebrow is white-space:nowrap and losing the shrink
 * removes the escape valve. The longest string on the site is "Ottawa's Renovation Experts" at
 * 27 characters; verified no eyebrow overflows its container at 390px or 360px. .eyebrow.wrap
 * remains as the opt-out for anything longer, and is now identical to this base rule.
 */
.eyebrow::before{
  content:'';flex:0 0 32px;height:2px;background:var(--brown);
}
/* Kept as a no-op so existing markup that asks for it still reads correctly. */
.eyebrow.nowrap{white-space:nowrap;}
/* Opt-OUT, for an eyebrow long enough to genuinely need two lines. */
.eyebrow.wrap{white-space:normal;}
.eyebrow.wrap::before{flex:0 0 32px;}
.eyebrow{width:fit-content;}          /* display:flex would otherwise span the container */
.eyebrow.center{justify-content:center;width:auto;}
.eyebrow.center::after{content:'';flex:0 0 32px;height:2px;background:var(--brown);}

.section-heading{
  font-family:var(--heading);
  font-size:clamp(30px,3.8vw,48px);
  font-weight:900;color:var(--navy);
  letter-spacing:var(--track-heading, -0.01em);line-height:1.08;
  margin-bottom:18px;
  text-wrap:pretty;
}
/* Upright, in the heading's own font: brown is the accent, not a second typeface. Jesse, 2026-09-29, on the homepage
   h1: "Drop the italics here imo we don't use it anywhere else". The estimator's heading does the same
   (.estimator-heading em). Was italic DM Sans 500. */
.section-heading em{
  color:var(--brown);
  font-style:normal;
}
.section-sub{
  font-size:18px;color:var(--text-2);
  line-height:1.6;max-width:560px;
  text-wrap:pretty;
}

/* ════════════════════
   INTRO — 2 col
════════════════════ */
.intro-grid{
  display:grid;grid-template-columns:1fr 1fr;
  gap:72px;align-items:center;
}
@media(max-width:768px){.intro-grid{grid-template-columns:1fr;gap:40px;}}

/* Animated navy left border on service links */
.service-link{
  display:flex;align-items:center;justify-content:space-between;
  padding:18px 16px;
  border:1.5px solid var(--border);
  border-radius:12px;
  text-decoration:none;
  position:relative;
  transition:all 0.25s var(--ease);
  margin-bottom:10px;
  background:var(--white);
}
/* ⚠ INSET FROM TOP AND BOTTOM, not top:0/bottom:0. It was the pill's full height, which is
   the same height — but the pill has a 12px radius, so at the very top and bottom its own edge
   has curved away and a straight bar there reads as OVERSHOOTING the card. Jesse saw it as the
   bar being taller than the pill; it was the corners, not the height. 10px clears the radius.

   3px rather than 2px, and rounded, so that at the shorter length it still reads as a
   deliberate marker rather than a hairline. Navy is the theme's only blue — there is no
   separate accent blue token — so "accent colour blue" is what it already was. */
.service-link::before{
  content:'';
  position:absolute;left:0;top:10px;bottom:10px;
  width:3px;border-radius:3px;background:var(--navy);
  transform:scaleY(0);transform-origin:center;
  transition:transform 0.3s var(--ease);
}
.service-link:hover::before{transform:scaleY(1);}
/* Keyboard users get the same marker. It was hover-only, so tabbing through these showed the
   background change but not the thing that says which one you are on. */
.service-link:focus-visible::before{transform:scaleY(1);}
.service-link:hover{border-color:var(--brown);background:var(--brown-pale);transform:translateX(4px);}
.service-link:first-child{}

.sl-left{display:flex;flex-direction:column;gap:3px;}
.sl-name{
  font-family:var(--heading);font-size:15px;font-weight:700;
  color:var(--navy);letter-spacing:-0.01em;
  display:flex;align-items:center;gap:10px;
}
/* A tint and a fine border, not a solid chip: the hero panel's "New" (.panel-new) and the site's other brown tints work
   this way. Jesse, 2026-09-29: "make the NEW button pill a bit more transparent to match how you do it across the site".
   Brown-dark text, because plain brown on its own tint is too faint at 8px. Padding 1px less all round for the
   border, so the pill keeps its size. */
.sl-new{
  font-family:var(--heading);font-size:8px;font-weight:700;
  letter-spacing:0.12em;text-transform:uppercase;
  background:rgba(167,120,69,.14);color:var(--brown-dark);
  border:1px solid rgba(167,120,69,.4);
  padding:1px 6px;border-radius:20px;
}

/*
 * The homepage's first call to action breathes: a soft navy ring that grows off the button and fades, every 3.4s,
 * then rests. Jesse, 2026-09-29: "make build your estimate a slow pulse for attention but not too much or over the
 * top". Box-shadow only, so nothing moves or reflows; it stops on hover and focus so the hover shadow shows, and it is
 * off entirely for reduced motion.
 */
@keyframes nc-cta-pulse{
  0%   { box-shadow:0 0 0 0 rgba(22,16,68,.30); }
  45%  { box-shadow:0 0 0 11px rgba(22,16,68,0); }
  100% { box-shadow:0 0 0 11px rgba(22,16,68,0); }
}
.intro-grid .btn-dark{ animation:nc-cta-pulse 3.4s cubic-bezier(.25,.46,.45,.94) infinite; }
.intro-grid .btn-dark:hover,
.intro-grid .btn-dark:focus-visible{ animation:none; }
@media (prefers-reduced-motion:reduce){ .intro-grid .btn-dark{ animation:none; } }
.sl-sub{font-size:13px;color:var(--muted);}
.sl-arr{
  font-size:16px;color:var(--light);
  transition:transform 0.2s,color 0.2s;
}
.service-link:hover .sl-arr{transform:translateX(5px);color:var(--navy);}

/* CTA buttons */
.btn{
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.07em;text-transform:uppercase;
  padding:13px 26px;border-radius:10px;
  transition:all 0.22s var(--ease);
  text-decoration:none;border:none;cursor:pointer;
}
.btn:hover{transform:translateY(-1px);}
.btn-dark{background:var(--navy);color:white;}
.btn-dark:hover{background:#1e1660;box-shadow:0 6px 20px rgba(22,16,68,0.18);}
.btn-brown{background:var(--brown);color:white;}
.btn-brown:hover{background:var(--brown-light);box-shadow:0 6px 20px rgba(167,120,69,0.28);}
.btn-outline{
  background:transparent;
  border:1.5px solid var(--border);
  color:var(--navy);
}
.btn-outline:hover{border-color:var(--navy);}

/* The same secondary button, inverted for dark surfaces — photographs, the navy sections.
   .btn-outline itself is navy-on-transparent and disappears on any of them, which is the
   gap the service heroes had been filling with a bespoke underlined link (.svc-ghost).
   That link was a one-off: three uses, all in heroes, nowhere else on the site, while the
   pattern it was reinventing — a phone number sitting beside the primary CTA — already
   exists as .btn btn-outline in template-parts/intro.php.

   Being a real .btn also fixes what it looked like. .svc-ghost carried padding:14px for a
   44px tap target plus a border-bottom, so the rule sat 16px BELOW the text (measured) and
   read as a detached line rather than an underline, in a box 50px tall next to a 45px
   button. This matches the primary's height by construction. */
.btn-outline-light{
  background:rgba(255,255,255,.06);
  border:1.5px solid rgba(255,255,255,.38);
  color:#fff;
  /* .btn is border:none, so a 1.5px border here would ADD 3px and leave this sitting 48px
     tall beside a 45px primary — measured, and visible when they are side by side in a
     hero. Absorbing the border into the padding keeps the pair the same height without
     touching .btn itself, which every button on the site inherits. */
  padding:calc(13px - 1.5px) calc(26px - 1.5px);
}
.btn-outline-light:hover{border-color:#fff;background:rgba(255,255,255,.14);}
.btn-outline-light:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;}

.btn-sm{padding:10px 18px;font-size:11px;}

/* ════════════════════
   SERVICE FEATURE BLOCKS
   Ultra-clean. Full width image,
   no coloured backgrounds.
   Text uses navy lines as accent only.
════════════════════ */
.feature{padding:80px 48px;}
.feature.alt{background:var(--off);}
@media(max-width:768px){.feature{padding:52px 24px;}}

.feature-grid{
  display:grid;grid-template-columns:1fr 1fr;
  gap:72px;align-items:center;
  max-width:var(--max);margin:0 auto;
}
.feature-grid.rev .feature-text{order:-1;}
@media(max-width:768px){
  .feature-grid{grid-template-columns:1fr;gap:36px;}
  .feature-grid.rev .feature-text{order:0;}
}

.feature-image{
  position:relative;
  border-radius:16px;
  overflow:hidden;
  aspect-ratio:4/3;
}
/* No hover zoom and no transform transition: the photo drifts on scroll instead (.nc-drift,
   motion.js). Jesse, 2026-09-22: "drop the hover zoom in favour of what I asked for". The two
   could not coexist anyway. The drift writes an inline transform every frame, which overrides
   a :hover scale, and a 0.6s transition on transform would make the drift trail the scroll. */
.feature-image img{
  width:100%;height:100%;
  object-fit:cover;display:block;
}

/* Thin brown line accent on image corner */
.feature-image::after{
  content:'';
  position:absolute;
  bottom:-12px;right:-12px;
  width:60px;height:60px;
  border-bottom:2px solid var(--brown);
  border-right:2px solid var(--brown);
  pointer-events:none;
}
.feature-grid.rev .feature-image::after{
  bottom:-12px;left:-12px;right:auto;
  border-right:none;
  border-left:2px solid var(--brown);
}

/* Also used as an <h2> in feature-exterior — margin:0 keeps it identical either way. */
.feature-eyebrow{margin:0;
  font-family:var(--heading);font-size:9px;font-weight:700;
  letter-spacing:0.2em;text-transform:uppercase;
  color:var(--brown);margin-bottom:14px;
}
.feature-heading{
  font-family:var(--heading);
  font-size:clamp(22px,2.8vw,34px);
  font-weight:800;color:var(--navy);
  letter-spacing:-0.02em;line-height:1.15;
  margin-bottom:10px;
  position:relative;
  padding-bottom:20px;
}
/* Thin navy underline on heading */
.feature-heading::after{
  content:'';
  position:absolute;bottom:0;left:0;
  width:40px;height:1.5px;
  background:var(--navy);
}
.feature-hook{
  font-size:15px;font-weight:500;
  color:var(--brown);margin-bottom:16px;
  font-style:italic;
}
.feature-body{
  font-size:15px;color:var(--muted);
  line-height:1.75;margin-bottom:12px;
}
.feature-tagline{
  font-family:var(--heading);font-size:13px;font-weight:700;
  color:var(--navy);margin-bottom:26px;
  letter-spacing:0.02em;
}
.feature-pills{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:28px;}
.feature-pill{
  font-family:var(--heading);font-size:9px;font-weight:600;
  letter-spacing:0.08em;text-transform:uppercase;
  color:var(--muted);
  border:1px solid var(--border);
  padding:5px 11px;border-radius:20px;
}
.feature-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}

/* "Keep going for bathrooms and siding", at the foot of the homepage's kitchen feature (feature-kitchen.php has why). The
   About page's cue dot, breathing and nudging the same way (its keyframes), in a box. */
.feature-cue-row{ max-width:var(--max); margin:56px auto 0; display:flex; justify-content:center; }
@media(max-width:768px){ .feature-cue-row{ margin-top:40px; } }
.home-cue{
  display:inline-flex; align-items:center; gap:14px; padding:8px 22px 8px 8px;
  border:1.5px solid var(--border-warm); border-radius:999px; background:#fff;
  color:var(--navy); font-family:var(--sans); font-size:15px; line-height:1.35; text-decoration:none;
  transition:border-color .2s var(--ease);
}
.home-cue strong{ font-weight:700; }
.home-cue:hover{ border-color:var(--brown); }
.home-cue:focus-visible{ outline:3px solid var(--navy); outline-offset:4px; }
.home-cue-dot{
  position:relative; isolation:isolate; flex:none; display:grid; place-items:center; width:34px; height:34px; border-radius:50%;
  background:var(--brown); color:#fff; transition:background .3s var(--ease);
}
.home-cue-dot::before{
  content:""; position:absolute; inset:0; z-index:-1; border-radius:50%; background:var(--brown);
  animation:ab-cue-breathe 3s ease-in-out infinite;
}
.home-cue-dot svg{ display:block; animation:ab-cue-nudge 3s ease-in-out infinite; }
.home-cue:hover .home-cue-dot{ background:var(--navy); }
@media(max-width:400px){ .home-cue{ font-size:14px; gap:12px; padding-right:18px; } }
@media(prefers-reduced-motion:reduce){
  .home-cue-dot::before{ animation:none; transform:scale(1.3); opacity:.16; }
  .home-cue-dot svg{ animation:none; }
}

/* ════════════════════
   ESTIMATOR SECTION
   White bg. Navy border accent.
   Doesn't break the white aesthetic.
════════════════════ */
.estimator-section{
  padding:80px 48px;
  border-top:1px solid var(--border);
  border-bottom:1px solid var(--border);
  background:var(--off);
}
/* ⚠ A PASSED-IN BAND CLASS BEATS THE DEFAULT ABOVE. Callers that hand this section a
   .tp-band / .tp-band--alt put it into the page's alternation; callers that do not (the
   homepage) keep the fixed --off it always had. Both rules are two classes deep, so these
   win on source order and must stay after. */
.estimator-section.tp-band{ background:var(--cream); }
.estimator-section.tp-band--alt{ background:var(--off); }

.estimator-inner{max-width:860px;margin:0 auto;}

/* ── THE HEADER SITS ON THE PAGE GUTTER ───────────────────────────────────────────────────
   ⚠ .estimator-inner caps at 860px and centres, which put the h2 290px from the edge while
   every other heading on the page starts at 48px. Left-aligning the text inside that box was
   only half the fix — Jesse: "the left align of the header is still not correct to the page".
   A heading that is left-aligned but at a different left edge from its neighbours reads as a
   mistake in a way a centred one does not.

   So the HEADER escapes the 860px box and the WIDGET keeps it, sharing the same left edge:
   both now start at the section's own 48px padding. The form keeps the measure it was designed
   for; only its introduction spans the page. */
/* EVERY ESTIMATOR, NOT ONLY THE BANDED ONES (2026-09-29). These rules were keyed on .tp-band, which only the service
   pages pass, so the homepage's estimator, called with no band, kept the old centred 860px box: its heading 340px in
   from every other section's at 1440. Jesse: "the estimator tool - needs to be left aligned correctly". The band
   still sets the background (above); the layout is now the component's own, the same on every page. */
/* ⚠ margin:0 auto, NOT margin-left:0. These have to CENTRE inside the section the way .inner
   does, not hug the left padding — .inner is max-width:var(--max) with auto side margins, so
   above 1440px it is inset by (viewport - 1440) / 2 and every heading on the page moves right
   with it.

   The first version used margin-left:0, which left-aligns. At exactly 1440 that is
   indistinguishable — both land on the 48px padding — and 1440 was the only width I measured,
   so it passed. At 1920 every other heading sits at 240px and the estimator's sat at 48px,
   which is what Jesse was looking at.

   ⚠ ANY ALIGNMENT FIX ON THIS PAGE NEEDS CHECKING ABOVE 1440 AS WELL AS AT IT. Below --max
   nothing is centred and every left edge agrees by accident. */
.estimator-section .estimator-inner{ max-width:var(--max); margin:0 auto; }

/* ── TWO COLUMNS, BECAUSE ONE LEFT 484px DOING NOTHING ────────────────────────────────────
   Jesse: "how can we use this page better? it's empty". Measured at 1440: an 860px widget in
   a 1344px band, 484px of dead space to its right, with a 221px header stacked above it.

   The page already answers this two sections down — .faq-grid is 5fr/7fr, heading column left
   and content right. Same split here, so the estimator stops being the one block that ignores
   the page's layout vocabulary.

   ⚠ NOT STICKY ANY MORE (2026-09-29). The header rode alongside the widget to keep "Rather just
   talk to someone?" on screen through the flow. Jesse, after a step change landed the widget
   half off screen: "it's an issue caused by this header following alongside as i scroll … maybe
   just fix the header at the top of the tile". So it sits at the top of its column, level with
   the widget. (The landing itself was estimator.js scrolling natively under Lenis; that is fixed
   there, in goTo().) */
/* ⚠ 1200px, NOT 1000. At 1000 the 7fr column left the widget 504px wide — narrower than
   the 860 it was designed for and too tight for its two-column choice grids, which is trading
   a working form for a tidier margin. At 1200 the widget holds 618px and its binary grids sit
   at ~260 a side. Below that the stacked layout is simply better. */
@media(min-width:1200px){
  .estimator-section .estimator-inner{
    display:grid; grid-template-columns:5fr 7fr; gap:clamp(40px,4vw,72px);
    align-items:start;
  }
  .estimator-section .estimator-header{ align-self:start; margin-bottom:0; }
  /* ⚠ NO SMALLER HEADING IN THIS COLUMN. It used to drop to 30px here (clamp 22–30px) to keep the two sentences to
     three lines, which made it the one main heading off the section scale. Jesse, 2026-09-29, once the homepage's
     estimator took this layout: "header is not the same font size as the other headers - we JUST went through fixing
     this issue". So it keeps .estimator-heading's section size (48px) and wraps, balanced (mont.css), in the column. */
  .estimator-section .estimator-sub{ font-size:15.5px; }
  .estimator-section .estimator-wrap{ max-width:none; }
}

/* Below the split it is one column again, and the header goes back to being an intro. */
@media(max-width:1199px){
  .estimator-section .estimator-header{ max-width:var(--max); margin:0 auto; }
  .estimator-section .estimator-wrap{ max-width:860px; margin-right:auto; }
}

/* ── THE ESTIMATOR HEADER WAS THE ONLY CENTRED TEXT ON THE SITE ───────────────────────────
   Jesse: "thoughts on it being the only center aligned wording on the whole page? or do we
   left align for parity?"

   MEASURED BEFORE ANSWERING, and it is not a Kitchen-Update question — the estimator is an
   outlier on the HOMEPAGE too. Nine of that page's ten h2s sit at text-align:start, left edge
   48px; this one sat centred with its left edge at 290px. Same on the service page, where
   every other section heading is at 48px and this one was 242px adrift.

   ⚠ AND IT DISAGREED WITH ITS OWN WIDGET. The step headings inside the builder
   (.step-heading) are already left-aligned, so a centred header sat directly on top of a
   left-aligned form — the misalignment was internal to the component, not just against the
   page.

   So it goes left everywhere rather than being keyed to the page, which also stops the
   homepage and the service pages diverging on the same component. */
.estimator-header{text-align:left;margin-bottom:44px;}
.estimator-heading{
  font-family:var(--heading);
  font-size:clamp(30px,3.8vw,48px);   /* the section size, not 38px: 2026-09-29 */
  font-weight:800;color:var(--navy);
  letter-spacing:-0.025em;line-height:1.15;
  margin-bottom:12px;
}
.estimator-heading em{color:var(--brown);font-style:normal;}
/* max-width was 440px with margin:0 auto — a narrow centred column that ran the Kitchen Update
   copy to three ragged lines. Left-aligned it no longer needs centring, and 62ch is the
   reading measure the rest of the page's prose uses. */
.estimator-sub{
  font-size:16px;color:var(--muted);
  max-width:62ch;margin:0;line-height:1.65;
}
.estimator-wrap{
  border:1.5px solid var(--border);
  border-radius:20px;
  overflow:hidden;
  background:var(--white);
  box-shadow:0 2px 40px rgba(22,16,68,0.06);
}

/* ════════════════════
   REVIEWS

   Own markup, not a plugin's widget. Base rules FIRST, mobile overrides in the
   media query at the END of this block — media queries add no specificity, so a
   base rule placed after one silently wins. dev/nc test now checks for exactly
   that, because this stylesheet has been bitten by it repeatedly.
════════════════════ */
/* Left-aligned, matching every other section header on the page. This block was the
   only centred one, which is what made the section read as a different site. */
.reviews-header{text-align:left;margin-bottom:28px;}

/* Legacy pair, still used by the old markup elsewhere. Kept so nothing regresses. */
.stars{font-size:20px;letter-spacing:3px;color:var(--brown);display:block;margin-bottom:6px;}
.rating-text{
  font-family:var(--heading);font-size:13px;font-weight:700;
  color:var(--navy);letter-spacing:0.04em;
}

/* Smaller than .section-heading's clamp(30px,3.8vw,48px). At 48px "Ottawa homeowners'
   experiences" ran to two lines and dominated a section that is supporting evidence,
   not a headline. */
.rv-heading{
  text-align:left;margin-bottom:16px;
}

/* ── Aggregate: the score is the point of the section, so it gets real size ── */
.rv-aggregate{
  display:inline-flex;align-items:center;gap:14px;
  padding:12px 22px;border-radius:999px;
  background:var(--white);border:1px solid var(--border-warm);
}
.rv-agg-score{
  font-family:var(--heading);font-size:38px;font-weight:800;line-height:1;
  color:var(--navy);
}
.rv-agg-meta{display:flex;flex-direction:column;align-items:flex-start;gap:3px;}
.rv-agg-count{
  font-family:var(--sans);font-size:12px;color:var(--muted);letter-spacing:0.02em;
}

/* ── Stars ── */
.rv-stars{display:inline-flex;gap:2px;line-height:1;}
.rv-star{color:var(--brown);font-size:14px;}
.rv-star.is-empty{color:var(--border);}

/* ── Cards ── */
.rv-grid{
  display:grid;grid-template-columns:repeat(3,1fr);gap:18px;
  align-items:start;
}
/* Ported from the uiverse card Jesse picked. Tailwind → tokens:
     bg-white p-6 space-y-4      → var(--white), 22px padding, 14px flex gap
     border border-gray-300      → 1px solid var(--border-warm)
     rounded-lg / shadow-lg      → 14px radius + the soft navy-tinted shadow below
     bg-red-500 (avatar)         → var(--brown); red is not in the palette and reads
                                   as an error state next to navy and cream
     text-red-500 (stars, link)  → var(--brown) / var(--brown-dark)
   Structure is his — identity, stars, text, Read more. */
.rv-card{
  /* blockquote carries UA margin — reset it or the gap doubles up. */
  margin:0;
  display:flex;flex-direction:column;gap:14px;
  padding:22px 20px;
  background:var(--white);
  border:1px solid var(--border-warm);border-radius:14px;
  /* shadow-lg equivalent, tinted navy rather than neutral grey so it sits in the
     warm palette instead of looking like a grey box on cream. */
  box-shadow:0 4px 16px rgba(11,8,42,0.06);
}
.rv-text{
  font-family:var(--sans);font-size:15px;line-height:1.6;color:var(--navy);
}
.rv-text p{margin:0 0 10px;}
.rv-text p:last-child{margin-bottom:0;}

/* ── Clamp for long reviews ──
   line-clamp needs -webkit-box even in 2026: the standardised `line-clamp` is still
   not universally supported, and -webkit-line-clamp is. The full text stays in the
   DOM either way — this only limits what is painted.

   .no-js safety: the toggle is display:none until nav.js adds body.rv-js, and the
   clamp is gated on that same class. If the script never runs you get the full review
   and no dead button, rather than truncated text with no way to expand it. */
.rv-card.is-long .rv-text{
  display:block;
}
body.rv-js .rv-card.is-long:not(.is-open) .rv-text{
  display:-webkit-box;
  /* 5 lines on desktop, 4 on mobile (see the media query at the end of this block).
     Every card clamps, so every card is the same height and the grid reads as a set. */
  -webkit-line-clamp:5;
  -webkit-box-orient:vertical;
  overflow:hidden;
  /* FIXED height, not min-height — min-height was tried and does not work. The clamp
     counts rendered lines but says nothing about the 10px gap BETWEEN paragraphs, so a
     two-paragraph review came out 5 lines + 10px = taller than a one-paragraph review
     at 5 clean lines, and no floor can pull it back down. A fixed box clips that gap
     and every clamped card measures identically.
     1.6em is one line at this line-height, so this tracks the type scale — a hard px
     value would break the moment font-size changed. Paragraph margins are left intact
     rather than zeroed, because running two paragraphs together mid-sentence reads as
     a missing space. */
  height:calc(5 * 1.6em);
}
/* ⚠ AND WHILE CLAMPED, THE PARAGRAPHS RUN ON. The fixed height above could not clip the gap between two paragraphs,
   because the gap sits mid-text: it pushed the last line down and the box cut that line in half (the mobile review,
   2026-10-07, Joseph M.'s review on every kitchen page). Run inline, a two-paragraph review is one block of whole lines
   ending in the clamp's ellipsis; opened, the paragraphs come back. The space is one the markup lacks between </p><p>. */
body.rv-js .rv-card.is-long:not(.is-open) .rv-text p{ display:inline; margin:0; }
body.rv-js .rv-card.is-long:not(.is-open) .rv-text p + p::before{ content:" "; }
/* align-items:START, and it must stay that way.
   This was briefly `stretch` to equalise card heights, which backfired badly: grid
   stretches every item to the ROW height, so expanding one card dragged its two
   neighbours to the same height and left them with a block of dead space. Expanding one
   review must not touch the others.
   Equal heights come for free anyway while clamped — same clamp, same header, same
   toggle means identical natural heights — so nothing was gained by forcing it. */
body.rv-js .rv-grid{align-items:start;}

.rv-toggle{
  display:none;                       /* shown only once nav.js is confirmed running */
  align-self:flex-start;
  background:none;border:0;padding:4px 0;margin:0;cursor:pointer;
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.05em;text-transform:uppercase;
  color:var(--brown-dark);
  border-bottom:1px solid var(--border-warm);
  min-height:32px;                    /* not a 44px target: it is secondary to the card */
}
body.rv-js .rv-card.is-long .rv-toggle{display:inline-flex;align-items:center;}
.rv-toggle:hover{color:var(--navy);border-color:var(--navy);}
.rv-toggle .rv-toggle-less{display:none;}
.rv-card.is-open .rv-toggle .rv-toggle-more{display:none;}
.rv-card.is-open .rv-toggle .rv-toggle-less{display:inline;}

/* Now a <header> at the TOP of the card, not a footer. No margin-top:auto — that was
   pushing it to the bottom, which is exactly the layout this replaced. */
.rv-by{display:flex;align-items:center;gap:11px;}
.rv-role{
  font-family:var(--sans);font-size:12px;color:var(--muted);
  letter-spacing:0.01em;line-height:1.3;
}

/* 48px displayed while the tag carries width="96" height="96" for a 2x screen.
   The explicit height here is load-bearing: an <img> height ATTRIBUTE beats
   aspect-ratio, so without a CSS height these would render 48x96 and stretch every
   card in the row — the same bug that inflated the estimator upgrade cards. */
.rv-avatar{
  width:48px;height:48px;flex:0 0 48px;
  border-radius:50%;object-fit:cover;display:block;
  background:var(--grey);
}
/* Solid disc with white initials, per the uiverse card — bg-red-500 mapped to
   var(--brown). The earlier pale-gold-on-cream version was too quiet to anchor the
   card now that identity leads it. */
.rv-avatar-ph{
  display:flex;align-items:center;justify-content:center;
  font-family:var(--heading);font-size:16px;font-weight:800;
  letter-spacing:0.02em;
  color:#fff;background:var(--brown);
}
.rv-who{display:flex;flex-direction:column;gap:2px;min-width:0;}
.rv-name{
  font-family:var(--heading);font-size:14px;font-weight:700;
  color:var(--navy);font-style:normal;   /* cite italicises by default */
}
/* Unnamed reviewer. Deliberately quieter than a real name — it should not look like
   equally strong attribution, because it isn't. */
.rv-name-anon{font-weight:600;color:var(--muted);font-size:13px;}
.rv-date{font-family:var(--sans);font-size:12px;color:var(--muted);}

/* ── The single outbound link, kept quiet and at the foot ── */
.rv-more{text-align:left;margin:20px 0 0;}
.rv-more a{
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.05em;text-transform:uppercase;
  color:var(--muted);text-decoration:none;
  border-bottom:1px solid var(--border-warm);padding-bottom:2px;
}
.rv-more a:hover{color:var(--navy);border-color:var(--navy);}

@media(max-width:900px){
  .rv-grid{grid-template-columns:1fr;gap:10px;}
  /* Tighter on a phone: three stacked cards are the tallest thing in this section,
     and the trust strip above already taught us how fast vertical space goes. */
  .rv-card{padding:14px 15px;gap:9px;border-radius:12px;}
  .rv-text{font-size:14px;line-height:1.55;}
  /* 4 lines here, and the fixed height must track it — 1.55em is one line at the
     mobile line-height set just above. */
  body.rv-js .rv-card.is-long:not(.is-open) .rv-text{
    -webkit-line-clamp:4;
    height:calc(4 * 1.55em);
  }
  .rv-aggregate{gap:12px;padding:9px 16px;}
  .rv-agg-score{font-size:30px;}
  .reviews-header{margin-bottom:18px;}
  .rv-heading{margin-bottom:12px;}
  /* The avatar is the only thing forcing the footer taller than the text needs. */
  .rv-avatar{width:40px;height:40px;flex-basis:40px;}
  .rv-avatar-ph{font-size:16px;}
}

/* ════════════════════
   WHY CHOOSE — minimal
   No background fill. Just grid of
   items with a brown dot accent.
════════════════════ */
.why-grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:0;
}
@media(max-width:900px){.why-grid{grid-template-columns:1fr 1fr;}}
@media(max-width:500px){.why-grid{grid-template-columns:1fr;}}
.why-item{
  padding:32px;
  border-right:1px solid var(--border);
  border-bottom:1px solid var(--border);
  transition:background 0.2s;
}
.why-item:hover{background:var(--off);}
.why-item:nth-child(3n){border-right:none;}
.why-item:nth-last-child(-n+3){border-bottom:none;}
@media(max-width:900px){
  .why-item:nth-child(2n){border-right:none;}
  .why-item:nth-child(3n){border-right:1px solid var(--border);}
  .why-item:nth-last-child(-n+3){border-bottom:1px solid var(--border);}
  .why-item:nth-last-child(-n+2){border-bottom:none;}
}
.why-dot{
  width:7px;height:7px;border-radius:50%;
  background:var(--brown);margin-bottom:16px;
}
.why-name{
  font-family:var(--heading);font-size:13px;font-weight:700;
  /* margin-top:0 is required: these are <h3> so they carry the site's six "Why Choose"
     headings in the document outline (V1 had them as H3 and V2 had demoted them to
     divs). An h3 brings a UA margin-block-start of ~1em, which would push every item
     down inside its grid cell. */
  color:var(--navy);margin:0 0 7px;letter-spacing:var(--track-heading, -0.01em);
}
.why-body{font-size:13px;color:var(--muted);line-height:1.6;}

/* (The homepage's About tile and its .about-* rules went on 2026-09-29: its values moved into the Our story band and
   Guillaume's line became template-parts/manifesto.php. See front-page.php.) */

/* ════════════════════
   CONTACT FORM
   Dark section — the only non-white bg.
   Justified since it's functional.
════════════════════ */
.contact-section{background:var(--navy);padding:80px 48px;}
@media(max-width:768px){.contact-section{padding:52px 24px;}}
.contact-grid{
  display:grid;grid-template-columns:1fr 2fr;
  gap:72px;align-items:center;
  max-width:var(--max);margin:0 auto;
}
@media(max-width:768px){.contact-grid{grid-template-columns:1fr;gap:36px;}}
.contact-heading{
  font-family:var(--heading);
  font-size:clamp(30px,3.8vw,48px);   /* the section size: 2026-09-29 */
  font-weight:800;color:var(--white);
  letter-spacing:-0.025em;line-height:1.1;
  margin-bottom:12px;
}
.contact-sub{font-size:14px;color:rgba(255,255,255,0.45);line-height:1.65;margin-bottom:24px;}
/* .contact-line, the 32px brown rule under the form block's sub line, is gone (2026-09-29): the eyebrow above already
   draws the same rule. Jesse: "remove the duplicate line below the wording imo it's weird". */
/* "business day." never splits, so the sub line can't end on "day." alone: when both words don't fit, both move down
   and the break falls after "one". Jesse, 2026-09-29: "make the subtext wrap at one so the second line is business
   day - right now one word only on the second line looks uneven". A span, not a forced <br>, which would misfire at
   the widths where the first line is shorter. */
.contact-sub .nowrap{ white-space:nowrap; }
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;}
@media(max-width:500px){.form-row{grid-template-columns:1fr;}}

/* Cross-link between the short form and the estimator, so whichever one a
   visitor lands on, the other is one tap away rather than a dead end. */
.form-alt{
  margin:18px 0 0;
  font-size:14px;line-height:1.55;color:var(--muted);
}
/* .center is still honoured where a caller asks for it, but the estimator's escape-hatch line
   now sits under a left-aligned header, so it is left too — see .estimator-header. */
.form-alt.center{text-align:center;margin-top:14px;}
.estimator-header .form-alt.center{text-align:left;}

/* ── Estimator offer, inset above the first field ──────────
   Gold wash so it registers before someone starts typing, but
   stays clearly secondary to the navy submit button below —
   the goal is a visible fork in the road, not a second CTA
   competing with the primary one.
─────────────────────────────────────────────────────────── */
.form-alt-box{
  display:flex;align-items:center;gap:16px;flex-wrap:wrap;
  background:var(--brown-pale);
  border:1px solid rgba(167,120,69,0.28);
  border-radius:14px;
  padding:14px 16px;
  margin:0 0 22px;
}
.fab-text{flex:1 1 200px;min-width:0;}
.fab-text strong{
  display:block;
  font-family:var(--heading);font-weight:700;font-size:14.5px;
  line-height:1.3;letter-spacing:-0.005em;color:var(--navy);
}
.fab-text span{
  display:block;margin-top:3px;
  font-size:12.5px;line-height:1.45;color:var(--brown-dark);
}
.fab-link{
  flex:0 0 auto;
  display:inline-flex;align-items:center;gap:7px;
  background:var(--white);
  border:1px solid rgba(167,120,69,0.40);
  color:var(--brown-dark);
  font-family:var(--heading);font-size:11.5px;font-weight:700;
  letter-spacing:0.05em;text-transform:uppercase;
  padding:10px 16px;border-radius:999px;
  text-decoration:none;white-space:nowrap;
  transition:background 0.2s,color 0.2s,border-color 0.2s;
}
.fab-link:hover{background:var(--navy);border-color:var(--navy);color:#fff;}
.fab-link .arr{transition:transform 0.2s var(--ease);}
.fab-link:hover .arr{transform:translateX(3px);}

/* ── Quiet variant ──
   Postmedia: the boxed version pulled attention out of the form it lives inside. This
   strips the panel and the button down to a single line of text with an inline link,
   so the fork stays discoverable without competing with the fields below it.

   Placed AFTER the rules it overrides — .form-alt-box and .fab-link are both defined
   above, so anything here wins on equal specificity. Reversing that order silently
   breaks it, which is what dev/nc test's first check exists to catch. */
/*
 * ⚠ A WHOLE PILL, OR NO PILL. This first kept the base pill's border-radius:999px while swapping its border for a
 * bottom-only underline, so the underline curved up at both ends like half a pill; and it sat on its own line 5px in
 * from the question. Jesse, 2026-09-29: "the pill around it seems a bit off pls fix that". Now a small, whole outline
 * pill on the question's line, wrapping under it only when the column is too narrow. Still quiet: no fill until hover.
 * Coloured for the navy form block, its only home (contact-form-top.php): the question at the block's own muted white,
 * the pill in brown-light. The old grey and brown-dark were light-background colours, at about 3:1 on navy.
 */
.form-alt-box.form-alt-quiet{
  display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;
  background:none;border:0;border-radius:0;
  padding:0;margin:0 0 16px;
}
.form-alt-quiet .fab-text{flex:none;}
.form-alt-quiet .fab-text strong{
  display:inline;
  font-family:var(--sans);font-weight:500;font-size:13px;
  letter-spacing:0;color:rgba(255,255,255,.62);
}
.form-alt-quiet .fab-link{
  display:inline-flex;align-items:center;gap:6px;width:auto;
  background:none;border:1px solid rgba(187,140,87,.55);border-radius:999px;
  padding:5px 12px;margin:0;
  font-family:var(--sans);font-size:13px;font-weight:600;
  letter-spacing:0;text-transform:none;
  color:var(--brown-light);
}
.form-alt-quiet .fab-link:hover{
  background:var(--brown);border-color:var(--brown);color:#fff;
}

/* Narrow columns: the pill goes full-width under the text rather than squeezing. */
@media(max-width:560px){
  .form-alt-box{gap:12px;padding:14px;}
  .fab-link{width:100%;justify-content:center;}
}
.form-alt a{
  color:var(--brown-dark);text-decoration:none;font-weight:600;
  white-space:nowrap;
}
.form-alt a:hover{color:var(--navy);text-decoration:underline;}
.form-alt a .arr{display:inline-block;transition:transform 0.2s var(--ease);}
.form-alt a:hover .arr{transform:translateX(3px);}
.form-alt-note{
  display:block;margin-top:3px;
  font-size:12.5px;color:var(--light);
}
.fi{
  width:100%;
  background:rgba(255,255,255,0.07);
  border:1px solid rgba(255,255,255,0.12);
  color:white;padding:12px 14px;border-radius:8px;
  font-size:14px;font-family:var(--sans);
  transition:border-color 0.2s;outline:none;
  margin-bottom:0;
}
.fi:focus-visible{border-color:rgba(167,120,69,0.5);box-shadow:0 0 0 3px rgba(167,120,69,0.28);}
/* Ring, not just a border tint. outline:none is set above, and on a dark field a 1px
   border-colour change is not a discoverable focus indicator for anyone tabbing through. */
.fi:focus{border-color:rgba(167,120,69,0.5);}
.fi::placeholder{color:rgba(255,255,255,0.28);}
.fi-select{
  width:100%;
  background:rgba(255,255,255,0.07);
  border:1px solid rgba(255,255,255,0.12);
  color:rgba(255,255,255,0.6);padding:12px 14px;border-radius:8px;
  font-size:14px;font-family:var(--sans);
  transition:border-color 0.2s;outline:none;
  margin-bottom:10px;cursor:pointer;
  -webkit-appearance:none;
}
.fi-select option{background:var(--navy);color:white;}
.fi-select:focus-visible{border-color:rgba(167,120,69,0.5);box-shadow:0 0 0 3px rgba(167,120,69,0.28);}
.fi-select:focus{border-color:rgba(167,120,69,0.5);}
.fi-textarea{
  width:100%;
  background:rgba(255,255,255,0.07);
  border:1px solid rgba(255,255,255,0.12);
  color:white;padding:12px 14px;border-radius:8px;
  font-size:14px;font-family:var(--sans);
  resize:vertical;min-height:90px;
  transition:border-color 0.2s;outline:none;
  margin-bottom:10px;
}
.fi-textarea:focus-visible{border-color:rgba(167,120,69,0.5);box-shadow:0 0 0 3px rgba(167,120,69,0.28);}
.fi-textarea:focus{border-color:rgba(167,120,69,0.5);}
.fi-textarea::placeholder{color:rgba(255,255,255,0.28);}
.fi-submit{
  width:100%;background:var(--brown);color:white;
  border:none;padding:14px;border-radius:8px;
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.1em;text-transform:uppercase;
  cursor:pointer;transition:background 0.2s;
}
.fi-submit:hover{background:var(--brown-light);}

/* ════════════════════
   FOOTER
════════════════════ */
/* ── MINIMAL FOOTER (standalone careers build) ─────────────────────────────────────────
   .footer-minimal and .footer-min-* live in assets/css/careers.css, but these three came
   from here and were deleted with the old footer block on 2026-09-03 — an index-based slice
   that took three classes the careers page still uses along with the ones it was meant to
   replace. Exactly the range-edit mistake this project has a standing rule against, and the
   careers site is LIVE, so it would have shipped unstyled links to real applicants.

   Restored VERBATIM, not improved. The values are poor — .38 on links and .20 on the
   copyright are roughly 2.3:1 and 1.4:1 against this background, both failing WCAG AA — but
   the careers build is closed work and quietly restyling a live page is not a side effect a
   footer rebuild gets to have. Flagged for Jesse separately. */
.footer-minimal .flink,
.footer-minimal .fcopy,
.footer-minimal .footer-bottom{font-family:var(--sans);}
.flink{font-size:13px;color:rgba(255,255,255,0.38);text-decoration:none;transition:color 0.2s;}
.flink:hover{color:rgba(255,255,255,0.75);}
.footer-bottom{
  max-width:var(--max);margin:0 auto;
  padding-top:24px;border-top:1px solid rgba(255,255,255,0.06);
  display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;
}
.fcopy{font-size:12px;color:rgba(255,255,255,0.2);}

/* ── FOOTER ────────────────────────────────────────────────────────────────────────────
   Rebuilt after wearevap.com, 2026-10-06 (Jesse: "largely copy this layout and adjust to our theme but have our logo
   there"; footer.php has the brief). The logo large and alone at the top left, the link columns under it, and on the
   right "Follow us" over three tall cards of our social posts, where short clips will go. Our navy, not their black.

   CONTRAST WAS THE QUIET PROBLEM IN THE OLD ONE. Links sat at rgba(255,255,255,.38) and the copyright at .20 over
   #0d0a26, roughly 2.3:1 and 1.4:1, well under WCAG AA's 4.5:1, and close to invisible for a 40-65 audience on a phone
   in daylight. Links stay at .78 and the copyright at .55. */
.footer{background:var(--navy-dark);padding:clamp(48px,5vw,64px) 48px 0;}
.ftr-wrap{max-width:var(--max);margin:0 auto;}

/* Three rows, one job each (Jesse, on the first compact build: "kinda busy - the top has no flow"). Compact on purpose:
   the scroll reveal only runs where the whole footer fits the window (motion.js), and the first build was 970px tall
   at 1440x900.

   Row one: the logo, and the way to act at the other end. */
.ftr-head{
  display:flex;align-items:center;justify-content:space-between;gap:20px 24px;flex-wrap:wrap;
  padding-bottom:clamp(28px,3vw,40px);margin-bottom:clamp(32px,3.5vw,48px);border-bottom:1px solid rgba(255,255,255,.12);
}
.ftr-brandline{display:flex;align-items:center;gap:20px clamp(24px,3vw,40px);flex-wrap:wrap;}
.ftr-logo{display:inline-block;flex:none;}
.ftr-logo img{display:block;width:clamp(130px,10vw,160px);height:auto;}
/* The Google rating, a quiet pill beside the logo: the reviews band's ★ in --brown, the average, the count. */
.ftr-rating{
  display:inline-flex;align-items:center;gap:10px;padding:9px 16px;border-radius:999px;
  border:1px solid rgba(255,255,255,.16);text-decoration:none;font-family:var(--sans);font-size:14px;line-height:1;
  transition:border-color .2s, background-color .2s;
}
.ftr-rating:hover{border-color:var(--brown);background:rgba(255,255,255,.04);}
.ftr-rating:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;}
.ftr-rating-stars{color:var(--brown);letter-spacing:2px;font-size:14px;}
.ftr-rating-num{color:#fff;font-weight:700;}
.ftr-rating-txt{color:rgba(255,255,255,.72);}
.ftr-logo:focus-visible{outline:2px solid var(--brown-light);outline-offset:4px;border-radius:4px;}
.ftr-act{display:flex;align-items:center;gap:clamp(18px,2vw,28px);flex-wrap:wrap;}
.ftr-phone{
  font-family:var(--heading);font-weight:700;font-size:clamp(18px,1.5vw,22px);
  color:#fff;text-decoration:none;letter-spacing:var(--track-heading, normal);
}
.ftr-phone:hover{color:var(--brown-light);}
.ftr-phone:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;border-radius:2px;}

/* Row two: the link columns and "Follow us", their titles on one line. */
.ftr-grid{
  display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);
  gap:44px clamp(40px,5vw,96px);align-items:start;padding-bottom:clamp(36px,4vw,52px);
}
.ftr-nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:32px 24px;}

.ftr-col-title{
  font-family:var(--sans);font-size:12px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;
  color:#fff;margin:0 0 16px;
}
.ftr-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:11px;}
.ftr-link{
  font-family:var(--sans);font-size:14px;line-height:1.4;
  color:rgba(255,255,255,.78);text-decoration:none;
  transition:color .2s;
}
.ftr-link:hover{color:#fff;text-decoration:underline;text-underline-offset:3px;}
.ftr-link:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;border-radius:2px;}
.ftr-addr{font-family:var(--sans);font-size:14px;line-height:1.5;color:rgba(255,255,255,.62);}

/* The reference badges these in green. Gold here — the palette is locked to the brand, and a
   green pill is the one colour on the page that belongs to nothing else. */
.ftr-badge{
  display:inline-block;vertical-align:middle;margin-left:6px;
  font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  padding:3px 7px;border-radius:5px;
  background:var(--brown);color:#fff;
}
.ftr-cta{margin:0;display:inline-flex;}

/* "Follow us": the heading with the three platforms beside it (the reference's "see all"), over the cards. */
.ftr-follow{min-width:0;}
.ftr-follow-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:12px;}   /* the title centred on the icons, the cards close under them */
.ftr-follow-head .ftr-col-title{margin:0;}
.ftr-socials{display:flex;gap:10px;}
.ftr-soc{
  display:inline-flex;align-items:center;justify-content:center;
  width:38px;height:38px;border-radius:50%;
  border:1px solid rgba(255,255,255,.16);
  color:rgba(255,255,255,.72);
  transition:color .2s,border-color .2s,background-color .2s,transform .2s;
}
.ftr-soc:hover{background:var(--brown);border-color:var(--brown);color:#fff;transform:translateY(-2px);}
.ftr-soc:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;}

/* The cards: the reels' covers, 3:4 as they're cut (assets/images/social/). Clips will take the same box. */
.ftr-reels{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;}
.ftr-reel{
  position:relative;display:block;aspect-ratio:3 / 4;border-radius:14px;overflow:hidden;background:#1a1640;
  transition:transform .45s var(--ease), box-shadow .45s var(--ease);
}
.ftr-reel img{display:block;width:100%;height:100%;object-fit:cover;margin:0;transition:transform .6s var(--ease);}
.ftr-reel:hover{transform:translateY(-4px);box-shadow:0 18px 40px rgba(0,0,0,.35);}
.ftr-reel:hover img{transform:scale(1.04);}
.ftr-reel:focus-visible{outline:3px solid var(--brown-light);outline-offset:3px;}
.ftr-reel-badge{
  position:absolute;top:10px;right:10px;width:28px;height:28px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.92);color:var(--navy);
}
@media(prefers-reduced-motion:reduce){ .ftr-reel, .ftr-reel img{ transition:none; } }

/* Row three: the small print, the contact details at one end and the copyright at the other. */
.ftr-bottom{
  border-top:1px solid rgba(255,255,255,.12);
  padding:22px 0 30px;
  display:flex;align-items:center;justify-content:space-between;gap:12px 24px;flex-wrap:wrap;
}
.ftr-contact{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:4px 24px;}
.ftr-bottom .ftr-link,
.ftr-bottom .ftr-addr{font-size:13px;}
.ftr-fine{display:flex;align-items:center;gap:8px 22px;flex-wrap:wrap;}
.ftr-copy{font-family:var(--sans);font-size:13px;color:rgba(255,255,255,.55);margin:0;}
.ftr-legal{display:flex;gap:22px;}
/* "Cookie settings" reopens the cookie notice (assets/js/consent.js): a button, since it goes nowhere, dressed as its neighbour. */
.ftr-cookie{background:none;border:0;padding:0;cursor:pointer;}

/* Narrower: the cards go under the links, three across. */
@media(max-width:1100px){
  .ftr-grid{grid-template-columns:minmax(0,1fr);align-items:start;}
  .ftr-reels{grid-template-columns:repeat(3,minmax(0,220px));}
}
@media(max-width:900px){
  .footer{padding:48px 24px 0;}
  .ftr-nav{grid-template-columns:repeat(2,minmax(0,1fr));gap:36px 24px;}
  .ftr-contact{gap:0 22px;}
  .ftr-bottom{flex-direction:column;align-items:flex-start;gap:12px;}

  /* Footer links are 19px of text. On a phone that is a 19px touch target, and 85% of this
     site's traffic is mobile with an audience of 40-65 — the group least served by a small
     target. Padding rather than font-size: the type stays where the design put it and the
     TARGET grows to ~43px, close to the 44px guideline. The row gap goes to zero because the
     padding now supplies the separation; without that the footer grew by a third. */
  .ftr-list{gap:0;}
  .ftr-link{display:inline-block;padding:12px 0;}
  .ftr-addr{padding:12px 0;}
}
/* A phone: two columns of links (the old footer was one long column, 1,731px), and the cards a row you swipe, the next
   one peeking, as the reference does. */
@media(max-width:700px){
  .ftr-reels{
    display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:12px;
    margin:0 -24px;padding:0 24px 8px;scroll-padding:0 24px;scrollbar-width:none;   /* the snap keeps the gutter */
  }
  .ftr-reels::-webkit-scrollbar{display:none;}
  .ftr-reels li{flex:0 0 62%;scroll-snap-align:start;}
}
.flegal a{font-size:12px;color:rgba(255,255,255,0.2);text-decoration:none;}
.flegal a:hover{color:rgba(255,255,255,0.45);}


/* ── WIDGET ── */
.widget{
  width:100%;max-width:820px;
  background:var(--white);
  border-radius:22px;
  box-shadow:var(--shadow-lg);
  overflow:hidden;
}

/* ════════════════════════════════════════════════════
   V2 MOTIFS — 2026 refresh
   Services trio · Founder quote · Recent work grid
   Testimonial · FAQ · Navy contact band
════════════════════════════════════════════════════ */

/* Shared section rhythm on cream */
.v2-section{padding:128px 48px;}
.v2-section.tight{padding:96px 48px;}
.v2-section.cream-raised{background:var(--cream-raised);}
.v2-section.navy{background:var(--navy);color:#fff;}
.v2-inner{max-width:var(--max);margin:0 auto;}
@media(max-width:768px){
  .v2-section{padding:80px 24px;}
  .v2-section.tight{padding:64px 24px;}
}

/* Shared: photo placeholder block (cream-raised with hairline label) */
.v2-photo{
  position:relative;width:100%;aspect-ratio:4/3;
  background:var(--cream-raised);
  border-radius:10px;overflow:hidden;
  display:flex;align-items:flex-end;
  border:1px solid var(--border-warm);
}
.v2-photo--tall{aspect-ratio:3/4;}
.v2-photo--wide{aspect-ratio:16/10;}
.v2-photo--square{aspect-ratio:1/1;}

/* Real photo inside a .v2-photo slot.
   height:100% is LOAD-BEARING. The tag carries width="760" height="570" so the browser
   reserves space and the page does not jump on load — but a height attribute outranks
   the parent's aspect-ratio, so without this the image renders 373x570 and stretches
   the card. object-fit:cover then crops rather than distorts. Same failure the
   estimator upgrade photos hit; dev/nc test now guards it. */
.v2-photo-img{
  display:block;width:100%;height:100%;
  object-fit:cover;object-position:center;
  /* Above .v2-photo::before, which is absolutely positioned and would otherwise paint
     its placeholder diagonal across the photo. See the note on that rule. */
  position:relative;z-index:1;
}
.v2-photo-label{
  font-family:var(--sans);font-size:11px;font-weight:500;
  color:var(--muted);letter-spacing:0.14em;text-transform:uppercase;
  padding:14px 18px;
  border-top:1px solid var(--border-warm);
  background:rgba(250,247,242,0.6);
  width:100%;
}
/* Diagonal "empty slot" marker. It is position:absolute, so it paints ABOVE the
   non-positioned children — including a real <img> — which drew a faint line straight
   across the service photos once they landed. Two guards, deliberately both:
     :has(img)  skips painting it entirely when a photo is present
     z-index    on .v2-photo-img keeps the photo on top regardless, so this is still
                right if :has() is ever unavailable or the markup changes shape */
.v2-photo::before{
  content:"";
  position:absolute;inset:0;
  background-image:
    linear-gradient(135deg, transparent 49.5%, var(--border-warm) 49.5%, var(--border-warm) 50.5%, transparent 50.5%);
  opacity:0.5;
}
.v2-photo:has(img)::before{display:none;}

/* ── Service card photo hover: the tint ──
   A navy rgba(10,8,30,…) tint, matched to the hero panels (.panel) so the two sections feel
   like one system. The panels' scale(1.04) zoom used to be matched here too. It was dropped
   on 2026-09-22 in favour of the scroll drift (see the note on .feature-image img). ::after is
   the tint layer because ::before is already the placeholder diagonal above.

   Gated on @media(hover:hover) as well as the desktop width. Width alone is not enough:
   on a touch device :hover STICKS after a tap, so the tint would stay stuck over the
   photo until you tapped elsewhere — which looks like a rendering bug. hover:hover
   excludes touch entirely, and 85% of this site's traffic is touch. */
@media (hover:hover) and (min-width:901px){
  /* The scale(1.04) zoom is gone; the photo drifts on scroll instead. The tint below stays.
     See the note on .feature-image img: same decision, same reason. */

  .v2-photo:has(img)::after{
    content:'';position:absolute;inset:0;
    /* Above .v2-photo-img, which sits at z-index 1. */
    z-index:2;pointer-events:none;
    background:rgba(10,8,30,0);
    transition:background 0.4s var(--ease);
  }
  .svc-card:hover .v2-photo:has(img)::after{background:rgba(10,8,30,0.28);}
}

/* ── Eyebrow + heading block (reusable header) ── */
.v2-header{max-width:640px;margin:0 0 56px;}
.v2-header.center{margin-left:auto;margin-right:auto;text-align:center;}
.v2-h2{
  font-family:var(--heading);font-weight:900;
  font-size:clamp(30px,3.8vw,48px);   /* the section size (.section-heading), not 56px: 2026-09-29 */
  line-height:1.04;letter-spacing:var(--track-heading, -0.01em);
  color:var(--navy);
  text-wrap:pretty;
  margin:0;
}
/* Upright, in the heading's own font, as the h1's accent is (.section-heading em). Jesse, 2026-09-29, on the
   homepage's italic accents: "do them all now". Was italic DM Sans 500. */
.v2-h2 em{
  color:var(--brown);font-style:normal;
}
/* 620px, not 560px. Measured at 19px: 560 gives 57 characters a line and 620 gives
   ~63 — both inside the 45–75 range that reads comfortably, so this is not a
   readability change. It exists so the paragraph fills its 640px column instead of
   stopping 80px short of the heading above it, which was the actual visual complaint.
   Do NOT remove the cap entirely: at the full 1184px inner width this would run to
   ~120 characters a line, which is genuinely hard to read. */
.v2-lede{
  font-size:19px;line-height:1.55;color:var(--text-2);
  margin:20px 0 0;max-width:620px;
  text-wrap:pretty;
}
.v2-header.center .v2-lede{margin-left:auto;margin-right:auto;}
/*
 * The services trio's header takes the width it needs: the heading on one line wherever it fits (1,036px at its 56px
 * top size, so from about 1024px up), wrapping only on smaller screens, and the lede at 900px (about 85 characters a
 * line) instead of 620. Jesse, 2026-09-29: "the header can be one line (wrapping on smaller displays, fair, but not
 * baseline), and extend the subheading to go longer, this is a waste of space imo right now". Scoped to this section:
 * the FAQ's header sits in a column, and the recent-work header, which shares its row with "All projects", has its own
 * rule (.work-head .v2-header).
 */
#services-trio .v2-header{ max-width:none; }
#services-trio .v2-lede{ max-width:900px; }

/* ════════════════════
   Services trio (3-up)
════════════════════ */
.services-grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:32px;
}
@media(max-width:900px){.services-grid{grid-template-columns:1fr;gap:40px;}}


/* ══════════════════════════════════════════════
   SERVICES DETAIL — one card per service, real H2s
   Plain cream so it reads as its own band, not
   another feature row; photograph cards since
   2026-09-29 (below).
════════════════════════════════════════════════ */
.svcd-section{padding:112px 48px;background:var(--cream);}
@media(max-width:768px){.svcd-section{padding:72px 24px;}}
/* Its intro as a lede, the size and weight of Our story's (.founder-story-p), across the band rather than held to the
   640px header and 620px lede caps. Jesse, 2026-09-29: "Make this bigger, like the about us tile … and the everpresent
   issue of it wrapping too soon, please let it breathe a little". Navy on cream, as Our story's is white on navy. */
.svcd-section .v2-header{ max-width:none; }
.svcd-section .v2-lede{ max-width:none; margin-top:0; font-size:clamp(20px,1.9vw,26px); font-weight:500; line-height:1.5; color:var(--navy); text-wrap:pretty; }

/* ── PHOTOGRAPH CARDS, NOT A DIRECTORY ───────────────────────────────────────────────────────────────────────────
   Jesse, 2026-09-29: "restyle the services tile so it's less of a directory". It was a bordered two-column table of
   text cells. Now each service is a card with its page's photograph, the family, the heading (still a real, visible
   H2: the point of the block), its line and the link, and the whole card is the link (the anchor's ::after stretches
   over it). The four kitchen services fill the first row and bathrooms and the two exteriors the second, so each row
   is a family. The photographs stand one height across both rows, whatever the card's width. */
.svcd-grid{ display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:44px 24px; margin-top:52px; }
.svcd-card{ grid-column:span 3; position:relative; display:flex; flex-direction:column; min-width:0; }
.svcd-card:nth-child(n+5){ grid-column:span 4; }
.svcd-pic{ display:block; height:clamp(180px,15vw,230px); border-radius:14px; overflow:hidden; background:var(--off); margin-bottom:22px; }
.svcd-pic img{ display:block; width:100%; height:100%; object-fit:cover; }

.svcd-eyebrow{
  font-family:var(--sans);font-size:10px;font-weight:700;
  letter-spacing:0.14em;text-transform:uppercase;
  color:var(--brown);margin-bottom:10px;
}
.svcd-h2{
  font-family:var(--heading);font-weight:800;
  font-size:clamp(18px,1.45vw,21px);
  line-height:1.22;letter-spacing:var(--track-heading, -0.005em);
  color:var(--navy);margin:0 0 10px;
  text-wrap:balance;
  transition:color .2s var(--ease);
}
.svcd-blurb{
  font-size:15px;line-height:1.6;color:var(--text-2);
  margin:0 0 18px;text-wrap:pretty;
}
.svcd-link{
  margin-top:auto;
  font-family:var(--heading);font-size:12px;font-weight:700;
  letter-spacing:0.05em;text-transform:uppercase;
  color:var(--brown-dark);text-decoration:none;
  display:inline-flex;align-items:center;gap:8px;
}
/* The whole card is the link. Its keyboard focus draws round the card, where the click lands. */
.svcd-link::after{ content:""; position:absolute; inset:-10px; border-radius:18px; }
.svcd-link:focus-visible{ outline:none; }
.svcd-link:focus-visible::after{ outline:2px solid var(--brown); outline-offset:0; }
.svcd-link .arr{transition:transform 0.25s var(--ease);}
.svcd-card:hover .svcd-h2{ color:var(--brown-dark); }
.svcd-card:hover .svcd-link .arr{ transform:translateX(4px); }

/* A tablet: two across; the seventh, soffit and fascia, takes the last row whole. */
@media(max-width:1100px){
  .svcd-card, .svcd-card:nth-child(n+5){ grid-column:span 6; }
  .svcd-card:nth-child(7){ grid-column:1 / -1; }
}
/* A phone: rows, the photograph a square beside the words, so seven services don't run to seven screens. */
@media(max-width:700px){
  .svcd-grid{ grid-template-columns:minmax(0,1fr); gap:28px; margin-top:40px; }
  .svcd-card, .svcd-card:nth-child(n+5), .svcd-card:nth-child(7){
    grid-column:auto; display:grid; grid-template-columns:104px minmax(0,1fr); column-gap:16px; align-content:start;
  }
  .svcd-pic{ grid-row:1 / span 4; height:104px; margin-bottom:0; border-radius:12px; }
  .svcd-card > *:not(.svcd-pic){ grid-column:2; }
  .svcd-eyebrow{ margin-bottom:6px; }
  .svcd-h2{ font-size:18px; }
  .svcd-blurb{ font-size:14.5px; margin-bottom:12px; }
  /* Inline, so a link that wraps keeps its arrow after the last word instead of stranding it at the far edge. */
  .svcd-link{ display:inline; }   /* and the arrow is joined to the last word (&nbsp;, services-detail.php) */
}
.svc-card{
  display:flex;flex-direction:column;
  background:transparent;
  border-radius:16px;
  transition:transform var(--dur-base,200ms) var(--ease);
}
.svc-card .v2-photo{
  border-radius:10px;
  margin-bottom:24px;
  aspect-ratio:4/3;
}
.svc-eyebrow{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:0.18em;text-transform:uppercase;
  color:var(--brown);margin-bottom:12px;
  display:flex;align-items:center;gap:10px;
}
.svc-eyebrow::before{
  content:"";width:24px;height:2px;background:var(--brown);flex:0 0 24px;
}
.svc-name{
  font-family:var(--heading);font-weight:900;
  font-size:28px;line-height:1.1;letter-spacing:var(--track-heading, -0.01em);
  color:var(--navy);margin:0 0 10px;
}
.svc-blurb{
  font-size:15px;line-height:1.6;color:var(--text-2);
  margin:0 0 20px;
}
.svc-meta{
  display:flex;gap:16px;align-items:center;
  margin-bottom:20px;
}
.svc-meta-item{
  font-family:var(--sans);font-size:12px;font-weight:500;
  color:var(--muted);
}
.svc-meta-dot{
  display:inline-block;width:3px;height:3px;border-radius:50%;
  background:var(--brown);vertical-align:middle;margin:0 4px 3px;
}
.svc-link{
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--sans);font-size:14px;font-weight:700;
  color:var(--navy);text-decoration:none;
  border-bottom:1.5px solid var(--brown);padding-bottom:2px;
  align-self:flex-start;
  transition:color var(--dur-base,200ms) var(--ease);
}
.svc-link:hover{color:var(--brown-dark);}
.svc-link .arr{transition:transform var(--dur-base,200ms) var(--ease);}
.svc-card:hover .svc-link .arr{transform:translateX(4px);}

/* New badge */
/* The tinted pill, as the intro's (.sl-new): Jesse's "match how you do it across the site", 2026-09-29. Padding 1px
   less for the border, so it keeps its size. */
.svc-new{
  display:inline-block;font-family:var(--sans);font-size:10px;font-weight:700;
  letter-spacing:0.14em;text-transform:uppercase;
  color:var(--brown-dark);background:rgba(167,120,69,.14);
  border:1px solid rgba(167,120,69,.4);
  padding:3px 9px;border-radius:999px;
  margin-left:10px;vertical-align:middle;
  position:relative;top:-2px;
}

/* ════════════════════
   Founder quote band
════════════════════ */
.founder{
  background:var(--navy);
  padding:128px 48px;
  color:#fff;
  position:relative;overflow:hidden;
}
@media(max-width:768px){.founder{padding:80px 24px;}}
.founder-inner{max-width:960px;margin:0 auto;}
.founder-eyebrow{
  font-family:var(--sans);font-size:12px;font-weight:700;
  letter-spacing:0.18em;text-transform:uppercase;
  color:var(--brown-light);margin-bottom:32px;
  display:inline-flex;align-items:center;gap:12px;
}
.founder-eyebrow::before{
  content:"";width:32px;height:2px;background:var(--brown);
}
/* ── THE MANIFESTO: Guillaume's line, the homepage's last word before the closing form (template-parts/manifesto.php) ──
   Jesse, 2026-09-29: "the eyebrow becomes 'THE MANIFESTO' … a cool ending before the form fill at the bottom". It closed
   the Our story band until the values took that place, so these are that band's quote and signature rules, moved and
   turned for cream: one sentence to a line on the section scale, the second in brown, the homepage's accent pattern. */
.manifesto{ background:var(--cream); padding:120px 48px; }
@media(max-width:768px){ .manifesto{ padding:80px 24px; } }
.manifesto-inner{ max-width:var(--max); margin:0 auto; }
.manifesto .eyebrow{ margin-bottom:24px; }
.manifesto-quote{
  font-family:var(--heading); font-weight:900; font-size:clamp(30px,3.8vw,48px);
  line-height:1.12; letter-spacing:-0.01em; color:var(--navy); margin:0 0 28px;
}
.manifesto-quote > span{ display:block; text-wrap:balance; }
.manifesto-accent{ color:var(--brown); }
.manifesto-attr{ display:flex; align-items:center; gap:14px; font-family:var(--sans); font-size:15px; color:var(--text-2); }
.manifesto-attr::before{ content:""; width:28px; height:1.5px; background:var(--brown); flex:0 0 28px; }
/* The name upright; on a phone a two-line signature, name over title, not a line that breaks wherever it runs out. */
.manifesto-name{ color:var(--navy); font-weight:600; }
@media(max-width:600px){
  .manifesto-attr{ align-items:flex-start; line-height:1.5; }
  .manifesto-attr::before{ margin-top:.75em; }
  .manifesto-role{ display:block; }
  .manifesto-sep{ display:none; }
}

/* The founder band as the homepage's way into the story (Jesse: "a huge trust builder"), then and now (2026-09-29): the
   story straight under "Our story", across the band; 2015 beside today under it, the arrow between; "Read our story"
   under 2015's caption; and what it was built on, the four values, to close. Each photograph's track is its width at
   a height of 1 (the flyer .4758, the throwback cropped square 1, the team 2.4169), so the three stand exactly the same
   height at every width and the two captions line up under them. */
.founder--story .founder-inner{ max-width:var(--max); }
.founder-story{ display:grid; grid-template-columns:minmax(0,1fr); grid-template-areas:"eyebrow" "story" "tn" "values" "cta"; }   /* cta under the values since 2026-10-07 */
.founder-story > .founder-eyebrow{ grid-area:eyebrow; justify-self:start; }
/* "Our story" over the story and "In short" over the quote (Jesse: "it'll be cool and serve as a TLDR"), one style. */
.founder--story .founder-eyebrow{ margin-bottom:24px; }
.founder-values > .founder-eyebrow{ display:flex; }   /* block-level, as the grid makes "Our story", so no line box adds space */
/* The story as the band's lede: bigger and brighter than body copy (Jesse wanted it at the quote's size), but not in the
   quote's display type. At 48px it ran 5 lines on a desktop and 10 on a phone, pushed the photographs 200px down, and
   left two headlines in one band, so Guillaume's line no longer closed anything. Compared side by side, 2026-09-29.
   Across the whole band, not held to a measure: two lines on a desktop (Jesse: "go over across the page more and not
   wrap - we have real estate"). Pretty wrapping, so a last line is never one word on its own. */
.founder-story-p{ grid-area:story; margin:0; font-size:clamp(20px,1.9vw,26px); font-weight:500; line-height:1.5; color:#fff; text-wrap:pretty; }
/* Under 2015's caption, closing the photographs off from the quote (Jesse: "place the read our story button below the
   2015 Dave and Dan writing - separating that area from the Guillaume quote"). */
.founder-cta{ grid-area:cta; justify-self:start; margin-top:40px; }
/* "What we're built on": the four values About Us prints, with its line for each, closing the band (Jesse, 2026-09-29:
   "shift this to 'what we're built on' as the eyebrow where Guillaume's quote is? with our new values?"). Each a link to
   its section on About Us. Cards like the navy tile's (.tp-band--navy .tp-bento-card): four across, two on a tablet,
   one on a phone. */
.founder-values{ grid-area:values; margin-top:72px; }
.founder-values-list{ list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; }
.founder-value{
  display:flex; flex-direction:column; gap:8px; height:100%; padding:24px 22px; border-radius:16px;
  background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.14); color:#fff; text-decoration:none;
  transition:background .2s var(--ease), border-color .2s var(--ease);
}
.founder-value:hover{ background:rgba(255,255,255,.1); border-color:rgba(187,140,87,.6); }
.founder-value:focus-visible{ outline:2px solid var(--brown-light); outline-offset:3px; }
.founder-value-n{ font-family:var(--sans); font-size:12px; font-weight:700; letter-spacing:.18em; color:var(--brown-light); }
.founder-value-name{ font-family:var(--heading); font-weight:700; font-size:clamp(18px,1.5vw,21px); line-height:1.25; color:#fff; margin-top:10px; letter-spacing:var(--track-heading, normal); }
.founder-value-arr{ display:inline-block; color:var(--brown-light); transition:transform .2s var(--ease); }
.founder-value:hover .founder-value-arr{ transform:translateX(3px); }
.founder-value-line{ font-family:var(--sans); font-size:15px; line-height:1.55; color:rgba(255,255,255,.78); }
@media(max-width:1100px){ .founder-values-list{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media(max-width:600px){ .founder-values-list{ grid-template-columns:minmax(0,1fr); gap:12px; } }
.founder-tn{
  grid-area:tn; margin-top:56px; display:grid; grid-template-columns:minmax(0,.4758fr) minmax(0,1fr) 40px minmax(0,2.4169fr);
  grid-template-areas:"flyer then arrow now" "tcap tcap . ncap"; column-gap:12px; row-gap:16px;
}
.founder-tn img{ display:block; width:100%; height:auto; border-radius:14px; object-fit:cover; }
.founder-tn-flyer{ grid-area:flyer; aspect-ratio:640 / 1345; }
.founder-tn-then{ grid-area:then; aspect-ratio:1; }
.founder-tn-arrow{ grid-area:arrow; align-self:center; width:40px; height:40px; color:var(--brown-light); }
.founder-tn-now{ grid-area:now; position:relative; }
.founder-cap--then{ grid-area:tcap; }
.founder-cap--now{ grid-area:ncap; }
.founder-cap{ margin:0; display:flex; align-items:center; flex-wrap:wrap; gap:6px 12px; font-family:var(--sans); font-size:15px; line-height:1.4; color:rgba(255,255,255,.72); }
/* The year in the site's quiet pill, the form's "See your range". */
.founder-when{ display:inline-flex; align-items:center; border:1px solid rgba(187,140,87,.55); border-radius:999px; padding:4px 12px; font-size:13px; font-weight:700; letter-spacing:.04em; color:var(--brown-light); }
/* "That's us!": About Us's note on the same photograph, sized off the photograph so it lands on the same patch of wall
   above the heads at every width. Its own class and hook (data-nc-note, armed in motion.js), so About's .ab-note stays
   as it is. Without the script, or with reduced motion, it is simply there. */
.founder-note{
  position:absolute; left:4%; top:4%; width:28%; height:auto; color:#FAF7F2;
  transform:rotate(-5deg); filter:drop-shadow(0 2px 3px rgba(0,0,0,.35)); pointer-events:none;
}
.founder-note path{ stroke:currentColor; }
/* Hidden with a gap longer than the stroke, pushed just past its start. A dash and gap of 1 each, offset 1, leaves a
   zero-length dash touching each stroke's end, and a round cap draws that as a dot: eight dots on the wall before the
   pen reached them (Jesse, 2026-09-29: "there's a dot there where each letter starts or ends … kinda ruins the illusion
   of hand written"). With a gap of 2 and an offset of 1.01, nothing touches the stroke until it is drawn. */
.founder-note.is-armed path{ stroke-dasharray:1 2; stroke-dashoffset:1.01; }
.founder-note.is-armed.is-drawn path{ stroke-dashoffset:0; transition:stroke-dashoffset var(--d,.3s) ease-in-out var(--t,0s); }
@media(prefers-reduced-motion:reduce){ .founder-note.is-armed path{ stroke-dashoffset:0; transition:none; } }
/* Stacked: the story, 2015 over today, the way in under them, then Guillaume's line. No arrow; the note points down at
   the team instead. */
@media(max-width:900px){
  .founder-tn{ margin-top:40px; }
  .founder-values{ margin-top:48px; }
  .founder-tn{ grid-template-columns:minmax(0,.4758fr) minmax(0,1fr); grid-template-areas:"flyer then" "tcap tcap" "now now" "ncap ncap"; column-gap:10px; row-gap:14px; }
  .founder-tn-arrow{ display:none; }
}
/* A tablet: full width, 2015's pair would stand taller than today's team. So the pair takes the photograph's own share
   (the third track, .9411, makes the three add up to the team's 2.4169, so the pair stands as tall as the team) and its
   caption sits beside it. */
@media(min-width:601px) and (max-width:900px){
  .founder-tn{ grid-template-columns:minmax(0,.4758fr) minmax(0,1fr) minmax(0,.9411fr); grid-template-areas:"flyer then tcap" "now now now" "ncap ncap ncap"; }
  .founder-cap--then{ align-self:end; flex-direction:column; align-items:flex-start; gap:10px; padding-left:10px; }
  .founder-tn-now{ margin-top:14px; }
}
/* On a phone the wall is a sliver, so the note sits above the photograph instead, as it does on About Us. */
@media(max-width:600px){
  .founder-tn-now{ margin-top:46px; }
  .founder-note{ left:auto; right:6px; top:-42px; width:200px; filter:none; transform:rotate(-4deg); }
}

/* subtle compass watermark (optional) */
.founder-mark{
  position:absolute;
  right:-60px;bottom:-60px;
  width:360px;height:360px;
  opacity:0.06;
  pointer-events:none;
}

/* ════════════════════
   Recent work grid (2-up with meta)
════════════════════ */
.work-head{
  display:flex;justify-content:space-between;align-items:flex-end;
  gap:32px;margin-bottom:48px;flex-wrap:wrap;
}
.work-head .v2-header{margin-bottom:0;}
/* The heading takes the row up to "All projects", not the 640px cap: one line wherever it fits, two on a laptop, never
   three (Jesse, 2026-09-29, once Mont had pushed it to "Renovations / we've completed / across Ottawa.": "why three
   lines for the header here? this could be one or two lines"). The 480px basis is what makes the link drop below it on a
   phone, as it did before, instead of squeezing the heading beside it. */
.work-head .v2-header{ max-width:none; flex:1 1 480px; }
.work-viewall{
  font-family:var(--sans);font-size:14px;font-weight:700;
  color:var(--navy);text-decoration:none;
  display:inline-flex;align-items:center;gap:6px;
  border-bottom:1.5px solid var(--brown);padding-bottom:2px;
  transition:color var(--dur-base,200ms) var(--ease);
}
.work-viewall:hover{color:var(--brown-dark);}
.work-viewall .arr{transition:transform var(--dur-base,200ms) var(--ease);}
.work-viewall:hover .arr{transform:translateX(4px);}

.work-grid{
  display:grid;grid-template-columns:1fr 1fr;
  gap:32px;
}
@media(max-width:800px){.work-grid{grid-template-columns:1fr;}}
/* On a service page (the 'work' section type, template-parts/topic-section.php): three projects, one row from 801px.
   The homepage's two columns hold its four; three in two columns left one alone on a second row (Custom Kitchen
   Cabinets, 2026-10-01). */
@media(min-width:801px){.tp-fill .work-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;}}
.work-card{display:flex;flex-direction:column;cursor:pointer;text-decoration:none;color:inherit;}
.work-card .v2-photo{
  aspect-ratio:4/3;
  border-radius:10px;
  margin-bottom:20px;
  transition:transform var(--dur-base,200ms) var(--ease);
}
.work-card:hover .v2-photo{transform:translateY(-4px);}
/* Real project photographs since 2026-09-29 (template-parts/recent-work.php): the picture fills the 4:3 frame, and the
   placeholder's border goes. */
.work-pic{ border:0; }
.work-pic img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.work-meta{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:0.14em;text-transform:uppercase;
  color:var(--muted);margin-bottom:8px;
}
.work-meta .dot{
  display:inline-block;width:3px;height:3px;border-radius:50%;
  background:var(--brown);vertical-align:middle;margin:0 8px 3px;
}
.work-meta .svc-type{color:var(--brown);}
.work-title{
  font-family:var(--sans);font-size:17px;font-weight:700;
  color:var(--navy);line-height:1.4;
  margin:0;text-wrap:pretty;
}

/* ════════════════════
   Testimonial (asymmetric image + pullquote)
════════════════════ */
.testi-grid{
  display:grid;grid-template-columns:5fr 7fr;
  gap:80px;align-items:center;
}
@media(max-width:900px){.testi-grid{grid-template-columns:1fr;gap:40px;}}
.testi-photo{aspect-ratio:4/5;}
.testi-body{max-width:640px;}
.testi-quote{
  font-family:var(--heading);font-weight:900;
  font-size:clamp(24px,2.6vw,34px);
  line-height:1.28;letter-spacing:-0.005em;
  color:var(--navy);margin:24px 0 24px;
  text-wrap:pretty;
}
.testi-attr{
  display:flex;align-items:center;gap:14px;
  font-family:var(--sans);font-size:14px;
  color:var(--muted);
}
.testi-attr::before{
  content:"";width:28px;height:1.5px;background:var(--brown);flex:0 0 28px;
}
.testi-attr em{color:var(--text);font-style:italic;font-weight:500;}

/* ════════════════════
   FAQ (2-col, accordion)
════════════════════ */
.faq-grid{
  display:grid;grid-template-columns:5fr 7fr;
  gap:80px;align-items:start;
}
@media(max-width:900px){.faq-grid{grid-template-columns:1fr;gap:40px;}}
.faq-list{display:flex;flex-direction:column;}
.faq-item{
  border-top:1px solid var(--border-warm);
  padding:24px 0;
}
.faq-item:last-child{border-bottom:1px solid var(--border-warm);}
.faq-q{
  all:unset;cursor:pointer;
  display:flex;justify-content:space-between;align-items:center;gap:24px;
  font-family:var(--sans);font-size:17px;font-weight:700;
  color:var(--navy);line-height:1.4;width:100%;
  /* A 48px tap target, not the text's 24px (the mobile review, 2026-10-07): padding out, the same margin back, so the
     row's look is unchanged while the button covers most of it. */
  padding:12px 0;margin:-12px 0;
}
.faq-q .faq-icon{
  flex:0 0 20px;width:20px;height:20px;position:relative;
  transition:transform var(--dur-base,200ms) var(--ease);
}
.faq-q .faq-icon::before,
.faq-q .faq-icon::after{
  content:"";position:absolute;background:var(--brown);
  left:50%;top:50%;transform:translate(-50%,-50%);
}
.faq-q .faq-icon::before{width:14px;height:2px;}
.faq-q .faq-icon::after{width:2px;height:14px;transition:transform var(--dur-base,200ms) var(--ease);}
.faq-item.open .faq-q .faq-icon::after{transform:translate(-50%,-50%) scaleY(0);}
.faq-a{
  max-height:0;overflow:hidden;
  transition:max-height var(--dur-slow,320ms) var(--ease);
}
.faq-a-inner{
  padding:16px 0 0;
  font-size:15px;line-height:1.65;color:var(--text-2);
  max-width:560px;
}
.faq-item.open .faq-a{max-height:320px;}

/* ════════════════════
   Navy contact band (V2)
════════════════════ */
.v2-contact{
  background:var(--navy);
  padding:96px 48px;
  color:#fff;
}
@media(max-width:768px){.v2-contact{padding:72px 24px;}}
.v2-contact-grid{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:5fr 7fr;
  gap:80px;align-items:start;
}
@media(max-width:900px){.v2-contact-grid{grid-template-columns:1fr;gap:48px;}}
.v2-contact-heading{
  font-family:var(--heading);font-weight:900;
  font-size:clamp(30px,3.8vw,48px);   /* the section size, not 56px: 2026-09-29 */
  line-height:1.02;letter-spacing:var(--track-heading, -0.01em);
  color:#fff;margin:18px 0 24px;
}
/* Upright, in the heading's own font, like every other heading accent on the homepage (Jesse, 2026-09-29: "do them
   all now"). Was italic DM Sans 500. */
.v2-contact-heading em{
  color:var(--brown-light);font-style:normal;
}
.v2-contact-eyebrow{
  font-family:var(--sans);font-size:12px;font-weight:700;
  letter-spacing:0.18em;text-transform:uppercase;
  color:var(--brown-light);
  display:inline-flex;align-items:center;gap:12px;
}
.v2-contact-eyebrow::before{
  content:"";width:32px;height:2px;background:var(--brown);
}
.v2-contact-copy{
  font-size:16px;line-height:1.65;color:rgba(255,255,255,0.72);
  margin:0 0 32px;max-width:420px;
}
.v2-contact-meta{
  font-size:14px;color:rgba(255,255,255,0.55);line-height:1.8;
}
.v2-contact-meta strong{color:#fff;font-weight:600;}
.v2-contact-meta a{color:rgba(255,255,255,0.85);text-decoration:none;border-bottom:1px solid rgba(167,120,69,0.5);padding-bottom:1px;}
.v2-contact-meta a:hover{color:var(--brown-light);}

/* Form */
.v2-form{
  background:rgba(255,255,255,0.04);
  border:1px solid rgba(255,255,255,0.1);
  border-radius:16px;
  padding:32px;
}
.v2-form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;}
@media(max-width:600px){.v2-form-row{grid-template-columns:1fr;}}
.v2-field{display:flex;flex-direction:column;gap:6px;}
.v2-field label{
  font-family:var(--sans);font-size:12px;font-weight:600;
  color:rgba(255,255,255,0.72);letter-spacing:0.04em;
}
.v2-field input,
.v2-field select,
.v2-field textarea{
  width:100%;
  background:rgba(255,255,255,0.05);
  border:1px solid rgba(255,255,255,0.14);
  border-radius:6px;
  color:#fff;padding:12px 14px;
  font-family:var(--sans);font-size:14px;line-height:1.5;
  transition:border-color var(--dur-base,200ms) var(--ease),background var(--dur-base,200ms) var(--ease);
  outline:none;
}
.v2-field textarea{min-height:100px;resize:vertical;}
.v2-field input::placeholder,
.v2-field textarea::placeholder{color:rgba(255,255,255,0.35);}
.v2-field input:focus,
.v2-field select:focus,
.v2-field textarea:focus{
  border-color:var(--brown-light);
  background:rgba(255,255,255,0.08);
  box-shadow:0 0 0 3px rgba(167,120,69,0.28);
}
.v2-field select{appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='none' stroke='%23BB8C57' stroke-width='1.75' d='M1 1l5 5 5-5'/></svg>");background-repeat:no-repeat;background-position:right 16px center;padding-right:40px;}
.v2-submit{
  display:inline-flex;align-items:center;gap:10px;
  background:var(--brown);color:#fff;
  border:none;border-radius:8px;
  padding:14px 28px;
  font-family:var(--sans);font-size:14px;font-weight:700;
  letter-spacing:0.02em;
  cursor:pointer;
  transition:background var(--dur-base,200ms) var(--ease);
  margin-top:8px;
}
.v2-submit:hover{background:var(--brown-light);}
.v2-submit .arr{transition:transform var(--dur-base,200ms) var(--ease);}
.v2-submit:hover .arr{transform:translateX(3px);}

/* Tweaks panel CSS removed alongside the edit-mode IIFE it styled — that block
   injected a floating panel, wrote --cream/--brown/--brown-dark inline onto
   :root (shadowing the tokens declared above), reached for the homepage .hero
   via a bare querySelector, and postMessage'd to window.parent with a wildcard
   origin. Prototype scaffolding from the tool that generated the widget; it had
   no business running on a production page. */


/* ── HERO — clean white gradient ── */

/* ══ ESTIMATOR — SCOPED UNDER .est-root ══ */
.est-root { display:block; width:100%; height:auto !important; min-height:0 !important; }
.est-root .widget {
  box-shadow:none;
  border-radius:20px;
  height:auto !important;
  min-height:0 !important;
  overflow:visible !important;
}






/* ── WIDGET SHELL ── */
.est-root .widget {
  width:100%;max-width:860px;
  background:var(--white);
  border-radius:22px;
  box-shadow:var(--shadow-lg);
  overflow:hidden;
}


/* The widget's internal hero lived here (~45 lines: .est-hero, .hero-left,
   .hero-eyebrow, .hero-title, .hero-sub, .hero-trust, .hero-cta-*) along with a
   trust-pill strip that briefly replaced it. All removed — the hero restated the
   section header above it almost verbatim, carried a second competing CTA, and
   cost 407px on a phone; the pills then restated "no commitment, no pressure"
   from the same header. The widget now opens on the progress bar. */


/* ── PROGRESS ── */
.est-root .progress-bar {height:3px;background:var(--border);}

.est-root .progress-fill {height:100%;background:linear-gradient(to right,var(--brown),var(--brown-light));transition:width 0.4s var(--ease);}


/* ── STEP NAV DOTS ── */
.est-root .step-nav {display:flex;align-items:center;padding:0 52px;border-bottom:1px solid var(--border);background:var(--white);overflow-x:auto;gap:0;}

@media(max-width:640px){.est-root .step-nav {padding:0 28px;}
}
.est-root .step-dot {display:flex;align-items:center;gap:7px;padding:14px 0;cursor:default;opacity:0.35;transition:opacity 0.3s;white-space:nowrap;flex-shrink:0;}

.est-root .step-dot:not(:last-child) {margin-right:24px;}

.est-root .step-dot.active {opacity:1;}

.est-root .step-dot.done {opacity:0.7;}

.est-root .dot-num {width:22px;height:22px;border-radius:50%;background:var(--border);display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-size:10px;font-weight:700;color:var(--muted);transition:all 0.3s;flex-shrink:0;}

.est-root .step-dot.active .dot-num {background:var(--navy);color:white;}

.est-root .step-dot.done .dot-num {background:var(--brown);color:white;}

.est-root .dot-lbl {font-family:var(--heading);font-size:10px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--muted);}

.est-root .step-dot.active .dot-lbl {color:var(--navy);}


/* ── STEP BODY ── */
.est-root .w-body {padding:40px 52px 44px;}

@media(max-width:640px){.est-root .w-body {padding:28px 28px 36px;}
}
.est-root .step-panel {display:none;}

.est-root .step-panel.active {display:block;}


/* ── STEP TYPE/HEADINGS ── */
.est-root .step-heading {font-family:var(--heading);font-size:22px;font-weight:800;color:var(--navy);letter-spacing:-0.02em;margin-bottom:7px;}

.est-root .step-sub {font-size:14px;color:var(--muted);line-height:1.6;margin-bottom:28px;}


/* ── CATEGORY GRID (Step 1 — 3 big tiles) ── */
.est-root .cat-grid {display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:28px;}

@media(max-width:640px){.est-root .cat-grid {grid-template-columns:1fr;}
}
.est-root .cat-card {
  border:2px solid var(--border);border-radius:16px;
  padding:28px 22px;cursor:pointer;
  transition:all 0.22s var(--ease);
  position:relative;overflow:hidden;
  display:flex;flex-direction:column;gap:10px;
  background:var(--white);
}

.est-root .cat-card:hover {border-color:var(--brown-border);box-shadow:0 4px 20px rgba(22,16,68,0.07);}

.est-root .cat-card.selected {border-color:var(--navy);background:linear-gradient(135deg,#f8f6ff 0%,#f0edfa 100%);}

.est-root .cat-card.selected::after {content:'✓';position:absolute;top:14px;right:14px;width:22px;height:22px;border-radius:50%;background:var(--navy);color:white;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;font-family:var(--heading);}

.est-root .cat-icon {font-size:32px;}

/* Sits inline after the card name rather than on its own row above it — that row
   cost ~26px per card and pushed My Exterior further down the page. The ✦ went
   too: fussy at 8px and it widened the badge for no information. */
/* Tinted, like every other NEW on a light ground (.sl-new, .svc-new): Jesse, 2026-09-29, "make the 'NEW' pill the same
   slightly transparent vibe as you did at the top / across the site". It was solid brown with white text. A pixel
   off the padding for the border, so the pill keeps its size. */
.est-root .cat-new {
  display:inline-block;
  background:rgba(167,120,69,.14);color:var(--brown-dark);
  border:1px solid rgba(167,120,69,.4);
  font-family:var(--heading);font-size:8px;font-weight:700;
  letter-spacing:0.12em;text-transform:uppercase;
  padding:2px 6px;border-radius:20px;
  vertical-align:2px;      /* optically centres against the 17px cap height */
  margin-left:6px;
  white-space:nowrap;
}

.est-root .cat-name {font-family:var(--heading);font-size:17px;font-weight:800;color:var(--navy);letter-spacing:-0.02em;line-height:1.2;}

/* ── Category card photos ──────────────────────────────────
   Real photos rather than emoji: renovation is a visual purchase and the card is
   selling a finished room. Replaces the emoji discs, which were always a
   placeholder.

   DESKTOP: photo on top. Cards sit in a 3-up grid at ~270px each, so a side-by-
   side split would leave too little room for the copy.

   MOBILE: photo BESIDE the copy (see the media query further down). A photo above
   the text costs 100–300px of height and would push My Exterior back below the
   fold; beside it, the photo fits inside the height the name + sub + pills already
   occupy, so it costs nothing.

   Source files are 240×240 centre crops (12–20KB) derived from the existing
   hero-*.jpg, which were 1024×1536 at 108–145KB — 10–15× oversized for this.
─────────────────────────────────────────────────────────── */
.est-root .cat-photo{
  display:block;width:100%;height:120px;
  object-fit:cover;border-radius:10px;
  background:var(--grey);
}

/* Emoji disc — the MOBILE stand-in for the photo above. Hidden on desktop. */
.est-root .cat-icon{
  display:none;
  align-items:center;justify-content:center;
  width:28px;height:28px;border-radius:50%;
  background:var(--brown-pale);
  font-size:14px;line-height:1;
  margin-right:9px;vertical-align:-8px;
  flex-shrink:0;
}

.est-root .cat-sub {font-size:13px;color:var(--muted);line-height:1.55;}

.est-root .cat-pills {display:flex;flex-wrap:wrap;gap:5px;margin-top:2px;}

.est-root .cat-pill {font-family:var(--heading);font-size:8px;font-weight:600;letter-spacing:0.07em;text-transform:uppercase;color:var(--muted);border:1px solid var(--border);padding:3px 9px;border-radius:20px;}

/* ── Category cards · mobile density ──────────────────────
   Stacked one-per-row on a phone, so every px of card chrome pushes My Exterior
   further below the fold. Tightening padding, internal gap and grid gap recovers
   roughly 70px across the three cards without touching type sizes.

   AFTER the base rules above — a media query adds no specificity.
─────────────────────────────────────────────────────────── */
@media(max-width:768px){
  .est-root .cat-grid{gap:10px;margin-bottom:20px;}
  .est-root .cat-sub{font-size:12.5px;line-height:1.5;}
  .est-root .cat-pills{gap:4px;}

  /* ── Photos off, emoji on ────────────────────────────────
     Photos were tried here and measured badly. At a 331px card there is no
     layout where an image is free:

       emoji, no photo   147px cards  → all three visible
       photo BESIDE copy 187px cards  → exterior partly cut  (96px photo leaves
                                        174px for copy, so the pills wrap to two
                                        rows and the text column outgrows the
                                        photo it was meant to hide behind)
       photo ABOVE copy  248px cards  → exterior off screen entirely

     So the emoji disc carries mobile and the photo is desktop-only, where cards
     sit 3-up with room to spare. Same markup, one shown per breakpoint. */
  .est-root .cat-photo{display:none;}
  .est-root .cat-icon{display:inline-flex;}
  .est-root .cat-card{padding:18px 16px;gap:7px;border-radius:14px;}
}


/* ── SERVICE GRID (Step 2 — within category) ── */
.est-root .service-grid {display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:28px;}

@media(max-width:580px){.est-root .service-grid {grid-template-columns:1fr;}
}
.est-root .service-card {
  border:2px solid var(--border);border-radius:var(--r);
  padding:22px;cursor:pointer;
  transition:all 0.22s var(--ease);
  position:relative;
  background:var(--white);
}

.est-root .service-card:hover {border-color:var(--brown-border);box-shadow:var(--shadow-md);}

.est-root .service-card.selected {border-color:var(--navy);background:linear-gradient(135deg,#f8f6ff 0%,#f0edfa 100%);}

.est-root .sc-tick {position:absolute;top:14px;right:14px;width:20px;height:20px;border-radius:50%;background:var(--border);font-size:9px;font-weight:700;color:transparent;display:flex;align-items:center;justify-content:center;transition:all 0.2s;font-family:var(--heading);}

.est-root .service-card.selected .sc-tick {background:var(--navy);color:white;}

.est-root .sc-eyebrow {font-family:var(--heading);font-size:9px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:var(--brown);margin-bottom:6px;display:flex;align-items:center;gap:6px;}

.est-root .sc-popular {background:var(--brown-pale);color:var(--brown);font-size:8px;padding:2px 7px;border-radius:20px;letter-spacing:0.08em;}

.est-root .sc-name {font-family:var(--heading);font-size:16px;font-weight:800;color:var(--navy);line-height:1.2;letter-spacing:-0.015em;margin-bottom:8px;}

.est-root .sc-tagline {font-size:13px;color:var(--muted);line-height:1.55;margin-bottom:10px;}

.est-root .sc-divider {height:1px;background:var(--border);margin-bottom:10px;}

.est-root .sc-pills {display:flex;flex-wrap:wrap;gap:5px;}

.est-root .sc-pill {font-family:var(--heading);font-size:8px;font-weight:600;letter-spacing:0.07em;text-transform:uppercase;color:var(--muted);border:1px solid var(--border);padding:3px 8px;border-radius:20px;}


/* ── MULTI-SELECT GRID (checkboxes for exterior) ── */
.est-root .multi-grid {display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:28px;}

.est-root .multi-card {
  border:2px solid var(--border);border-radius:var(--r);
  padding:18px 20px;cursor:pointer;
  transition:all 0.22s var(--ease);
  display:flex;align-items:center;gap:16px;
  background:var(--white);
}

.est-root .multi-card:hover {border-color:var(--brown-border);}

.est-root .multi-card.selected {border-color:var(--navy);background:linear-gradient(135deg,#f8f6ff 0%,#f0edfa 100%);}

.est-root .mc-check {
  width:22px;height:22px;border-radius:6px;
  border:2px solid var(--border);
  display:flex;align-items:center;justify-content:center;
  font-size:12px;color:transparent;flex-shrink:0;
  transition:all 0.2s;font-family:var(--heading);font-weight:700;
}

.est-root .multi-card.selected .mc-check {background:var(--navy);border-color:var(--navy);color:white;}

.est-root .mc-body {flex:1;}

.est-root .mc-name {font-family:var(--heading);font-size:14px;font-weight:700;color:var(--navy);margin-bottom:3px;letter-spacing:-0.01em;}

.est-root .mc-desc {font-size:13px;color:var(--muted);line-height:1.5;}


/* ── MATERIAL / OPTION GRID ── */
.est-root .option-grid {display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:28px;}

@media(max-width:580px){.est-root .option-grid {grid-template-columns:1fr;}
}
.est-root .option-card {
  border:2px solid var(--border);border-radius:var(--r);
  padding:18px 16px;cursor:pointer;
  transition:all 0.22s var(--ease);
  position:relative;background:var(--white);
}

.est-root .option-card:hover {border-color:var(--brown-border);}

.est-root .option-card.selected {border-color:var(--navy);background:linear-gradient(135deg,#f8f6ff 0%,#f0edfa 100%);}

.est-root .option-card.selected::after {content:'✓';position:absolute;top:10px;right:10px;width:18px;height:18px;border-radius:50%;background:var(--navy);color:white;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;font-family:var(--heading);}

.est-root .oc-icon {font-size:22px;margin-bottom:8px;}

.est-root .oc-tier {font-family:var(--heading);font-size:8px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;margin-bottom:4px;}

.est-root .oc-tier.budget {color:#22a06b;}

.est-root .oc-tier.mid {color:var(--brown);}

.est-root .oc-tier.premium {color:var(--navy);}

.est-root .oc-name {font-family:var(--heading);font-size:14px;font-weight:700;color:var(--navy);margin-bottom:4px;letter-spacing:-0.01em;}

.est-root .oc-brand {font-size:11px;color:var(--light);margin-bottom:6px;font-style:italic;}

.est-root .oc-desc {font-size:12px;color:var(--muted);line-height:1.5;}


/* ── YES/NO BINARY CARDS ── */
.est-root .binary-grid {display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:28px;}

.est-root .binary-card {
  border:2px solid var(--border);border-radius:var(--r);
  padding:20px;cursor:pointer;text-align:center;
  transition:all 0.22s var(--ease);background:var(--white);
  display:flex;flex-direction:column;align-items:center;gap:8px;
}

.est-root .binary-card:hover {border-color:var(--brown-border);}

.est-root .binary-card.selected {border-color:var(--navy);background:linear-gradient(135deg,#f8f6ff 0%,#f0edfa 100%);}

.est-root .bc-icon {font-size:28px;}

.est-root .bc-label {font-family:var(--heading);font-size:15px;font-weight:700;color:var(--navy);}

.est-root .bc-sub {font-size:12px;color:var(--muted);line-height:1.5;}


/* ── UPGRADE CARDS ── */
/* ── Upgrade card photos ───────────────────────────────────
   Half these labels are trade jargon ("Headers & Crown", "LeMans Corner System",
   "Appliance Garage", "Built-In Niche") — a photo is the explanation, not
   decoration, so unlike the category cards it earns its height here.

   Rendered by northco_upgrade_photo() in functions.php: a real <img> once
   assets/images/upgrades/upg-<key>.jpg exists, otherwise the labelled placeholder
   below. Both use .uc-photo so the layout is identical either way and swapping in
   real photos can't shift anything.
─────────────────────────────────────────────────────────── */
.est-root .uc-photo{
  display:block;width:100%;
  /* height:auto is load-bearing. The <img> carries width="480" height="300" —
     which reserves space and prevents layout shift — but those attributes are
     presentational hints that map to CSS width/height, and an explicit height
     BEATS aspect-ratio. Without this the image rendered 208x300 instead of
     208x130, and since grid rows stretch to the tallest cell, one real photo
     inflated every card in its row from 248px to 427px. */
  height:auto;
  aspect-ratio:16 / 10;
  object-fit:cover;
  border-radius:9px;
  margin-bottom:11px;
  background:var(--grey);
}

/* Obvious-on-purpose placeholder — dashed, tinted, captioned, so an unshot
   upgrade is impossible to miss in review and can't quietly ship. */
.est-root .uc-photo-ph{
  display:flex;align-items:center;justify-content:center;
  padding:8px;
  background:repeating-linear-gradient(
    135deg, var(--brown-pale) 0 8px, #EFE4D2 8px 16px );
  border:1.5px dashed var(--brown-border, rgba(167,120,69,.45));
}
.est-root .uc-photo-ph span{
  font-family:var(--heading);font-size:9px;font-weight:700;
  letter-spacing:0.08em;text-transform:uppercase;
  color:var(--brown-dark);text-align:center;line-height:1.35;
}

.est-root .upgrade-grid {display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:28px;}

@media(max-width:640px){.est-root .upgrade-grid {grid-template-columns:1fr 1fr;}
}
@media(max-width:380px){.est-root .upgrade-grid {grid-template-columns:1fr;}
}
.est-root .upgrade-card {
  border:2px solid var(--border);border-radius:var(--r);
  padding:16px;cursor:pointer;
  transition:all 0.22s var(--ease);
  position:relative;background:var(--white);
  display:flex;flex-direction:column;gap:5px;
}

.est-root .upgrade-card:hover {border-color:var(--brown-border);}

.est-root .upgrade-card.selected {border-color:var(--brown);background:var(--brown-pale);}

.est-root .uc-badge {position:absolute;top:10px;right:10px;width:18px;height:18px;border-radius:50%;background:var(--border);display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:transparent;transition:all 0.2s;font-family:var(--heading);}

.est-root .upgrade-card.selected .uc-badge {background:var(--brown);color:white;}

.est-root .uc-icon {font-size:20px;}

.est-root .uc-name {font-family:var(--heading);font-size:12px;font-weight:700;color:var(--navy);line-height:1.3;letter-spacing:-0.01em;}

.est-root .uc-desc {font-size:11px;color:var(--muted);line-height:1.5;}

.est-root .uc-add {font-family:var(--heading);font-size:9px;font-weight:700;color:var(--brown);letter-spacing:0.06em;text-transform:uppercase;margin-top:2px;}


/* ── CONTACT FORM ── */
.est-root .contact-form {display:flex;flex-direction:column;gap:12px;margin-bottom:28px;}

.est-root .form-row-2 {display:grid;grid-template-columns:1fr 1fr;gap:12px;}

@media(max-width:500px){.est-root .form-row-2 {grid-template-columns:1fr;}
}
.est-root .form-input, .est-root .form-select {
  width:100%;padding:13px 15px;
  border:1.5px solid var(--border);border-radius:10px;
  font-size:14px;font-family:var(--sans);
  color:var(--text);background:var(--white);
  transition:border-color 0.2s;outline:none;
}

.est-root .form-input:focus-visible, .est-root .form-select:focus-visible {
  border-color:var(--navy);box-shadow:0 0 0 3px rgba(167,120,69,0.28);
}
.est-root .form-input:focus, .est-root .form-select:focus {border-color:var(--navy);}

.est-root .form-input::placeholder {color:var(--light);}

.est-root .form-select {color:var(--muted);cursor:pointer;}

.est-root .form-select option {color:var(--text);}


/* ── UPSELL NUDGE ── */
.est-root .upsell-nudge {
  display:none;
  background:linear-gradient(135deg,var(--brown-pale),#f0edfa);
  border:1.5px solid var(--brown-border);
  border-radius:var(--r);padding:18px 20px;
  margin-bottom:24px;
  gap:14px;align-items:flex-start;
}

.est-root .upsell-nudge.show {display:flex;}

/* .nudge-icon was the styled class but NOTHING in the markup uses it — the nudge
   renders .nudge-indicator and .nudge-wrap, both of which had no rule at all. So
   the arrow was unstyled body text and the content column couldn't grow. */
.est-root .nudge-indicator{
  flex:0 0 26px;
  width:26px;height:26px;border-radius:50%;
  background:var(--brown);color:#fff;
  display:flex;align-items:center;justify-content:center;
  font-size:13px;font-weight:700;line-height:1;
  margin-top:1px;
}
.est-root .nudge-wrap{flex:1 1 auto;min-width:0;}

.est-root .nudge-title {font-family:var(--heading);font-size:13px;font-weight:700;color:var(--navy);margin-bottom:5px;}

.est-root .nudge-body {font-size:13px;color:var(--muted);line-height:1.6;margin-bottom:10px;}

.est-root .nudge-cta {background:none;border:none;font-family:var(--heading);font-size:12px;font-weight:700;color:var(--navy);cursor:pointer;letter-spacing:0.04em;text-decoration:underline;text-underline-offset:3px;padding:0;}

.est-root .nudge-cta:hover {color:var(--brown);}


/* ── SUMMARY / ESTIMATE RESULT ── */
.est-root .estimate-result {
  background:linear-gradient(135deg,var(--navy) 0%,#1e1660 100%);
  border-radius:var(--r);padding:36px;
  color:white;margin-bottom:28px;
  position:relative;overflow:hidden;
}

.est-root .estimate-result::before {content:'';position:absolute;top:-60px;right:-60px;width:200px;height:200px;border-radius:50%;border:1.5px solid rgba(255,255,255,0.06);}

.est-root .er-label {font-family:var(--heading);font-size:9px;font-weight:700;letter-spacing:0.2em;text-transform:uppercase;color:rgba(255,255,255,0.5);margin-bottom:8px;}

.est-root .er-range {font-family:var(--heading);font-size:clamp(28px,5vw,42px);font-weight:800;color:white;letter-spacing:-0.025em;line-height:1;margin-bottom:8px;}

.est-root .er-range span {color:var(--brown-light);}

.est-root .er-note {font-size:13px;color:rgba(255,255,255,0.5);margin-bottom:24px;line-height:1.55;}

.est-root .er-breakdown {display:flex;flex-direction:column;gap:8px;}

.est-root .er-line {display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:rgba(255,255,255,0.06);border-radius:8px;}

.est-root .er-line-label {font-size:13px;color:rgba(255,255,255,0.7);}

.est-root .er-line-val {font-family:var(--heading);font-size:13px;font-weight:700;color:var(--brown-light);}

/* Whether the estimate's details reached us (assets/js/estimator.js, 2026-10-08). It sits under the navy range box on the
   panel's light ground, so it takes the contact card's light status colours (.cu-form-card .nc-quote-status). */
.est-root .er-sent {font-family:var(--sans);font-size:15px;line-height:1.5;border-radius:10px;padding:12px 14px;margin:0 0 24px;border:1px solid var(--border-warm);background:var(--off);color:var(--text-2);}
.est-root .er-sent.is-ok {background:#E8F5E9;border-color:#A5D6A7;color:#1B5E20;}
.est-root .er-sent.is-error {background:#FDECEA;border-color:#F5C2C0;color:#8E1B12;}


/* ── THE KITCHEN BUILD (2026-10-26: #step-2-kitchen-build and #step-kb-result, template-parts/estimator.php) ──
   Its cards are buttons (a keyboard reaches them, a locked answer can be disabled), so they shed the button look. */
.est-root button.binary-card {font:inherit;color:inherit;width:100%;}
.est-root .binary-card:disabled {opacity:0.4;cursor:not-allowed;}
.est-root .binary-card:disabled:hover {border-color:var(--border);}
.est-root .kb-ask {margin-bottom:14px;}
.est-root .kb-q .binary-grid {margin-bottom:10px;}
/* Why an answer was made for them: "✓ Comes with new cabinets". */
.est-root .kb-lock {font-family:var(--sans);font-size:14px;font-weight:600;color:var(--brown-dark);margin:0 0 6px;}

.est-root .kb-edit-row {margin:-8px 0 28px;}
.est-root .kb-next-ask {font-family:var(--heading);font-size:17px;font-weight:700;color:var(--navy);margin-bottom:14px;}
.est-root .kb-next {margin-bottom:20px;}
.est-root .kb-form {margin-bottom:24px;}
/* .contact-form's display:flex outranks the hidden attribute, so the closed form says so itself. */
.est-root .kb-form[hidden] {display:none;}
.est-root .kb-form-h {font-family:var(--heading);font-size:17px;font-weight:700;color:var(--navy);}
.est-root .kb-send {align-self:flex-start;}
.est-root .kb-form .er-sent {margin:0;}
.est-root .kb-consent {display:flex;gap:10px;align-items:flex-start;font-size:15px;line-height:1.55;color:var(--text-2);cursor:pointer;}
.est-root .kb-consent input {width:20px;height:20px;margin:2px 0 0;flex-shrink:0;accent-color:var(--navy);}
.est-root .kb-fine {font-size:13px;color:var(--muted);line-height:1.5;margin:0;}
.est-root .kb-portfolio {display:flex;justify-content:space-between;align-items:center;gap:12px;padding:16px 18px;border:1.5px solid var(--border-warm);border-radius:var(--r);background:var(--cream);font-family:var(--heading);font-size:15px;font-weight:700;color:var(--navy);text-decoration:none;transition:border-color 0.2s;}
.est-root .kb-portfolio:hover {border-color:var(--brown);}
.est-root .kb-portfolio .arr {color:var(--brown);}
@media(max-width:600px){
  .est-root .kb-send {align-self:stretch;justify-content:center;}
}


/* ── NEXT STEPS ── */
.est-root .next-steps {display:flex;flex-direction:column;gap:10px;margin-bottom:28px;}

.est-root .ns-item {display:flex;align-items:flex-start;gap:14px;padding:14px 16px;background:var(--off);border-radius:10px;border:1px solid var(--border);}

.est-root .ns-num {width:24px;height:24px;border-radius:50%;background:var(--navy);color:white;display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-size:10px;font-weight:700;flex-shrink:0;}

.est-root .ns-text {font-size:14px;color:var(--muted);line-height:1.6;padding-top:2px;}


/* ── SECTION DIVIDER ── */
.est-root .section-divider {display:flex;align-items:center;gap:12px;margin:20px 0 24px;}

.est-root .section-divider-line {flex:1;height:1px;background:var(--border);}

.est-root .section-divider-label {font-family:var(--heading);font-size:9px;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:var(--light);}


/* ── BUTTONS ── */
.est-root .btn-row {display:flex;gap:12px;justify-content:space-between;align-items:center;}

/* Mobile: the sticky pill IS the Continue button, so the in-panel one is a
   duplicate sitting a few pixels above it. Back and Skip stay — the pill only
   replaces the forward action.

   Gated on body.est-pill-nav, which nav.js only sets once the pill has actually
   taken over. If that script fails the in-panel button remains, so there's never
   a state with no way forward. Desktop keeps it either way. */
@media(max-width:900px){
  body.est-pill-nav .est-root .btn-next{display:none;}
  /* With the forward button gone, Back sits left and Skip right. */
  body.est-pill-nav .est-root .btn-row{justify-content:space-between;}
  body.est-pill-nav .est-root .btn-row > div:empty{display:none;}
}

.est-root .btn-back {font-family:var(--sans);font-size:14px;color:var(--muted);background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:6px;transition:color 0.2s;padding:4px 0;}

.est-root .btn-back:hover {color:var(--navy);}

.est-root .btn-next {background:var(--navy);color:var(--white);border:none;padding:15px 32px;border-radius:10px;font-family:var(--heading);font-size:14px;font-weight:700;cursor:pointer;transition:all 0.2s var(--ease);display:flex;align-items:center;gap:9px;letter-spacing:0.03em;}

.est-root .btn-next:hover {background:#1e1660;transform:translateY(-1px);box-shadow:0 6px 22px rgba(22,16,68,0.22);}

.est-root .btn-next:disabled {opacity:0.35;cursor:default;transform:none;box-shadow:none;}

.est-root .btn-next.gold {background:var(--brown);}

.est-root .btn-next.gold:hover {background:var(--brown-light);box-shadow:0 6px 22px rgba(167,120,69,0.3);}

.est-root .btn-arrow {transition:transform 0.2s;}

.est-root .btn-next:hover:not(:disabled) .btn-arrow {transform:translateX(3px);}

.est-root .btn-skip {font-family:var(--sans);font-size:13px;color:var(--light);background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:3px;transition:color 0.2s;}

.est-root .btn-skip:hover {color:var(--muted);}


@media(max-width:600px){
  .est-root .step-nav {padding:0 28px;}

  .est-root .w-body {padding:28px 28px 36px;}

  .est-root .estimate-result {padding:24px;}

  .est-root .cat-grid, .est-root .option-grid {grid-template-columns:1fr;}

  .est-root .upgrade-grid {grid-template-columns:1fr 1fr;}

}


/* ══════════════════════════════════════════════
   ESTIMATOR · MOBILE
   The widget had never been reviewed below desktop width. Two
   things were badly wrong at 402px:

   1. .estimator-section kept its desktop 48px side padding while
      every other section drops to 24px — so the widget got only
      303px of a 402px screen, 99px lost to nesting alone.

   2. Six labelled step dots overflowed 2.3× (692px of content in
      a 303px box). overflow-x:auto meant it technically scrolled,
      but nothing signalled that and the active step was never
      scrolled into view — on step 4 you were still looking at
      steps 1–2.

   Placed at the END of the estimator CSS on purpose: a media
   query adds no specificity, so overrides must follow the base
   rules they replace.
════════════════════════════════════════════════ */
@media(max-width:768px){

  /* ── 1 · Reclaim the width ─────────────────────────────── */
  .estimator-section{padding:56px 14px;}
  /* The widget keeps the reclaimed width; its introduction goes back to the page's 24px gutter, where every other
     heading on a phone starts (it sat at 14px, 10px left of its neighbours, on the homepage and the service pages). */
  .estimator-header{margin-bottom:28px;padding:0 10px;}

  /* ── 2 · Numbers-only step nav ─────────────────────────────
     6 × 22px dots + a single active label ≈ 194px, which fits the
     reclaimed width comfortably, so the nav no longer scrolls.
     .active / .done are already applied by updateDots(), so the
     label swap needs no JS. */
  .est-root .step-nav{
    padding:0 14px;
    justify-content:space-between;
    overflow-x:visible;
  }
  .est-root .step-dot,
  .est-root .step-dot:not(:last-child){
    margin-right:0;
    flex-shrink:1;
    min-width:0;
  }
  .est-root .dot-lbl{display:none;}
  .est-root .step-dot.active .dot-lbl{
    display:inline;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
    color:var(--brown-dark);
  }

  /* The progress bar becomes the primary position cue once the
     other five labels are hidden, so give it more presence. */
  .est-root .progress-bar{height:4px;}
}




/* ══════════════════════════════════════════════
   iOS INPUT ZOOM FIX
   ══════════════════════════════════════════════

   iOS Safari automatically zooms the page when you focus an input whose computed
   font-size is under 16px — and the zoom PERSISTS after the field loses focus. In
   the estimator that meant filling in step 5 and arriving at your estimate on
   step 6 zoomed in, with no obvious way back out short of pinching.

   Every field in the theme was 14px, so this affected all three forms: the
   homepage lead form, the navy contact band, and the estimator.

   16px is the exact threshold — at 16px there is no zoom. Mobile only; desktop
   keeps 14px, where it reads better.

   The other "fix" for this is maximum-scale=1 on the viewport meta. Do NOT do
   that — it disables pinch zoom for everyone, which is an accessibility failure.
   Raising the font size is the correct fix.

   Placed at the very END of the file deliberately: a media query adds no
   specificity, so this has to follow every base rule it overrides.
════════════════════════════════════════════════ */
@media(max-width:900px){
  .fi,
  .fi-select,
  .fi-textarea,
  .v2-field input,
  .v2-field select,
  .v2-field textarea,
  .est-root .form-input,
  .est-root .form-select,
  .est-root .form-textarea{
    font-size:16px;
  }
}

/* ── Skip link ───────────────────────────────────────────────────────────────────
   Clipped off-screen, NOT display:none — a display:none element is removed from the tab order,
   which would make the skip link unreachable and therefore pointless. It becomes visible only
   on :focus-visible, so it never appears for mouse users. */
.skip-link{
  position:absolute;left:-9999px;top:0;z-index:2000;
  font-family:var(--sans);font-size:14px;font-weight:600;
  background:var(--navy);color:#fff;
  padding:12px 18px;border-radius:0 0 6px 0;
  text-decoration:none;
}
.skip-link:focus-visible,
.skip-link:focus{
  left:0;
  outline:2px solid var(--brown-light);outline-offset:2px;
}

/* ════════════════════════════════════════════════════════════════════════════════════
   PAGE / POST CONTENT — the_content() long-form prose
   ════════════════════════════════════════════════════════════════════════════════════
   The body copy for the ~90 inner Pages (page.php → .page-content) and the 67 blog/
   material Posts (single.php → .post-content). Both render the SAME the_content() output,
   so they share ONE type system via :is() — style only .page-content and half the URLs
   render bare.

   Built for CLEAN semantic HTML. The live pages carry Elementor's inline font-size/color/
   font-family and wrapper classes; migration strips those, so this stylesheet owns the look
   (you cannot out-specificity an inline style without !important everywhere). Element list
   comes from auditing the rendered DOM of the live mirror — the vocabulary that appears in
   the wild across both content models:

     headings h2–h6 (h4 is used as often as h3, and levels are picked for size not depth)
     p · inline a/strong/b/em/i/sup/sub/mark/code · br
     ul/ol + nested lists
     img (bare, on Pages) AND figure/figcaption (on Posts) — both occur
     table (rare but real; must scroll on mobile, cannot reflow at 375px)
     blockquote · hr  (defensive — not seen in the sample, cheap to cover)

   Mobile-first: verified at 375px. 85% of traffic is mobile.
──────────────────────────────────────────────────────────────────────────────────────── */

/* Reading column for PAGES. A Page's .inner is --max (1440px) — far too wide for 6,900
   words of prose — so the article is capped to a comfortable measure inside it.

   ⚠ .post-article is deliberately NOT in this rule any more. It used to be, from when
   single.php wrapped its article in .section > .inner. That wrapper is gone: single.php
   now owns its own column, left-aligned to the site's content edge, via .post-head-inner
   / .post-body / .post-figure.

   Leaving .post-article capped here made those rules unreachable — the article box was
   760px wide and centred, so every child sat at x=340 or x=388 no matter what max-width
   or margin they declared, while the nav and the RECENT POSTS block on the same page sat
   at x=48. Two competing owners of one column, and the outer one silently won. */
.page-article{
  max-width:760px;
  margin-inline:auto;
}

/* ── SERVICE PAGES ─────────────────────────────────────────────────────────────────────
   Left-aligned at the site's content edge, NOT centred. Jesse's call: these must match the
   rest of V2 rather than read as a centred document while the homepage and portfolio read
   as a marketing site.

   The prose keeps the 760px measure for the same reason the blog column does — that is
   about 65 characters, and the readable range is 45-75. Filling 1440px would be roughly
   double the upper bound on pages running 1,500 to 2,900 words. So this removes the
   CENTRING, not the measure: the column starts where every other h1 on the site starts and
   simply stops at a readable width.

   Scoped to .service-article rather than changed on .page-article, because .page-article's
   centring is deliberate for generic inner pages and is what the blog fix left in place. */
.service-article{
  max-width:none;
  margin-inline:0;
}
.service-article .page-content{max-width:760px;}

/* With a hero above it, the body section must NOT also reserve nav clearance — the hero
   already sits under the fixed bar and owns the top of the page. Without this the copy
   started ~120px below a full-bleed photograph, reading as a gap rather than a break. */
.service-article > .svc-body.has-hero:first-of-type{padding-top:64px;}
@media(max-width:900px){
  .service-article > .svc-body.has-hero:first-of-type{padding-top:40px;}
}

/* The nav is position:fixed and 68px tall, so the FIRST section has to clear it explicitly.
   .section's own padding does at desktop (96px) but not on a phone, where it drops to 56px
   and put the h1 twelve pixels UNDERNEATH the bar — the heading was visibly sliced. Same
   calc() the portfolio header uses, so the two land at the same height. */
.service-article > .svc-body:first-child{padding-top:calc(var(--nav-clear) + 52px);}
@media(max-width:900px){
  .service-article > .svc-body:first-child{padding-top:calc(var(--nav-clear) + 32px);}
}

/* Base flow */
.page-content,
.post-content{
  font-family:var(--sans);
  font-size:18px;
  line-height:1.75;
  color:var(--text);
}
@media(max-width:768px){
  .page-content,
  .post-content{ font-size:17px; }
}

/* First block butts cleanly under the header's 32px gap; last block adds no trailing space. */
.page-content > :first-child,
.post-content > :first-child{ margin-top:0; }
.page-content > :last-child,
.post-content > :last-child{ margin-bottom:0; }

/* ── Paragraphs ───────────────────────────────────────────────────────────────────── */
:is(.page-content,.post-content) p{
  margin:0 0 1.15em;
  text-wrap:pretty;
}

/* ── Headings ─────────────────────────────────────────────────────────────────────────
   Poppins, navy. Each level is visually distinct rather than assuming a clean hierarchy,
   because authors pick heading size not depth. Generous top margin gives the long pages
   scannable section breaks. Fluid via clamp(), matching the theme's other headings. */
:is(.page-content,.post-content) h2,
:is(.page-content,.post-content) h3,
:is(.page-content,.post-content) h4,
:is(.page-content,.post-content) h5,
:is(.page-content,.post-content) h6{
  font-family:var(--heading);
  color:var(--navy);
  font-weight:700;
  line-height:1.22;
  letter-spacing:var(--track-heading, -0.01em);
  text-wrap:balance;
  margin:1.9em 0 0.5em;
}
:is(.page-content,.post-content) h2{ font-size:clamp(24px,3vw,32px); font-weight:800; }
:is(.page-content,.post-content) h3{ font-size:clamp(20px,2.2vw,25px); }
:is(.page-content,.post-content) h4{ font-size:clamp(18px,1.6vw,20px); }
:is(.page-content,.post-content) h5,
:is(.page-content,.post-content) h6{ font-size:17px; }

/* A body <h1> must not survive migration — the page title already owns the only h1, and the
   live Pages carry 20+ of them. Style defensively so a missed one is not an unstyled monster;
   the migration test is what actually catches it. */
:is(.page-content,.post-content) h1{
  font-family:var(--heading);color:var(--navy);font-weight:800;
  font-size:clamp(26px,3.4vw,36px);line-height:1.15;margin:1.6em 0 0.5em;
  letter-spacing:var(--track-heading, normal);
}
/* ...except the one page-service.php puts there on purpose (2026-10-30): the h2 that shows V1's h1, promoted to h1 so
   V1's h1 is the h1 again. It keeps the h2's look, value for value (the h2 rules below, and .svc-band-copy's further
   down), so the tag changes and nothing on screen does. */
:is(.page-content,.post-content) h1.nc-h1-as-h2{
  font-family:var(--heading);color:var(--navy);font-weight:800;
  line-height:1.22;letter-spacing:var(--track-heading, -0.01em);
  text-wrap:balance;margin:1.9em 0 0.5em;
  font-size:clamp(24px,3vw,32px);
}

/* ── Inline ─────────────────────────────────────────────────────────────────────────────
   Global a{} has NO underline; body links must read as links. Underline + gold, visible
   focus ring (focus-visible only, so mouse users don't get it). Most links live inside
   paragraphs on Posts (45 in the sample), so this carries a lot of the page. */
:is(.page-content,.post-content) a{
  color:var(--brown-dark);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:2px;
  text-decoration-color:var(--border-warm);
  transition:color .15s var(--ease), text-decoration-color .15s var(--ease);
}
:is(.page-content,.post-content) a:hover{
  color:var(--brown-light);
  text-decoration-color:currentColor;
}
:is(.page-content,.post-content) a:focus-visible{
  outline:2px solid var(--brown-light);
  outline-offset:2px;
  border-radius:2px;
}
:is(.page-content,.post-content) strong,
:is(.page-content,.post-content) b{ font-weight:700; color:var(--text); }
:is(.page-content,.post-content) em,
:is(.page-content,.post-content) i{ font-style:italic; }
:is(.page-content,.post-content) sup,
:is(.page-content,.post-content) sub{ font-size:0.75em; line-height:0; }
:is(.page-content,.post-content) mark{ background:var(--brown-pale); color:inherit; padding:0 .15em; }
:is(.page-content,.post-content) code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:0.9em; background:var(--grey); padding:.1em .35em; border-radius:4px;
}

/* ── Lists ──────────────────────────────────────────────────────────────────────────── */
:is(.page-content,.post-content) ul,
:is(.page-content,.post-content) ol{
  margin:0 0 1.15em;
  padding-left:1.4em;
}
:is(.page-content,.post-content) li{ margin:0 0 0.5em; }
:is(.page-content,.post-content) li:last-child{ margin-bottom:0; }
:is(.page-content,.post-content) ul{ list-style:disc; }
:is(.page-content,.post-content) ol{ list-style:decimal; }
:is(.page-content,.post-content) ul ul{ list-style:circle; }
:is(.page-content,.post-content) ul li::marker{ color:var(--brown); }
:is(.page-content,.post-content) ol li::marker{ color:var(--brown-dark); font-weight:700; }
/* Nested lists (seen ×6): tighten the top gap, keep the extra indent from padding-left. */
:is(.page-content,.post-content) li > ul,
:is(.page-content,.post-content) li > ol{ margin:0.5em 0 0; }

/* ── Media ──────────────────────────────────────────────────────────────────────────────
   img{} is already max-width:100%;display:block globally. Bare <img> appears on Pages,
   <figure><img> on Posts — cover both. */
:is(.page-content,.post-content) img{
  height:auto;
  border-radius:8px;
  margin:1.6em 0;
}
:is(.page-content,.post-content) figure{ margin:1.8em 0; }
:is(.page-content,.post-content) figure img{ margin:0; }
:is(.page-content,.post-content) figcaption{
  font-size:14px; color:var(--muted); line-height:1.5;
  margin-top:8px; text-align:center;
}
/* WordPress core alignment classes (these survive migration; Elementor's do not). */
:is(.page-content,.post-content) .aligncenter{ margin-inline:auto; }
:is(.page-content,.post-content) .has-text-align-center{ text-align:center; }
:is(.page-content,.post-content) .has-text-align-right{ text-align:right; }

/* ── Tables ─────────────────────────────────────────────────────────────────────────────
   Rare but real. On mobile a table cannot reflow, so it scrolls horizontally instead of
   squishing. The migrated markup is a bare <table> with no wrapper, so the scroll is put on
   the table itself: display:block fills the column, nowrap cells overflow it, overflow-x
   catches them. A .table-scroll wrapper is also styled in case migration adds one later. */
:is(.page-content,.post-content) .table-scroll{ overflow-x:auto; margin:1.8em 0; }
:is(.page-content,.post-content) .table-scroll table{ margin:0; }
:is(.page-content,.post-content) table{
  width:100%;
  border-collapse:collapse;
  margin:1.8em 0;
  font-size:16px;
  line-height:1.5;
}
:is(.page-content,.post-content) th,
:is(.page-content,.post-content) td{
  border:1px solid var(--border);
  padding:10px 14px;
  text-align:left;
  vertical-align:top;
}
:is(.page-content,.post-content) thead th{
  background:var(--cream-raised);
  font-family:var(--heading); font-weight:700; color:var(--navy);
}
:is(.page-content,.post-content) tbody tr:nth-child(even){ background:var(--off); }
/* V1's tables were pasted from a document: each cell's text sits in a span with its own inline font and size, down to
   xx-small, 9px on a phone, in Custom Colours' table (the mobile review, 2026-10-07; four posts carry them). The table's
   own type wins; the spans keep their colours. !important because nothing else outranks an inline style. */
:is(.page-content,.post-content) :is(th,td) span[style]{ font-family:inherit !important; font-size:inherit !important; }
@media(max-width:768px){
  :is(.page-content,.post-content) table{
    display:block;
    overflow-x:auto;
    -webkit-overflow-scrolling:touch;
  }
  :is(.page-content,.post-content) th,
  :is(.page-content,.post-content) td{ white-space:nowrap; }
  /* ⚠ EXCEPT A ONE-COLUMN TABLE: that's a callout box, not data. Postmedia's newer posts box their closing call to action in
     a single cell (td width="672"), and on a phone its sentence ran one line long off the screen, to be scrolled sideways
     to read (found porting 6090, 6096 and 6102, 2026-10-08). Tables of two or more columns keep the scroll. */
  :is(.page-content,.post-content) table:not(:has(tr > :nth-child(2))) :is(th,td){ white-space:normal; width:auto; }
}

/* ── Blockquote / rule (defensive) ──────────────────────────────────────────────────── */
:is(.page-content,.post-content) blockquote{
  margin:1.8em 0;
  padding:6px 0 6px 24px;
  border-left:3px solid var(--brown);
  color:var(--text-2);
  font-size:1.1em; line-height:1.6; font-style:italic;
}
:is(.page-content,.post-content) blockquote > :last-child{ margin-bottom:0; }
:is(.page-content,.post-content) hr{
  border:0;
  border-top:1px solid var(--border);
  margin:2.4em 0;
}

/* ════════════════════════════════════════════════════════════════════════════════════
   SERVICE PAGE — kitchen renovation, and the pattern the other service pages follow
   ════════════════════════════════════════════════════════════════════════════════════
   Built to carry V1's outline in V2's design language: every heading V1 had is still a
   heading here, including the h3 strapline under the h1 and the h4 under each h3. Those
   pairings look unusual in isolation; they are V1's structure and the parity diff counts
   them, so they are styled to read correctly rather than restructured away.

   Mobile-first checks apply as everywhere else: 85% of traffic is mobile, and the step and
   upgrade grids collapse to one column before they get cramped.
──────────────────────────────────────────────────────────────────────────────────────── */

/* ── Hero — editorial, full-bleed ──────────────────────────────────────────────────
   Oversized display type on a photograph, with the strapline and actions in a frosted
   panel beneath it. Content is anchored to the BOTTOM: the top of a full-bleed image is
   where the fixed nav sits, and pinning type up there fights it on every scroll.

   Height is dvh, not vh. On iOS, vh counts the area behind Safari's toolbars, so a 100vh
   hero puts its own CTA under the browser chrome on first paint — on a site where the
   people who actually click are disproportionately mobile (search CTR is 4x desktop). */
.svc-hero{
  position:relative;
  min-height:min(86dvh, 780px);
  display:flex;align-items:flex-end;
  padding:120px 48px 56px;
  margin-top:var(--nav-clear);                 /* clears the fixed nav */
  overflow:hidden;
  background:var(--navy-dark);
}
.svc-hero-media{position:absolute;inset:0;}
.svc-hero-media img{
  width:100%;height:100%;object-fit:cover;
  /* Focal point, not the default. object-fit:cover centres the crop vertically, so a
     landscape kitchen in a tall phone frame shows mostly CEILING — the flat, empty part of
     the photo. Biasing downward keeps cabinetry, counter and island in frame. Desktop is
     near enough to the image's own ratio that it barely crops, so it stays centred. */
  object-position:center center;
}
/* Two-stop scrim. A single flat overlay either washes the photograph out or leaves the
   lower copy on a bright worktop; this keeps the image open at the top and lands the type
   on something solid. */
.svc-hero::after{
  content:'';position:absolute;inset:0;
  background:linear-gradient(180deg,
    rgba(11,8,42,.12) 0%,
    rgba(11,8,42,.34) 30%,
    rgba(11,8,42,.72) 58%,
    rgba(11,8,42,.92) 100%);
}
/* The gradient above is tuned to where the TYPE sits, not to taste. Content is bottom
   anchored, so the eyebrow lands around 60% and the h1 around 70-80% of the hero's height
   — the earlier ramp was still only 38% opaque there, and a gold eyebrow on a bright white
   kitchen simply vanished. It has to hold up against a LIGHT photograph, because the
   placeholder is one and the real image is not chosen yet; a scrim tuned to a dark hero
   image would break the moment the photo is swapped. */
.svc-hero-inner{
  position:relative;z-index:2;
  max-width:var(--max);margin:0 auto;width:100%;
}
.svc-hero .eyebrow{color:var(--brown-light);margin-bottom:14px;text-shadow:0 1px 18px rgba(11,8,42,.55);}
.svc-hero .eyebrow::before{background:var(--brown-light);}

/* Display scale. Poppins is geometric and wide, so it will not read like the condensed
   grotesque in the references — it is pushed to size instead of faked. 8vw keeps the
   longest line ("Kitchen Renovations") on one line from about 900px up while staying
   sane on a phone; text-wrap:balance stops a single orphaned word on the last line. */
.svc-hero-title{
  font-family:var(--heading);font-weight:800;color:#fff;
  /* -10% from clamp(40,8vw,104) on Jesse's call — the headline was crowding the frosted
     card on a laptop and leaving the photograph no room to breathe. */
  font-size:clamp(36px,7.2vw,94px);
  line-height:0.98;letter-spacing:-0.025em;
  margin:0 0 28px;max-width:14ch;
  text-wrap:balance;
  /* Insurance, not decoration. The scrim is tuned to where type sits, but the hero is
     taller on desktop so the eyebrow and first line ride up into the lighter part of the
     gradient — and this image is a wall of white cabinetry. A wide, soft shadow costs
     nothing, is invisible against dark areas, and stops the type dissolving if the photo
     is ever swapped for a brighter one. */
  text-shadow:0 2px 28px rgba(11,8,42,.42);
}
/* The accent words, upright in the heading's own font, as every heading's accent is (.section-heading em, .v2-h2 em).
   Jesse, 2026-09-29, on the italic accents: "we don't use it anywhere else" and "do them all now". This one was
   missed because no page used it until the design consultation page (2026-10-28: "you know I don't use italics").
   brown-light, not brown: it sits on the hero's dark wash. */
.svc-hero-title em{
  color:var(--brown-light);
  font-style:normal;
}
/* A sentence-long headline ('wide', the design consultation page's "Kitchen renovations, designed in Ottawa."): two lines
   on desktop, not four. At 14ch it stacked so tall that the eyebrow rode up into the white cabinets and vanished. */
.svc-hero--wide .svc-hero-title{max-width:20ch;}

/* Frosted panel. The rgba fill alone meets contrast, so browsers without backdrop-filter
   degrade to a plain translucent card rather than white-on-photograph. */
.svc-hero-card{
  display:inline-block;max-width:560px;
  background:rgba(13,10,38,.52);
  backdrop-filter:blur(18px) saturate(140%);
  -webkit-backdrop-filter:blur(18px) saturate(140%);
  border:1px solid rgba(255,255,255,.16);
  border-radius:18px;
  padding:26px 28px 24px;
}
/* Still an h3 — V1's outline. Sized as a deck, not a section head. */
.svc-hero-strap{
  font-family:var(--sans);font-weight:400;
  font-size:clamp(16px,1.7vw,20px);line-height:1.45;
  color:rgba(255,255,255,.92);
  margin:0 0 22px;text-wrap:pretty;
}
.svc-hero-cta{display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px;}

/* .svc-ghost and .svc-arrow lived here — a bespoke underlined ↗ link used as the heroes'
   secondary action. Deleted rather than left lying around: it was a one-off, three uses and
   all of them in heroes, reinventing a pattern the site already had as .btn btn-outline.
   Its replacement is .btn-outline-light, defined with the other buttons. */

@media(max-width:900px){
  /* Show the kitchen rather than the ceiling. 80%, not 68%: the ceiling and bulkhead are
     the top third of this photograph and carry no information, so the crop is pushed well
     down the frame to land on cabinetry, backsplash and range. */
  .svc-hero-media img{object-position:center 80%;}
}
@media(max-width:768px){
  .svc-hero{padding:96px 24px 36px;min-height:min(80dvh, 640px);}
  .svc-hero-title{margin-bottom:22px;max-width:none;}
  .svc-hero-card{display:block;max-width:none;padding:22px 20px 20px;border-radius:16px;}
  .svc-hero-cta{gap:6px 16px;}
  .svc-hero-cta .btn{flex:1 1 100%;text-align:center;justify-content:center;}

  /* ── BOTH hero actions are hidden on mobile ───────────────────────────────────────
     Not a style preference — on a phone both are already PERSISTENT CHROME:

       - the quote CTA is the sticky bar, which follows the user down the whole page
       - the phone number is the orange pill in the fixed header, on screen at all times

     Repeating either in the hero puts two instances of the same action in one viewport,
     competing for the same tap and splitting the signal. The hero card keeps the strapline
     and nothing else, which is also what stops the first screen reading as a wall of
     buttons over a photograph.

     Desktop keeps both: there is no sticky bar and no persistent call pill up there, so the
     hero is the only place either action exists above the fold.

     Gated on :not(.no-sticky-cta). A page that opts OUT of the sticky bar has no persistent
     quote CTA, so it must keep the hero's — otherwise it would ship with no primary action
     above the fold at all. */
  body:not(.no-sticky-cta) .svc-hero-cta{display:none;}
  body:not(.no-sticky-cta) .svc-hero-strap{margin-bottom:0;}
  body:not(.no-sticky-cta) .svc-hero-card{padding:20px;}
}
/* ══ SERVICE HERO — VARIANT C ("gallery") ═══════════════════════════════════════════
   The service name set as the hero itself: one enormous line across the frame, with
   translucent cells resting along the bottom instead of a single opaque card.

   Emits exactly the same two headings as variants A and B, in the same order, so the
   choice between them is purely visual as far as dev/parity-diff.php is concerned.
   ─────────────────────────────────────────────────────────────────────────────────── */
.svcc-hero{
  position:relative;
  /* ⚠ FILLS THE VIEWPORT UNDER THE NAV, with no pixel cap. The cap used to be 820px, so on any
     screen taller than ~888px the hero stopped short and the cream band beneath it showed as a
     strip along the bottom of the fold — Jesse read it as a stray white line, which is fair:
     a 30px sliver of the next section looks like a rendering fault, not a design. dvh rather
     than vh so mobile browser chrome is accounted for. */
  min-height:calc(100dvh - var(--nav-clear));
  display:flex;flex-direction:column;justify-content:flex-end;
  padding:120px 48px 40px;
  margin-top:var(--nav-clear);                 /* clears the fixed nav, as variant A */
  overflow:hidden;
  background:var(--navy-dark);
}
.svcc-media{position:absolute;inset:0;}
.svcc-media img{width:100%;height:100%;object-fit:cover;object-position:center center;}

/* ── THE SCROLL CUE ─────────────────────────────────────────────────────────────────────────
   A bobbing, glowing arrow beside the portfolio cell. It exists because the hero now fills the
   fold exactly, so there is no sliver of the next section to hint that one exists — and the
   client-side ICP is 40-65, where "there is more below" is worth stating rather than assuming.

   ⚠ IT RETIRES ONCE THE READER SCROLLS. A cue that keeps pulsing after its advice has been
   taken is just movement in the corner of the eye. .is-gone fades it and takes it out of the
   tab order. */
/* ⚠ SOLID BRAND BROWN, NOT FROSTED GLASS. The first pass was a 46px translucent circle that
   sat quietly against the photograph — too quiet for a cue whose entire job is to be noticed by
   someone who might not realise the page continues. Jesse's mock is a filled brown disc with a
   heavy white arrow, and the point stands: a hint nobody sees is not a hint. Brown is the
   theme's action colour, so it also reads as "this does something". */
.svcc-scroll{
  flex:none; align-self:center;
  width:72px; height:72px; padding:0;
  border-radius:50%; cursor:pointer;
  border:0;
  background:var(--brown);
  color:#fff;
  display:inline-flex; align-items:center; justify-content:center;
  animation:svcc-scroll-bob 2.6s ease-in-out infinite,
            svcc-scroll-glow 2.6s ease-in-out infinite;
  transition:opacity .45s var(--ease), visibility 0s linear .45s,
             background-color .2s var(--ease), transform .2s var(--ease);
}
.svcc-scroll-arrow{ width:30px; height:30px; display:block; }
.svcc-scroll:hover{ background:var(--brown-light); }
.svcc-scroll:focus-visible{ outline:2px solid #fff; outline-offset:3px; }
.svcc-scroll.is-gone{ opacity:0; visibility:hidden; pointer-events:none; }

@keyframes svcc-scroll-bob{
  0%,100%{ transform:translateY(-4px); }
  50%    { transform:translateY(5px); }
}
/* Glow on box-shadow rather than filter: a filter on this element would blur the backdrop
   behind it too, and spread-only shadows cost nothing to composite. */
/* Brown-tinted, not white: a white halo around a brown disc reads as a rendering artifact,
   a brown one reads as the disc itself glowing. */
@keyframes svcc-scroll-glow{
  0%,100%{ box-shadow:0 0 0 0 rgba(167,120,69,0); }
  50%    { box-shadow:0 0 26px 8px rgba(167,120,69,.42); }
}

@media (prefers-reduced-motion: reduce){
  /* The cue still needs to READ as a cue, so it keeps its shape and simply stops moving. */
  .svcc-scroll{ animation:none; }
}
@media(max-width:768px){
  .svcc-scroll{ width:58px; height:58px; }
  .svcc-scroll-arrow{ width:24px; height:24px; }
}

/* ── THE SLOW ZOOM ──────────────────────────────────────────────────────────────────────────
   Copied from rsd-agencements.com's services hero, read rather than eyeballed: they animate the
   frame to transform:scale(1.12) and LEAVE it there — the inline style still reads scale(1.12)
   long after the page settles. Jesse: "slow zoom until it gradually stops and stays at it's
   zoomed in state."

   So: forwards, never reversing and never looping. A looping Ken Burns quietly snaps back to
   the start every cycle, and on a hero that is the one moment a reader is guaranteed to see.

   The easing does the "gradually stops" — most of the travel is early, then it creeps. 16s is
   long enough that no single glance catches movement, which is what reads as premium rather
   than as an animation.

   ⚠ The hero photograph is already object-fit:cover, so scaling crops further in rather than
   revealing any edge. Nothing can pull away from the frame. */
@keyframes svcc-hero-zoom{
  from{ transform:scale(1); }
  to  { transform:scale(1.12); }
}
.svcc-media img{
  transform-origin:center center;
  /* 17.6s and easeOutQuad — two passes, both from Jesse: "a bit slower on first load, it's
     kinda aggressive", then "soften the curve too".

     DURATION and EASING do different jobs here and it is worth not confusing them again.
     Stretching 16s -> 17.6s scaled velocity at every point by 1/1.1 but left the SHAPE alone,
     so the run stayed front-loaded — ~59% of the travel in the first quarter. The curve is what
     owns that ratio: easeOutCubic (.22,.61,.36,1) opens at 2.77x the average rate; easeOutQuad
     opens at 1.84x and puts ~44% in the first quarter instead.

     Still a TRUE ease-out — it never accelerates, so it keeps the "gradually stops and stays"
     that made the reference feel expensive. An ease-IN-out would have softened the opening
     further but would read as a delay before the movement starts, which on a hero is worse
     than a brisk one.

     ⚠ (.25,.46,.45,.94) is rsd-agencements' own button easing, taken from the same site the
     zoom came from — so the page is at least borrowing one vocabulary rather than two. */
  animation:svcc-hero-zoom 17.6s cubic-bezier(.25,.46,.45,.94) forwards;
  will-change:transform;
}
@media (prefers-reduced-motion: reduce){
  /* Held at the end state rather than the start: the composition was tuned against the zoomed
     frame, so stopping at scale(1) would show a different crop to everyone else. */
  .svcc-media img{ animation:none; transform:scale(1.12); }
}

/* Bottom-heavy ramp, the same shape variant A uses — light at the top so the kitchen is
   actually visible, gathering weight down the frame to seat the type and the cells.

   An earlier pass here used a FLAT wash instead, because the headline was floating in the
   middle of the hero where A's ramp is still nearly transparent, and an even tint was the
   only way to hold white type up there. That constraint is gone: the headline now sits low,
   just above the cells, so it lands on the strong part of the ramp by itself and the top
   two thirds of the photograph can stay open. Moving the copy down is what paid for the
   lighter tint — the two changes are one change.

   Still stops well short of A's .92 at the base. A can go that dark because its copy sits
   on a solid card; here the cells are meant to read as glass, and glass over a near-black
   scrim is just a dark box.

   Tuned against a LIGHT photograph, same reason as A: this kitchen is a wall of white
   cabinetry, and a scrim tuned to a dark hero breaks the moment the photo is swapped. */
.svcc-hero::after{
  content:'';position:absolute;inset:0;
  background:linear-gradient(180deg,
    rgba(11,8,42,.10) 0%,
    rgba(11,8,42,.24) 34%,
    rgba(11,8,42,.46) 66%,
    rgba(11,8,42,.74) 100%);
}
/* flex:1 so the inner fills the hero's content box, which is what lets the auto margins on
   the title do their work. Without it the inner is only as tall as its content, the hero
   collapses onto that, and there is no free space for anything to be distributed into —
   the first attempt at lifting the title added top padding instead and simply made the
   hero taller, leaving the composition identical. */
.svcc-inner{
  position:relative;z-index:2;
  max-width:var(--max);margin:0 auto;width:100%;
  flex:1 1 auto;display:flex;flex-direction:column;
}

/* Display line. 9.4vw keeps "Kitchen Renovations" on ONE line from roughly 900px up,
   which is what makes it read as a wordmark rather than a headline; below that it wraps
   and the clamp floor stops it collapsing. text-wrap:balance evens the wrap when it does.

   The heavy text-shadow is load-bearing here, not insurance. Variant A lands its title on
   the solid part of a steep scrim; this one sits mid-frame over cabinetry at ~16-34%
   opacity, so the shadow is what separates white type from a white kitchen. */
.svcc-title{
  font-family:var(--heading);font-weight:800;color:#fff;
  /* ⚠ THE SECOND TERM IS WHAT KEEPS A LONG SERVICE NAME ON ONE LINE. --nc-title-len is the
     headline's character count, set inline by template-parts/service-hero-c.php. The largest size
     that still fits is k × available ÷ characters, where 1/k is the face's widest width per
     character. The clamp wins for short titles, so "Kitchen Updates" renders exactly as before.

     ⚠ k WAS 1.9 FOR THE OLD FACE, AND MONT (243cc47) IS WIDER. Measured 2026-10-01 in Mont at
     this weight and spacing, the 22 service titles run 0.446–0.549 × font-size per character.
     "Bathroom Renovation" is the widest, at 0.549, which allows k ≤ 1.821. At 1.9, Bathroom
     Renovation, Custom Kitchen Cabinets and House Wrap Services wrapped to two lines at every
     width from 1100 to 1920px. Jesse saw it on Custom Kitchen Cabinets. At 1.8, all 22 titles fit
     on one line from 820 to 1920px. Re-measure if the face or a title changes.

     ⚠ "AVAILABLE" IS min(100vw - 96px, --max), NOT 100vw - 96px, AND THE FIRST VERSION HAD IT
     WRONG. .svcc-inner is capped at max-width:var(--max) — 1440px — so past a 1536px window the
     column STOPS GROWING while 100vw keeps going. The formula therefore reported 1824px of room
     at 1920 when there were 1440, computed a cap the clamp beat, and the headline wrapped
     exactly as before on precisely the wide screens the fix was supposed to cover. Jesse
     reloaded and saw two lines; the deploy was fine, the arithmetic was not.
     The 96px is the hero's two 48px gutters, which apply below --max. */
  /* ⚠ max(40px, …) IS LOAD-BEARING AND WAS MISSING ON THE FIRST PASS. Without it the fitting
     term keeps shrinking below the clamp's own floor: at 375px it drove "Kitchen Renovations"
     to 27.9px, on the viewport that carries 85% of this site's traffic. On a phone the headline
     SHOULD hit 40px and wrap to two lines — that is the designed behaviour and Kitchen Updates
     does it too. The fitting term is a cap for wide screens, never a floor for narrow ones. */
  font-size:max(
    40px,
    min(
      clamp(40px,9.4vw,152px),
      calc( min(100vw - 96px, var(--max)) * 1.8 / var(--nc-title-len, 15) )
    )
  );
  line-height:0.92;letter-spacing:-0.035em;
  margin:0;
  text-wrap:balance;
  text-shadow:0 4px 40px rgba(11,8,42,.55), 0 1px 4px rgba(11,8,42,.30);
}
/* margin-top:auto only. All the free space goes ABOVE this block, so eyebrow and headline
   drop to sit just above the cells instead of hovering mid-frame.

   That was the earlier behaviour (auto on both sides, centring it in the leftover space)
   and it cost twice over: it left the composition looking arbitrary, and it put the type on
   the weakest part of the gradient, which is what forced the flat scrim that was washing out
   the kitchen. Sitting the copy low fixes both — the type lands where the ramp is already
   dark, so the top of the photograph no longer has to be tinted to compensate.

   Padding cannot do this job: the hero's height is content-driven, so top padding simply
   makes the hero taller and moves nothing. On .svcc-head rather than the h1, so the eyebrow
   travels with the headline. */
.svcc-head{margin-top:auto;margin-bottom:0;}
.svcc-title-main{display:block;}

/* Plain gold on the photograph, dropped back from a chip on Jesse's call — the chip read as
   a stray UI pill in the middle of an editorial hero.

   ⚠ Known and accepted, so nobody re-measures this and files it as a new bug: gold #bb8c57
   here composites to about 1.33:1 against the photograph behind it, where AA at this size
   wants 4.5. The shadows below help the edges read but do not change the ratio — a shadow
   softens, it does not raise contrast.

   It got WORSE, not better, when the scrim was lightened to show more of the kitchen: at
   the old flat 50% wash it measured 2.34:1, and at the current ramp it sits around 34%
   opacity where the eyebrow lands. That is the honest trade — the hero shows the kitchen,
   and this label pays for it.

   It is not fixable in place. Even PURE WHITE only reaches ~3.7:1 over the brightest part
   of this photograph, and the arithmetic says no text colour can hit 4.5 there: it would
   need a relative luminance above 1.0. Over a bright photo the only real fixes are a
   backing (the chip), a much heavier scrim, or a darker photograph.

   Judged acceptable because this is a decorative label, not content: it says "Kitchens"
   directly above an H1 that says "Kitchen Renovations", so nothing is lost by a reader who
   cannot pick it out. If that call ever changes, white is the cheapest improvement — it
   roughly triples the ratio without touching the layout. */
.svcc-hero .eyebrow{
  color:var(--brown-light);margin-bottom:20px;
  text-shadow:0 1px 3px rgba(11,8,42,.9), 0 0 22px rgba(11,8,42,.8);
}
.svcc-hero .eyebrow::before{background:var(--brown-light);}

/* ⚠ ORDER IS PARITY-CRITICAL, not just visual.
   This <em> must stay AFTER .svcc-title-main in the DOM. Moving "in Ottawa" above the
   display line — the obvious way to sit it on the stronger part of the scrim — would make
   the H1 read "in Ottawa Kitchen Renovations" and the diff would report a changed H1.
   It has to be fixed with contrast, in place.

   Which needs a heavy hand: it lands around 62% of the hero, where the ramp is only 34%
   opaque, and it is gold on a white quartz counter. At the first pass it was invisible.
   Two shadows — a tight one for edge definition and a wide one to darken the ground it
   sits on — plus a size and weight bump, rather than one soft blur that just greys it. */
.svcc-title em{
  display:block;
  font-family:var(--sans);font-style:italic;font-weight:500;
  color:var(--brown-light);
  font-size:clamp(16px,1.6vw,24px);letter-spacing:0.01em;
  margin-top:.85em;
  text-shadow:0 1px 3px rgba(11,8,42,.95), 0 0 26px rgba(11,8,42,.85);
}

.svcc-cells{
  display:flex;align-items:stretch;gap:14px;
  margin-top:44px;
}

/* The cell. Tinted glass, not clear glass — see the note in service-hero-c.php: clear
   panels over white cabinetry put white text on a white worktop. blur(26px) is doing the
   real work; it smears the bright detail behind the panel into a soft field so the copy
   holds, while the photograph still reads THROUGH the cell rather than behind a box.

   The rgba fill alone clears AA without the blur, so a browser with no backdrop-filter
   degrades to a plain translucent panel instead of losing the text. */
.svcc-cell{
  background:rgba(16,12,44,.34);
  backdrop-filter:blur(26px) saturate(130%);
  -webkit-backdrop-filter:blur(26px) saturate(130%);
  border:1px solid rgba(255,255,255,.20);
  border-radius:20px;
}
/* ⚠ THE CELL IS TALLER THAN ITS CONTENT, AND SOMETHING HAS TO DECIDE WHERE THE SLACK GOES.
   Its height is set by the portfolio thumb beside it, not by the copy: measured at 1800, the
   cell is 170px while the strapline plus buttons come to 94px. As a plain block that slack all
   fell BELOW the buttons — 51px of it against 22px of intended bottom padding — which is the
   dead space Jesse flagged: "lots of empty space under the button and words".

   A flex column with space-between hands the slack to the GAP instead, so the strapline sits
   against the top padding and the buttons against the bottom one, and the pill reads as filled
   rather than top-weighted. min-height on the gap keeps the two apart when the strapline wraps
   to two lines and there is no slack left to distribute. */
.svcc-cell-copy{
  flex:1 1 auto;max-width:560px;
  padding:24px 26px 22px;
  display:flex;flex-direction:column;justify-content:space-between;gap:20px;
}
.svcc-strap{
  font-family:var(--sans);font-weight:400;
  font-size:clamp(16px,1.7vw,20px);line-height:1.45;
  color:rgba(255,255,255,.94);
  /* ⚠ NO BOTTOM MARGIN ANY MORE. The column's gap owns the separation now; leaving the margin
     on would add to the distributed space and re-create a lopsided block. */
  margin:0;text-wrap:pretty;
}
.svcc-cta{display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px;}

/* Second cell — the reference's video slot, standing in as a portfolio link. */
.svcc-cell-thumb{
  flex:0 0 200px;
  display:flex;flex-direction:column;
  padding:10px;text-decoration:none;
  transition:border-color .2s var(--ease), transform .2s var(--ease);
}
.svcc-cell-thumb:hover{border-color:rgba(255,255,255,.42);transform:translateY(-2px);}
.svcc-cell-thumb:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;}
/* aspect-ratio, not min-height, and the distinction cost a round trip worth recording.
   Left to itself this box takes its height from the THUMBNAIL'S own proportions —
   feature-kitchen.jpg is 1024x1536 portrait, which drove the media to 267px, the cell to
   324px, and stretched the copy cell to match a picture it has nothing to do with.
   min-height:0 did not fix it: that only permits a flex item to shrink, and nothing here
   has a definite height to shrink into, so the intrinsic ratio still won.

   Declaring the ratio makes the height a property of the LAYOUT rather than of whichever
   photograph is passed in, so a portrait or landscape thumb both behave. flex:1 1 auto is
   kept so the cell can still grow to match a taller copy cell. Measured at 1440x900. */
.svcc-thumb-media{
  display:block;flex:1 1 auto;min-height:0;overflow:hidden;
  border-radius:12px;
  aspect-ratio:16 / 10;
}
.svcc-thumb-media img{width:100%;height:100%;object-fit:cover;display:block;}
/* Sentence case, not the uppercase+tracking this label started with, because the reference's
   own button is sentence case — all-caps micro-labels read as UI chrome, and this cell is
   meant to read as a piece of the page.

   Purely a typographic call, NOT a fitting one. Measured, after an earlier note here claimed
   otherwise: "VIEW OUR PORTFOLIO" at 12px/.06em uppercase is 140px against 178px of inner
   width, so it fits on one line perfectly well. Sentence case at 13px is 119px. Either
   works; this one just reads better. */
.svcc-thumb-label{
  display:block;
  font-family:var(--heading);font-size:13px;font-weight:600;
  letter-spacing:.01em;
  color:#fff;padding:12px 6px 4px;
}

/* ── Tablet ───────────────────────────────────────────────────────────────────────
   iPad sits above the 768px breakpoint, so it was inheriting the desktop hero: an 820px
   tall frame holding a headline and two small cells, with the middle third empty. Shorter
   frame, and the headline drops to sit just above the cells instead of floating in the
   centre of all that space. */
@media(max-width:1024px){
  .svcc-hero{min-height:min(66dvh, 620px);padding:104px 32px 34px;}
  .svcc-cells{margin-top:26px;}
}
/* 900px, matching .sticky-cta's OWN breakpoint rather than 768.
   The hero's quote button hides because the sticky bar already carries that action, and the
   phone number because the header pill already carries that one — but the sticky bar starts
   at 900px while this rule used to start at 768, so between 769 and 900 the hero showed a
   quote button with the sticky bar showing the same one directly beneath it. Exactly the
   duplication the rule exists to prevent, in the band nobody had looked at. */
@media(max-width:900px){
  /* 170, not 150. "View Our Portfolio" is 119px of text and 150px left 118px of inner width
     after padding — one pixel short, so the label wrapped to two lines. Measured. The row
     only fills ~76% here, so the width was free. */
  .svcc-cell-thumb{flex-basis:170px;}
  body:not(.no-sticky-cta) .svcc-cta{display:none;}
  /* With the CTAs hidden here, a cell that also has no strapline is empty — hide the box
     rather than painting an empty one. */
  body:not(.no-sticky-cta) .svcc-cell-copy--bare{display:none;}
  body:not(.no-sticky-cta) .svcc-strap{margin-bottom:0;}

  /* The portfolio cell goes wherever the BUTTONS go, and that is the whole rule.
     A previous pass kept it on tablets on the grounds that it is the one hero action not
     duplicated in persistent chrome — true, but it missed what the cell is paired WITH.
     With its buttons the copy cell is ~151px and the two read as a matched pair; without
     them it collapses to a single 23px line, and a short wide pill beside a tall photo
     card just looks unbalanced, which is exactly how it looked. Tying both to the same
     condition keeps the row either a pair or a single cell, never a mismatch. */
  body:not(.no-sticky-cta) .svcc-cell-thumb{display:none;}

  /* Once the buttons are hidden the copy cell holds ONE 23px line, while the thumb cell is
     ~151px tall because of its photograph. Two things then have to be switched off, and
     getting only one of them is what made this look absurd:

       align-items — stretch is the flex default, so the copy cell inherited the thumb's
       height and became a 560x151 panel around a single line. Measured at 820px: 88px of
       dead vertical space. Centring the text INSIDE that panel does not help; the panel is
       the problem. align-items:center lets each cell keep its own height instead.

       max-width — the max-content cap only ran below 768, so through this band the cell
       also kept the desktop's flat 560px. Capping it here too means the panel tracks the
       strapline in both directions rather than neither. */
  body:not(.no-sticky-cta) .svcc-cells{align-items:center;}
  body:not(.no-sticky-cta) .svcc-cell-copy{max-width:max-content;}
}
@media(max-width:768px){
  .svcc-hero{padding:88px 20px 28px;min-height:min(62dvh, 520px);}
  .svcc-title{letter-spacing:-0.03em;}
  .svcc-hero .eyebrow{margin-bottom:14px;}

  /* Crops the TOP of the photograph, which object-position cannot do here and it is worth
     being precise about why: this is a 3:2 landscape image in a portrait frame, so
     object-fit:cover scales it to match the frame's HEIGHT and spills over 600px sideways
     — the vertical overflow is exactly zero, and a vertical object-position has nothing to
     move. That was tuned twice on a previous pass before it was measured, and it never did
     anything.

     Oversizing the media box and hanging it above the frame does work: the box is 110% of
     the hero and its bottom edge is pinned to the hero's, so the top ~9% falls outside and
     the hero's overflow:hidden clips it. Real pixels removed, not a repositioned crop. */
  .svcc-media{top:-10%;height:110%;}

  .svcc-cells{margin-top:24px;gap:10px;}
  /* Only the padding is specific to this breakpoint now — the max-content cap moved up to
     the 900 block, where it belongs, since that is where the buttons come out and the cell
     stops needing the width. Repeated here because the cascade would otherwise hand this
     rule's own declaration back to the desktop 560px.

     The cap is harmless on a phone, which is the point of using one value for both: at
     375px the strapline's max-content is wider than the column, so it never binds and the
     cell still goes edge to edge. Verified at 335 of 335. */
  .svcc-cell-copy{max-width:max-content;padding:20px;}
  .svcc-cta{gap:6px 16px;}
  .svcc-cta .btn{flex:1 1 100%;text-align:center;justify-content:center;}

}

/* The portfolio cell's own breakpoint used to live here. It is gated on the sticky bar in
   the 900 block instead — it now comes and goes with the hero buttons rather than on a
   width of its own, so there is nothing left to declare at 600. */
@media(prefers-reduced-motion:reduce){
  .svcc-cell-thumb{transition:none;}
}

/* ── Intro ────────────────────────────────────────────────────────────────────────── */
.svc-intro{max-width:760px;}
/* h4 under the h2 — again V1's structure. Reads as a deck, so it is styled as one. */
.svc-sub{
  font-family:var(--sans);font-weight:500;
  font-size:clamp(18px,2vw,21px);line-height:1.5;
  color:var(--brown-dark);margin:0 0 24px;text-wrap:pretty;
}
.svc-prose > :first-child{margin-top:0;}
.svc-intro-cta{margin-top:28px;}

/* ── Why NorthCo ──────────────────────────────────────────────────────────────────── */
.svc-why{background:var(--cream-raised);}
.svc-why-list{
  list-style:none;margin:28px 0 0;padding:0;
  display:grid;grid-template-columns:repeat(2,1fr);gap:24px;
}
@media(max-width:800px){.svc-why-list{grid-template-columns:1fr;gap:18px;}}
.svc-why-item{
  background:var(--white);border:1px solid var(--border-warm);
  border-radius:14px;padding:24px 26px;
}
/* Deliberately a span, not a heading: V1 marks these as list items, and promoting them
   would add four headings to the outline as a side effect of migrating. */
.svc-why-title{
  display:block;font-family:var(--heading);font-weight:700;
  font-size:18px;color:var(--navy);margin-bottom:8px;line-height:1.3;
  letter-spacing:var(--track-heading, normal);
}
.svc-why-item p{margin:0;color:var(--text-2);line-height:1.65;font-size:16px;}
.svc-why-foot{margin:26px 0 0;font-weight:600;color:var(--brown-dark);}

/* ── Process: nine numbered steps ─────────────────────────────────────────────────── */
.svc-steps{list-style:none;margin:36px 0 0;padding:0;counter-reset:none;}
.svc-step{
  display:grid;grid-template-columns:64px 1fr;gap:20px;
  padding:26px 0;border-top:1px solid var(--border-warm);
}
.svc-step:first-child{border-top:0;padding-top:8px;}
.svc-step-num{
  font-family:var(--heading);font-weight:800;
  font-size:clamp(26px,3vw,34px);line-height:1;
  color:var(--brown);opacity:.55;
}
.svc-step-title{
  font-family:var(--heading);font-weight:700;color:var(--navy);
  font-size:clamp(19px,2.2vw,23px);line-height:1.25;margin:0 0 4px;
  letter-spacing:var(--track-heading, normal);
}
/* h4 strapline under each step h3 — V1's pairing. Small, gold, italic so the two read as
   title + caption rather than two competing headings. */
.svc-step-strap{
  font-family:var(--sans);font-weight:500;font-style:italic;
  font-size:15px;color:var(--brown-dark);margin:0 0 12px;
}
.svc-step-points{margin:0;padding-left:1.15em;}
/* A step's full sentence ('text'), as body copy: Design & Build's journey, 2026-10-05. No measure cap; the column sets it. */
.svc-step-text{margin:0;color:var(--text-2);line-height:1.65;font-size:16px;}
.svc-step-points li{margin:0 0 6px;color:var(--text-2);line-height:1.6;}
.svc-step-points li::marker{color:var(--brown);}
@media(max-width:600px){
  .svc-step{grid-template-columns:44px 1fr;gap:14px;padding:22px 0;}
}

/* ── Popular upgrades ─────────────────────────────────────────────────────────────── */
.svc-upgrades{background:var(--off);}
.svc-upgrades-sub{margin-bottom:32px;}
.svc-upgrade-grid{
  display:grid;grid-template-columns:repeat(3,1fr);gap:22px;
}
@media(max-width:900px){.svc-upgrade-grid{grid-template-columns:repeat(2,1fr);}}
@media(max-width:600px){.svc-upgrade-grid{grid-template-columns:1fr;}}
.svc-upgrade{
  background:var(--white);border:1px solid var(--border-warm);
  border-radius:14px;padding:24px 24px 22px;
}
.svc-upgrade-title{
  font-family:var(--heading);font-weight:700;color:var(--navy);
  font-size:18px;line-height:1.3;margin:0 0 4px;
  letter-spacing:var(--track-heading, normal);
}
.svc-upgrade-strap{
  font-family:var(--sans);font-weight:500;font-style:italic;
  font-size:14px;color:var(--brown-dark);margin:0 0 12px;
}
.svc-upgrade-points{margin:0;padding-left:1.1em;}
.svc-upgrade-points li{margin:0 0 6px;font-size:15px;color:var(--text-2);line-height:1.55;}
.svc-upgrade-points li::marker{color:var(--brown);}

/* ── Examples ─────────────────────────────────────────────────────────────────────── */
.svc-example-grid{display:grid;grid-template-columns:1fr 1fr;gap:26px;margin-top:28px;}
@media(max-width:768px){.svc-example-grid{grid-template-columns:1fr;gap:20px;}}
.svc-example{margin:0;}
.svc-example img{width:100%;height:auto;border-radius:14px;display:block;}
.svc-example figcaption{margin-top:12px;}
.svc-example-name{
  display:block;font-family:var(--heading);font-weight:700;
  color:var(--navy);font-size:17px;margin-bottom:2px;
  letter-spacing:var(--track-heading, normal);
}
.svc-example figcaption a{
  color:var(--brown-dark);text-decoration:underline;
  text-underline-offset:2px;text-decoration-color:var(--border-warm);font-size:15px;
}
.svc-example figcaption a:hover{text-decoration-color:currentColor;}

/* ── Social ───────────────────────────────────────────────────────────────────────── */
/* Two h2s that are one sentence — V1's markup, kept for parity. Styled as one line so the
   split is invisible to a reader even though it is still two headings in the DOM. */
.svc-social{background:var(--navy);}
.svc-social-inner{text-align:center;}
/* ⚠ ONE HEADING NOW. This was .svc-social-a,.svc-social-b with display:inline, because the
   markup was two h2s that had to sit on one line; the merge made both the pair and the
   display override redundant. .svc-social-b survives as a span inside the h2 and still
   carries the second colour, which is the only thing the split was ever for. */
.svc-social-h{
  color:#fff;margin:0;
  font-size:clamp(24px,3.4vw,38px);
}
.svc-social-b{color:var(--brown-light);}
.svc-social-links{display:flex;justify-content:center;gap:14px;margin-top:24px;flex-wrap:wrap;}
.svc-social-links a{
  color:#fff;border:1px solid rgba(255,255,255,.4);
  border-radius:999px;padding:10px 22px;
  font-family:var(--heading);font-size:13px;font-weight:600;
  letter-spacing:.06em;text-transform:uppercase;text-decoration:none;
}
.svc-social-links a:hover{background:rgba(255,255,255,.12);border-color:#fff;}

/* ── Service hero — VARIANT B (editorial / stacked) ─────────────────────────────────
   The photograph shown whole on cream, with the type above and the actions below, rather
   than type laid over the image. Chosen when the image is high-key or landscape: a bright
   3:2 kitchen in a tall overlay hero has to be both cropped to a sliver and scrimmed to
   mud before white type is legible on it. Here it is neither.
──────────────────────────────────────────────────────────────────────────────────────── */
.svcb-hero{
  background:var(--cream);
  padding:calc(var(--nav-clear) + 40px) 48px 56px;   /* 68px clears the fixed nav */
}
.svcb-head{max-width:var(--max);margin:0 auto 28px;}
.svcb-hero .eyebrow{margin-bottom:14px;}
.svcb-title{
  font-family:var(--heading);font-weight:800;color:var(--navy);
  font-size:clamp(38px,7vw,86px);
  line-height:1.0;letter-spacing:-0.025em;
  margin:0;max-width:16ch;text-wrap:balance;
}
/* Same gold-italic counterpoint as the homepage's .section-heading em, so the two pages
   read as one system rather than two designs. */
.svcb-title em{
  display:block;
  font-family:var(--sans);font-style:italic;font-weight:400;
  color:var(--brown-dark);
  font-size:.5em;letter-spacing:-0.01em;margin-top:.1em;
}
/* No fixed height and no object-fit: the image keeps its own ratio, so it is never cropped
   at any width. That is the entire point of this variant. */
.svcb-media{margin:0;}
.svcb-media img{
  width:100%;height:auto;display:block;
  border-radius:20px;
}
.svcb-foot{
  max-width:var(--max);margin:28px auto 0;
  display:flex;align-items:center;justify-content:space-between;
  gap:32px;flex-wrap:wrap;
}
.svcb-strap{
  font-family:var(--sans);font-weight:400;
  font-size:clamp(17px,1.8vw,22px);line-height:1.45;
  color:var(--text-2);margin:0;max-width:46ch;text-wrap:pretty;
}
.svcb-cta{display:flex;align-items:center;gap:12px 22px;flex-wrap:wrap;}
/* .svcb-ghost lived here — variant B's light-background twin of .svc-ghost, retired with
   it. B now uses .btn btn-outline, the standard secondary for light surfaces. */

@media(max-width:900px){
  .svcb-foot{flex-direction:column;align-items:flex-start;gap:20px;}
}
@media(max-width:768px){
  .svcb-hero{padding:calc(var(--nav-clear) + 24px) 24px 40px;}
  .svcb-head{margin-bottom:20px;}
  .svcb-media img{border-radius:14px;}
  .svcb-cta{width:100%;}
  .svcb-cta .btn{flex:1 1 100%;text-align:center;justify-content:center;}
}

/* ══ BLOG — single post (single.php) ════════════════════════════════════════════════
   The site's highest-traffic template. GSC by page type: blog content takes 5,843
   clicks and 1,290,950 impressions at avg position 23.2, against 3,512 / 665,896 /
   28.8 for every service page combined. 47% of organic clicks land here.

   So this is a READING page first — 4,400-word posts, 85% of them on a phone — and a
   conversion path second. It deliberately does NOT borrow the service pages' full-bleed
   photographic hero: white type over a photograph is the wrong opening for an article,
   and reusing that language would make journalism look like a landing page.
   ─────────────────────────────────────────────────────────────────────────────────── */
/* ── Alignment ─────────────────────────────────────────────────────────────────────
   The reading column is LEFT-ALIGNED to the site's content edge, not centred in the
   viewport. Centring a 760px column inside a 1440px --max put the article at x=388
   while the nav logo, and the RECENT POSTS block further down THIS SAME PAGE, both sat
   at x=48 — 340px of drift that read as the article floating loose.

   So the gutter moves onto the column itself: 760px of reading measure plus the site's
   48px gutters, flush left. The measure is unchanged; only where it starts is. */
.post-head{
  padding:calc(var(--nav-clear) + 48px) 0 0;   /* 68px clears the fixed nav; gutter is on the inner */
}
.post-head-inner,
.post-body,
.post-figure{
  max-width:856px;                 /* 760 reading column + 48px gutter each side */
  margin-left:0;margin-right:auto;
  padding-left:48px;padding-right:48px;
}
/* One flex row, so the pieces cannot collide. The previous version had the back link as
   inline-block and the category as .eyebrow (inline-flex) inside a block parent, which put
   them side by side on the same line looking like a rendering fault. A flex container makes
   the arrangement explicit instead of leaving it to inline layout. */
.post-crumbs{
  display:flex;align-items:center;flex-wrap:wrap;gap:10px;
  margin-bottom:18px;
}
.post-crumb{
  font-family:var(--sans);font-size:13px;font-weight:600;
  color:var(--muted);text-decoration:none;
  display:inline-flex;align-items:center;gap:6px;
}
.post-crumb:hover{color:var(--brown-dark);}
.post-crumb:focus-visible{outline:2px solid var(--brown);outline-offset:3px;border-radius:2px;}
.post-crumb-cat{
  color:var(--brown-dark);
  letter-spacing:.06em;text-transform:uppercase;font-size:11.5px;font-weight:700;
}
.post-crumb-cat:hover{color:var(--navy);}
.post-crumb-sep{color:var(--border-warm);font-size:13px;}

/* Sized against the reading column, not the viewport. A display-scale headline works on
   a service hero because almost nothing follows it; here it sits directly above 4,000
   words and has to hand over to them rather than shout across them. */
.post-title{
  font-family:var(--heading);font-weight:800;
  font-size:clamp(28px,4.4vw,44px);
  line-height:1.1;letter-spacing:-.022em;
  color:var(--navy);
  margin:0 0 18px;text-wrap:balance;
}
.post-meta{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  font-family:var(--sans);font-size:14px;
  color:var(--muted);
}
.post-meta-dot{color:var(--border-warm);}

/* Fixed 3:2, not height:auto. A featured image is whatever the author uploaded, and a
   portrait one at height:auto rendered 760x1140 here — 1.2x the viewport, pushing the
   first paragraph of every such post below the fold on the template that carries 47% of
   the site's organic traffic. Measured, with a 1024x1536 source.

   3:2 because it matches the service-page hero photography, so a post and a service page
   crop the same way, and because it is the ratio most of this library was shot at. */
.post-figure{margin-top:34px;}
/* The ratio goes on the IMAGE, not the figure — a figure with aspect-ratio squeezes its
   own figcaption inside the box, so the caption gets cropped along with the photo. */
.post-figure img{
  width:100%;display:block;
  aspect-ratio:3 / 2;object-fit:cover;
  border-radius:14px;background:var(--cream-raised);
}
.post-figure figcaption{
  font-family:var(--sans);font-size:13px;color:var(--muted);
  padding:10px 2px 0;
}

/* The hero now sits INSIDE .post-main so the contents sidebar can start beside it rather
   than 833px below the fold — see the note in single.php. The shared rule above still gives
   .post-figure the 856px max-width and 48px gutters it needed as a top-level sibling, and
   inside the column both come from the column instead. Left in place, the gutter doubles up
   against .post-main's own inset and the photo shrinks 760 → 664px, which is exactly what
   the first attempt did. */
.post-main > .post-figure{
  max-width:none;
  padding-left:0;padding-right:0;
}

.post-body{padding-bottom:8px;}
.post-main > .post-content{margin-top:38px;}

/* ── Contents sidebar ──────────────────────────────────────────────────────────────
   The reading column stays 760px because that measures 65 characters per line and the
   readable range is 45-75; filling a 1440px viewport would be 144, roughly double the
   upper bound, on articles past 4,000 words. The column cannot widen — so the ~630px
   beside it gets a tenant instead of staying empty.

   Only appears when a post has 5+ headings. Below that a sidebar is decoration. */
.post-body.has-toc{
  /* 760 prose + 72 gap + 300 contents = 1132, left-aligned at the site's content edge.
     Not 1fr for the second column: that handed the list 520px, far wider than a nav list
     wants, and a 520px-wide contents column competes with the article instead of serving
     it. Fixed 300 leaves ~260px of margin at the far right, down from 632 — enough that
     the composition closes without the sidebar pretending to be content. */
  max-width:1132px;
  display:grid;
  grid-template-columns:760px 300px;
  column-gap:72px;
  align-items:start;
}
.post-body.has-toc .post-main{min-width:0;}   /* lets long code/tables shrink, not overflow */

/* align-self:stretch, and it is what makes the sticky work at all. The grid sets
   align-items:start so the prose column governs the row height; without stretch this
   column collapses to the height of the list itself, and position:sticky inside a box
   with no spare height has nowhere to travel — measured, it simply scrolled away. */
.post-toc{position:relative;align-self:stretch;}
.post-toc-inner{
  position:sticky;top:calc(var(--nav-clear) + 36px);            /* 68px nav + breathing room */
  max-height:calc(100dvh - 140px);
  overflow-y:auto;
  padding:22px 0 22px 26px;
  border-left:1px solid var(--border);
}
.post-toc-label{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;
  color:var(--brown-dark);
  margin:0 0 14px;
}
.post-toc-list{list-style:none;margin:0;padding:0;}
.post-toc-item{margin:0;}
.post-toc-item a{
  display:block;
  font-family:var(--sans);font-size:13.5px;line-height:1.4;
  color:var(--text-2);text-decoration:none;
  padding:6px 0;
  transition:color .16s var(--ease);
}
.post-toc-item a{position:relative;}
.post-toc-item a:hover{color:var(--navy);}
.post-toc-item a:focus-visible{outline:2px solid var(--brown);outline-offset:2px;border-radius:2px;}
.post-toc-l3 a{padding-left:14px;font-size:12.5px;color:var(--muted);}

/* ── Active section (post-toc.js) ───────────────────────────────────────────────────────
   The marker sits ON the rail rather than beside it: .post-toc-inner has padding-left:26px
   over a 1px border, so -27px puts this bar exactly on that border line and it reads as the
   rail thickening at your position, not as a second piece of furniture.

   Colour AND weight change together. Colour alone is a ~2.1:1 shift against the resting
   grey, which is not a reliable signal for anyone with reduced colour vision — the weight
   is what makes it legible, and the bar is what makes it scannable at a glance.

   Without JS none of this ever applies and the list is exactly what it was: working links. */
.post-toc-item a::before{
  content:'';position:absolute;left:-27px;top:5px;bottom:5px;width:2px;
  background:var(--brown);border-radius:1px;
  opacity:0;transform:scaleY(.4);transform-origin:center;
  transition:opacity .18s var(--ease),transform .18s var(--ease);
}
.post-toc-item a.is-active{color:var(--navy);font-weight:600;}
.post-toc-item a.is-active::before{opacity:1;transform:scaleY(1);}
.post-toc-l3 a.is-active{color:var(--text-2);}

@media(prefers-reduced-motion:reduce){
  .post-toc-item a::before{transition:none;transform:none;}
  .post-toc-item a.is-active::before{transform:none;}
}

/* Thin scrollbar so a 27-item list does not put a chunky native bar mid-page. */
.post-toc-inner{scrollbar-width:thin;scrollbar-color:var(--border) transparent;}
.post-toc-inner::-webkit-scrollbar{width:6px;}
.post-toc-inner::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}

/* Anchor targets have to clear the fixed nav or a jump lands under it. */
.post-content :is(h2,h3)[id]{scroll-margin-top:96px;}

@media(max-width:1180px){
  /* Below this the sidebar would squeeze the reading column, which is the one thing it
     exists to protect. The post's own inline Contents list covers navigation. */
  .post-body.has-toc{display:block;max-width:856px;}
  .post-toc{display:none;}
}

/* ── End-of-article CTA ────────────────────────────────────────────────────────────
   No heading in here on purpose — see single.php. Adding one would add a heading to all
   82 posts at once, and this template's diff should stay silent so that when it does
   report something it means it. */
/* ⚠ NAVY, NOT CREAM, AND THE FORM IS WHY.
   This block used to be a cream card holding two buttons, and cream was right for that.
   It now holds the real quote form, and `.fi` is built for a dark ground — background
   rgba(255,255,255,.07), border rgba(255,255,255,.12), color:white. On cream that is white
   text in a 7%-white box: measured, and the fields were completely invisible while every
   DOM check said they were present, 281px wide and opacity 1.
   Navy is also what the form sits on everywhere else it appears (.contact-section is
   var(--navy)), so this is the form's own surface rather than a new one, and the dark panel
   marks the end of the article as a change of register. */
.post-cta{
  margin:56px 0 0;padding:30px 32px;
  background:var(--navy);
  border:1px solid rgba(255,255,255,0.10);
  border-radius:16px;
}
@media(max-width:600px){ .post-cta{padding:26px 22px;} }
.post-cta-lead{
  font-family:var(--heading);font-weight:700;
  font-size:20px;line-height:1.25;color:#fff;
  margin:0 0 8px;text-wrap:balance;
  letter-spacing:var(--track-heading, normal);
}
.post-cta-body{
  font-family:var(--sans);color:rgba(255,255,255,0.78);
  margin:0 0 22px;max-width:52ch;
}
.post-cta-actions{display:flex;flex-wrap:wrap;gap:12px;}
/* "Would rather talk it through? Call …" under the form. The audience that will not type
   a form into a phone will still tap a number, and making them scroll back up to the
   sticky bar to find one is how that lead is lost. */
.post-cta-call{
  font-family:var(--sans);font-size:14px;
  color:rgba(255,255,255,0.72);
  margin:16px 0 0;
}
.post-cta-call a{color:#fff;text-decoration:underline;text-underline-offset:3px;}
.post-cta-call a:hover{color:var(--brown-pale);}

/* ── Prev / next ───────────────────────────────────────────────────────────────── */
.post-nav{
  display:grid;grid-template-columns:1fr 1fr;gap:20px;
  margin-top:48px;padding-top:26px;
  border-top:1px solid var(--border);
}
.post-nav a{
  display:flex;flex-direction:column;gap:5px;
  text-decoration:none;color:var(--navy);
}
.post-nav-next{text-align:right;}
.post-nav-dir{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;color:var(--brown-dark);
}
.post-nav-title{font-family:var(--heading);font-weight:600;font-size:15px;line-height:1.35; letter-spacing:var(--track-heading, normal);}
.post-nav a:hover .post-nav-title{color:var(--brown-dark);}
.post-nav a:focus-visible{outline:2px solid var(--brown);outline-offset:4px;border-radius:4px;}

/* ── RECENT POSTS ──────────────────────────────────────────────────────────────────
   ⚠ PARITY: the h2 text and the item COUNT are fixed by V1 — see single.php. Style it
   freely; do not change what it emits. */
.post-recent{
  margin-top:72px;padding:56px 48px 64px;
  background:var(--cream);
  border-top:1px solid var(--border-warm);
}
.post-recent-inner{max-width:var(--max);margin:0 auto;}
.post-recent-heading{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:.16em;
  color:var(--brown-dark);
  margin:0 0 26px;
}
.post-recent-grid{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:28px;
}
.post-recent-card{
  display:flex;flex-direction:column;gap:14px;
  text-decoration:none;color:var(--navy);
}
.post-recent-media{
  display:block;overflow:hidden;border-radius:12px;
  aspect-ratio:16 / 10;background:var(--cream-raised);
}
.post-recent-media img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .35s var(--ease);}
.post-recent-card:hover .post-recent-media img{transform:scale(1.03);}
.post-recent-title{
  font-family:var(--heading);font-weight:600;
  font-size:18px;line-height:1.3;margin:0;text-wrap:balance;
  letter-spacing:var(--track-heading, normal);
}
.post-recent-card:hover .post-recent-title{color:var(--brown-dark);}
.post-recent-card:focus-visible{outline:2px solid var(--brown);outline-offset:4px;border-radius:6px;}
.post-recent-date{font-family:var(--sans);font-size:13px;color:var(--muted);}

@media(max-width:768px){
  /* 24px to match .section's own mobile gutter — the post columns are separate elements
     from .section, so they need the breakpoint stated rather than inherited. */
  .post-head{padding:calc(var(--nav-clear) + 30px) 0 0;}
  .post-head-inner,
  .post-body,
  .post-figure{padding-left:24px;padding-right:24px;max-width:808px;}
  .post-cta{padding:24px 22px;border-radius:14px;}
  .post-cta-actions .btn{flex:1 1 100%;justify-content:center;text-align:center;}
  .post-nav{grid-template-columns:1fr;gap:22px;}
  .post-nav-next{text-align:left;}
  .post-recent{margin-top:52px;padding:40px 20px 48px;}
  .post-recent-grid{grid-template-columns:1fr;gap:24px;}
}
@media(prefers-reduced-motion:reduce){
  .post-recent-media img{transition:none;}
}

/* ══ BLOG — index / archive (index.php) ═════════════════════════════════════════════
   Replaces V1's /northco-journals/.

   Designed against its measured job rather than its apparent one. This page has ZERO
   GSC impressions (it is not in the sitemap) and took 42 GA4 sessions with 0
   conversions in six months, while the posts beneath it carry 47% of the site's
   organic clicks. So it is not an acquisition surface — it is a crawlable hub and a
   second click, and the design serves browsing, not landing.

   Which is why there is no photographic hero and no illustration: nothing here needs
   to sell, and a big header would only push the actual content — the posts — further
   down the one page whose whole purpose is to list them.
   ─────────────────────────────────────────────────────────────────────────────────── */
.jn-head{padding:calc(var(--nav-clear) + 52px) 48px 0;}
.jn-head-inner,
.jn-filters-inner,
.jn-list-inner{max-width:var(--max);margin:0 auto;}
.jn-title{
  font-family:var(--heading);font-weight:800;
  font-size:clamp(30px,5vw,52px);
  line-height:1.06;letter-spacing:-.026em;
  color:var(--navy);
  margin:14px 0 0;text-wrap:balance;
}
.jn-strap{max-width:56ch;margin-top:16px;}
.jn-strap p{
  font-family:var(--sans);font-size:clamp(16px,1.7vw,19px);
  line-height:1.55;color:var(--text-2);margin:0;text-wrap:pretty;
}

/* ── Category chips ────────────────────────────────────────────────────────────────
   Real anchors to real archives, so they work without JS and each is indexable.
   Horizontally scrollable on a phone rather than wrapping to four rows and pushing the
   grid off screen. */
.jn-filters{padding:36px 48px 0;}
.jn-filters-inner{
  display:flex;gap:8px;flex-wrap:wrap;
  padding-bottom:4px;
}
.jn-chip{
  display:inline-flex;align-items:center;gap:7px;
  font-family:var(--sans);font-size:13px;font-weight:600;
  color:var(--text-2);text-decoration:none;
  padding:9px 15px;border-radius:999px;
  border:1px solid var(--border);
  background:transparent;
  transition:border-color .18s var(--ease), color .18s var(--ease), background .18s var(--ease);
  white-space:nowrap;
}
.jn-chip:hover{border-color:var(--brown);color:var(--navy);}
.jn-chip:focus-visible{outline:2px solid var(--brown);outline-offset:2px;}
.jn-chip.is-on{
  background:var(--navy);border-color:var(--navy);color:#fff;
}
.jn-chip-n{
  font-size:11px;font-variant-numeric:tabular-nums;
  color:var(--muted);
}
.jn-chip.is-on .jn-chip-n{color:rgba(255,255,255,.62);}
.jn-chip:hover .jn-chip-n{color:var(--brown-dark);}

/* ── Grid ──────────────────────────────────────────────────────────────────────── */
.jn-list{padding:36px 48px 88px;}
.jn-grid{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:44px 32px;
}
.jn-card-link{
  display:flex;flex-direction:column;gap:14px;
  text-decoration:none;color:var(--navy);height:100%;
}
.jn-card-media{
  display:block;overflow:hidden;border-radius:12px;
  aspect-ratio:16 / 10;background:var(--cream-raised);
}
.jn-card-media img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .4s var(--ease);
}
.jn-card-link:hover .jn-card-media img{transform:scale(1.035);}
.jn-card-link:focus-visible{outline:2px solid var(--brown);outline-offset:5px;border-radius:8px;}
.jn-card-text{display:flex;flex-direction:column;gap:9px;}
.jn-card-meta{
  display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  font-family:var(--sans);font-size:12.5px;color:var(--muted);
}
.jn-card-tag{
  font-size:10.5px;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;
  color:var(--brown-dark);
  background:var(--cream-raised);
  padding:4px 9px;border-radius:3px;
}
.jn-dot{color:var(--border-warm);}
.jn-card-title{
  font-family:var(--heading);font-weight:600;
  font-size:19px;line-height:1.28;letter-spacing:var(--track-heading, -.008em);
  margin:0;text-wrap:balance;
}
.jn-card-link:hover .jn-card-title{color:var(--brown-dark);}
.jn-card-excerpt{
  font-family:var(--sans);font-size:14.5px;line-height:1.55;
  color:var(--text-2);text-wrap:pretty;
}

/* Lead card — spans the grid and runs side by side. Earns the space from the data:
   post performance on this site ranges from 860 clicks to single digits, so the page
   should say which one to read. */
.jn-card-lead{grid-column:1 / -1;}
.jn-card-lead .jn-card-link{
  display:grid;grid-template-columns:1.35fr 1fr;
  gap:0 40px;align-items:center;
}
.jn-card-lead .jn-card-media{aspect-ratio:16 / 9;border-radius:14px;}
.jn-card-lead .jn-card-title{font-size:clamp(24px,2.7vw,34px);line-height:1.15;font-weight:700;}
.jn-card-lead .jn-card-excerpt{font-size:16px;}
.jn-card-lead .jn-card-text{gap:12px;}

/* ── Pagination ────────────────────────────────────────────────────────────────── */
.jn-list .pagination{margin-top:64px;padding-top:30px;border-top:1px solid var(--border);}
.jn-list .nav-links{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
.jn-list .page-numbers{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:40px;height:40px;padding:0 12px;
  font-family:var(--sans);font-size:14px;font-weight:600;
  font-variant-numeric:tabular-nums;
  color:var(--text-2);text-decoration:none;
  border:1px solid var(--border);border-radius:8px;
  transition:border-color .18s var(--ease), color .18s var(--ease);
}
.jn-list .page-numbers:hover{border-color:var(--brown);color:var(--navy);}
.jn-list .page-numbers:focus-visible{outline:2px solid var(--brown);outline-offset:2px;}
.jn-list .page-numbers.current{background:var(--navy);border-color:var(--navy);color:#fff;}
.jn-list .page-numbers.dots{border-color:transparent;}
.jn-list .screen-reader-text{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;
}
.jn-empty{
  font-family:var(--sans);color:var(--text-2);
  padding:20px 0 40px;
}

@media(max-width:1024px){
  .jn-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:36px 28px;}
}
@media(max-width:768px){
  .jn-head{padding:calc(var(--nav-clear) + 32px) 24px 0;}
  .jn-filters{padding:26px 0 0;}
  /* Scroll rather than wrap — four rows of chips would push the first post off screen
     on the page whose entire job is listing posts. */
  .jn-filters-inner{
    flex-wrap:nowrap;overflow-x:auto;
    padding:0 24px 6px;
    scrollbar-width:none;
  }
  .jn-filters-inner::-webkit-scrollbar{display:none;}
  .jn-list{padding:28px 24px 64px;}
  .jn-grid{grid-template-columns:1fr;gap:34px;}
  /* The lead card stacks — a 1.35fr/1fr split at 375px gives neither side enough room. */
  .jn-card-lead .jn-card-link{grid-template-columns:1fr;gap:14px;}
  .jn-card-lead .jn-card-media{aspect-ratio:16 / 10;}
  .jn-card-lead .jn-card-title{font-size:22px;}
  .jn-card-lead .jn-card-excerpt{font-size:14.5px;}
}
@media(prefers-reduced-motion:reduce){
  .jn-card-media img,.jn-chip,.jn-list .page-numbers{transition:none;}
}

/* ══ PORTFOLIO ═════════════════════════════════════════════════════════════════════
   53 projects, each already carrying a name, a project type and 5-38 photographs in
   post_content — and V1 rendered all of that as one heading, six words and a lightbox.
   GSC: 51 portfolio URLs at an average position of 16.6 with THREE clicks between them.
   Good positions arriving at nothing. The photographs are the asset; this gives them a
   layout.
   ─────────────────────────────────────────────────────────────────────────────────── */

/* ── Index ─────────────────────────────────────────────────────────────────────── */
/* The bottom padding is the gap between the strap and whatever comes next — the switcher,
   or the grid directly when a family has one type or no projects. It lives on the head
   rather than on the switcher so every branch gets it. */
.pf-head{padding:calc(var(--nav-clear) + 52px) 48px 40px;}
.pf-head-inner{max-width:var(--max);margin:0 auto;}
.pf-title{
  font-family:var(--heading);font-weight:800;
  font-size:clamp(30px,5vw,52px);line-height:1.06;letter-spacing:-.026em;
  color:var(--navy);margin:14px 0 0;text-wrap:balance;
}
.pf-strap{
  font-family:var(--sans);font-size:clamp(16px,1.7vw,19px);line-height:1.55;
  color:var(--text-2);margin:16px 0 0;max-width:56ch;text-wrap:pretty;
}


/* ── PORTFOLIO FRONT PAGE, THE 2026-09-23 CONCEPT ──────────────────────────────────────────
   page-portfolio.php has the why. Hero, an inked statement, a twelve-tile mosaic, the family cards
   (the .pf-choose rules below), one before and after, the estimate. Motion is portfolioMotion() in
   motion.js: transform and opacity on the tiles, colour on the statement's words, nothing under
   reduced motion, and every state readable without the script. Revert with the commit. */

/* HERO: one real project, full-bleed, text on a bottom scrim. The photo is the page's LCP, so it is
   eager with fetchpriority=high in the template. */
.pf-hero{
  position:relative;isolation:isolate;overflow:hidden;
  min-height:min(88vh, 820px);
  display:flex;flex-direction:column;justify-content:flex-end;
  padding:calc(var(--nav-clear) + 40px) 48px 56px;
  background:var(--navy);color:#fff;
}
/* The scrim carries the type. Bottom-heavy, because the doors sit there too. */
.pf-hero-shade{
  position:absolute;inset:0;z-index:-1;
  background:linear-gradient(180deg, rgba(11,8,42,.28) 0%, rgba(11,8,42,0) 30%, rgba(11,8,42,.25) 52%, rgba(11,8,42,.86) 100%);
}
.pf-hero-inner{max-width:var(--max);width:100%;margin:0 auto;}
/* White over the photographs (brown read faint there, 2026-09-23); the dash keeps the brand colour. */
.pf-hero .eyebrow{color:rgba(255,255,255,.94);text-shadow:0 1px 14px rgba(11,8,42,.7);}
.pf-hero .eyebrow::before{background:var(--brown-light, #e7c9a2);}
.pf-hero-title{
  font-family:var(--heading);font-weight:800;color:#fff;
  font-size:clamp(40px,7.2vw,96px);line-height:1;letter-spacing:-.03em;
  margin:0;max-width:12ch;text-wrap:balance;
  text-shadow:0 2px 30px rgba(11,8,42,.35);
}
.pf-hero-strap{
  font-family:var(--sans);font-size:clamp(16px,1.6vw,20px);line-height:1.5;
  color:rgba(255,255,255,.9);margin:18px 0 0;max-width:46ch;
}
/* THE THREE PANELS (Jesse: "a cool blend of our three business units"). Each family's photograph, side
   by side, each panel the link to its family, so the doors are the picture. Every panel carries its own
   scrim (::after) so its label sits ABOVE the shading; the headline floats over all three with
   pointer-events:none, so a click anywhere on a photograph goes to that family. On a desktop pointer the
   panel you are on widens a little. */
/* ⚠ THE HEADLINE SITS BELOW THE PICKER, NOT ON IT. Jesse, 2026-09-23: "the proof is in the projects now
   kinda fights the interactive part". So the three panels come first, full width under the nav, and
   the eyebrow, h1 and strap follow on the page's cream. With nothing laid over them, each panel's scrim
   only has to carry its own label. (On a phone the headline comes first instead, above the bands; see
   the 900px block.) */
.pf-hero--tri .pf-hero-shade{display:none;}
.pf-hero--tri{
  min-height:0;justify-content:flex-start;
  padding:var(--nav-clear) 0 0;
  background:var(--cream);color:var(--navy);
}
/* The gutter OUTSIDE the 1440 box, as every section below has it (.pf-show is padding 48px around a
   max-width inner), so the headline's left edge meets theirs at any width. */
.pf-hero--tri .pf-hero-inner{position:relative;z-index:1;order:1;width:min(var(--max), calc(100% - 96px));padding:clamp(28px,3.4vw,48px) 0 0;}
.pf-hero--tri .pf-hero-title{color:var(--navy);text-shadow:none;font-size:clamp(40px,6vw,84px);max-width:none;}
.pf-hero--tri .pf-hero-strap{color:var(--text-2);}
.pf-hero--tri .eyebrow{color:var(--brown);text-shadow:none;}
.pf-hero--tri .eyebrow::before{background:var(--brown);}
.pf-tri{position:relative;z-index:0;display:flex;gap:3px;height:min(64vh,640px);background:var(--cream);}
.pf-tri-panel{
  position:relative;flex:1 1 0;min-width:0;overflow:hidden;display:block;
  color:#fff;text-decoration:none;
  transition:flex-grow .6s var(--ease-reveal);
}
.pf-tri-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform .8s var(--ease);}
.pf-tri-panel::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(180deg, rgba(11,8,42,0) 58%, rgba(11,8,42,.5) 100%);
}
.pf-tri-label{
  position:absolute;left:24px;bottom:24px;z-index:2;
  display:inline-flex;align-items:center;gap:10px;min-height:44px;
  padding:10px 18px;border-radius:999px;
  font-family:var(--heading);font-weight:700;font-size:15px;letter-spacing:-.005em;
  color:var(--navy);background:#fff;
  transition:background-color .25s var(--ease), color .25s var(--ease);
}
.pf-tri-panel:hover .pf-tri-label{background:var(--brown);color:#fff;}
.pf-tri-panel:focus-visible{outline:3px solid var(--brown-light);outline-offset:-6px;}
@media(hover:hover) and (min-width:901px){
  .pf-tri:hover .pf-tri-panel{flex-grow:.8;}
  .pf-tri .pf-tri-panel:hover{flex-grow:1.5;}
  .pf-tri-panel:hover .pf-tri-img{transform:scale(1.03);}
}

/* STATEMENT: big type whose words ink from faint to solid as the line reaches the reading line.
   The faint state only exists under .pf-ink-on, which the script adds; without it the line is
   simply solid. --ink is written per word, 0..1. */
.pf-say{padding:clamp(72px,10vw,140px) 48px clamp(40px,6vw,80px);}
.pf-say-inner{max-width:var(--max);margin:0 auto;}
.pf-say-line{
  font-family:var(--heading);font-weight:700;color:var(--navy);
  font-size:clamp(28px,4.6vw,64px);line-height:1.12;letter-spacing:-.025em;
  margin:0;text-wrap:balance;   /* no 20ch cap since 2026-10-07: the line breaks after "exteriors" (page-portfolio.php) */
}
.pf-ink-on .pf-say-w{opacity:calc(.16 + .84 * var(--ink, 0));}
.pf-say-w--accent{color:var(--brown);}
.pf-say-keep{white-space:nowrap;}   /* "across Ottawa." stays together on a phone (2026-10-07) */   /* "Kitchens, bathrooms and exteriors" (2026-10-07) */

/* MOSAIC: four columns, wide tiles span two, fixed row heights so a row of mixed tiles is one
   clean band. dense packing fills the gaps the phone's two columns would otherwise leave. */
.pf-show{padding:0 48px clamp(72px,9vw,120px);}
.pf-show-inner{max-width:var(--max);margin:0 auto;}
.pf-show-head{margin-bottom:28px;}
.pf-show-title{margin:0;}
.pf-mosaic{
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  grid-auto-rows:clamp(220px,22vw,330px);grid-auto-flow:dense;gap:14px;
}
.pf-tile{
  position:relative;display:block;overflow:hidden;border-radius:14px;
  background:var(--cream-raised, #efe8dc);text-decoration:none;
}
.pf-tile--wide{grid-column:span 2;}
.pf-tile-media{position:absolute;inset:0;display:block;}
.pf-tile-media img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .6s var(--ease);}
.pf-tile:hover .pf-tile-media img{transform:scale(1.04);}
.pf-tile::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg, rgba(11,8,42,0) 42%, rgba(11,8,42,.8) 100%);
}
/* The shadow is for the bright photographs (Fraser's leaves, Redbridge's white kitchen), where a two-line
   type label climbs above the dark end of the scrim on a phone. */
.pf-tile-cap{
  position:absolute;left:16px;right:16px;bottom:14px;z-index:1;
  display:flex;flex-direction:column;gap:2px;color:#fff;
  text-shadow:0 1px 10px rgba(11,8,42,.6), 0 0 2px rgba(11,8,42,.35);
}
.pf-tile-type{font-family:var(--sans);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:rgba(255,255,255,.82);}
.pf-tile-name{font-family:var(--heading);font-weight:700;font-size:clamp(16px,1.5vw,20px);letter-spacing:var(--track-heading, -.01em);}
.pf-tile:focus-visible{outline:3px solid var(--brown);outline-offset:3px;}
/* The slide-in. Only under .pf-tiles-on (the script's), so no tile is ever hidden without it. */
.pf-tiles-on .pf-tile{opacity:0;transform:translateX(var(--pf-dx, 0));}
.pf-tiles-on .pf-tile[data-pf-from="l"]{--pf-dx:-44px;}
.pf-tiles-on .pf-tile[data-pf-from="r"]{--pf-dx:44px;}
.pf-tiles-on .pf-tile.is-in{
  opacity:1;transform:none;
  transition:opacity .75s var(--ease-reveal), transform .75s var(--ease-reveal);
}

/* BEFORE AND AFTER: navy band, copy beside a labelled pair. */
.pf-ba{background:var(--navy);color:#fff;padding:clamp(64px,8vw,112px) 48px;}
.pf-ba-inner{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(28px,5vw,72px);align-items:center;
}
.pf-ba .eyebrow{color:var(--brown-light, #e7c9a2);}
.pf-ba .eyebrow::before{background:var(--brown-light, #e7c9a2);}
.pf-ba-title{color:#fff;margin:0;text-wrap:balance;}
.pf-ba-text{color:rgba(255,255,255,.82);font-size:17px;line-height:1.6;margin:16px 0 0;max-width:40ch;}
.pf-ba-links{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px;margin-top:28px;}
.pf-ba-link{color:#fff;font-weight:600;text-decoration:underline;text-underline-offset:4px;}
/* The pair is the site's drag wipe since 2026-10-07 (a Kitchen Update, page-portfolio.php); on navy its frame drops the warm
   hairline the cream bands give it. The side-by-side pair and the closing prompt it replaced are gone, and so are their
   rules (.pf-ba-pair, .pf-ba-fig, .pf-close*). */
.pf-ba-copy{min-width:0;}   /* a grid item: lets the copy column shrink rather than push the wipe */
.pf-ba-wipe .tp-cmp-wipe{border-color:transparent;}

@media(max-width:900px){
  .pf-hero{min-height:86svh;padding:calc(var(--nav-clear) + 28px) 24px 64px;}
  /* ⚠ ON A PHONE THE THREE STACK AS LANDSCAPE BANDS, UNDER THE HEADLINE. Side by side they were ~128px
     slices of landscape photographs: each showed a sliver, and a sharp sliver at 86svh tall needs the
     2048px file three times over on the first screen (measured 2026-09-23: the 300px copies loaded and
     rendered soft). Stacked full-width bands crop a landscape photo the way it was shot, need about a
     1024-1536px copy each, and read as three plain doors. The headline moves above them, on the page's
     cream, where it is legible without a scrim and is the first thing painted. */
  /* min-height:0 again, as above the breakpoint: .pf-hero's 86svh just above comes later and wins, which at 390x844 is
     shorter than the content and does nothing, but on a portrait tablet left 100-240px of blank cream under the three
     bands (found by the mobile review, 2026-10-07, when a 2000px-tall capture made it 1,000px). */
  .pf-hero--tri{padding:calc(var(--nav-clear) + 24px) 0 0;min-height:0;}
  .pf-hero--tri .pf-hero-inner{order:-1;width:calc(100% - 48px);padding:0 0 22px;}
  /* 54svh: the sticky Free Estimate bar is on screen from the first paint (its top at 768 of 844), and
     at 66svh, then 58, the third band's label still reached under it (measured 2026-09-23, 390x844). */
  .pf-tri{position:relative;inset:auto;flex-direction:column;gap:3px;height:min(54svh,480px);background:var(--cream);}
  .pf-tri-panel::after{background:linear-gradient(90deg, rgba(11,8,42,.55) 0%, rgba(11,8,42,.1) 55%, rgba(11,8,42,0) 100%);}
  .pf-tri-label{left:16px;bottom:14px;min-height:40px;padding:8px 14px;font-size:13px;}
  .pf-say{padding:64px 24px 32px;}
  .pf-show{padding:0 24px 72px;}
  .pf-mosaic{grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:44vw;gap:10px;}
  .pf-tile-cap{left:12px;right:12px;bottom:10px;}
  .pf-tile::after{background:linear-gradient(180deg, rgba(11,8,42,0) 30%, rgba(11,8,42,.84) 100%);}
  .pf-tile-type{font-size:10px;letter-spacing:.1em;}
  .pf-tiles-on .pf-tile[data-pf-from="l"]{--pf-dx:-24px;}
  .pf-tiles-on .pf-tile[data-pf-from="r"]{--pf-dx:24px;}
  .pf-ba{padding:64px 24px;}
  .pf-ba-inner{grid-template-columns:1fr;}
}
@media(prefers-reduced-motion:reduce){
  .pf-tile-media img,.pf-tri-panel,.pf-tri-img,.pf-tri-label{transition:none;}
}

/* ── Chooser ───────────────────────────────────────────────────────────────────────
   V1's structure kept: /portfolio/ asks which kind of work you want rather than listing
   everything. Someone who arrived for siding should not scroll past kitchens.

   This is the one place in the portfolio where type still sits ON a photograph. It earns
   it: these are three large display-scale labels acting as buttons, not 11px captions, so
   they survive a much lighter scrim than the project tiles ever could. */
.pf-choose{padding:44px 48px 96px;}
.pf-choose-inner{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:24px;
}
.pf-choice{
  position:relative;display:block;overflow:hidden;
  border-radius:18px;text-decoration:none;
  background:var(--cream-raised);
  aspect-ratio:4 / 5;
}
.pf-choice-media{position:absolute;inset:0;display:block;}
.pf-choice-media img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .55s var(--ease);
}
.pf-choice:hover .pf-choice-media img{transform:scale(1.05);}
/* ── TWO LAYERS: A LEGIBILITY SCRIM, AND A HOVER TINT ─────────────────────────────────────
   Jesse: "less blue tint for the four heros... more akin to the homepage three hero tint -
   where it's light but not insane, and blue tints more when highlighted".

   ⚠ THE HOMEPAGE'S TREATMENT CANNOT BE COPIED LITERALLY, and that is the whole design
   problem here. .svc-card is rgba(10,8,30,0) at rest — no tint at all — because its text sits
   BELOW the photograph (measured: heading top 3362, photo bottom 3309). Nothing has to stay
   readable over the image, so nothing has to be darkened.

   These cards put the text ON the photograph, starting 29% down with the heading at 37%. Drop
   the scrim to zero and white type lands on a bright kitchen. So the SHAPE is borrowed, not
   the values: light where the photo should show, dark only where words sit.

   What changed: the old gradient was already .11 at the very top and .27 by 34% — tinting the
   whole upper half of an image nobody needed to read anything over. That is the blue cast. It
   now starts fully transparent and stays under .03 until 28%, then ramps only where the words
   are.

   ⚠ EVERY STOP IS LIGHTER THAN BEFORE, INCLUDING THE BOTTOM. A first pass lightened the top
   but pushed the lower half DARKER (.646 to .70 at 68%), which is the opposite of "a bit less
   tint" — Jesse asked for less, not less-in-one-place-and-more-in-another. Measured against
   the old curve at every depth: 0% .11 to 0 · 28% .24 to .03 · 37% .31 to .20 · 50% .46 to
   .38 · 68% .65 to .56 · 100% .84 to .76.

   ⚠ WHAT ACTUALLY CARRIES LEGIBILITY IS THE TEXT-SHADOW, NOT THE SCRIM, which is what makes
   this safe. .pf-choice-name has a 24px halo at .55 and .pf-choice-blurb a 12px at .7 (see
   below). Run the numbers on the worst case — the Cabinetry Refacing card, white cabinets
   directly behind the heading — and white-on-photo fails a 3:1 contrast check at the OLD .31
   just as it does at .20. The scrim was never what made that text readable; the halo is. So
   dropping it a third costs nothing that was there to lose. */
.pf-choice::after{
  content:'';position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,
    rgba(11,8,42,0)   0%,
    rgba(11,8,42,.03) 28%,
    rgba(11,8,42,.30) 42%,
    rgba(11,8,42,.56) 68%,
    rgba(11,8,42,.76) 100%);
}

/* The hover tint, as a SEPARATE FLAT LAYER. ⚠ It has to be separate because a gradient's
   colour stops are not interpolable in most engines — transitioning background-image between
   two gradients jumps rather than fades. The homepage solves it the same way, by animating a
   flat rgba instead, and this matches its 0 -> .28 and its 0.4s. */
.pf-choice::before{
  content:'';position:absolute;inset:0;z-index:1;
  background:rgba(11,8,42,0);
  transition:background .4s var(--ease);
}
/* .24 rather than the homepage's .28, so that even hovered the card stays lighter than the
   old resting state: .76 base composited with .24 lands at .82 against the old .84. */
.pf-choice:hover::before,
.pf-choice:focus-visible::before{ background:rgba(11,8,42,.24); }

@media(prefers-reduced-motion:reduce){
  .pf-choice::before{ transition:none; }
}
.pf-choice-body{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
  padding:30px 30px 28px;
  display:flex;flex-direction:column;gap:8px;
}
.pf-choice-name{
  font-family:var(--heading);font-weight:800;color:#fff;
  font-size:clamp(28px,3.2vw,40px);line-height:1.05;letter-spacing:-.024em;
  margin:0;
  text-shadow:0 2px 24px rgba(11,8,42,.55);
}
/* A line about the service, not a project count. A count here would read "34 kitchens,
   0 bathrooms" and sell one service by undercutting another. */
.pf-choice-blurb{
  font-family:var(--sans);font-size:14.5px;line-height:1.45;
  color:rgba(255,255,255,.88);
  max-width:34ch;text-wrap:pretty;
  text-shadow:0 1px 12px rgba(11,8,42,.7);
}
.pf-choice-meta{
  display:flex;align-items:center;gap:10px;
  margin-top:4px;
  font-family:var(--sans);font-size:13px;font-weight:700;
  letter-spacing:.04em;
  color:rgba(255,255,255,.92);
}
.pf-choice-arrow{transition:transform .25s var(--ease);}
.pf-choice:hover .pf-choice-arrow{transform:translateX(4px);}
.pf-choice:focus-visible{outline:3px solid var(--brown);outline-offset:3px;}

/* ── Type switcher ─────────────────────────────────────────────────────────────────
   Sits at the top of a category page; the selected type's projects sit underneath. Real
   links, one per type, so each state is a URL and it works with no JavaScript.

   Deliberately NOT a row of compact chips. Each option carries its label, a count, and
   V1's one-line explanation of what the type means — because the person choosing between
   painting, updating and renovating is often the person who does not yet know the
   difference. Room for that sentence is the whole point of the control. */
.pf-switch{padding:0 48px 40px;}
.pf-switch-inner{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;
}
.pf-switch-opt{
  display:flex;flex-direction:column;gap:6px;
  padding:18px 20px;
  border:1.5px solid var(--border);border-radius:14px;
  background:#fff;text-decoration:none;color:var(--navy);
  transition:border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
  min-height:44px;
}
.pf-switch-opt:hover{border-color:var(--brown);transform:translateY(-1px);}
.pf-switch-opt:focus-visible{outline:2px solid var(--brown);outline-offset:3px;}
/* The selected type is unmistakable — navy fill, not just a darker border. At a glance,
   on a phone, "which one am I looking at" should not need reading. */
.pf-switch-opt.is-on{
  background:var(--navy);border-color:var(--navy);color:#fff;
}
.pf-switch-opt.is-on:hover{transform:none;border-color:var(--navy);}
.pf-switch-label{
  display:flex;align-items:center;gap:8px;
  font-family:var(--heading);font-weight:700;font-size:16px;line-height:1.2;
}
.pf-switch-n{
  font-family:var(--sans);font-size:12px;font-weight:600;
  font-variant-numeric:tabular-nums;color:var(--muted);
}
.pf-switch-opt.is-on .pf-switch-n{color:rgba(255,255,255,.66);}
.pf-switch-blurb{
  font-family:var(--sans);font-size:13.5px;line-height:1.45;
  color:var(--text-2);text-wrap:pretty;
}
.pf-switch-opt.is-on .pf-switch-blurb{color:rgba(255,255,255,.86);}

/* Back to the chooser, from a category page. */
.pf-back{
  display:inline-block;margin-bottom:16px;
  font-family:var(--sans);font-size:13px;font-weight:600;
  color:var(--muted);text-decoration:none;
}
.pf-back:hover{color:var(--brown-dark);}
.pf-back:focus-visible{outline:2px solid var(--brown);outline-offset:3px;border-radius:2px;}

/* Three across. Two felt like a slideshow at this tile size, and the caption sitting
   below the photograph means a narrower column still reads — the name no longer has to
   compete with the image behind it. */
/* Gutters on the OUTER (.pf-group), max-width on the INNER (.pf-list) — the same split as
   .pf-head/.pf-head-inner and .pf-switch/.pf-switch-inner. Both on one element, under
   border-box, insets the content by the gutter: the tiles sat 48px right of the h1 on wide
   screens (the cap binds from 1536px: --max 1440px plus both gutters) and by a viewport-
   dependent fraction between 1440 and 1536px — hence "not always". */
.pf-list{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:40px 28px;
}
/* The type tabs switch in place (assets/js/portfolio-switch.js; Jesse: "switching between these should be seamless").
   The chosen tab lights on the tap. The grid stays as it is until the next tab's cards and on-screen photographs are
   ready, then sweeps in (below), never through a fade: a grid dimmed over this light page, and cards shown before their
   photographs, read as a wash (Jesse, 2026-09-28: "there's a visible white shade as they load in"). A wait long enough
   to notice (0.3s) fades in a line moving along the bottom of the chosen tab; a fade, so a wait that ends just after
   it starts doesn't flash. The line a screen reader announces ("Kitchen Update Portfolio: 9 projects") is hidden
   from view. */
.pf-switch-opt{ position:relative;overflow:hidden; }
.pf-switch.is-loading .pf-switch-opt.is-on::after{
  content:"";position:absolute;left:0;bottom:0;width:40%;height:3px;
  background:rgba(255,255,255,.6);
  animation:pf-loading 1.1s ease-in-out infinite, pf-fade-in .3s ease-out both;
}
@keyframes pf-loading{ from{ transform:translateX(-100%); } to{ transform:translateX(250%); } }
@keyframes pf-fade-in{ from{ opacity:0; } }
/* The sweep. Jesse chose it from five options side by side, 2026-09-28 ("I like C"). The script lays the old grid over
   the new one (.pf-list-leaving, placed and coloured inline), and this carries it away left to right behind a soft edge
   a fifth of the grid wide: the team photos' move on About, quicker off the mark. The mask is 2.5 grids wide, so 80% to
   0 takes the edge from just off the left side to just off the right. */
.pf-group{ position:relative; }
.pf-list-leaving{
  position:absolute;margin:0;pointer-events:none;
  -webkit-mask-image:linear-gradient(90deg,transparent 40%,#000 48%);mask-image:linear-gradient(90deg,transparent 40%,#000 48%);
  -webkit-mask-size:250% 100%;mask-size:250% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  animation:pf-sweep .55s cubic-bezier(.4,0,.2,1) both;
}
@keyframes pf-sweep{ from{ -webkit-mask-position:80% 0;mask-position:80% 0; } to{ -webkit-mask-position:0 0;mask-position:0 0; } }
.pf-group .screen-reader-text{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){ .pf-switch.is-loading .pf-switch-opt.is-on::after{ animation:none;width:100%;opacity:.5; } }

/* ── Project tile ──────────────────────────────────────────────────────────────────
   CAPTION BELOW THE PHOTOGRAPH, not over it — and that is a legibility decision before
   it is an aesthetic one.

   Overlaid white type needs a scrim heavy enough to guarantee contrast on any photo,
   which is what turned these tiles into blue rectangles: the eyebrow measured 1.59:1
   until the scrim went to ~62%, at which point the photograph is barely visible. Small
   light text on a busy image is also precisely what a 40-65 audience finds hardest, and
   85% of this site's visitors are reading it on a phone.

   Below the image there is no trade-off left to make. Navy on cream clears AA without
   thinking about it, the photograph carries no tint at all, and the tile stops competing
   with the thing it is advertising. */
.pf-item{
  display:flex;flex-direction:column;gap:14px;
  text-decoration:none;color:var(--navy);
}
.pf-item-media{
  display:block;overflow:hidden;border-radius:14px;
  aspect-ratio:4 / 3;background:var(--cream-raised);
}
.pf-item-media img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .5s var(--ease);
}
.pf-item:hover .pf-item-media img{transform:scale(1.04);}
.pf-item:focus-visible{outline:3px solid var(--brown);outline-offset:4px;border-radius:8px;}
.pf-item-body{display:flex;flex-direction:column;gap:4px;}
.pf-item-name{
  font-family:var(--heading);font-weight:700;
  font-size:20px;line-height:1.2;letter-spacing:var(--track-heading, -.012em);
  color:var(--navy);
}
.pf-item:hover .pf-item-name{color:var(--brown-dark);}
.pf-item-meta{
  font-family:var(--sans);font-size:13.5px;
  color:var(--muted);
}
.pf-item-dot{color:var(--border-warm);margin:0 3px;}

/* ── Groups within a category ──────────────────────────────────────────────────────
   V1 segmented twice and the second level is the one a homeowner needs: 34 kitchens in
   one undifferentiated grid is not a portfolio, it is a pile. Painting, updating and
   renovating answer different questions, so they get their own sections with V1's own
   explainer copy. */
.pf-group{padding:0 48px 56px;}
/* Cleared so a jump link does not land the heading under the fixed nav. */
.pf-group{scroll-margin-top:96px;}

/* ── Jump links ────────────────────────────────────────────────────────────────────
   Plain anchors, big enough to hit with a thumb. One tap from "I want cabinet painting"
   to cabinet painting, instead of scrolling past seventeen renovations. */
.pf-jump{padding:8px 48px 34px;}
.pf-jump-inner{
  max-width:var(--max);margin:0 auto;
  display:flex;flex-wrap:wrap;gap:10px;
}
.pf-jump-link{
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--sans);font-size:14px;font-weight:600;
  color:var(--navy);text-decoration:none;
  padding:12px 18px;border-radius:999px;
  border:1px solid var(--border);background:#fff;
  transition:border-color .18s var(--ease), background .18s var(--ease);
  min-height:44px;                 /* thumb target */
  /* Never wrap. A pill with a 999px radius that wraps to four lines does not become a
     taller pill, it becomes a CIRCLE — measured at 116px on a phone, three of them filling
     the screen. "Cabinet Painting & Refacing" is 27 characters and will always want to
     wrap in a 375px column, so the row scrolls sideways instead. */
  white-space:nowrap;
}
.pf-jump-link:hover{border-color:var(--brown);background:var(--cream);}
.pf-jump-link:focus-visible{outline:2px solid var(--brown);outline-offset:2px;}
.pf-jump-n{
  font-size:12px;font-variant-numeric:tabular-nums;
  color:var(--muted);
}

.pf-empty{
  grid-column:1 / -1;
  font-family:var(--sans);color:var(--text-2);
  background:var(--cream);border:1px solid var(--border-warm);
  border-radius:16px;padding:32px 34px;
  max-width:640px;
}
.pf-empty-lead{
  font-family:var(--heading);font-weight:700;font-size:20px;
  color:var(--navy);margin:0 0 8px;
  letter-spacing:var(--track-heading, normal);
}
.pf-empty p{margin:0 0 18px;}
.pf-empty p:last-child{margin-bottom:0;}
.pf-empty-actions{display:flex;flex-wrap:wrap;gap:12px;}

/* ── One project ───────────────────────────────────────────────────────────────── */
.pfp-hero{
  position:relative;overflow:hidden;
  margin-top:var(--nav-clear);                     /* clears the fixed nav */
  min-height:min(78dvh, 720px);
  display:flex;align-items:flex-end;
  padding:110px 48px 48px;
  background:var(--navy-dark);
}
.pfp-hero-media{position:absolute;inset:0;}
.pfp-hero-media img{width:100%;height:100%;object-fit:cover;display:block;}
/* The service heroes' slow zoom (svcc-hero-zoom, THE SLOW ZOOM above), on Jesse's call, 2026-09-28: "I want that same
   slow zoom in when you click into a project's portfolio … zooms to a max then slowly comes to a stop … same as hero on
   service pages - I feel it is cinematic". The same keyframes, length and curve, so the two heroes move alike; .pfp-hero
   already clips the photograph. Less motion holds it at the end frame, as the service heroes do. */
.pfp-hero-media img{
  transform-origin:center center;
  animation:svcc-hero-zoom 17.6s cubic-bezier(.25,.46,.45,.94) forwards;
  will-change:transform;
}
@media (prefers-reduced-motion: reduce){
  .pfp-hero-media img{ animation:none; transform:scale(1.12); }
}
/* Less tint, and a gradient rather than a wash — Jesse's call after seeing the sandbox: the
   old .34 at the very top greyed the whole photograph. Clear through the upper half, then a
   hard ramp over the bottom third where the type sits. The eyebrow (gold) and h1 (white)
   depend on that bottom band; lighten the last two stops and the eyebrow goes first. */
.pfp-hero::after{
  content:'';position:absolute;inset:0;
  background:linear-gradient(180deg,
    rgba(11,8,42,0)   0%,
    rgba(11,8,42,.04) 40%,
    rgba(11,8,42,.36) 68%,
    rgba(11,8,42,.78) 100%);
}
.pfp-hero-inner{position:relative;z-index:2;max-width:var(--max);margin:0 auto;width:100%;}
.pfp-back{
  display:inline-block;margin-bottom:20px;
  font-family:var(--sans);font-size:13px;font-weight:600;
  color:rgba(255,255,255,.78);text-decoration:none;
}
.pfp-back:hover{color:#fff;}
.pfp-back:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;border-radius:2px;}
.pfp-eyebrow{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:.16em;text-transform:uppercase;
  /* Gold, on Jesse's call — the brand's mark on the one line that names the work. It was
     white because gold cannot reach WCAG AA over a photograph of white cabinetry: gold's
     luminance (~0.31) needs a near-black backing (L ≤ 0.03) for 4.5:1, which no scrim a
     photograph survives can give. Legibility here comes from the dark halo below, not from
     the backing — keep the text-shadow, and keep the bottom stops of .pfp-hero::after. */
  color:var(--brown-light);margin-bottom:14px;
  text-shadow:0 1px 3px rgba(11,8,42,.95), 0 0 22px rgba(11,8,42,.85);
}
.pfp-name{
  font-family:var(--heading);font-weight:800;color:#fff;
  font-size:clamp(38px,8vw,104px);line-height:.96;letter-spacing:-.03em;
  margin:0;text-wrap:balance;
  text-shadow:0 3px 34px rgba(11,8,42,.5);
}

/* "I want this look" — the hero's one action. Sits under the project name, spaced off it so
   it reads as a deliberate offer rather than part of the title block. */
.pfp-want{
  margin-top:26px;
  align-self:flex-start;
}
@media(max-width:900px){
  /* The sticky bar already carries a quote CTA on phones, so this would be the second one
     on screen. Same reasoning as the service hero's CTAs. */
  body:not(.no-sticky-cta) .pfp-want{display:none;}
}

/* The binder's blurb for the project (northco_portfolio_intros()), set on the gallery's left edge, and a size up from
   body text: it is the only prose on the page. It runs the column's full width, the photographs' edges, on Jesse's
   call, 2026-09-29: "Have the wording go all the way across". The 62ch measure it had stopped it two thirds of the way
   across, with a short last line under a full-width photograph. The eyebrow above it ("The brief") is the same one the
   Before & after section wears. */
.pfp-intro{padding:56px 48px 0;}
.pfp-intro-inner{max-width:var(--max);margin:0 auto;}
.pfp-intro p{
  margin:0 0 14px;
  font-family:var(--sans);font-size:clamp(18px,1.6vw,22px);line-height:1.6;color:var(--text-2);
  text-wrap:pretty;
}
.pfp-intro p:last-child{margin-bottom:0;}
/* The page's two section eyebrows, "The brief" and "Before & after", wear the site's eyebrow (.eyebrow: 12px, .14em,
   brown, a 32 by 2px rule ahead of the words). Jesse, 2026-09-29: "can you put a thin line next to "the brief"? the same
   thin line eyebrow we have across the site". Copied rather than adding the class, so the markup the crawl checked
   stays as it is. Before & after wraps, because its longer form ("… · drag to compare") runs past a phone's column. */
.pfp-intro p.pfp-intro-eyebrow, .pfp-ba-eyebrow{
  display:flex;align-items:center;gap:12px;width:fit-content;
  margin:0 0 18px;font-family:var(--sans);font-size:12px;font-weight:700;line-height:1.4;
  letter-spacing:.14em;text-transform:uppercase;color:var(--brown);white-space:nowrap;
}
.pfp-intro p.pfp-intro-eyebrow::before, .pfp-ba-eyebrow::before{content:'';flex:0 0 32px;height:2px;background:var(--brown);}
.pfp-ba-eyebrow{white-space:normal;}
.pfp-intro + .pfp-gallery{padding-top:40px;}

/* See the design come to life (portfolio-project.php, .pfp-design): the project's films, after the before and after
   and before the gallery. Jesse, 2026-11-02: "with some wording (instead of the eyebrow)". Three across, the wording
   and two films, or one film and a photograph at its 4:5, so the row is full; the wording sits level with the films. Its line is the page's display type,
   in a <p> (one h1, one h2), with the accent upright in brown like every heading's (.section-heading em). */
.pfp-design{padding:56px 48px 0;}
.pfp-design-inner{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;align-items:center;
}
.pfp-design .pfp-feature-film, .pfp-design .pfp-shot{align-self:start;}
/* The gallery's rhythm (.pfp-shot:nth-child(3n+1), full width at 16:9, below) is unscoped, so it reached in here: a photo
   that is the row's fourth child, as Cherington's is under its two 16:9 films (2026-11-02), ran full width. Out-ranked
   here, not rescoped there, so the gallery and the before and after don't move. */
.pfp-design .pfp-design-inner > .pfp-shot{grid-column:auto;}
.pfp-design .pfp-design-inner .pfp-shot--tall img{aspect-ratio:4 / 5;height:auto;}
/* One landscape across two empty cells (Riverside, 2026-11-03): at 16:10 its height is a 4:5 film's beside it. */
.pfp-design .pfp-design-inner > .pfp-shot--two{grid-column:span 2;}
.pfp-design .pfp-design-inner .pfp-shot--two img{aspect-ratio:16 / 10;height:auto;}
/* A 16:9 film takes two of the three columns, beside the wording (northco_portfolio_films() lists it first). */
.pfp-design .pfp-feature-film--wide{grid-column:span 2;}
.pfp-design-copy{padding-right:clamp(0px,2vw,28px);}
.pfp-design-title{
  margin:0 0 16px;font-family:var(--heading);font-weight:800;color:var(--navy);
  font-size:clamp(32px,3.4vw,52px);line-height:1.06;letter-spacing:-0.02em;text-wrap:balance;
}
.pfp-design-title em{color:var(--brown);font-style:normal;}
.pfp-design-text{margin:0;font-family:var(--sans);font-size:clamp(17px,1.4vw,20px);line-height:1.6;color:var(--text-2);text-wrap:pretty;}
.pfp-design + .pfp-gallery{padding-top:56px;}

/* Before and after (northco_portfolio_before_after()): the service pages' wipe (.tp-cmp-wipe, unscoped) in a
   two-across grid on the gallery's edges. A single pair sits at the width of two, not stretched full bleed. */
.pfp-ba{padding:48px 48px 0;}
.pfp-ba-inner{max-width:var(--max);margin:0 auto;}
.pfp-ba-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;}
.pfp-ba-grid--one{grid-template-columns:minmax(0,1fr);}
/* A lone wipe with no photograph to stand beside it (every one repeats it: the powder rooms; northco_portfolio_beside_skip())
   runs full width at 16:9, like the gallery's wide rows, rather than leaving a third of the row empty. */
.pfp-ba-grid--one:not(.pfp-ba-duo) .tp-cmp-wipe{aspect-ratio:16 / 9;}
.pfp-ba-grid--one:not(.pfp-ba-duo) .tp-cmp-layer--after{height:100%;}
.pfp-ba-item{margin:0;}
.pfp-ba-item .tp-cmp-wipe{border-radius:12px;}
.pfp-ba-cap{margin:12px 2px 0;font-family:var(--sans);font-size:14.5px;line-height:1.55;color:var(--text-2);}
.pfp-ba + .pfp-gallery{padding-top:44px;}
/* Tap to reveal (template-parts/portfolio-project.php, .pfp-flip; nav.js): the before and after that don't line up for
   a drag, V1's labelled phone snaps. Without script both photographs show side by side, each tagged. With it
   (.is-live) they share one 4:3 frame, and the Before / After buttons sweep the before away left to right behind a soft
   edge, or back: the move the team photos make on About, on its .75s curve. The mask is 2.5 frames wide, so 80% to 0
   takes the edge from just off the left side to just off the right. Less motion: a cut. */
.pfp-flip{max-width:880px;}
.pfp-flip-frame{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;}
.pfp-flip-shot, .pfp-ba-shot{
  position:relative;display:block;overflow:hidden;border-radius:12px;
  aspect-ratio:4 / 3;background:var(--cream-raised);
}
.pfp-flip-shot img, .pfp-ba-shot img{width:100%;height:100%;object-fit:cover;display:block;}
.pfp-flip-tag{
  position:absolute;left:12px;top:12px;
  font-family:var(--heading);font-size:10.5px;font-weight:700;letter-spacing:.07em;
  text-transform:uppercase;color:#fff;line-height:1;
  padding:7px 12px;border-radius:999px;background:rgba(22,16,68,.7);pointer-events:none;
}
.pfp-flip-tag--a{background:var(--brown);}   /* the wipe's After pill (.tp-cmp-tag--a), so a state reads by colour */
.pfp-flip.is-live .pfp-flip-frame{grid-template-columns:minmax(0,1fr);}
.pfp-flip.is-live .pfp-flip-shot{grid-area:1 / 1;}
.pfp-flip.is-live .pfp-flip-shot--b{
  z-index:1;
  -webkit-mask-image:linear-gradient(90deg,transparent 40%,#000 48%);mask-image:linear-gradient(90deg,transparent 40%,#000 48%);
  -webkit-mask-size:250% 100%;mask-size:250% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:80% 0;mask-position:80% 0;
  transition:-webkit-mask-position .75s cubic-bezier(.65,0,.35,1),mask-position .75s cubic-bezier(.65,0,.35,1);
}
.pfp-flip.is-live[data-side="after"] .pfp-flip-shot--b{-webkit-mask-position:0 0;mask-position:0 0;}
.pfp-flip-ctl{display:inline-flex;gap:4px;margin-top:14px;padding:4px;border-radius:999px;background:var(--cream-raised);}
.pfp-flip-ctl[hidden]{display:none;}
.pfp-flip-btn{
  appearance:none;-webkit-appearance:none;border:0;margin:0;cursor:pointer;
  min-height:40px;padding:0 20px;border-radius:999px;background:transparent;
  font-family:var(--heading);font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--navy);transition:background .2s var(--ease),color .2s var(--ease);
}
.pfp-flip-btn[aria-pressed="true"]{background:var(--navy);color:#fff;}
.pfp-flip-btn:focus-visible{outline:2px solid var(--brown);outline-offset:2px;}
.pfp-ba-rest{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;max-width:880px;margin-top:20px;}
/* A lone before and after with photographs beside it (northco_portfolio_ba_side()). Jesse, 2026-11-02: alone at 880px it
   left a third of the row empty. It keeps two thirds, near the 880 it had, and the gallery's photographs take the rest at
   its height: columns two to one make a 4:3 frame's neighbour 2:3, so one portrait fills it, or two photographs stack in
   it. One landscape photograph alone splits the row in halves, both 4:3. The flip's controls and the wipe's caption hang
   below, outside the match. The rest of the befores run six across under the row. */
.pfp-ba-grid.pfp-ba-duo, .pfp-ba-duo{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:20px;align-items:start;}
.pfp-ba-grid.pfp-ba-duo--even, .pfp-ba-duo--even{grid-template-columns:repeat(2,minmax(0,1fr));}
.pfp-ba-duo .pfp-flip{max-width:none;}
.pfp-ba-side{display:grid;grid-auto-rows:minmax(0,1fr);gap:20px;aspect-ratio:2 / 3;}
.pfp-ba-duo--even .pfp-ba-side{aspect-ratio:4 / 3;}
.pfp-ba-side .pfp-shot, .pfp-ba-side .pz-open{height:100%;}
.pfp-ba-duo .pfp-ba-side .pfp-shot img{height:100%;aspect-ratio:auto;}
.pfp-ba-duo + .pfp-ba-rest{grid-template-columns:repeat(6,minmax(0,1fr));max-width:none;}
@media (prefers-reduced-motion:reduce){
  .pfp-flip.is-live .pfp-flip-shot--b, .pfp-flip-btn{transition:none;}
}
/* Phones: one pair per row, and the intro and pairs on the gallery's 24px gutter (the intro shipped without this
   and sat 24px inside the gallery's left edge on a phone). */
@media(max-width:900px){
  .pfp-intro{padding:36px 24px 0;}
  .pfp-design{padding:40px 24px 0;}
  .pfp-design-inner{grid-template-columns:1fr;gap:14px;}
  .pfp-design .pfp-feature-film--wide{grid-column:auto;}
  .pfp-design .pfp-design-inner > .pfp-shot--two{grid-column:auto;}
  .pfp-design-copy{padding:0 0 6px;}
  .pfp-design + .pfp-gallery{padding-top:40px;}
  .pfp-ba{padding:36px 24px 0;}
  .pfp-ba-grid{grid-template-columns:1fr;gap:18px;}
  .pfp-ba-grid--one{grid-template-columns:1fr;}
  .pfp-ba-grid--one:not(.pfp-ba-duo) .tp-cmp-wipe{aspect-ratio:4 / 3;}
  .pfp-ba-rest{grid-template-columns:repeat(2,minmax(0,1fr));}
  /* The photographs beside a lone pair follow it, at the gallery's shapes: a portrait at the films' 4:5, others 3:2. */
  .pfp-ba-grid.pfp-ba-duo, .pfp-ba-duo, .pfp-ba-grid.pfp-ba-duo--even, .pfp-ba-duo--even{grid-template-columns:1fr;gap:14px;}
  .pfp-ba-side, .pfp-ba-duo--even .pfp-ba-side{aspect-ratio:auto;gap:14px;}
  .pfp-ba-side .pfp-shot, .pfp-ba-side .pz-open{height:auto;}
  .pfp-ba-duo .pfp-ba-side .pfp-shot img{height:auto;aspect-ratio:3 / 2;}
  .pfp-ba-duo .pfp-ba-side--tall .pfp-shot img{aspect-ratio:4 / 5;}
  .pfp-ba-duo + .pfp-ba-rest{grid-template-columns:repeat(2,minmax(0,1fr));}
}

/* Gallery. A plain grid, because Elementor's lightbox needed Elementor's JS and a
   photograph you can simply see beats one that has to be opened. Each one can also be
   clicked open, larger and whole, in a card (assets/js/photo-zoom.js; .pz below). */
.pfp-gallery{padding:64px 48px 0;}
.pfp-gallery-grid{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;
}
.pfp-shot{margin:0;overflow:hidden;border-radius:12px;background:var(--cream-raised);}
.pfp-shot img{width:100%;height:100%;object-fit:cover;display:block;aspect-ratio:3 / 2;}
/* Every third shot runs full width, so a long gallery has rhythm instead of being a
   uniform wall of thumbnails. */
.pfp-shot:nth-child(3n+1){grid-column:1 / -1;}
.pfp-shot:nth-child(3n+1) img{aspect-ratio:16 / 9;}
/* …and a grid that would end on a lone half-width shot, a hole beside it, runs that last one full width too. */
.pfp-gallery-grid > .pfp-shot:nth-child(3n+2):last-child{grid-column:1 / -1;}
.pfp-gallery-grid > .pfp-shot:nth-child(3n+2):last-child img{aspect-ratio:16 / 9;}
/* A film (portfolio-project.php, $nc_film), in "See the design come to life" (.pfp-design). */
.pfp-feature-film{margin:0;}
.pfp-film-frame{position:relative;display:block;}
.pfp-film{display:block;width:100%;height:auto;aspect-ratio:4 / 5;object-fit:cover;border-radius:12px;background:var(--cream-raised);}
.pfp-feature-film--wide .pfp-film{aspect-ratio:16 / 9;}
.pfp-film-note{margin:12px 2px 0;font-family:var(--sans);font-size:13px;line-height:1.5;color:var(--muted);}

/* Click a photograph to see it whole (assets/js/photo-zoom.js; Jesse, 2026-10-01). The button is
   the script's, so without it the grid is unchanged. The site's photo-card hover zoom. The keyboard
   ring sits inside the photograph, because the frame's overflow:hidden clips anything outside it. */
.pz-open{
  display:block;width:100%;margin:0;padding:0;border:0;background:none;
  border-radius:inherit;color:inherit;font:inherit;cursor:zoom-in;
}
.pz-open img{transition:transform .5s var(--ease);}
.pz-open:hover img{transform:scale(1.04);}
.pz-open:focus-visible{outline:3px solid var(--brown);outline-offset:-3px;}
/* The card: the configurator's "Up close" dialog (.tp-cfg-dialog) in look, with 16px corners, its
   frosted navy behind it and its white close button. The script sizes the photograph to the screen,
   and the photograph sizes the card.
   max-width/max-height:none, because the browser's own cap on a modal dialog (100% less 2em and 6px)
   is narrower than the card on a phone. margin:auto centres it, because the theme's reset zeroes it.
   No shadow: the script animates a clip-path, which would cut one off and then pop it back in at the
   end, and against the frost the photograph needs none. */
.pz{
  margin:auto;padding:0;border:0;border-radius:16px;overflow:hidden;background:#1a1640;
  max-width:none;max-height:none;
}
.pz::backdrop{background:rgba(22,16,68,.55);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);}
.pz-img{display:block;max-width:none;object-fit:cover;}
.pz-close{
  position:absolute;top:12px;right:12px;width:40px;height:40px;border-radius:50%;border:0;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  background:rgba(255,255,255,.92);color:var(--navy);font-size:24px;line-height:1;
  box-shadow:0 4px 14px rgba(0,0,0,.25);
}
.pz-close:focus-visible{outline:3px solid var(--brown);outline-offset:2px;}
/* The page can't scroll behind the card. The gutter stays, so nothing shifts sideways where scrollbars
   take room. */
html.pz-locked{overflow:hidden;scrollbar-gutter:stable;}

.pfp-notes{max-width:760px;margin:56px auto 0;padding:0 48px;}

/* "Also in this home": the other room we did in the same house (northco_portfolio_siblings()). One link
   covers the whole card, so the phone's tap target is the card and not a line of text. Raised cream,
   the colour the gallery's own frames use, so it reads as part of the page rather than an ad in it. */
.pfp-sib{padding:56px 48px;}
.pfp-sib-card{
  max-width:var(--max);margin:0 auto;
  display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);
  min-height:300px;
  background:var(--cream-raised);border:1px solid var(--border-warm);border-radius:18px;
  overflow:hidden;text-decoration:none;color:inherit;
}
/* ⚠ The photograph must not size the row. Half the covers are portrait (684x1024), and an in-flow img
   at width:100% took its height from its own aspect: an 840px-tall card on the sandbox (2026-09-23).
   Out of flow, it fills whatever height the words and min-height give the row. */
.pfp-sib-media{position:relative;display:block;overflow:hidden;}
.pfp-sib-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform .55s var(--ease);}
.pfp-sib-card:hover .pfp-sib-media img{transform:scale(1.04);}
.pfp-sib-body{display:flex;flex-direction:column;justify-content:center;gap:10px;padding:36px 40px;}
/* brown-dark, not brown: gold-600 on raised cream fails AA at 11px; gold-700 is 4.6:1. */
.pfp-sib-eyebrow{
  font-family:var(--sans);font-size:11px;font-weight:700;
  letter-spacing:.16em;text-transform:uppercase;color:var(--brown-dark);
}
.pfp-sib-line{
  font-family:var(--heading);font-weight:700;color:var(--navy);
  font-size:clamp(22px,2.4vw,32px);line-height:1.15;letter-spacing:-.02em;text-wrap:balance;
}
.pfp-sib-arrow{display:inline-block;transition:transform .25s var(--ease);}
.pfp-sib-card:hover .pfp-sib-arrow{transform:translateX(4px);}
.pfp-sib-meta{font-family:var(--sans);font-size:14px;color:var(--text-2);}
.pfp-sib-card:focus-visible{outline:3px solid var(--brown);outline-offset:3px;}
@media(prefers-reduced-motion:reduce){
  .pfp-sib-media img,.pfp-sib-arrow{transition:none;}
}
@media(max-width:900px){
  .pfp-sib{padding:40px 24px;}
  .pfp-sib-card{grid-template-columns:1fr;min-height:0;}
  .pfp-sib-media{aspect-ratio:3 / 2;}
  .pfp-sib-body{padding:22px 22px 24px;gap:8px;}
}

.pfp-cta{padding:72px 48px 88px;}
.pfp-cta-inner{
  max-width:var(--max);margin:0 auto;
  background:var(--cream);border:1px solid var(--border-warm);
  border-radius:18px;padding:34px 36px;
}
.pfp-cta-lead{
  font-family:var(--heading);font-weight:700;font-size:22px;
  color:var(--navy);margin:0 0 8px;
  letter-spacing:var(--track-heading, normal);
}
.pfp-cta-body{font-family:var(--sans);color:var(--text-2);margin:0 0 22px;max-width:52ch;}
.pfp-cta-actions{display:flex;flex-wrap:wrap;gap:12px;}

@media(max-width:900px){
  .pf-list{grid-template-columns:1fr;gap:30px;}
  .pf-item-media{aspect-ratio:3 / 2;}
  .pf-group{padding:0 24px 44px;}
  .pf-switch{padding:0 24px 28px;}
  .pf-switch-inner{grid-template-columns:1fr;gap:10px;}
  .pf-switch-opt{padding:16px 18px;}
  .pf-jump{padding:4px 0 26px;}
  /* Scrolls rather than wrapping to four rows and pushing the first project off screen. */
  .pf-jump-inner{
    flex-wrap:nowrap;overflow-x:auto;padding:0 24px 6px;
    scrollbar-width:none;
  }
  .pf-jump-inner::-webkit-scrollbar{display:none;}
  .pf-head{padding:calc(var(--nav-clear) + 32px) 24px 28px;}
  .pf-choose{padding:32px 24px 64px;}
  .pf-choose-inner{grid-template-columns:1fr;gap:18px;}
  /* 4:5 portrait cards stack into a very long page on a phone; 3:2 keeps three choices
     reachable without a scroll marathon. */
  .pf-choice{aspect-ratio:3 / 2;}
  .pf-choice-body{padding:24px;}

  .pfp-hero{padding:92px 24px 34px;min-height:min(64dvh, 520px);}
  .pfp-gallery{padding:44px 24px 0;}
  .pfp-gallery-grid{grid-template-columns:1fr;gap:14px;}
  /* Every shot is already full width on a phone, so the rhythm rule has nothing to do
     and its 16/9 crop would just make each photo shorter. */
  .pfp-shot:nth-child(3n+1) img, .pfp-gallery-grid > .pfp-shot:nth-child(3n+2):last-child img{aspect-ratio:3 / 2;}
  .pfp-notes{padding:0 24px;}
  .pfp-cta{padding:52px 24px 64px;}
  .pfp-cta-inner{padding:26px 22px;}
  .pfp-cta-actions .btn{flex:1 1 100%;justify-content:center;text-align:center;}
}
@media(prefers-reduced-motion:reduce){
  .pf-item-media img,.pf-choice-media img,.pf-choice-arrow,.pf-jump-link,.pf-switch-opt,.pz-open img{transition:none;}
}

/* ─── QUOTE FORM ──────────────────────────────────────────────────────────────────────
   The site's one real lead form (template-parts/quote-form.php). It reuses the existing
   .contact-section / .fi / .fi-select / .fi-textarea / .fi-submit rules above — those were
   always styled, it was the markup underneath them that was a mockup. Only the parts that
   did not exist before are defined here. */

/* Visually-hidden labels. Every field had a placeholder and no label, which leaves a
   screen reader announcing "edit text" nine times, and leaves everyone else with no idea
   what a field was once they have typed in it. */
.nc-sr{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
}

/* Honeypot. Off-screen rather than display:none — some bots skip fields that are
   explicitly hidden, which defeats the point of the trap. */
.nc-quote-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;}

/* Result message. Sits under the Send button, inside the dark form column (2026-11-06; it was above the fields). */
.nc-quote-status{
  font-family:var(--sans);font-size:15px;line-height:1.5;
  border-radius:10px;padding:12px 14px;margin:14px 0 0;
  border:1px solid transparent;
}
.nc-quote-status:focus-visible{outline:2px solid var(--brown-light);outline-offset:2px;}
.nc-quote-status.is-ok{
  background:rgba(129,199,132,.14);border-color:rgba(129,199,132,.45);color:#DFF3E1;
}
.nc-quote-status.is-error{
  background:rgba(255,138,128,.14);border-color:rgba(255,138,128,.5);color:#FFD9D4;
}

.nc-quote-fine{
  font-family:var(--sans);font-size:13px;line-height:1.5;
  color:rgba(255,255,255,.55);margin:12px 0 0;
}

/* The page's upsell tickbox (northco_quote_upsells()), between the message box and the button. The whole
   line is the label, so the tap target is the sentence, not an 18px square. */
.nc-quote-upsell{
  display:flex;align-items:flex-start;gap:10px;
  margin:2px 0 14px;cursor:pointer;
  font-family:var(--sans);font-size:14px;line-height:1.45;color:rgba(255,255,255,.78);
}
.nc-quote-upsell input{
  flex:none;width:18px;height:18px;margin:1px 0 0;
  accent-color:var(--brown);cursor:pointer;
}
.nc-quote-upsell input:focus-visible{outline:2px solid var(--brown-light);outline-offset:2px;}

/* What happens next, under the sub in the navy left column (quote-form.php 'steps': the design consultation page). */
.nc-quote-steps{list-style:none;counter-reset:ncstep;margin:4px 0 8px;padding:0;display:flex;flex-direction:column;gap:14px;}
.nc-quote-steps li{counter-increment:ncstep;position:relative;padding-left:40px;font-family:var(--sans);font-size:16px;line-height:1.6;color:rgba(255,255,255,.72);}
.nc-quote-steps li strong{color:#fff;font-weight:600;}
.nc-quote-steps li::before{content:counter(ncstep);position:absolute;left:0;top:1px;width:26px;height:26px;border-radius:50%;background:var(--brown);color:#fff;font-family:var(--heading);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;}

/* "Rather talk now?" beside the form, in the navy left column. */
.nc-quote-call{
  font-family:var(--sans);font-size:16px;color:rgba(255,255,255,.72);margin:20px 0 0;
}
.nc-quote-call a{
  color:var(--brown-light);font-weight:700;text-decoration:none;
  border-bottom:1px solid rgba(187,140,87,.4);
}
.nc-quote-call a:hover{color:#fff;border-bottom-color:rgba(255,255,255,.6);}
.nc-quote-call a:focus-visible{outline:2px solid var(--brown-light);outline-offset:3px;border-radius:2px;}

/* The standalone section (project pages) sits directly on the footer, so it carries its
   own bottom rhythm rather than relying on whatever precedes the footer elsewhere. */
.nc-quote-section{margin-top:8px;}

@media(max-width:900px){
  .nc-quote-status{font-size:14px;}
  .nc-quote-call{font-size:15px;}
}

/* ─── SCROLL MOTION ────────────────────────────────────────────────────────────────────
   Section reveals and the footer curtain, both from the rsd-agencements reference Jesse
   brought in. See assets/js/motion.js for the reasoning; the load-bearing detail is that
   EVERY hiding rule below is gated on html.nc-motion, a class only that script adds. With
   JavaScript off, blocked or broken, nothing here applies and the page is simply visible.

   The reference uses 1.2s. This is 0.8s: their pacing suits a gallery where slowness reads
   as luxury, ours is for someone pricing a renovation who is trying to get somewhere. */
:root{
  /* easeOutQuart — fast departure, long settle. This curve is most of what makes the
     reference feel weighted rather than springy. */
  --ease-reveal:cubic-bezier(0.25, 1, 0.5, 1);
}

/* ── TWO STAGES, AND THE REASON IS MEASURED ──
   Armed sections are only moved and faded. That is cheap: no filter, no layer promotion.

   The blur and the will-change hint are added ONLY when a section is within about a viewport
   of being seen (.reveal-ready, set by the prepare observer in motion.js).

   The first version put blur(14px) and will-change on all of them at load. On the homepage
   that was 16 sections, 17.7 megapixels of blurred surface — THIRTEEN TIMES the viewport —
   promoted to compositor layers and re-composited on every frame while Lenis scrolls. It was
   visibly choppy, which is how it was caught. Two stages keeps two or three promoted layers
   alive instead of sixteen — still why prepObserver exists, now that the blur itself is
   off by default. */
html.nc-motion [data-reveal]{
  opacity:0;
  transform:translateY(48px);
}
/*
 * COMPOSITOR-ONLY, AND THAT IS THE WHOLE POINT.
 *
 * opacity and transform are the only two properties a browser can animate without
 * repainting: the GPU re-composites a texture it already holds. filter:blur() is the exact
 * opposite — it must re-rasterise the layer and re-run a Gaussian convolution EVERY FRAME.
 * On a full-width section that is roughly 1.3 megapixels convolved sixty times a second,
 * two or three sections at once, while Lenis moves the page underneath.
 *
 * That was the chop, and Jesse called it before I did.
 *
 * Measured against serviceplusaquatics.com — a site he finds smooth, running the SAME
 * library we do. Across its entire page: zero animated filters, two static brightness()
 * calls on logos, will-change on exactly ONE element, and a reveal of opacity plus
 * translateY(20px). Six elements pending at a time against our sixteen. The library was
 * never the difference. The blur was.
 *
 * The blur is kept behind ?blur so the two can be felt back to back instead of argued
 * about. It is the one switch in this project that ADDS behaviour rather than removing it —
 * deliberate, and safe, because it enables a purely decorative effect and cannot reach a
 * visitor who does not type it.
 */
html.nc-motion [data-reveal].reveal-ready{
  will-change:opacity, transform;
}
html.nc-motion [data-reveal].is-revealed{
  opacity:1;
  transform:none;
  transition:opacity .8s var(--ease-reveal),
             transform .8s var(--ease-reveal);
  transition-delay:var(--reveal-delay, 0s);
}

/* ?blur — the previous blurred reveal, kept only for comparison. See the note above. */
html.nc-motion.nc-blur [data-reveal].reveal-ready{
  filter:blur(10px);
  will-change:opacity, transform, filter;
}
html.nc-motion.nc-blur [data-reveal].is-revealed{
  filter:blur(0);
  transition:opacity .8s var(--ease-reveal),
             transform .8s var(--ease-reveal),
             filter .8s var(--ease-reveal);
}

/* ── LOADING CURTAIN (EXPERIMENT) ──
   A PANEL THAT SLIDES, carrying the NorthCo arrow and a loading hairline. Enters from the
   bottom to cover the page being left, exits through the top to reveal the one arrived at.

   Raised and lowered by the inline bootstrap in header.php, which also guarantees it comes
   down — see the comment there for why both halves live in one script.

   ── WHY A SHARED SHIFT VARIABLE, IN vh ──
   The panel and the hairline are two separate pseudo-elements that must travel as one. They
   cannot share a percentage transform: translateY(-100%) on a full-viewport panel is the
   whole screen, and on a 1px bar it is one pixel. So the travel is expressed in vh, which
   means the same distance to both. */
:root{
  --nc-curtain-shift:-100vh;   /* resting: parked above the viewport, where the exit ends */
}
html.nc-loading{--nc-curtain-shift:0vh;}
html.nc-below{--nc-curtain-shift:100vh;}
html.nc-covering{--nc-curtain-shift:0vh;}

/* ── dvh, BECAUSE vh IS THE WRONG VIEWPORT ON A PHONE ────────────────────────────────────
   Jesse, on iOS: "the white loading screen doesn't appear from true bottom of viewport."

   vh is the LARGE viewport — the height the page would have with the browser chrome hidden.
   What you can actually see while the chrome is showing is smaller. So nc-below parked the
   panel at +100vh, roughly 120px BELOW the visible bottom edge on an iPhone, and the first
   120px of its travel happened off-screen. The panel therefore entered the frame already
   moving, partway up, instead of rising from the bottom edge. Same error mirrored on the
   resting -100vh.

   dvh is the DYNAMIC viewport: whatever is visible right now. Parking at 100dvh is exactly
   the bottom edge, so the whole travel is on screen and the panel arrives from the true edge.

   ⚠ THAT APPLIES TO THE ENTERING STATE ONLY. The exit needs the opposite unit for a reason
   spelled out in the @supports block below — it has to clear the panel's own box, which is
   measured in the large viewport. Changing the resting value to dvh broke the exit once.

   ⚠ @supports, NOT THE USUAL TWO-DECLARATION FALLBACK. Writing
       --nc-curtain-shift:-100vh;  --nc-curtain-shift:-100dvh;
   would be a TRAP: custom properties accept any token at parse time, so a browser with no
   dvh support stores the literal "-100dvh" and then translateY(var(--nc-curtain-shift))
   becomes invalid at computed-value time — the transform dies and the panel never moves at
   all, which is far worse than the bug being fixed. @supports asks the question properly.
   dvh is iOS 15.4+ and every current browser; anything older keeps the vh behaviour above.

   The vh reasoning in the note above still holds and is why this is a shared variable rather
   than a percentage: the panel and the 1px hairline must travel the same DISTANCE, which a
   percentage cannot express for two elements of different heights. dvh keeps that property. */
@supports (height:1dvh){
  /*
   * ⚠ ONLY THE ENTERING STATE. The resting/exit value above stays -100vh, and my first pass
   * at this changed it to -100dvh, which BROKE THE EXIT — Jesse: "Still not loading from
   * true bottom - see?", with the panel covering the top of the screen and the page showing
   * through at the bottom.
   *
   * Why: body::before is position:fixed with inset:0, so its box is the containing block —
   * the LARGE viewport on iOS, the toolbar-collapsed height. vh is that same large viewport
   * by spec, so -100vh clears the box exactly. dvh while the toolbar is showing is SMALLER,
   * so -100dvh moves the panel less far than its own height and leaves a band of cream
   * behind. The unit has to match what the box is measured against, and for the exit that is
   * vh, not dvh.
   *
   * The entering state is the opposite case and is why dvh is here at all: nc-below parks the
   * panel below the screen, and it should start at the edge the viewer can SEE, which is dvh.
   * At +100dvh the panel's top edge sits on the visible bottom, so the whole rise is on
   * screen instead of the first ~120px happening under the toolbar.
   *
   * Two states, two viewports, for two different reasons. Do not "make them consistent".
   */
  html.nc-below{ --nc-curtain-shift:100dvh; }
}

/* Insurance on the coverage itself, separate from the travel: inset:0 sizes the panel to its
   containing block, and if a browser ever hands a fixed box the SMALL viewport while the
   toolbar is collapsing, the panel would be short and leave a gap at the bottom — which is
   the other half of what Jesse's screenshot could show. min-height cannot make it smaller,
   only guarantee a floor, so this is safe in every case. */
@supports (height:1lvh){
  body::before{ min-height:100lvh; }
}

/* ── THE CURTAIN EXISTS ONLY WHILE IT IS RUNNING ──
   Both curtain pseudo-elements are position:fixed. body::before is inset:0 — a FULL-VIEWPORT
   fixed layer — and body::after carries a mask, a gradient background and a filter. Defined
   unconditionally, they sat on every page for an entire visit, parked off-screen by
   --nc-curtain-shift but still real: two permanent composited layers that a browser has to
   keep and reposition as the page scrolls, for an effect that runs for about two seconds at a
   page transition.

   Chrome absorbed that. Safari, which is markedly worse at fixed layers, masks and filters,
   is where Jesse's chop survived every other fix — Chrome went smooth and Safari did not.

   Worse on the majority of traffic: the loader returns early below 900px, so the curtain NEVER
   RUNS ON A PHONE, and those two layers were pure cost with no effect at all for the 85% of
   visitors on mobile.

   Gating on the four state classes is safe because every one of them is cleared. nc-exiting is
   dropped a full second after nc-loading, and the panel's own transition is .7s, so the layers
   outlive the animation by 300ms and vanish only once there is nothing left to see. The
   nc-below staging step still works because the click handler forces a reflow (void
   d.offsetHeight) between parking the panel and sliding it up. */
body::before,
body::after{ display:none; }
html.nc-loading body::before,  html.nc-loading body::after,
html.nc-below body::before,    html.nc-below body::after,
html.nc-covering body::before, html.nc-covering body::after,
html.nc-exiting body::before,  html.nc-exiting body::after,
html.nc-back-exit body::before, html.nc-back-exit body::after{ display:block; }

/* THE PANEL, with the arrow mark centred on it. The arrow rides as a background rather than
   a child because a pseudo-element cannot have children — which is also why the loading
   hairline below is a second pseudo-element rather than a sibling node. */
body::before{
  content:'';
  position:fixed;
  inset:0;
  z-index:9000;
  background:var(--cream);
  pointer-events:none;
  transform:translateY(var(--nc-curtain-shift));
  /* THE EXIT CURVE. easeInOutQuart, and this base transition is only ever used for leaving —
     the covering and below states override it.

     It was easeOutQuart, cubic-bezier(0.25, 1, 0.5, 1), which is the right shape for
     something ARRIVING: fast off the mark, long settle. Reversed onto an exit it reads as a
     yank — the panel is already at speed on the first frame, then crawls the last of the way
     off-screen. Jesse: "comes up nicely but then leaving it's a bit too abrupt."

     Accelerating away instead means the first frames are slow enough to register as a
     departure, and the panel is at its fastest when it no longer matters because it is
     mostly off the screen. */
  /* THEIR NUMBERS, read out of the reference site's own bundle rather than guessed.
     Their exit is gsap `{ yPercent:-100, duration:.7, ease:"power3.inOut" }`. GSAP's power3
     is quart, so power3.inOut is easeInOutQuart = cubic-bezier(0.76, 0, 0.24, 1).

     My first attempt used easeOutQuart, which is the right shape for something ARRIVING —
     fast off the mark, long settle — and reversed onto an exit it reads as a yank. The curve
     was then right by instinct but the duration was not: I had .95s where theirs is .7s, and
     I waited for the mark to finish fading before moving. Theirs OVERLAPS the two, which is
     what makes it one motion instead of two steps. */
  transition:transform .7s cubic-bezier(0.76, 0, 0.24, 1);
}

/* THE MARK IS THE LOADER.
   Jesse found a loader that clip-paths a triangle outline and rotates a conic gradient
   inside it, so light travels around the shape. The NorthCo arrow already IS a triangle
   outline, so instead of putting his triangle next to our mark, the sweep is masked BY the
   mark: the light runs around the real logo.

   His version needs a child element to hold the rotating layer. A pseudo-element cannot have
   children, so the rotation is done by animating the gradient's own start angle through a
   registered custom property instead — same effect, no extra node, and nothing injected into
   the page.

   Degrades honestly: where @property is unsupported the angle never animates and the arrow
   simply renders with a static gradient across it, which reads as intentional rather than
   broken. */
@property --nc-mark-lift{
  syntax:'<length>';
  inherits:false;
  initial-value:0px;
}

body::after{
  content:'';
  position:fixed;
  left:50%;
  top:50%;
  width:104px;
  height:104px;
  z-index:9001;
  /* The arrow, used as a stencil. -webkit- first for Safari, which still needs the prefix. */
  -webkit-mask:url('assets/images/logo-arrow.svg') no-repeat center / contain;
  mask:url('assets/images/logo-arrow.svg') no-repeat center / contain;
  /* A SEAMLESSLY TILING SWEEP, not a conic rotation.
     The conic version stuttered, and the reason is geometric: a conic gradient fans out from
     the element's centre, but this mark is a narrow chevron whose mass sits near the point
     and the tail with almost nothing at the sides. So the lit wedge spent part of every turn
     crossing empty mask, the light appeared to stall, then jumped when it hit solid shape
     again. His 25%-thick triangle spreads evenly around its centre and hides that; a fine
     outline cannot.

     This is a vertical gradient tiled at exactly twice the element height and scrolled by
     exactly one tile per cycle. Because the gradient's first and last stops are the same
     colour, the tile boundary is invisible and the loop point is mathematically identical to
     the start — it cannot seam, however long it runs.

     The base tint is the first and last stop rather than a separate layer, which is what
     keeps the whole arrow present between passes. */
  background-image:linear-gradient(to top,
    rgba(167, 120, 69, 0.26) 0%,
    rgba(167, 120, 69, 0.26) 26%,
    var(--brown-light) 44%,
    var(--brown) 50%,
    var(--brown-light) 56%,
    rgba(167, 120, 69, 0.26) 74%,
    rgba(167, 120, 69, 0.26) 100%);
  background-size:100% 208px;   /* two element heights: one visible, one queued below */
  background-repeat:repeat-y;
  background-position:0 0;
  /* Barely any blur. His 12px was tuned for an 80px solid triangle; on an outline this fine
     even 1.5px started dissolving the stroke. */
  filter:blur(0.4px);
  opacity:0;
  pointer-events:none;
  transform:translate(-50%, -50%) translateY(var(--nc-curtain-shift)) translateY(var(--nc-mark-lift, 0px));
  transition:transform .7s cubic-bezier(0.76, 0, 0.24, 1), opacity .35s cubic-bezier(0.32, 0, 0.67, 0);
}

/* COVERING, from the first paint. No transition on the way up, or the panel would animate
   itself into place over the content it exists to hide. */
html.nc-loading body::before{transition:none;}
html.nc-loading body::after{
  transition:none;
  opacity:1;
  animation:nc-sweep 2s linear infinite;
}

/* PARKED BELOW — the staging step before sliding in on a link click.
   The mark is staged the way theirs is: `lt.set(t, { opacity:0, y:15 })`. It starts BELOW its
   final position and invisible, so on the way in it rises into place rather than simply being
   present when the panel arrives. */
html.nc-below body::before,
html.nc-below body::after{transition:none;}
html.nc-below body::after{
  opacity:0;
  --nc-mark-lift:15px;
}

/* SLIDING IN from the bottom. Faster and on a sharper curve than the reveals: this one is
   covering something, and hesitating over it reads as a stall. */
/* THE ENTRANCE, also theirs: `{ yPercent:0, duration:.6, ease:"power3.inOut" }` for the
   panel, with the mark arriving on `power2.out` over .35s — easeOutCubic. */
html.nc-covering body::before,
html.nc-covering body::after{
  transition:transform .6s cubic-bezier(0.76, 0, 0.24, 1);
}
html.nc-covering body::after{
  opacity:1;
  --nc-mark-lift:0px;
  animation:nc-sweep 2s linear infinite;
  /* TWO CLOCKS ON ONE ELEMENT. The shared curtain travel runs on the panel's .6s curve with
     no delay; the mark's own rise and fade run on power2.out over .35s, starting .4s in.
     That delay is theirs: `.to(t, {...}, "-=0.2")` against a .6s panel tween puts the mark's
     entrance two thirds of the way through the panel's, so the panel is almost home before
     the logo settles onto it. Starting them together made the logo look carried in on the
     panel rather than arriving onto it.

     The lift animates through the custom property because `transform` is already claimed by
     the shared travel and a single element cannot run two different clocks on one property.
     @property is what makes a length interpolatable. */
  transition:transform .6s cubic-bezier(0.76, 0, 0.24, 1) 0s,
             opacity .35s cubic-bezier(0.33, 1, 0.68, 1) .4s,
             --nc-mark-lift .35s cubic-bezier(0.33, 1, 0.68, 1) .4s;
}

/* Exactly one tile of travel. 208px is the background-size height, so the end state is
   pixel-identical to the start and the animation restart is invisible. */
@keyframes nc-sweep{
  from{background-position:0 0;}
  to{background-position:0 -208px;}
}

/* THE MARK FADES BEFORE THE PANEL MOVES.
   Riding all the way up with the panel is a second thing to track at exactly the moment the
   page underneath is being revealed, and it is most of what made the exit feel busy. The
   mark settles out first, then the panel leaves an empty field. The bootstrap holds ~220ms
   between the two, which doubles as a beat of stillness before the reveal. */
/* THE MARK FADES AND LIFTS, their `{ opacity:0, y:-15, duration:.35, ease:"power2.in" }`.
   power2 is cubic, so power2.in is easeInCubic = cubic-bezier(0.32, 0, 0.67, 0).

   The 15px lift is the part I had missed. A mark that only fades sits still while it
   disappears; one that drifts up as it goes is already travelling in the panel's direction
   when the panel starts, which is what makes the handover invisible. */
html.nc-exiting body::after{
  opacity:0;
  --nc-mark-lift:-15px;
  transition:opacity .35s cubic-bezier(0.32, 0, 0.67, 0),
             --nc-mark-lift .35s cubic-bezier(0.32, 0, 0.67, 0);
}

/* GOING BACK, THE REVERSE (2026-10-07, header.php's pageshow). Jesse: "what if the curtain comes the opposite direction
   when you go back?" A page restored by the Back button opens covered; the mark fades and drifts DOWN (the mirror of
   nc-exiting's lift), then the panel leaves through the BOTTOM on the same exit curve, so the move reads top to bottom.
   +100vh clears the panel's own box, measured in the large viewport, as the -100vh exit does. */
html.nc-exiting-down body::after{
  opacity:0;
  --nc-mark-lift:15px;
  transition:opacity .35s cubic-bezier(0.32, 0, 0.67, 0),
             --nc-mark-lift .35s cubic-bezier(0.32, 0, 0.67, 0);
}
html.nc-back-exit{ --nc-curtain-shift:100vh; }

/* Nobody scrolls what they cannot see, and it stops a scroll started behind the curtain
   from landing mid-page when it lifts. */
html.nc-loading,
html.nc-covering{overflow:hidden;}

/* Reduced motion never sees the curtain at all — the bootstrap returns before adding the
   class — but the sweep is belt-and-braces in case the panel is ever shown another way. */
@media (prefers-reduced-motion: reduce){
  body::after{animation:none !important;}
}

/* ── BACKDROP BLUR IS SUSPENDED WHILE SMOOTH-SCROLLING ──
   The fixed nav carries backdrop-filter:blur(12px) across the full viewport width. That is
   cheap at rest and expensive during scroll, because everything behind it has to be
   re-sampled and blurred as it moves.

   Native scrolling lets a browser run that on its own optimised path. Lenis drives scroll
   from JavaScript on every frame, so the work becomes synchronous per-frame instead — which
   is why Chrome absorbed it and Safari, much weaker at backdrop-filter, went choppy. It is
   an INTERACTION between two things that are each fine alone, which is why it did not show
   up until smooth scroll landed.

   Lenis puts .lenis-scrolling on <html> only while the scroll is actually moving, so the
   blur is dropped exactly for those frames and returns the moment it settles. The nav's own
   background is already rgba(...,0.88); it goes fully opaque here so nothing shows through
   in the meantime. In motion this is imperceptible.

   ⚠ .lenis-scrolling ALONE COVERED THE WRONG AUDIENCE. Lenis is deliberately off on touch,
   so that class never appears on a phone — and this nav is fixed across the full viewport
   width on every page. The 85% of this site's traffic that is mobile was paying a full-width
   backdrop blur on every frame of every scroll, on the weakest hardware, with no guard at
   all. .nc-scrolling is set by a passive scroll listener in motion.js, so it fires for
   native scrolling, Lenis, keyboard and scrollTo alike. */
html.lenis-scrolling .nav,
html.lenis-scrolling .sticky-cta,
html.nc-scrolling .nav,
html.nc-scrolling .sticky-cta{
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}
html.lenis-scrolling .nav,
html.nc-scrolling .nav{background:var(--cream);}

/* ══════════════════════════════════════════════════════════════════════════════════════
   THE POSTMEDIA PAGES: MARKUP THE TYPE SYSTEM WAS NEVER BUILT FOR
   ══════════════════════════════════════════════════════════════════════════════════════
   Jesse: "https://…/interior-services/kitchen-designers/ is completely broken layout wise
   and not like the rest of the site yet."

   Twelve pages came from Postmedia rather than the Elementor migration, and their markup is
   a different animal: 187 tags carrying ZERO classes, so everything depends on element
   selectors — plus content shapes the five ported service pages never had.

   ⚠ THE CATASTROPHE WAS THIRTEEN SVGs AT 735 x 735 PIXELS EACH. Measured. Their FAQ
   accordion emits two inline icons per question with no width, no height and no class, and
   an SVG with no intrinsic size fills its container — so each one became a chevron the full
   width of the 760px reading column. Thirteen of them made the page 26,250px tall. Nothing
   else about the layout mattered while that was happening.

   ⚠ AND THE ACCORDION THEY BELONG TO DOES NOT EXIST HERE. The questions are
   <a tabindex="0"> with no href and no script, so nothing opens or closes. The answers are
   already visible in the <p> that follows, which is the right outcome for a page whose FAQ
   Google may lift — it just needs to LOOK like a question and an answer.
   ────────────────────────────────────────────────────────────────────────────────────── */

/* 1. NO INLINE SVG MAY EVER SIZE ITSELF AGAIN. A hard cap rather than display:none, so a
      legitimate inline icon in future content still renders — just never at 735px. */
:is(.page-content,.post-content) svg{
  width:1em;
  height:1em;
  flex:none;
  vertical-align:-0.125em;
  fill:var(--brown);
}
/* Two icons per question — an open and a close state for an accordion that is not here. */
:is(.page-content,.post-content) svg + svg{ display:none; }

/* 2. THE FAQ QUESTION IS AN ANCHOR WITH NO HREF, so the type system's link styling does not
      reach it and it read as body copy. It is the heading of its answer in everything but
      name, so it gets treated as one — without becoming a real heading, which would change
      the outline and the parity diff. */
:is(.page-content,.post-content) a[tabindex="0"]{
  display:block;
  margin:26px 0 6px;
  font-family:var(--heading);
  font-size:clamp(18px,1.6vw,20px);
  font-weight:700;
  line-height:1.35;
  color:var(--navy);
  text-decoration:none;
  border-top:1px solid var(--border-warm);
  padding-top:22px;
  cursor:default;
}
:is(.page-content,.post-content) a[tabindex="0"]:hover{ color:var(--navy); text-decoration:none; }
/* The icon sits with the question rather than orphaned on the line above it. */
:is(.page-content,.post-content) svg{ margin-right:8px; }

/* 3. THE CTAs LOST THEIR BUTTONS. Elementor styled them; the migration kept the links and
      dropped the chrome, so "Get Free Quote Today" and "Call 343-321-0527" rendered as two
      bare text links mid-paragraph, four times over. They are real anchors — href="#quote"
      and href="tel:" — so they can be recognised without touching the markup. */
:is(.page-content,.post-content) a[href="#quote"],
:is(.page-content,.post-content) a[href^="tel:"]{
  display:inline-flex; align-items:center; justify-content:center;
  min-height:46px; padding:12px 26px; margin:6px 8px 6px 0;
  border-radius:999px;
  font-family:var(--heading); font-size:14px; font-weight:700;
  letter-spacing:0.04em; text-transform:uppercase;
  text-decoration:none;
  transition:background .18s var(--ease), color .18s var(--ease);
}
:is(.page-content,.post-content) a[href="#quote"]{
  background:var(--navy); color:#fff; border:1px solid var(--navy);
}
:is(.page-content,.post-content) a[href="#quote"]:hover{ background:var(--navy-700); color:#fff; }
:is(.page-content,.post-content) a[href^="tel:"]{
  background:transparent; color:var(--navy); border:1px solid var(--border-warm);
}
:is(.page-content,.post-content) a[href^="tel:"]:hover{ background:var(--cream-raised); color:var(--navy); }

/* 4. The reviews block is injected INSIDE the 760px column, so its own grid must not assume
      the full page width. */
:is(.page-content,.post-content) .rv-section{ padding:8px 0 0; }
:is(.page-content,.post-content) .rv-section .inner{ padding:0; max-width:none; }
@media(min-width:901px){
  :is(.page-content,.post-content) .rv-grid{ grid-template-columns:1fr; gap:14px; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   TOPIC SECTIONS — the only new CSS the additive sections need
   ══════════════════════════════════════════════════════════════════════════════════════
   template-parts/topic-sections.php renders four section types and three of them reuse CSS
   that already ships: steps borrow .svc-process/.svc-step* from kitchen-process, grids
   borrow .why-grid/.why-item from why-choose, and the cross-sell rail borrows .svc-more
   from the interior hub. Same discipline the exterior hub set: "No new CSS for any of this,
   which is the point: two hub pages."

   Only two things had no home already. */

/* 1. A FOUR-ITEM GRID NEEDS FOUR COLUMNS, NOT THREE. .why-grid is repeat(3,1fr) with
      hairline borders, which is right for the homepage's six reasons. Four items in it
      leaves one alone on a second row with the borders framing the gap. Two columns is the
      honest fit for a set of four. */
.why-grid.tp-grid--2{ grid-template-columns:repeat(2,1fr); }
@media(max-width:900px){
  .why-grid.tp-grid--2{ grid-template-columns:1fr; }
}

/* 2. A CALLOUT, because the theme had none — grepped for callout/note/aside/pull/highlight
      and found nothing. Raised ground and a left rule rather than a box: the page already
      has bordered grids above and below it, and a third bordered rectangle would read as
      more of the same instead of as an aside. */
.tp-note{
  background:var(--cream-raised);
  border-left:3px solid var(--brown);
  border-radius:0 14px 14px 0;
  padding:28px 32px;
}
.tp-note-title{
  margin:0 0 10px;
  font-family:var(--heading);
  font-size:clamp(22px,2.2vw,28px);
  line-height:1.2;
  letter-spacing:var(--track-heading, -0.01em);
  color:var(--navy);
}
.tp-note-body{
  margin:0;
  font-size:17px;
  line-height:1.65;
  color:var(--text);
  max-width:68ch;   /* a measure, not the full 1440 — this is prose, not a layout */
}
.tp-note-cta{ margin:20px 0 0; }
.tp-note-cta .btn{ text-decoration:none; }

@media(max-width:768px){
  .tp-note{ padding:22px 20px; border-radius:0 12px 12px 0; }
  .tp-note-body{ font-size:16px; }
}

/* ── FOOTER CURTAIN ──
   The footer is pinned behind the page; #site-content rides above it on an opaque ground
   with a bottom margin equal to the footer's measured height, so the last stretch of scroll
   slides the page up and uncovers it.

   --nc-footer-h is set by motion.js from the real element. Hardcoding it (as the reference
   does, at 679px) would be wrong at every width our responsive footer changes height. */
@media (min-width: 900px){
  html.nc-curtain .footer{
    position:fixed;
    left:0; right:0; bottom:0;
    z-index:1;
  }
  html.nc-curtain #site-content{
    position:relative;
    z-index:2;
    /* Opaque, or the footer shows through the page it is meant to be hiding behind. */
    background:var(--cream);
    margin-bottom:var(--nc-footer-h, 0px);
  }
  /* The sticky quote bar has to stay above both layers. */
  html.nc-curtain .sticky-cta{z-index:3;}
}

@media (prefers-reduced-motion: reduce){
  html.nc-motion [data-reveal],
  html.nc-motion [data-reveal].is-revealed{
    opacity:1; transform:none; filter:none; transition:none;
  }
}

/* ── /free-quote-estimate/ ──
   The page copy sits on light and the form on navy, because the form's own fine print and
   phone line are white text while .page-content h1 is navy — see page-free-quote-estimate.php.

   The top padding is the fixed nav's 68px plus breathing room, matching .pf-head and .jn-head.
   .section's own 96px would clear the nav by only 28px, and .section.tight's 64px would put
   the h1 UNDERNEATH it — the same collision that sliced 12px off the service-page headings. */
.fqe-head{ padding-top:calc(var(--nav-clear) + 52px); padding-bottom:40px; }
.fqe-intro{ max-width:640px; }
@media(max-width:768px){
  .fqe-head{ padding-top:calc(var(--nav-clear) + 32px); padding-bottom:28px; }
}

/* ════════════════════ CONTACT US — /contact-us/ (page-contact-us.php) ════════════════════
   V1's content, taken apart by the template: the h1 and its line, V1's phone and email h2s and its
   hours as labelled blocks, the working quote form, and the map. Layout notes below. */
/* ⚠ Both pages undo .page-article's 760px centring, as the service and FAQ pages do (see
   .nc-faq-article): under it the navy form was a 760px card in the middle of the page, the copy
   started at x=388 where every other h1 starts at the content edge, and a 197px column broke
   "info@northcoservices.com" mid-word. The bands go full width; .fqe-intro and .cu-intro keep
   the reading measure. */
.fqe-article, .cu-article{ max-width:none; margin-inline:0; }
/* Jesse's references, 2026-09-25: one card, the homepage's three hero photographs behind a navy wash, the h1 top left, the
   ways to reach us as labelled blocks bottom left, the form on a white card to the right, then
   the map. The card's outer edge sits on the content edge (48px
   at 1440), as every other V2 card does; its copy is inset by the card's own padding. */
.cu-stage{ padding:calc(var(--nav-clear) + 24px) 48px 0; }
.cu-card{ position:relative; max-width:var(--max); margin:0 auto; border-radius:24px; overflow:hidden; background:var(--navy); isolation:isolate; }
.cu-card-bg{ position:absolute; inset:0; z-index:-2; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); }
/* Monochrome under the wash, and no seams: Jesse, on the full-colour three: "background needs some love it's
   kinda busy". The three services still read by their shapes; the colour and the joins were the noise. */
.cu-card-bg img{ display:block; width:100%; height:100%; object-fit:cover; filter:grayscale(1) contrast(1.05); }
.cu-card::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(90deg, rgba(22,16,68,.95) 0%, rgba(22,16,68,.88) 55%, rgba(22,16,68,.76) 100%);
}
.cu-card-grid{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,500px);
  grid-template-areas:"copy form" "facts form";grid-template-rows:auto 1fr;
  gap:40px 56px;padding:56px;
}
.cu-copy{ grid-area:copy; max-width:620px; }
.cu-title{
  font-family:var(--heading);font-weight:800;color:#fff;margin:0 0 18px;
  font-size:clamp(34px,4.4vw,60px);line-height:1.05;letter-spacing:-.02em;
}
.cu-lead{ margin:0; max-width:52ch; font-size:17px; line-height:1.65; color:rgba(255,255,255,.84); }
/* The fallback path prints the whole filtered content here: keep it legible on the navy. */
.cu-copy h1, .cu-copy h2{ color:#fff; }
.cu-copy p{ color:rgba(255,255,255,.84); }
.cu-copy a{ color:#fff; }
.cu-form-card{
  grid-area:form;align-self:start;background:#fff;border-radius:18px;padding:28px;
  box-shadow:0 24px 60px rgba(0,0,0,.25);
}
.cu-facts{ grid-area:facts; align-self:end; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:28px 40px; }
.cu-fact-label{
  margin:0 0 8px;font-family:var(--sans);font-size:12px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;color:var(--brown-light);
}
.cu-fact-h{
  margin:0;font-family:var(--heading);font-size:clamp(17px,1.5vw,21px);font-weight:700;
  line-height:1.3;color:#fff;overflow-wrap:anywhere;
  letter-spacing:var(--track-heading, normal);
}
.cu-fact-h a{ color:#fff; text-decoration:none; border-bottom:1px solid rgba(255,255,255,.3); }
.cu-fact-h a:hover{ border-bottom-color:var(--brown-light); }
.cu-fact-h a:focus-visible{ outline:2px solid var(--brown-light); outline-offset:3px; border-radius:2px; }
.cu-fact-h b{ color:inherit; font-weight:700; }
.cu-fact-body p{ margin:0 0 6px; font-size:14.5px; line-height:1.65; color:rgba(255,255,255,.8); }
.cu-fact-body em{ color:rgba(255,255,255,.58); }

/* The quote form on white. Its own rules are written for navy (white text, translucent fields),
   so each one it relies on is restated here for a light ground. */
.cu-form-card .fi, .cu-form-card .fi-select, .cu-form-card .fi-textarea{ background:#fff; border-color:var(--border-warm); color:var(--navy); }
.cu-form-card .fi::placeholder, .cu-form-card .fi-textarea::placeholder{ color:#8b8699; }
.cu-form-card .fi-select{ color:var(--text-2); }
.cu-form-card .fi-select option{ background:#fff; color:var(--navy); }
.cu-form-card .nc-quote-fine{ color:var(--muted); }
.cu-form-card .nc-quote-upsell{ color:var(--text-2); }
.cu-form-card .nc-quote-status.is-ok{ background:#E8F5E9; border-color:#A5D6A7; color:#1B5E20; }
.cu-form-card .nc-quote-status.is-error{ background:#FDECEA; border-color:#F5C2C0; color:#8E1B12; }

.cu-map-stage{ padding:40px 48px 96px; }
.cu-bottom{ max-width:var(--max); margin:0 auto; }
/* The map and a square socials tile side by side (Jesse: "cut the map tile in half… make it a square"). The
   tile sets the row's height; the map fills the rest of the width and matches it. */
.cu-bottom--pair{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,480px); gap:24px; align-items:stretch; }
/* ⚠ The card's own margin:0 auto (from its full-width days) shrank it, inside a grid cell, to the iframe's
   default 300px, centred. In the pair it fills its cell. */
.cu-bottom--pair .cu-map-card{ margin:0; width:100%; }
.cu-bottom--pair .cu-map-card, .cu-bottom--pair .cu-map-card .tp-embed-map{ height:100%; }
.cu-bottom--pair .cu-map-card .tp-embed-map iframe{ height:100%; min-height:420px; }
.cu-social{
  aspect-ratio:1/1;display:flex;flex-direction:column;justify-content:center;
  background:#fff;border:1px solid var(--border-warm);border-radius:24px;padding:36px;
}
.cu-social-title{ margin:0 0 6px; font-family:var(--heading); font-weight:800; font-size:clamp(26px,2.4vw,34px); color:var(--navy); line-height:1.1; letter-spacing:var(--track-heading, normal); }
.cu-social .tp-social-sub{ margin:0 0 22px; font-size:15.5px; color:var(--text-2); }
.cu-social .tp-social-list{ grid-template-columns:1fr; gap:10px; }
.cu-social .tp-social-list a{ background:var(--cream); }
.cu-social .tp-social-icon{ background:#fff; }
.cu-map-card{ position:relative; max-width:var(--max); margin:0 auto; border-radius:24px; overflow:hidden; background:#fff; }
.cu-map-card .tp-embed-map{ margin:0; }
.cu-map-card .tp-embed-map iframe{ display:block; height:420px; border-radius:0; }
.cu-fact-go{ display:inline-block; margin-top:6px; font-size:14px; font-weight:700; color:var(--brown-light); text-decoration:none; }
.cu-fact-go:hover{ color:#fff; }
.cu-fact-go:focus-visible{ outline:2px solid var(--brown-light); outline-offset:3px; border-radius:2px; }
@media(max-width:1000px){
  .cu-card-grid{ grid-template-columns:1fr; grid-template-areas:"copy" "form" "facts"; grid-template-rows:auto; padding:40px 28px; }
  /* Stacked, as the homepage's panels stack: kitchen behind the heading, bathroom behind the form,
     exterior behind the blocks. The bathroom's vanity sits low in its frame (see .panel--bathrooms). */
  .cu-card-bg{ grid-template-columns:1fr; grid-template-rows:repeat(3,minmax(0,1fr)); }
  .cu-card-bg-hero-bathroom{ object-position:center 85%; }
  .cu-form-card{ padding:22px; }
}
@media(max-width:768px){
  .cu-stage{ padding:calc(var(--nav-clear) + 12px) 12px 0; }
  .cu-card{ border-radius:20px; }
  .cu-card::before{ background:rgba(22,16,68,.9); }
  .cu-card-grid{ padding:32px 20px 28px; gap:28px; }
  .cu-facts{ gap:22px 18px; }
  .cu-map-stage{ padding:28px 12px 56px; }
  .cu-map-card{ border-radius:20px; }
  .cu-map-card .tp-embed-map iframe{ height:300px; }
  .cu-bottom--pair{ grid-template-columns:1fr; gap:16px; }
  .cu-bottom--pair .cu-map-card .tp-embed-map iframe{ height:300px; min-height:0; }
  .cu-social{ aspect-ratio:auto; padding:26px 22px; border-radius:20px; }
}
@media(max-width:420px){ .cu-facts{ grid-template-columns:1fr; } }

/* ── SOCIAL THUMBNAILS AND THE COMPACT ROW (.tp-social-thumbs, .tp-social--compact) ─────────────────────────────
   Three recent posts, fanned a little as if set down on a table and squared up on hover: Jesse's "friendly,
   approachable, human". Portrait 3:4, from his 9:16 reel covers. The compact row is the channels as named
   buttons, for a tile too small for three cards. */
.tp-social-thumbs{ list-style:none; margin:0 0 20px; padding:6px 4px 0; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.tp-social-thumbs a{ display:block; border-radius:14px; overflow:hidden; box-shadow:0 10px 24px rgba(22,16,68,.14); transition:transform .35s var(--ease), box-shadow .35s var(--ease); }
.tp-social-thumbs li:nth-child(1) a{ transform:rotate(-3deg); }
.tp-social-thumbs li:nth-child(2) a{ transform:translateY(-6px); }
.tp-social-thumbs li:nth-child(3) a{ transform:rotate(3deg); }
.tp-social-thumbs a:hover, .tp-social-thumbs a:focus-visible{ transform:translateY(-4px) scale(1.03); box-shadow:0 16px 32px rgba(22,16,68,.2); }
.tp-social-thumbs a:focus-visible{ outline:3px solid var(--brown); outline-offset:3px; }
.tp-social-thumbs img{ display:block; width:100%; height:auto; aspect-ratio:3 / 4; object-fit:cover; }
/* ⚠ THE ICON SITS ABOVE THE NAME, NOT BESIDE IT. Side by side, a third of the tile was never wide enough: "Instagram"
   needs 74px and got 66 on a desktop and 27 on a 360px phone, so the contact card read "Instagra", "Faceboo" (the mobile
   review, 2026-10-07, measured at seven widths). Stacked, each name has the whole tile, and the three sit under the
   three reel covers on the same 12px gap. */
.tp-social--compact .tp-social-list{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.tp-social--compact .tp-social-list li{ min-width:0; }
.tp-social--compact .tp-social-list a{ flex-direction:column; justify-content:center; gap:6px; padding:12px 6px; text-align:center; }
.tp-social--compact .tp-social-icon{ width:30px; height:30px; }
.tp-social--compact .tp-social-text{ flex:none; align-items:center; }
.tp-social--compact .tp-social-name{ font-size:13.5px; }
@media(prefers-reduced-motion:reduce){ .tp-social-thumbs a{ transition:none; } }

/* ════════════════════ ABOUT US — /about-us/ (page-about-us.php) ════════════════════
   The story as a timeline that fills as you read (assets/js/about-timeline.js), the mission on
   navy, the values as four cards and four long sections, then the team: a list of names whose
   photographs appear on hover, and the crews as one group photograph each. Jesse:
   "a follow along a story with northco kinda thing… have it feel intimate": so a reading column,
   the photographs soft-cornered beside each chapter, and V1's brown lead phrases kept. Without
   JavaScript every chapter shows and the line is drawn in full. */
.ab-article{ max-width:none; margin-inline:0; }
/* Not a .section, so its gutter is restated: .section's 48px (24px on a phone), or the story sat flush at x=0. */
.ab-story{ padding:calc(var(--nav-clear) + 52px) 48px 96px; }
.ab-title{
  margin:12px 0 56px;font-family:var(--heading);font-weight:800;color:var(--navy);
  font-size:clamp(38px,5vw,64px);line-height:1.02;letter-spacing:-.02em;
}
/* The scroll cue: a breathing dot at the head of the line, so it is plain that scrolling tells the story (Jesse: "to
   suggest a scroll down does something… might not be clear to our ICP"). It sits on the line, a short stem joining it
   to the line below; its words are chapter one's eyebrow ("have the scroll to follow our story be an eyebrow above it,
   so to welcome someone in"), so the link carries them as its label. It breathes with transform and opacity only, and
   holds still for reduced motion. */
.ab-cue{ display:flex;width:fit-content;margin:-24px 0 20px -7px;padding:3px 0;color:var(--brown);text-decoration:none; }
.ab-cue-dot{
  position:relative;isolation:isolate;flex:none;display:grid;place-items:center;width:38px;height:38px;border-radius:50%;
  background:var(--brown);color:#fff;box-shadow:0 6px 16px rgba(167,120,69,.28);transition:background .3s var(--ease);
}
.ab-cue-dot::before{
  content:"";position:absolute;inset:0;z-index:-1;border-radius:50%;background:var(--brown);
  animation:ab-cue-breathe 3s ease-in-out infinite;
}
/* The stem: from the cue down to the first chapter's dot (the cue's 20px margin, and that dot's 6px). */
.ab-cue-dot::after{ content:"";position:absolute;left:calc(50% - 1px);top:100%;width:2px;height:29px;background:var(--border-warm); }
.ab-cue-dot svg{ display:block; animation:ab-cue-nudge 3s ease-in-out infinite; }
@keyframes ab-cue-breathe{ 0%,100%{ transform:scale(1); opacity:.3; } 50%{ transform:scale(1.6); opacity:0; } }
@keyframes ab-cue-nudge{ 0%,100%{ transform:translateY(-1px); } 50%{ transform:translateY(3px); } }
.ab-cue:hover{ color:var(--navy); }
.ab-cue:hover .ab-cue-dot{ background:var(--navy); }
/*
 * The line out of the title, on a desktop (about-timeline.js draws it): from the end of "About NorthCo Services", down
 * and round into the centre line just above the cue, through it and on to the chapters' own line, so the space under the
 * title belongs to the line. Jesse sketched it on 2026-09-25 and chose the second of two mockups: "2 with the draw-in".
 * It draws itself in once the page has arrived. While it is shown (.has-lead), it stands in for the cue's grey stem, and
 * the cue sits on top of it.
 */
.ab-story .inner{ position:relative; }
.ab-lead{ display:none; position:absolute; left:0; top:0; width:100%; overflow:visible; pointer-events:none; }
.has-lead > .ab-lead{ display:block; }
.ab-lead path{
  fill:none; stroke:var(--brown); stroke-width:2; stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:1; stroke-dashoffset:1; opacity:0; transition:stroke-dashoffset 1.3s cubic-bezier(.65,0,.35,1);
}
.ab-lead.is-drawn path{ stroke-dashoffset:0; opacity:1; }
.has-lead .ab-cue{ position:relative; z-index:1; }
.has-lead .ab-cue-dot::after{ display:none; }
@media(prefers-reduced-motion:reduce){ .ab-lead path{ transition:none; } }
.ab-cue:focus-visible{ outline:3px solid var(--navy); outline-offset:4px; border-radius:999px; }
@media(prefers-reduced-motion:reduce){
  .ab-cue-dot::before{ animation:none; transform:scale(1.3); opacity:.16; }
  .ab-cue-dot svg{ animation:none; }
}
/* The cue's target. html's scroll-padding-top already clears the nav (a scroll-margin here doubled it, and Chrome's
   smooth scroll ignored it, so the glide and the jump landed 92px apart). The script focuses it: no ring needed. */
.ab-tl:focus{ outline:none; }
.ab-tl{ position:relative; list-style:none; margin:0; padding:0 0 0 48px; max-width:1120px; }
.ab-tl::before, .ab-tl::after{ content:""; position:absolute; left:11px; top:8px; width:2px; border-radius:2px; }
.ab-tl::before{ bottom:8px; background:var(--border-warm); }
.ab-tl::after{ height:calc((100% - 16px) * var(--ab-tl-p, 1)); background:var(--brown); }
.ab-step{ position:relative; display:grid; grid-template-columns:minmax(0,1fr); gap:24px; padding:0 0 80px; }
.ab-step:last-child{ padding-bottom:0; }
/* Desktop: the line down the centre and the chapters alternating sides, words on one, pictures on the other
   (Jesse: "can you have text alternate side to side? not all on the left"). A phone keeps one column, line on the left. */
@media(min-width:901px){
  .ab-tl{ padding-left:0; max-width:none; }
  .ab-tl::before, .ab-tl::after{ left:calc(50% - 1px); }
  /* Each chapter's words sit level with the middle of its picture (Jesse: "center this text with the image beside it"),
     chapter one included. The dot rides in the heading, so it stays on the line wherever the words land. */
  .ab-step{ grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:120px; align-items:center; }
  /* The dot rides in the heading, so it is placed from the heading's column onto the line: 60px past an odd chapter's
     words, 60px before an even one's (half the 120px gap), less half the dot. .ab-tl in front: the base .ab-step-dot
     rule comes later in this file and would otherwise win. */
  .ab-tl .ab-step-dot{ left:calc(100% + 48px); }
  .ab-tl .ab-step:nth-child(even) .ab-step-dot{ left:-72px; }
  .ab-step-copy{ grid-column:1; grid-row:1; }
  .ab-step-media{ grid-column:2; grid-row:1; }
  .ab-step:nth-child(even) .ab-step-copy{ grid-column:2; }
  .ab-step:nth-child(even) .ab-step-media{ grid-column:1; justify-content:flex-end; }
  /* Words right of the line: a link's arrow comes first and turns to face the line (Jesse: "make the arrow face the
     line - it's backwards rn"). Left of it, the arrow already points at the line. */
  .ab-step:nth-child(even) .ab-step-copy .ab-step-link{ flex-direction:row-reverse; }
  .ab-step:nth-child(even) .ab-step-copy .ab-step-link > span[aria-hidden]{ transform:scaleX(-1); }
  .ab-cue{ margin:-20px auto 20px; }
}
.ab-step-dot{
  position:absolute;left:-48px;top:6px;width:24px;height:24px;border-radius:50%;
  background:var(--cream);border:2px solid var(--border-warm);
  transition:background .5s var(--ease),border-color .5s var(--ease),box-shadow .5s var(--ease);
}
.ab-step.is-in .ab-step-dot{ background:var(--brown); border-color:var(--brown); box-shadow:0 0 0 6px rgba(167,120,69,.16); }
/* .ab-step-copy in front: the chapter's paragraph rule would otherwise make the eyebrow body text. */
.ab-step-copy .ab-step-eyebrow{ max-width:none; margin:0 0 14px; font-size:12px; line-height:1.4; color:var(--brown); white-space:normal; }
/* Balanced lines, so a heading breaks where its phrases do: "We Started with a Paintbrush, / and Built Something
   Bigger." (Jesse: "should "and built" start on the second line?"), and no last word left alone on a line ("build.",
   "Started."). The text is unchanged; an older browser wraps as before. */
.ab-step-h{
  position:relative;margin:0 0 14px;font-family:var(--heading);font-weight:800;color:var(--navy);
  font-size:clamp(24px,2.6vw,34px);line-height:1.15;letter-spacing:var(--track-heading, -.01em);text-wrap:balance;
}
.ab-accent{ color:var(--brown); }
.ab-step-copy p{ margin:0 0 14px; max-width:58ch; font-size:17px; line-height:1.75; color:var(--text-2); }
.ab-step-copy p strong{ color:var(--navy); }
.ab-step-link{
  display:inline-flex;align-items:center;gap:8px;margin:6px 18px 0 0;
  font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--brown);text-decoration:none;
}
.ab-step-link:hover{ color:var(--navy); }
/* No card around the pictures: the flyer and the OBJ artwork carry their own frames, and a card around them was a
   frame inside a frame (Jesse: "the frame inside a pill frame you know? not ideal"). drop-shadow follows the picture's
   own shape, transparent edges included. Tall pictures are capped, so the flyer does not run a screen and a half. */
.ab-step-media{ display:flex; flex-wrap:wrap; align-items:flex-start; gap:16px; }
.ab-shot{ margin:0; flex:1 1 0; min-width:0; }
.ab-step-media--1 .ab-shot{ flex:0 1 auto; }
.ab-shot img{ display:block; width:auto; max-width:100%; height:auto; max-height:560px; border-radius:18px; filter:drop-shadow(0 14px 26px rgba(22,16,68,.14)); }
/* Chapter one's pictures: the first flyer tall, the founders in the painting days beside it (Jesse: "a humble real
   opener… impact is bigger alongside the pamphlet"). The flyer's column is the flyer's own width at the grid's height
   (0.476 of it, over the grid's 1.22 ratio: 39%), so it is never cropped; the two photographs share the rest. */
.ab-step-media--collage{ display:block; width:100%; }
.ab-collage{ margin:0; }
.ab-collage-grid{ display:grid; grid-template-columns:39% minmax(0,1fr); grid-template-rows:repeat(2,minmax(0,1fr)); gap:14px; aspect-ratio:1.22; }
.ab-collage-grid img{ display:block; width:100%; height:100%; object-fit:cover; object-position:50% 30%; border-radius:16px; box-shadow:0 14px 26px rgba(22,16,68,.14); }
.ab-collage-grid .ab-collage-flyer{ grid-row:1 / 3; object-position:center; }
@media(max-width:560px){
  /* Square on a phone, so the photographs are not slivers: the flyer's column grows to match (0.476). */
  .ab-collage-grid{ grid-template-columns:47.6% minmax(0,1fr); gap:10px; aspect-ratio:1; }
  .ab-collage-grid img{ border-radius:12px; }
}
/* Who is in the pictures. */
.ab-shot-cap{ margin:14px 0 0; font-size:14px; line-height:1.5; color:var(--muted); text-align:center; }
/* Theme photographs, square, filling the column beside the words, two side by side (Jesse: "crop both to fit their
   subject matter side by side"). A plain box-shadow: a photograph has no transparent edges for drop-shadow to follow.
   A phone stacks them, each a landscape slice (.ab-shot-top keeps a subject that sits high in the frame). */
.ab-step-media .ab-shot--photo{ flex:1 1 0; }
.ab-shot--photo img{ width:100%; max-height:none; aspect-ratio:1 / 1; object-fit:cover; filter:none; box-shadow:0 14px 26px rgba(22,16,68,.14); }
@media(max-width:560px){
  .ab-step-media .ab-shot--photo{ flex-basis:100%; }
  .ab-shot--photo img{ aspect-ratio:4 / 3; }
  .ab-shot--photo img.ab-shot-top{ object-position:center 20%; }
}
/* One photograph across the column, landscape, with its line under it (the booklet chapter's home show booth). */
.ab-shot.ab-shot--wide img{ aspect-ratio:3 / 2; }
/* A picture at its own shape, the framed press features: no square crop. */
.ab-shot.ab-shot--frame img{ aspect-ratio:auto; object-fit:contain; border-radius:4px; }
/* The team at ten years: across the full width under the chapter's words, over the line, because a group this wide is
   faces too small to see in half a column. A phone runs it edge to edge. */
.ab-shot.ab-shot--band img{ aspect-ratio:auto; }
.ab-tl .ab-step .ab-step-media--band{ position:relative; z-index:1; }
/* "That's us!", hand-drawn on the wall above the team, writing itself stroke by stroke as the photograph comes into view
   (about-timeline.js arms it; each path's --t and --d are its start and length). Without the script, or with reduced
   motion, it is simply there. On a phone the wall is a sliver, so it sits above the photograph instead, in brown. */
.ab-shot--band{ position:relative; }
/* Sized off the photograph, not the screen, so the arrow lands on the same patch of wall at every width. */
.ab-note{
  position:absolute;left:4%;top:4%;width:28%;height:auto;color:#FAF7F2;
  transform:rotate(-5deg);filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));pointer-events:none;
}
.ab-note path{ stroke:currentColor; }
/* A gap longer than the stroke, pushed just past its start: dash and gap of 1 each, offset 1, left a dot at each stroke's
   ends before it was drawn (the homepage's note has the same fix and the reason in full). */
.ab-note.is-armed path{ stroke-dasharray:1 2; stroke-dashoffset:1.01; }
.ab-note.is-armed.is-drawn path{ stroke-dashoffset:0; transition:stroke-dashoffset var(--d,.3s) ease-in-out var(--t,0s); }
@media(max-width:768px){ .ab-note{ left:auto; right:14px; top:-44px; width:220px; color:var(--brown); filter:none; } }
@media(min-width:901px){
  .ab-tl .ab-step .ab-step-media--band{ grid-column:1 / -1; grid-row:2; margin-top:16px; }
  /* Above the photograph, the heading sits left of the line and its words right of it, so neither half is empty. On
     the right, the link's arrow faces the line, as in every chapter there. */
  .ab-tl .ab-step--band .ab-step-copy{ grid-column:1 / -1; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:120px; align-items:start; }
  .ab-tl .ab-step--band .ab-step-copy > *{ grid-column:2; }
  .ab-tl .ab-step--band .ab-step-copy > .ab-step-h{ grid-column:1; grid-row:1 / span 9; }
  .ab-tl .ab-step--band .ab-step-link{ flex-direction:row-reverse; justify-self:start; }
  .ab-tl .ab-step--band .ab-step-link > span[aria-hidden]{ transform:scaleX(-1); }
}
@media(max-width:900px){
  .ab-tl .ab-step .ab-step-media--band{ margin:0 -48px 0 -96px; }
  .ab-shot.ab-shot--band img{ border-radius:0; }
}
@media(max-width:768px){
  .ab-tl .ab-step .ab-step-media--band{ margin:0 -24px 0 -60px; }
}
/* The portfolio hero in small: three panels, each drifting slowly, each a link to its family. */
.ab-pf{ flex-direction:column; align-items:stretch; width:100%; }
.ab-pf-tri{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:4px; height:380px; border-radius:18px; overflow:hidden; box-shadow:0 14px 30px rgba(22,16,68,.12); }
.ab-pf-panel{ position:relative; display:block; overflow:hidden; background:var(--off); }
.ab-pf-panel img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:ab-pf-drift 16s var(--ease) infinite alternate; }
.ab-pf-panel:nth-child(2) img{ animation-delay:-5s; }
.ab-pf-panel:nth-child(3) img{ animation-delay:-10s; }
@keyframes ab-pf-drift{ from{ transform:scale(1.04) translate(0,0); } to{ transform:scale(1.16) translate(-3%,-2%); } }
.ab-pf-label{ position:absolute; left:10px; bottom:10px; padding:6px 11px; border-radius:999px; background:rgba(22,16,68,.78); color:#fff; font-size:13px; font-weight:700; }
.ab-pf-panel:hover .ab-pf-label, .ab-pf-panel:focus-visible .ab-pf-label{ background:var(--brown); }
.ab-pf-panel:focus-visible{ outline:3px solid var(--brown); outline-offset:-3px; }
@media(prefers-reduced-motion:reduce){ .ab-pf-panel img{ animation:none; } }
.ab-tl.is-armed .ab-step{ opacity:0; transform:translateY(26px); transition:opacity .8s var(--ease-reveal), transform .8s var(--ease-reveal); }
.ab-tl.is-armed .ab-step.is-in{ opacity:1; transform:none; }
@media(prefers-reduced-motion:reduce){
  .ab-tl.is-armed .ab-step{ opacity:1; transform:none; transition:none; }
  .ab-step-dot{ transition:none; }
}

/* What makes us different: the booklet's four, in its words, after the story that earned them. Rules over the four,
   not cards, so it does not repeat the values' four cards just below. "How are we different? Check it out" lands here. */
.ab-diff{ background:var(--off); scroll-margin-top:calc(var(--nav-clear) + 16px); }
.ab-diff-head{ margin:0 0 44px; }   /* no width cap since 2026-10-07: the heading ran three lines (Jesse: "wrapping too early"); two now */
.ab-diff-h{ margin:0; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(28px,3.2vw,44px); line-height:1.1; letter-spacing:var(--track-heading, -.01em); text-wrap:balance; }
.ab-diff-list{ list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:32px; }
.ab-diff-item{ display:flex; flex-direction:column; gap:10px; padding-top:20px; border-top:2px solid var(--brown); }
.ab-diff-name{ margin:0; font-family:var(--heading); font-weight:800; font-size:20px; color:var(--navy); letter-spacing:var(--track-heading, normal); }
.ab-diff-p{ margin:0; font-size:16px; line-height:1.6; color:var(--text-2); }
@media(max-width:1100px){ .ab-diff-list{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media(max-width:560px){ .ab-diff-list{ grid-template-columns:1fr; gap:24px; } }

/* The mission: one centred statement, introducing the values below it (2026-10-07; it was two columns, the heading alone on
   the left). The words keep a reading measure under the heading; the first line leads. */
.ab-mission{ background:var(--navy); text-align:center; }
.ab-mission-h{ margin:0 0 22px; font-family:var(--heading); font-weight:800; color:#fff; font-size:clamp(32px,3.6vw,48px); line-height:1.05; letter-spacing:var(--track-heading, normal); }
.ab-kicker{ margin:0 0 14px; font-size:13px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--brown-light); }
.ab-mission-copy{ max-width:880px; margin:0 auto; }
.ab-mission-p{ margin:0 0 12px; font-size:18px; line-height:1.7; color:rgba(255,255,255,.82); text-wrap:pretty; }
.ab-mission-p strong{ color:#fff; font-weight:600; }
.ab-mission-copy .ab-mission-p:first-child{ font-size:clamp(19px,1.6vw,22px); color:#fff; }
.ab-mission-go{ margin-top:18px; }

.ab-values{ scroll-margin-top:calc(var(--nav-clear) + 16px); }
.ab-values-head{ max-width:760px; margin:0 0 40px; }
.ab-values-h{ margin:0 0 10px; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(30px,3.4vw,46px); line-height:1.06; letter-spacing:var(--track-heading, normal); }
.ab-values-sub{ margin:0 0 12px; font-size:13px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:var(--brown); }
.ab-values-lead{ margin:0; font-size:18px; line-height:1.65; color:var(--text-2); }
.ab-cards{ list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:20px; }
.ab-card-v{ display:flex; flex-direction:column; gap:10px; padding:26px 24px; background:#fff; border:1px solid var(--border-warm); border-radius:20px; }
.ab-card-n{ font-family:var(--heading); font-weight:800; font-size:14px; letter-spacing:.1em; color:var(--brown); }
.ab-card-name{ margin:0; font-family:var(--heading); font-weight:800; font-size:19px; letter-spacing:.02em; color:var(--navy); }
.ab-card-tag{ flex:1; margin:0; font-size:15.5px; line-height:1.55; color:var(--text-2); }
.ab-card-go{ font-size:12.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--brown); text-decoration:none; }
.ab-card-go:hover{ color:var(--navy); }

.ab-value{ scroll-margin-top:calc(var(--nav-clear) + 16px); }
.ab-value--alt{ background:var(--off); }
/* The name across the top, then the photograph and the words side by side, starting on the same line (Jesse: "values
   are not lined up"). The words used to start level with the small number, and the photograph 140px lower. */
/* Since 2026-10-07 the name sits over the words on the left, and the photograph runs taller on the right beside both
   (Jesse: "put the title above the words on the left side and then make the photo bigger vertically and scroll like the
   interior service page images scroll"). It still drifts (motion.js); the image is 130% of the frame. */
/* Since 2026-10-15 the name and words sit CENTRED against the photograph (Jesse, on the empty space under each value's
   words: "do you want to center space the body wording or???"). Two empty 1fr rows above and below share out the
   photograph's extra height; the name and words hug their content in the middle. If the words ever run taller than the
   photograph, those rows go to zero and everything starts at the top, as before. */
.ab-value-grid{
  display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);grid-template-areas:". media" "head media" "copy media" ". media";
  grid-template-rows:1fr auto auto 1fr;
  gap:20px 64px;align-items:start;
}
.ab-value-copy{ grid-area:copy; }
/* Not sticky any more: the photograph under the name drifts, and the drift only reads as lag while its frame travels
   with the page (motion.js, SERVICE BANDS). Pinned, it would fight the scroll. */
.ab-value-head{ grid-area:head; min-width:0; }
.ab-value-name{ margin:8px 0 10px; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(28px,3vw,40px); line-height:1.08; letter-spacing:var(--track-heading, normal); }
.ab-value-tag{ margin:0; font-size:18px; font-weight:600; line-height:1.5; color:var(--brown); }
.ab-value-copy p{ margin:0 0 16px; max-width:62ch; font-size:17.5px; line-height:1.75; color:var(--text-2); }
.ab-value-copy strong{ color:var(--navy); }
/* Each value's photograph (V1's, for now) in a 3:2 frame under its name, drifting as the section scrolls (Jesse: "for
   each of the values we should have the scrolling image thing"). 3:2 keeps each picture's full width; the drift crops
   top and bottom only. */
.ab-value-media{ grid-area:media; aspect-ratio:4 / 5; border-radius:18px; overflow:hidden; background:var(--off); }
.ab-value-media img{ display:block; width:100%; height:100%; object-fit:cover; }

/* Where the story goes next: the 2035 ambition, where the timeline's line ends. The line runs on past the last chapter
   into a final dot: centred above the words on a desktop, on the rail beside them on a phone. */
.ab-tl-end{ --ab-tl-end-h:80px; position:relative; padding:72px 0 0 48px; }
/* The last stretch of line fills with the same reading progress as the rest (about-timeline.js sets --ab-tl-end-p), so
   the brown runs on from the last chapter into the dot (Jesse: "the line doesn't finish in the end dot"). */
.ab-tl-end::before, .ab-tl-end::after{ content:""; position:absolute; left:11px; top:-8px; width:2px; }
.ab-tl-end::before{ height:var(--ab-tl-end-h); background:var(--border-warm); }
.ab-tl-end::after{ height:calc(var(--ab-tl-end-h) * var(--ab-tl-end-p, 1)); background:var(--brown); }
.ab-tl-end-dot{
  position:absolute;z-index:1;left:3px;top:66px;width:18px;height:18px;border-radius:50%;
  background:var(--brown);border:2px solid var(--brown);box-shadow:0 0 0 6px rgba(167,120,69,.16);
  transition:background .5s var(--ease),border-color .5s var(--ease),box-shadow .5s var(--ease);
}
/* Hollow like an unread chapter's dot until the line reaches it, then lit. Only once the script arms it: without it,
   or with reduced motion, the dot is simply lit. */
.ab-tl-end.is-armed:not(.is-reached) .ab-tl-end-dot{ background:var(--cream); border-color:var(--border-warm); box-shadow:none; }
.ab-ambition-h{ margin:0; max-width:20ch; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(30px,4vw,54px); line-height:1.08; letter-spacing:var(--track-heading, -.015em); text-wrap:balance; }
.ab-ambition-p{ margin:18px 0 0; max-width:52ch; font-size:18px; line-height:1.65; color:var(--text-2); }
.ab-ambition-go{ display:flex; flex-wrap:wrap; gap:12px; margin-top:28px; }
@media(min-width:901px){
  .ab-tl-end{ display:flex; flex-direction:column; align-items:center; padding:104px 0 0; text-align:center; }
  .ab-tl-end{ --ab-tl-end-h:74px; }
  .ab-tl-end::before, .ab-tl-end::after{ left:calc(50% - 1px); }
  .ab-tl-end-dot{ left:calc(50% - 9px); top:66px; }
  .ab-ambition-go{ justify-content:center; }
}
@media(max-width:768px){
  .ab-tl-end{ padding-left:36px; }
  .ab-tl-end::before, .ab-tl-end::after{ left:9px; }
  .ab-tl-end-dot{ left:1px; }
}
.ab-close{ text-align:center; }
.ab-close-a{ margin:0 0 12px; font-size:14px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:var(--brown); }
.ab-close-b{ margin:0 auto; max-width:none; text-wrap:balance; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(28px,3.6vw,48px); line-height:1.12; letter-spacing:var(--track-heading, normal); }

.ab-team{ scroll-margin-top:calc(var(--nav-clear) + 16px); }
.ab-team-h{ margin:0; font-family:var(--heading); font-weight:800; color:var(--navy); font-size:clamp(30px,3.4vw,46px); line-height:1.06; letter-spacing:var(--track-heading, normal); }
.ab-team-group{ margin:44px 0 16px; font-family:var(--sans); font-size:13px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--brown); }
/* The people a homeowner meets, as a list: a name and a role per line, the face appearing on hover and following the
   cursor (Jesse's reference: quatrecentquatre.com/agence). A phone cannot hover, so there each name gets a small
   photograph beside it instead. The crews are one group photograph each. */
.ab-crew-block{ margin-top:44px; }
.ab-crew{ position:relative; list-style:none; margin:0; padding:0; border-top:1px solid var(--border-warm); }
.ab-crew-row{
  position:relative;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:baseline;gap:24px;
  padding:22px 4px;border-bottom:1px solid var(--border-warm);
}
.ab-crew-name{ font-family:var(--heading); font-weight:700; color:var(--navy); font-size:clamp(22px,2.4vw,32px); line-height:1.15; transition:color .25s var(--ease), transform .35s var(--ease); letter-spacing:var(--track-heading, normal); }
.ab-crew-role{ font-size:15px; letter-spacing:.02em; color:var(--muted); text-align:right; }
.ab-crew-thumb{ display:none; }
/* Each row's own photograph: what a desktop shows without the script, centred in the row's empty middle. With it,
   one frame floats for the whole list instead (below), and these step aside. */
.ab-crew-photo{
  position:absolute;left:50%;top:50%;z-index:5;width:240px;height:300px;object-fit:cover;object-position:center 18%;
  border-radius:14px;box-shadow:0 18px 40px rgba(22,16,68,.22);pointer-events:none;opacity:0;
  translate:-50% -50%;transition:opacity .3s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .ab-crew-row:hover .ab-crew-name{ color:var(--brown); transform:translateX(8px); }
  .ab-crew-row:hover .ab-crew-photo{ opacity:1; }
}
/* The floating frame (about-timeline.js). Jesse: "on desktop it should be bigger like the inspo site… have the
   transition between photos not be so jarring". So it is sized off the screen as quatrecentquatre's is, it glides after
   the cursor rather than sticking to it, and faces crossfade inside it. The script moves it with the translate
   property alone, so opacity and scale are free to transition here. */
.ab-team .inner{ position:relative; }
.is-floating .ab-crew-photo{ display:none; }
.ab-crew-float{
  position:absolute;left:0;top:0;z-index:6;width:clamp(280px,32vw,600px);aspect-ratio:4 / 5;border-radius:20px;
  overflow:hidden;background:var(--off);box-shadow:0 28px 64px rgba(22,16,68,.24);pointer-events:none;
  opacity:0;scale:.94;transition:opacity .45s var(--ease), scale .6s var(--ease);
}
.ab-crew-float.is-shown{ opacity:1; scale:1; }
/* One layer per person (about-timeline.js): their serious photograph, and the fun one over it. Person to person, whole
   layers cross-fade. */
.ab-crew-layer{ position:absolute; inset:0; opacity:0; scale:1.06; transition:opacity .55s var(--ease), scale .9s var(--ease); }
.ab-crew-layer.is-on{ opacity:1; scale:1; }
.ab-crew-layer img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:center 18%; }
/*
 * Serious to fun as a sweep: the fun photograph rises in behind a soft edge and sweeps back down (.75s), so there is no
 * see-through moment and no ghost of the other pose. The cross-fade before it faded both photographs at once. Jesse chose
 * this from five options side by side, 2026-09-25: "sweep for sure". The mask is 2.5 frames tall, its soft band a fifth
 * of a frame; is-swept slides it from all-clear to all-shown.
 */
.ab-crew-layer .ab-crew-fun{
  -webkit-mask-image:linear-gradient(to top,#000 46%,transparent 54%);mask-image:linear-gradient(to top,#000 46%,transparent 54%);
  -webkit-mask-size:100% 250%;mask-size:100% 250%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:0 0%;mask-position:0 0%;
  transition:-webkit-mask-position .75s cubic-bezier(.65,0,.35,1),mask-position .75s cubic-bezier(.65,0,.35,1);
}
.ab-crew-layer .ab-crew-fun.is-swept{ -webkit-mask-position:0 100%; mask-position:0 100%; }
/* A phone's list, by width as well as by input (2026-10-07, Jesse: "on mobile we need a simpler image view next to names …
   or a small image next to the name and title"): a narrow window with a mouse got the desktop hover list squeezed. */
@media (hover:none), (pointer:coarse), (max-width:768px){
  .ab-crew-photo{ display:none; }
  .ab-crew-row{ grid-template-columns:56px minmax(0,1fr); grid-template-areas:"thumb name" "thumb role"; align-items:center; gap:2px 14px; padding:14px 0; }
  .ab-crew-thumb{ display:block; grid-area:thumb; width:56px; height:56px; border-radius:50%; overflow:hidden; background:var(--off); }
  .ab-crew-thumb img{ display:block; width:100%; height:100%; object-fit:cover; object-position:center 22%; }
  .ab-crew-name{ grid-area:name; font-size:19px; }
  .ab-crew-role{ grid-area:role; text-align:left; font-size:14px; }
}
/* Two across: the interior and exterior trades (the shop joined interior, 2026-10-08; inc/about-team.php). */
.ab-crews{ list-style:none; margin:56px 0 0; padding:0; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:20px; }
/* 16:9, the group photographs' own shape (the interior crew's, 2026-11-06, is 1500x844): at 4:3 the people at each end were cut. */
.ab-crews-photo{ display:block; width:100%; aspect-ratio:16 / 9; object-fit:cover; border-radius:18px; background:var(--off); }
.ab-crews-photo--todo{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border:2px dashed var(--border-warm); color:var(--muted); font-size:14px; text-align:center; }
.ab-crews-photo--todo small{ font-size:11.5px; letter-spacing:.06em; text-transform:uppercase; }
/* Clickable since 2026-11-06 (photo-zoom.js wraps the photograph in .pz-open): the frame keeps the 18px corners and clips
   the hover zoom, as .pfp-shot does in a portfolio. */
.ab-crews .pz-open{ border-radius:18px; overflow:hidden; }
.ab-crews-h{ margin:12px 0 0; font-family:var(--heading); font-weight:700; font-size:18px; color:var(--navy); letter-spacing:var(--track-heading, normal); }
@media(prefers-reduced-motion:reduce){
  .ab-crew-name, .ab-crew-photo{ transition:none; }
  /* The frame holds still (the script stops gliding it) and faces change by a short fade alone. */
  .ab-crew-float, .ab-crew-layer{ scale:1; transition:opacity .2s linear; }
}

@media(max-width:1100px){ .ab-cards{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media(max-width:900px){
  .ab-step-media{ margin-top:4px; }
  .ab-pf-tri{ height:300px; }
  .ab-mission .inner, .ab-value-grid{ grid-template-columns:1fr; gap:20px; }
  .ab-value-grid{ grid-template-areas:"head" "media" "copy"; grid-template-rows:none; }   /* drop the centring rows when stacked */
  .ab-value-media{ aspect-ratio:3 / 2; }   /* stacked, the tall frame would fill the screen */
  .ab-value-head{ position:static; }
}
@media(max-width:768px){
  .ab-story{ padding:calc(var(--nav-clear) + 32px) 24px 64px; }
  .ab-title{ margin-bottom:40px; }
  .ab-tl{ padding-left:36px; }
  .ab-tl::before, .ab-tl::after{ left:9px; }
  .ab-cue{ margin:-16px 0 20px -7px; }
  .ab-cue-dot{ width:34px; height:34px; }
  .ab-step{ padding-bottom:56px; }
  .ab-step-dot{ left:-36px; width:20px; height:20px; }
  .ab-crews{ grid-template-columns:1fr; }
}
@media(max-width:560px){ .ab-cards{ grid-template-columns:1fr; } }

/* ════════════════════ OUR VALUES — /our-values/ (page-our-values.php) ════════════════════
   The four values in depth. It borrows About Us's components on purpose (the navy mission band, the four cards, the
   value sections with their drifting photographs), so the two pages read as one family; what is its own is the
   booklet's side of each value: its principle, a line on what it means, and what it looks like and doesn't. */
.ov-article{ max-width:none; margin-inline:0; }
/* Not a .section, so its gutter is restated, as About Us's story restates it. */
.ov-hero{ padding:calc(var(--nav-clear) + 52px) 48px 72px; }
.ov-title{
  margin:12px 0 22px;font-family:var(--heading);font-weight:800;color:var(--navy);
  font-size:clamp(38px,5vw,64px);line-height:1.02;letter-spacing:-.02em;
}
.ov-hero-lead{ margin:0; max-width:62ch; font-size:clamp(17px,1.5vw,20px); line-height:1.7; color:var(--text-2); }
.ov-quote{ margin:32px 0 0; }
.ov-quote blockquote{
  margin:0;font-family:var(--heading);font-weight:700;color:#fff;
  font-size:clamp(20px,1.9vw,26px);line-height:1.35;text-wrap:pretty;
}
.ov-quote figcaption{ margin-top:14px; font-size:14px; font-weight:600; letter-spacing:.04em; color:var(--brown-light); }
.ov-welcome strong{ color:#fff; }
.ov-principle{ margin:6px 0 0; font-size:12px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--brown); }
.ov-looks{ margin-top:48px; padding-top:40px; border-top:1px solid var(--border-warm); }
/* The body face at a medium weight, not the heading face: Partnership's line runs four sentences, and set as a heading
   it read as a wall of bold on a phone. */
.ov-lead{
  margin:0 0 32px;max-width:68ch;font-family:var(--sans);font-weight:500;color:var(--navy);
  font-size:clamp(17px,1.5vw,20px);line-height:1.6;text-wrap:pretty;
}
.ov-looks-grid{ display:grid; grid-template-columns:minmax(0,7fr) minmax(0,5fr); gap:48px; align-items:start; }
.ov-looks-h{ margin:0 0 16px; font-family:var(--heading); font-weight:800; font-size:20px; color:var(--navy); letter-spacing:var(--track-heading, normal); }
.ov-looks-k{ margin:0 0 16px; font-size:13px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
.ov-list{ list-style:none; margin:0; padding:0; display:grid; gap:12px; }
.ov-list li{ position:relative; padding-left:30px; font-size:17px; line-height:1.55; color:var(--text-2); }
.ov-list li::before{
  content:"\2713" / "";position:absolute;left:0;top:2px;width:20px;height:20px;border-radius:50%;
  display:grid;place-items:center;background:rgba(167,120,69,.14);color:var(--brown);font-size:12px;font-weight:800;
}
.ov-list--not li{ color:var(--muted); }
.ov-list--not li::before{ content:"\2715" / ""; background:rgba(107,102,118,.12); color:var(--muted); font-size:10px; }
.ov-close-go{ display:flex; flex-wrap:wrap; justify-content:center; gap:12px; margin-top:32px; }
@media(max-width:900px){ .ov-looks-grid{ grid-template-columns:1fr; gap:28px; } }
@media(max-width:768px){
  .ov-hero{ padding:calc(var(--nav-clear) + 32px) 24px 48px; }
  .ov-looks{ margin-top:36px; padding-top:28px; }
}

/* ════════════════════ INTERIOR SERVICES — /interior-services/ ════════════════════
   A SELECTOR page. Its top half is /portfolio/'s chooser reused as-is (.pf-head +
   .pf-choose + .pf-choice), so there is deliberately no CSS for that here.

   Below it, one band per service: the SEO copy in a column, a sticky image beside it.
   Modelled on serviceplusaquatics.com/services. See page-interior-services.php.
════════════════════ */

.svc-band{ padding:0 48px; }

/* The line between bands is a PSEUDO-ELEMENT rather than a border-top, so effect 3 below can
   draw it on. Un-transformed it renders exactly as the border did — which is what a visitor
   with no JavaScript still gets, because the scaleX(0) half lives behind html.nc-bands. */
.svc-band + .svc-band .svc-band-inner::before{
  content:"";
  position:absolute; top:0; left:0; right:0; height:1px;
  background:var(--border-warm);
  transform-origin:left center;
}

.svc-band-inner{
  position:relative;   /* anchors the drawn rules */
  max-width:var(--max);
  margin:0 auto;
  display:grid;
  /*
   * EQUAL COLUMNS — 640px each at 1440, which is about 65 characters a line.
   *
   * ⚠ THIS WAS 7fr 5fr, AND THE MEASURE IS WHY IT CHANGED. That split was chosen against the
   * reference site's 480/960 on the reasoning that 250 words in a 480px column is a trench.
   * True, and it overshot: 7fr gave the copy 747px, roughly 90 characters a line, well past
   * the 45-75 that reading research puts as comfortable. Jesse, holding V1 up against it:
   * "see how much easier the text is to read?" V1's column is ~550px. Ours was the widest of
   * the three and the hardest to read of the three.
   *
   * Equal also gives the photograph a 640px column, which at 4:5 stands 800px tall — level
   * with the open copy without either being stretched to the other. See the min-width block
   * further down for why nothing is stretched any more.
   */
  grid-template-columns:1fr 1fr;
  gap:64px;
  padding:64px 0 72px;
  align-items:start;
}

.svc-band-copy{ max-width:none; }
/* ── THE BAND TITLE HAS TO READ AS A TITLE ─────────────────────────────────────────────
   Jesse: "can you make the titles bigger? ... Kitchen Renovation isn't very title-based if
   you know what I mean".

   MEASURED, and he is describing a real ratio rather than a feeling. The generic
   .page-content scale gives h2 clamp(24px,3vw,32px) and h3 clamp(20px,2.2vw,25px), so at
   1440 the band title was 32px against a 25px sub-head sitting right beneath it — seven
   pixels apart, both navy, both bold. Two headings that close in size do not read as a title
   and a subtitle, they read as two subtitles.

   V1's is about 44px against a ~22px sub-head, roughly 2:1. This takes the title to 44px at
   1440, which is 1.76:1 — the sub-head is deliberately left alone, since the ask was for a
   bigger title and not a smaller H3. Say the word if it still competes.

   Scoped to this column rather than raising .page-content's h2 globally, which would resize
   every migrated heading on twelve service pages to fix one hub. */
.svc-band-copy h2{
  margin-top:0;
  /* ⚠ ON THE ONE HEADING SCALE (.section-heading's clamp), SINCE 2026-10-01. It was clamp(30px, 3.4vw, 44px), set
     against V1's band above, and the site's move to one 48px heading size missed it: on eleven pages the band title was
     the one section heading a step smaller than the rest (Crawling Dev's brief for Custom Kitchen Cabinets; Jesse,
     2026-09-29, on another heading: "not the same font size as the other headers - we JUST went through fixing
     this"). 48 against the ~25px sub-head is 1.9:1, nearer V1's 2:1 than 44 was. */
  font-size:clamp(30px,3.8vw,48px);
  /* 1.22 is fine at 32px and loose at 44px; the tracking follows the same logic. */
  line-height:1.1;
  letter-spacing:-0.02em;
}
/* The band title promoted to h1 (see .nc-h1-as-h2 above): the same four values, so it stays this title's size. */
.svc-band-copy h1.nc-h1-as-h2{ margin-top:0; font-size:clamp(30px,3.8vw,48px); line-height:1.1; letter-spacing:-0.02em; }

/* ⚠ A SEPARABLE SECOND TWEAK, flagged so it can be reverted on its own. The sub-head inherits
   the generic scale's margin:1.9em, which at 25px is a MEASURED 48px gap below the title —
   enough that the two read as unrelated blocks rather than as a title and its subtitle, which
   is part of why the title was not reading as a title. V1's pair sits about 20px apart.
   This is the only thing here that is not simply "bigger". */
.svc-band-copy h3{ margin-top:16px; }

/* ⚠ THE VERTICAL RULE IS GONE ON PURPOSE. There was a hairline here, in the middle of the
   column gap, copied from the reference's spec-sheet grid. It was added and removed the same
   day: a rule between the copy and the photo plus a rule between every band reads as a GRID,
   and the enclosure Jesse was actually asking for is the opposite of a grid — the reference's
   ruled look is the "hard cuts" he objected to. The horizontal line between bands stays,
   because without it the bands have nothing separating them at all. */

/* The reference's small "type" label above each service name. */
/* ── THE EYEBROW IS WHAT MAKES A BAND TITLE NOT A PAGE TITLE ───────────────────────────
   Jesse: the H1 and its strap "seem almost to take away from the Kitchen Renovation wording
   since it looks the same."

   MEASURED, and it was worse than looking the same — they matched on three axes out of four:

     H1  "Interior Renovation Services in Ottawa"   52px / 800 / navy / Poppins
     H2  "Pick your kitchen service."               52px / 800 / navy / Poppins
     H2  "Kitchen Renovation"                       44px / 800 / navy / Poppins

   One type treatment doing three jobs, four steps inside 12px. Eight pixels of size was the
   ONLY thing separating a page heading from a section heading, which is not enough to read as
   a difference — the same failure that had the 44px title indistinguishable from its own 25px
   sub-head one round ago, one level up.

   Fixing it on size alone would only widen the axis that was already doing the work. So the
   H1 goes to 60px (see .hub-seo .pf-title) AND the band title gains something the H1 has not
   got: a ruled eyebrow above it, so a band reads as a card header rather than a rival page
   heading. Three axes apart instead of one.

   ⚠ THE RULE IS THE SITE'S OWN, not a new one — .eyebrow::before is flex:0 1 32px, 2px tall,
   in --brown, and it is what every other eyebrow on the site already draws. Jesse's sales
   binder does the same thing ("—— HOOD FANS · 2 OF 3"), which is presumably where the instinct
   came from. currentColor rather than --brown so the rule follows this eyebrow's own
   --brown-dark instead of drifting a shade lighter than its text.

   Deliberately NOT applied to the page's own .eyebrow at the top: giving both the same rule
   would hand back the sameness this exists to remove.

   Scoped under .svc-band-copy because it is a <p> in a .page-content column, where a bare
   class loses its margin to `:is(.page-content,.post-content) p` — the trap that has now
   caught five rules on this page. */
.svc-band-copy .svc-band-type{
  font-family:var(--sans);
  font-size:11px; font-weight:700;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--brown-dark);
  margin:0 0 12px;
  display:flex; align-items:center; gap:12px;
  width:fit-content;              /* flex would otherwise span the whole column */
  white-space:nowrap;
  line-height:1.4;
}
/*
 * flex:0 0 32px, matching .eyebrow::before. This is a separate class rather than a variant of
 * .eyebrow, so it needs its own rule.
 *
 * It was written non-shrinking because a shrinkable basis inside a width:fit-content parent
 * collapses to its min-width — measured at 14px, which is a stub rather than a dash. That was
 * true of every eyebrow on the site when this was written; .eyebrow::before has since been
 * fixed the same way, so the two now agree by design rather than by coincidence.
 */
.svc-band-copy .svc-band-type::before{
  content:'';
  flex:0 0 32px; height:2px;
  background:currentColor;
}

/* ── THE PROSE AS A DISCLOSURE ─────────────────────────────────────────────────────────
   Native <details>, not a scripted accordion, and that is the whole reason it is safe here.
   It works with JavaScript blocked, it is keyboard-operable and screen-reader-announced for
   free, and the browser does the hiding — so this never becomes another case of our own CSS
   being what makes content invisible, which is the rule at the top of motion.js and a bug
   this project has already shipped once.

   The H3 is the handle. It is migrated ranking copy, so it stays an H3 at the same level and
   stays on screen collapsed; <summary> accepts heading content, so the outline is unchanged.

   Indexing: every word ships in the HTML on first byte, because it comes out of
   the_content(). Collapsed content is indexed and given full weight under mobile-first
   indexing; what would NOT be is content fetched on expand, which is exactly what this
   avoids. See the note in page-interior-services.php. */
/* ── V1's READING RHYTHM, WHICH WAS THE ACTUAL DIFFERENCE ──────────────────────────────
   Jesse, with V1 open beside V2: "see how much easier the text is to read? our current text
   imo is a regression".

   V1's copy is not better written — it is the same migrated words. It is better SPACED, in
   one specific way: the bold lead-in sits TIGHT against its own paragraph, and the air goes
   BETWEEN sections. Four labelled blocks, obvious at a glance.

   Ours applied `:is(.page-content,.post-content) p{ margin:0 0 1.15em }` uniformly, so the
   gap under "Not your average paint job:" was identical to the gap between sections — which
   reads as eight loose paragraphs rather than four labelled ones, and is also about 100px
   taller per band for nothing.

   No borders. The lead-ins do the dividing, which is what V1 does — a rule per section would
   put back the ruled grid that started this whole thread. */
.svc-points{ margin:26px 0 0; }

.svc-point + .svc-point{ margin-top:28px; }

/*
 * ⚠ SCOPED UNDER .svc-band-copy, AND THE WHOLE FIX DEPENDS ON IT. This is the FOURTH place in
 * this one column where `:is(.page-content,.post-content) p{ margin:0 0 1.15em }` at (0,1,1)
 * quietly beat a (0,1,0) class — after .svc-band-type, .svc-band-meta and .svc-band-cta.
 *
 * Here it mattered most: unscoped, the label's margin-bottom stayed at 1.15em, MEASURED as an
 * 18px gap between a lead-in and its own paragraph — identical to the gap between paragraphs.
 * Which is precisely the flattening this rule exists to undo, so the readability fix was
 * shipping as a no-op while reading as if it had been applied.
 *
 * Worth generalising: in this column any bare `.class` on a <p> loses. Scope it or it is
 * decoration that does nothing.
 */
.svc-band-copy .svc-point-label{
  margin:0 0 8px;                  /* tight to its own paragraph — this is the whole fix */
  font-family:var(--sans);
  font-size:16px; font-weight:700;
  line-height:1.4;
  color:var(--navy);
}

/* Same reason — a bare :first-child/:last-child here is (0,1,0) and loses too, which would
   have left the section gap carrying an extra 1.15em it was never meant to have. */
.svc-band-copy .svc-point-body > :first-child{ margin-top:0; }
.svc-band-copy .svc-point-body > :last-child{ margin-bottom:0; }

/* ── THE ONE CLEAR NEXT STEP ──
   Jesse: "a big learn more button at the bottom of each so it's clear". .btn-brown is the
   site's own primary button (13 uses across the templates), not a new one for this page.
   The label is V1's own trailing link text, which was previously an inline sentence lost at
   the end of the prose. */
/* ⚠ SCOPED FOR SPECIFICITY, NOT FOR TIDINESS. This column also carries .page-content, and
   `:is(.page-content,.post-content) p{ margin:0 0 1.15em }` is (0,1,1) — which beats a bare
   .svc-band-cta at (0,1,0). Unscoped, this rule silently lost its margin entirely, and so did
   the margin-top:auto that pins the button to the foot of the column: it computed to 0 and the
   button sat 230px short of the photo's bottom edge with no rule visibly at fault.

   ⚠ THE SAME COLLISION HITS .svc-band-type AND .svc-band-meta BELOW. Both are <p> inside this
   column, both declare a margin, and both have been overridden by that same rule the entire
   time — the eyebrow's 10px and the meta row's 26px have never applied. Left alone on purpose:
   correcting them changes spacing Jesse has already been looking at and approving, so it is a
   change to show him, not one to slip in alongside this. */
.svc-band-copy .svc-band-cta{ margin:26px 0 0; }

/*
 * ⚠ THE BUTTON WAS ILLEGIBLE, AND NOT AS A MATTER OF TASTE. Jesse: "button is illegible".
 * Same collision as the margins above, one property over: `:is(.page-content,.post-content) a`
 * is (0,1,1) and beats .btn-brown at (0,1,0). So the button rendered var(--brown-dark) TEXT on
 * a var(--brown) FILL — brown on brown — with .btn's text-decoration:none overridden back to
 * an underline. Nothing was wrong with the button; the selector simply lost.
 *
 * Scoped to (0,2,0) so the button's own colours win inside a .page-content column.
 */
.svc-band-cta .btn{
  /* ⚠ The colour and the underline are the fix for the illegibility, and the type is the fix
     for the wall of caps. .btn is 12px/0.07em/uppercase, which is right for "FREE ESTIMATE"
     in the nav and wrong for a 46-character sentence. V1 sets this label mixed-case at
     roughly 18px and lets it wrap to two lines; so does this. */
  color:#fff;
  text-decoration:none;
  font-size:17px;
  letter-spacing:0;
  text-transform:none;
  line-height:1.3;
  padding:17px 30px;
  border-radius:12px;
  white-space:normal;              /* .btn is inline-flex, which would otherwise not wrap */
  text-align:center;
  display:inline-block;            /* two-line centred text, not a flex row */
}
.svc-band-cta .btn:hover{ color:#fff; }
.svc-band-cta .btn{ width:100%; text-align:center; }

@media(min-width:600px){
  /* Full-width is right on a phone and looks like a form control on a desktop column, so it
     shrinks to its label once there is room. */
  .svc-band-cta .btn{ width:auto; }
}

.svc-band-meta{
  display:flex; align-items:baseline; gap:14px;
  margin:26px 0 0; padding-top:18px;
  border-top:1px solid var(--border-warm);
  font-family:var(--sans); font-size:14px;
}
.svc-band-meta-label{
  font-size:11px; font-weight:700;
  letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted);
  flex:0 0 auto;
}
.svc-band-meta a{ font-weight:700; color:var(--brown-dark); text-decoration:none; border-bottom:1px solid rgba(140,98,54,.35); }
.svc-band-meta a:hover{ color:var(--brown-light); }

/* ── THE IMAGE FRAME ──
   It SCROLLS WITH THE PAGE, exactly as the reference's does. Taller than it was, and capped
   against the viewport so a short laptop still sees the whole frame.

   ⚠ TWO CORRECTIONS, BOTH 2026-09-08, AND THE SECOND IS THE USEFUL ONE.

   1. This comment used to claim the reference's frame being shorter than its photo was "the
      whole effect — the crop, not a parallax script". Wrong on both halves.
      serviceplusaquatics.com runs framer-motion (useScroll -> useTransform ->
      <motion.img style={{y}}>), and the height:100% here left OUR image exactly filling its
      frame with zero slack, so ours was not even a crop — it was an entirely static photo.

   2. THE STICKY PIN IS GONE, AND IT WAS NEVER EARNING ITS KEEP. It was justified as the
      adaptation that let a 250-word column keep the reference's rhythm — the frame holding
      still while the prose scrolled past. Measured at 1440x900, the bands are 756-782px tall
      and the copy columns 521-646px against a 620px frame: the two columns were already the
      same height, so there was almost no band left for the pin to hold through. What it did
      instead was make the photo the only motionless thing on screen, so the drift inside it
      read as a second motion opposing the page rather than as lag. Jesse caught it on sight:
      "side image is scrolling, the words are scrolling".

      The reference has ONE motion — the page's — with the photo trailing inside a frame that
      travels with it. Now so do we. 864x664 in a 760px band there; 533x720 in an 856px band
      here, which is the same proportion at our narrower image column.

   ⚠ Do not re-add position:sticky without re-measuring the band heights. The pin only ever
   makes sense if the copy column becomes MUCH taller than the frame, which is not what this
   page's copy actually does. */
.svc-band-media{
  /*
   * 4:5 — AND THAT IS THE SOURCE PHOTOGRAPHS' OWN RATIO, not a taste call. They are
   * 1800x2250. So object-fit:cover discards nothing horizontally, where the previous
   * copy-driven height produced a 533x1070 panel (0.50:1) and threw away 37% off each side
   * of every kitchen. At the 533px column this is 666px tall, which lands within 2px of the
   * reference site's own 864x664 frame — arrived at independently, which is reassuring.
   */
  aspect-ratio:4 / 5;
  border-radius:18px;      /* matches .pf-choice's 18px, not the reference's sharp corners */
  overflow:hidden;
  background:var(--cream-raised);
  /* It is an <a> now — Jesse: "image isn't clickable" — and an anchor is inline by default,
     which would collapse the panel. */
  display:block;
}

/* The photograph goes where the button goes. Discoverable by the cursor plus a lift on hover;
   deliberately not a zoom, which would fight the drift already moving inside the frame. */
.svc-band-media[href]:hover{
  box-shadow:0 10px 30px rgba(29,42,58,.16);
}
.svc-band-media img{
  width:100%; height:100%;
  object-fit:cover; object-position:center;
  display:block;
}

/* ── THE THREE REFERENCE EFFECTS, ALL BEHIND html.nc-bands ─────────────────────────────
   serviceplusaquatics.com/services runs three separate things per band. Read out of its
   framer-motion bundle rather than guessed at, because the last reading of this page by eye
   got it wrong — see the correction above:

     1. DRIFT   image is height:130% of an overflow:hidden frame, and its translateY comes from
                the section's own scroll progress:
                  useScroll({ target: section, offset: ["start end", "end start"] })
                  useTransform(progress, [0, 1], ["-20%", "20%"])
     2. WIPE    the frame opens clip-path inset(0 0 100% 0) -> inset(0), 1.2s,
                cubic-bezier(.76,0,.24,1), once, when the band is ~30% in view.
     3. RULES   its borders draw themselves on — scaleY for the vertical, scaleX for the
                horizontal on a 0.67s delay.

   ⚠ EVERY HIDING HALF SITS BEHIND html.nc-bands, a class only motion.js adds, for exactly the
   reason the reveals do: with JavaScript blocked, slow, broken or simply not run, no image is
   ever clipped to nothing and no line is ever scaled to zero. The page then renders precisely
   as it did before this block existed. This project has already shipped that bug once with
   .fade-up — see the note at the top of motion.js.

   nc-bands is added only when motionWanted() is true — desktop >=900px AND no
   prefers-reduced-motion — so all three are desktop-only by construction. Mobile keeps
   today's static, un-slacked photo, which is why the 1100px block below undoes the slack.

   Turn any single one off from the URL, no code edit:
       ?no-drift   ?no-wipe   ?no-rules   ?no-bands   ?drift=0.5 */

/* 1. DRIFT — the slack the transform travels through. 130% of the frame leaves 30% of its
      height spare, centred by the flexbox, so the reach is +/-15% and the photo's edge can
      never come inside the frame. The reference over-travels its own slack by 173px and gets
      away with it only because both extremes happen off-screen. motion.js writes the
      transform; nothing here animates. */
html.nc-bands .svc-band-media{ display:flex; align-items:center; }
html.nc-bands .svc-band-media img{ height:130%; flex:none; }

/* 2. WIPE — `round 18px` repeats the frame's own radius. Without it the corners square
      themselves for the length of the animation and snap back at the end. */
/*
 * ⚠ THE WIPE IS GONE. Jesse, twice: "the photos need to load in - since right now they pop
 * in", and then "there's currently pop in for the hero images of the lower 4 - when scrolling
 * there's no image then it pops in".
 *
 * The second report is the effect's own failure mode, not a tuning problem. The wipe clipped
 * the frame to nothing until the photograph had decoded — which is what made the FIRST report
 * go away — so every time the bytes lost the race the visitor got an empty frame, then the
 * 4-second safety timeout opened it on nothing, and the photo arrived afterwards with no
 * transition at all. Exactly "no image, then it pops in", caused by the thing meant to
 * prevent it.
 *
 * An effect whose whole job is to introduce the picture, and whose failure mode is hiding the
 * picture, is not worth a third round. The photos are eager now (see the template) so they are
 * simply there, and the drift stays.
 */

/* 3. RULES — just the one line between bands now, drawing left to right. The vertical
      companion it used to have is gone; see the note where .svc-band-copy is defined. */
html.nc-bands .svc-band + .svc-band .svc-band-inner::before{ transform:scaleX(0); }
html.nc-bands .svc-band + .svc-band .svc-band-inner.is-ruled::before{
  transform:scaleX(1);
  transition:transform 1.2s cubic-bezier(.25,1,.5,1);
}

@media(max-width:1100px){
  .svc-band-inner{ grid-template-columns:1fr; gap:32px; padding:52px 0 56px; }

  /* ── STACKED, THE PHOTOGRAPH LEADS ─────────────────────────────────────────────────────
     Jesse: "pls put the images for each service at the top of the service, since currently
     they're below."
     Measured before: the grid ran [copy 1175px, media 256px], so the photo was ~1100px below
     the heading it belongs to — you read the whole band and then met its picture.
     Scoped to the ONE-COLUMN range rather than to phones, because that is the actual
     condition: at 1101+ the two columns put copy left and photo right, side by side, and
     there is no "above" to move it to.
     order, not a DOM change: the markup keeps heading-then-prose, so the reading order for
     a screen reader and for a crawler is untouched. Safe for focus too — the media is a
     redundant link already carrying aria-hidden and tabindex="-1", so it is not in the tab
     order to be reshuffled. */
  .svc-band-media{ order:-1; }
  /* One column: a 4:5 portrait the full width of a phone is a wall you scroll past, so the
     panel goes landscape here and the desktop ratio is deliberately not inherited. */
  .svc-band-media{ height:auto; aspect-ratio:16/10; }

  /* ONE COLUMN UNDOES TWO OF THE THREE, BUT ONLY IN THE TABLET RANGE NOW.
     Jesse: "for the way the images scroll here, use the spa scrolling behind image animation
     thing we have here on desktop" — so phones get the drift, and the flex + 130% slack that
     makes it possible has to survive down there.
     ⚠ min-width:769px added for exactly that. This override still applies from 769 to 1100,
     where the vertical rule would hang in a gutter with no second column to divide; below
     769 the base nc-bands rules above take effect again and the image has its slack back. */
  @media(min-width:769px){
    html.nc-bands .svc-band-media{ display:block; }
    html.nc-bands .svc-band-media img{ height:100%; }
  }
}
@media(max-width:768px){
  .svc-band{ padding:0 24px; }
  .svc-band-inner{ padding:40px 0 44px; gap:24px; }
  .svc-band-media{ aspect-ratio:4/3; }
}

/* ── MORE KITCHEN SERVICES ─────────────────────────────────────────────────────────────
   Three children of this hub that it had never linked: 69,795 impressions and 88 clicks
   between them, with zero routes in from their own parent. See the note in the template for
   the measurements and for why they are a link row rather than three more photo tiles.

   Chips rather than a bare list of links. The ICP is 40-65 on a phone 85% of the time, and a
   bordered target you can see the edges of beats an underlined word — the same reason the
   band CTA became a real button. 12px radius matches that button rather than .pf-choice's
   18px, because these are controls, not cards. */
.svc-more{ padding:0 48px; }

/* The one line that leaves the kitchen silo. Quiet on purpose — it sits under a list of four
   in-silo services and should read as "and if you are here for something else", not as a fifth
   option competing with them. */
.svc-more-cross{
  margin:28px 0 0;
  font-size:15px; line-height:1.6; color:var(--muted);
}
.svc-more-cross a{
  color:var(--navy); font-weight:700; text-decoration:none;
  border-bottom:1px solid var(--border-warm);
  transition:color var(--dur-base,200ms) var(--ease), border-color var(--dur-base,200ms) var(--ease);
}
.svc-more-cross a:hover{ color:var(--brown); border-color:var(--brown); }
.svc-more-cross a span{ display:inline-block; margin-left:2px; }

/* The override that used to live here is gone: it pinned this eyebrow's rule to 32px to stop
   the page carrying two different rule lengths one screen apart, and .eyebrow::before now does
   that for every eyebrow on the site. Kept as a note so nobody re-adds it. */
.svc-more-inner{
  max-width:var(--max);
  margin:0 auto;
  padding:8px 0 64px;
  border-top:1px solid var(--border-warm);
  padding-top:40px;
}
/* ── NOW AT EVERY WIDTH, NOT JUST PHONES ──────────────────────────────────────────────
   This started as a mobile fix and desktop measured WORSE: 64px of trailing padding here
   plus .rv-section's 96px top = a 160px gap between the chips and What our clients say,
   against 104 on a phone. It was half of what Jesse meant by "on desktop it's weird rn -
   empty af" — the other half being three chips filling 55% of a 1344px row, which the photo
   cards below fix.

   The base rule's own comment says why the trailing space exists at all: 64px, "because it
   was built to close the interior page". It closes nothing now; the reviews section follows
   it directly, confirmed in the DOM.

   :not(:last-child) so it cannot rot — a .svc-more that really does end a page keeps its
   closing space. Desktop 160 -> 104, mobile unchanged at 64. */
.svc-more:not(:last-child) .svc-more-inner{ padding-bottom:8px; }

.svc-more-list{
  display:flex; flex-wrap:wrap; gap:12px;
  list-style:none; margin:16px 0 0; padding:0;
}
.svc-more-list a{
  display:inline-flex; align-items:center; gap:10px;
  min-height:44px;                 /* a real tap target, not a line of text */
  padding:11px 22px;
  border:1px solid var(--border-warm);
  border-radius:12px;
  background:var(--cream-raised);
  font-family:var(--sans); font-size:15px; font-weight:700;
  color:var(--brown-dark); text-decoration:none;
  transition:border-color .18s var(--ease), background .18s var(--ease), color .18s var(--ease);
}
.svc-more-list a:hover{
  border-color:var(--brown);
  background:#fff;
  color:var(--brown);
}
.svc-more-list a:focus-visible{
  outline:2px solid var(--brown-dark);
  outline-offset:3px;
}
/*
 * ⚠ SCOPED TO THE LABEL, NOT TO EVERY SPAN INSIDE THE LINK. It used to be
 * `.svc-more-list a span`, which was correct while the arrow was the ONLY span in there.
 * The card treatment below adds .svc-more-media and .svc-more-label, both spans, and the old
 * selector would have set 14px/line-height:1 on the label wrapper too.
 */
.svc-more-label{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  flex:1 1 auto; min-width:0;
}
/* The arrow is decorative and aria-hidden; nudge it rather than let the gap carry it. */
.svc-more-label span{ font-size:14px; line-height:1; flex:none; }

/* Hidden by default so a phone keeps the text chip; the card block below turns it on. */
.svc-more-media{ display:none; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   THE MATERIAL CHIPS END WHERE THE TILES END
   ══════════════════════════════════════════════════════════════════════════════════════
   Jesse: "can you make the siding material pills on desktop the same width as the two main
   hero buttons? bothers my OCD."

   ⚠ MEASURED THE ANCESTORS FIRST, because "looks misaligned" has been a container problem
   three times on this project. NOT this time: .pf-choose's tile row and .svc-more-inner are
   both 1344px wide and both start at x=48. Left edges already agree to the pixel. The right
   edge disagreed only because the chips are content-width in a wrapping flex row, so the row
   stops wherever the seventh label happens to end — 55px short on Jesse's monitor.

   ⚠ AND THEY DO NOT ALL FIT ON ONE ROW AT 1440. Their natural widths sum to 1313 and six
   12px gaps add 72, so the row needs 1385 in a 1344 container: at a 1440 viewport the seventh
   chip is alone on a second row. Jesse's screen is wider, which is why his screenshot shows
   one row. That is the reason for what looks like an over-specific breakpoint below.

   ⚠ WHY flex:1 1 auto AND NOT justify-content:space-between. space-between is the obvious
   answer and it is wrong here: the moment the row wraps it spreads each row independently, so
   a second row holding two chips puts one hard left and one hard right with a 1000px hole
   between them. flex-grow shares the free space equally instead, so every gap stays exactly
   12px and both edges land flush — and because chips only ever GROW from their content width,
   no label can be squeezed into wrapping.

   ⚠ SO IT IS GATED AT 1440, WHERE ALL SEVEN ARE KNOWN TO FIT. flex-grow on a wrapped row
   would stretch a lone seventh chip across the full 1344px, which is far worse than the 55px
   of slack being fixed. Below 1440 the chips keep today's behaviour: natural widths, left
   aligned, wrapping when they must. The 16px side padding buys the headroom — 1229 + 72 = 1301
   against a 1344 container, 43px of slack, rather than the 15px that padding:22 would leave. */
@media(min-width:1440px){
  .svc-more--pick .svc-more-list li{ flex:1 1 auto; }
  .svc-more--pick .svc-more-list a{ width:100%; padding:11px 16px; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   PHOTO CARDS FROM 769px UP
   ══════════════════════════════════════════════════════════════════════════════════════
   Jesse, on the desktop chips: "on desktop it's weird rn - empty af - how else could we
   handle these pills?" — and he picked photo cards from the options.

   MEASURED WHY IT READ EMPTY, so the fix targets the real cause: the <ul> is 1344px wide,
   the full container, but three chips only fill the left ~55% of it, and the block then had
   160px of dead space below (its own 64px bottom plus .rv-section's 96px top). Three cards
   on a 1fr grid use the whole width by construction, which is most of the problem gone.

   ⚠ 769px UP, NOT EVERYWHERE. A phone keeps the full-width text chips it already had: the
   complaint was desktop-only, cards there would add three photographs of height to the 85%
   of traffic that is mobile, and Jesse had just asked to make this page SHORTER on a phone.
   .svc-more-media stays display:none below 769.

   ⚠ THE PILLS ARE STILL SECONDARY AND MUST LOOK IT. Compact 16/10 landscape and one line of
   label — deliberately not the 4:5 portrait of the four service tiles above, which are the
   page's actual chooser. These three exist to route link equity to pages sitting on 69,795
   impressions and 88 clicks; that is worth a photograph, not equal billing. */
/*
 * ⚠ :not(.svc-more--pick) ON EVERY SELECTOR BELOW, AND IT IS NOT DEFENSIVE — IT IS A BUG FIX.
 *
 * .svc-more-list is SHARED. The exterior hub's "Siding materials we install" row uses the
 * same class with SEVEN items and no photographs, and the first version of this block styled
 * it too: measured at 1440 it turned seven compact chips into a 3-column grid of 435x50 boxes
 * with an 18px radius and padding:0, because .svc-more-list a{padding:0} assumes a photo is
 * supplying the shape. Maibec, Cape Cod, Celect, Vinyl, James Hardie, Fiber cement and
 * Longboard, each stretched across a third of the page. It shipped to the sandbox before I
 * caught it.
 *
 * The exterior row is chips BY DESIGN — .svc-more--pick exists precisely to mark "this is a
 * compact material list, not a chooser" — so the cards must be scoped to the block that has
 * photographs rather than to the class both blocks happen to use.
 */
@media(min-width:769px){
  .svc-more:not(.svc-more--pick) .svc-more-list{
    display:grid;
    grid-template-columns:repeat(3, minmax(0, 1fr));
    gap:20px;
  }
  .svc-more:not(.svc-more--pick) .svc-more-list li{ display:flex; }

  .svc-more:not(.svc-more--pick) .svc-more-list a{
    flex-direction:column; align-items:stretch;
    width:100%; padding:0; gap:0;
    border-radius:18px;      /* matches .pf-choice and .svc-band-media */
    overflow:hidden;         /* so the photo takes the corner radius */
  }

  .svc-more:not(.svc-more--pick) .svc-more-media{
    display:block;
    aspect-ratio:16 / 10;
    overflow:hidden;
    background:var(--cream-raised);
  }
  .svc-more:not(.svc-more--pick) .svc-more-media img{
    display:block; width:100%; height:100%;
    object-fit:cover;
    transition:transform .7s var(--ease);
  }
  /* The photo moves, the card does not: a lift would fight the four tiles above, which
     already own that gesture. */
  .svc-more:not(.svc-more--pick) .svc-more-list a:hover .svc-more-media img{ transform:scale(1.04); }

  .svc-more:not(.svc-more--pick) .svc-more-label{ padding:14px 18px 16px; }
}

@media(max-width:768px){
  .svc-more{ padding:0 24px; }
  .svc-more-inner{ padding-top:32px; padding-bottom:48px; }

  /* ── TWO PADDINGS WERE STACKING, AGAIN ────────────────────────────────────────────────
     Jesse: "pls fix the spacing here on the interior services page", on the gap between the
     More kitchen services chips and What our clients say.

     Measured, and it decomposes exactly: .svc-more-inner's 48px bottom + .rv-section's 56px
     top = 104, against a measured 103 chip-to-eyebrow. Neither value is wrong on its own;
     they were never meant to be adjacent.

     ⚠ THE BASE RULE'S OWN COMMENT EXPLAINS WHY IT HAS TRAILING SPACE AT ALL: 64px below,
     "because it was built to close the interior page". It no longer closes anything — this
     block is child 8 of 10 in #site-content and the reviews section follows it directly — so
     that space is a leftover from a layout that changed around it. Exactly the shape of the
     problem .svc-more--pick was invented to fix ("roughly 136px and a hairline between the
     tiles and the label").

     :not(:last-child) rather than a flat number, so it stays true either way: a .svc-more
     that really does end a page keeps its closing space, and one with a section after it
     lets that section's own padding own the gap. 8 + 56 = 64, which matches the 56px rhythm
     .rv-section already uses above and below itself. */
  /* Full width on a phone: three chips wrapping mid-row reads as debris. */
  .svc-more-list{ gap:10px; }
  .svc-more-list li{ width:100%; }
  .svc-more-list a{ width:100%; justify-content:space-between; }
}

/* ── THE PHOTO IS FLUSH WITH THE COPY, AND IT IS AN APERTURE ───────────────────────────
   Jesse picked this over the enclosing-card variant on 2026-09-08. The card version is gone;
   dev/render-card-variants.php in the history has it if it is ever wanted back.

   ── THE VOID WAS STRUCTURAL, NOT A BADLY CHOSEN NUMBER ──
   Jesse: "the reason this is throwing me is you don't have the images bottom to bottom with
   the text." Right, and no frame height fixes it. MEASURED against the old fixed frame:

       viewport   copy column   copy heights   frame   image vs copy bottom
       1440       747px         521-646        720     image hung 74-199px BELOW
       1250       636px         614-740        720     -106 to +20 — the sign FLIPPED

   Copy height is a function of column width, because text reflows; the frame's was a fixed
   min(82vh,720px). So they could never agree at every width, and they disagreed in OPPOSITE
   DIRECTIONS either side of about 1300px — which is why a height tuned by eye on one monitor
   was visibly wrong on another, and why three rounds of picking a better number would all
   have failed.

   ⚠ SO THE COPY SETS THE HEIGHT AND THE PHOTO FILLS IT — align-items:stretch. Flush at every
   width by construction rather than by number. Verified flush to the pixel (delta 0 on all
   four bands) at 1440 and 1250. Do not put a fixed height back on .svc-band-media.

   The photo is then heavily oversized inside that panel, so it reads as a large photograph
   glimpsed through a rounded window rather than a fitted picture — which is what Jesse asked
   for at the start: "the images scroll behind".

   Desktop only: below 1101px the bands are one column and there is no second column for the
   photo to be flush WITH, so this sits inside a min-width query. */
@media(min-width:1101px){

  /*
   * ⚠ THE COPY NO LONGER SETS THE PHOTO'S HEIGHT, AND THAT REVERSES THIS MORNING'S FIX ON
   * PURPOSE. Stretching the panel to the copy did make them flush — verified to the pixel —
   * and then the wpautop repair gave the copy its real paragraphs, took it from 584px to
   * 1,070px, and the flush panel became a 533x1070 sliver. The fix was correct about the
   * void and wrong about the cause: the void was never the panel's height, it was 250 words
   * of prose sitting beside one photograph.
   *
   * With the prose behind a disclosure the band is short, so the photo can hold a fixed,
   * good ratio and the COPY is what centres beside it. Collapsed, the photo is the taller
   * element and there is no void to fix. Expanded, the copy runs past it — which is a gap
   * that only exists while someone is actively reading, the moment it matters least.
   */
  /*
   * ⚠ THE PHOTO TAKES THE COPY'S HEIGHT, AND THIS TIME IT IS RIGHT — because the thing that
   * broke it is gone. Stretching the panel to the copy failed earlier today only because the
   * copy was 1,070px of open prose, which made the photo a 0.50:1 sliver. Behind disclosures
   * the copy is ~410px, so the same rule now yields roughly 533x410 — about 1.3:1, which is
   * the reference site's own 864x664 to two decimal places. The void cannot exist: there is
   * no second height to disagree with.
   *
   * ⚠ WHAT WAS HERE BEFORE, AND WHY IT WENT. A fixed 4:5 panel plus a flex column with
   * margin-top:auto on the button. It measured perfectly — eyebrow level with the photo's
   * top, button level with its bottom, both deltas 0 — and it looked terrible: ~330px of
   * dead cream between the last drop-down and the button, because pinning the ends of a
   * short column inside a tall photo spreads the emptiness rather than filling it. Jesse:
   * "This sucks man ... tons of empty space."
   *
   * That is the standing instruction, violated: do not optimise a measurable number over how
   * the thing looks. bottomDelta:0 was the number. The void was the design.
   *
   * The flex column went with it, and good — it stopped the copy's margins collapsing, which
   * is where the ~90px of air under the H2 came from.
   */
  /*
   * ⚠ THE PANEL TAKES THE COPY'S HEIGHT — and the number is finally in the right place, which
   * it was not the two previous times this was tried.
   *
   * Jesse: "button is a bit lower than the bottom of the image pls fix". A fixed 4:5 panel is
   * 800px at this column and the open copy measures 908px, so the copy overhung by 108px and
   * the button sat below the photograph. Stretching removes the second height entirely, so
   * they cannot disagree.
   *
   * WHY IT WORKS NOW AND DID NOT BEFORE. Stretching failed at 7fr (copy 747px wide, 1,070px
   * tall) because it made the photo 0.50:1 — a sliver cropping 37% off each side. Two things
   * changed since: the column went to 1fr (640px) for the measure, and V1's rhythm tightened
   * the copy to 908px. 640x908 is 0.70:1, which is V1's own 553x780 to a hundredth. The
   * derived shape is now the shape we wanted anyway.
   *
   * ⚠ So do not reinstate a fixed aspect-ratio here, and do not widen the copy column back
   * toward 7fr without re-measuring: both push the derived ratio back toward the sliver.
   */
  .svc-band-inner{ align-items:stretch; }
  .svc-band-media{ aspect-ratio:auto; }   /* 4:5 is the mobile case; here the row decides */

  /*
   * THE OVERSIZE IS GATED ON MOTION, because without the drift it buys nothing and costs a
   * tighter crop. Un-gated, a reduced-motion visitor would get 160% of the photo cropped to
   * the middle 62% for no reason.
   *
   * ABSOLUTE, NOT A PERCENTAGE HEIGHT ON A FLEX CHILD. The panel's height is content-derived
   * now, and a % height needs a definite containing block — which the stretched grid item
   * has, but the flex box the drift block sets up does not. Positioning the photo makes 160%
   * resolve against the panel's padding box in every case, so the slack is REAL rather than
   * silently collapsing to the panel's exact height. That collapse is the same "looks done,
   * changes nothing" failure this frame already shipped once with height:100%.
   */
  html.nc-bands .svc-band-media{ position:relative; display:block; }
  html.nc-bands .svc-band-media img{
    position:absolute; left:0; width:100%;
    top:-30%; height:160%;
  }
}

/* ── HEAD SPLIT IN TWO (/interior-services/) ──
   The page leads with the four tiles, so the top of it carries only an eyebrow and the H1
   moved below them. .pf-head's own padding is calc(var(--nav-clear) + 52px) to clear the fixed nav and
   then hold a heading and a strap; with just an eyebrow up there that is a canyon. */
.pf-head--pick{ padding-bottom:4px; }

/* ⚠ TRIMS THE BREATHING ROOM, NOT THE NAV CLEARANCE. Jesse: "you can tighten the padding a
   bit more here — maybe 10% ish". .pf-head is calc(var(--nav-clear) + 52px), where the 68px IS the fixed
   nav's height and the 52px is the air below it. Reducing the total to a flat number would
   work today and silently slice a heading under the bar the next time the nav's height
   changes — which has already happened once on this project, where a 56px mobile padding put
   an h1 twelve pixels beneath it. So the nav term stays symbolic and only the air moves:
   52 -> 44. Applies to both hubs, since they share this class and should stay identical. */
.pf-head--pick{ padding-top:calc(var(--nav-clear) + 44px); }
/* .pf-choose carries 44px of its own top padding, so the visible gap under the heading was
   the sum of both. Jesse: "bring the buttons up a bit, seems a bit far". */
/* ── THE H1 BELOW HAS TO PEEK ABOVE THE FOLD ──
   Jesse asked whether an animated down arrow should suggest scrolling. MEASURED first, and
   the measurement answered it:

       tiles section ends            874px
       H1 "Interior Renovation…"     944px  — 44px below a 900px fold
       visible space below the tiles  26px
       page height                 6,565px

   A cue IS warranted: at 900px the first screen ends with the tiles and 26px of cream, giving
   no sign that another 5,600px follows — and the quote form is at the bottom of it. But an
   ARROW cannot work here. There are 26px of room and an arrow needs 40-60, so it would sit
   below the fold itself: a scroll hint you have to scroll to find. Fixed positioning would fix
   that and overlap the tiles instead.

   So real content does the job. Cutting 80px of bottom padding brings the H1's top edge to
   ~864, which puts about 36px of its 66px line box on screen at a 900px fold — enough to read
   as a headline rather than a sliver. 96 -> 16 rather than a gentler trim because at 880 only
   20px showed, which is the tops of the letters and reads as a rendering glitch.

   The visual gap does not collapse to 16px: .hub-seo still contributes 56px of its own top
   padding, so the tiles' bottom edge to the H1 is 86px.

   ⚠ Scoped to --svc. .pf-choose's 96px bottom is shared with /portfolio/, which has no heading
   underneath to reveal and would just lose its spacing.

   The two viewport cases cover each other, which is why this needs nothing further: tall
   screens fit the tiles and get the peek; short screens (760, 860) clip the tiles by 114px and
   14px, and a clipped tile is its own cue. */
.pf-choose--svc{ padding-top:22px; padding-bottom:16px; }
/* SAME SIZE as the H1 below. I had set this smaller — clamp(24px,3.4vw,38px) — reasoning that
   an instruction should not compete with the page's subject heading. Wrong priority: this is
   the first line on the page and it was reading as a caption. Jesse: "too small, below website
   standard". It inherits .pf-title now, so the two stay in step if that scale ever changes. */
@media(max-width:768px){
  /* 18 -> 8. Jesse: "tighten the space a little bit between kitchen service and the kitchen
     renovation image bubble pill - like 5-10% imo."
     The gap is 39px and it is another stacked pair: this 18px of trailing air plus
     .pf-choose--svc's 22px of leading air. A literal 10% would be 4px and invisible, so the
     chooser's 22 is left to own the gap and only this side gives way: 39 -> 30, which is a
     quarter of it and actually reads. If it wants to be tighter still, this is the value —
     the 22 above it is the chooser's own rhythm and is shared with the exterior hub. */
  .pf-head--pick{ padding-bottom:8px; }

  /* ⚠ THE AIR SHRINKS, THE NAV TERM DOES NOT. Jesse: "Please shift the eyebrow for kitchens
     as well as Pick your kitchen service up - wasted space currently imo."
     44 -> 20, measured: the eyebrow sat at 112 absolute, 44px clear of a 68px nav, and now
     sits at 88. A phone has a third of the vertical room a desktop does, so the same air
     reads as a canyon here even though it is right up there.
     var(--nav-clear) stays symbolic for the reason in the note above: a flat number works
     today and slices a heading under the bar the next time the nav's height changes, which
     has already happened once on this project. */
  .pf-head--pick{ padding-top:calc(var(--nav-clear) + 20px); }
}

/* The H1 and strap, opening the explanatory half of the page.
   Named .hub-seo, NOT .svc-intro. I originally called it .svc-intro and collided with
   kitchen-intro.php, which already owns that class with max-width:760px — so this block
   inherited a 760px cap and sat hard left while every other row on the page centred a 1440px
   measure. Invisible at 1440, where the cap never binds; obvious at Jesse's ~1900px window,
   which is where he spotted it. Measured left edges: everything at 230, this at 48. */
/* 32px, not 56 — the second half of the fold peek; see the note at .pf-choose--svc. Trimming
   only the chooser put the H1's top at 864, which clears a 900px fold but not an 860px one,
   and 860 is what a 1512x982 laptop actually gives after browser chrome. 32px puts it at 841.
   The tiles' bottom edge to the H1 is then 62px, which is tighter than the 86px it was and
   still a clear section break. Mobile keeps 36px via the override below. */
.hub-seo{ padding:32px 48px 8px; }
.hub-seo-inner{ max-width:var(--max); margin:0 auto; }
.hub-seo .pf-strap{ max-width:62ch; }

/* ── THE PAGE HEADING OUTRANKS THE BAND TITLES ON SIZE TOO ──
   .pf-title is clamp(30px,5vw,52px), which put this H1 at 52px against a 44px band title —
   eight pixels, same weight, same colour, same family. See the note at .svc-band-type for the
   full measurement and for the second half of the fix.

   ⚠ THE MOBILE FLOOR MATTERS MORE THAN THE DESKTOP CAP, and raising only the cap would have
   missed it: .pf-title's minimum is 30px and the band title's is also 30px, so on a phone the
   two were EXACTLY equal — the collision was total there and merely bad at 1440. 38px against
   30px is 1.27:1. It wraps to three or four lines on a 342px column, which is what a real
   page heading does.

   Scoped to .hub-seo: .pf-title is shared with /portfolio/ and with the chooser heading above,
   and neither asked for a 60px title. */
.hub-seo .pf-title{ font-size:clamp(38px, 4.2vw, 60px); }
@media(max-width:768px){
  .hub-seo{ padding:36px 24px 4px; }
}

/* ── SERVICE CHOOSER (/interior-services/) ──
   Same tiles as /portfolio/, three adjustments Jesse asked for: larger, tighter together, and
   far less blue. Scoped with --svc so the portfolio chooser is untouched.

   LARGER comes from a taller frame plus a smaller gap, not from a wider container: --max is
   the site's measure and breaking it here would misalign the tiles against the heading above.
   2/3 instead of 4/5 takes a tile from 318x398 to about 325x487 at 1440. */
.pf-choose--svc .pf-choose-inner{ gap:14px; }
/* 3/5, not 2/3. Jesse: "make them taller, more commanding". At 325px wide that is 542px
   instead of 488 — noticeably more vertical without the tile outgrowing the fold, which a
   1/2 ratio would (650px, plus the eyebrow above it). */
.pf-choose--svc .pf-choice{ aspect-ratio:3 / 5; }

/* NO TINT OVERRIDE. The tiles use /portfolio/'s scrim exactly as it is.

   I had replaced it with a two-layer thing — a light scrim on the tile plus a ground on the
   copy block — chasing measured contrast ratios. Jesse: "revert the tile tinting - this is
   disgusting." He is right, and the lesson is the one worth keeping: I optimised a number and
   degraded the design, on a treatment he had already approved on /portfolio/. The contrast
   maths was correct and the judgement was wrong.

   If legibility does become a real problem on a specific photograph, the fix is that
   photograph, not a gradient stack. */

/* LIFT THE COPY OFF THE BOTTOM EDGE. Jesse: "shift the hero buttons up".
   The shared .pf-choice-body is padding:30px 30px 28px, which sits the block hard against the
   bottom of the tile. 44px of bottom padding gives it a floor to stand on, and because the
   block is bottom-anchored the whole group moves up together. */
.pf-choose--svc .pf-choice-body{ padding:30px 28px 64px; }

/* ── THE BLURB RESERVES THREE LINES SO THE FOUR TITLES SIT IN A ROW ──
   The tile body is bottom-anchored, so blurb height does not push the card downward — it
   pushes the TITLE upward. MEASURED at 1440 before this: titles at 312 / 270 / 291 / 312 from
   the top of their tiles, a 42px spread across a row that should read as one.

   Three lines rather than two because Cabinetry Refacing's 81-character sentence genuinely
   needs three at this measure, and shortening a complete, well-written line just to save a row
   of space is the wrong trade. The 105-character cap in the template handles the one real
   outlier; this reserves the room so the shorter three do not ride up to meet it.

   1.45em is this element's own line-height, so the three tracks stay correct if the type size
   ever changes. Scoped to --svc: /portfolio/'s chooser has its own copy lengths. */
.pf-choose--svc .pf-choice-blurb{ min-height:calc(1.45em * 3); }

@media(max-width:1100px){
  .pf-choose--svc .pf-choice{ aspect-ratio:4 / 5; }
}
@media(max-width:768px){
  .pf-choose--svc .pf-choose-inner{ gap:12px; }
  .pf-choose--svc .pf-choice{ aspect-ratio:3 / 2; }
  /* A phone tile is short and wide, so the generous floor would eat it. */
  .pf-choose--svc .pf-choice-body{ padding:24px 22px 26px; }
  /* ⚠ THE PEEK IS A DESKTOP CONCERN ONLY, so the spacing comes back here. On a phone the four
     tiles stack and the fourth is already clipped by the fold — the content itself is the cue,
     and 16px would just crush the join between two sections for no gain. .hub-seo contributes
     36px at this width, so this restores a 76px gap. */
  .pf-choose--svc{ padding-bottom:40px; }
}

/* TWO-TILE CHOOSER — /exterior-services/.
   The shared chooser grid is repeat(auto-fit, minmax(300px,1fr)), which with two tiles gives
   each a 660px column at 1440. At the chooser's 4:5 aspect that is 825px tall: a single tile
   taller than most laptop viewports. Widen the aspect rather than narrowing the column, so
   the pair still fills the row instead of leaving a gap beside it.

   Mobile already overrides .pf-choice to 3/2 in the shared block, so this needs nothing
   there. */
.pf-choose--two .pf-choice{ aspect-ratio:4 / 3; }

/* ── THE MATERIALS ROW SITS TIGHT UNDER THE CHOOSER ────────────────────────────────────
   Jesse: "the padding between these is a bit excessive". It was. .pf-choose carries 96px of
   bottom padding — right for a chooser that ENDS a section — and .svc-more-inner adds its
   own 40px above its rule and 64px below, because it was built to close the interior page.
   Stacked, that put roughly 136px and a hairline between the tiles and the label.

   Here the row is a NARROWING of the choice just made, so it should read as attached to the
   tiles above rather than as its own chapter. 40 + 24 instead of 96 + 40, and 24 below rather
   than 64 since the first band brings its own 64px of top padding. */
/*
 * ⚠ THE TOP ONE IS A CONSISTENCY FIX, NOT A TASTE TRIM. .pf-choose's base top padding is 44px,
 * and the interior hub overrides it to 22 (.pf-choose--svc) — so the gap under the heading was
 * MEASURED at 48px here against 26px there, on two pages that are meant to be the same thing.
 * Exterior was simply never given the override. 26px matches interior exactly.
 *
 * The rest is the ~10% Jesse asked for: 40 -> 32 below the tiles, 24 -> 20 either side of the
 * materials label. Top block total goes 268px -> 224px, about 16% tighter, of which the single
 * biggest piece is the 22px that was only ever an oversight.
 */
.pf-choose--two{ padding-top:22px; padding-bottom:32px; }
.svc-more--pick .svc-more-inner{ padding-top:20px; padding-bottom:20px; }

@media(max-width:768px){
  .pf-choose--two{ padding-bottom:24px; }
  .svc-more--pick .svc-more-inner{ padding-top:16px; padding-bottom:16px; }
}
@media(min-width:1100px){
  .pf-choose--two .pf-choice{ aspect-ratio:16 / 10; }
}

/* ════════════════════════════════════════════════════════════════════════════════════════
   TOPIC BODY — the Postmedia pages rebuilt as sections
   ════════════════════════════════════════════════════════════════════════════════════════

   The block above this one styles the FLAT rendering of those pages: it caps their runaway
   SVGs, gives their hrefless FAQ anchors a heading's weight, and puts button chrome back on
   the CTAs Elementor used to style. All of that still ships, because only the pages listed in
   northco_topic_layout_pages() are rebuilt and the rest still render as prose.

   ⚠ BUT IT WAS NOT ENOUGH, AND THAT IS THE POINT OF THIS BLOCK. Capping the SVGs took
   kitchen-designers from 26,250px to 15,987px — a real bug fixed — and the page still read as
   one 760px ribbon down the left of a 1440px screen. Jesse, twice: "completely broken layout
   wise and not like the rest of the site", then "Layout is bad, looks broken and uncomplete."

   With 187 tags carrying zero classes there is nothing for CSS to hook, so the structure is
   rebuilt in PHP — inc/topic-layout.php parses, template-parts/topic-body.php draws — and
   these rules dress what comes out. Everything here is a .tp-* class on markup this theme
   emits, so none of it depends on Postmedia's markup staying as it is.
════════════════════════════════════════════════════════════════════════════════════════ */

/* ── BANDS ────────────────────────────────────────────────────────────────────────────────
   The article is a stack of full-width sections now, so the alternation is what separates
   one stop from the next. Counted over RENDERED sections in the template, so a dropped group
   never leaves two same-coloured bands touching. */
.tp-band{ background:var(--cream); }
.tp-band--alt{ background:var(--off); }

/* ── THE LEAD ──────────────────────────────────────────────────────────────────────────── */
.tp-lead{ max-width:none; }
.tp-lead p{ font-size:clamp(17px,1.5vw,20px); line-height:1.6; color:var(--text-2); margin:0 0 16px; text-wrap:pretty; }
.tp-lead p:last-child{ margin-bottom:0; }
/* V1's geo siding pages colour their breadcrumb links white, inline, for the dark band V1 set them on; V2 sets them on
   cream, where "Exterior Renovation > Siding" vanished (the mobile review, 2026-10-07: Kanata, Nepean, Orléans and
   Stittsville). !important because nothing else outranks an inline style. */
.tp-lead a[style*="color: white"], .tp-lead a[style*="color:white"]{ color:var(--brown-dark) !important; }
/*
 * V1's bolding in running prose, off (Jesse, 2026-09-28: "The first H1 thing on most service pages, there's a ton of the
 * random bolding we did in V1 - it doesn't look super clean"). Sentences were set bold for emphasis mid-paragraph, in
 * the intro beside the first photograph and in the "… in Ottawa" band: 14 of them, on 4 pages. They now read at their
 * paragraph's own weight and colour. A paragraph that is bold from end to end is a lead-in or a label ("Want to go
 * further?", "Style refresh, zero teardown:"), and it keeps its bold, or a label over its paragraph reads as a broken
 * list. northco_lead_para() marks those 15 as .tp-lead-line (inc/topic-layout.php), because CSS cannot see text.
 * The <strong> tags themselves stay, so nothing changes for search or a screen reader. Bold elsewhere is untouched:
 * run-in list labels ("Superior Durability:", "Includes:"), the FAQ answers' one-line lead, and the estimator.
 */
.tp-band--intro .tp-intro-copy p:not(.tp-lead-line) :is(strong,b), .svc-band .svc-band-copy p:not(.tp-lead-line) :is(strong,b){ font-weight:inherit; color:inherit; }

.tp-cta{ display:flex; flex-wrap:wrap; gap:12px; margin-top:28px; }
/* A long label that wraps on a phone breaks evenly: "Not sure yet? Let's walk you through it!" left
   "IT!" alone on its second line at 390px (KR and CP). */
.tp-cta .btn{ text-wrap:balance; }
/* ── INTRO BAND: IMAGE LEFT, COPY RIGHT ───────────────────────────────────────────────────
   V1's opening band under the hero is two columns. Stacked into one it read as a heading, a
   wall of text, then a 675px photograph with half the page empty beside it. */
.tp-intro-split{
  display:grid; grid-template-columns:minmax(0,0.85fr) minmax(0,1fr);
  gap:clamp(32px,5vw,72px); align-items:center;
}
.tp-intro-media{ position:relative; }
.tp-intro-media img{
  width:100%; height:auto; display:block;
  border-radius:16px; position:relative; z-index:1;
}
/* ⚠ A GOLD BAR USED TO SIT BEHIND THE RIGHT EDGE OF THIS PHOTOGRAPH. Removed, not restyled —
   Jesse: "remove the weird vertical line we don't use that design standard anymore". Do not
   reintroduce it from V1: V1 having it is not a reason to keep it. */
.tp-intro-copy{ min-width:0; }

/* ── STAND-IN PHOTOGRAPH ────────────────────────────────────────────────────────────────────
   A real kitchen rather than a grey box — Jesse: "use a kitchen for now". Styled identically to
   a normal intro photo so that swapping the final shot in changes nothing but the src. */
.tp-intro-ph{
  width:100%; height:100%; display:block;
  object-fit:cover;
  border-radius:16px; position:relative; z-index:1;
}

/* ⚠ THE DRIFT NEEDS A FRAME TO DRIFT INSIDE. A naturally-sized image has no slack: the effect
   is an oversized photo translating within a fixed, clipped box. 675/487 is the V1 photo's own
   ratio, the same one the placeholder held, so opting in costs no layout change.

   ⚠ overflow:hidden IS LOAD-BEARING — without it the 130% image simply overlaps the copy. */
.tp-intro-media.nc-drift{
  aspect-ratio:675 / 487;
  overflow:hidden;
  border-radius:16px;
}
.tp-intro-media.nc-drift img{ width:100%; height:100%; object-fit:cover; display:block; }

/* The shared drift hook. Mirrors what html.nc-bands does to .svc-band-media, so any frame
   carrying .nc-drift inside a [data-drift-band] section gets the same motion — Jesse: "a good
   effect for static images across V2". Still behind nc-bands, so with JS blocked or slow
   nothing is ever clipped or over-sized. */
html.nc-bands .nc-drift{ display:flex; align-items:center; }
html.nc-bands .nc-drift img{ height:130%; flex:none; }
@media(max-width:1100px){
  /* Same call the bands make: no slack on smaller screens, so the photo stays put. */
  html.nc-bands .nc-drift{ display:block; }
  html.nc-bands .nc-drift img{ height:100%; }
}

/* ── JUMP LINKS ────────────────────────────────────────────────────────────────────────────
   .service-link is the homepage's component and is NOT redefined here — only laid out. The row
   inherits every hover, focus and border state it already has, so the two stay in step. */
.tp-jump{
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr));
  gap:12px; margin-top:clamp(28px,3.5vw,44px);
}
/* .service-link carries margin-bottom for the homepage's stacked column; in a grid row that
   just knocks the cards out of alignment with each other. */
.tp-jump .service-link{ margin-bottom:0; }
.tp-jump .sl-arr{ flex:none; margin-left:12px; }

@media(max-width:860px){
  .tp-jump{ grid-template-columns:1fr; gap:10px; }
}

/* ⚠ ANCHOR OFFSET LIVES HERE, NOT IN SCRIPT. The nav is fixed, so an un-offset jump parks the
   target heading underneath it. scroll-margin-top is honoured by the browser's own instant jump
   AND by scrollIntoView, so the no-JS path lands correctly too. */
#ku-process, #ku-value, #ku-steps, #ku-before-after{
  scroll-margin-top:calc(var(--nav-clear) + 24px);
}
.tp-intro-copy .tp-lead{ max-width:none; }
@media(max-width:900px){
  .tp-intro-split{ grid-template-columns:1fr; gap:28px; }
}

/* The intro: what appears before the page's first h2. Its title is a <p> since 2026-10-30 (see below). */
.tp-intro-block{ max-width:66ch; margin:26px 0 0; }
.tp-intro-split .tp-intro-block{ margin-top:0; }
.tp-intro-title{
  font-family:var(--heading); font-weight:800; color:var(--navy);
  font-size:clamp(20px,2.2vw,26px); line-height:1.25; letter-spacing:var(--track-heading, -0.01em); margin:0 0 8px;
  /* No one-word last line (the site's heading rule). Without it, measured 2026-10-01: KR "…outgrown your / kitchen?" and KU
     "…just not the / look?" at 1100px, and Kitchen Designers "…picture it / yet?" on a phone. Balanced, every intro title on
     the five pages that have one breaks into two even lines, or stays on one. */
  text-wrap:balance;
}
.tp-intro-block p{ margin:0 0 12px; color:var(--text-2); line-height:1.7; }
.tp-intro-block p:last-child{ margin-bottom:0; }
/* The title is a <p> since 2026-10-30 (topic-body.php): a lead-in question, which as a heading skipped levels after the
   h1 or came before it. Same look, so the three values .tp-intro-block p would change are put back. */
.tp-intro-block p.tp-intro-title{ margin:0 0 8px; color:var(--navy); line-height:1.25; }

/* Group-level copy inside a section that ALSO carries a grid. Constrained to a reading
   measure rather than to the section, because the grids below want the full width and a
   paragraph at 1344px does not. */
.tp-prose{ max-width:none; }   /* no measure cap: see .tp-fill .section-sub below */

.tp-prose p{ margin:0 0 14px; color:var(--text-2); line-height:1.7; font-size:16px; }
.tp-prose p:last-child{ margin-bottom:0; }

/* A heading V1 shipped with nothing under it. Kept — parity — but set as a lead-in to what
   follows rather than as a section with an empty body. */
.tp-band--lead .section-heading{ margin-bottom:0; }
/* A banner heading joined to the section below it: no bottom padding, and the renderer has
   already given it that section's background, so the two paint as one. */
.tp-band--joined{ padding-bottom:0; }
/* …and the section it joins starts without its own top padding, or the pair still reads as two
   sections with a stripe between them: Cabinet Painting's "Follow us" / "on our social media!",
   kept as V1's two h2s, sat 90px apart. Now they stack like the one sentence V1 styled them as, and
   bathroom-renovation's banners read as the kicker-above-heading this rule always described. */
.tp-band--joined + .tp-band{ padding-top:0; }
/* The subsection a banner introduces (northco_topic_subhead()): V1's second h2 under its banner,
   drawn one step down so the pair reads as a title and its subtitle. Still an h2. */
.tp-band--sub > .inner > .section-heading{ font-size:clamp(22px,2.2vw,28px); font-weight:800; margin-top:0; }
/* The gap between the two is padding on the subsection, not a margin on its heading: with no top padding a
   heading margin collapses through the section and opens a stripe of page background between the bands (it
   did, 14px, 2026-09-25). And the banner's own bottom padding goes, which .tp-band--joined meant to do but
   cannot: .section.tight outranks it. Scoped to a subsection pair; the joined banners elsewhere keep theirs. */
.section.tp-band--joined:has(+ .tp-band--sub){ padding-bottom:0; }
.tp-band--joined + .tp-band--sub{ padding-top:20px; }
/* A banner drawn with its one-liners as cards (topic-body.php): the cards sit closer under the banner than a
   section's own grid, because they are its points rather than a new block. */
.tp-band--briefs .tp-cards{ margin-top:24px; }
/* A before/after pair side by side (topic-body.php). Not the drag-wipe: the frames do not match. */
.tp-figs-pair{ display:grid; grid-template-columns:1fr 1fr; gap:18px; margin:28px 0 0; }
.tp-figs-pair-shot{ position:relative; margin:0; }
.tp-figs-pair-shot img{ display:block; width:100%; height:auto; aspect-ratio:3 / 2; object-fit:cover; border-radius:14px; }
.tp-figs-pair-label{
  position:absolute; left:14px; top:14px; padding:6px 12px; border-radius:999px;
  background:rgba(255,255,255,.92); color:var(--navy);
  font-family:var(--heading); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
}
@media (max-width:600px){ .tp-figs-pair{ grid-template-columns:1fr; gap:14px; } }
/* A map V1 embedded (the geo siding pages' town map), full width. Lazy, as V1 wrote it. */
.tp-embed-map{ margin:28px 0 0; }
.tp-embed-map iframe{ display:block; width:100%; height:360px; border:0; border-radius:14px; }
@media (max-width:600px){ .tp-embed-map iframe{ height:280px; } }

/* ── CARDS ────────────────────────────────────────────────────────────────────────────────
   The h3/h4 sub-blocks inside an h2. Postmedia writes two to four of these per section, each
   with a paragraph and a short list, which is a card rack in every other part of this site
   and was a run of same-weight headings down one column here. */
.tp-cards{ list-style:none; margin:36px 0 0; padding:0; display:grid; gap:20px; }
/* A grid's lead line (topic-body.php): a title-only first card, drawn above the cards instead of as an
   empty one. A real h3 carrying the outline, so margin is set, not inherited. */
.tp-cards-lead{
  font-family:var(--heading); font-weight:700; color:var(--navy);
  font-size:clamp(19px,1.8vw,22px); line-height:1.3; letter-spacing:var(--track-heading, -0.01em);
  margin:28px 0 0;
}
.tp-cards-lead + .tp-cards{ margin-top:18px; }
.tp-cards--1{ grid-template-columns:1fr; }
.tp-cards--2{ grid-template-columns:repeat(2,1fr); }
.tp-cards--3{ grid-template-columns:repeat(3,1fr); }
.tp-cards--4{ grid-template-columns:repeat(4,1fr); }

.tp-card{
  background:#fff; border:1px solid var(--border-warm); border-radius:16px;
  padding:28px 26px 26px;
}
.tp-band--alt .tp-card{ background:var(--cream); }

/* margin:0 is required, not tidying: these are real h3/h4 elements carrying the page's
   outline, and a UA margin-block-start would push every title down inside its own card.
   Same reason .why-name states it — see the note there. */
.tp-card-title{
  font-family:var(--heading); font-weight:700; color:var(--navy);
  font-size:clamp(17px,1.5vw,19px); line-height:1.3; letter-spacing:var(--track-heading, -0.01em);
  margin:0 0 10px;
}
/* The folded h4 strapline. margin:0 required — it is a real heading carrying the outline. */
.tp-card-strap{
  font-family:var(--sans); font-style:italic; font-weight:500;
  font-size:14px; line-height:1.4; color:var(--brown-dark, var(--brown));
  margin:0 0 10px;
}
.tp-card-body{ font-size:14.5px; line-height:1.65; color:var(--text-2); margin:0 0 12px; }
.tp-card-body:last-child{ margin-bottom:0; }

.tp-card-list{
  list-style:none; margin:14px 0 0; padding:14px 0 0;
  border-top:1px solid var(--border-warm);
}
.tp-card-list li{ position:relative; padding-left:18px; font-size:13.5px; line-height:1.55; color:var(--text-2); margin:0 0 8px; }
.tp-card-list li:last-child{ margin-bottom:0; }
.tp-card-list li::before{ content:""; position:absolute; left:0; top:.5em; width:6px; height:6px; border-radius:50%; background:var(--brown); }

.tp-card-cta{ margin:14px 0 0; }
.tp-card-cta a{ font-family:var(--heading); font-size:13px; font-weight:700; color:var(--brown); text-decoration:none; }
.tp-card-cta a:hover{ text-decoration:underline; }

/* A section with ONE sub-block is prose, not a lone card in a three-up rack. */
.tp-cards--1 .tp-card{ border:0; background:none; padding:0; }
.tp-band--alt .tp-cards--1 .tp-card{ background:none; }

/* ── PHOTO CARDS ──────────────────────────────────────────────────────────────────────────
   V1's "Our Other Services" block: four h4s, a line each, and a fifth copy of the words
   "Learn More". The whole card is the link and its anchor text is the service name — same
   hrefs, better anchor text, and the h4s stay in the outline, which is why this is NOT the
   .svc-more rail (its labels are spans and using it would have dropped four headings). */
.tp-cards--photo{ gap:20px; }
.tp-cards--photo .tp-card{ padding:0; overflow:hidden; transition:transform .35s var(--ease), box-shadow .35s var(--ease); }
.tp-card-hit{ display:block; height:100%; text-decoration:none; color:inherit; }
.tp-card--linked:hover{ transform:translateY(-3px); box-shadow:0 14px 34px rgba(22,16,68,.10); }
.tp-card-media{ display:block; aspect-ratio:5/4; overflow:hidden; background:var(--off); }
.tp-card-media img{ width:100%; height:100%; object-fit:cover; display:block; transition:transform .6s var(--ease); }
.tp-card-hit:hover .tp-card-media img{ transform:scale(1.045); }
.tp-card-text{ display:block; padding:22px 24px 24px; }
.tp-card-text .tp-card-body{ display:block; margin:0; }
.tp-card-go{ display:inline-block; margin-top:14px; color:var(--brown); font-size:15px; font-weight:700; line-height:1; }

/* ── CHECKS ───────────────────────────────────────────────────────────────────────────────
   Postmedia's bulleted benefit lists, which carried an inline checkmark <svg> per item. The
   svg is stripped in the parser and the tick is drawn here, so it cannot come back at 735px. */
.tp-checks{ list-style:none; margin:28px 0 0; padding:0; display:grid; grid-template-columns:repeat(2,1fr); gap:14px 40px; }
.tp-check{ position:relative; padding-left:30px; font-size:15px; line-height:1.6; color:var(--text-2); }
.tp-check::before{
  content:""; position:absolute; left:0; top:.15em;
  width:19px; height:19px; border-radius:50%; background:rgba(167,120,69,.13);
}
.tp-check::after{
  content:""; position:absolute; left:6px; top:.48em;
  width:7px; height:4px;
  border-left:2px solid var(--brown); border-bottom:2px solid var(--brown);
  transform:rotate(-45deg);
}

/* ── PILLS ────────────────────────────────────────────────────────────────────────────────
   Eight Ottawa place names. As a bullet list down the left of a wide page this was the worst
   -looking thing on the page after the orange badges. */
.tp-pills{ list-style:none; margin:28px 0 0; padding:0; display:flex; flex-wrap:wrap; gap:10px; }
.tp-pills li{
  display:inline-flex; align-items:center; min-height:42px; padding:0 22px;
  border:1px solid var(--border-warm); border-radius:999px; background:#fff;
  font-family:var(--heading); font-size:14px; font-weight:600; color:var(--navy);
  letter-spacing:.01em;
}
.tp-band--alt .tp-pills li{ background:var(--cream); }

/* ── CHIPS THAT CARRY A LINE OF COPY ──────────────────────────────────────────────────────
   The four door profiles are a NAME plus a phrase each ("Shaker — clean recessed panel,
   timeless and versatile"), and the binder's phrases are the part that helps someone choose.
   A single-line pill would have thrown them away; a second card rack would have given one
   decision two h2s.

   ⚠ NOT A PILL SHAPE ANY MORE IN THIS VARIANT. A 999px radius on a two-line box cuts the
   corners off the text at the ends of both lines — the radius has to come down to something
   the content fits inside. align-items:flex-start because the base rule centres a single line
   and would centre the whole stack here. */
/* ── TYPED FILL SECTIONS ──────────────────────────────────────────────────────────────────
   ⚠ LIFTS .section-sub's 560px CAP, AND THAT IS THE SAME BUG .tp-cmp ALREADY HAS A NOTE ABOUT
   further up this file: "THIS IS WHY THE SUB LOOKED BROKEN, AND IT WAS NEVER THE WORDING."

   Measured on Kitchen Renovation at a 2000px viewport, where every section's inner is 1440
   wide: the timeline, before/after and design subs all run the full 1440 through .tp-subline,
   and these typed sections were the only V2 tiles still stopping at 560 — a five-line column
   under a full-width heading with 880px of nothing beside it. Jesse, on the carpentry tile:
   "broken formatting or intentional idk".

   Paired with the .tp-subline fold in template-parts/topic-section.php, so the copy breaks
   where the author chose rather than wherever 560px happened to land. */
/* ⚠ NO CHARACTER CAPS ON TILE COPY. Copy runs the width of whatever holds it (the tile, the column, the card); the
   container sets the measure, never a character count. Jesse, 2026-10-02: "fix the wrapping of the subtext in screenshotted
   tile - this is the same issue I tell you about legit soooooooo many times" (and "it folds prematurely (common theme
   issue)" the same day). The fixes kept landing one tile at a time while the caps lived on: the global .section-sub and
   .faq-a-inner at 560px, and ch caps on the closer, the prose, the FAQ aside, the cards, the steps and the tools. Swept
   2026-10-02 across 13 service pages at 2000px, with a probe that flags any text whose cap folds it 60px+ short of its
   container. The global 560px still holds outside the tiles (the homepage, centred headers). */
.tp-fill .section-sub, .tp-band .section-sub{ max-width:none; }
.tp-band .faq-a-inner{ max-width:none; }

.tp-pills-label{
  margin:28px 0 0; font-family:var(--sans); font-size:10px; font-weight:700;
  letter-spacing:.12em; text-transform:uppercase; color:var(--muted);
}
.tp-pills-label + .tp-pills{ margin-top:12px; }
.tp-pills--note li{
  flex-direction:column; align-items:flex-start; justify-content:center;
  gap:3px; min-height:0; padding:13px 20px 14px; border-radius:14px;
  max-width:260px;
}
.tp-pill-name{ font-family:var(--heading); font-size:14px; font-weight:700; color:var(--navy); }
.tp-pill-note{
  font-family:var(--sans); font-size:12.5px; font-weight:400; line-height:1.45;
  color:var(--muted); letter-spacing:0; text-wrap:pretty;
}

@media(max-width:600px){
  /* One per row rather than two half-width chips with three-line notes in them. */
  .tp-pills--note{ display:grid; grid-template-columns:1fr; }
  .tp-pills--note li{ max-width:none; }
}

/* ── NUMBERED LIST ────────────────────────────────────────────────────────────────────────
   Postmedia's "why choose us": a 70x70 orange stock badge, then one line of copy, six times.
   Migrated, the badge sat on its own line ABOVE its text with the numbering off-palette and
   the copy stranded below a floating circle — the fourth screenshot Jesse sent. The images
   are dropped in the parser; the numbering they carried is drawn here, on palette, beside
   the line it belongs to. Numerals borrowed from .svc-step-num. */
.tp-num{ list-style:none; margin:32px 0 0; padding:0; display:grid; grid-template-columns:repeat(3,1fr); gap:0; border:1px solid var(--border); border-radius:16px; overflow:hidden; }
.tp-num-item{ display:grid; grid-template-columns:auto 1fr; gap:16px; align-items:start; padding:26px 28px; border-right:1px solid var(--border); border-bottom:1px solid var(--border); }
.tp-num-item:nth-child(3n){ border-right:none; }
.tp-num-item:nth-last-child(-n+3){ border-bottom:none; }
.tp-num-badge{ font-family:var(--heading); font-weight:800; font-size:22px; line-height:1.15; color:var(--brown); opacity:.5; }
.tp-num-body{ font-size:14.5px; line-height:1.6; color:var(--text-2); }
.tp-num-body p{ margin:0; }

/* ── COMPARISON TABLE ─────────────────────────────────────────────────────────────────────
   A real three-column table Postmedia put under "Kitchen Designers vs DIY". overflow-x on the
   wrapper, not the page: a wide table must scroll inside its own box rather than make the
   body scroll sideways. */
.tp-table{ margin:32px 0 0; overflow-x:auto; border:1px solid var(--border-warm); border-radius:14px; background:#fff; }
/* The line V1 set after its table, the section's closing question on the siding pages. */
.tp-prose--close{ margin-top:28px; }
.tp-band--alt .tp-table{ background:var(--cream); }
.tp-table table{ width:100%; border-collapse:collapse; min-width:560px; }
.tp-table th, .tp-table td{ padding:15px 20px; text-align:left; font-size:14.5px; line-height:1.5; color:var(--text-2); border-bottom:1px solid var(--border-warm); }
.tp-table tr:last-child th, .tp-table tr:last-child td{ border-bottom:0; }
/* Postmedia's first row is a header in intent but not always in markup, so both are styled. */
.tp-table thead th, .tp-table thead td,
.tp-table tr:first-child th{
  font-family:var(--heading); font-size:12px; font-weight:700; color:var(--navy);
  text-transform:uppercase; letter-spacing:.05em; background:var(--off);
}
.tp-band--alt .tp-table thead th, .tp-band--alt .tp-table tr:first-child th{ background:#fff; }
.tp-table tbody td:first-child, .tp-table th[scope="row"]{ font-weight:700; color:var(--navy); }
/* On a phone the table fits rather than scrolls. Both tables on these pages are three short columns (34
   characters a cell at most), and a sideways scroll with nothing to say so hid the column each exists for:
   the accessible side of Accessible Bathrooms' comparison, at 390px (2026-09-25). overflow-x stays, for a
   wider table one day. */
@media (max-width:600px){
  .tp-table table{ min-width:0; }
  .tp-table th, .tp-table td{ padding:12px; font-size:13.5px; }
  .tp-table thead th, .tp-table thead td, .tp-table tr:first-child th{ font-size:11px; letter-spacing:.04em; }
}

/* ── FAQ ──────────────────────────────────────────────────────────────────────────────────
   The real accordion. Two fixes it needs on these pages:
   1. Answers here run to a five-item <ol>, and .faq-item.open{max-height:320px} clipped them.
   2. The left column is only the h2, so it gets a line that hands the reader to the form. */
.tp-band .faq-item.open .faq-a{ max-height:720px; }
.tp-band .faq-a-inner p{ margin:0 0 10px; }
.tp-band .faq-a-inner p:last-child{ margin-bottom:0; }
.tp-band .faq-a-inner ol, .tp-band .faq-a-inner ul{ margin:10px 0 0; padding-left:1.2em; }
.tp-band .faq-a-inner li{ margin:0 0 6px; line-height:1.6; }
.tp-band .faq-a-inner li::marker{ color:var(--brown); }
.tp-faq-aside{ margin:24px 0 0; font-size:14.5px; line-height:1.6; color:var(--muted); }
.tp-faq-aside a{ color:var(--navy); font-weight:700; }

/* ── THE BINDER FAQ (.tp-faq) ─────────────────────────────────────────────────────────────
   Same accordion as above, supplied by northco_topic_faq() instead of parsed out of V1
   content. Only the differences live here.

   The left column now carries an eyebrow above the h2, matching the design and day-by-day
   sections, so the h2 loses the top margin it would otherwise stack on. */
.tp-faq .eyebrow{ margin-bottom:10px; }
.tp-faq .faq-grid > div:first-child .section-heading{ margin-top:0; }

/* Binder answers bold their first clause — that is the answer, the rest is detail, and on a
   phone it is what makes the accordion scannable rather than a wall. Navy rather than bolder
   black so it reads as emphasis inside a paragraph, not as a heading. */
.tp-faq .faq-a-inner strong{ color:var(--navy); font-weight:700; }

/* Two answers link out (kitchen renovation, the refacing post). Underlined on purpose: an
   unmarked colour change inside a collapsed panel is missed, and these two links are the
   cross-sell the section is carrying. */
.tp-faq .faq-a-inner a{
  color:var(--brown); font-weight:600;
  text-decoration:underline; text-underline-offset:2px; text-decoration-thickness:1px;
}
.tp-faq .faq-a-inner a:hover{ color:var(--navy); }

/* No max-height override here, and that is the finding rather than an omission. The two long
   answers (cost, update-or-renovate) are the tallest at 375px and measure 239px and 264px
   against the 720px cap .tp-band already sets — so they clear it with room to spare.
   ⚠ If you measure this yourself, .faq-a reports max-height:0px in the preview pane even with
   .open applied: the 0.32s transition never runs because the pane freezes animation, so the
   computed value stays at its start. Measure .faq-a-inner's height against the cap in the
   stylesheet, not the computed max-height of .faq-a. */

/* ── THE QUOTE SECTION ────────────────────────────────────────────────────────────────────
   ⚠ WHITE, EXPLICITLY. .contact-section is a NAVY band and .section-heading is navy — so
   V1's quote headings rendered here would have been navy on navy and invisible. The five
   pages already on this template never hit that because their form is rendered `bare`, with
   no heading in the left column at all. These pages have three headings to carry. */
.tp-form-title{ color:#fff; margin-bottom:12px; max-width:24ch; }
.tp-form-kicker{
  font-family:var(--heading); font-size:clamp(16px,1.5vw,19px); font-weight:700;
  color:#fff; margin:0 0 8px; line-height:1.3;
  letter-spacing:var(--track-heading, normal);
}
.tp-form .contact-sub{ margin-bottom:16px; }
.tp-form .contact-sub:last-of-type{ margin-bottom:24px; }
/* A list inside the quote run, printed with its own heading since 2026-10-06: the siding pages' eight nearby areas
   (ops/V2-VS-V1.md §B9). Place names, so pills: one line of short words on the navy, not a column of bullets. */
.tp-form-list{ list-style:none; margin:-4px 0 24px; padding:0; display:flex; flex-wrap:wrap; gap:8px; }
.tp-form-list li{
  font-family:var(--sans); font-size:14px; line-height:1.2; color:#fff;
  padding:7px 13px; border:1px solid rgba(255,255,255,.24); border-radius:999px;
}

/* ── THE CLOSER ───────────────────────────────────────────────────────────────────────────
   V1's last section is a call to action, and it now looks like one. */
.tp-closer{ background:var(--navy); color:#fff; }
.tp-closer-title{
  font-family:var(--heading); font-size:clamp(28px,3.6vw,44px); font-weight:900;
  line-height:1.1; letter-spacing:-0.02em; color:#fff; margin:0 0 16px;
  /* One line wherever it fits; balanced lines where it can't. Jesse, 2026-10-02, on Kitchen Designers' "Ready to Start
     Your Design & Renovation Project?": "ensure the header for this doesn't wrap like it currently does - there's lots of
     room on the page for it NOT to wrap". A 22ch cap wrapped it even at 2000px. */
  text-wrap:balance;
}
.tp-closer-body{ font-size:16.5px; line-height:1.65; color:rgba(255,255,255,.74); margin:0 0 10px; }
.tp-closer-body:last-of-type{ margin-bottom:0; }
.tp-cta--light{ margin-top:32px; }

/* ── MISCELLANY V1 LEAVES BEHIND ──────────────────────────────────────────────────────────
   A stray non-icon <figure>, and the odd bare link like "view our recent projects". */
.tp-figure{ margin:32px 0 0; max-width:760px; }
.tp-figure img{ border-radius:14px; height:auto; }   /* height:auto: the global img rule caps the width (max-width:100%), and V1's
   height attribute then kept its full 1152px under a 760px width, stretching Wrap's soffit photographs (Jesse, 2026-10-07:
   "the photos are stretched and broken"). With auto, the attributes give the ratio and the photo keeps its shape. */
/* Two or more of those photographs in one section, in a row (template-parts/topic-body.php). One crop for the row, so
   photographs of different shapes still line up. One column on a phone. */
.tp-figs-row{ display:grid; gap:16px; margin:32px 0 0; }
.tp-figs-row--2{ grid-template-columns:repeat(2,minmax(0,1fr)); }
.tp-figs-row--3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
.tp-figs-row .tp-figure{ margin:0; max-width:none; }
.tp-figs-row .tp-figure img{ width:100%; height:auto; aspect-ratio:3/2; object-fit:cover; }
@media (max-width:700px){ .tp-figs-row{ grid-template-columns:1fr; } }
.tp-links{ margin:22px 0 0; display:flex; flex-wrap:wrap; gap:20px; }
.tp-links a{ font-family:var(--heading); font-size:14px; font-weight:700; color:var(--brown); }

/* ── RESPONSIVE ───────────────────────────────────────────────────────────────────────────
   Breakpoints match the ones the rest of the theme already uses, so a card rack and the
   .why-grid beside it collapse on the same screen widths rather than one column apart. */
@media(max-width:1100px){
  .tp-cards--3{ grid-template-columns:repeat(2,1fr); }
  .tp-cards--4{ grid-template-columns:repeat(2,1fr); }
  .tp-num{ grid-template-columns:repeat(2,1fr); }
  .tp-num-item:nth-child(3n){ border-right:1px solid var(--border); }
  .tp-num-item:nth-child(2n){ border-right:none; }
  .tp-num-item:nth-last-child(-n+3){ border-bottom:1px solid var(--border); }
  .tp-num-item:nth-last-child(-n+2){ border-bottom:none; }
}
@media(max-width:900px){
  .tp-cards--2, .tp-cards--3, .tp-cards--4{ grid-template-columns:1fr; }
  /* A single-column photo card would be a 700px-tall picture on a tablet. Side by side
     instead: image left, copy right, so the rail stays a rail. */
  .tp-cards--photo{ grid-template-columns:1fr; }
  .tp-cards--photo .tp-card-hit{ display:grid; grid-template-columns:200px 1fr; align-items:center; }
  /* ⚠ aspect-ratio MUST GO HERE, not be overridden to 1/1. In a two-column grid cell,
     `aspect-ratio:1/1` plus `height:100%` makes the box derive its WIDTH from the row
     height the copy sets — so a three-line card pushed a 260px image through a 132px
     column and the text rendered on top of the photograph. The column sets the width;
     the row sets the height; the ratio must not have an opinion. */
  .tp-cards--photo .tp-card-media{ aspect-ratio:auto; width:100%; height:100%; min-height:118px; }
  /* ⚠ A card with no photograph (every siding rail: no material has a card photo yet) must not
     get the two columns — its copy lands in the photo column, and a phone showed a 94px strip of
     text beside an empty one. */
  .tp-cards--photo .tp-card--nophoto .tp-card-hit{ display:block; }
  .tp-cards--photo .tp-card-go{ margin-top:10px; }
  .tp-checks{ grid-template-columns:1fr; gap:12px; }
}
@media(max-width:768px){
  .tp-cards{ margin-top:26px; gap:16px; }
  .tp-card{ padding:22px 20px; }
  .tp-num{ grid-template-columns:1fr; margin-top:24px; }
  .tp-num-item{ padding:20px 22px; border-right:none; }
  .tp-num-item:nth-child(3n){ border-right:none; }
  .tp-num-item:nth-last-child(-n+2){ border-bottom:1px solid var(--border); }
  .tp-num-item:last-child{ border-bottom:none; }
  /* Full-width CTAs, matching .svc-hero-cta's mobile rule so the buttons on these pages
     behave like the buttons everywhere else on a phone. */
  .tp-cta .btn{ flex:1 1 100%; justify-content:center; text-align:center; }
  .tp-pills li{ min-height:38px; padding:0 16px; font-size:13px; }
  .tp-cards--photo .tp-card-hit{ grid-template-columns:132px 1fr; }
  .tp-cards--photo .tp-card-text{ padding:16px 18px; }
  .tp-lead p{ font-size:16.5px; }
}

/* ── STAT ROW ─────────────────────────────────────────────────────────────────────────────
   Postmedia writes stats as a heading holding only the figure, with the label as loose text
   beside it. Every h2 opens a section, so three of them became three full-height bands each
   containing one number — "200+", then "15+", then "100%". They are folded into the section
   above and drawn as a row here; see northco_topic_groups().

   ⚠ .tp-stat-num IS A REAL <h2>, not a div. Rendering the figure as a div would read better
   semantically and would silently drop three headings from the outline, which the parity diff
   flags and which is not a call this layer gets to make. Heading level fixed, box free —
   the same bargain .tp-card-title strikes. Hence the margin:0 reset. */
.tp-stats{
  list-style:none; margin:32px 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:16px;
}
/* A stat keeps its own width and does not grow (2026-10-06). With flex:1 a lone stat stretched across the whole section:
   Accessible's "10+ Years Accessibility Experience" was a 1,540px box (Jesse: "this looooong box"), and Bathroom's the
   same, once their false stats were dropped. Two stats (CKC, Design & Build) were 760px each. */
.tp-stat{
  flex:0 1 280px; min-width:0;
  padding:26px 28px;
  background:#fff; border:1px solid var(--border-warm); border-radius:16px;
}
.tp-band--alt .tp-stat{ background:var(--cream); }
.tp-stat-num{
  font-family:var(--heading); font-weight:900;
  font-size:clamp(30px,4vw,44px); line-height:1;
  letter-spacing:-0.02em; color:var(--brown);
  margin:0 0 8px;   /* required: it is an h2 and would otherwise carry a UA margin */
}
.tp-stat-label{
  display:block;
  font-size:14px; line-height:1.45; color:var(--text-2);
}
@media(max-width:768px){
  .tp-stats{ gap:12px; margin-top:24px; }
  .tp-stat{ flex:1 1 140px; max-width:280px; padding:18px 20px; }
}

/* ── CAROUSEL ─────────────────────────────────────────────────────────────────────────────
   Replaces the Elementor `slides` widget the migration dropped, which left Kitchen Updates
   rendering sixteen photographs stacked down a column.

   ⚠ SLIDES ARE NEVER display:none. The track is moved with a transform and the viewport
   clips it, so every slide stays in the DOM and in the flow. Hiding the off-screen ones would
   remove sixteen headings and their copy from the page for a crawler — the exact loss this
   whole rebuild exists to prevent.

   ⚠ AND IT WORKS WITHOUT JAVASCRIPT. The viewport is a scroll container with scroll-snap, so
   with JS off it is a swipeable row and every slide is reachable. The buttons are [hidden] in
   the markup and JS unhides them; they are an enhancement, not the mechanism. */
.tp-carousel{ margin:36px 0 0; }
.tp-carousel + .tp-carousel{ margin-top:28px; }
/* V1's graphic between the cabinet pages' two step sliders, the shop's and the home's (topic-body.php). */
.tp-carousel-between{ margin:36px 0 0; }
.tp-carousel-between img{ width:100%; max-width:800px; margin:0 auto; }
.tp-carousel-between + .tp-carousel{ margin-top:28px; }
.tp-carousel-viewport{
  overflow-x:auto; overflow-y:hidden;
  scroll-snap-type:x mandatory;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  /* Room for the card shadow, clipped otherwise by overflow. */
  padding:4px 4px 12px;
  margin:-4px -4px -12px;
  /* The same 4px, so a slide that snaps to an edge lands on the content edge, not on the padding edge. */
  scroll-padding-inline:4px;
}
.tp-carousel-viewport::-webkit-scrollbar{ display:none; }
.tp-carousel-track{
  list-style:none; margin:0; padding:0;
  display:flex; gap:24px;
  /* Required for scroll-snap-align:center — without side padding the first and last slides
     can never reach the middle of the viewport and neither can ever take focus. */
  padding-inline:max(0px, calc((100% - clamp(280px, 68%, 860px)) / 2));
}
/* ⚠ ONE SLIDE IN FOCUS, NEIGHBOURS SHRUNK BACK. First cut showed three or four abreast, which
   Jesse called correctly: "on desktop it's info overload - far too much on screen. before it
   was one step by one step."

   This is the "focus" effect he pointed at (swiper-effect-focus): the active slide at full
   size, its neighbours scaled to 0.85 and dimmed, consistent gaps throughout. One thing to
   read, with enough of the next one showing to say "there is more here".

   ⚠ BUILT NATIVELY RATHER THAN BY ADDING SWIPER. The reference reaches that look through
   Swiper 14 plus a plugin plus two CDN stylesheets — roughly 150KB and two external requests
   on a site that is 85% mobile and today carries exactly ONE vendored third-party runtime
   dependency, 13KB. The effect itself is a transform on the non-active slides, which the
   scroll-snap track already in place supports directly. Same look, nothing new to load, and
   no CDN in the critical path. Say the word and it swaps for the real library. */
.tp-slide{
  scroll-snap-align:center;
  flex:0 0 clamp(280px, 68%, 860px);
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  background:#fff; border:1px solid var(--border-warm); border-radius:16px;
  overflow:hidden;
  transform:scale(.85);
  opacity:.55;
  transform-origin:center;
  transition:transform .45s var(--ease-reveal, cubic-bezier(.25,1,.5,1)),
             opacity .45s var(--ease-reveal, cubic-bezier(.25,1,.5,1));
}
/* JS marks the slide nearest the centre. Without JS every slide stays at full size and the
   track is an ordinary swipeable row — the effect is the enhancement, never the mechanism. */
.tp-slide.is-focus{ transform:scale(1); opacity:1; }
.tp-carousel.no-js .tp-slide{ transform:none; opacity:1; }
/* ⚠ THE ENDS REST FLUSH, NOT CENTRED. Centred, slide one opened beside the track's inset: an empty gap the width of a
   neighbour, on the left (Jesse, 2026-10-02: "when first opened this is kinda janky"). The last slide could not centre
   at all, because the track's end padding is not scrollable. So the first slide snaps to the heading's edge and the
   last to the right edge, and only the slides between them centre with a neighbour either side. nav.js rests them by
   the same rules. Rails (.tp-rail-slide) snap every card to start and are left as they are. */
/* ⚠ EACH END SLIDE SCALES FROM ITS OWN SNAP EDGE. A dimmed slide is scale(.85), and the browser measures snap positions
   (and the scroll range) from the SCALED box. Scaled from its centre, the last slide's right edge sat 64px in from where
   it would rest once focused: the carousel stopped there, the slide took focus and grew, and the snap jumped another
   64px (Jesse, 2026-10-02: "step 5 loads in then suddenly pops over to the left"). Scaled from the edge it snaps by,
   that edge never moves, so neither does its snap position. The middle slides already scale from the centre they snap to. */
.tp-slide:not(.tp-rail-slide):first-child{ scroll-snap-align:start; transform-origin:left center; }
.tp-slide:not(.tp-rail-slide):last-child{ scroll-snap-align:end; transform-origin:right center; }
/* The track's end padding isn't scrollable, so the scroll range stops at the last slide's own edge, 4px short of the
   end snap that puts it on the content edge. A 4px spacer gives that room. Rails are left out (their last card would
   rest short of the edge), and so is the folded grid (the spacer would be a stray cell). */
.tp-carousel-track:has(> .tp-slide:not(.tp-rail-slide))::after{ content:""; flex:0 0 4px; }
.tp-fold-body .tp-carousel-track::after{ content:none; }
@media (prefers-reduced-motion: reduce){
  .tp-slide{ transition:none; }
}
.tp-band--alt .tp-slide{ background:var(--cream); }
.tp-slide-media{ display:block; overflow:hidden; background:var(--off); min-height:100%; }
.tp-slide-media img{ width:100%; height:100%; object-fit:cover; display:block; }
.tp-slide-body{ padding:clamp(24px,3vw,44px); align-self:center; }
@media(max-width:768px){
  .tp-slide{
    grid-template-columns:1fr;
    flex:0 0 84%;
    /* Less shrink on a phone: at this width a 0.85 neighbour is a sliver, and the shrink
       reads as a rendering fault rather than as depth. */
    transform:scale(.94);
  }
  .tp-slide-media{ aspect-ratio:16/10; min-height:0; }
  /* align-self:center is for the two-column desktop slide, where the copy sits beside a
     full-height photograph. Stacked, it centres the text in whatever height the tallest slide
     forces and opens a gap under the image. */
  .tp-slide-body{ align-self:start; }
  .tp-carousel-track{ padding-inline:8%; }
}
/* margin:0 required — these are real h3/h4 carrying the outline, see the note in topic-body.php */
.tp-slide-title{
  font-family:var(--heading); font-weight:800; color:var(--navy);
  font-size:clamp(20px,2.2vw,28px); line-height:1.2; letter-spacing:var(--track-heading, -0.01em); margin:0 0 6px;
}
.tp-slide-strap{
  font-family:var(--sans); font-style:italic; font-weight:500;
  font-size:clamp(15px,1.4vw,17px); color:var(--brown-dark, var(--brown));
  margin:0 0 16px; line-height:1.4;
}
.tp-slide-text{ font-size:15.5px; line-height:1.7; color:var(--text-2); margin:0 0 10px; }
.tp-slide-text:last-child{ margin-bottom:0; }

/* ── THE STORY CAROUSEL (topic-section.php 'story'): the stage over its title, and a named placeholder for a photograph
   that hasn't arrived, same contract as the rails' (.tp-rail-media--empty). Kitchen Designers' sequence, 2026-10-02. */
/* ⚠ FULL WIDTH OF THE PADDED TRACK. The track's side padding already frames the focused slide at the intended
   clamp(280px, 68%, 860px), so the slide's own 68% applied to what was left: 585px at 1440, 70% of a phone. A photograph
   per stage wants the full frame, and the larger share of it. Scoped to the story; Kitchen Updates' step carousel has
   the same quirk and is Jesse's call. */
.tp-story .tp-slide{ flex-basis:100%; grid-template-columns:minmax(0,11fr) minmax(0,9fr); }
@media(max-width:768px){
  .tp-story .tp-slide{ flex-basis:100%; grid-template-columns:1fr; }
}
.tp-slide-step{
  font-family:var(--sans); font-size:12px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:var(--brown); margin:0 0 10px;
}
.tp-slide-media--empty{
  display:flex; align-items:center; justify-content:center; text-align:center;
  min-height:clamp(260px,30vw,420px);
  background:repeating-linear-gradient(45deg,var(--off) 0 10px,var(--cream) 10px 20px);
}
.tp-slide-media--empty > span{
  font-family:var(--sans); font-size:13px; line-height:1.5; color:var(--muted); max-width:30ch; padding:0 18px;
}
.tp-slide-media--empty b{
  display:block; margin-bottom:4px; font-family:var(--heading); font-size:12px; letter-spacing:.04em; color:var(--navy);
}

.tp-carousel-nav{ display:flex; align-items:center; gap:14px; margin-top:18px; }
/* ⚠ [hidden] MUST ACTUALLY HIDE. The rule above is author CSS, so its display:flex beats the
   browser's own [hidden]{display:none}. The attribute did nothing on its own: the no-JS state
   has always shown the buttons, and nav.js could not hide them when every slide fits. Same
   lesson as assets/css/careers.css. */
.tp-carousel-nav[hidden]{ display:none; }
.tp-carousel-btn{
  width:44px; height:44px; flex:none;      /* 44px: the minimum comfortable touch target */
  border-radius:50%; border:1px solid var(--border-warm); background:#fff;
  color:var(--navy); font-size:17px; line-height:1; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  transition:background .2s var(--ease), border-color .2s var(--ease), opacity .2s;
}
.tp-carousel-btn:hover{ background:var(--off); border-color:var(--navy); }
.tp-carousel-btn[disabled]{ opacity:.35; cursor:default; }
.tp-carousel-btn:focus-visible{ outline:2px solid var(--brown); outline-offset:2px; }
.tp-carousel-count{
  font-family:var(--heading); font-size:13px; font-weight:700; letter-spacing:.04em;
  color:var(--muted); margin:0;
}

/* ── BEFORE / AFTER ───────────────────────────────────────────────────────────────────────
   Replaces the two eael-image-comparison widgets. Both photographs stay in the DOM.

   ⚠ DRIVEN BY A REAL <input type="range">, not a custom drag handler. That buys keyboard
   support, touch, and screen-reader semantics for free — a div with pointer events buys none
   of them and is more code. The input is transparent and stretched over the figure; JS only
   mirrors its value into --pos, and with JS off it still drags because the browser owns it. */
.tp-compare-grid{
  display:grid; grid-template-columns:repeat(2,1fr); gap:20px; margin:36px 0 0;
}
.tp-compare{
  position:relative; margin:0; overflow:hidden; border-radius:16px;
  border:1px solid var(--border-warm); background:var(--off);
  line-height:0;   /* kills the inline-image gap under the photo */
}
.tp-compare span{ display:block; }
.tp-compare img{ width:100%; height:auto; display:block; }
.tp-compare-after{ position:relative; z-index:1; }
.tp-compare-before{
  position:absolute; inset:0; z-index:2;
  /* Clip from the right, so --pos reads as "how much BEFORE is showing". */
  clip-path:inset(0 calc(100% - var(--pos,50%)) 0 0);
}
.tp-compare-before img{ height:100%; object-fit:cover; }
.tp-compare-handle{
  position:absolute; top:0; bottom:0; z-index:3;
  left:var(--pos,50%); width:2px; margin-left:-1px;
  background:#fff; box-shadow:0 0 0 1px rgba(22,16,68,.18);
  pointer-events:none;
}
.tp-compare-handle::after{
  content:""; position:absolute; top:50%; left:50%;
  width:38px; height:38px; margin:-19px 0 0 -19px;
  border-radius:50%; background:#fff; box-shadow:0 2px 10px rgba(22,16,68,.28);
}
.tp-compare-range{
  position:absolute; inset:0; z-index:4;
  width:100%; height:100%; margin:0; padding:0;
  opacity:0; cursor:ew-resize; -webkit-appearance:none; appearance:none; background:none;
}
.tp-compare-range:focus-visible + .tp-compare-handle::after{ outline:2px solid var(--brown); outline-offset:2px; }
.tp-compare-tag{
  position:absolute; bottom:12px; z-index:3;
  font-family:var(--heading); font-size:11px; font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:#fff; line-height:1;
  padding:7px 12px; border-radius:999px; background:rgba(22,16,68,.72);
  pointer-events:none;
}
.tp-compare-tag--b{ left:12px; }
.tp-compare-tag--a{ right:12px; }

@media(max-width:900px){
  .tp-compare-grid{ grid-template-columns:1fr; }
}
@media(max-width:768px){
  .tp-carousel-track{ gap:14px; }
  .tp-compare-grid{ gap:14px; margin-top:26px; }
}

/* ── BENTO: heading cell beside a grid of icon cards ──────────────────────────────────────
   For a run of Elementor icon-lists — abstract benefit claims with no photograph. The
   homepage trio was tried first and read as four text blocks in boxes, because its card is
   built around an image these do not have.

   The heading cell spans both rows so an EVEN number of cards lands beside it with no hole.
   An odd count gets .tp-bento--odd, which drops the span and lets them flow. */
.tp-bento{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:20px;
  margin-top:8px;
}
/* ⚠ CENTRED, not start-aligned. Jesse: "have the side header wording in the middle on the
   side, so it feels together and not disjointed". It spans both card rows, so pinning it
   to the top left it floating against two rows of cards with nothing beside its lower
   half. Centring reads as one block rather than a heading and a separate grid. */
.tp-bento-head{ grid-column:1; grid-row:1 / span 2; align-self:center; padding-right:8px; }
.tp-bento--odd .tp-bento-head{ grid-row:auto; }
.tp-bento-head .section-heading{ font-size:clamp(26px,3vw,38px); margin-bottom:14px; }
.tp-bento-head .section-sub{ font-size:16px; }
.tp-bento-links{ margin:20px 0 0; }
.tp-bento-links a{
  font-family:var(--heading); font-size:14px; font-weight:700; color:var(--brown);
  display:inline-flex; align-items:center; gap:8px;
}
.tp-bento-links a:hover{ text-decoration:underline; }
/* Under the section's CTA (northco_topic_group_links()): closer to the button than a lede's links sit. On navy,
   gold-500, because gold-600 is only 4.5:1 there and this is the one line meant to be read in passing. */
.tp-bento-links--after{ margin-top:16px; }
.tp-band--navy .tp-bento-links--after a{ color:var(--brown-light); }

.tp-bento-card{
  background:var(--off);
  border:1px solid var(--border-warm);
  border-radius:16px;
  padding:clamp(20px,2vw,28px);
  display:flex; flex-direction:column;
  min-width:0;
}
.tp-band--alt .tp-bento-card{ background:#fff; }

/* ⚠ margin-bottom:auto WAS HERE AND HAS BEEN REMOVED. It pushed each card's text block to the
   BOTTOM of the card, and the comment on .tp-bento-title justified it as "what gives the
   reference grid its shape when cards are different heights".

   The cards are NOT different heights. The grid stretches every card in a row to match its
   tallest — measured on Kitchen Update: 247/247 in row one, 316/316 in row two. So bottom
   anchoring bought none of the shape it was there for, and cost the titles their alignment:
   with equal cards, the one with more body text starts HIGHER, putting row one's two titles
   24px apart and row two's 16px apart. Jesse: "the four subheadings - do you mean to have them
   top-wise not align?" No.

   Without it the content packs from the top, so every icon and every title sits at the same
   offset and the spare space collects at the bottom of the shorter card, where it reads as
   breathing room rather than as misalignment. */
.tp-bento-icon{
  width:46px; height:46px; flex:none;
  border-radius:12px;
  background:#fff;
  border:1px solid var(--border-warm);
  color:var(--brown);
  display:inline-flex; align-items:center; justify-content:center;
}
.tp-band--alt .tp-bento-icon{ background:var(--off); }
.tp-bento-icon svg{ width:22px; height:22px; display:block; }

/* margin-top on the title rather than margin-bottom on the icon, so the gap belongs to the
   text block. (The icon's margin-bottom:auto that used to pair with this is gone — see the
   note on .tp-bento-icon for why.) */
.tp-bento-title{
  font-family:var(--heading); font-weight:700; color:var(--navy);
  font-size:clamp(16px,1.5vw,19px); line-height:1.3; letter-spacing:var(--track-heading, -0.01em);
  margin:36px 0 8px;
}
.tp-bento-body{ font-size:14.5px; line-height:1.6; color:var(--text-2); margin:0 0 8px; }
.tp-bento-body:last-child{ margin-bottom:0; }

/* One point of emphasis, as the reference has. Without it the grid reads as identical boxes. */
.tp-bento-card--accent{
  background:var(--brown);
  border-color:var(--brown);
}
.tp-band--alt .tp-bento-card--accent{ background:var(--brown); }
.tp-bento-card--accent .tp-bento-title{ color:#fff; }
.tp-bento-card--accent .tp-bento-body{ color:rgba(255,255,255,.86); }
.tp-bento-card--accent .tp-bento-icon{
  background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.28); color:#fff;
}
/* ⚠ LINKS INSIDE THE ACCENT CARD WERE INVISIBLE. The global a{color:var(--brown)} put brown
   text on the brown card — the two cross-sell links in the last card's copy read as blank gaps
   mid-sentence. Underlined as well as recoloured, because on a coloured panel colour alone is
   not enough to mark a link. */
.tp-bento-card--accent a{
  color:#fff;
  text-decoration:underline;
  text-underline-offset:3px;
  text-decoration-thickness:1px;
}
.tp-bento-card--accent a:hover{ text-decoration-thickness:2px; }
.tp-bento-card--accent a:focus-visible{ outline:2px solid #fff; outline-offset:2px; }

/* ── THE HYBRID BENTO (.tp-hybrid) ─────────────────────────────────────────────────────────
   Design & Build's portal tile, 2026-10-05 (binder p9): the heading cell beside a wide picture across two thirds, then
   the four points as the bento's own cards, four a row. The cards are .tp-bento-card, so they look and band-shade as
   every bento's do. The picture sits contained on white: binder art comes on white, and a crop would cut a laptop off. */
.tp-hybrid{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:20px; align-items:center; }
.tp-hybrid-head{ grid-column:1; padding-right:8px; }
.tp-hybrid-head .section-heading{ font-size:clamp(26px,3vw,38px); margin-bottom:14px; }
.tp-hybrid-head .section-sub{ font-size:16px; }
.tp-hybrid-media{
  grid-column:2 / span 2; margin:0; aspect-ratio:3 / 1;
  background:#fff; border:1px solid var(--border-warm); border-radius:16px; overflow:hidden;
}
.tp-hybrid-media img{ display:block; width:100%; height:100%; object-fit:contain; }
.tp-hybrid-media--empty{
  display:flex; align-items:center; justify-content:center; text-align:center; padding:16px;
  background:repeating-linear-gradient(135deg, var(--off) 0 10px, #fff 10px 20px); color:var(--muted); font-size:13px;
}
.tp-hybrid-media--empty b{ display:block; color:var(--navy); margin-bottom:4px; }
.tp-hybrid-cards{ grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:20px; }
.tp-hybrid-more{ margin:clamp(44px,5vw,72px) 0 18px; }
.tp-hybrid-more-title{
  font-family:var(--heading); font-weight:800; color:var(--navy);
  font-size:clamp(22px,2.2vw,28px); line-height:1.2; margin:0 0 8px; letter-spacing:var(--track-heading, normal);
}
.tp-hybrid-more-lead{ margin:0; color:var(--text-2); font-size:16px; line-height:1.6; }
@media(max-width:1100px){
  .tp-hybrid{ grid-template-columns:1fr; }
  .tp-hybrid-head, .tp-hybrid-media{ grid-column:1; }
  .tp-hybrid-cards{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media(max-width:600px){ .tp-hybrid-cards{ grid-template-columns:1fr; } }

/* SIX CARDS (Design & Build's care, 2026-10-05): the heading cell beside three cards a row, two rows deep, so an even six
   lands with no hole under the heading. Desktop only, so the shared two-column flow below 1101px still applies. */
@media(min-width:1101px){
  .tp-bento--six{ grid-template-columns:minmax(0,1.15fr) repeat(3,minmax(0,1fr)); }
}
@media(max-width:1100px){
  .tp-bento{ grid-template-columns:repeat(2,minmax(0,1fr)); }
  .tp-bento-head{ grid-column:1 / -1; grid-row:auto; padding-right:0; }
}
@media(max-width:700px){
  .tp-bento{ grid-template-columns:1fr; gap:14px; }
  .tp-bento-title{ margin-top:22px; }
  .tp-bento-icon{ width:42px; height:42px; }
}

/* ── THREE CARDS ── the heading keeps its two-row span and the accent card fills the second row
   beside it (topic-body.php adds .tp-bento--three). Under .tp-bento--odd the third card dropped
   under the heading, leaving two empty cells: Cabinet Painting, 2026-09-23, Jesse: "this tile is
   broken on CP". */
/* ── SIX CARDS ── the homepage's why-choose (template-parts/why-choose.php): the heading cell spans three rows, so two
   columns of three sit beside it with no hole; with the two-row span the last two dropped under the heading and left
   one cell empty. Desktop only, since at 1100px and under the heading takes its own row anyway. */
@media(min-width:1101px){
  .tp-bento--six .tp-bento-head{ grid-row:1 / span 3; }
}
/* The homepage's tile heading on the page's one section size, like every other heading there (3f4ab0c), not the tile's
   38px cap. It runs two lines at 1440. The service pages' tiles keep 38px for now: their headings are longer, and 48px
   in this column ran Kitchen Renovation's to five lines at 1440 and six at 1101 (measured in Mont, 2026-09-29). */
.why-bento .tp-bento-head .section-heading{ font-size:clamp(30px,3.8vw,48px); }
.tp-bento--three .tp-bento-card--accent{ grid-column:2 / span 2; }
@media(max-width:1100px){
  .tp-bento--three .tp-bento-card--accent{ grid-column:1 / -1; }
}
/* ── FIVE CARDS ── Kitchen Designers' "why" (2026-10-06): the heading keeps its two-row span beside four cards, two by
   two, and the accent card runs the full width under them, laid out as the three-card accent is (below). Under the odd
   flow the two cards beside the heading stretched to its height and stood a third empty. */
.tp-bento--five .tp-bento-card--accent{ grid-column:1 / -1; }
/* Two columns wide, so the copy sits beside the title instead of running ~115 characters to a
   line: icon and title on the left, body on the right, their bottoms level. */
@media(min-width:701px){
  .tp-bento--three .tp-bento-card--accent, .tp-bento--five .tp-bento-card--accent{
    display:grid; grid-template-columns:minmax(0,5fr) minmax(0,7fr);
    column-gap:clamp(20px,3vw,40px); align-content:start;
  }
  .tp-bento--three .tp-bento-card--accent .tp-bento-icon, .tp-bento--five .tp-bento-card--accent .tp-bento-icon{ grid-column:1; grid-row:1; }
  .tp-bento--three .tp-bento-card--accent .tp-bento-title, .tp-bento--five .tp-bento-card--accent .tp-bento-title{ grid-column:1; grid-row:2; margin-bottom:0; }
  .tp-bento--three .tp-bento-card--accent .tp-bento-body, .tp-bento--five .tp-bento-card--accent .tp-bento-body{ grid-column:2; grid-row:1 / span 2; align-self:end; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   PROTOTYPE · VARCO-STYLE FIXED HEADLINE SLIDER  (.tp-vp)

   The day-by-day for Kitchen Updates. There was a second prototype (.tp-tl, a scroll-driven
   rail) built alongside this one; Jesse chose this, and that one was removed.

   Measured off varcopruden.com's .cmp-fixed-headline-slider rather than eyeballed:
     · slides crossfade in place (their Swiper: effect:fade, speed 600)
     · a vertical rail of thin segments runs down the CENTRE of the tile, and the active
       segment swells to 3x an inactive one (theirs: 312px vs 104px, 0.7s transition)
     · the headline and arrows never move
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* ⚠ NO PIN. An earlier build made this twelve viewports tall and held the reader through all
   of it to get past. Jesse: "I have changed my mind - i want to be able to scroll past it. 12
   scrolls to keep going is too much." Ordinary section height; the wheel is captured only over
   the tile, and released at the first and last step so the page can always scroll on. */
.tp-vp{
  background:var(--navy); color:#fff;
  padding-block:clamp(56px,8vw,96px);
}
.tp-vp .section-heading{ color:#fff; }
.tp-vp .eyebrow{ color:var(--brown-soft, #d6b28a); }
.tp-vp .section-sub{ color:rgba(255,255,255,.76); max-width:none; }
/* ⚠ SHARED, NOT PER-SECTION, AND INLINE AT EVERY WIDTH since 2026-10-02. It used to be block from
   900px, so a sub given as two parts broke at a deliberate point. Jesse, on Kitchen Designers' ideas:
   "have this go the length of the tile, it folds prematurely (common theme issue lol)". The forced
   break ended the first line early wherever the sub runs the full width, so every sub now flows as
   one paragraph and wraps where the width says. Subs still pass as parts; only the break went. */
.tp-subline{ display:inline; }
.tp-vp-head-text{ max-width:none; }

.tp-vp-head{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:24px; flex-wrap:wrap;
  margin-bottom:clamp(28px,3.5vw,48px);
}
.tp-vp-nav{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px;
}
.tp-vp-count{
  margin:0; font-family:var(--heading); font-size:12px; font-weight:700;
  letter-spacing:.06em; color:rgba(255,255,255,.62); font-variant-numeric:tabular-nums;
}
/* ⚠ BROWN RING, WHITE GLYPH — measured, not taste. rgba(255,255,255,.34) on this navy blends
   to #656184, which is 3.02:1 against the band: it scrapes past WCAG 1.4.11's 3:1 floor for UI
   controls and looks it. Brown is 4.54:1, half again as much separation, which is what Jesse
   was reacting to.

   ⚠ OUTLINED, NOT FILLED, and that is the point. The section's payoff CTA is a SOLID brown
   button; making these solid too would put a navigation control and the conversion action at
   the same visual weight. Outline reads as "move around", fill reads as "do the thing".

   The glyph stays white at 17.6:1 rather than going brown with the ring — a brown arrow inside
   a brown ring is a quieter target than the ring implies. */
.tp-vp-btn{
  width:44px; height:44px; flex:none; border-radius:50%;
  border:1.5px solid var(--brown); background:transparent; color:#fff;
  font-size:17px; line-height:1; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  transition:background .2s var(--ease), border-color .2s var(--ease), opacity .2s;
}
.tp-vp-btn:hover:not([disabled]){ background:var(--brown); border-color:var(--brown); }
.tp-vp-btn[disabled]{ opacity:.3; cursor:default; }
.tp-vp-btn:focus-visible{ outline:2px solid #fff; outline-offset:2px; }

/* ⚠ WITHOUT .is-ready THIS IS A PLAIN VERTICAL LIST of all twelve steps. The script adds the
   class, so no-JS and crawlers get the whole sequence instead of one step and eleven
   invisible ones. Never replace this with display:none on the slides. */
.tp-vp-stage{ position:relative; display:flex; flex-direction:column; gap:48px; }
.tp-vp-stage.is-ready{
  display:grid;
  /* 136px, not 124: room for KU/KR's "Finished result" at its active 13px (118px) on one line.
     A longer label wraps instead (below). */
  grid-template-columns:minmax(0,1fr) 136px minmax(0,1fr) 56px;
  gap:clamp(18px,2.2vw,30px);
  align-items:center;
}
.tp-vp-stage.is-ready .tp-vp-film{ grid-column:1; }
.tp-vp-stage.is-ready .tp-vp-rail{ grid-column:2; }
.tp-vp-stage.is-ready .tp-vp-copies{ grid-column:3; }
/* Air between the rail's labels and the step copy, on top of the grid gap: 40-58px in all from
   1000px up, where the gap alone left 22-30px. Jesse, 2026-09-23: "they're kinda close together
   - the step wording and the body / heading of the inside steps". Photo-to-rail is unchanged. */
@media(min-width:901px){
  .tp-vp-stage.is-ready .tp-vp-copies{ padding-left:clamp(18px,2vw,28px); }
}
.tp-vp-stage.is-ready .tp-vp-nav{ grid-column:4; }

/* ⚠ ONE VARIABLE MOVES BOTH COLUMNS. The copy used to drift 52px per step while the
   photographs travelled a full frame, so they visibly diverged — Jesse: "they should come in at
   the same speed and direction as the images". Sharing --vp-y makes identical motion structural
   rather than two numbers someone has to keep in agreement. */
/* ⚠ ONLY THE PHOTOGRAPHS RIDE --vp-y. The copy used to share this transform, which made the
   words feel welded to the image — Jesse: "yours are attached to the image and it feels weird".
   The images travel; the words stay put and pop in. */
.tp-vp-stage.is-ready .tp-vp-film-inner{
  transform:translate3d(0, var(--vp-y, 0px), 0);
}

/* Every copy block shares one cell, so the column is as tall as the tallest step and nothing
   reflows as the step changes. */
.tp-vp-stage.is-ready .tp-vp-copies-inner{ display:grid; }
/* ⚠ A STRAIGHT OPACITY CROSSFADE — NO MOVEMENT. Taken from the reference's own Swiper rather
   than approximated: effect:fade, speed:600, fadeEffect:{crossFade:true}, transition-property
   opacity, timing ease, and NO transform on the text at all. My previous version added a 22px
   rise and a 120ms delay, which is what Jesse meant by the words feeling weird. Do not
   reintroduce either — the reference deliberately keeps the words still while the photographs
   move, and that contrast is the effect. */
.tp-vp-stage.is-ready .tp-vp-slide{
  grid-area:1 / 1;
  opacity:0; visibility:hidden;
  transition:opacity .6s ease, visibility 0s linear .6s;
}
.tp-vp-stage.is-ready .tp-vp-slide.is-active{
  opacity:1; visibility:visible;
  transition:opacity .6s ease, visibility 0s;
}
/* visibility, not display: the inactive blocks stay laid out so the column keeps its height,
   and the outgoing one stops taking clicks the moment it has faded. */

/* ── THE FILMSTRIP ──────────────────────────────────────────────────────────────────────────
   Varco's shape exactly: a fixed clipping window with a vertical strip inside, moved by
   translateY. The 0.1s transition is theirs too — long enough to smooth the per-frame writes,
   short enough that the strip still tracks the scroll rather than trailing it. */
.tp-vp-film{ overflow:hidden; border-radius:16px; aspect-ratio:4/3; }
/* ⚠ height:100% IS LOAD-BEARING. Without it this box is auto-height, so the frames' own
   height:100% resolves against nothing and every frame collapsed to 0px — twelve photographs
   stacked at the same point, strip translating over empty space. */
.tp-vp-film-inner{
  display:block; height:100%;
  transition:transform .1s linear;
  will-change:transform;
}
.tp-vp-frame{
  position:relative; margin:0;
  height:100%;                 /* one frame fills the window; JS reads this to set the pitch */
  background:rgba(255,255,255,.08);
}
.tp-vp-frame img{ width:100%; height:100%; object-fit:cover; display:block; }
/* A pair in one frame: two photographs side by side, split by a hairline of the band colour. */
.tp-vp-pair{ display:grid; grid-template-columns:1fr 1fr; gap:4px; width:100%; height:100%; }
.tp-vp-pair img{ min-width:0; }
.tp-vp-stage:not(.is-ready) .tp-vp-pair{ height:auto; }
.tp-vp-stage:not(.is-ready) .tp-vp-pair img{ aspect-ratio:3/4; height:auto; }

/* Without .is-ready nothing is clipped and no transform is written, so the strip is simply a
   column of twelve photographs. */
.tp-vp-stage:not(.is-ready) .tp-vp-film{ aspect-ratio:auto; overflow:visible; }
.tp-vp-stage:not(.is-ready) .tp-vp-frame{ height:auto; margin-bottom:16px; }

/* Every slide in ONE grid cell: the stage is then as tall as the tallest step, with no
   absolute positioning and no hard-coded height to drift out of date. */
/* ⚠ NO TRANSITION ON THESE WHILE PINNED. Opacity and --d are written every scroll frame, so a
   transition would lag behind the scroll and feel like drag rather than motion. The easing
   comes from the scroll itself. */

/* The strip glides to the next frame rather than tracking a scroll, now that a step is a
   discrete gesture rather than a scroll position. */
.tp-vp-film-inner{ transition:transform .62s var(--ease-reveal); }
@media (prefers-reduced-motion: reduce){
  .tp-vp-film-inner{ transition:none; }
  .tp-vp-stage.is-ready .tp-vp-slide{ transition:opacity .01s; }
  /* ⚠ .tp-vp-seg IS NOT SILENCED HERE, ON PURPOSE. Its base transition is declared LATER in
     this file, so a reduce rule at this point loses on source order and does nothing —
     dev/test-css-traps.php flags exactly that. The one that actually wins sits after the base
     rule, at the end of the day-by-day block. Reduced motion IS honoured; this line was
     redundant, and a dead rule that looks like a safeguard is worse than no rule. */
}

/* ⚠ THE SLIDE IS A PLAIN BLOCK. It used to be a three-column grid holding media, rail and copy
   together; after the restructure it holds ONLY the copy, but the old template survived and was
   still placing that copy in column 3 — an 82px sliver of a 340px column, with the words
   wrapping two to a line. Leftover grid placement on an element that no longer has the children
   it was written for. */

/* The stand-in shown when a timeline photograph has not landed yet — see the note in
   template-parts/topic-stack.php. Deliberately plain and legible rather than decorative: its
   job is to name the file that is missing. */
.tp-vp-ph{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.06);
  border:1px dashed rgba(255,255,255,.34);
  border-radius:inherit;
  color:rgba(255,255,255,.72);
  font-family:var(--sans); font-size:13px; letter-spacing:.04em;
}
.tp-vp-badge{
  position:absolute; top:12px; left:12px;
  padding:7px 13px; border-radius:8px;
  background:rgba(0,0,0,.55); color:#fff;
  font-family:var(--heading); font-size:11px; font-weight:700;
  letter-spacing:.07em; text-transform:uppercase;
}
.tp-vp-day{
  margin:0 0 10px; font-family:var(--heading); font-size:14px; font-weight:700;
  letter-spacing:.1em; text-transform:uppercase; color:var(--brown-soft, #d6b28a);
}
.tp-vp-title{
  font-family:var(--heading); font-weight:800; color:#fff;
  font-size:clamp(22px,2.7vw,34px); line-height:1.16; letter-spacing:var(--track-heading, -0.015em); margin:0 0 14px;
  text-wrap:balance;   /* "Doors off, boxes sprayed in / place" left one word on a line */
}
.tp-vp-body{ margin:0 0 14px; font-size:16px; line-height:1.7; color:rgba(255,255,255,.8); max-width:46ch; }
.tp-vp-group{
  margin:0; font-family:var(--heading); font-size:11px; font-weight:700;
  letter-spacing:.07em; text-transform:uppercase; color:rgba(255,255,255,.5);
}
/* The payoff CTA on the last step only. .btn.btn-brown is the theme's own button, reused
   rather than restyled, so it picks up every hover and focus state the rest of the site has. */
.tp-vp-cta{ margin:20px 0 0; }
/* ⚠ The copy blocks share one grid cell, so the column is already as tall as the tallest step.
   The extra height this adds to step twelve therefore costs no reflow when you reach it. */

/* ── THE CENTRE RAIL ── */
.tp-vp-rail{ display:none; }
/* The rail now has its OWN grid column, so it no longer needs the fixed width and auto margins
   that were compensating for it overlaying the whole stage. */
.tp-vp-stage.is-ready .tp-vp-rail{
  display:flex; flex-direction:column; align-items:stretch; justify-content:center;
  gap:4px; z-index:2;
  pointer-events:none;                      /* only the segments take clicks */
}
/* The segment is now a bar PLUS its day label, so the button is the row and .tp-vp-seg-bar is
   the 4px rule. The whole row stays clickable — a 4px click target is a dart game. */
/* --near is 1 exactly on this step and falls to 0 one step away, written per frame. The height
   therefore GROWS AS YOU APPROACH rather than snapping when you arrive — Jesse: "timeline
   numbers and days shrink until you're on it then it's larger". 1 + 2*near reaches the same 3x
   Varco uses, but continuously. */
.tp-vp-seg{
  --near:0;
  pointer-events:auto;
  display:flex; align-items:center; gap:10px;
  padding:0; border:0; background:none; cursor:pointer;
  flex:none; width:100%;
  height:calc(var(--vp-unit) * (1 + 2 * var(--near)));
}
/* The transition only applies OFF the pinned path (mobile, clicks), where nothing writes
   --near every frame and a step change really is a jump that wants easing. */
/* ⚠ THE SWELL IS BACK. I dropped --near when the pin came out and the segments went to a hard
   on/off — a regression Jesse caught: "I liked how the given day you were on grew dynamically
   like before". --near is written per step now rather than per scroll frame, and the transition
   is what makes it grow rather than jump. The falloff runs over 1.5 steps so the neighbours
   swell a little too, which is what gave the rail its shape. */
.tp-vp-seg{ transition:height .55s var(--ease-reveal); }
@media(max-width:1023px){
  .tp-vp-seg{ transition:width .55s var(--ease-reveal); }
}
.tp-vp-seg-bar{
  width:4px; flex:none; align-self:stretch;
  background:rgba(255,255,255,.28);
  transition:background-color .7s var(--ease);
}
.tp-vp-seg-day{
  font-family:var(--heading); font-weight:700; letter-spacing:.06em;
  /* ⚠ WRAPS, NOT nowrap. Cabinet Painting's "Back in your home" is 146px active against the rail's
     122px of text room, and nowrap pushed it into the copy column beside it (2026-09-23). A label
     that fits stays on one line; one that doesn't breaks into two balanced lines, which the
     segment's height (--vp-unit, 26px+ wherever the rail shows) holds at rest. */
  text-transform:uppercase; white-space:normal; text-wrap:balance; line-height:1.15;
  text-align:left;     /* the row is a <button>, which centres a wrapped second line */
  /* Both scale with proximity, so the labels swell toward the one you are on. */
  font-size:calc(11px + 2px * var(--near));
  color:rgba(255,255,255,calc(0.3 + 0.7 * var(--near)));
}
.tp-vp-seg.is-done .tp-vp-seg-bar{ background:rgba(255,255,255,.5); }
.tp-vp-seg.is-done .tp-vp-seg-day{ color:rgba(255,255,255,.55); }

/* ⚠ BROWN MARKS THE CURRENT STEP. Jesse: "make it brown for the one we're on, differentiating".
   White reads as "covered" and brown as "here", which is the same brown-vs-navy split the other
   prototype's rail uses — inverted, because this band is navy. */
/* Brown fades IN with proximity rather than switching on at the active step, so the marker
   arrives with the growth instead of after it. */
.tp-vp-seg-bar{ position:relative; }
.tp-vp-seg-bar::after{
  content:""; position:absolute; inset:0;
  background:var(--brown); opacity:var(--near);
}
.tp-vp-seg.is-active .tp-vp-seg-day{ color:var(--brown-soft, #d6b28a); }
.tp-vp-stage.is-ready{ --vp-unit:clamp(20px,2.6vw,34px); }

/* "TAP ME" ON THE STEP LOG. Jesse, 2026-10-13, once KR's first label fitted on one line: "now it is not clear to go to
   the next step, how can we make this more clear, maybe a tap here or click here on the kickoff step?". The answer is
   the paint dot's invitation (.tp-rc .is-hint), a ring pulsing on the thing to press until the reader first moves the
   log. nav.js adds .is-hint after the opening paint and drops it for good at the first step change.
   - On a wide screen it sits on the rail's next row, the Kickoff step itself: a solid dot on the bar with the ring
     breathing out of it, the label lifted to white with an arrow after it.
   - On a phone, where the rail has no labels, it rings the ↓ button.
   ⚠ BROWN-SOFT, NOT --brown. The paint dot's --brown ring reads on cream; on this navy band it all but vanished in the
   first build (a 12px ring at --brown was a smudge beside the label). Transform and opacity only; under reduced motion
   the ring stays, still, so the invitation survives without the movement. */
.tp-vp-seg.is-hint{ position:relative; }
.tp-vp-seg.is-hint .tp-vp-seg-day{ color:rgba(255,255,255,.95); }
.tp-vp-seg.is-hint .tp-vp-seg-day::after{ content:"\00a0\2192"; }
.tp-vp-seg.is-hint .tp-vp-seg-bar::before,
.tp-vp-seg.is-hint::after{
  content:''; position:absolute; top:50%; border-radius:50%; pointer-events:none;
}
.tp-vp-seg.is-hint .tp-vp-seg-bar::before{
  left:50%; width:10px; height:10px; margin:-5px 0 0 -5px; background:var(--brown-soft, #d6b28a);
}
.tp-vp-seg.is-hint::after{
  left:2px; width:10px; height:10px; margin:-5px 0 0 -5px;
  border:2px solid var(--brown-soft, #d6b28a);
  animation:ncVpHint 1.8s var(--ease) infinite;
}
.tp-vp-btn[data-vp-next].is-hint{ position:relative; }
.tp-vp-btn[data-vp-next].is-hint::before{
  content:''; position:absolute; inset:-3px; border-radius:50%;
  border:2px solid var(--brown-soft, #d6b28a); pointer-events:none;
  animation:ncVpHintBtn 1.8s var(--ease) infinite;
}
@keyframes ncVpHint{
  0%   { transform:scale(1);   opacity:.95; }
  75%  { transform:scale(2.6); opacity:0; }
  100% { transform:scale(2.6); opacity:0; }
}
@keyframes ncVpHintBtn{
  0%   { transform:scale(1);   opacity:.95; }
  75%  { transform:scale(1.45); opacity:0; }
  100% { transform:scale(1.45); opacity:0; }
}
@media(min-width:901px){
  .tp-vp-btn[data-vp-next].is-hint::before{ display:none; }
}
@media(max-width:900px){
  .tp-vp-seg.is-hint::after,
  .tp-vp-seg.is-hint .tp-vp-seg-bar::before,
  .tp-vp-seg.is-hint .tp-vp-seg-day::after{ display:none; }
}
@media(prefers-reduced-motion:reduce){
  .tp-vp-seg.is-hint::after{ animation:none; transform:scale(1.8); opacity:.6; }
  .tp-vp-btn[data-vp-next].is-hint::before{ animation:none; transform:none; opacity:.85; }
}

@media(max-width:900px){
  /* One column: photo over copy, and the rail becomes a horizontal bar under the panel —
     a 4px vertical thread between two stacked blocks would read as a divider, not progress. */
  .tp-vp-stage.is-ready{ grid-template-columns:1fr; gap:18px; }
  .tp-vp-stage.is-ready .tp-vp-film,
  .tp-vp-stage.is-ready .tp-vp-rail,
  .tp-vp-stage.is-ready .tp-vp-copies,
  .tp-vp-stage.is-ready .tp-vp-nav{ grid-column:1; }
  .tp-vp-nav{ flex-direction:row; justify-content:flex-start; }
  .tp-vp-stage.is-ready .tp-vp-rail{
    grid-area:auto; flex-direction:row; align-items:center; justify-content:flex-start;
    margin-top:22px;
  }
  .tp-vp-seg{
    display:block; height:4px; width:calc(var(--vp-unit) * 1);
    transition:width .7s var(--ease-reveal);
  }
  .tp-vp-seg-bar{ display:block; width:100%; height:4px; align-self:auto; }
  /* Each 4px bar is a button: an invisible 44px-tall touch area around it (the mobile review, 2026-10-07: they were
     14x4px targets). The bars themselves don't move. */
  .tp-vp-seg{ position:relative; }
  .tp-vp-seg::before{ content:''; position:absolute; left:-3px; right:-3px; top:-20px; bottom:-20px; }
  /* Twelve day labels will not fit across 327px; the panel's own day line carries it there. */
  .tp-vp-seg-day{ display:none; }
  .tp-vp-seg.is-active{ height:4px; width:calc(var(--vp-unit) * 3); }
  .tp-vp-stage.is-ready{ --vp-unit:14px; }
  .tp-vp-head{ flex-direction:column; align-items:flex-start; }
}

@media (prefers-reduced-motion: reduce){
  .tp-vp-stage.is-ready .tp-vp-slide, .tp-vp-seg{ transition:none; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════
   IN-HOUSE DESIGN SECTION (.tp-design)

   Three cards on the theme's existing .tp-cards rack. Only the media frame, the copy padding
   and the closing note are new — the card chrome, borders and radii all come from .tp-card.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
/* ⚠ NO CAP ON THE HEAD. 70ch resolved to 766px, and "You work with an in-house designer"
   needs 875px at 48px to stay on one line — so it broke after "in-house" with 578px of the
   container sitting unused beside it. Jesse: "put designer on the same top line pls".

   The measure still exists where it matters: .section-sub carries its own 560px cap globally,
   so the paragraph underneath is unaffected. A heading and a paragraph want different line
   lengths, and capping the block capped both to the paragraph's. */
.tp-design-head{ max-width:none; }
/* ⚠ AND THE PARAGRAPH TOO. Lifting the cap off the head was not enough: .section-sub carries
   its OWN max-width:560px globally, so the sub still folded after eight words with 368px of the
   container unused at 1024 and nearly 800px unused at 1440 — Jesse: "it folds in too soon -
   same as we've seen before". Same fix .tp-vp already uses on its sub, so the two sections that
   sit near each other on this page now wrap alike. */
.tp-design-head .section-sub{ max-width:none; }
.tp-design-cards{ margin-top:clamp(28px,3.5vw,44px); }

/* padding:0 so the photograph can reach the card's edges; the copy re-introduces it. */
.tp-design-card{ padding:0; overflow:hidden; display:flex; flex-direction:column; }

.tp-design-media{
  aspect-ratio:4 / 3;
  background:var(--off);
  border-bottom:1px solid var(--border-warm);
}
.tp-design-media img{ width:100%; height:100%; object-fit:cover; display:block; }

/* ⚠ THE PLACEHOLDER NAMES THE FILE IT WANTS. The drawings are not exported yet, and a card that
   collapses without its image would have Jesse judging a layout that is not the one that ships.
   Printing the expected filename also means the hand-off needs no separate list. */
.tp-design-media--empty{
  display:flex; align-items:center; justify-content:center;
  border:1px dashed var(--border-warm); border-bottom-style:solid;
}
.tp-design-media--empty span{
  font-family:var(--heading); font-size:12px; font-weight:700;
  letter-spacing:.06em; color:var(--muted);
}

.tp-design-copy{ padding:24px 24px 26px; }
.tp-design-copy .tp-card-title{ margin:0 0 10px; }
.tp-design-copy .tp-card-body{ margin:0; }

/* 'lead' (topic-cards.php): the first card runs the full width with its picture beside its words. Three drawings in two
   columns would leave one alone, so Kitchen Designers' marked-up floor plan leads, and its hand notes get the room
   (2026-10-02). From 901px; below that every card stacks, as before. */
@media(min-width:901px){
  .tp-cards--lead > .tp-design-card:first-child{ grid-column:1 / -1; flex-direction:row; align-items:center; }
  .tp-cards--lead > .tp-design-card:first-child .tp-design-media{ flex:0 0 62%; border-bottom:0; border-right:1px solid var(--border-warm); }
  .tp-cards--lead > .tp-design-card:first-child .tp-design-copy{ padding:32px 36px; }
}

/* Binder p57 as a section, the 'selections' type (template-parts/topic-section.php): the six choices on the left; on
   the right, the sample board and the kitchen it became. Rules between the choices, as the binder draws them. */
/* One picture beside the choices (2026-10-02): the picture left, the six choices in two columns right, their note under
   them, centred against each other so neither side runs long. */
.tp-sel-grid{ display:grid; grid-template-columns:minmax(0,5fr) minmax(0,7fr); gap:clamp(28px,4vw,64px); align-items:center; margin-top:clamp(28px,3.5vw,44px); }
.tp-sel-list{ list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:clamp(20px,2.4vw,36px); }
.tp-sel-item{ padding:18px 0; border-top:1px solid var(--border-warm); }
.tp-sel-name{ font-family:var(--heading); font-weight:800; font-size:19px; line-height:1.3; color:var(--navy); margin:0 0 6px; letter-spacing:var(--track-heading, normal); }
.tp-sel-body{ margin:0; color:var(--text-2); line-height:1.6; }
.tp-sel-fig{ margin:0; }
.tp-sel-cap{ font-family:var(--sans); font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin:12px 0 0; }
.tp-sel-cap a{ color:var(--brown-dark); text-decoration:none; }
.tp-sel-cap a:hover{ color:var(--brown); }
.tp-sel-fig img{ display:block; width:100%; height:auto; border-radius:14px; }
/* The binder's closing line, set like the design section's note below it, so the two read as one family. */
.tp-sel-note{ margin:clamp(20px,2.4vw,28px) 0 0; padding-left:16px; border-left:3px solid var(--brown); border-radius:2px; font-size:16px; line-height:1.7; color:var(--text-2); }
.tp-sel-note strong{ color:var(--navy); }
@media(max-width:900px){
  .tp-sel-grid{ grid-template-columns:1fr; }
}
@media(max-width:560px){
  .tp-sel-list{ grid-template-columns:1fr; }
}

/* The binder's closing line. Measured rather than centred: it reads as a footnote to the three
   cards, so it keeps the head's left edge instead of floating in the middle. */
.tp-design-note{
  /* ⚠ NO CAP, for the same reason the sub above has none: at 74ch the first half wrapped
     BEFORE reaching the em dash, so the note ran to three lines and the deliberate break
     was buried in the middle of one. Jesse asked for it to fold ONLY at the dash. */
  max-width:none; margin:clamp(24px,3vw,36px) 0 0;
  padding-left:16px; border-left:3px solid var(--brown); border-radius:2px;
  font-size:16px; line-height:1.7; color:var(--text-2);
}

@media(max-width:900px){
  .tp-design-cards{ grid-template-columns:1fr; }
  .tp-design-card{ display:grid; grid-template-columns:150px 1fr; align-items:stretch; }
  .tp-design-media{ aspect-ratio:auto; border-bottom:0; border-right:1px solid var(--border-warm); }
  .tp-design-copy{ padding:18px 18px 20px; }
}
@media(max-width:560px){
  /* Below this a 150px thumbnail leaves the copy in a column too narrow to read. */
  .tp-design-card{ grid-template-columns:1fr; }
  .tp-design-media{ aspect-ratio:4/3; border-right:0; border-bottom:1px solid var(--border-warm); }
}

/* The design section's secondary link. Sits under the note, above the closing CTA band, so it
   is deliberately outline rather than filled — see the reasoning in topic-layout.php. */
.tp-design-cta{ margin:clamp(20px,2.4vw,28px) 0 0; }

/* ── UPGRADE IDEAS RACK (.tp-upg) ──────────────────────────────────────────────────────────
   A browse rack, not a form. See northco_topic_upgrades() for why this and the estimator's
   upgrade grid answer "how many is too many" in opposite directions.

   Native overflow + scroll-snap, no carousel script: it scrolls correctly before any JS runs
   and keeps scrolling if a script fails. The arrows in nav.js only nudge scrollLeft. */
.tp-upg-head{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:24px; flex-wrap:wrap; margin-bottom:clamp(20px,2.4vw,30px);
}
.tp-upg-head-text{ max-width:none; }
/* Same cap, same symptom — both sublines wrapped, so a two-line fold rendered as four. The
   head-text column is 811px here, wide enough for either line whole. */
.tp-upg .section-sub{ max-width:none; }
.tp-upg .eyebrow{ margin-bottom:10px; }
.tp-upg .section-heading{ margin-top:0; }

.tp-upg-nav{ display:none; gap:10px; }
@media(min-width:900px){ .tp-upg-nav{ display:flex; } }
.tp-upg-arr{
  width:44px; height:44px; border-radius:50%;
  border:1px solid var(--border-warm); background:#fff; color:var(--navy);
  font-size:17px; line-height:1; cursor:pointer;
  transition:background var(--dur-base,200ms) var(--ease), color var(--dur-base,200ms) var(--ease);
}
.tp-upg-arr:hover{ background:var(--navy); color:#fff; }
.tp-upg-arr[disabled]{ opacity:.35; cursor:default; }
.tp-upg-arr[disabled]:hover{ background:#fff; color:var(--navy); }

/* ── Chips ──────────────────────────────────────────────────────────────────────────────
   A radio group: one choice at a time, arrow keys work, and the checked state IS the state —
   nothing for JS to hold or get out of sync with. */
/* ⚠ ONE SCROLLING ROW ON A PHONE, NOT A WRAPPING BLOCK. Measured at 375px: four chips wrap
   to THREE rows and push the first card 100px further down, on a page where 85% of traffic is
   a thumb and the rack below already scrolls sideways. Wrapping returns at 700px, where all
   four fit on one line anyway and a scroll region would be a worse affordance than none. */
.tp-upg-chips{
  display:flex; gap:8px; flex-wrap:nowrap;
  overflow-x:auto; overscroll-behavior-x:contain;
  scrollbar-width:none; -webkit-overflow-scrolling:touch;
  margin:0 0 clamp(18px,2vw,26px);
  padding-bottom:2px;
}
.tp-upg-chips::-webkit-scrollbar{ display:none; }
.tp-upg-chip{ flex:0 0 auto; }
@media(min-width:700px){
  .tp-upg-chips{ flex-wrap:wrap; overflow-x:visible; }
}
/* ⚠ position:relative IS LOAD-BEARING, NOT TIDINESS. The radio below is absolutely
   positioned; without a positioned ancestor its containing block is the INITIAL one, i.e. the
   document — so it is laid out against the page, not the chip, and .tp-upg-chips' overflow
   cannot clip it. With the chips scrolled, those escaped radios sat far to the right and gave
   the whole document a scrollWidth of 558 against a 375 viewport. The page scrolled sideways
   on a phone, and every 100%-width element (the nav, the scrim) stretched to match, which is
   what made it look like a nav bug. */
.tp-upg-chip{ cursor:pointer; position:relative; }
/* Visually hidden the safe way — 1px and clipped, never display:none, which would take the
   radio out of the tab order and stop the chips working from a keyboard. Same treatment as
   .tp-cmp-radio; a bare opacity:0 still occupies its full box. */
.tp-upg-chip input{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}
.tp-upg-chip span{
  display:inline-flex; align-items:center; gap:7px;
  padding:9px 15px; border-radius:999px;
  border:1px solid var(--border-warm); background:#fff;
  font-family:var(--heading); font-size:12.5px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:var(--navy);
  transition:background var(--dur-base,200ms) var(--ease), color var(--dur-base,200ms) var(--ease), border-color var(--dur-base,200ms) var(--ease);
}
.tp-upg-chip span i{ font-style:normal; font-size:11px; opacity:.55; }
.tp-upg-chip:hover span{ border-color:var(--navy); }
.tp-upg-chip input:checked + span{ background:var(--navy); border-color:var(--navy); color:#fff; }
/* Keyboard focus has to be visible on a control whose own input is off-screen. */
.tp-upg-chip input:focus-visible + span{ outline:2px solid var(--brown); outline-offset:2px; }

/* ── The track ──────────────────────────────────────────────────────────────────────────
   scroll-padding matches the inner gutter so a snapped card lands flush with the heading
   above it rather than jammed against the viewport edge. */
.tp-upg-track{
  display:flex; gap:clamp(14px,1.6vw,20px);
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory; scroll-behavior:smooth;
  padding:4px 0 18px; margin:0 0 -4px;
  -webkit-overflow-scrolling:touch;
}
.tp-upg-track:focus-visible{ outline:2px solid var(--brown); outline-offset:4px; }
/* A scrollbar under a photo rack reads as chrome; the cards themselves are the affordance. */
.tp-upg-track{ scrollbar-width:none; }
.tp-upg-track::-webkit-scrollbar{ display:none; }

/* ── EDGE FADE ────────────────────────────────────────────────────────────────────────────
   Jesse: "can you put some blur or something on the edge one? so it doesn't just cut off?
   looks like it's missing something you know?"

   The card at the container edge was sliced by a hard vertical line, which reads as a broken
   layout rather than as more content. A mask fades it out instead, so the cut says "keep
   swiping" — the same thing the peeking card does, finished properly.

   ⚠ FADED PER SIDE, NOT BOTH AT ONCE. A symmetric mask would dim the FIRST card before anyone
   has scrolled, hiding content that is not overflowing anything — the fade has to mean "there
   is more this way", so it only appears on a side that actually has more. nav.js writes
   data-at as the track scrolls; the arrows' sync() already computes exactly this state, so
   there is no second listener and nothing new to keep in step.

   mask-image is unprefixed everywhere current; Safari needed -webkit- until 15.4 and still
   honours it, so both are set. Where neither applies the rack simply has hard edges again —
   which is where it started, not a break. */
.tp-upg-track{
  --upg-fade:56px;
  -webkit-mask-image:var(--upg-mask,none);
          mask-image:var(--upg-mask,none);
}
.tp-upg-track[data-at="end"],
.tp-upg-track[data-at="middle"]{
  --upg-mask:linear-gradient(to right, transparent 0, #000 var(--upg-fade), #000 100%);
}
.tp-upg-track[data-at="start"]{
  --upg-mask:linear-gradient(to right, #000 0, #000 calc(100% - var(--upg-fade)), transparent 100%);
}
.tp-upg-track[data-at="middle"]{
  --upg-mask:linear-gradient(to right, transparent 0, #000 var(--upg-fade), #000 calc(100% - var(--upg-fade)), transparent 100%);
}
/* Nothing overflows, so nothing should be faded. */
.tp-upg-track[data-at="none"]{ --upg-mask:none; }

.tp-upg-card{
  scroll-snap-align:start;
  flex:0 0 78%;               /* a sliver of the next card shows, which is what says "swipe" */
  max-width:320px;
  display:flex; flex-direction:column;
  background:#fff; border:1px solid var(--border-warm); border-radius:14px;
  overflow:hidden;
}
@media(min-width:700px){ .tp-upg-card{ flex-basis:44%; max-width:340px; } }
@media(min-width:1100px){ .tp-upg-card{ flex-basis:30%; max-width:360px; } }

.tp-upg-media{ position:relative; aspect-ratio:8/5; background:var(--off); }
.tp-upg-media img, .tp-upg-media .uc-photo{
  width:100%; height:100%; object-fit:cover; display:block;
}
/* northco_upgrade_photo() renders .uc-photo-ph when no file exists yet. Four keys here are
   still awaiting photos, so this state is on screen today, not hypothetical. */
.tp-upg-media .uc-photo-ph{
  display:flex; align-items:center; justify-content:center; text-align:center;
  height:100%; padding:16px;
  border:1px dashed var(--border-warm); background:var(--off);
  font-family:var(--heading); font-size:12px; font-weight:700;
  letter-spacing:.04em; text-transform:uppercase; color:var(--muted);
}

.tp-upg-body{ padding:16px 18px 20px; display:flex; flex-direction:column; flex:1; }
.tp-upg-name{ margin:0; font-size:clamp(16px,1.15vw,18px); line-height:1.25; color:var(--navy); letter-spacing:var(--track-heading, normal); }
.tp-upg-strap{
  margin:5px 0 12px; font-family:var(--body); font-size:13.5px; font-weight:600;
  line-height:1.4; color:var(--brown);
}
.tp-upg-points{ margin:0; padding:0 0 0 1.05em; list-style:disc; }
.tp-upg-points li{ margin:0 0 7px; font-size:14px; line-height:1.55; color:var(--ink); }
.tp-upg-points li:last-child{ margin-bottom:0; }
.tp-upg-points li::marker{ color:var(--brown); }

/* The Kitchen Renovation label. Sits above the name so the scope is read before the idea is
   wanted, which is the honest order. */
.tp-upg-pill{
  align-self:flex-start; margin:0 0 9px;
  padding:4px 10px; border-radius:999px;
  background:var(--off); border:1px solid var(--border-warm);
  font-family:var(--heading); font-size:10.5px; font-weight:700;
  letter-spacing:.05em; text-transform:uppercase; color:var(--brown);
  text-decoration:none;
}
.tp-upg-pill:hover{ background:var(--brown); border-color:var(--brown); color:#fff; }

.tp-upg-cta{ margin-top:clamp(18px,2vw,26px); }

/* ── Chip filtering, done in CSS ────────────────────────────────────────────────────────
   :has() on the section lets a checked radio hide the cards that do not match, with no JS and
   no nodes removed — every card stays in the DOM under every filter, which is what keeps the
   seven V1 h3/h4 pairs present for the heading diff.

   ⚠ NO :has() FALLBACK NEEDED, and that is a measured claim rather than an optimistic one:
   :has() has been in every current engine since Firefox 121 (Dec 2023). Where it is missing
   the rule simply never matches, every card stays visible, and the rack degrades to an
   unfiltered sixteen-card swipe — which is the pre-chip design, not a broken one. */
.tp-upg:has(input[value="storage"]:checked)   .tp-upg-card:not([data-upg-group="storage"]),
.tp-upg:has(input[value="cabinetry"]:checked) .tp-upg-card:not([data-upg-group="cabinetry"]),
.tp-upg:has(input[value="surfaces"]:checked)  .tp-upg-card:not([data-upg-group="surfaces"]),
/* Kitchen Designers' ideas tile, the same rack with a tab per surface (topic-section.php 'ideas'). */
.tp-upg:has(input[value="countertops"]:checked) .tp-upg-card:not([data-upg-group="countertops"]),
.tp-upg:has(input[value="backsplash"]:checked)  .tp-upg-card:not([data-upg-group="backsplash"]),
.tp-upg:has(input[value="flooring"]:checked)    .tp-upg-card:not([data-upg-group="flooring"]),
/* Bathroom Renovation's rack. ⚠ A NEW GROUP NAME NEEDS ITS LINE HERE, or its chip does nothing when clicked: these two
   shipped without one (Jesse, 2026-10-06: "the organization buttons aren't doing anything"). dev/test-upg-filters.php
   now fails for any rendered chip without a rule. */
.tp-upg:has(input[value="shower"]:checked)      .tp-upg-card:not([data-upg-group="shower"]),
.tp-upg:has(input[value="vanity"]:checked)      .tp-upg-card:not([data-upg-group="vanity"]),
/* Siding's materials carousel (2026-10-07): a chip per family, from the exterior binder's catalogue. */
.tp-upg:has(input[value="vinyl"]:checked)       .tp-upg-card:not([data-upg-group="vinyl"]),
.tp-upg:has(input[value="fiber"]:checked)       .tp-upg-card:not([data-upg-group="fiber"]),
.tp-upg:has(input[value="wood"]:checked)        .tp-upg-card:not([data-upg-group="wood"]),
.tp-upg:has(input[value="metal"]:checked)       .tp-upg-card:not([data-upg-group="metal"]),
/* The exterior rack (Siding and Wrap, 2026-10-07). */
.tp-upg:has(input[value="soffit"]:checked)      .tp-upg-card:not([data-upg-group="soffit"]),
.tp-upg:has(input[value="eaves"]:checked)       .tp-upg-card:not([data-upg-group="eaves"]),
.tp-upg:has(input[value="extras"]:checked)      .tp-upg-card:not([data-upg-group="extras"]){
  display:none;
}

@media(prefers-reduced-motion:reduce){
  .tp-upg-track{ scroll-behavior:auto; }
}

/* ── BEFORE / AFTER PROJECTS (.tp-cmp) ────────────────────────────────────────────────────
   Four projects, two across, each a drag-to-compare wipe with its caption underneath.

   The wipe is a range input stretched over the frame at opacity 0, driving --pos, which clips
   the BEFORE layer. Deliberately boring, and that is why it works: drag, tap-to-jump, touch,
   arrow keys, Home/End, screen-reader announcement and a real focus ring all come free from
   the browser. See the note at the head of template-parts/topic-compare.php. */
.tp-cmp .eyebrow{ margin-bottom:10px; }
.tp-cmp .section-heading{ margin-top:0; }

/* ⚠ THIS IS WHY THE SUB LOOKED BROKEN, AND IT WAS NEVER THE WORDING. .section-sub caps at
   max-width:560px, which is about 62 characters at 18px — so the first .tp-subline (69 chars)
   wrapped, and a deliberate TWO-line fold rendered as three ragged lines.

   The day-by-day and the design section already solve this: .tp-vp-head-text and
   .tp-design-head both lift the cap, which is how a 139-character line sits on one line there.
   Jesse asked for these to look "like the rest of the page" — so they get the same treatment
   rather than copy cut to fit a cap the rest of the page does not have. */
.tp-cmp .section-sub{ max-width:none; }

.tp-cmp-list{
  display:grid; gap:clamp(24px,3vw,40px);
  margin-top:clamp(26px,3vw,40px);
}
@media(min-width:760px){ .tp-cmp-list{ grid-template-columns:1fr 1fr; } }
@media(min-width:760px){ .tp-cmp-list--one{ grid-template-columns:minmax(0,880px); } }
.tp-cmp-item{ margin:0; display:flex; flex-direction:column; }

/* ── The frame ────────────────────────────────────────────────────────────────────────────
   4:3, matching the day-by-day timeline's photo frame (measured 537x403 at 1440) so the page
   keeps one photo language. overflow:hidden is load-bearing — it is what the clip-path edge
   and the handle are trimmed against. */
.tp-cmp-wipe{
  position:relative; aspect-ratio:4/3; overflow:hidden;
  border-radius:14px; border:1px solid var(--border-warm); background:var(--off);
  line-height:0;                 /* kills the inline-image gap under the photo */
  touch-action:pan-y;            /* let the page scroll vertically off the frame */
}
.tp-cmp-layer{ display:block; }
.tp-cmp-layer img{ width:100%; height:100%; object-fit:cover; display:block; }

.tp-cmp-layer--after{ position:relative; z-index:1; }
.tp-cmp-layer--before{
  position:absolute; inset:0; z-index:2;
  /* Clipped from the right, so --wipe reads as "how much BEFORE is showing". */
  clip-path:inset(0 calc(100% - var(--wipe,50) * 1%) 0 0);
}

/* ── The handle ───────────────────────────────────────────────────────────────────────────
   A hairline with a grip on it. The grip carries two arrows drawn in CSS rather than an icon
   font or an SVG request, because it is two triangles and the frame should not wait on a
   network round trip to look interactive. */
.tp-cmp-handle{
  position:absolute; top:0; bottom:0; z-index:3;
  left:calc(var(--wipe,50) * 1%); width:2px; margin-left:-1px;
  background:#fff; box-shadow:0 0 0 1px rgba(22,16,68,.18);
  pointer-events:none;           /* the range underneath is the real control */
}
.tp-cmp-grip{
  position:absolute; top:50%; left:50%;
  width:44px; height:44px; margin:-22px 0 0 -22px;   /* 44px: the minimum comfortable touch target */
  border-radius:50%; background:#fff;
  box-shadow:0 2px 12px rgba(22,16,68,.3);
  display:flex; align-items:center; justify-content:center; gap:4px;
  transition:transform var(--dur-base,200ms) var(--ease);
}
.tp-cmp-grip::before, .tp-cmp-grip::after{
  content:''; width:0; height:0;
  border-top:5px solid transparent; border-bottom:5px solid transparent;
}
.tp-cmp-grip::before{ border-right:6px solid var(--navy); }
.tp-cmp-grip::after{ border-left:6px solid var(--navy); }

.tp-cmp-wipe:hover .tp-cmp-grip{ transform:scale(1.08); }
.tp-cmp-range:active ~ .tp-cmp-handle .tp-cmp-grip{ transform:scale(.94); }

/* ── The control ──────────────────────────────────────────────────────────────────────────
   ⚠ opacity:0 AND NOT display:none / visibility:hidden — it has to stay hit-testable and
   focusable. The thumb is sized to the grip so the browser's own value mapping lines the
   handle up with the pointer at both extremes; a default thumb is ~16px and the handle would
   drift from the cursor near the edges. */
.tp-cmp-range{
  position:absolute; inset:0; z-index:4;
  width:100%; height:100%; margin:0; padding:0;
  opacity:0; cursor:ew-resize;
  -webkit-appearance:none; appearance:none; background:none;
}
.tp-cmp-range::-webkit-slider-thumb{ -webkit-appearance:none; width:44px; height:44px; }
.tp-cmp-range::-moz-range-thumb{ width:44px; height:44px; border:0; }
.tp-cmp-range:focus{ outline:none; }
.tp-cmp-range:focus-visible ~ .tp-cmp-handle .tp-cmp-grip{ outline:2px solid var(--brown); outline-offset:3px; }

/* ── Labels ───────────────────────────────────────────────────────────────────────────────
   They fade out as their own side is squeezed away, so the frame never claims "Before" over
   a sliver of before. Driven off --pos with no JS. */
.tp-cmp-tag{
  position:absolute; bottom:12px; z-index:3;
  font-family:var(--heading); font-size:10.5px; font-weight:700; letter-spacing:.07em;
  text-transform:uppercase; color:#fff; line-height:1;
  padding:7px 12px; border-radius:999px; background:rgba(22,16,68,.7);
  pointer-events:none;
  transition:opacity var(--dur-base,200ms) var(--ease);
}
/* ⚠ --wipe IS UNITLESS (0-100) AND --pos IS NOT. The first build did arithmetic on --pos,
   which carries a %, and CSS calc cannot divide a percentage by a unit — the whole declaration
   was invalid and dropped, so the labels sat at opacity 1 at every position and the fade
   silently did nothing. Caught by reading computed opacity at 5%, 50% and 90%, not by looking
   at it. --pos is still set alongside, because the older .tp-compare markup on other pages
   interpolates it straight into clip-path and left and needs the unit. */
.tp-cmp-tag--b{ left:12px;  opacity:clamp(0, calc((var(--wipe,50) - 8) / 10), 1); }
/* After wears the brand brown so the two pills read as two STATES rather than two labels —
   at a glance the colour tells you which side of the handle you are looking at. Solid rather
   than the navy's 70% alpha: brown over a bright after-shot needs the extra weight to hold
   white text. */
.tp-cmp-tag--a{ right:12px; background:var(--brown); opacity:clamp(0, calc((92 - var(--wipe,50)) / 10), 1); }

/* ── Placeholder, until a pair lands ──────────────────────────────────────────────────── */
.tp-cmp-ph{
  display:flex; align-items:center; justify-content:center; text-align:center;
  padding:16px; border-style:dashed; line-height:1.8;
  font-family:var(--heading); font-size:11px; font-weight:700;
  letter-spacing:.04em; text-transform:uppercase; color:var(--muted);
}

/* ── The caption ──────────────────────────────────────────────────────────────────────── */
.tp-cmp-copy{ padding:16px 2px 0; }
.tp-cmp-name{ margin:0; font-size:clamp(18px,1.35vw,21px); line-height:1.2; color:var(--navy); }
.tp-cmp-strap{
  margin:5px 0 10px; font-family:var(--body); font-size:14.5px; font-weight:600;
  line-height:1.4; color:var(--brown);
}
.tp-cmp-copy p{ margin:0; font-size:14.5px; line-height:1.6; color:var(--ink); }
.tp-cmp-link{
  display:inline-block; margin-top:12px;
  font-family:var(--heading); font-size:12px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:var(--brown); text-decoration:none;
}
.tp-cmp-link:hover{ color:var(--navy); }

@media(prefers-reduced-motion:reduce){
  .tp-cmp-grip, .tp-cmp-tag{ transition:none; }
}

/* ── DEFERRED SECTION, FOLDED (.tp-fold) ──────────────────────────────────────────────────
   Wraps whole generated sections, so it is <details> rather than the .faq-item accordion —
   see the note at the foot of template-parts/topic-body.php. */
.tp-fold-d{ border-top:1px solid var(--border-warm); border-bottom:1px solid var(--border-warm); }
.tp-fold-s{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:22px 0; cursor:pointer;
  list-style:none;                       /* Firefox */
}
.tp-fold-s::-webkit-details-marker{ display:none; }   /* Safari/Chrome default triangle */
.tp-fold-label{
  font-family:var(--heading); font-size:clamp(15px,1.15vw,17px); font-weight:700;
  color:var(--navy);
}
.tp-fold-note{ font-size:14px; line-height:1.5; color:var(--muted); }
.tp-fold-icon{
  position:relative; margin-left:auto; flex:0 0 auto; width:18px; height:18px;
}
.tp-fold-icon::before, .tp-fold-icon::after{
  content:''; position:absolute; top:50%; left:50%;
  transform:translate(-50%,-50%); background:var(--brown);
}
.tp-fold-icon::before{ width:14px; height:2px; }
.tp-fold-icon::after{ width:2px; height:14px; transition:transform var(--dur-base,200ms) var(--ease); }
.tp-fold-d[open] .tp-fold-icon::after{ transform:translate(-50%,-50%) scaleY(0); }
.tp-fold-s:focus-visible{ outline:2px solid var(--brown); outline-offset:3px; }

/* The sections inside were generated as full-width bands, so their own padding and alt
   backgrounds have to stand down — they are content within a section now, not siblings of it. */
.tp-fold-body > .section{ padding-left:0; padding-right:0; }
.tp-fold-body > .section:first-child{ padding-top:0; }
.tp-fold-body > .section .inner{ padding-left:0; padding-right:0; max-width:none; }
.tp-fold-body > .tp-band--alt{ background:transparent; }

/* ── INSIDE THE FOLD, THE STEP-BY-STEP LAYS FLAT ─────────────────────────────────────────
   Jesse, 2026-09-22, on KR and then KU: "clean up the formatting inside the step by step drop
   down". The focus carousel is right for the page body: one step at a time, his own call. It is
   wrong in here. Whoever opens "Read the full step-by-step" asked to read every step, and inside
   the fold the carousel's centring set the first card off the heading's left edge, with its
   neighbours shrunk and dimmed. So in the fold the same markup lays flat: every step at once,
   in reading order, aligned to the heading. The markup is untouched, so every V1 h2/h3/h4 is
   still there and still indexed. */
.tp-fold-body h2.section-heading{ font-size:clamp(24px,2.4vw,32px); }
.tp-fold-body .tp-carousel-viewport{ overflow:visible; scroll-snap-type:none; padding:0; margin:0; }
.tp-fold-body .tp-carousel-track{
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:20px;
  padding-inline:0;
}
.tp-fold-body .tp-slide{
  transform:none; opacity:1; transition:none;
  grid-template-columns:1fr; align-content:start;
}
.tp-fold-body .tp-slide-media{ aspect-ratio:16/10; min-height:0; }
.tp-fold-body .tp-slide-body{ padding:20px 22px 24px; align-self:start; }
.tp-fold-body .tp-slide-title{ font-size:19px; }
.tp-fold-body .tp-slide-strap{ font-size:15px; margin-bottom:10px; }
.tp-fold-body .tp-slide-text{ font-size:14.5px; line-height:1.6; max-width:none; }
.tp-fold-body .tp-carousel-nav{ display:none; }
@media(max-width:1000px){
  .tp-fold-body .tp-carousel-track{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media(max-width:640px){
  .tp-fold-body .tp-carousel-track{ grid-template-columns:1fr; }
}

/* The "more kitchens" row under the before/after grid. Carries the two internal links V1's
   before/after block held — see northco_topic_compare()'s 'more'. */
.tp-cmp-more{
  margin:clamp(22px,2.6vw,32px) 0 0;
  font-size:14.5px; line-height:1.9; color:var(--muted);
}
.tp-cmp-more span{ margin-right:4px; }
.tp-cmp-more a{
  color:var(--navy); font-weight:600;
  text-decoration:underline; text-underline-offset:2px; text-decoration-thickness:1px;
}
.tp-cmp-sep{ margin:0 7px; color:var(--border-warm); }
.tp-cmp-more a:hover{ color:var(--brown); }

/* ── A NAVY BAND, TO BREAK A LONG SCROLL (.tp-band--navy) ─────────────────────────────────
   Applied per-section by northco_topic_band_tone(). See the note there for why it is one per
   page and why this heading in particular.

   ⚠ IT SITS ALONGSIDE .tp-band--alt RATHER THAN REPLACING IT, so every rule below has to beat
   the --alt rules it inherits. They are equal specificity (0,0,2,0), so these win on source
   order alone — which is why this block lives at the END of the file and must stay there. Move
   it above the .tp-band--alt rules and the cards silently go white-on-navy-on-white again. */
.tp-band--navy{ background:var(--navy); }

.tp-band--navy .section-heading,
.tp-band--navy .tp-bento-title,
.tp-band--navy .eyebrow{ color:#fff; }
.tp-band--navy .section-sub,
.tp-band--navy .tp-prose p,
.tp-band--navy .tp-bento-body{ color:rgba(255,255,255,.82); }

/* ⚠ .eyebrow's rule is drawn with ::before and inherits currentColor nowhere — it is set to
   var(--brown), which is legible on cream and muddy on navy. Lifted to the lighter gold. */
.tp-band--navy .eyebrow::before,
.tp-band--navy .eyebrow::after{ background:var(--brown-light); }

/* Cards become a lift out of the navy rather than a panel on top of it. A solid light card
   here would read as four cream holes punched in the band, which is louder than the band. */
.tp-band--navy .tp-bento-card{
  background:rgba(255,255,255,.07);
  border-color:rgba(255,255,255,.14);
}
/* ⚠ THE PANEL GOES DARKER, NOT LIGHTER, AND THAT IS COUNTER-INTUITIVE ENOUGH TO WRITE DOWN.
   The instinct when an icon is hard to see is to brighten its chip. Gold is a MID-TONE, so
   lifting the chip toward white moves it toward the glyph and contrast falls: at .12 white the
   gold read 4.22:1, and "brightening" it to .16 took it DOWN to 3.56. I shipped that for one
   commit before measuring it.

   Gold sits at luminance .299 against navy's .0096 — 5.86:1 with no chip at all. So the chip
   should be barely there: .06 white keeps the rounded-square shape the cream version has while
   staying close enough to navy to leave the glyph its contrast. The border carries the shape
   instead, tinted gold so the chip still reads as a container rather than a smudge. */
.tp-band--navy .tp-bento-icon{
  background:rgba(255,255,255,.06);
  border-color:rgba(187,140,87,.38);
  color:var(--brown-light);
}

/* ⚠ THE ACCENT CARD HAD TO BE RE-ASSERTED, and the comment here used to claim the opposite.
   I wrote that it "keeps its job without any change" — it did not. .tp-band--navy
   .tp-bento-card above is 0,0,2,0, the same specificity as .tp-band--alt .tp-bento-card--accent,
   and later in the file, so the one point of emphasis on the page quietly became the same
   translucent white as its three neighbours. Measured, not spotted: computed background came
   back rgba(255,255,255,.07) where brown was expected.

   Brown on navy carries MORE contrast than brown on cream did, so the card does its job better
   here — but only because this rule exists. */
.tp-band--navy .tp-bento-card--accent{
  background:var(--brown);
  border-color:var(--brown);
}
/* ⚠ FULL WHITE ON THE ACCENT CARD ONLY. The band sets .tp-bento-body to rgba(255,255,255,.82),
   which is comfortably readable on navy (measured 10.37:1) and washed out on brown: 3.17:1
   against the 4.5 small text needs. Pure white takes it to 3.88 — strictly better, visually
   almost identical, and no brand colour moved.

   ⚠ IT STILL DOES NOT REACH AA, AND THAT IS A DECISION NOT A MISS. #A77845 cannot carry small
   white text at 4.5:1 no matter the alpha. The two ways to get there both change how the card
   looks, so they are Jesse's call rather than mine:
       navy body text on the brown   4.54  — keeps the brand colour, inverts the card
       darken the brown to #96683A   4.84  — keeps white text, moves the colour
   Recorded here rather than left as a silent 3.17. The card's TITLE is fine either way: at
   18px+ bold it needs 3:1 and measures 3.88.

   Pre-existing on Kitchen Updates too — this rule fixes both, since the navy band is shared. */
.tp-band--navy .tp-bento-card--accent .tp-bento-body{ color:#fff; }

/* ⚠ LINKS. The global a{color:var(--brown)} is unreadable on navy — the same trap already
   recorded for the accent card. */
.tp-band--navy a:not(.btn){ color:var(--brown-light); }
.tp-band--navy a:not(.btn):hover{ color:#fff; }
/* The numbered list on navy. Bathroom's "why NorthCo" (2026-09-24) is the first .tp-num on the band, and its body text
   (--text-2) and half-opacity badges all but vanished against it. Borders dim to match; none are added. */
.tp-band--navy .tp-num,
.tp-band--navy .tp-num-item{ border-color:rgba(255,255,255,.16); }
.tp-band--navy .tp-num-body{ color:rgba(255,255,255,.86); }
.tp-band--navy .tp-num-badge{ color:var(--brown-light); opacity:1; }
/* The checklist on navy, the same fix: the siding pages' "why NorthCo" (2026-09-25) is the first .tp-checks on the band,
   and its --text-2 lines were near-invisible against it. The tick's disc and mark lift to match. */
.tp-band--navy .tp-check{ color:rgba(255,255,255,.86); }
.tp-band--navy .tp-check::before{ background:rgba(255,255,255,.12); }
.tp-band--navy .tp-check::after{ border-color:var(--brown-light); }

/* ══ THREE THINGS THE NAVY BAND BROKE, ALL BY WINNING A SPECIFICITY TIE ══════════════════
   Jesse: "button outline is gone and the icons inside the squares of the four areas are hard
   to see or not visible at all". All three are this block overriding rules that were correct
   before it existed, and all three measured rather than eyeballed.

   1. THE ACCENT CARD'S ICON. .tp-bento-card--accent .tp-bento-icon sets color:#fff; the navy
      rule above is the same specificity and later, so the glyph became --brown-light ON BROWN
      — measured 1.29:1, which is invisible, exactly as reported.
   2. THE ACCENT CARD'S LINKS. Same cause via a:not(.btn). "Kitchen Renovation" and "Cabinet
      Painting" were also 1.29:1 on their own card.
   3. THE BUTTON. .btn-dark is background:var(--navy) with white text — a navy button on a
      navy band, with border-width 0. It did not lose an outline; it never had one, and until
      now it never needed one because it sat on cream.

   ⚠ THE ACCENT CARD IS ITS OWN SURFACE, NOT PART OF THE NAVY ONE. That is the general lesson
   here: every rule scoped to .tp-band--navy has to exclude it, or it will keep repainting a
   brown card as though it were navy. */
.tp-band--navy .tp-bento-card--accent .tp-bento-icon{
  background:rgba(255,255,255,.16);
  border-color:rgba(255,255,255,.3);
  color:#fff;
}
.tp-band--navy .tp-bento-card--accent a:not(.btn){
  color:#fff;
  text-decoration:underline; text-underline-offset:2px;
}
.tp-band--navy .tp-bento-card--accent a:not(.btn):hover{ color:var(--navy); }

/* ⚠ BROWN, NOT WHITE. This rule used to invert .btn-dark to a white fill with navy text, on the
   reasoning that a white button is the loudest thing available on navy. It is the loudest, and
   that was the problem — Jesse: "default white isn't clear enough". A white slab on a dark band
   reads as a panel rather than a control, and it is the only white fill anywhere on the page, so
   it looks like a mistake rather than a call to action.

   Brown is what every other primary CTA on the site already is (.btn-brown, same fill and the
   same white text), so the section's single call to action now looks like a call to action
   everywhere else on the site.

   Measured before changing it:
     button against the band   4.34:1  — non-text UI contrast needs 3:1, so the shape reads
     white text on the brown   3.88:1  — identical to every .btn-brown on the site
   ⚠ That second figure is below AA for small text, and it is the SITE-WIDE brown-button
   standard rather than anything introduced here — the hero CTAs, the careers buttons and this
   one all sit at 3.88. Worth fixing everywhere at once or not at all; see the note on the
   accent card above for the two ways to get there. */
.tp-band--navy .btn-dark{
  background:var(--brown); color:#fff;
  border:1px solid var(--brown);
}
/* Matches .btn-brown's own hover, so the two behave identically. */
.tp-band--navy .btn-dark:hover{
  background:var(--brown-light); border-color:var(--brown-light); color:#fff;
}
/* ⚠ THE SECONDARY BUTTON TOO. .btn-outline is navy text on a transparent fill, so on a navy tile it painted an empty
   box: Kitchen Designers' "Why Choose…" tile, "Call 613-783-1446", navy on navy (measured rgb(22,16,68) on
   rgb(22,16,68), found 2026-10-05). It takes .btn-outline-light's look, the inverted secondary the heroes already use
   for the same phone button. Colours only: the box keeps .btn-outline's size, as on a cream tile. */
.tp-band--navy .btn-outline{
  background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.38); color:#fff;
}
.tp-band--navy .btn-outline:hover{ border-color:#fff; background:rgba(255,255,255,.14); }
.tp-band--navy .btn-outline:focus-visible{ outline:2px solid var(--brown-light); outline-offset:3px; }

/* ── CABINET PAINTING TRUST TILE (.tp-paint) ──────────────────────────────────────────────
   A cross-link, not a pitch. See northco_topic_paint() for why the copy is about method. */
.tp-paint-grid{ display:grid; gap:clamp(24px,3vw,44px); align-items:center; }
@media(min-width:900px){
  /* Copy first and wider: it is the argument, the photograph is the evidence. */
  .tp-paint-grid{ grid-template-columns:1fr 0.8fr; }
}
/* ⚠ overflow:hidden AND A FIXED RATIO ARE BOTH LOAD-BEARING FOR THE DRIFT. The effect is an
   oversized photo translating inside a clipped box — a naturally-sized image has no slack to
   move in, and without the clip the 130% image simply overlaps the copy. Same contract as
   .tp-intro-media.nc-drift; only the ratio differs. */
.tp-paint-media{
  border-radius:16px; overflow:hidden; background:var(--off);
  aspect-ratio:4/5; max-height:440px;
}
.tp-paint-media img{ width:100%; height:100%; object-fit:cover; display:block; }

.tp-paint .eyebrow{ margin-bottom:10px; }
.tp-paint .section-heading{ margin-top:0; }
.tp-paint .section-sub{ max-width:none; }
.tp-paint-body{ margin:14px 0 0; font-size:15.5px; line-height:1.65; color:var(--text-2); }

/* The four stages as a row of labels. An <ol> underneath, because they are sequential and a
   screen reader should say so — this is styling, not structure. */
.tp-paint-steps{
  list-style:none; margin:22px 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:8px 10px;
}
.tp-paint-steps li{
  display:inline-flex; align-items:center; gap:7px;
  padding:7px 13px; border-radius:999px;
  border:1px solid var(--border-warm); background:#fff;
  font-family:var(--heading); font-size:12px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:var(--navy);
}
.tp-band--alt .tp-paint-steps li{ background:var(--off); }
.tp-paint-num{ font-size:11px; color:var(--brown); }

/* The lacquer spec — the tile's one claim, so it gets the emphasis a pull-quote would. */
.tp-paint-note{
  margin:22px 0 0; padding:14px 0 0;
  border-top:1px solid var(--border-warm);
  font-size:14.5px; line-height:1.6; color:var(--text-2);
}
.tp-paint-note strong{ color:var(--navy); }

.tp-paint-cta{ margin-top:22px; }

/* ── SOCIAL BLOCK (.tp-social) ────────────────────────────────────────────────────────────
   Replaces V1's paragraph of <br>-separated links and its 500x281 oEmbed. See
   northco_topic_social() for why that could not simply be styled, and why the video is gone.

   Three across now the video has been dropped: with only three cards and no companion, a
   narrow column would leave the same empty right-hand third the estimator had. */
.tp-social-sub{ margin:0 0 clamp(22px,2.4vw,32px); max-width:none; }
.tp-social-list{
  list-style:none; margin:0; padding:0;
  display:grid; gap:12px;
}
@media(min-width:760px){ .tp-social-list{ grid-template-columns:repeat(3,1fr); } }

.tp-social-list a{
  display:flex; align-items:center; gap:14px; height:100%;
  padding:18px 20px; border-radius:14px;
  border:1px solid var(--border-warm); background:#fff;
  text-decoration:none;
  transition:border-color var(--dur-base,200ms) var(--ease), transform var(--dur-base,200ms) var(--ease);
}
.tp-band--alt .tp-social-list a{ background:var(--cream); }
.tp-social-list a:hover{ border-color:var(--brown); }

/* The glyph sits in the same chip the bento icons use, so the two read as one family. */
.tp-social-icon{
  width:42px; height:42px; flex:none;
  border-radius:11px;
  background:var(--off); border:1px solid var(--border-warm);
  color:var(--brown);
  display:inline-flex; align-items:center; justify-content:center;
}
.tp-band--alt .tp-social-icon{ background:#fff; }
.tp-social-icon svg{ display:block; }
.tp-social-list a:hover .tp-social-icon{ background:var(--brown); border-color:var(--brown); color:#fff; }

.tp-social-text{ display:flex; flex-direction:column; gap:3px; min-width:0; flex:1 1 auto; }
.tp-social-name{ font-family:var(--heading); font-size:15px; font-weight:700; color:var(--navy); }
.tp-social-note{ font-size:13.5px; line-height:1.4; color:var(--muted); text-wrap:pretty; }

.tp-social-arr{ color:var(--brown); font-size:15px; flex:none; transition:transform var(--dur-base,200ms) var(--ease); }
.tp-social-list a:hover .tp-social-arr{ transform:translateX(3px); }

@media(prefers-reduced-motion:reduce){
  .tp-social-list a, .tp-social-arr, .tp-social-icon{ transition:none; }
}

/* The social block continues its heading's section rather than opening a new one, so the seam
   between them has to go: the heading's band keeps its top padding and drops its bottom, and
   this one does the reverse. :has() lets the heading section know what follows it. */
.tp-social-section{ padding-top:0; }
.section:has(+ .tp-social-section){ padding-bottom:0; }

/* ── THE REELS LOOK (.tp-social--reels; northco_topic_social() 'look' => 'reels') ─────────────────────────────────────
   The Contact tile's tilted covers (.tp-social-thumbs) at a band's width, as one card and one row: the three covers on
   the left, fanned the same way (a tilt, a lift, the opposite tilt), and on the right the channels as three big
   stacked buttons with the sub wrapped under them. Jesse, 2026-09-29, on the first build (pills and sub along the top,
   covers under them): "rebuild it like this pls - wrap the text / make the social icons bigger - this'll shorten the
   tile too", then "on desktop stack the icons for socials so they're bigger". The covers are the Contact tile's own
   (northco_social_thumbs()). The card is white on the band, like the Contact tile; the covers straighten and lift on
   hover. */
/* The gap under the heading is the section's padding, not the card's margin: a margin collapsed through the
   section's zero top padding and opened a strip of page colour between the two bands. */
.tp-social-section:has(.tp-social--reels){padding-top:clamp(20px,2.4vw,32px);}
.tp-social--reels{
  display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);align-items:center;gap:clamp(28px,4vw,64px);
  background:#fff;border:1px solid var(--border-warm);border-radius:20px;
  padding:clamp(24px,3vw,40px) clamp(22px,3.4vw,48px);
  box-shadow:0 24px 48px -30px rgba(22,16,68,.3);
}
.tp-social-side{display:flex;flex-direction:column;gap:clamp(18px,2vw,26px);min-width:0;}
.tp-social--reels .tp-social-list{display:flex;flex-direction:column;gap:12px;}
.tp-social--reels .tp-social-list a{gap:16px;padding:14px 22px 14px 14px;border-radius:14px;background:var(--cream);}
.tp-social--reels .tp-social-icon{width:48px;height:48px;border-radius:12px;background:#fff;}
.tp-social--reels .tp-social-icon svg{width:24px;height:24px;}
.tp-social--reels .tp-social-name{font-size:18px;}
.tp-social--reels .tp-social-sub{margin:0;max-width:40ch;}
/* Instagram leads (Jesse: "insta is where i want them to go ideally"): the filled button, navy like the site's primary
   actions. Facebook and YouTube stay as the quieter cream buttons. */
.tp-social--reels .tp-social-list a.is-lead{background:var(--navy);border-color:var(--navy);}
.tp-social--reels .tp-social-list a.is-lead .tp-social-name{color:#fff;}
.tp-social--reels .tp-social-list a.is-lead .tp-social-icon{border-color:#fff;}
.tp-social--reels .tp-social-list a.is-lead:hover{background:var(--navy-700);border-color:var(--navy-700);}
.tp-social--reels .tp-social-list a.is-lead:hover .tp-social-icon{background:#fff;color:var(--brown);border-color:#fff;}
.tp-social-reels{
  list-style:none;margin:0;padding:12px 4px 4px;
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(12px,2.4vw,32px);
}
.tp-social-reels a{
  position:relative;display:block;border-radius:16px;overflow:hidden;background:var(--cream-raised);
  box-shadow:0 18px 36px -14px rgba(22,16,68,.38);
  transition:transform .45s var(--ease),box-shadow .45s var(--ease);
}
.tp-social-reels li:nth-child(1) a{transform:rotate(-3deg);}
.tp-social-reels li:nth-child(2) a{transform:translateY(-10px);}
.tp-social-reels li:nth-child(3) a{transform:rotate(3deg);}
.tp-social-reels a:hover,.tp-social-reels a:focus-visible{transform:translateY(-8px) scale(1.02);box-shadow:0 26px 46px -16px rgba(22,16,68,.42);}
.tp-social-reels a:focus-visible{outline:3px solid var(--brown);outline-offset:4px;}
.tp-social-reels img{display:block;width:100%;height:auto;aspect-ratio:3 / 4;object-fit:cover;}
/* Phones and narrow tablets: one column, in source order: the covers, the three buttons full width, the sub. */
@media(max-width:860px){
  .tp-social--reels{grid-template-columns:minmax(0,1fr);gap:24px;}
  .tp-social--reels .tp-social-list a{padding:10px 16px 10px 10px;}
  .tp-social--reels .tp-social-icon{width:40px;height:40px;}
  .tp-social--reels .tp-social-name{font-size:17px;}
}
@media(prefers-reduced-motion:reduce){ .tp-social-reels a{transition:none;} }


/* ══════════════════════════════════════════════════════════════════════════════════════════
   /faq/ — THE TABBED FAQ PAGE
   ══════════════════════════════════════════════════════════════════════════════════════════
   Pill tabs above a left-aligned accordion, per the reference Jesse supplied — left-aligned
   rather than the reference's centred layout, because every other V2 section is.

   ⚠ THE DEFAULT STATE OF THIS PAGE IS FULLY EXPANDED, AND THAT IS THE POINT.
   Everything below that collapses the page is scoped to `html.nc-faq-js`, a class set by a
   one-line inline script in template-parts/faq-tabs.php. With no JavaScript — or with a script
   that failed to load — all five panels are visible and all 76 answers are open, because the
   site's accordion is .faq-a{max-height:0} plus a class added on click: on a service page a
   dead script costs one section, and on THIS page it would cost every answer on it.

   That inline script runs BEFORE the section is parsed, so the collapsed state is the first
   paint. Putting it in the footer script instead would paint 76 open answers and then collapse
   them — a flash and a very large layout shift, on the viewport carrying 85% of traffic.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ⚠ UNDOES .page-article's 760px CENTRING, AND WITHOUT THIS THE WHOLE PAGE IS A NARROW
   COLUMN. post_class() puts .page-article on the article, which caps it at 760px and centres
   it — deliberate for a generic prose page, wrong here. Measured at 1440: every band inside
   was 760px wide starting at x=388, including the full-bleed cream band and the navy quote
   section, so the page read as a centred document pasted onto a site that is left-aligned
   everywhere else. Same override, same reasoning, as .service-article above: the BANDS go full
   width, and the reading measure is re-imposed where it belongs — on .faq-list and .tp-prose,
   not on the page box. */
.nc-faq-article{ max-width:none; margin-inline:0; }

/* ── THE RAIL RIDES THE PANELS ────────────────────────────────────────────────────────────
   Jesse, pointing at the Kitchen Update estimator: "the way it is left aligned, content is on
   the right of it - then it scrolls down with you until the tile ends? do that for FAQ page".
   Same 5fr/7fr split and the same sticky column as .estimator-section.tp-band, and for the
   same reason: the panels are thousands of pixels tall, and the line that tells someone what
   to do when their question ISN'T here should not scroll away after the third question.

   ⚠ THE TWO COLUMNS MUST SHARE ONE GRID PARENT. The heading block used to be its own
   <section> above the tabs, which cannot stick to anything — page-faq.php now owns both
   columns and template-parts/faq-tabs.php renders only the right one.

   ⚠ 1200px, NOT 1000 — copied from the estimator's own measurement. Below that the 7fr column
   is too narrow for the accordion's 760px measure and the stacked layout is simply better.
   align-self:start is required, or the grid stretches the rail and sticky has no travel.
   top:88px clears the 68px fixed nav. */
/* ⚠ THE RAIL MUST NOT BE TALLER THAN THE VIEWPORT, or sticky stops working: a column taller
   than the screen pins its TOP and the bottom of it can never be reached while scrolling down.
   Measured at 1440x900 the rail is ~470px against 812px of space under the nav, so there is
   headroom — but it is the h1, the intro and the aside together, and any one of them growing
   is what would break it silently. */
.nc-faq-rail .section-heading{ margin:0 0 18px; }
.nc-faq-intro{ max-width:62ch; }
.nc-faq-intro p{ font-size:17px; }

/* The page's size, stated once. Metadata about the page rather than a sentence in it, so it is
   set as a label and not as prose. Jesse's framing for the whole page was "the idea isn't info
   overload but a 'oh wow they have this down to a science' kinda thing" — one honest number
   does that; the same number repeated on five pills did it while costing a row of layout. */
.nc-faq-meta{
  margin:18px 0 0; font-size:13px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; color:var(--brown-dark);
}

.nc-faq-aside{ max-width:52ch; margin-top:14px; }

@media(min-width:1200px){
  .nc-faq-grid{
    display:grid; grid-template-columns:5fr 7fr; gap:clamp(40px,4vw,72px);
    align-items:start;
  }
  .nc-faq-rail{ position:sticky; top:88px; align-self:start; }

  /* ⚠ THE H1 COMES DOWN IN THIS COLUMN. .section-heading clamps to 48px, sized for a block
     spanning the page; in a 536px column "Frequently Asked Questions" ran to three lines and
     dominated everything under it. 34px keeps it comfortably the largest thing in the rail,
     which is all the hierarchy it needs once it is no longer spanning the page. Same call, and
     same reasoning, as .estimator-heading in its sticky column. */
  .nc-faq-rail .section-heading{ font-size:clamp(28px,2.6vw,34px); }
  .nc-faq-intro p{ font-size:16px; }
}

@media(prefers-reduced-motion:reduce){
  /* Sticky is not motion, but it is unexpected movement to some readers — the estimator makes
     the same exemption. */
  .nc-faq-rail{ position:static; }
}

/* ── THE TAB STRIP ────────────────────────────────────────────────────────────────────────
   Wrapping, not horizontally scrolling. A scroll region with five short labels gives no
   affordance that anything is off-screen, and "Exteriors" disappearing off the right edge of a
   375px phone is a tab nobody finds. Two rows is honest. */
.nc-faq-tabs{
  display:flex; flex-wrap:wrap; gap:10px;
  margin:0 0 44px; padding:0;
}
.nc-faq-tab{
  display:inline-flex; align-items:center; padding:11px 18px; border-radius:999px;
  background:#fff; border:1px solid var(--border-warm);
  font-family:var(--sans); font-size:15px; font-weight:700; line-height:1;
  color:var(--navy); text-decoration:none; cursor:pointer;
  transition:background var(--dur-base,200ms) var(--ease),
             border-color var(--dur-base,200ms) var(--ease),
             color var(--dur-base,200ms) var(--ease);
}
.nc-faq-tab:hover{ border-color:var(--brown); color:var(--brown-dark); }

/* ⚠ THERE IS NO PER-TAB COUNT, AND THAT IS A MEASUREMENT. Each pill used to carry its question
   count ("Materials & trades 31"), which cost ~26px a pill — a whole extra row at TWO
   breakpoints: three rows instead of two at 375px, and at the desktop split the five pills
   wanted 801px in a 750px column, leaving "General" orphaned on a line of its own. The number
   was worth stating once, not five times: .nc-faq-meta in the rail now carries the page total
   and the strip is a single clean row from 601px up with ~79px to spare. */

.nc-faq-tab.is-active{ background:var(--navy); border-color:var(--navy); color:#fff; }
.nc-faq-tab.is-active:hover{ color:#fff; border-color:var(--navy); }

/* ⚠ WITHOUT JS THE STRIP IS A JUMP LIST, NOT A TAB BAR. The links point at real panel ids and
   every panel is on the page, so they work — but showing one of them "selected" would be a
   lie, since all five panels are visible at once. */
html:not(.nc-faq-js) .nc-faq-tab.is-active{
  background:#fff; border-color:var(--border-warm); color:var(--navy);
}

/* ── PANELS ───────────────────────────────────────────────────────────────────────────────
   display:none only once the script is present to bring them back. */
html.nc-faq-js .nc-faq-panel{ display:none; }
html.nc-faq-js .nc-faq-panel.is-active{ display:block; }
.nc-faq-panel:focus{ outline:none; }

/* Panels separate themselves when they are all on screen at once (the no-JS case); in tab mode
   only one is ever visible, so the rule costs nothing there. */
html:not(.nc-faq-js) .nc-faq-panel + .nc-faq-panel{
  margin-top:56px; padding-top:48px; border-top:1px solid var(--border-warm);
}

/* ── SETS ─────────────────────────────────────────────────────────────────────────────────
   The h2 inside a panel. Four binder pages land in the Materials tab and 31 questions with no
   landmark is a wall — these are what a reader hunting for grout actually scans for, and they
   give a page with one h1 a real heading tree. */
.nc-faq-set + .nc-faq-set{ margin-top:52px; }
.nc-faq-set-h{
  font-family:var(--heading); font-weight:800; color:var(--navy);
  font-size:clamp(21px,2.3vw,27px); line-height:1.25; letter-spacing:var(--track-heading, -0.01em);
  margin:0 0 6px;
}
.nc-faq-set-sub{
  margin:0 0 4px; color:var(--muted);
  font-size:15px; line-height:1.6; max-width:60ch; text-wrap:pretty;
}

/* ── THE ACCORDION ────────────────────────────────────────────────────────────────────────
   .faq-item / .faq-q / .faq-a are the site's own, inherited from the block above this one, so
   the question rows here and on every service page cannot drift apart. Only the differences
   this page needs live below.

   Capped at 760px. Above 1200px the 7fr column is already about 750px so the cap is inert
   there; it is what holds the measure at every width BELOW the split, where the column is the
   full 1344. Left-aligned rather than centred — Jesse, on the reference layout: "still left
   align as we do on the V2". */
.nc-faq .faq-list{ max-width:760px; }
.nc-faq .faq-a-inner{ max-width:700px; }

/* The question text is wrapped in a <span> here so a long question wraps against the icon
   instead of pushing it off the row; .faq-q is a flex row with space-between. */
.nc-faq .faq-q > span:first-child{ flex:1 1 auto; min-width:0; }

/* ⚠ THE CAP HAS TO CLEAR THE TALLEST ANSWER, AND FOUR OF THESE CARRY <ul> LISTS.
   .tp-band sets 720px, which the service pages' longest answer (264px) clears easily. The
   exterior siding-types and maintenance answers are four-item lists; measured in the rendered
   DOM at 375px — not read off this stylesheet — the tallest is 322px. 720 still clears it, but
   the margin is now 2.2x rather than 2.7x and the next binder answer with a five-item list
   would be the one that clips silently. 900 keeps the headroom.

   ⚠ .faq-a REPORTS max-height:0px IN THE PREVIEW PANE EVEN WITH .open APPLIED — the transition
   never runs because the pane freezes animation. Measure .faq-a-inner's own height against this
   number, never the computed max-height of .faq-a. */
html.nc-faq-js .nc-faq .faq-item.open .faq-a{ max-height:900px; }

/* No JS: every answer open, no transition to animate it there. */
html:not(.nc-faq-js) .nc-faq .faq-a{ max-height:none; transition:none; }
html:not(.nc-faq-js) .nc-faq .faq-q{ cursor:default; }
html:not(.nc-faq-js) .nc-faq .faq-icon{ display:none; }

/* Binder answers bold their lead clause and four of them print bullets. .tp-band already
   styles both; these are the two things it does not cover. */
.nc-faq .faq-a-inner ul{ list-style:disc; }
.nc-faq .faq-a-inner li strong{ color:var(--navy); }

@media(max-width:600px){
  .nc-faq-tabs{ gap:8px; margin-bottom:36px; }
  .nc-faq-tab{ padding:10px 14px; font-size:14px; }
  .nc-faq-set + .nc-faq-set{ margin-top:40px; }
  .nc-faq-meta{ font-size:12px; }
}

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


/* ══════════════════════════════════════════════════════════════════════════════════════════
   DOORS — THE PROFILE RAIL AND THE MATERIAL SWITCHER
   ══════════════════════════════════════════════════════════════════════════════════════════
   Jesse's brief for the carpentry tile, after the first cut read as "too repetitive": a slider
   of door profiles with nothing but the profile name under each, and a static door with a
   material switcher carrying pros and cons — "education is key type thing".
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── THE PROFILE RAIL ─────────────────────────────────────────────────────────────────────
   Rides the site's own carousel: .tp-carousel gives the scroll-snap viewport, the track, the
   prev/next buttons and the focus effect. Only the SLIDE changes shape — .tp-slide is a wide
   two-column media-plus-copy card sized at clamp(280px, 68%, 860px), which is one-at-a-time.
   Profiles are the opposite case: you compare them, so several have to be on screen at once. */
.tp-rail-label{ margin-bottom:0; }
.tp-rail{ margin-top:14px; }
/* ⚠ SNAP TO THE CONTENT EDGE. The viewport's 4px padding (room for the card shadow) sits inside the scrollport, so
   start-snapping parked the first card on the padding edge instead: scrollLeft 4, and every rail's cards 4px left of
   the heading above them (measured 2026-10-02 on KR's and CKC's detail rails and KD's ideas). */
.tp-rail .tp-carousel-viewport{ scroll-padding-inline:4px; }
.tp-rail .tp-carousel-track{
  gap:16px;
  /* No centring inset — a comparison rail starts at the content edge like everything else on
     the page, where the one-at-a-time carousel centres its focused slide. */
  padding-inline:0;
}
.tp-rail-slide{
  /* 4 across at 1440, ~2.2 on a phone: enough of the next one showing to say "there is more". */
  flex:0 0 clamp(190px, 30vw, 268px);
  display:block;
  transform:none; opacity:1;          /* no focus dimming — every profile stays readable */
  scroll-snap-align:start;
  background:#fff;
}
.tp-band--alt .tp-rail-slide{ background:var(--cream); }

/* 3:4 portrait, because doors are. aspect-ratio rather than a fixed height so the box is
   correct before the image loads and nothing shifts underneath it. */
.tp-rail-media{
  display:block; aspect-ratio:3 / 4; overflow:hidden; background:var(--off);
}
.tp-rail-media img{ width:100%; height:100%; object-fit:cover; display:block; }
/* A card's film (topic-details.php 'film', Kitchen Designers' Cherington): filled and cropped like the card's picture. */
.tp-rail-media .tp-rail-film{ width:100%; height:100%; object-fit:cover; display:block; }

/* Named placeholder, same contract as .tp-design-media--empty: a missing file is a visible,
   labelled gap that doubles as the shopping list — never a collapsed card. */
.tp-rail-media--empty{
  display:flex; align-items:center; justify-content:center;
  border-bottom:1px solid var(--border-warm);
  background:repeating-linear-gradient(45deg,var(--off) 0 10px,var(--cream) 10px 20px);
}
.tp-rail-media--empty span{
  font-family:var(--heading); font-size:11px; font-weight:700; letter-spacing:.04em;
  color:var(--muted); text-align:center; padding:0 10px; word-break:break-word;
}

/* ── KR CARPENTRY, PART ONE: THE BINDER'S "THE DETAIL" PAGE (template-parts/topic-details.php) ──
   The door rail's own carousel in a wide variant: two landscape photographs across, the other
   two an arrow or a swipe away, then the door-profile rail below as part two. It shipped first
   as a 2x2 grid under its own heading and lede. Jesse: "super busy… make the detail 4 cards on
   a carousel so you can go to the right to see the other two". Titles in brown, as the binder
   sets them. */
.tp-rail--wide{ margin-bottom:clamp(34px,3.8vw,52px); }
.tp-rail--wide .tp-rail-slide{ flex:0 0 calc((100% - 16px) / 2); }
.tp-rail--wide .tp-rail-media{ aspect-ratio:3 / 2; }
/* Kitchen Designers' drawings (2026-10-02): 4:3 like the drawings themselves, so no plan loses its edges to the crop. */
.tp-rail--4x3 .tp-rail-media{ aspect-ratio:4 / 3; }
/* Three a row (topic-details.php 'size' => 'three'): Design & Build's portal screenshots, 2026-10-05, smaller than the
   drawings' two. Two a row on a tablet, the phone's 86% from the rule below. */
.tp-rail--three .tp-rail-slide{ flex:0 0 calc((100% - 32px) / 3); }
@media(max-width:1100px){ .tp-rail--three .tp-rail-slide{ flex:0 0 calc((100% - 16px) / 2); } }
.tp-detail-title{
  font-family:var(--heading); font-weight:700; color:var(--brown);
  font-size:clamp(17px,1.5vw,20px); line-height:1.25; margin:0 0 6px;
  letter-spacing:var(--track-heading, normal);
}
.tp-detail-text{ font-size:14.5px; line-height:1.6; color:var(--text-2); margin:0; }
@media(max-width:700px){
  .tp-rail--wide .tp-rail-slide{ flex:0 0 86%; }
}

/* One picture, no carousel (topic-details.php, a single item): the picture beside its words. Bathroom Renovation's design
   tile, 2026-10-05: one dimensioned drawing, so it's contained, never cropped, on the card's white. Stacks below 900px. */
.tp-detail-solo{
  display:grid; grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);
  gap:clamp(24px,4vw,56px); align-items:center;
  margin:36px 0 clamp(34px,3.8vw,52px);   /* the carousel's 36px above (.tp-carousel), so the picture clears the sub */
}
.tp-detail-solo .tp-rail-media{ aspect-ratio:4 / 3; border-radius:16px; background:#fff; border:1px solid var(--border-warm); }
.tp-detail-solo .tp-rail-media img{ object-fit:contain; }
.tp-detail-solo-body .tp-detail-title{ font-size:clamp(19px,1.7vw,23px); margin-bottom:10px; }
.tp-detail-solo-body .tp-detail-text{ font-size:15.5px; text-wrap:pretty; }   /* no one-word last line, as the theme's other paragraphs */
.tp-detail-solo-body .tp-detail-text + .tp-detail-text{ margin-top:12px; }
.tp-detail-solo-body .tp-detail-text + .tp-detail-title{ margin-top:30px; }   /* a 'more' block's title (topic-details.php) */
@media(max-width:900px){ .tp-detail-solo{ grid-template-columns:1fr; } }
.tp-detail-solo-body .tp-detail-text strong{ color:var(--navy); font-weight:600; }
/* 'zoom' (topic-details.php; nav.js, ZOOM): a "+" on the picture that opens a second picture in a card. Bathroom's heated
   floors, 2026-10-06 (Jesse: "add a small circle with a plus … that expands out like we did for the door profile viewer",
   then "with a slow pulse to catch attention … consistent with the door profile thing"). The circle is the configurator's
   Edge profile "+" (.tp-cfg-edge-zoom), larger for a thumb, on a white pill so it reads on any photograph. The pulse is
   the homepage CTA's (nc-cta-pulse): slow, stopped on hover and focus, and off for reduced motion. */
.tp-detail-solo .tp-rail-media{ position:relative; }
.tp-zoom{
  position:absolute; left:16px; bottom:16px; z-index:1;
  display:inline-flex; align-items:center; gap:10px; padding:7px 7px 7px 15px; border-radius:999px;
  background:rgba(255,255,255,.95); box-shadow:0 4px 14px rgba(11,8,42,.18);
  font-family:var(--sans); font-size:10.5px; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
  color:var(--navy); text-decoration:none; cursor:zoom-in;
}
.tp-zoom:focus{ outline:none; }
.tp-zoom:focus-visible{ box-shadow:0 0 0 3px var(--brown); }
.tp-zoom-plus{
  flex:none; width:24px; height:24px; border-radius:50%;
  background:var(--navy) no-repeat center / 12px 12px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M6 1v10M1 6h10' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
  animation:nc-cta-pulse 3.4s cubic-bezier(.25,.46,.45,.94) infinite; transition:transform .2s var(--ease);
}
.tp-zoom-label{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.tp-zoom:hover .tp-zoom-plus,
.tp-zoom:focus-visible .tp-zoom-plus{ animation:none; transform:scale(1.12); }
@media (prefers-reduced-motion:reduce){ .tp-zoom-plus{ animation:none; } }
/* With a 'thumb': a card, its label over a small crop of what it opens (Jesse, 2026-10-06: "make it a small preview of
   how it works … so it entices someone to check it out"). Two classes on the crop, because .tp-rail-media img stretches
   every picture in the frame to fill it. The crop is never narrower than its label row (160px), or the card shows white
   beside it, as it did at 132px on a phone. */
.tp-zoom--preview{ flex-direction:column; align-items:stretch; gap:9px; padding:10px; border-radius:14px; }
.tp-zoom--preview .tp-zoom-label{ padding-left:5px; }
.tp-zoom .tp-zoom-thumb{
  display:block; margin:0; width:clamp(160px,15vw,220px); height:auto; aspect-ratio:480 / 215; object-fit:cover;
  border-radius:7px; transition:transform .45s var(--ease);
}
.tp-zoom--preview:hover .tp-zoom-thumb{ transform:scale(1.03); }
@media(max-width:700px){ .tp-zoom{ left:12px; bottom:12px; } }
@media (prefers-reduced-motion:reduce){ .tp-zoom .tp-zoom-thumb{ transition:none; } }
/* The card: the configurator's dialog (.tp-cfg-dialog) in look, white as it is for the edge drawing, with its 16px
   corners, frosted navy behind and white close button. The picture keeps its 4:3 at whatever size fits the screen, so
   the card measures right the moment it opens, before the file has loaded. margin:auto centres it (the theme's reset
   zeroes it), and max-width/height:none lift the browser's own cap on a modal dialog. */
/* Room round the picture, as the configurator gives its edge drawing, and a top margin the close button sits in, so it
   never covers the picture (on a phone it sat on the diagram's corner). */
.tp-zoom-dialog{
  --zp:clamp(12px,2.4vw,28px);
  margin:auto; padding:60px var(--zp) var(--zp); border:0; border-radius:16px; overflow:hidden; background:#fff; color:var(--navy);
  max-width:none; max-height:none; box-shadow:0 30px 80px rgba(11,8,42,.45);
}
.tp-zoom-dialog::backdrop{ background:rgba(22,16,68,.55); -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px); }
.tp-zoom-img{
  display:block; margin:0; max-width:none; height:auto; aspect-ratio:4 / 3;
  width:min(calc(92vw - 2 * var(--zp)), calc((86vh - 60px - var(--zp)) * 4 / 3), 1040px);
  object-fit:contain;
}
.tp-zoom-close{
  position:absolute; top:12px; right:12px; width:40px; height:40px; border-radius:50%; border:0;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  background:rgba(255,255,255,.92); color:var(--navy); font-size:24px; line-height:1;
  box-shadow:0 4px 14px rgba(0,0,0,.25);
}
.tp-zoom-close:focus-visible{ outline:3px solid var(--brown); outline-offset:2px; }

.tp-rail-body{ padding:16px 18px 20px; }
.tp-rail-name{
  font-family:var(--heading); font-size:15px; font-weight:700; color:var(--navy);
  margin:0 0 4px; letter-spacing:var(--track-heading, -0.01em);
}
.tp-rail-note{ margin:0; font-size:13px; line-height:1.5; color:var(--muted); text-wrap:pretty; }

/* ── THE DESIGN IDEAS (Kitchen Designers): a tab per surface ──────────────────────────────
   Binder pp.103 and 110 and the countertop pages. Tabbed since 2026-10-02 (Jesse: "have this be tabbed like the KU or
   KR page"): the upgrades rack's chips, track, arrows and fades (.tp-upg-*), holding the profile rail's cards. A card
   is 3:4 and four across at 1440, as the rails were. The chips and the arrows share one row. */
.tp-ideas-rack{ margin-top:clamp(18px,2vw,24px); }
.tp-ideas-bar{
  display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap;
  gap:12px 24px; margin:0 0 clamp(16px,1.8vw,22px);
}
.tp-ideas-bar .tp-upg-chips{ margin:0; }
/* ⚠ ON A PHONE, A 2x2 GRID, NOT A SWIPED ROW. Four tabs need about 356px and a 390px screen has 342, so the row cut the
   last tab off (the upgrades rack's chips do the same). Shrinking the type to fit would cost legibility for a 40–65
   reader. Two across, each tab is full size with its count, and nothing is hidden. */
@media(max-width:480px){
  .tp-ideas-bar .tp-upg-chips{ display:grid; grid-template-columns:1fr 1fr; gap:8px; flex:1 1 100%; overflow:visible; }
  .tp-ideas-bar .tp-upg-chip{ display:block; }
  .tp-ideas-bar .tp-upg-chip span{ display:flex; justify-content:center; }
}
.tp-idea-card{ flex-basis:62%; max-width:268px; }
@media(min-width:700px){ .tp-idea-card{ flex-basis:31%; } }
@media(min-width:1100px){ .tp-idea-card{ flex-basis:23%; } }
.tp-band--alt .tp-idea-card{ background:var(--cream); }
/* "All" shows its own order, the strongest photographs first (--i, from the data). The tabs keep the DOM's order. */
.tp-ideas-rack:has(input[value="all"]:checked) .tp-idea-card{ order:var(--i, 99); }
/* ── "I WANT THIS" ON A TILE (topic-section.php $nc_want) ────────────────────────────────────
   Jesse, 2026-10-02: "the tiles are missing CTA buttons like other pages - big miss imo - easy context aware CTA's".
   The theme's own .btn.btn-brown, the site's "I want this" button. Only the spacing is new. */
.tp-want{ margin:clamp(24px,2.6vw,34px) 0 0; }

/* ── TYPICAL TIMELINES (.tp-weeks) ─────────────────────────────────────────────────────────
   Design & Build, 2026-10-05, from binder p8: one row per kind of job on a shared week scale, each bar cut into its
   phases, with a legend and a note under it. The segments are placed by custom properties (--s and --e, in weeks,
   against --weeks), so the data says where a phase sits, not the CSS. Name | chart from 761px; stacked below.
   The phase colours are the binder's, held to the palette: navy for demo, a deeper --brown-pale for carpentry, a cool
   grey for installation, --brown for the finish, and a hatch for "large-scale projects typically extend here". */
.tp-weeks{
  --wk-name:clamp(200px,22vw,280px); --wk-gap:clamp(20px,3vw,40px);
  --wk-carpentry:#E6D9C2; --wk-install:#CBC7D8;
  margin:clamp(28px,3vw,40px) 0 0;
}
.tp-weeks-axis,
.tp-weeks-row{ display:grid; grid-template-columns:var(--wk-name) 1fr; column-gap:var(--wk-gap); }
.tp-weeks-axis{ align-items:end; padding-bottom:10px; border-bottom:1px solid var(--border); }
.tp-weeks-axis-label{ font-size:13.5px; font-style:italic; color:var(--muted); }
.tp-weeks-ticks{
  display:grid; grid-template-columns:repeat(var(--weeks), 1fr);
  font-family:var(--heading); font-size:12.5px; font-weight:700; color:var(--light);
}
.tp-weeks-ticks span{ padding-left:5px; border-left:1px solid var(--border); }
.tp-weeks-rows{ list-style:none; margin:0; padding:0; }
/* A grid child: without min-width:0 the legend's longest line could push a narrow chart column wider than its track. */
.tp-weeks-chart{ min-width:0; }
.tp-weeks-row{ padding:24px 0; border-bottom:1px solid var(--border); }
.tp-weeks-name{
  font-family:var(--heading); font-weight:800; color:var(--navy);
  font-size:clamp(18px,1.6vw,21px); line-height:1.25; margin:0 0 4px;
}
.tp-weeks-span{ margin:0; font-style:italic; color:var(--text-2); font-size:15px; }
/* The week grid behind each bar: one hairline at the start of every week, the same columns as the ticks above. */
.tp-weeks-bar{
  position:relative; display:block; height:30px;
  background-image:repeating-linear-gradient(to right, var(--border) 0 1px, transparent 1px calc(100% / var(--weeks)));
}
.tp-weeks-seg{
  position:absolute; top:0; bottom:0; border-radius:6px;
  left:calc(var(--s) / var(--weeks) * 100% + 2px);
  width:calc((var(--e) - var(--s)) / var(--weeks) * 100% - 4px);
}
.tp-weeks-seg--demo{ background:var(--navy); }
.tp-weeks-seg--carpentry{ background:var(--wk-carpentry); }
.tp-weeks-seg--install{ background:var(--wk-install); }
.tp-weeks-seg--finish{ background:var(--brown); }
.tp-weeks-seg--extend{
  background:repeating-linear-gradient(135deg, var(--wk-carpentry) 0 7px, var(--cream) 7px 14px);
  outline:1.5px dashed var(--brown-light); outline-offset:-1.5px;
}
.tp-weeks-key{
  list-style:none; margin:12px 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap:6px 18px; font-size:14.5px; color:var(--text-2);
}
.tp-weeks-key li{ display:inline-flex; align-items:center; gap:8px; }
.tp-weeks-dot{ display:inline-block; width:12px; height:12px; border-radius:3px; flex:none; }
.tp-weeks-note{ margin:10px 0 0; font-size:14.5px; font-style:italic; color:var(--muted); }
/* DRAWN ON SCROLL, option A (Jesse, 2026-10-05: "yeah try A pls"). nav.js adds .is-armed to a row still below the fold, then
   .is-in as it scrolls in: phase k grows from its left edge at k x 0.17s for 0.42s, so each lands as the next starts, and
   the legend and note settle once the bar has drawn (--n phases). The transitions live on .is-in only, so arming is
   instant and nothing visible shrinks. Unarmed rows (no script, reduced motion, already on screen) never change. */
.tp-weeks-row.is-armed:not(.is-in) .tp-weeks-seg{ transform:scaleX(0); }
.tp-weeks-row.is-armed:not(.is-in) .tp-weeks-key,
.tp-weeks-row.is-armed:not(.is-in) .tp-weeks-note{ opacity:0; transform:translateY(6px); }
.tp-weeks-row .tp-weeks-seg{ transform-origin:left center; }
.tp-weeks-row.is-in .tp-weeks-seg{ transition:transform .42s cubic-bezier(.2,.8,.2,1) calc(var(--k, 0) * .17s); }
.tp-weeks-row.is-in .tp-weeks-key,
.tp-weeks-row.is-in .tp-weeks-note{
  transition:opacity .45s cubic-bezier(.2,.8,.2,1) calc(var(--n, 1) * .17s + .1s), transform .45s cubic-bezier(.2,.8,.2,1) calc(var(--n, 1) * .17s + .1s);
}
@media (prefers-reduced-motion:reduce){
  .tp-weeks-row.is-armed .tp-weeks-seg,
  .tp-weeks-row.is-armed .tp-weeks-key,
  .tp-weeks-row.is-armed .tp-weeks-note{ transform:none; opacity:1; transition:none; }
}
@media(max-width:760px){
  .tp-weeks-axis,
  .tp-weeks-row{ grid-template-columns:1fr; }
  .tp-weeks-axis-label{ margin-bottom:8px; }
  .tp-weeks-head{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:12px; }
  /* Every second week's number on a phone, where twelve won't fit; the hairlines all stay. */
  .tp-weeks-ticks span:nth-child(odd){ color:transparent; }
}

/* A style photographed in one of our kitchens names it, set like the selections' "Cherington, finished" caption. */
.tp-rail-link{
  display:inline-block; margin-top:10px; font-family:var(--sans); font-size:11px; font-weight:700;
  letter-spacing:.14em; text-transform:uppercase; color:var(--brown-dark); text-decoration:none;
}
.tp-rail-link:hover{ color:var(--brown); }

/* ── THE MATERIAL SWITCHER ────────────────────────────────────────────────────────────────
   ⚠ THE DEFAULT STATE IS EVERY PANEL SHOWN, AND THAT IS NOT A FALLBACK — it is binder p73,
   the three-card page this component came from. assets/js/nav.js adds .is-live and only then
   do the tabs appear and the panels collapse to one. Shipping complete and removing is what
   makes a blocked script cost a reader nothing on a section whose whole job is education. */
.tp-switcher{ margin:28px 0 0; }
.tp-switcher-panels{ display:grid; gap:20px; grid-template-columns:repeat(3,1fr); }
@media(max-width:900px){ .tp-switcher-panels{ grid-template-columns:1fr; } }

.tp-switcher-panel{
  background:#fff; border:1px solid var(--border-warm); border-radius:16px; overflow:hidden;
}
.tp-band--alt .tp-switcher-panel{ background:var(--cream); }

/* Live: one panel, and it becomes a wide media band with the copy beside it. */
.tp-switcher.is-live .tp-switcher-panels{ display:block; }
.tp-switcher.is-live .tp-switcher-panel{ display:none; }
.tp-switcher.is-live .tp-switcher-panel.is-active{ display:block; }
.tp-switcher-panel:focus{ outline:none; }

.tp-switcher-media{ position:relative; display:block; aspect-ratio:2.25 / 1; overflow:hidden; background:var(--off); }
.tp-switcher-media img{ width:100%; height:100%; object-fit:cover; display:block; }
.tp-switcher-media--empty{
  display:flex; align-items:center; justify-content:center;
  background:repeating-linear-gradient(45deg,var(--off) 0 10px,var(--cream) 10px 20px);
  border-bottom:1px solid var(--border-warm);
}
.tp-switcher-media--empty span{
  font-family:var(--heading); font-size:11px; font-weight:700; letter-spacing:.04em; color:var(--muted);
}

/* The badge, in HTML rather than burned into the photograph — selectable, translatable, read
   by a screen reader, and in the same place whatever the crop. */
.tp-switcher-tag{
  position:absolute; top:14px; left:14px;
  padding:6px 13px; border-radius:999px; background:var(--brown); color:#fff;
  font-family:var(--sans); font-size:10.5px; font-weight:700;
  letter-spacing:.09em; text-transform:uppercase;
}

.tp-switcher-body{ padding:22px 22px 24px; }
.tp-switcher-name{
  font-family:var(--heading); font-size:18px; font-weight:800; color:var(--navy);
  margin:0 0 3px; letter-spacing:var(--track-heading, -0.01em);
}
.tp-switcher-sub{
  margin:0 0 16px; font-family:var(--sans); font-size:11px; font-weight:700;
  letter-spacing:.09em; text-transform:uppercase; color:var(--brown-dark);
}
.tp-switcher-cols{ display:grid; gap:20px; }
.tp-switcher-col-label{
  margin:0 0 8px; font-family:var(--sans); font-size:10px; font-weight:700;
  letter-spacing:.12em; text-transform:uppercase; color:var(--muted);
}
.tp-switcher-col ul{ list-style:none; margin:0; padding:0; }
.tp-switcher-col li{
  position:relative; padding-left:18px; margin:0 0 7px;
  font-size:14px; line-height:1.55; color:var(--text-2); text-wrap:pretty;
}
.tp-switcher-col li:last-child{ margin-bottom:0; }
/* A tick and a dot rather than green and red: this is a trade-off, not a verdict, and colour
   alone would carry the meaning for anyone who cannot separate the two hues. */
.tp-switcher-col--strengths li::before{
  content:"✓"; position:absolute; left:0; top:-1px; color:var(--brown); font-weight:700;
}
.tp-switcher-col--consider li::before{
  content:"•"; position:absolute; left:4px; top:-1px; color:var(--light); font-weight:700;
}

/* Live and wide: the copy sits beside the photograph instead of under it. */
@media(min-width:900px){
  .tp-switcher.is-live .tp-switcher-panel.is-active{
    display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); align-items:stretch;
  }
  .tp-switcher.is-live .tp-switcher-media{ aspect-ratio:auto; height:100%; min-height:300px; }
  .tp-switcher.is-live .tp-switcher-body{ padding:28px 30px 30px; align-self:center; }
  .tp-switcher.is-live .tp-switcher-cols{ grid-template-columns:1fr 1fr; gap:26px; }
}

/* ── THE TAB STRIP ────────────────────────────────────────────────────────────────────────
   Same pill vocabulary as the FAQ page's topic tabs, so the two read as one system. */
.tp-switcher-tabs{ display:flex; flex-wrap:wrap; gap:10px; margin:0 0 20px; }
.tp-switcher-tab{
  display:inline-flex; align-items:center; padding:10px 17px; border-radius:999px;
  background:#fff; border:1px solid var(--border-warm);
  font-family:var(--sans); font-size:14.5px; font-weight:700; line-height:1;
  color:var(--navy); text-decoration:none; cursor:pointer;
  transition:background var(--dur-base,200ms) var(--ease),
             border-color var(--dur-base,200ms) var(--ease),
             color var(--dur-base,200ms) var(--ease);
}
.tp-band--alt .tp-switcher-tab{ background:var(--cream); }
.tp-switcher-tab:hover{ border-color:var(--brown); color:var(--brown-dark); }
.tp-switcher-tab.is-active{ background:var(--navy); border-color:var(--navy); color:#fff; }
.tp-switcher-tab.is-active:hover{ color:#fff; border-color:var(--navy); }

@media(max-width:600px){
  .tp-switcher-tab{ padding:9px 14px; font-size:13.5px; }
  .tp-switcher-body{ padding:18px 18px 20px; }
}
@media(prefers-reduced-motion:reduce){
  .tp-switcher-tab{ transition:none; }
}

/* ── THE DOOR CONFIGURATOR ─────────────────────────────────────────────────────────────────
   KR's doors tile since 2026-09-23: a showroom stage on the left, two pickers on the right, and
   the door on the stage drawn in the picked material. The markup, and how the drawing works:
   template-parts/topic-configurator.php. The pickers and the collapsing: assets/js/nav.js.
   Jesse: "make it like a porsche dealer where it feels premium to see the materials and
   profiles". Hence one lit stage, numbered steps, and restraint rather than more boxes. */
.tp-cfg{
  margin-top:30px; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);   /* the panel a little wider than the stage: less wrapping, less height */
  background:#fff; border:1px solid var(--border-warm); border-radius:18px; overflow:hidden;
}
.tp-band--alt .tp-cfg{ background:var(--cream); }

/* The stage: a lit backdrop with a floor, so the door stands in a room rather than on a page. */
.tp-cfg-stage{
  position:relative; min-height:520px; display:flex; align-items:center; justify-content:center;
  padding:64px 24px 72px; overflow:hidden;
  background:radial-gradient(115% 90% at 50% 30%, #FFFFFF 0%, #F7F3ED 52%, #ECE5D9 100%);
}
.tp-cfg-stage::before{
  content:''; position:absolute; left:0; right:0; bottom:0; height:24%;
  background:linear-gradient(180deg, rgba(236,229,217,0) 0%, rgba(226,217,202,.6) 100%);
}

/* The door. isolation keeps the blend inside the door; without it the texture would also blend
   with the stage behind.
   ⚠ IT WAS A 3D SLAB FOR AN AFTERNOON AND IS FLAT ON PURPOSE. A drag-to-tilt (face, four edges, a
   sheen) was built and pulled on 2026-09-23. Jesse: "the illusion of depth is gone once we drag…
   a side effect of a not truly 3D image". The relief is baked, lit from one side, so turning the
   door shows a printed card. Depth in motion needs real layers or moving light: see
   ops/INTERACTIVE-TILES.md. */
.tp-cfg-door{
  position:relative; isolation:isolate; width:min(50%,330px); aspect-ratio:950 / 1180;
  border-radius:2px; overflow:hidden; background:#fff;
  box-shadow:0 40px 46px -28px rgba(11,8,42,.38), 0 2px 5px rgba(11,8,42,.08);
}
/* The material underneath, and the profile's RELIEF MAP over it in hard-light: mid-grey leaves
   the texture as it is, darker shades it, lighter lights it. Multiply over the photograph's own
   greys, the first version, could only darken, and on walnut every profile looked the same.
   The relief maps come from dev/door-shades.py. */
.tp-cfg-door-tex{
  position:absolute; inset:0; z-index:1;
  background-size:cover; background-position:center;
  opacity:0; transition:opacity .45s var(--ease);
}
.tp-cfg-door-shade{
  position:absolute; inset:0; z-index:2; width:100%; height:100%; object-fit:cover;
  mix-blend-mode:hard-light;
  opacity:0; transition:opacity .45s var(--ease);
}
/* A texture too small to cover the door is tiled along the grain rather than stretched. */
.tp-cfg-tex--tile{ background-size:auto 100%; background-repeat:repeat-x; }
/* A grained material in five pieces (topic-configurator.php): the panel under everything, a stile up
   each side, a rail across between them, sized by the profile's --stile and --rail. They morph when
   the profile changes, so shaker to slab reads as the frame closing into one piece of wood. */
.tp-cfg-part{
  position:absolute; background-size:cover; background-repeat:no-repeat;
  transition:width .45s var(--ease), height .45s var(--ease), left .45s var(--ease), right .45s var(--ease),
             clip-path .45s var(--ease);
}
.tp-cfg-part--panel{ inset:0; background-image:var(--tex-v); background-position:62% 50%; }
.tp-cfg-part--stile{
  top:0; bottom:0; width:calc(var(--stile) * 100%); background-image:var(--tex-v);
  clip-path:polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
/* The stiles take slices from inside the texture, not its extreme edges, where a sample board
   is most likely to change colour (the interim walnut turns to pale sapwood at its right edge). */
.tp-cfg-part--stile-l{ left:0; background-position:12% 50%; }
.tp-cfg-part--stile-r{ right:0; background-position:72% 50%; }
/* ⚠ THE RAILS SPAN THE DOOR AND ARE CUT, NOT RESIZED, so square and mitred corners are the same
   four-point polygon moved, and switching between them animates the cut. Square (cope and stick):
   the rail fits between the stiles. Mitred: rail and stile meet on the 45° from the outer corner to
   the inner one, like a picture frame. */
.tp-cfg-part--rail{
  left:0; right:0; height:calc(var(--rail) * 100%);
  background-image:var(--tex-h); background-position:50% 30%;
  clip-path:polygon(calc(var(--stile) * 100%) 0, calc(100% - var(--stile) * 100%) 0, calc(100% - var(--stile) * 100%) 100%, calc(var(--stile) * 100%) 100%);
}
.tp-cfg-door.is-mitred .tp-cfg-part--rail-t{ clip-path:polygon(0 0, 100% 0, calc(100% - var(--stile) * 100%) 100%, calc(var(--stile) * 100%) 100%); }
.tp-cfg-door.is-mitred .tp-cfg-part--rail-b{ clip-path:polygon(calc(var(--stile) * 100%) 0, calc(100% - var(--stile) * 100%) 0, 100% 100%, 0 100%); }
.tp-cfg-door.is-mitred .tp-cfg-part--stile-l{ clip-path:polygon(0 0, 100% calc(var(--rail) * 100%), 100% calc(100% - var(--rail) * 100%), 0 100%); }
.tp-cfg-door.is-mitred .tp-cfg-part--stile-r{ clip-path:polygon(0 calc(var(--rail) * 100%), 100% 0, 100% 100%, 0 calc(100% - var(--rail) * 100%)); }
.tp-cfg-part--rail-t{ top:0; }
.tp-cfg-part--rail-b{ bottom:0; background-position:50% 70%; }
/* The seams, on wood only. A box at each corner, stile wide and rail high: square draws the joint
   down its inner edge, mitred draws its diagonal, and they cross-fade with the cut. At .35 alpha the
   joint read as a drawn line rather than a seam; the change of grain already says most of it.
   The diagonal uses linear-gradient's "magic corners": the midline of `to top right` runs top-left
   to bottom-right whatever the box's proportions, and `to bottom right` runs top-right to bottom-left. */
.tp-cfg-seam{ position:absolute; width:calc(var(--stile) * 100%); height:calc(var(--rail) * 100%); pointer-events:none; }
.tp-cfg-seam--tl{ left:0; top:0; } .tp-cfg-seam--tr{ right:0; top:0; }
.tp-cfg-seam--bl{ left:0; bottom:0; } .tp-cfg-seam--br{ right:0; bottom:0; }
.tp-cfg-seam::before,
.tp-cfg-seam::after{ content:''; position:absolute; transition:opacity .45s var(--ease); }
.tp-cfg-seam::before{ top:0; bottom:0; width:1px; background:rgba(38,22,8,.2); }
.tp-cfg-seam--tl::before, .tp-cfg-seam--bl::before{ right:0; }
.tp-cfg-seam--tr::before, .tp-cfg-seam--br::before{ left:0; }
.tp-cfg-seam::after{ inset:0; opacity:0; }
.tp-cfg-seam--tl::after, .tp-cfg-seam--br::after{ background:linear-gradient(to top right, transparent calc(50% - .8px), rgba(38,22,8,.26) 50%, transparent calc(50% + .8px)); }
.tp-cfg-seam--tr::after, .tp-cfg-seam--bl::after{ background:linear-gradient(to bottom right, transparent calc(50% - .8px), rgba(38,22,8,.26) 50%, transparent calc(50% + .8px)); }
.tp-cfg-door.is-mitred .tp-cfg-seam::before{ opacity:0; }
.tp-cfg-door.is-mitred .tp-cfg-seam::after{ opacity:1; }
/* A tiled texture tiles ACROSS its grain: along the width up the stiles and panel, down the height
   along the rails. */
.tp-cfg-tex--tile .tp-cfg-part{ background-size:auto 100%; background-repeat:repeat-x; }
.tp-cfg-tex--tile .tp-cfg-part--rail{ background-size:100% auto; background-repeat:repeat-y; }
/* PAINTED WOOD: each piece gets its own opaque coat, the paint MULTIPLIED BY THE WOOD'S PORE MAP
   (--tex-vp, --tex-hp: near-white, the open grain slightly darker, made by dev/door-shades.py), so
   the grain shows as texture in the paint and runs each piece's way, rails across. Paint hides a
   wood's colour completely; its texture is what telegraphs. The coat sits in the piece's ::after, so
   it takes the piece's size, cut and background-position with it. The seams stay on top, because a
   painted frame door still shows its joints. It fades, so painting reads as a coat going on.
   ⚠ IT WAS THE OAK'S COLOUR UNDER A 91% COAT, the grain greyed and its contrast raised. The figure
   came through as thin dark lines, pencil on white paint. Jesse, 2026-09-23: "wood painted - the
   lines are weird". (At .84 before that it read as limed oak, a stain, not paint.) */
.tp-cfg-door-tex--paintable .tp-cfg-part::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background-color:var(--paint, var(--coat)); background-image:var(--tex-vp);
  background-size:inherit; background-position:inherit; background-repeat:inherit;
  background-blend-mode:multiply;
  opacity:0; transition:opacity .45s var(--ease), background-color .45s var(--ease);
}
.tp-cfg-door-tex--paintable .tp-cfg-part--rail::after{ background-image:var(--tex-hp); }
.tp-cfg-door.is-painted .tp-cfg-door-tex--paintable .tp-cfg-part::after{ opacity:1; }
.tp-cfg-paintcoat{ position:absolute; inset:0; opacity:0; pointer-events:none; transition:opacity .45s var(--ease), background-color .45s var(--ease); }
.tp-cfg-door-shade.is-active,
.tp-cfg-door-tex.is-active{ opacity:1; }
.tp-cfg-door-shade--empty{ display:flex; align-items:center; justify-content:center; background:var(--off); }
.tp-cfg-door-shade--empty span{ font-family:var(--heading); font-size:11px; font-weight:700; color:var(--muted); }

.tp-cfg-summary{
  position:absolute; left:28px; top:26px; z-index:1; margin:0;
  font-family:var(--sans); font-size:10.5px; font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--brown-dark);
}
.tp-cfg-summary i{ font-style:normal; color:var(--light); margin:0 4px; }

/* The edge profile: Caron's cross-section of the door's edge, as a mask filled with navy. It is
   the clearest single difference between the four profiles. */
.tp-cfg-edge{ position:absolute; left:26px; bottom:26px; z-index:1; display:flex; flex-direction:column; gap:8px; }
.tp-cfg-edge-label{
  font-family:var(--sans); font-size:9.5px; font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted);
}
.tp-cfg-edge-art{ position:relative; display:block; width:150px; aspect-ratio:935 / 258; }
.tp-cfg-edge-img{
  position:absolute; inset:0; background:var(--navy);
  -webkit-mask-size:contain; mask-size:contain; -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:left bottom; mask-position:left bottom;
  opacity:0; transition:opacity .45s var(--ease);
}
.tp-cfg-edge-img.is-active{ opacity:1; }
/* Live, the edge callout is a button (nav.js swaps the element) that opens the drawing large. */
button.tp-cfg-edge{
  -webkit-appearance:none; appearance:none; border:0; background:none; padding:0; margin:0;
  font:inherit; color:inherit; text-align:left; cursor:zoom-in; border-radius:6px;
}
button.tp-cfg-edge:focus{ outline:none; }
button.tp-cfg-edge:focus-visible{ box-shadow:0 0 0 3px var(--brown); }
.tp-cfg-edge-zoom{
  display:none; width:18px; height:18px; margin-left:8px; vertical-align:-5px; border-radius:50%;
  background:var(--navy) no-repeat center / 10px 10px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M6 1v10M1 6h10' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
  transition:transform .2s var(--ease);
}
button.tp-cfg-edge .tp-cfg-edge-zoom{ display:inline-block; }
button.tp-cfg-edge:hover .tp-cfg-edge-zoom{ transform:scale(1.12); }

/* ── THE TILE FITS ON A SCREEN (Jesse, 2026-09-23: "can we condense it a bit vertically so we can
   see it without having to scroll?"). Corners sits beside the profile thumbnails, and Finish and
   Colour beside the material swatches, in one wrapping row each (.tp-cfg-optrow), instead of three
   rows of their own. Where one changes nothing it is simply absent, and it fades in when it applies.
   The row holding its space when hidden was the first version, and it left holes; a folding row was
   the second. Both cost height the tile does not have. */
.tp-cfg-optrow{ display:flex; flex-wrap:wrap; align-items:center; column-gap:26px; row-gap:10px; margin:0 0 12px; }
.tp-cfg-optrow .tp-cfg-opts{ margin:0; }
.tp-cfg-corners,
.tp-cfg-finish,
.tp-cfg-paints{ display:flex; align-items:center; gap:10px; animation:ncCfgIn .3s var(--ease) both; }
.tp-cfg-corners[hidden],
.tp-cfg-finish[hidden],
.tp-cfg-paints[hidden],
.tp-cfg-corners.is-na,
.tp-cfg-finish.is-na,
.tp-cfg-paints.is-na{ display:none; }
@keyframes ncCfgIn{ from{ opacity:0; transform:translateY(3px); } to{ opacity:1; transform:none; } }
.tp-cfg-corners-label{
  font-family:var(--sans); font-size:10px; font-weight:700; letter-spacing:.12em;
  text-transform:uppercase; color:var(--muted);
}
.tp-cfg-seg{ display:inline-flex; gap:2px; padding:3px; border-radius:999px; background:var(--off); border:1px solid var(--border-warm); }
.tp-cfg-seg-opt{
  -webkit-appearance:none; appearance:none; border:0; background:none; margin:0;
  display:inline-flex; align-items:center; gap:6px; padding:6px 13px 6px 11px; border-radius:999px;
  font-family:var(--sans); font-size:12.5px; font-weight:700; color:var(--muted); cursor:pointer;
  transition:background .2s var(--ease), color .2s var(--ease), box-shadow .2s var(--ease);
}
.tp-cfg-seg-opt svg{ width:14px; height:14px; flex:0 0 auto; }
.tp-cfg-seg-opt:hover{ color:var(--navy); }
.tp-cfg-seg-opt.is-active{ background:#fff; color:var(--navy); box-shadow:0 1px 3px rgba(11,8,42,.14); }
.tp-cfg-seg-opt:focus{ outline:none; }
.tp-cfg-seg-opt:focus-visible{ box-shadow:0 0 0 2px var(--brown); }

/* Colour: a row of paint dots, for MDF and for painted hardwood. The picked one is ringed and named. */
.tp-cfg-paint-opts{ display:flex; gap:9px; }
.tp-cfg-paint{
  -webkit-appearance:none; appearance:none; border:0; padding:0; margin:0; cursor:pointer;
  width:26px; height:26px; border-radius:50%;
  box-shadow:inset 0 0 0 1px rgba(11,8,42,.14); transition:box-shadow .2s var(--ease), transform .2s var(--ease);
}
.tp-cfg-paint:hover{ transform:scale(1.08); }
.tp-cfg-paint.is-active{ box-shadow:inset 0 0 0 1px rgba(11,8,42,.14), 0 0 0 2px #fff, 0 0 0 3.5px var(--navy); }
.tp-cfg-paint:focus{ outline:none; }
.tp-cfg-paint:focus-visible{ box-shadow:inset 0 0 0 1px rgba(11,8,42,.14), 0 0 0 2px #fff, 0 0 0 4px var(--brown); }
.tp-cfg-paint-name{ font-family:var(--sans); font-size:12.5px; font-weight:700; color:var(--navy); }
/* A tone dot is a texture, not a paint: it shows the wood. */
.tp-cfg-tone{ background-size:cover; background-position:center; }
/* One colour, --paint on the root, so the MDF door, the coat on painted oak and the MDF swatch all
   change together, and fade rather than blink. */
.tp-cfg-door-tex{ transition:opacity .45s var(--ease), background-color .45s var(--ease); }
.tp-cfg-swatch{ transition:box-shadow .2s var(--ease), background-color .45s var(--ease); }

/* "I want these doors in my kitchen": the way from the tile to the quote, at the panel's foot. Brown
   (.btn-brown, in the markup), the site's "I want this" button; orange stays the page's Free Estimate. */
.tp-cfg-cta{
  display:flex; flex-wrap:wrap; align-items:center; gap:8px 18px;
  margin-top:16px; padding-top:16px; border-top:1px solid var(--border-warm);
}
.tp-cfg-cta[hidden]{ display:none; }
/* A shade smaller than the site's .btn, so the note fits beside it rather than under it. */
.tp-cfg-cta-btn{ white-space:nowrap; padding:11px 18px; font-size:11.5px; }
.tp-cfg-cta-note{ margin:0; flex:1 1 180px; font-size:12px; line-height:1.4; color:var(--muted); }
/* Where the picks land: the notes field glows gold for a moment, so the visitor sees what changed. */
.fi-textarea.is-prefilled{ box-shadow:0 0 0 3px rgba(167,120,69,.38); }
.fi-textarea{ transition:box-shadow .5s var(--ease); }
@media(max-width:600px){ .tp-cfg-cta-btn{ white-space:normal; } }

/* "Up close": a photograph of the real material, beside the rendering. A link to a larger
   photograph, which the script opens in the dialog below. */
.tp-cfg-loupe{
  position:absolute; right:26px; bottom:24px; z-index:1; display:flex; align-items:center; gap:12px;
  color:inherit; text-decoration:none; cursor:zoom-in; border-radius:999px;
}
.tp-cfg-loupe[hidden]{ display:none; }
.tp-cfg-loupe:focus{ outline:none; }
.tp-cfg-loupe:focus-visible .tp-cfg-loupe-img{ box-shadow:0 0 0 3px var(--brown), 0 10px 24px rgba(11,8,42,.2); }
.tp-cfg-loupe-zoom{
  position:absolute; right:2px; top:2px; z-index:1; width:24px; height:24px; border-radius:50%;
  background:var(--navy) no-repeat center / 12px 12px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M6 1v10M1 6h10' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
  box-shadow:0 2px 6px rgba(11,8,42,.25); transition:transform .2s var(--ease);
}
.tp-cfg-loupe:hover .tp-cfg-loupe-zoom{ transform:scale(1.12); }
.tp-cfg-loupe-label{
  font-family:var(--sans); font-size:9.5px; font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted); text-align:right;
}
.tp-cfg-loupe-label b{
  display:block; margin-top:3px; font-family:var(--heading); font-size:13px; font-weight:700;
  letter-spacing:0; text-transform:none; color:var(--navy);
}
/* ⚠ THE LABEL WRAPS, AND GIVES WAY ON A NARROW STAGE. It grows with the pick ("Polyester wood look in
   Smoked walnut" is three times "Hardwood") and, anchored right with no width, it grew leftward over
   the edge drawing: 8px apart at 1280px, overlapping from 1100px down (measured). Found by the
   home-show iPad session, 2026-09-23. Capped, it wraps to two lines; below 1120px the circle stands
   alone, as it does on a phone, and the stage's summary line still names the material. */
.tp-cfg-loupe-label{ max-width:150px; }
@media(max-width:1120px){ .tp-cfg-loupe-label{ display:none; } }
.tp-cfg-loupe-img{
  position:relative; width:88px; height:88px; border-radius:50%; overflow:hidden;
  border:3px solid #fff; box-shadow:0 10px 24px rgba(11,8,42,.2); background:var(--off);
}
/* The circle magnifies the material live (topic-configurator.php): the texture at about twice the
   door's scale, or the paint; for painted wood, the same coat as the door, the paint multiplied by
   the pore map, at the same scale as the wood it covers. */
.tp-cfg-loupe-tex{ position:absolute; inset:0; opacity:0; transition:opacity .45s var(--ease), background-color .45s var(--ease); }
.tp-cfg-loupe-tex.is-active{ opacity:1; }
.tp-cfg-loupe-wood{
  position:absolute; inset:0; background-image:var(--tex-v); background-size:700% auto; background-position:38% 42%;
}
.tp-cfg-loupe-tex--paintable .tp-cfg-paintcoat{
  background-image:var(--tex-vp); background-size:700% auto; background-position:38% 42%; background-blend-mode:multiply;
}
.tp-cfg.is-painted .tp-cfg-loupe-tex--paintable .tp-cfg-paintcoat{ opacity:1; }
/* A soft dome of light, so the circle reads as a sample in the hand rather than a flat disc. */
.tp-cfg-loupe-img::after{
  content:''; position:absolute; inset:0; border-radius:50%; pointer-events:none;
  background:radial-gradient(circle at 32% 26%, rgba(255,255,255,.34), rgba(255,255,255,0) 52%),
             radial-gradient(circle at 70% 82%, rgba(11,8,42,.14), rgba(11,8,42,0) 58%);
}

.tp-cfg-panel{ padding:28px 34px 26px; display:flex; flex-direction:column; justify-content:center; }
.tp-cfg-group + .tp-cfg-group{ margin-top:18px; padding-top:18px; border-top:1px solid var(--border-warm); }
.tp-cfg-step{
  margin:0 0 9px; display:flex; gap:10px; align-items:baseline;
  font-family:var(--sans); font-size:10.5px; font-weight:700; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted);
}
.tp-cfg-step span{ font-family:var(--heading); color:var(--brown); letter-spacing:.04em; }

/* The pickers. Hidden in the markup and shown by the script: a picker that picks nothing is
   worse than none. */
.tp-cfg-opts{ display:flex; flex-wrap:wrap; gap:12px; margin:0 0 12px; }
.tp-cfg-opts[hidden]{ display:none; }
.tp-cfg-opt{
  -webkit-appearance:none; appearance:none; border:0; background:none; padding:0; margin:0;
  display:flex; flex-direction:column; align-items:center; gap:8px;
  font:inherit; color:inherit; cursor:pointer;
}
.tp-cfg-opt:focus{ outline:none; }
/* Thumbnails are the same relief map over one neutral stone. They pick a SHAPE, so they carry
   no material, and the relief is what tells a shaker from a beaded inset at 56px. */
.tp-cfg-thumb{
  position:relative; display:block; width:50px; aspect-ratio:950 / 1180; overflow:hidden; isolation:isolate;
  border-radius:2px; background:#E4DDD1; box-shadow:0 0 0 1px var(--border-warm);
  transition:box-shadow .2s var(--ease);
}
.tp-cfg-thumb img{ display:block; width:100%; height:100%; object-fit:cover; mix-blend-mode:hard-light; }
.tp-cfg-swatch{
  display:block; width:44px; height:44px; border-radius:50%;
  background-size:cover; background-position:center; box-shadow:0 0 0 1px var(--border-warm);
  transition:box-shadow .2s var(--ease);
}
.tp-cfg-opt:hover .tp-cfg-thumb,
.tp-cfg-opt:hover .tp-cfg-swatch{ box-shadow:0 0 0 1px var(--brown); }
.tp-cfg-opt.is-active .tp-cfg-thumb,
.tp-cfg-opt.is-active .tp-cfg-swatch{ box-shadow:0 0 0 2px #fff, 0 0 0 3.5px var(--navy); }
.tp-cfg-opt:focus-visible .tp-cfg-thumb,
.tp-cfg-opt:focus-visible .tp-cfg-swatch{ box-shadow:0 0 0 2px #fff, 0 0 0 4px var(--brown); }
.tp-cfg-opt-name{ font-family:var(--sans); font-size:11px; font-weight:700; color:var(--muted); white-space:nowrap; }
.tp-cfg-opt.is-active .tp-cfg-opt-name{ color:var(--navy); }

/* Without the script, every entry is listed in full. With it, one entry shows, stacked in a single
   grid cell with the rest hidden, so the panel keeps the height of its tallest entry and does not
   jump when you pick. */
.tp-cfg-detail + .tp-cfg-detail{ margin-top:18px; }
.tp-cfg.is-live .tp-cfg-details{ display:grid; }
.tp-cfg.is-live .tp-cfg-detail{
  grid-area:1 / 1; margin:0; visibility:hidden; opacity:0;
  transition:opacity .25s var(--ease), visibility 0s linear .25s;
}
.tp-cfg.is-live .tp-cfg-detail.is-active{ visibility:visible; opacity:1; transition:opacity .25s var(--ease) .05s, visibility 0s; }
.tp-cfg-name{
  margin:0 0 5px;
  font-family:var(--heading); font-size:19px; font-weight:700; color:var(--navy); letter-spacing:var(--track-heading, -.01em);
}
.tp-cfg-name-row{ display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin:0 0 5px; }
.tp-cfg-name-row .tp-cfg-name{ margin:0; }
.tp-cfg-tag{
  padding:5px 11px; border-radius:999px; background:var(--brown); color:#fff;
  font-family:var(--sans); font-size:9.5px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
}
.tp-cfg-note{ margin:0; font-size:14.5px; line-height:1.55; color:var(--text-2); text-wrap:pretty; }
.tp-cfg .tp-switcher-sub{ margin:0 0 10px; }
.tp-cfg .tp-switcher-col-label{ margin-bottom:5px; }
.tp-cfg .tp-switcher-col li{ margin-bottom:3px; line-height:1.45; }
.tp-cfg-cols{ grid-template-columns:1fr 1fr; gap:24px; }

/* ⚠ 44px TO THE FINGER, 26px TO THE EYE. The paint and tone dots are drawn small, but on a phone
   (most of this site's visitors) a 26px target is a miss waiting to happen; the home-show iPad
   session had to grow them to 46px. An invisible ring widens what a tap can land on, and the look
   does not change. Both tools use these dots. */
.tp-cfg-paint{ position:relative; }
.tp-cfg-paint::after{ content:''; position:absolute; inset:-9px; border-radius:50%; }

/* ── THE COLOUR TOOL (template-parts/topic-recolour.php) ─────────────────────────────────
   The configurator's frame and panel, with a photograph where the door stood. */
.tp-rc .tp-cfg-stage{ display:flex; align-items:stretch; min-height:0; padding:0; }
.tp-rc .tp-cfg-stage::before{ display:none; }
/* The picture fills its half, whatever the panel's height; on a phone it keeps 4:3 (below). */
.tp-rc-frame{ position:relative; flex:1; min-height:340px; overflow:hidden; background:#E9E2D6; }
/* ⚠ A ONE-COLOUR TOOL KEEPS AT LEAST 4:3 ON DESKTOP. Its panel is short (one picker, no Go further), and a frame that
   only fills the panel's height letterboxed CR's kitchen to about 1.9:1, cropping the tops of the upper doors, which are
   the subject. So the frame sets the height and the panel takes the spare room. Phones already keep 4:3 (below). */
@media(min-width:901px){ .tp-rc--fixed .tp-rc-frame{ aspect-ratio:4 / 3; } }
.tp-rc-shot{ position:absolute; inset:0; margin:0; opacity:0; transition:opacity .5s var(--ease); }
.tp-rc-shot.is-active{ opacity:1; }
/* A set whose pictures are not the same frame swaps at once: a crossfade between two different
   photographs reads as the room moving (topic-recolour.php, 'swap' => 'instant'). */
.tp-rc--instant .tp-rc-shot{ transition:none; }
.tp-rc-shot img{ display:block; width:100%; height:100%; object-fit:cover; }
/* Obvious-on-purpose filler, like every placeholder on the site: dashed, captioned with the file it
   wants, and washed in the colour it stands for, so flipping colours visibly works before the
   photographs exist. */
.tp-rc-ph{
  position:absolute; inset:14px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
  padding:16px; border:1.5px dashed rgba(167,120,69,.45); border-radius:8px; text-align:center;
  background:linear-gradient(rgba(255,255,255,.62), rgba(255,255,255,.62)), var(--rc-tint, #EFE4D2);
}
.tp-rc-ph b{ font-family:var(--heading); font-size:15px; font-weight:700; color:var(--navy); }
.tp-rc-ph span{ font-family:var(--heading); font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:#7A5530; }
/* The summary sits over a photograph here, so it gets a ground to be read on. */
.tp-rc .tp-cfg-summary{ z-index:2; padding:7px 11px; border-radius:6px; background:rgba(255,255,255,.9); box-shadow:0 2px 10px rgba(11,8,42,.08); }
/* Kitchens are landscape photographs, not relief maps: no blend. */
.tp-rc-thumb{ width:76px; aspect-ratio:4 / 3; }
.tp-rc-thumb img{ mix-blend-mode:normal; }
.tp-rc-thumb--empty{ background:repeating-linear-gradient(135deg, #F3EADD 0 6px, #EFE4D2 6px 12px); box-shadow:0 0 0 1px rgba(167,120,69,.35); }
/* "Current finish" is the photograph's own finish, drawn as a split dot rather than a colour. */
.tp-rc-orig{ background:linear-gradient(135deg, #CDBFAE 0 50%, #FFFFFF 50% 100%); }
.tp-rc-note{ margin-top:14px; font-size:13px; }
/* "Go further" (upsell #1): a toggle over the colours, not another dot. Pressed, it goes navy and its + turns
   to a ×, and the colours dim because they are not what the picture shows until one is tapped. */
.tp-rc-go-btn{
  display:inline-flex; align-items:center; gap:10px;
  padding:10px 16px 10px 10px; border-radius:999px; border:1.5px solid var(--border-warm);
  background:#fff; color:var(--navy); cursor:pointer;
  font-family:var(--heading); font-size:14px; font-weight:700;
  transition:border-color .2s var(--ease), background .2s var(--ease), color .2s var(--ease);
}
.tp-rc-go-btn:hover{ border-color:var(--brown); }
.tp-rc-go-btn:focus-visible{ outline:3px solid var(--brown); outline-offset:3px; }
.tp-rc-go-btn.is-active{ background:var(--navy); border-color:var(--navy); color:#fff; }
.tp-rc-go-plus{
  display:inline-grid; place-items:center; width:22px; height:22px; border-radius:50%;
  background:var(--cream-raised); color:var(--brown-dark); font-size:16px; line-height:1;
  transition:transform .25s var(--ease), background .2s var(--ease), color .2s var(--ease);
}
.tp-rc-go-btn.is-active .tp-rc-go-plus{ transform:rotate(45deg); background:rgba(255,255,255,.16); color:#fff; }
.tp-rc-go-desc{ margin:12px 0 0; font-size:14px; line-height:1.5; color:var(--text-2); }
.tp-rc-go-desc a{ color:var(--brown-dark); font-weight:700; text-decoration:none; white-space:nowrap; }
.tp-rc-go-desc a:hover{ text-decoration:underline; }
/* "Tap me": one paint dot pulses until the first pick ('hint' in the fill; nav.js drops .is-hint). Jesse: "make some
   pulsating round whatever around one of the colours to get someone to wanna click it". On ::before, because ::after
   is the dot's invisible 44px tap target. Transform and opacity only, so it never repaints; a still ring under
   reduced motion, so the invitation stays without the movement. */
.tp-rc .tp-cfg-paint.is-hint::before{
  content:''; position:absolute; inset:-6px; border-radius:50%;
  border:2px solid var(--brown); pointer-events:none;
  animation:ncRcHint 1.8s var(--ease) infinite;
}
@keyframes ncRcHint{
  0%   { transform:scale(.82); opacity:.95; }
  70%  { transform:scale(1.4);  opacity:0; }
  100% { transform:scale(1.4);  opacity:0; }
}
@media(prefers-reduced-motion:reduce){
  .tp-rc .tp-cfg-paint.is-hint::before{ animation:none; transform:none; opacity:.85; }
}
/* The same invitation on a picker option (CR: the first door profile, pulsing while "Current doors" is up). The ring is
   the thumbnail's own shadow, because the thumbnail clips its children. #a77845 is --brown. */
.tp-rc .tp-cfg-opt.is-hint .tp-cfg-thumb{ animation:ncRcHintThumb 1.8s var(--ease) infinite; }
@keyframes ncRcHintThumb{
  0%   { box-shadow:0 0 0 1px var(--border-warm), 0 0 0 0 rgba(167,120,69,.8); }
  70%  { box-shadow:0 0 0 1px var(--border-warm), 0 0 0 10px rgba(167,120,69,0); }
  100% { box-shadow:0 0 0 1px var(--border-warm), 0 0 0 10px rgba(167,120,69,0); }
}
@media(prefers-reduced-motion:reduce){
  .tp-rc .tp-cfg-opt.is-hint .tp-cfg-thumb{ animation:none; box-shadow:0 0 0 1px var(--border-warm), 0 0 0 3px rgba(167,120,69,.85); }
}
/* A photograph in a door-shaped thumbnail ("Current doors"), shown as it is, not through the relief maps' hard-light. */
.tp-rc .tp-rc-thumb--photo img{ mix-blend-mode:normal; }
/* Five options on a phone (CR: Current doors plus four profiles). At their natural widths "Beaded / inset" dropped to a
   second row by itself, so on a phone they share the row equally and the names wrap. Only this tool's picker: the KR
   configurator's four fit as they are. */
@media(max-width:560px){
  .tp-rc .tp-rc-kitchens{ flex-wrap:nowrap; gap:6px; }
  .tp-rc .tp-rc-kitchens .tp-cfg-opt{ flex:1 1 0; min-width:0; }
  .tp-rc .tp-rc-kitchens .tp-cfg-opt-name{ white-space:normal; text-align:center; line-height:1.25; }
}
.tp-rc .tp-rc-colours{ transition:opacity .2s var(--ease); }
.tp-rc.is-go .tp-rc-colours{ opacity:.45; }
@media(prefers-reduced-motion:reduce){ .tp-rc-go-btn, .tp-rc-go-plus, .tp-rc .tp-rc-colours{ transition:none; } }
@media(max-width:900px){
  .tp-rc-frame{ flex:none; width:100%; min-height:0; aspect-ratio:4 / 3; }
}

/* The dialog. Native <dialog>: focus moves in, Escape closes, focus returns. The backdrop is the
   mega menu's frost, so the site has one way of saying "this is in front". */
.tp-cfg-dialog{
  /* margin:auto is the browser's own centring for a modal dialog. The theme's global reset
     zeroes every margin, and without this the dialog opened in the top-left corner. */
  margin:auto; padding:0; border:0; border-radius:16px; overflow:hidden; background:#0b082a; color:#fff;
  max-width:min(92vw, 1100px); max-height:92vh; box-shadow:0 30px 80px rgba(11,8,42,.45);
}
.tp-cfg-dialog::backdrop{ background:rgba(22,16,68,.55); -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px); }
/* min-content: the dialog is as wide as its picture, and a long caption wraps under it rather than
   widening the dialog past the picture's edge. */
.tp-cfg-dialog-fig{ margin:0; width:min-content; }
/* Up close: your door, magnified, and a real example photo, in ONE box, so switching between them
   never resizes the dialog. 4:5, the photos' shape (ops/theme-notes/northco-classic-v2/assets/images/doors/README.md), fitted to the
   screen with room for a two-line caption. The copy of the door hangs off the box's bottom right,
   so what shows is its top-left corner: the edge, the joint, the frame meeting the panel, and the
   grain or paint through it, about three times the size on the stage.
   ⚠ THE PHOTO IS NAMED BY ITS CLASS, NEVER AS "img" IN THE FIGURE: the copied door inside the
   close-up has an <img> of its own, the profile's relief map. Written as the figure's img, this
   rule also sized that to the box, 59% of the door, and the profile's lines landed in the wrong
   place: Jesse's screenshot of the beaded inset, 2026-09-23. */
.tp-cfg-dialog-live,
.tp-cfg-dialog-photo{
  display:block; width:min(88vw, calc((92vh - 96px) * .8), 600px); height:auto; aspect-ratio:4 / 5;
}
.tp-cfg-dialog-live[hidden]{ display:none; }
.tp-cfg-dialog-live{
  position:relative; overflow:hidden;
  background:radial-gradient(120% 100% at 25% 15%, #FFFFFF 0%, #F7F3ED 55%, #ECE5D9 100%);
}
.tp-cfg-dialog-live .tp-cfg-door{ position:absolute; left:6%; top:6%; width:170%; }
/* The joints are drawn in pixels, so here, magnified, they are drawn thicker, or they read as
   scratches rather than seams. */
.tp-cfg-dialog-live .tp-cfg-seam::before{ width:2px; }
.tp-cfg-dialog-live .tp-cfg-seam--tl::after,
.tp-cfg-dialog-live .tp-cfg-seam--br::after{ background:linear-gradient(to top right, transparent calc(50% - 1.6px), rgba(38,22,8,.26) 50%, transparent calc(50% + 1.6px)); }
.tp-cfg-dialog-live .tp-cfg-seam--tr::after,
.tp-cfg-dialog-live .tp-cfg-seam--bl::after{ background:linear-gradient(to bottom right, transparent calc(50% - 1.6px), rgba(38,22,8,.26) 50%, transparent calc(50% + 1.6px)); }
/* max-width:none: the theme's img{max-width:100%} is a percentage of a min-content figure, which
   an image counts as zero, and "See a real one" shrank the dialog to 105px wide. */
.tp-cfg-dialog-photo{ object-fit:cover; max-width:none; margin:0; background:#1a1640; }
.tp-cfg-dialog-cap{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:10px 16px;
  padding:15px 20px 16px; font-family:var(--sans); font-size:13px; font-weight:700;
  letter-spacing:.06em; color:rgba(255,255,255,.86); text-wrap:pretty;
}
/* "See a real one" / "Back to your door": quiet, on the caption bar, only where a real photo exists. */
.tp-cfg-dialog-alt{
  flex:none; -webkit-appearance:none; appearance:none; margin:0; cursor:pointer;
  padding:8px 14px; border-radius:999px; border:1px solid rgba(255,255,255,.38); background:transparent;
  font-family:var(--sans); font-size:10.5px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:#fff;
  transition:background-color .2s var(--ease), border-color .2s var(--ease);
}
.tp-cfg-dialog-alt:hover{ background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.7); }
.tp-cfg-dialog-alt:focus-visible{ outline:3px solid var(--brown); outline-offset:2px; }
.tp-cfg-dialog-alt[hidden]{ display:none; }
.tp-cfg-dialog-close{
  position:absolute; top:12px; right:12px; width:40px; height:40px; border-radius:50%; border:0;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  background:rgba(255,255,255,.92); color:var(--navy); font-size:24px; line-height:1;
  box-shadow:0 4px 14px rgba(0,0,0,.25);
}
.tp-cfg-dialog-close:focus-visible{ outline:3px solid var(--brown); outline-offset:2px; }
.tp-cfg-dialog-photo[hidden]{ display:none; }
/* The edge, large: the same mask, navy on white, with room round it. */
.tp-cfg-dialog--edge{ background:#fff; color:var(--navy); }
.tp-cfg-dialog-edge[hidden]{ display:none; }
.tp-cfg-dialog-edge{ padding:64px 56px 4px; }
.tp-cfg-dialog-edge-art{
  display:block; width:min(80vw, 900px); aspect-ratio:935 / 258; background:var(--navy);
  -webkit-mask-size:contain; mask-size:contain; -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center; mask-position:center;
}
.tp-cfg-dialog--edge .tp-cfg-dialog-cap{ padding:12px 56px 26px; color:var(--text-2); }
.tp-cfg-dialog--edge .tp-cfg-dialog-close{ background:var(--cream); }
@media(max-width:600px){
  .tp-cfg-dialog-edge{ padding:56px 18px 4px; }
  .tp-cfg-dialog--edge .tp-cfg-dialog-cap{ padding:10px 18px 20px; }
}

@media(max-width:900px){
  .tp-cfg{ grid-template-columns:1fr; }
  /* The door sits higher on a phone so the two callouts under it have their own band, and
     "Up close" is the circle alone: its label ran over the door at 390px. */
  .tp-cfg-stage{ min-height:0; padding:48px 20px 100px; }
  .tp-cfg-loupe-label{ display:none; }
  .tp-cfg-door{ width:min(48%,200px); }
  .tp-cfg-summary{ left:20px; top:20px; }
  /* ⚠ THE SUMMARY SAT ON THE DOOR at phone widths once a long pick wrapped it to two lines. "Beaded / inset · Polyester
     wood look in Smoked walnut" overlapped the door by 6px at 320–414, measured 2026-09-24 after the home-show session did
     the arithmetic. Single-column, the stage is only the door plus padding, so an absolute summary can only be padded
     clear for today's longest string. In the flow, above the door, the gap is real and grows when the text wraps.
     Configurator only: the colour tool's summary floats over its photograph by design. */
  .tp-cfg:not(.tp-rc) .tp-cfg-stage{ flex-direction:column; padding-top:20px; }
  .tp-cfg:not(.tp-rc) .tp-cfg-summary{ position:static; align-self:stretch; margin:0 0 14px; }
  .tp-cfg-loupe{ right:16px; bottom:14px; }
  .tp-cfg-edge{ left:16px; bottom:16px; gap:6px; }
  .tp-cfg-edge-art{ width:96px; }
  .tp-cfg-loupe-img{ width:60px; height:60px; }
  .tp-cfg-panel{ padding:24px 20px 26px; }
  .tp-cfg-opts{ gap:12px; }
}
@media(max-width:600px){
  .tp-cfg-cols{ grid-template-columns:1fr; gap:14px; }
}
@media(prefers-reduced-motion:reduce){
  .tp-cfg-corners, .tp-cfg-finish, .tp-cfg-paints{ animation:none; }
  .tp-cfg-door-shade, .tp-cfg-door-tex, .tp-cfg-loupe-tex, .tp-cfg-loupe-wood, .tp-cfg-thumb, .tp-cfg-swatch,
  .tp-cfg-edge-img, .tp-cfg-loupe-zoom, .tp-cfg-part, .tp-cfg-seam::before, .tp-cfg-seam::after, .tp-cfg-seg-opt,
  .tp-cfg-paintcoat, .tp-cfg-door-tex--paintable .tp-cfg-part::after, .tp-cfg-paint, .tp-cfg-door-tex, .tp-cfg-swatch,
  .tp-cfg.is-live .tp-cfg-detail, .tp-cfg.is-live .tp-cfg-detail.is-active{ transition:none; }
}


/* The page-title header when the CONTENT already owns the h1 — see page.php. Same type, a
   <p> instead, so the outline has one h1 and nothing moves on screen. */
.page-header-title{ margin:0 0 18px; }

/* ════════════════════════════════════════════════════════════════════════
   PHONE TYPE FLOOR (2026-10-14). LAST IN THE FILE ON PURPOSE.
   Alex (Postmedia ads): "this website struggles with accessibility. The font sizes are for the most part too small …
   especially true given your core demographic". Measured at 375px: 72% of the homepage's text was under 16px and 19%
   under 14px (body copy 14–15px, pills 8–9px), where V1 had 30% and 1% with paragraphs at 18px; Kitchen Renovation
   70% / 14%. Agreed with Jesse: on phones, reading text 17px, secondary 15px, nothing under 12px.
   ⚠ HOW THIS BLOCK WORKS. Every selector below is the EXACT selector that was setting that text's size at 375px,
   found by a headless scan of all 48 pages (dev render), re-declared here. Same specificity, later in the file, so it
   wins on phones without !important. A component that needs a different phone size changes it HERE, not in its own
   rule; a new component with small text belongs in this list too. The careers pages are left alone (their own
   stylesheet, and a younger audience: see the mobile-first note in memory).
   Desktop is unchanged by this block. */
@media(max-width:700px){
  /* 17px — Reading text: answers, reviews, card and step copy, lists */
  .contact-sub,
  .cu-fact-body p,
  .cu-social .tp-social-sub,
  .faq-a-inner,
  .feature-body,
  .feature-hook,
  .founder-value-line,
  .jn-card-excerpt,
  .jn-card-lead .jn-card-excerpt,
  .nc-faq-set-sub,
  .rv-text,
  .svc-blurb,
  .svcd-blurb,
  .tp-bento-body,
  .tp-card-body,
  .tp-card-list li,
  .tp-check,
  .tp-cmp-copy p,
  .tp-detail-solo-body .tp-detail-text,
  .tp-detail-text,
  .tp-faq-aside,
  .tp-fold-body .tp-slide-text,
  .tp-fold-note,
  .tp-form-list li,
  .tp-paint-body,
  .tp-paint-note,
  .tp-rc-go-desc,
  .tp-slide-text,
  .tp-switcher-col li,
  .tp-upg-points li,
  .tp-weeks-note{ font-size:17px; }
  /* 16px — Lead-ins, step straps, the call line */
  .ab-card-tag,
  .est-root .bc-label,
  .nc-quote-call,
  .post-nav-title,
  .sl-name,
  .svc-more-cross,
  .svc-more-list a,
  .tp-fold-body .tp-slide-strap,
  .tp-rail-name,
  .tp-weeks-span{ font-size:16px; }
  /* 15px — Secondary: links, notes, table cells, form helpers */
  .ab-crew-role,
  .ab-shot-cap,
  .est-root .btn-back,
  .est-root .btn-next,
  .est-root .oc-name,
  .est-root .step-sub,
  .feature-tagline,
  .flink,
  .form-alt,
  .ftr-bottom .ftr-addr,
  .ftr-bottom .ftr-link,
  .ftr-link,
  .nc-faq-tab,
  .nc-quote-upsell,
  .pf-choice-blurb,
  .pf-switch-blurb,
  .post-cta-call,
  .rv-name,
  .svc-link,
  .tp-bento-links a,
  .tp-card-cta a,
  .tp-cfg-note,
  .tp-cmp-strap,
  .tp-links a,
  .tp-rail-note,
  .tp-stat-label,
  .tp-table td,
  .tp-upg-strap,
  .tp-weeks-key{ font-size:15px; }
  /* 14px — Fine print, meta lines, form labels */
  .est-root .bc-sub,
  .est-root .cat-sub,
  .est-root .mc-desc,
  .est-root .oc-desc,
  .est-root .sc-tagline,
  .ftr-copy,
  .nc-quote-fine,
  .pf-item-meta,
  .post-recent-date,
  .rv-agg-count,
  .sl-sub,
  .tp-cfg-cta-note,
  .tp-pills li,
  .tp-rc-note,
  .v2-field label{ font-size:14px; }
  /* 13px — Small meta (review role, dates, tags) */
  .est-root .oc-brand,
  .fcopy,
  .jn-card-meta,
  .rv-role,
  .svc-meta-item,
  .tp-cfg-opt-name,
  .tp-cfg-seg-opt{ font-size:13px; }
  /* 12px — Labels and pills: the floor, nothing smaller */
  .btn-sm,
  .est-root .cat-new,
  .est-root .cat-pill,
  .est-root .dot-lbl,
  .est-root .oc-tier,
  .est-root .sc-eyebrow,
  .est-root .sc-popular,
  .est-root .section-divider-label,
  .est-scroll-hint,
  .feature-eyebrow,
  .feature-pill,
  .ftr-badge,
  .jn-card-tag,
  .jn-chip-n,
  .panel-cta,
  .panel-label,
  .panel-new,
  .panel-pill,
  .pf-tile-type,
  .pfp-eyebrow,
  .pfp-flip-tag,
  .pfp-sib-eyebrow,
  .post-crumb-cat,
  .post-nav-dir,
  .post-recent-heading,
  .sl-new,
  .svc-band-copy .svc-band-type,
  .svc-eyebrow,
  .svc-new,
  .svcd-eyebrow,
  .tp-cfg-corners-label,
  .tp-cfg-cta-btn,
  .tp-cfg-edge-label,
  .tp-cfg-step,
  .tp-cfg-summary,
  .tp-cfg-tag,
  .tp-figs-pair-label,
  .tp-pills-label,
  .tp-rail-link,
  .tp-sel-cap,
  .tp-switcher-col-label,
  .tp-switcher-sub,
  .tp-table tr:first-child th,
  .tp-upg-chip span i,
  .tp-upg-pill,
  .tp-vp-badge,
  .tp-vp-group,
  .tp-zoom,
  .trust-item{ font-size:12px; }
}

/* ════════════════════════════════════════════════════════════════════════
   WIDE-SCREEN TYPE FLOOR (2026-10-14), the phone floor's twin, also last in the file on purpose.
   Jesse, after the phone pass: "yes go ahead with the desktop pass, we can revert if needed". Alex's one example was a
   desktop view: the homepage's "Kitchens, Reimagined" card, its line 13px and its pills 9px. At 1440 the homepage had
   76% of its text under 16px (paragraphs 14.5–15px) where V1 had 30% (paragraphs 18–20px).
   Same method as the phone block: each selector is the exact rule setting that text at 1440 and 1024 (headless scan of
   all 48 pages), re-declared here at the new size. Same targets: reading 17, secondary 15, nothing under 12.
   ⚠ LEFT OUT ON PURPOSE: the header and its menus (.nav-*, tuned to fit at 1024), the step-log rail labels (.tp-vp-seg-*,
   whose 13px active "Custom design" fills 117 of 122px; any bigger and it wraps, which Jesse rejected), and the careers
   pages (careers.css). Phones are the block above. */
@media(min-width:701px){
  /* 17px — Reading text */
  .contact-sub,
  .cu-fact-body p,
  .cu-social .tp-social-sub,
  .estimator-section .estimator-sub,
  .faq-a-inner,
  .feature-body,
  .feature-hook,
  .founder-value-line,
  .jn-card-excerpt,
  .nc-faq-set-sub,
  .rv-text,
  .svc-blurb,
  .svcd-blurb,
  .tp-bento-body,
  .tp-card-body,
  .tp-card-list li,
  .tp-check,
  .tp-cmp-copy p,
  .tp-detail-solo-body .tp-detail-text,
  .tp-detail-text,
  .tp-faq-aside,
  .tp-fold-body .tp-slide-text,
  .tp-fold-note,
  .tp-form-list li,
  .tp-paint-body,
  .tp-paint-note,
  .tp-rc-go-desc,
  .tp-slide-text,
  .tp-switcher-col li,
  .tp-upg-points li,
  .tp-weeks-note{ font-size:17px; }
  /* 16px — Lead-ins, step straps, the call line, the form textarea */
  .ab-card-tag,
  .est-root .bc-label,
  .fi-textarea,
  .post-nav-title,
  .sl-name,
  .svc-more-cross,
  .svc-more-list a,
  .tp-fold-body .tp-slide-strap,
  .tp-rail-name,
  .tp-weeks-span{ font-size:16px; }
  /* 15px — Secondary: links, notes, table cells, the hero cards' line */
  .ab-crew-role,
  .ab-shot-cap,
  .cu-fact-go,
  .est-root .btn-back,
  .est-root .btn-next,
  .est-root .mc-name,
  .est-root .oc-name,
  .est-root .step-sub,
  .feature-tagline,
  .flink,
  .form-alt,
  .form-alt-quiet .fab-link,
  .form-alt-quiet .fab-text strong,
  .ftr-bottom .ftr-addr,
  .ftr-bottom .ftr-link,
  .ftr-link,
  .nc-faq-tab,
  .nc-quote-upsell,
  .ov-quote figcaption,
  .panel-sub,
  .pf-choice-blurb,
  .pf-switch-blurb,
  .pfp-sib-meta,
  .post-cta-call,
  .post-meta,
  .post-toc-item a,
  .rv-name,
  .svc-link,
  .tp-bento-links a,
  .tp-card-cta a,
  .tp-card-strap,
  .tp-cfg-note,
  .tp-cmp-more,
  .tp-cmp-strap,
  .tp-links a,
  .tp-rail-note,
  .tp-rc-go-btn,
  .tp-social--compact .tp-social-name,
  .tp-stat-label,
  .tp-table td,
  .tp-upg-strap,
  .tp-weeks-key{ font-size:15px; }
  /* 14px — Fine print, meta, form labels */
  .ab-pf-label,
  .est-root .bc-sub,
  .est-root .btn-skip,
  .est-root .cat-sub,
  .est-root .mc-desc,
  .est-root .oc-desc,
  .est-root .sc-tagline,
  .ftr-copy,
  .jn-chip,
  .nc-quote-fine,
  .pf-back,
  .pf-choice-meta,
  .pf-item-meta,
  .pfp-back,
  .post-crumb,
  .post-recent-date,
  .post-toc-l3 a,
  .rv-agg-count,
  .sl-sub,
  .svcc-thumb-label,
  .tp-carousel-count,
  .tp-cfg-cta-note,
  .tp-pills li,
  .tp-rc-note,
  .v2-field label{ font-size:14px; }
  /* 13px — Small meta */
  .est-root .oc-brand,
  .fcopy,
  .jn-card-meta,
  .rv-role,
  .svc-meta-item,
  .tp-cfg-opt-name,
  .tp-cfg-seg-opt{ font-size:13px; }
  /* 12px — Labels and pills: the floor */
  .btn-sm,
  .est-root .cat-new,
  .est-root .cat-pill,
  .est-root .dot-lbl,
  .est-root .oc-tier,
  .est-root .sc-eyebrow,
  .est-root .sc-popular,
  .est-root .section-divider-label,
  .feature-eyebrow,
  .feature-pill,
  .ftr-badge,
  .jn-card-tag,
  .jn-chip-n,
  .panel-cta,
  .panel-label,
  .panel-new,
  .panel-pill,
  .pf-tile-type,
  .pfp-eyebrow,
  .pfp-flip-tag,
  .pfp-sib-eyebrow,
  .post-crumb-cat,
  .post-nav-dir,
  .post-recent-heading,
  .post-toc-label,
  .sl-new,
  .svc-band-copy .svc-band-type,
  .svc-eyebrow,
  .svc-new,
  .svcd-eyebrow,
  .tp-cfg-corners-label,
  .tp-cfg-cta-btn,
  .tp-cfg-edge-label,
  .tp-cfg-step,
  .tp-cfg-summary,
  .tp-cfg-tag,
  .tp-figs-pair-label,
  .tp-pills-label,
  .tp-rail-link,
  .tp-sel-cap,
  .tp-switcher-col-label,
  .tp-switcher-sub,
  .tp-upg-chip span i,
  .tp-upg-pill,
  .tp-vp-badge,
  .tp-vp-group,
  .tp-cfg-loupe-label,
  .tp-zoom{ font-size:12px; }
}


/* ── THE DESIGN CONSULTATION PAGE (page-kitchen-design-consultation.php, 2026-10-28) ──
   Drawn → Built: the words beside the ads' film, the film in its 4:5 frame; stacked on a phone, the film under the words. */
.lp-drawn-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.8fr);gap:64px;align-items:center;}
/* "One wall gone." then "Whole room changed." on a line of its own, at every width (Jesse: "make it wrap so whole is
   on the second line visually"). */
.lp-drawn-copy .section-heading em{display:block;}
.lp-drawn-copy .section-sub{margin:0 0 16px;}
/* The column sets the measure, never a cap (the global .section-sub is 560px): the tiles' rule, .tp-band .section-sub. */
.lp-drawn .section-sub, .lp-built .section-sub{max-width:none;}
.lp-drawn-copy .tp-want{margin-top:28px;}
.lp-drawn-film{margin:0;}
.lp-film{display:block;width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;border-radius:var(--r);background:var(--cream-raised);}
/* Designed first, then built: the words above, the film full width under them. The wide film on desktop, the tall
   one on a phone; the hidden one never plays or loads (the page's script). */
.lp-built-head{margin-bottom:36px;}
.lp-built-film{margin:0;}
.lp-film--wide{aspect-ratio:16/9;}
.lp-film--tall{display:none;}
.lp-built .tp-want{margin-top:28px;}
@media(max-width:900px){
  .lp-film--wide{display:none;}
  .lp-film--tall{display:block;max-width:480px;margin:0 auto;}
  .lp-built-head{margin-bottom:28px;}
}
/* The film's honesty line: real photos, an AI transition (Mitch: AI must be labelled). */
.lp-film-note{margin:10px 2px 0;font-family:var(--sans);font-size:13px;line-height:1.5;color:var(--muted);}
/* The same film at the head of a service page's section (template-parts/topic-film.php, 2026-11-02): the section's words
   above it, its content under it. The note names the project, linked. */
/* The head's gap above it is the carousel's (.tp-carousel, 36px), and the carousel under it keeps a gap of its own. */
.tp-film{margin:36px 0 0;}
.tp-film + .tp-carousel, .tp-film + .tp-cfg{margin-top:40px;}
.tp-film .lp-film-note a{color:inherit;text-decoration:underline;text-underline-offset:2px;}
/* Two films side by side ('pair'): a 16:9 and a 4:5 at one height. Their widths are 16/9 : 4/5 = 20 : 9, and the gap
   comes off both in proportion, so the heights match exactly. Stacked on a phone. */
.tp-film-pair{display:grid;grid-template-columns:minmax(0,20fr) minmax(0,9fr);gap:20px;}
.tp-film-pair .tp-film-v--wide{aspect-ratio:16 / 9;}
.tp-film-pair .tp-film-v--tall{aspect-ratio:4 / 5;}
@media(max-width:900px){
  .tp-film{margin-top:28px;}
  .tp-film + .tp-carousel, .tp-film + .tp-cfg{margin-top:28px;}
  .tp-film-pair{grid-template-columns:1fr;gap:14px;}
}
@media(max-width:900px){
  .lp-drawn-grid{grid-template-columns:1fr;gap:32px;}
  .lp-film{max-width:480px;margin:0 auto;}
}
