/* Oswald, subset to just the wordmark's uppercase glyphs — 1.9 KB instead of
     168 KB. Condensed and bold: it holds at a truck door and at 36px in a
     sticky header, which is what the mark has to survive. font-display:swap so
     a slow font never blocks the header from painting. */
  @font-face{
    font-family:"Oswald Mark";
    src:url("/assets/fonts/oswald-bold-subset.woff2?v=74cf6ce4") format("woff2");
    font-weight:700; font-style:normal; font-display:swap;
  }
  /* ---- webfonts -------------------------------------------------------
     Archivo 600 for display, Inter for text. Both SIL OFL, latin subset,
     self-hosted woff2, font-display:swap so a slow font never blocks paint.

     Two files, 36.6 KB. Inter 600 was dropped by owner decision.

     That leaves bold body text without a real weight, so it is routed rather
     than synthesised: <b> -- the card titles, hero-meta labels and footer
     column heads, 14 of the 19 bold runs -- points at Archivo 600, a genuine
     weight already being downloaded. Those are labels, not prose, so a display
     face suits them. <strong> stays on Inter and synthesises, because it
     appears mid-sentence in the scope copy and changing typeface inside a
     sentence reads worse than a faux weight.

     Archivo: grotesque built for display -- tight apertures, sturdy verticals,
     weight at 49px without ornament, and it hints well on Windows at 96 DPI,
     which is what property managers are reading this on.
     Inter: engineered for small sizes; tall x-height and unambiguous 1/l/I,
     which matters on a scope line read at 15px in a stairwell. */
  @font-face{
    font-family:"Archivo"; src:url("/assets/fonts/archivo-600-latin.woff2?v=d9e8c29f") format("woff2");
    font-weight:600; font-style:normal; font-display:swap;
  }
  @font-face{
    font-family:"Inter"; src:url("/assets/fonts/inter-400-latin.woff2?v=8909904a") format("woff2");
    font-weight:400; font-style:normal; font-display:swap;
  }
  :root{
    --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    --display:"Archivo","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  }
  :root{
    --ink:#11161c; --ink-2:#2b3542; --muted:#5d6b7a; --line:#e2e6ea;
    --bg:#ffffff; --bg-2:#f5f7f9; --accent:#0f4c81; --accent-2:#12639f;
    --navy:#0B2C4D;
    --warn-bg:#fff8e6; --warn-line:#e8d9a8;
    --max:1100px;
  }
  *{box-sizing:border-box}
  html{scroll-behavior:smooth}
  body{
    margin:0; background:var(--bg); color:var(--ink);
    font:16px/1.65 var(--sans);
    -webkit-font-smoothing:antialiased;
  }
  .wrap{max-width:var(--max); margin:0 auto; padding:0 24px}
  a{color:var(--accent-2)}
  img{max-width:100%; height:auto}

  /* header */
  header{position:sticky; top:0; z-index:50; background:rgba(255,255,255,.94); backdrop-filter:blur(8px); border-bottom:1px solid var(--line)}
  .bar{display:flex; align-items:center; justify-content:space-between; gap:16px; padding:12px 0; transition:padding .25s ease}
  header.shrunk .bar{padding:7px 0}
  .brand{display:flex; align-items:center; gap:11px; text-decoration:none; color:var(--ink)}
  .brand img{width:46px; height:46px; flex:0 0 auto; transition:width .25s ease,height .25s ease}
  header.shrunk .brand img{width:36px; height:36px}
  .brand b{display:block; font-family:"Oswald Mark",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
           font-weight:700; font-size:21px; line-height:1.05; text-transform:uppercase; letter-spacing:.055em}
  .brand span.tag{display:block; font-size:10.5px; color:var(--muted); letter-spacing:.14em; text-transform:uppercase; font-weight:600; margin-top:2px}
  /* nav and the call button form one right-hand group: margin-left:auto
     pushes the pair over so the phone number lands at the far right,
     rather than space-between stranding the nav mid-bar. */
  nav{display:flex; align-items:center; gap:22px; margin-left:auto}
  nav a{color:var(--ink-2); text-decoration:none; font-size:14.5px; font-weight:500}
  nav a:hover{color:var(--accent)}
  .cta{background:var(--accent); color:#fff !important; padding:10px 17px; border-radius:6px; font-weight:600; white-space:nowrap}
  .cta:hover{background:var(--accent-2)}
  @media(max-width:800px){ nav .hide-sm{display:none} }

  /* hero */
  .hero{position:relative; overflow:hidden; background:linear-gradient(180deg,#0f1a26 0%,#16283a 100%); color:#fff;
        padding:96px 0 92px; min-height:min(80vh,760px); display:flex; align-items:center}
  .hero .wrap{position:relative; z-index:3; width:100%}

  /* slideshow: stacked covers, first one visible without JS */
  .hero-slides{position:absolute; inset:0; z-index:0; overflow:hidden}
  .hero-slides img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
                   opacity:0; will-change:opacity,transform}
  .hero-slides img:first-child{opacity:1}
  /* veil keeps white text legible over any frame — contrast cannot depend on
     which slide happens to be showing */
  /* Directional scrim, not a curtain.
     The old veil was 82-90% opaque across the whole frame: the headline passed
     contrast because the photograph had effectively been deleted, and we paid
     the full image download to show a flat #11161c rectangle.
     Now the darkness is spent where the words are -- heavy on the left, falling
     away to the right so the finished work is actually visible. */
  .hero-veil{position:absolute; inset:0; z-index:1; pointer-events:none;
             background:linear-gradient(100deg,
               rgba(11,17,22,.93) 0%,
               rgba(11,17,22,.91) 34%,
               rgba(11,17,22,.86) 56%,
               rgba(11,17,22,.42) 76%,
               rgba(11,17,22,.16) 100%)}
  /* The falloff has to start after the last glyph, not after the last element.
     The h1 is a block filling the wrap to 86.9%, but its INK ended at 68.5% --
     measuring the box rather than the text is how you get a scrim that looks
     generous and still fails contrast. Capping the headline wraps it to two
     lines, pulls the ink back to ~55%, and buys the photograph the right
     third of the frame instead of a sliver. */
  @media(min-width:1100px){ .hero h1{max-width:19ch} }
  /* Narrow screens have no "open side" -- the copy spans the full width. Turn
     the gradient vertical instead and drop the copy into the dark half, so the
     photo reads across the top third rather than being hidden everywhere. */
  @media(max-width:760px){
    .hero{align-items:flex-end; padding-top:64px; padding-bottom:76px}
    .hero-veil{background:linear-gradient(180deg,
                 rgba(11,17,22,.18) 0%,
                 rgba(11,17,22,.42) 26%,
                 rgba(11,17,22,.80) 52%,
                 rgba(11,17,22,.93) 74%,
                 rgba(11,17,22,.96) 100%)}
  }
  .hero-mark{position:absolute; right:-90px; top:50%; transform:translateY(-50%); width:520px; opacity:.07; pointer-events:none; z-index:2}
  @media(max-width:900px){ .hero-mark{right:-170px; width:420px; opacity:.05} }

  .hero-dots{position:absolute; left:0; right:0; bottom:20px; z-index:4;
             display:flex; gap:8px; justify-content:center}
  .hero-dots button{width:9px; height:9px; padding:0; border:0; border-radius:50%;
                    background:rgba(255,255,255,.40); cursor:pointer; transition:width .3s ease,background .3s ease}
  .hero-dots button[aria-current="true"]{background:#fff; width:26px; border-radius:5px}
  .hero-dots button:focus-visible{outline:2px solid #fff; outline-offset:3px}
  .hero h1{margin:0 0 16px; font-size:clamp(30px,4.6vw,49px); line-height:1.1; letter-spacing:-.8px; font-weight:600}
  .hero p.lede{margin:0 0 12px; font-size:clamp(16px,2vw,19px); color:#c6d3e0; max-width:62ch}
  .est{display:inline-block; margin-bottom:20px; font-size:12.5px; letter-spacing:.1em; text-transform:uppercase; color:#8fb4d6; font-weight:700}
  .hero-actions{display:flex; flex-wrap:wrap; gap:12px; margin-top:26px}
  .btn{display:inline-block; padding:13px 22px; border-radius:6px; text-decoration:none; font-weight:600; font-size:15.5px}
  .btn-p{background:#fff; color:#0f1a26}
  .btn-p:hover{background:#e8eef4}
  .btn-s{border:1px solid #4a6076; color:#fff}
  .btn-s:hover{background:rgba(255,255,255,.07)}
  .hero-meta{margin-top:30px; padding-top:22px; border-top:1px solid rgba(255,255,255,.14); display:flex; flex-wrap:wrap; gap:26px; font-size:14px; color:#a9bccd}
  .hero-meta b{color:#fff; display:block; font-size:15px}

  /* sections */
  section{padding:66px 0}
  section.alt{background:var(--bg-2); border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
  h1,h2,h3,h4{font-family:var(--display); font-weight:600; letter-spacing:-.2px}
  strong{font-weight:600}
  b{font-family:var(--display); font-weight:600}
  .btn,.cta{font-family:var(--display); font-weight:600}
  h2{margin:0 0 10px; font-size:clamp(23px,3vw,31px); letter-spacing:-.5px}
  .sub{margin:0 0 34px; color:var(--muted); max-width:65ch; font-size:16.5px}

  .grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(255px,1fr)); gap:18px}
  .card{background:#fff; border:1px solid var(--line); border-radius:9px; padding:22px}
  section.alt .card{background:#fff}
  .card h3{margin:0 0 8px; font-size:17px; letter-spacing:-.2px}
  .card p{margin:0; color:var(--muted); font-size:14.8px}
  .ico{width:34px;height:34px;border-radius:7px;background:#e8f0f7;display:flex;align-items:center;justify-content:center;margin-bottom:13px}

  ul.ticks{list-style:none; padding:0; margin:0; display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:9px 26px}
  ul.ticks li{padding-left:25px; position:relative; font-size:15.4px}
  ul.ticks li::before{content:"";position:absolute;left:0;top:8px;width:12px;height:7px;border-left:2.5px solid var(--accent);border-bottom:2.5px solid var(--accent);transform:rotate(-45deg)}

  /* gallery — masonry via CSS columns.
     These are real job photos, shot on a phone, mostly portrait. A fixed
     aspect-ratio grid would centre-crop them into a shape they were never
     framed for, cutting the top off showers and the counters off kitchens.
     Columns preserve each photo's native proportions. */
  .gallery{column-count:3; column-gap:16px}
  @media(max-width:1000px){ .gallery{column-count:2} }
  @media(max-width:640px){ .gallery{column-count:1} }
  .shot{break-inside:avoid; -webkit-column-break-inside:avoid; page-break-inside:avoid;
        margin:0 0 16px; border:1px solid var(--line); border-radius:9px; overflow:hidden; background:#fff}
  .shot img{display:block; width:100%; height:auto; transition:transform .5s ease}
  .shot:hover img{transform:scale(1.03)}
  @media(hover:none){ .shot:hover img{transform:none} }
  .shot figcaption{padding:12px 14px; font-size:14px; color:var(--ink-2); background:#fff; border-top:1px solid var(--line)}

  /* scope box */
  .scope{background:var(--warn-bg); border:1px solid var(--warn-line); border-radius:9px; padding:26px}
  .scope h3{margin:0 0 10px; font-size:18px}
  .scope p{margin:0 0 12px; font-size:15.4px}
  .scope p:last-child{margin-bottom:0}

  /* contact */
  .contact-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:20px; margin-bottom:30px}
  .cbox{background:#fff;border:1px solid var(--line);border-radius:9px;padding:22px}
  .cbox .lbl{font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:700;margin-bottom:7px}
  .cbox a{font-size:19px;font-weight:600;text-decoration:none;letter-spacing:-.3px}
  .cbox .note{font-size:14px;color:var(--muted);margin-top:6px}

  footer{background:#0f1a26; color:#93a7ba; padding:42px 0 34px; font-size:14px}
  footer .fgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:26px;margin-bottom:26px}
  footer b{color:#fff; display:block; margin-bottom:7px; font-size:14.5px}
  footer a{color:#c6d3e0}
  .legal{border-top:1px solid rgba(255,255,255,.13); padding-top:20px; font-size:12.8px; line-height:1.6; color:#7d90a3}

  /* --- motion -------------------------------------------------------------
     Only elements inside a document that has opted in (html.anim, set by the
     inline script below) start hidden. If JS fails, the CSS never applies and
     the page renders fully visible. Reduced-motion users never get the class. */
  html.anim .rise{opacity:0; transform:translateY(22px)}
  html.anim .fade{opacity:0}

  /* The hero is deliberately NOT JS-driven.
     A hoisted-variable bug in motion.js once threw before it could reveal
     anything, and because the hero was GSAP-controlled the headline, lede,
     buttons and phone number all stayed invisible in production. Above-the-fold
     content is the last thing that should depend on a script running. So the
     hero overrides the hidden state and animates itself in CSS: no JS, no
     library, nothing to throw. */
  html.anim .hero .rise{opacity:1; transform:none}
  @keyframes riseIn{ from{opacity:0; transform:translateY(20px)} to{opacity:1; transform:none} }
  @media (prefers-reduced-motion: no-preference){
    .hero .rise{animation:riseIn .65s cubic-bezier(.22,.61,.36,1) both}
    .hero .est{animation-delay:.04s}
    .hero h1{animation-delay:.12s}
    .hero .lede{animation-delay:.20s}
    .hero .hero-actions{animation-delay:.28s}
    .hero .hero-meta{animation-delay:.36s}
  }

  /* ---------------------------------------------------------------------
     Multi-page additions: wave dividers, wood page-head, proof strip, CTA
     --------------------------------------------------------------------- */

  nav a.here{color:var(--accent); font-weight:600}

  /* Ocean-wave band break. Inline SVG so it stretches edge to edge at any
     width without a second request and recolours with the palette. */
  .wave{line-height:0; background:var(--bg)}
  .wave svg{display:block; width:100%; height:clamp(38px,5vw,74px)}
  .wave svg path{fill:var(--navy)}
  .wave-flip{transform:rotate(180deg)}
  .wave + section.navy, section.navy + .wave{margin-top:0}

  section.navy{background:var(--navy); color:#e8eef5}
  section.navy h2{color:#fff}
  section.navy .sub{color:#a9c2d8}
  section.navy .card{background:rgba(255,255,255,.05); border-color:rgba(255,255,255,.14)}
  section.navy .card h3{color:#fff}
  section.navy .card p{color:#b3c6d8}

  /* Wood page-head for interior pages. Deliberately NOT behind the sticky
     nav: wood under nav text fights legibility on every scroll, and the bar
     is the one element that must stay readable over any background. */
  .page-head{
    position:relative; color:#fff; padding:52px 0 46px;
    background:var(--navy) url("/assets/wood-1600.webp?v=8d08c031") center/cover no-repeat;
  }
  .page-head::after{content:""; position:absolute; inset:0;
    background:linear-gradient(180deg,rgba(7,20,33,.58),rgba(7,20,33,.78))}
  .page-head .wrap{position:relative; z-index:2}
  .page-head h1{margin:0 0 8px; font-size:clamp(26px,4vw,40px); letter-spacing:-.6px; line-height:1.12}
  .page-head p{margin:0; color:#c2d5e6; font-size:16.5px; max-width:62ch}
  .crumb{font-size:12.5px; letter-spacing:.1em; text-transform:uppercase; color:#8fb4d6; font-weight:700; margin-bottom:10px}
  .crumb a{color:#8fb4d6; text-decoration:none}
  .crumb a:hover{color:#fff}

  /* Proof strip — photographs immediately after the hero, before any prose */
  .proof{padding:0}
  .proof-grid{display:grid; grid-template-columns:repeat(6,1fr); gap:8px}
  .proof-grid a{display:block; overflow:hidden; border-radius:7px; line-height:0}
  .proof-grid img{width:100%; height:100%; aspect-ratio:1/1; object-fit:cover; transition:transform .5s ease}
  .proof-grid a:hover img{transform:scale(1.05)}
  @media(max-width:900px){ .proof-grid{grid-template-columns:repeat(3,1fr)} }
  @media(max-width:520px){ .proof-grid{grid-template-columns:repeat(2,1fr)} }
  .proof-note{display:flex; flex-wrap:wrap; gap:10px 20px; align-items:baseline; justify-content:space-between; margin:14px 0 0; color:var(--muted); font-size:14.5px}
  .proof-note a{font-weight:600; text-decoration:none}

  /* long-form pages */
  .prose{max-width:72ch}
  .prose h3{margin:30px 0 8px; font-size:18px; letter-spacing:-.2px}
  .prose p{margin:0 0 14px; font-size:16px}
  .prose ul{margin:0 0 16px; padding-left:20px}
  .prose li{margin:0 0 7px; font-size:15.6px}
  .prose table{width:100%; border-collapse:collapse; margin:8px 0 20px; font-size:15px}
  .prose th,.prose td{text-align:left; padding:10px 12px; border-bottom:1px solid var(--line); vertical-align:top}
  .prose th{background:var(--bg-2); font-size:13px; letter-spacing:.04em; text-transform:uppercase; color:var(--ink-2)}
  .lede-lg{font-size:18px; color:var(--ink-2); margin:0 0 22px; max-width:66ch}

  /* ---------------------------------------------------------------------
     Wood header (owner request). Overrides the white bar defined above.
     Legibility is the whole risk here, so the wood carries a heavy dark
     veil and every element on it is set to white with a slight shadow.
     --------------------------------------------------------------------- */
  header#site-header{
    background:#171210 url("/assets/headwood-1600.webp?v=b80782a1") center/cover no-repeat;
    border-bottom:1px solid rgba(255,255,255,.12);
    backdrop-filter:none;
  }
  header#site-header::before{
    content:""; position:absolute; inset:0;
    background:linear-gradient(180deg,rgba(8,20,32,.80),rgba(8,20,32,.90));
    pointer-events:none;
  }
  header#site-header .wrap{position:relative; z-index:2}

  @media(max-width:900px){ header#site-header{background-image:url("/assets/headwood-900.webp?v=f461d417")} }
  .brand{color:#fff}
  .brand b{color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.45)}
  .brand span.tag{color:#a8c2da}
  .brand img{filter:drop-shadow(0 1px 3px rgba(0,0,0,.5))}

  header#site-header nav a{color:#dbe6f0}
  header#site-header nav a:hover{color:#fff}
  header#site-header nav a.here{color:#fff; font-weight:700}
  header#site-header nav a.here::after{
    content:""; display:block; height:2px; margin-top:4px;
    background:#8fb4d6; border-radius:2px;
  }
  header#site-header .cta{
    background:#fff; color:#0b2c4d !important;
    box-shadow:0 1px 3px rgba(0,0,0,.3);
  }
  header#site-header .cta:hover{background:#e6eef6}

  /* the wordmark gets a touch more room and presence on the darker ground */
  .brand img{width:50px; height:50px}
  header.shrunk .brand img{width:38px; height:38px}
  .brand b{font-size:22px; letter-spacing:.06em}


  /* ---------------------------------------------------------------------
     Photo cards that open an explanation beneath them, over ocean.
     --------------------------------------------------------------------- */
  /* overflow:clip keeps the pinned layer inside the section WITHOUT creating a
     scroll container, which is what lets position:sticky keep working. The
     hidden declaration before it is the fallback for browsers that lack clip;
     there sticky degrades to static, which is still fine because the layer has
     a fixed height and takes no space either way. */
  .tabs{position:relative; isolation:isolate; overflow:hidden; overflow:clip; padding:0 0 10px}
  .tabs > .wrap{padding-top:60px}
  /* The water is PINNED, not stretched.
     It used to be an inset:0 element with background-size:cover, so opening a
     taller panel grew the section and the image rescaled and shifted under
     you. Now it is a sticky viewport-height layer with a negative margin so it
     occupies no space: swapping cards changes the section's height without the
     backdrop moving or resizing at all. */
  .tabs-bg{
    position:sticky; top:0; height:100vh; margin-bottom:-100vh; z-index:0;
    overflow:hidden; pointer-events:none; background:#1f6ea8;
  }
  /* Two layers, each doing one job, so they never fight:
       .tabs-bg-shift — holds the pointer swirl and clips with the parent
       .tabs-bg-img   — the water itself, STATIC by owner decision
     There is no ambient animation: the water does not move on its own. The
     only motion is a local swirl that follows the pointer while it is
     genuinely over the water, and unwinds back to rest when it leaves. */
  .tabs-bg-shift{position:absolute; inset:0}
  .tabs-bg-img{
    position:absolute; inset:-4%;
    background:#1f6ea8 url("/assets/ocean-1600.webp?v=27f02333") center center/cover no-repeat;
  }
  /* A magnifier over the actual water, not a light sweeping across it.
     The old element was a rotating conic gradient -- it read as a torch beam,
     because it WAS one: a bright shape laid on top rather than the water
     itself moving. This is a small window showing the same ocean image at
     1.06x, anchored so the pixel under the cursor stays put and everything
     around it bulges. Moving the pointer drags the bulge, which is what
     water displacing actually looks like.
     Only the element's own transform changes per frame, and it is 360px, so
     the repaint is tiny rather than full-viewport. */
  .ocean-lens{
    position:absolute; left:0; top:0; width:360px; height:360px;
    margin:-180px 0 0 -180px; pointer-events:none; z-index:1;
    background-repeat:no-repeat; opacity:0;
    transition:opacity .28s ease;
    -webkit-mask-image:radial-gradient(closest-side,#000 30%,rgba(0,0,0,.55) 60%,transparent 78%);
    mask-image:radial-gradient(closest-side,#000 30%,rgba(0,0,0,.55) 60%,transparent 78%);
    transform:translate3d(-9999px,-9999px,0);
  }
  .ocean-lens.on{opacity:1}
  @media(hover:hover) and (pointer:fine){ .ocean-lens{will-change:transform,opacity} }
  @media(prefers-reduced-motion:reduce){ .ocean-lens{display:none} }
  /* Ambient water. See assets/js/ocean-ripple.js.
     The canvas shows the SAME ocean photograph as .tabs-bg-img, displaced by
     noise. It is inserted only after the texture has loaded and the shader has
     linked, so any failure leaves the still image untouched.
     The shader's illumination term is 0 on purpose: the sub-line margin noted
     below is 0.20, and displacement spends none of it where added light would. */
  .ocean-canvas{
    position:absolute; inset:0; width:100%; height:100%;
    display:block; z-index:0;
  }
  /* The magnifier existed to fake movement under the pointer. Once the water
     genuinely moves, a static 1.06x patch floating over it reads as a defect. */
  .has-ocean-ripple .ocean-lens{display:none}
  @media(prefers-reduced-motion:reduce){ .ocean-canvas{display:none} }
  @media(hover:hover) and (pointer:fine){ .ocean-swirl{will-change:transform,opacity} }
  @media(prefers-reduced-motion:reduce){ .ocean-swirl{display:none} }
  @media(min-width:1500px){ .tabs-bg-img{background-image:url("/assets/ocean-2560.webp?v=a5c3cfcf")} }
  /* Most of the veil is baked into the pixels now; this is the remainder.
     Sharpening cost some contrast margin -- blur had been smoothing the bright
     foam peaks that the worst-case sample lands on.
     CORRECTION 2026-09-04: this previously recorded the sub-line at 4.70. A
     direct measurement of the pixels under that line puts its WORST CASE at
     3.81, i.e. it was failing. 4.70 is close to the 1st-percentile figure
     (4.59), so it looks like a percentile rather than a worst case was recorded.
     The h2 is fine either way -- 31px/600 is WCAG large text, threshold 3.0,
     and it measures 4.47. The sub-line is fixed by its own colour, above, not
     by darkening this. */
  .tabs-bg::after{
    content:""; position:absolute; inset:0; pointer-events:none;
    background:linear-gradient(180deg,rgba(10,44,74,.10),rgba(10,44,74,.16));
  }
  /* The veil that used to live here (rgba(10,44,74,.60) -> .78) is now baked
     into the ocean pixels. Same composite, one less layer, and a darker source
     compresses far better: the sharp 1600 tier is 219 KB baked against 448 KB
     unbaked. The images were previously Gaussian-blurred to hold the file size
     down, which is why the backdrop read as low-resolution. They are sharp now. */
  .tabs > .wrap, .tabs .tabpanel{position:relative; z-index:2}
  .tabs > .wrap h2{color:#fff; text-shadow:0 2px 8px rgba(0,20,40,.5)}
  /* MEASURED 2026-09-04, not estimated. Sampling the actual ocean pixels under
     this line's box (cover fit + the shader's 0.86 zoom, dilated by the 0.062
     max displacement) and compositing the ::after veil at its local alpha:
       #dbeaf6 -> worst-case 3.81  (FAILS the 4.5 required at 16.5px/400)
       #ffffff -> worst-case 4.67  PASSES
     Everything between the two still fails: #e9f2fa 4.13, #f0f7fc 4.32,
     #f6fafd 4.45. White is the only value that clears it.
     The alternative was tripling the veil to ~0.28 alpha, which is the opposite
     of the direction this file took deliberately -- the veil was cut from
     .60/.78 to .10/.16 and baked into the pixels to keep the water sharp.
     Fixing the text costs the photograph nothing. */
  .tabs > .wrap .sub{color:#fff; text-shadow:0 1px 6px rgba(0,20,40,.5)}
  /* panels are white cards — never inherit the ocean colours */
  .tabpanel h2{color:var(--ink); text-shadow:none}
  .tabpanel .sub{color:var(--muted); text-shadow:none}
  @media(max-width:900px){ .tabs-bg-img{background-image:url("/assets/ocean-900.webp?v=93ce0dcd")} }

  .tabrow{display:grid; grid-template-columns:repeat(5,1fr); gap:12px; margin-bottom:26px}
  @media(max-width:900px){
    .tabrow{
      display:flex; gap:10px; overflow-x:auto; scroll-snap-type:x proximity;
      scrollbar-width:none; -webkit-overflow-scrolling:touch;
      /* Bleed to the screen edge so cards can be swiped past the gutter,
         then pad the content back to the text margin. scroll-padding is
         what keeps the FIRST card on that margin: snap positions align to
         the padding edge, not the content edge, so without it card one
         snapped flush to x=0 and sat out of line with the heading. */
      margin-left:-24px; margin-right:-24px; padding:2px 24px 10px;
      scroll-padding-inline:24px;
    }
    .tabrow::-webkit-scrollbar{display:none}
    .tabcard{flex:0 0 46%; scroll-snap-align:start}
    .tabcard img{aspect-ratio:5/4}
    /* Type for these lives in the mobile type scale at the END of this sheet.
       It cannot live here: these rules sit ABOVE the base .tabcard block, and
       at equal specificity the later rule wins -- so sizes set here were
       silently overridden and the phone kept desktop type. */
  }
  @media(max-width:520px){
    .tabcard{flex:0 0 62%}
  }

  .tabcard{
    position:relative; display:block; width:100%; padding:0; border:0; cursor:pointer;
    border-radius:11px; overflow:hidden; text-align:left; background:#0b2c4d;
    box-shadow:0 6px 18px rgba(4,20,36,.4); transition:transform .25s ease, box-shadow .25s ease;
  }
  .tabcard img{display:block; width:100%; aspect-ratio:4/3; object-fit:cover;
               transition:transform .5s ease, filter .3s ease; filter:brightness(.72)}
  .tabcard:hover{transform:translateY(-3px); box-shadow:0 12px 26px rgba(4,20,36,.5)}
  .tabcard:hover img{transform:scale(1.06); filter:brightness(.85)}
  .tabcard:focus-visible{outline:3px solid #fff; outline-offset:3px}
  .tabcard-label{
    position:absolute; left:0; right:0; bottom:0; padding:14px 14px 12px;
    background:linear-gradient(180deg,rgba(6,20,34,0),rgba(6,20,34,.88));
    color:#fff;
  }
  .tabcard-label b{display:block; font-size:16px; letter-spacing:-.2px; line-height:1.2}
  .tabcard-label em{display:block; font-style:normal; font-size:12.5px; color:#bcd3e6; margin-top:3px}
  .tabcard[aria-expanded="true"]{box-shadow:0 0 0 3px #fff, 0 12px 26px rgba(4,20,36,.5)}
  .tabcard[aria-expanded="true"] img{filter:brightness(.95)}

  /* Panels. No `hidden` in the markup: with JS off they all render open and
     the page reads as ordinary sections. */
  .tabpanel{background:#fff; border-radius:12px; margin:0 24px 22px;
            box-shadow:0 10px 30px rgba(4,20,36,.22)}
  .tabpanel > .wrap{padding-top:34px; padding-bottom:34px}
  .tabpanel[hidden]{display:none}
  .tabpanel h2{margin-top:0}
  @media(max-width:640px){ .tabpanel{margin:0 12px 18px} }

  /* ---------------------------------------------------------------------
     Wood header — warmer veil so the walnut actually reads as wood
     --------------------------------------------------------------------- */
  header#site-header::before{
    background:linear-gradient(180deg,rgba(12,10,10,.46),rgba(10,8,8,.62));
  }

  /* ---------------------------------------------------------------------
     Footer: a countertop capping dark cabinetry — the product, as the frame
     --------------------------------------------------------------------- */
  .counter-band{
    background:#ffffff url("/assets/slab-1600.webp?v=b0e995a0") center/100% 100% no-repeat;
  }
  @media(max-width:900px){ .counter-band{background-image:url("/assets/slab-900.webp?v=04b7d60b")} }
  footer{position:relative; box-shadow:inset 0 14px 20px -12px rgba(0,0,0,.85); background:#141010 url("/assets/cab-1600.webp?v=f19da211") center 42%/cover no-repeat}
  footer::before{content:""; position:absolute; inset:0;
    background:linear-gradient(180deg,rgba(10,8,8,.52),rgba(8,6,6,.74))}
  footer .wrap{position:relative; z-index:2}
  @media(max-width:900px){ footer{background-image:url("/assets/cab-900.webp?v=d02b19c1")} }

  /* CTA band: badge in full colour beside the copy */
  .cta-grid{display:grid; grid-template-columns:minmax(180px,300px) 1fr; gap:clamp(20px,4vw,54px); align-items:center}
  .cta-badge{width:100%; max-width:300px; height:auto; justify-self:center}
  .cta-copy h2{margin:0 0 10px}
  .cta-copy p{margin:0 0 4px; max-width:56ch}
  @media(max-width:760px){
    .cta-grid{grid-template-columns:1fr; text-align:center}
    .cta-badge{max-width:200px}
    .cta-copy p{margin-left:auto; margin-right:auto}
    }

  /* Slogan band — a breath between the work and the closing ask */
  .slogan{background:var(--navy); color:#dbeaf6; padding:26px 0; text-align:center}
  .slogan .wrap{display:flex; align-items:center; justify-content:center; gap:clamp(12px,3vw,26px); flex-wrap:wrap}
  .slogan span{font-size:clamp(12px,1.5vw,14.5px); letter-spacing:.30em; text-transform:uppercase; font-weight:500; color:#fff}
  .slogan i{display:block; width:clamp(40px,9vw,120px); height:1px; background:linear-gradient(90deg,rgba(143,180,214,0),#8fb4d6,rgba(143,180,214,0))}
  .slogan b{font-family:inherit; font-weight:500; font-size:clamp(12px,1.5vw,14.5px); letter-spacing:.30em; text-transform:uppercase; color:#fff}
  .slogan b em{font-style:normal; color:#8fb4d6}

  /* ---------------------------------------------------------------------
     The ask, set on the countertop. Dark ink on white stone; the wood band
     that used to carry this is gone, so the page runs water -> stone ->
     cabinet panel.
     --------------------------------------------------------------------- */
  .counter-band{position:relative; z-index:2; padding:clamp(34px,5vw,58px) 0 clamp(38px,5vw,68px)}
  /* the shadow the slab throws onto the cabinets below */


  .counter-band .wrap{position:relative; z-index:2}
  .counter-band .cta-grid{align-items:center}
  .counter-band h2{color:var(--ink); margin:0 0 10px; text-wrap:balance}
  .counter-band p{color:var(--ink-2); margin:0 0 4px; max-width:56ch; font-size:16.5px}
  .counter-band .hero-actions{justify-content:flex-start; margin-top:22px}
  .counter-badge{width:clamp(180px,21vw,272px); height:auto; display:block; justify-self:center;
                 margin-bottom:0; filter:drop-shadow(0 6px 16px rgba(0,0,0,.22))}
  .btn-ink{background:var(--navy); color:#fff}
  .btn-ink:hover{background:#0f3a63}
  .btn-outline{border:1.5px solid var(--navy); color:var(--navy)}
  .btn-outline:hover{background:rgba(11,44,77,.07)}
  @media(max-width:760px){
    .counter-band .cta-grid{text-align:center}
    .counter-band p{margin-left:auto; margin-right:auto}
    .counter-band .hero-actions{justify-content:center}
  }

  /* Slogan over water */
  .slogan{position:relative; overflow:hidden; background:var(--navy)}
  .slogan-bg{position:absolute; inset:0;
    background:#1f6ea8 url("/assets/ocean-1600.webp?v=27f02333") center/cover no-repeat}
  /* Shares the baked ocean file, so this only adds what the slogan band needs
     beyond the baked .60/.78 -- not the whole original .70/.80, which would
     double-darken it into black. */
  .slogan-bg::after{content:""; position:absolute; inset:0;
    background:linear-gradient(180deg,rgba(8,32,56,.20),rgba(8,32,56,.30))}
  .slogan .wrap{position:relative; z-index:2}
  .slogan span{text-shadow:0 1px 6px rgba(0,16,32,.6)}
  .slogan b em{color:#bcd9f2}
  .slogan b{text-shadow:0 1px 6px rgba(0,16,32,.6)}
  @media(max-width:900px){ .slogan-bg{background-image:url("/assets/ocean-900.webp?v=93ce0dcd")} }

  /* Centre the countertop block rather than letting it hang left */
  .counter-band .cta-grid{
    align-items:start;
    grid-template-columns:auto auto; justify-content:center; align-items:center;
    width:fit-content; max-width:min(1120px,94vw); margin-inline:auto;
  }
  .counter-band .cta-copy{
    max-width:660px; padding:18px 24px; border-radius:12px;
    background:rgba(255,255,255,.15);
  }
  /* This rule sits AFTER the max-width:760px block above and matches at every
     width, so it was still holding the badge beside the copy on a phone --
     squeezing the closing ask into a 172px column and wrapping its heading to
     four lines. Stack them below 760px: badge over copy, both full width. */
  @media(max-width:760px){
    /* On a phone the badge is too small to read as a mark and stealing a row
       for it squeezed the ask. Instead put it BEHIND the copy, sized to the
       countertop and dimmed hard: it becomes a watermark on the stone, and the
       words stay the priority. Both children share one grid cell. */
    .counter-band .cta-grid{
      grid-template-columns:1fr; width:auto; max-width:none; gap:0;
    }
    .counter-band .counter-badge,
    .counter-band .cta-copy{grid-area:1 / 1}
    .counter-band .counter-badge{
      width:min(88vw,460px); justify-self:center; align-self:center;
      opacity:.10; filter:none; pointer-events:none;
    }
    .counter-band .cta-copy{
      max-width:none; position:relative; z-index:1;
      background:rgba(255,255,255,.42);
      -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
    }
  }

  /* Panels: narrower than the full wrap so more water shows around them */
  .tabpanel{max-width:1000px; margin-left:auto; margin-right:auto}
  .tabpanel > .wrap{padding-left:clamp(20px,3vw,38px); padding-right:clamp(20px,3vw,38px)}
  @media(max-width:1060px){ .tabpanel{margin-left:16px; margin-right:16px} }

  /* ---------------------------------------------------------------------
     Work slideshow. A scroll-snap track: swipe, trackpad and keyboard all
     work with no JS. motion.js only adds arrows, dots and the counter.
     --------------------------------------------------------------------- */
  .workshow{position:relative; margin-top:6px}
  .workviewport{overflow:hidden}
  .worktrack{
    display:flex; gap:16px; overflow-x:auto; scroll-snap-type:x mandatory;
    scroll-behavior:smooth; padding:4px clamp(16px,8vw,140px) 10px;
    scrollbar-width:none;
  }
  .worktrack::-webkit-scrollbar{display:none}
  .worktrack:focus-visible{outline:3px solid var(--accent); outline-offset:-3px}
  .workslide{
    flex:0 0 auto; scroll-snap-align:center; margin:0;
    background:#0d151d; border-radius:12px; overflow:hidden;
    box-shadow:0 12px 32px rgba(6,20,34,.26);
  }
  .workslide img{
    display:block; height:clamp(320px,62vh,600px); width:auto;
    max-width:min(92vw,1000px); object-fit:cover;
  }
  /* width:0 + min-width:100% lets the caption fill the slide WITHOUT feeding
     its text length into the slide's intrinsic width. The slide is shrink-to-
     fit, so a long caption used to make it wider than the photo -- leaving a
     0-30px bar of #0d151d down each side of the image, varying per slide.
     The photo now defines the width and the caption follows it. */
  .workslide figcaption{
    width:0; min-width:100%; box-sizing:border-box;
    padding:13px 16px; font-size:15px; color:#dbe6f0; background:#0d151d;
    border-top:1px solid rgba(255,255,255,.08);
  }
  .worknav{
    position:absolute; top:calc(50% - 34px); transform:translateY(-50%); z-index:3;
    width:46px; height:46px; border:0; border-radius:50%; cursor:pointer;
    background:rgba(255,255,255,.94); color:var(--navy); font-size:26px; line-height:1;
    box-shadow:0 4px 14px rgba(6,20,34,.3);
  }
  .worknav:hover{background:#fff}
  .worknav:focus-visible{outline:3px solid var(--accent); outline-offset:2px}
  .worknav.prev{left:clamp(6px,2vw,30px)}
  .worknav.next{right:clamp(6px,2vw,30px)}
  .worknav[hidden]{display:none}
  .workdots{display:flex; gap:7px; justify-content:center; flex-wrap:wrap; margin-top:14px}
  .workdots button{width:8px; height:8px; padding:0; border:0; border-radius:50%;
                   background:var(--line); cursor:pointer; transition:width .3s ease,background .3s ease}
  .workdots button[aria-current="true"]{background:var(--accent); width:22px; border-radius:4px}
  .workdots button:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
  @media(max-width:700px){ .worknav{display:none} }

  /* The header is sticky, so anything scrolled to must reserve its height or
     the heading lands underneath it. Applies to native anchor jumps too. */
  html{scroll-padding-top:clamp(78px,9vw,108px)}
  section[id], #work{scroll-margin-top:clamp(78px,9vw,108px)}
  /* Panels take no scroll-margin either -- same stacking bug as .tabrow.
     On phones the panel is the scroll target, and the doubled offset left a
     sliced strip of card labels wedged under the sticky header, which reads
     as a rendering fault rather than a design. Land the panel flush. */
  .tabpanel{scroll-margin-top:0}
  /* The card row is its own scroll target (header links and card clicks both
     land on it, so the cards stay visible while the panel opens beneath).
     scroll-padding-top on <html> already clears the sticky header for
     scrollIntoView; adding a scroll-margin here too STACKED the two and
     pushed the row 142px down the viewport, eating panel space. Zero. */
  .tabrow{scroll-margin-top:0}

  /* Active nav item: underline only, and never a lingering focus box */
  header#site-header nav a{position:relative}
  header#site-header nav a:focus{outline:none}
  header#site-header nav a:focus-visible{outline:2px solid #fff; outline-offset:3px; border-radius:3px}

  /* =====================================================================
     MOBILE
     Three real problems, not cosmetics:
       1. nav .hide-sm was display:none below 800px, so a phone visitor had
          NO navigation at all — only the call button. The links now move to
          their own scrollable row instead of disappearing.
       2. the carousel dots were 8–9px. WCAG 2.5.8 asks for 24px minimum and
          Apple's guidance is 44px; a 9px target is not operable by thumb.
          The visual dot stays small, the hit area grows around it.
       3. 100vh includes the collapsing browser chrome on phones, so the
          pinned ocean jumped on every scroll. dvh tracks the visible
          viewport; the vh line before it is the fallback.
     ===================================================================== */

  /* Owner decision: on phones the header carries the logo alone — no links,
     no call button. The number is still reachable from the hero, the closing
     CTA and the footer; it simply is not persistent while scrolling. */
  @media(max-width:800px){
    header#site-header nav{display:none}
    header#site-header .cta{display:none}
    .bar{justify-content:center; padding:10px 0}
    .brand{gap:10px}
    .brand img{width:42px; height:42px}
    header.shrunk .brand img{width:34px; height:34px}
    .brand b{font-size:19px}

    /* one short row again, so anchored sections need less clearance */
    html{scroll-padding-top:74px}
    section[id], #work{scroll-margin-top:74px}
  }

  /* Touch targets: keep the dot small, make the tappable box big */
  .hero-dots button, .workdots button{
    position:relative; background-clip:padding-box;
  }
  .hero-dots button::after, .workdots button::after{
    content:""; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:44px; height:44px;
  }
  @media(hover:none){
    .hero-dots{gap:14px} .workdots{gap:14px}
  }

  /* Viewport units that survive a collapsing URL bar */
  .tabs-bg{height:100vh; margin-bottom:-100vh}
  .hero{min-height:min(80vh,760px)}
  .workslide img{height:clamp(320px,62vh,600px)}
  @supports (height:100dvh){
    .tabs-bg{height:100dvh; margin-bottom:-100dvh}
    .hero{min-height:min(80dvh,760px)}
    .workslide img{height:clamp(320px,62dvh,600px)}
  }

  @media(max-width:640px){
    .hero{padding:70px 0 78px}
    .hero-mark{display:none}          /* it only ever bled off-canvas here */
    .workslide img{
      width:min(92vw,520px); height:auto; aspect-ratio:3/4;
      max-width:none;
    }
    .workslide figcaption{font-size:14px; padding:11px 14px}
    .counter-band .cta-copy{padding:16px 18px}
    .slogan .wrap{gap:10px}
    .slogan i{display:none}
  }

  /* ---- mobile type scale -------------------------------------------------
     Deliberately the LAST thing in this sheet. An earlier attempt at the card
     sizes sat above the base .tabcard rules and lost the cascade at equal
     specificity, so phones silently kept desktop type. Anything that has to
     win on a phone belongs here, at the bottom.

     Sizes are set so each block fits inside its own widget -- the card label
     over its photo, the ask inside the countertop -- rather than being smaller
     for its own sake. Body copy stays at or above 14px and secondary text at
     or above 12px, which is the floor where this still reads at arm's length. */
  @media(max-width:800px){
    .hero h1{font-size:27px; line-height:1.16; letter-spacing:-.4px}
    .hero p.lede{font-size:15px}
    .est{font-size:11px; letter-spacing:.09em}
    .hero-meta{gap:14px 22px; font-size:12.5px; margin-top:24px; padding-top:18px}
    .hero-meta b{font-size:13px}
    .btn{padding:12px 18px; font-size:14.5px}

    h2{font-size:21px; letter-spacing:-.3px}
    .sub{font-size:14.6px; margin-bottom:24px}
    .card{padding:17px}
    .card h3{font-size:15.5px}
    .card p{font-size:13.6px}
    ul.ticks li{font-size:14px; padding-left:22px}
    .scope{padding:19px}
    .scope h3{font-size:16px}
    .scope p{font-size:14px}

    /* The label sits ON the photo, so it has to stay a caption, not a column
       of prose. Two lines maximum -- past that it swallowed the image. */
    .tabcard-label{padding:9px 10px 8px}
    .tabcard-label b{font-size:13px; line-height:1.16; letter-spacing:-.1px}
    .tabcard-label em{
      font-size:10.8px; line-height:1.25; margin-top:2px;
      display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
    }

    .counter-band h2{font-size:20px}
    .counter-band p{font-size:14.4px}
    .workslide figcaption{font-size:13.5px}
    .slogan span, .slogan b{font-size:14px}
    footer{font-size:13.5px}
    footer h4{font-size:13px}
  }
  @media(max-width:520px){
    .hero h1{font-size:25px}
    h2{font-size:20px}
    .tabcard-label b{font-size:12.5px}
    .tabcard-label em{font-size:10.5px}
  }

  /* ---- prose: the legal and SEO pages ------------------------------------
     The home page is a wide scroller; a column of legal text at the same
     1100px is unreadable. Cap the measure and give the headings room. */
  .prose{max-width:74ch}
  .prose h1{margin:0 0 12px; font-size:clamp(26px,3.4vw,36px); letter-spacing:-.6px}
  .prose h2{margin:32px 0 10px; font-size:clamp(19px,2.2vw,23px)}
  .prose h3{margin:0 0 8px}
  .prose p{font-size:16.2px; color:var(--ink-2)}
  .prose ul.ticks{grid-template-columns:1fr; gap:7px}
  .prose .grid{margin-top:20px}
  .prose .card h3 a{text-decoration:none}
  .prose .card h3 a:hover{text-decoration:underline}
  @media(max-width:800px){
    .prose p{font-size:15px}
    .prose h2{margin-top:26px}
  }
