/* ============================================================
   James Sharp Decorating Services — Premium Craftsmanship
   Design system 1a. Hand-built, no framework.
   ============================================================ */

/* ---------- Tokens ---------- */
:root{
  /* colour — gold sampled from the JdS seal */
  --black:#111110;
  --true-black:#000;
  --charcoal:#2a2a2a;
  --ink:#26241f;
  --gold:#c8962e;
  --gold-bright:#e3b95c;
  --gold-deep:#a87a22;
  --gold-text:#886519;
  --cream:#faf6ee;
  --cream-2:#f3ecdd;
  --paper:#ffffff;
  --line:rgba(17,17,16,.12);
  --line-soft:rgba(17,17,16,.07);
  --muted:#6a6557;
  --muted-2:#6f6a5c;
  --body:#3a382f;
  --error:#a8352a;

  /* type */
  --serif:'Playfair Display',Georgia,'Times New Roman',serif;
  --sans:'Mulish',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --mono:'Mulish',ui-monospace,Menlo,monospace;

  /* spacing scale */
  --s1:.5rem; --s2:1rem; --s3:1.5rem; --s4:2rem; --s5:3rem;
  --s6:4.5rem; --s7:6.5rem; --s8:9rem;

  --maxw:1200px;
  --radius:3px;
  --shadow:0 30px 70px -45px rgba(0,0,0,.55);
  --shadow-sm:0 14px 34px -22px rgba(0,0,0,.4);
  --ease:cubic-bezier(.22,.61,.36,1);

  /* motion — durations (approved motion system) */
  --dur-press:.12s;     /* :active depress */
  --dur-quick:.2s;      /* colour / underline / small hover */
  --dur-standard:.35s;  /* transforms: lifts, zooms, tags */
  --dur-slow:.7s;       /* entrances: reveals, line-draws */
  /* motion — confirmation curve (success/copy-done only; never routine hover) */
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
}

/* ---------- Reset ---------- */
*,*::before,*::after{box-sizing:border-box}
*{margin:0}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{
  font-family:var(--sans);
  color:var(--body);
  background:var(--cream);
  line-height:1.65;
  font-size:18px;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
img{max-width:100%;display:block;height:auto}
a{color:inherit;text-decoration:none}
ul{list-style:none;padding:0}
button{font-family:inherit;cursor:pointer}
:focus-visible{outline:2.5px solid var(--gold-deep);outline-offset:3px;border-radius:2px}
/* #main takes tabindex="-1" so the skip link moves real focus (and is announced).
   It is a scroll target, not a control, so it must not draw the focus ring —
   without this the skip would outline the whole page body. (F2, 2026-08-25) */
#main:focus,#main:focus-visible{outline:none}

/* ---------- Type ---------- */
h1,h2,h3,h4{font-family:var(--serif);color:var(--black);font-weight:600;line-height:1.08;letter-spacing:-.01em;text-wrap:balance}
h1{font-size:clamp(2.5rem,5.5vw,4.4rem);font-weight:500}
h2{font-size:clamp(1.9rem,3.6vw,2.9rem)}
h3{font-size:clamp(1.3rem,2.2vw,1.65rem)}
p{text-wrap:pretty}
.lead{font-size:clamp(1.1rem,1.6vw,1.3rem);line-height:1.6;color:var(--ink)}

.eyebrow{
  font-family:var(--sans);font-size:.72rem;font-weight:700;
  letter-spacing:.32em;text-transform:uppercase;color:var(--gold-text);
  display:inline-flex;align-items:center;gap:.8rem;
}
.eyebrow::before{content:"";width:34px;height:1px;background:var(--gold)}
.eyebrow.center::after{content:"";width:34px;height:1px;background:var(--gold)}
.eyebrow.center{justify-content:center}
.hero .eyebrow,.page-hero .eyebrow,.cta-band .eyebrow,.dark .eyebrow{color:var(--gold-deep)}

.script{font-style:italic;color:var(--gold-deep)}

/* ---------- Layout ---------- */
.wrap{max-width:var(--maxw);margin-inline:auto;padding-inline:clamp(1.25rem,4vw,2.5rem)}
.section{padding-block:clamp(3.5rem,7vw,7rem)}
/* Snag A16: the sticky header is 75px, so anything scrolled to by anchor landed
   under it — a section eyebrow measured y=66 against a header bottom of 75. */
.section,#main,[id]{scroll-margin-top:92px}
.section--tight{padding-block:clamp(2.5rem,5vw,4.5rem)}
.section.dark{background:var(--black);color:var(--cream)}
.section.dark h1,.section.dark h2,.section.dark h3{color:var(--cream)}
.section.ink{background:var(--cream-2)}
.center{text-align:center}
.section-head{max-width:660px;margin-bottom:clamp(2rem,4vw,3.2rem)}
.section-head.center{margin-inline:auto}
.section-head p{margin-top:1rem;color:var(--muted)}

/* skip link */
/* fixed, not absolute: absolute anchored the link to the document, so once the
   page had scrolled a tab-order wrap-around focused it off-screen (measured
   top:-338). Fixed pins it to the viewport. z-index 200 still clears the sticky
   header (100) and the mobile CTA (90) and sits under the lightbox (300); the
   unscrolled first-tab case is unchanged. (S3, 2026-08-25) */
.skip{position:fixed;left:-9999px;top:0;background:var(--black);color:var(--cream);padding:.75rem 1.2rem;z-index:200}
.skip:focus{left:.5rem;top:.5rem}

/* ---------- Buttons ---------- */
.btn{
  display:inline-flex;align-items:center;gap:.6rem;
  font-family:var(--sans);font-weight:700;font-size:.95rem;letter-spacing:.01em;
  padding:.92rem 1.6rem;border-radius:var(--radius);border:1px solid transparent;
  transition:transform .25s var(--ease),box-shadow .25s var(--ease),background .25s,color .25s;
}
.btn .arr{transition:transform .25s var(--ease)}
.btn:hover .arr{transform:translateX(4px)}
.btn-gold{background:linear-gradient(180deg,var(--gold-bright),var(--gold));color:#1a1410;box-shadow:0 12px 30px -14px rgba(200,150,46,.8)}
.btn-gold:hover{box-shadow:0 18px 40px -14px rgba(200,150,46,.95),0 0 0 1px var(--gold-bright);transform:translateY(-2px)}
.btn-ghost{border-color:currentColor;color:var(--black)}
.btn-ghost:hover{background:var(--black);color:var(--cream);transform:translateY(-2px)}
.dark .btn-ghost,.hero .btn-ghost,.page-hero .btn-ghost,.cta-band .btn-ghost{color:var(--cream)}
.dark .btn-ghost:hover,.hero .btn-ghost:hover,.page-hero .btn-ghost:hover,.cta-band .btn-ghost:hover{background:var(--cream);color:var(--black)}
.btn-lg{padding:1.05rem 1.9rem;font-size:1rem}
/* press — settle the hover lift back down; completes the tactile loop */
.btn:active{transform:translateY(0) scale(.98);transition-duration:var(--dur-press)}

.textlink{display:inline-flex;align-items:center;gap:.45rem;font-weight:700;color:var(--gold-text);
  border-bottom:1px solid transparent;transition:border-color var(--dur-quick)}
.textlink .arr{transition:transform .25s var(--ease)}
.textlink:hover{border-color:var(--gold)}
.textlink:hover .arr{transform:translateX(4px)}

/* ---------- Header ---------- */
.site-header{
  position:sticky;top:0;z-index:100;
  background:rgba(250,246,238,.86);backdrop-filter:saturate(140%) blur(10px);
  border-bottom:1px solid var(--line-soft);
  transition:box-shadow var(--dur-quick) var(--ease),background var(--dur-quick);
}
/* barely-there elevation once scrolled (class toggled in main.js) */
.site-header.scrolled{background:rgba(250,246,238,.94);box-shadow:0 10px 28px -22px rgba(17,17,16,.4)}
.header-inner{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;height:74px}
.lockup{display:flex;align-items:center;gap:.8rem}
.lockup img{width:46px;height:46px;border-radius:50%;object-fit:contain}
.lockup .wordmark{display:flex;flex-direction:column;line-height:1}
.lockup .wm-name{font-family:var(--serif);font-size:1.22rem;font-weight:600;color:var(--black);letter-spacing:.005em}
.lockup .wm-sub{font-size:.56rem;font-weight:700;letter-spacing:.26em;text-transform:uppercase;color:var(--gold-text);margin-top:3px}

.nav{display:flex;align-items:center;gap:.3rem}
.nav a{font-size:.92rem;font-weight:600;color:var(--ink);padding:.55rem .8rem;border-radius:2px;position:relative;transition:color var(--dur-quick)}
.nav a::after{content:"";position:absolute;left:.8rem;right:.8rem;bottom:.32rem;height:1.5px;background:var(--gold);transform:scaleX(0);transform-origin:left;transition:transform var(--dur-quick) var(--ease)}
.nav a:hover{color:var(--black)}
.nav a:hover::after,.nav a[aria-current="page"]::after{transform:scaleX(1)}
.nav a[aria-current="page"]{color:var(--black)}
.header-cta{margin-left:.6rem}

.nav-toggle{display:none;width:46px;height:46px;border:1px solid var(--line);background:transparent;border-radius:var(--radius);flex-direction:column;gap:5px;align-items:center;justify-content:center}
.nav-toggle span{width:20px;height:2px;background:var(--black);transition:.25s var(--ease)}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}

/* ---------- Hero ---------- */
.hero{position:relative;background:linear-gradient(158deg,#1b1a16 0%,#26241d 100%);color:var(--cream);overflow:hidden}
.hero::before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(50deg,rgba(200,150,46,.045) 0 2px,transparent 2px 24px);pointer-events:none}
.hero::after{content:"";position:absolute;right:-110px;top:-85px;width:520px;height:520px;border:1px solid rgba(200,150,46,.32);border-radius:50%;pointer-events:none}
.hero .wrap{position:relative;display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(2rem,5vw,4rem);align-items:center;padding-block:clamp(3.5rem,8vw,6.5rem)}
.hero h1{color:var(--cream)}
.hero h1 .script{color:var(--gold-bright);position:relative;display:inline-block}
/* the finishing stroke — a gold hairline laid under "lasts"; static by default, drawn in when motion is allowed */
.hero h1 .script::after{content:"";position:absolute;left:.04em;right:.1em;bottom:-.02em;height:2px;border-radius:1px;transform-origin:left;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold-bright) 62%,var(--gold))}
.hero .lead{color:#c4bdac;margin-top:1.4rem;max-width:34ch}
.hero-cta{display:flex;flex-wrap:wrap;gap:1rem;margin-top:2.2rem}
.hero-meta{display:flex;flex-wrap:wrap;gap:1.8rem;margin-top:2.6rem;padding-top:1.6rem;border-top:1px solid rgba(200,150,46,.22)}
.hero-meta div{display:flex;flex-direction:column;gap:.2rem}
.hero-meta .k{font-family:var(--serif);font-size:1.05rem;color:var(--gold-bright)}
.hero-meta .v{font-size:.78rem;color:#a59f8f;letter-spacing:.02em}
.hero-art{position:relative;aspect-ratio:4/5;border:1px solid rgba(200,150,46,.3);border-radius:var(--radius);overflow:hidden}

/* ---------- Placeholder imagery ---------- */
.ph{
  position:relative;overflow:hidden;background:
    repeating-linear-gradient(45deg,#ece3d2 0 12px,#e3d9c5 12px 24px);
  display:flex;align-items:center;justify-content:center;color:#a4906a;
}
.ph.dark{background:repeating-linear-gradient(45deg,#22211c 0 12px,#1a1915 12px 24px);color:#7d6f4e}
.ph .ph-label{font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;padding:.4rem .7rem;border:1px dashed currentColor;border-radius:2px;background:rgba(255,255,255,.35);text-align:center}
.ph.dark .ph-label{background:rgba(0,0,0,.25)}
/* Real photography dropped into placeholder slots */
.split-media,.hero-art{position:relative}
.split-media>.ph,.hero-art>.ph{position:absolute;inset:0}
.ph>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;border:0}

/* ---------- Service cards ---------- */
.grid{display:grid;gap:clamp(1.2rem,2.5vw,1.8rem)}
.grid-3{grid-template-columns:repeat(3,1fr)}
.grid-2{grid-template-columns:repeat(2,1fr)}
.card{
  background:var(--paper);border:1px solid var(--line-soft);border-radius:var(--radius);
  overflow:hidden;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;
  transition:transform .35s var(--ease),box-shadow .35s var(--ease);
}
.card:hover{transform:translateY(-5px);box-shadow:var(--shadow)}
.card .ph{aspect-ratio:3/2}
/* media hover — restrained zoom/settle; pointers only, motion allowed only.
   Packet 04 states the contract this rule now serves: the zoom is the PROMISE,
   and the lightbox pays it — every element that lifts here is a real photograph
   wrapped in an enrolled anchor (packet 04, "Held to the light"). Illustration
   stays mute: nothing that cannot be held to the light may zoom on hover. */
@media (pointer:fine) and (prefers-reduced-motion:no-preference){
  .card .ph img,.gallery-grid .tile img{transition:transform var(--dur-standard) var(--ease)}
  .card:hover .ph img,.gallery-grid .tile:hover img{transform:scale(1.04)}
  .case-fig img{transition:transform var(--dur-standard) var(--ease)}
  /* packet 04: the gallery joins the unified promise — one zoom, one payoff */
  .case-fig:hover img{transform:scale(1.04)}
  /* gallery CTA cell — the gold rule extends 34px→64px and the arrow nudges;
     scaleX on a transform-origin:left element keeps it on the compositor */
  .tile-cta .tile-cta-rule,.tile-cta .arr{transition:transform var(--dur-quick) var(--ease)}
  .tile-cta:hover .tile-cta-rule{transform:scaleX(1.882)}
  .tile-cta:hover .arr{transform:translateX(4px)}
}
.card-body{padding:1.6rem 1.7rem 1.8rem;display:flex;flex-direction:column;flex:1}
.card-rule{width:30px;height:2px;background:var(--gold);margin-bottom:1rem}
.card h3{margin-bottom:.55rem}
.card p{color:var(--muted);font-size:.98rem;margin-bottom:1.2rem}
.card .textlink{margin-top:auto}

/* ---------- Why-choose ledger (home only — .why scope) ----------
   Was a 5-across strip whose 1px "gap" hairlines used --line (black-alpha),
   i.e. invisible on --black. The ledger reads as rows; the rules between them
   are cream-alpha and actually render. */
.why .section-head{max-width:none}
.values{display:block}
.value{
  display:grid;
  grid-template-columns:5.5rem minmax(19rem,23rem) 1fr;
  column-gap:clamp(1.5rem,3vw,3rem);
  align-items:start;
  padding-block:clamp(1.4rem,2vw,1.9rem);
  position:relative;
}
.value .num{
  font-family:var(--serif);font-style:italic;color:var(--gold);
  font-size:clamp(1.9rem,2.6vw,2.5rem);line-height:.9;
  justify-self:end;
  /* optical: pulls the numeral's cap onto the h3 cap line (Playfair italic
     sits high in its box against Mulish at 1.3rem) */
  /* measured: lands the numeral's projected cap line on the h3 cap line
     (was 2.33px high at .16rem; 1440 residual −0.2px) */
  transform:translateY(.3rem);
}
.value h3{font-size:1.3rem;color:var(--cream);margin:0}
.value p{font-size:1rem;line-height:1.6;color:#a59f8f;max-width:58ch;margin:0}
/* R1 — the ledger is framed: a rule between every row PLUS a top edge above
   row i and a bottom edge below row v, so the spec-sheet closes at both ends */
.values .value::before,
.values .value:last-child::after{content:"";position:absolute;left:0;right:0;height:1px;background:rgba(250,246,238,.14)}
.values .value::before{top:0}
.values .value:last-child::after{bottom:0}

/* ---------- Process ---------- */
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(1.2rem,2.5vw,2rem);counter-reset:step}
.step{position:relative;padding-top:2.6rem}
.step::before{counter-increment:step;content:"0" counter(step);position:absolute;top:0;left:0;font-family:var(--serif);font-size:2.3rem;color:var(--gold);opacity:.55;line-height:1}
.step h3{font-size:1.18rem;margin-bottom:.5rem}
.step p{font-size:.92rem;color:var(--muted)}
.step::after{content:"";position:absolute;top:1.1rem;left:3.3rem;right:-1rem;height:1px;background:var(--line)}
.steps .step:last-child::after{display:none}

/* ---------- Before / After slider — "reveal the finish" ----------
   Driven by a single --pos custom property (0–100). The before layer
   is clipped (paint-only), the handle rides a full-width rail via
   transform — no layout properties are ever animated. The reveal edge
   is a feathered gold "wet paint" edge rather than a hard seam. */
/* touch-action:pan-y — the browser keeps vertical scrolling, we take horizontal.
   Snag A4: touch-action:none previously swallowed the vertical swipe, so a thumb
   landing on a slider could not scroll the page (526px of /gallery/ was dead). */
.ba{position:relative;aspect-ratio:16/10;border-radius:var(--radius);overflow:hidden;user-select:none;touch-action:pan-y;box-shadow:var(--shadow-sm);border:1px solid var(--line-soft);--pos:50;cursor:ew-resize}
.ba .ba-img{position:absolute;inset:0}
.ba .ba-after{z-index:1}
.ba .ba-before{z-index:2;clip-path:inset(0 calc(100% - var(--pos)*1%) 0 0)}
.ba .ba-tag{position:absolute;top:1rem;z-index:3;font-family:var(--mono);font-size:.66rem;letter-spacing:.16em;text-transform:uppercase;background:rgba(17,17,16,.78);color:var(--cream);padding:.35rem .65rem;border-radius:2px;transition:opacity .35s var(--ease),transform .35s var(--ease)}
.ba .ba-tag.before{left:1rem}
.ba .ba-tag.after{right:1rem}
.ba.hide-before .ba-tag.before{opacity:0;transform:translateX(-8px)}
.ba.hide-after .ba-tag.after{opacity:0;transform:translateX(8px)}
/* full-width rail; centre of the rail is the seam, moved by transform only */
.ba .ba-handle{position:absolute;top:0;bottom:0;left:0;width:100%;z-index:4;pointer-events:none;display:flex;align-items:center;justify-content:center;transform:translateX(calc((var(--pos) - 50)*1%))}
.ba .ba-handle::before{content:"";position:absolute;left:50%;top:0;bottom:0;width:2px;margin-left:-1px;
  background:linear-gradient(180deg,var(--gold-bright),var(--gold) 55%,var(--gold-deep));box-shadow:0 0 14px rgba(200,150,46,.5)}
/* feathered gold edge trailing the seam over the "before" side */
.ba .ba-handle::after{content:"";position:absolute;right:50%;top:0;bottom:0;width:38px;margin-right:1px;
  background:linear-gradient(90deg,rgba(200,150,46,0),rgba(200,150,46,.12) 48%,rgba(227,185,92,.38) 90%,rgba(227,185,92,.7))}
.ba .ba-knob{width:44px;height:44px;border-radius:50%;background:var(--gold);border:2px solid var(--cream);display:flex;align-items:center;justify-content:center;color:#1a1410;font-size:.9rem;font-weight:700;box-shadow:0 6px 18px -6px rgba(0,0,0,.6);position:relative;transition:transform .25s var(--ease),box-shadow .25s var(--ease)}
.ba.dragging .ba-knob{transform:scale(1.08);box-shadow:0 10px 24px -8px rgba(0,0,0,.7),0 0 0 6px rgba(200,150,46,.22)}
.ba input[type=range]{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:ew-resize;z-index:5;margin:0}
/* visible keyboard focus on the knob itself (REVIEW Bucket A #4) */
@supports selector(:has(*)){
  .ba:has(input[type=range]:focus-visible){outline:none}
  .ba input[type=range]:focus-visible{outline:none}
  .ba:has(input[type=range]:focus-visible) .ba-knob{outline:2.5px solid var(--cream);outline-offset:2px;box-shadow:0 0 0 6px rgba(200,150,46,.55),0 6px 18px -6px rgba(0,0,0,.6)}
}
/* first-visit affordance cue — injected by JS, fades on first touch */
.ba .ba-cue{position:absolute;left:50%;bottom:1rem;transform:translateX(-50%);z-index:3;white-space:nowrap;pointer-events:none;
  font-family:var(--mono);font-size:.66rem;letter-spacing:.16em;text-transform:uppercase;
  background:rgba(17,17,16,.78);color:var(--gold-bright);padding:.38rem .7rem;border-radius:2px;
  opacity:0;transition:opacity .5s var(--ease)}
.ba.cue-on .ba-cue{opacity:1}
.ba.touched .ba-cue{opacity:0}
@media (prefers-reduced-motion:reduce){
  .ba .ba-tag,.ba .ba-knob,.ba .ba-cue{transition:none}
}

/* ---------- Gallery grid ---------- */
.gallery-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;grid-auto-rows:1fr}
/* scroll-margin-top: these tiles carry no id, so they miss the [id] rule above;
   without it keyboard focus scrolls them under the sticky header. (F7) */
.gallery-grid .tile{position:relative;aspect-ratio:1;border-radius:var(--radius);overflow:hidden;scroll-margin-top:92px}
.gallery-grid .tile.tall{grid-row:span 2;aspect-ratio:auto}
.gallery-grid .ph{position:absolute;inset:0}

/* The gallery CTA is the ninth row-unit, not a button under the grid — it fills
   the r3/c2 hole at three columns. Inherits .tile (square, radius, clipped).
   Quiet by design: it must sit under the photographs, not compete with them. */
.tile-cta{
  background:var(--black);
  display:grid;
  grid-template-columns:auto auto 1fr;
  grid-template-rows:1fr auto auto;   /* row 1 is the spacer that bottom-anchors */
  padding:1.6rem;
}
.tile-cta .tile-cta-rule{
  grid-row:2;grid-column:1/-1;justify-self:start;
  width:34px;height:1px;background:var(--gold);margin-bottom:.85rem;
  transform-origin:left;
}
.tile-cta .tile-cta-label{
  grid-row:3;grid-column:1;
  color:var(--cream);font-weight:600;font-size:.98rem;line-height:1.3;
}
.tile-cta .arr{grid-row:3;grid-column:2;color:var(--gold);margin-left:.5rem;align-self:center}
/* Non-motion hover feedback so reduced-motion users still get a response:
   a color step, applied for all fine pointers (the motion variant above only
   adds the rule-extend/arrow-nudge on top of it). */
@media (pointer:fine){
  .tile-cta:hover .tile-cta-rule{background:var(--gold-bright)}
  .tile-cta:hover .arr{color:var(--gold-bright)}
}

/* ---------- Gallery case rows ---------- */
.case-rows{margin-top:clamp(2rem,4vw,3.2rem)}
.case-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--s3);margin-block:var(--s5)}
.case-row:first-child{margin-top:0}
.case-fig{position:relative;border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm)}
.case-fig img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.case-feat{grid-column:1/span 7;grid-row:1;aspect-ratio:4/3}
.case-sup{grid-column:8/span 5;grid-row:1}
.case-cap{grid-column:1/span 7;grid-row:2;margin-top:var(--s2)}
.case-row.right .case-feat{grid-column:6/span 7}
.case-row.right .case-sup{grid-column:1/span 5}
.case-row.right .case-cap{grid-column:6/span 7}
.case-stack{grid-column:8/span 5;grid-row:1;display:grid;grid-auto-rows:1fr;gap:var(--s3)}
.case-stack img[src*="gal-exterior-front"]{object-position:50% 66%}
.case-stack img[src*="gal-restored-door"]{object-position:58% 82%}
.case-cap .case-rule{display:block;width:44px;height:2px;background:var(--gold);margin-bottom:var(--s1)}
.case-cap .eyebrow::before{display:none}
.case-title{font-family:var(--serif);font-weight:500;font-size:1.35rem;color:var(--ink);margin:.35em 0 .25em}
.case-desc{font-size:1rem;color:var(--muted);max-width:52ch}

/* ---------- Split feature ---------- */
.split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,5vw,4.5rem);align-items:center}
.split.flip .split-media{order:2}
.split-media{aspect-ratio:4/3;border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.checklist{display:grid;gap:.85rem;margin-top:1.6rem}
.checklist li{display:flex;gap:.8rem;align-items:flex-start;font-size:1rem;color:var(--ink)}
.checklist li::before{content:"";flex:none;width:22px;height:22px;margin-top:2px;border-radius:50%;background:rgba(200,150,46,.16);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.2l2.6 2.6L10 3.2' fill='none' stroke='%23a87a22' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center}

/* ---------- CTA band ---------- */
.cta-band{position:relative;background:var(--black);color:var(--cream);border-radius:var(--radius);overflow:hidden;padding:clamp(2.5rem,5vw,4rem);text-align:center}
.cta-band::before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(50deg,rgba(200,150,46,.05) 0 2px,transparent 2px 24px)}
.cta-band > *{position:relative}
.cta-band h2{color:var(--cream)}
.cta-band p{color:#bdb6a5;max-width:48ch;margin:1rem auto 2rem}
.cta-actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}

/* ---------- Forms ---------- */
.quote-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(2rem,4vw,3.5rem);align-items:start}
.form{background:var(--paper);border:1px solid var(--line-soft);border-radius:var(--radius);padding:clamp(1.5rem,3vw,2.4rem);box-shadow:var(--shadow-sm)}
.field{margin-bottom:1.15rem;display:flex;flex-direction:column;gap:.4rem}
.field label{font-family:var(--serif);font-weight:500;font-size:1.05rem;color:var(--ink)}
.field label .req{color:var(--gold-text)}
/* job-sheet header + row numerals */
.form-head{font-size:.72rem;font-weight:700;letter-spacing:.32em;text-transform:uppercase;color:var(--gold-text);
  padding-bottom:var(--s2);margin-bottom:var(--s3);border-bottom:1px solid var(--line)}
.qnum{font-family:var(--serif);font-weight:500;font-size:1rem;color:var(--gold-deep);line-height:1}
@media (min-width:700px){
  .qrow{display:grid;grid-template-columns:2.2rem 1fr;align-items:baseline}
}
@media (max-width:699px){.qnum{display:block;margin-bottom:.3rem}}
.field input,.field select,.field textarea{
  font-family:var(--sans);font-size:1rem;color:var(--black);
  padding:.8rem 0;border:0;border-bottom:1px solid rgba(38,36,31,.25);border-radius:0;background:transparent;
  transition:border-color var(--dur-quick) var(--ease);width:100%;
}
.field input::placeholder,.field textarea::placeholder{color:var(--muted);opacity:1}
.field input:focus,.field select:focus,.field textarea:focus{border-bottom-width:2px;border-bottom-color:var(--gold)}
.field textarea{resize:vertical;min-height:120px;line-height:1.65em;
  background-image:repeating-linear-gradient(transparent,transparent calc(1.65em - 1px),rgba(38,36,31,.12) calc(1.65em - 1px),rgba(38,36,31,.12) 1.65em);
  background-attachment:local}

/* inline validation — border + message together; colour is never the only signal */
.field-error{
  font-size:.8rem;font-weight:600;color:var(--error);min-height:1.15em;
  opacity:0;transform:translateY(-2px);
  transition:opacity var(--dur-quick) var(--ease),transform var(--dur-quick) var(--ease);
}
.field input:user-invalid,.field select:user-invalid,.field textarea:user-invalid{border-bottom-color:var(--error)}
.field input:user-invalid:focus,.field textarea:user-invalid:focus{border-bottom-width:2px;border-bottom-color:var(--error)}
.field input:user-invalid ~ .field-error,.field textarea:user-invalid ~ .field-error{opacity:1;transform:none}

/* submit pending — set by JS; native POST proceeds untouched */
.btn[aria-busy="true"]{pointer-events:none;opacity:.85}
.btn .spin{
  width:.9em;height:.9em;flex:none;border-radius:50%;
  border:2px solid currentColor;border-right-color:transparent;
  animation:jds-spin .7s linear infinite;
}
@keyframes jds-spin{to{transform:rotate(360deg)}}

/* success banner — the one sanctioned spring confirmation */
@media (prefers-reduced-motion:no-preference){
  #form-success:not([hidden]){animation:jds-rise var(--dur-standard) var(--ease-spring) both}
}
@keyframes jds-rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.form-note{font-size:.8rem;color:var(--muted);margin-top:.4rem}
.contact-aside .aside-card{background:var(--black);color:var(--cream);border-radius:var(--radius);padding:1.8rem;margin-bottom:1.2rem}
.contact-aside .aside-card h3{color:var(--cream);margin-bottom:1rem}
.contact-line{display:flex;gap:.9rem;align-items:flex-start;padding:.7rem 0;border-top:1px solid rgba(255,255,255,.1)}
.contact-line:first-of-type{border-top:none}
.contact-line .ic{color:var(--gold);flex:none;margin-top:3px}
/* Snags A12/A25: the icon set was text dingbats (&#9633; rendered as tofu on
   every page). Inline SVG on currentColor, so it inherits the gold system. */
.ic-svg{display:block;flex:none}
.contact-line .lbl{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:#a59f8f}
.contact-line a,.contact-line span.val{display:block;color:var(--cream);font-weight:600;font-size:1.02rem}

/* ---------- Placeholder flag pill ---------- */
.flag{display:inline-flex;align-items:center;gap:.4rem;font-family:var(--mono);font-size:.7rem;font-weight:700;
  letter-spacing:.08em;color:var(--gold-text);background:rgba(200,150,46,.12);border:1px dashed var(--gold);
  padding:.18rem .55rem;border-radius:2px;text-transform:uppercase;vertical-align:middle}

/* ---------- Areas ---------- */
.area-list{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.area-list a{background:var(--cream);padding:1.15rem 1.4rem;display:flex;align-items:center;justify-content:space-between;font-weight:600;color:var(--ink);transition:background var(--dur-quick),color var(--dur-quick)}
.area-list a:hover{background:var(--black);color:var(--cream)}
.area-list a .arr{color:var(--gold);transition:transform var(--dur-standard) var(--ease)}
.area-list a:hover .arr{transform:translateX(4px)}

/* ---------- Footer ---------- */
.site-footer{background:var(--black);color:#b8b1a0}
.footer-top{display:grid;grid-template-columns:1.4fr 1fr 1fr 1.2fr;gap:2.5rem;padding-block:clamp(3rem,5vw,4.5rem)}
/* h2, not h4: the footer headings followed h1/h2 page content, skipping a level
   on every page. This rule re-declares every visual property, so the retag is
   pixel-identical. (F8, 2026-08-25) */
.site-footer h2{color:var(--cream);font-family:var(--sans);font-size:.78rem;letter-spacing:.18em;text-transform:uppercase;margin-bottom:1.1rem;font-weight:700}
.site-footer .f-lockup{display:flex;align-items:center;gap:.8rem;margin-bottom:1.2rem}
.site-footer .f-lockup img{width:50px;height:50px;border-radius:50%}
/* Snag A17: .lockup .wordmark (line 144) is scoped to the header, so the footer
   wordmark never stacked and the two spans collided inline at desktop widths. */
.site-footer .f-lockup .wordmark{display:flex;flex-direction:column;line-height:1}
.site-footer .f-lockup .wm-name{font-family:var(--serif);font-size:1.3rem;color:var(--cream)}
.site-footer .f-lockup .wm-sub{font-size:.56rem;letter-spacing:.24em;text-transform:uppercase;color:var(--gold);font-weight:700;margin-top:3px}
.site-footer .f-about{font-size:.92rem;line-height:1.7;max-width:34ch}
.site-footer ul li{margin-bottom:.6rem}
.site-footer ul a{font-size:.95rem;transition:color var(--dur-quick)}
.site-footer ul a:hover{color:var(--gold-bright)}
.footer-bottom{border-top:1px solid rgba(255,255,255,.1);padding-block:1.4rem;display:flex;flex-wrap:wrap;gap:1rem;justify-content:space-between;align-items:center;font-size:.82rem;color:#8a8576}
.footer-bottom a{transition:color var(--dur-quick)}
.footer-bottom a:hover{color:var(--gold-bright)}
.ig-link{display:inline-flex;align-items:center;gap:.5rem;color:var(--cream);font-weight:600}
.ig-link:hover{color:var(--gold-bright)}

/* ---------- Sticky mobile CTA ---------- */
.mobile-cta{position:fixed;left:0;right:0;bottom:0;z-index:90;display:none;gap:.6rem;padding:.6rem;background:rgba(17,17,16,.96);backdrop-filter:blur(8px);border-top:1px solid rgba(200,150,46,.3)}
.mobile-cta .btn{flex:1;justify-content:center;padding:.85rem}
.mobile-cta .btn-call{background:transparent;border:1px solid var(--gold);color:var(--gold-bright)}

/* ---------- Spacing shims (snag A20) ----------
   176 inline style="margin-top:…" attributes were hand-nudging vertical rhythm
   across all 23 pages (9 per town page alone). These classes hold the exact
   same values — nothing moved a pixel — but the rhythm is now in the stylesheet
   where it can be reasoned about. Named by value (mt-04 = .4rem, mt-10 = 1rem)
   so no false hierarchy is implied.
   TRANSITIONAL: these values sit outside the --s1…--s8 token scale. Deriving
   the rhythm from tokens is a design decision and belongs to the improvement
   packets, not to a snag pass. */
.mt-04{margin-top:.4rem}
.mt-06{margin-top:.6rem}
.mt-08{margin-top:.8rem}
.mt-10{margin-top:1rem}
.mt-11{margin-top:1.1rem}
.mt-12{margin-top:1.2rem}
.mt-14{margin-top:1.4rem}
.mt-16{margin-top:1.6rem}
.mt-18{margin-top:1.8rem}

/* ---------- Reveal animation ---------- */
.reveal{transition:opacity var(--dur-slow) var(--ease),transform var(--dur-slow) var(--ease)}
.js .reveal{opacity:0;transform:translateY(22px)}
.js .reveal.in{opacity:1;transform:none}
.reveal.d1{transition-delay:.08s}.reveal.d2{transition-delay:.16s}.reveal.d3{transition-delay:.24s}.reveal.d4{transition-delay:.32s}

/* per-child stagger — .values / .steps / .gallery-grid arrive in sequence,
   not as one block. 80ms unit, capped at 6 steps. Rides the existing
   observer: the group's .in flips, children carry the motion. */
.js .values.reveal,.js .steps.reveal,.js .gallery-grid.reveal,.js .case-row.reveal{opacity:1;transform:none}
.js .values.reveal>*,.js .steps.reveal>*,.js .gallery-grid.reveal>*,.js .case-row.reveal>*{
  opacity:0;transform:translateY(14px);
  transition:opacity var(--dur-slow) var(--ease),transform var(--dur-slow) var(--ease);
}
.js .values.reveal.in>*,.js .steps.reveal.in>*,.js .gallery-grid.reveal.in>*,.js .case-row.reveal.in>*{opacity:1;transform:none}
.js .values.reveal.in>:nth-child(2),.js .steps.reveal.in>:nth-child(2),.js .gallery-grid.reveal.in>:nth-child(2),.js .case-row.reveal.in>:nth-child(2){transition-delay:.08s}
.js .values.reveal.in>:nth-child(3),.js .steps.reveal.in>:nth-child(3),.js .gallery-grid.reveal.in>:nth-child(3),.js .case-row.reveal.in>:nth-child(3){transition-delay:.16s}
.js .values.reveal.in>:nth-child(4),.js .steps.reveal.in>:nth-child(4),.js .gallery-grid.reveal.in>:nth-child(4){transition-delay:.24s}
.js .values.reveal.in>:nth-child(5),.js .steps.reveal.in>:nth-child(5),.js .gallery-grid.reveal.in>:nth-child(5){transition-delay:.32s}
.js .values.reveal.in>:nth-child(n+6),.js .steps.reveal.in>:nth-child(n+6),.js .gallery-grid.reveal.in>:nth-child(n+6){transition-delay:.4s}
@media (prefers-reduced-motion:reduce){
  .reveal,.js .reveal{opacity:1;transform:none;transition:none}
  .js .values.reveal>*,.js .steps.reveal>*,.js .gallery-grid.reveal>*,.js .case-row.reveal>*{opacity:1;transform:none;transition:none}
  .btn,.card,.textlink .arr,.btn .arr{transition:none}
  .field-error{transition:none;transform:none}
  .field input,.field select,.field textarea{transition:none}
  .btn .spin{display:none}
  .site-header .nav{transition:none;transform:none}
  .site-header{transition:none}
}

/* ============================================================
   Signature moment 1 — "The finishing stroke" hero entrance.
   One gold line conducts the hero: the eyebrow hairline draws,
   the copy settles in its wake, a gold stroke is laid under
   "lasts" and holds, the photo is revealed by a wipe with a
   gold leading edge and one pass of light. CSS only; opacity &
   transform only; static composition when motion is reduced.
   ============================================================ */
@media (prefers-reduced-motion:no-preference){
  .js .hero-copy .eyebrow{animation:hero-rise .5s var(--ease) .05s both}
  .js .hero-copy .eyebrow::before{animation:hero-draw .55s var(--ease) .15s both}
  .js .hero-copy h1{animation:hero-rise .65s var(--ease) .2s both}
  .js .hero-copy .lead{animation:hero-rise .6s var(--ease) .34s both}
  .js .hero-copy .hero-cta{animation:hero-rise .6s var(--ease) .44s both}
  .js .hero-copy .hero-meta{animation:hero-rise .6s var(--ease) .54s both}
  /* the stroke under "lasts" — the beat that names the brand line */
  .js .hero h1 .script::after{animation:hero-draw .5s var(--ease) .95s both}
  /* pinstripe breathes in; the ring settles and holds */
  .js .hero::before{animation:hero-fade 1.1s ease .25s both}
  .js .hero::after{animation:hero-ring 1s var(--ease) .55s both}
  /* photo reveal: a wipe with a gold leading edge, then one sheen pass.
     Overlays only — the image itself paints immediately (LCP-safe). */
  .js .hero-art .ph::after{content:"";position:absolute;inset:-1px;z-index:3;pointer-events:none;
    background:linear-gradient(100deg,#1f1e18 0 76%,rgba(200,150,46,.5) 94%,rgba(227,185,92,.95) 100%);
    animation:hero-curtain .85s var(--ease) .35s both}
  .js .hero-art .ph::before{content:"";position:absolute;inset:0;z-index:4;pointer-events:none;
    background:linear-gradient(115deg,transparent 40%,rgba(255,246,225,.28) 50%,transparent 60%);
    transform:translateX(-130%);animation:hero-sheen 1.1s ease 1.25s both}
}
@keyframes hero-rise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
@keyframes hero-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@keyframes hero-fade{from{opacity:0}to{opacity:1}}
@keyframes hero-ring{from{opacity:0;transform:scale(.955)}to{opacity:1;transform:none}}
@keyframes hero-curtain{to{transform:translateX(103%)}}
@keyframes hero-sheen{0%{transform:translateX(-130%)}100%{transform:translateX(130%)}}

/* ============================================================
   Signature moment 3 — the gold line signs the page.
   The hero's drawn-hairline motif recurs at exactly two beats:
   section-head eyebrow rules draw in as the head reveals, and
   the process connector draws across as the steps arrive.
   Pure CSS riding the existing .reveal IntersectionObserver;
   static (fully drawn) under reduced motion or without JS.
   ============================================================ */
@media (prefers-reduced-motion:no-preference){
  .js .section-head .eyebrow::before{transform:scaleX(0);transform-origin:right;transition:transform .7s var(--ease) .25s}
  .js .section-head .eyebrow::after{transform:scaleX(0);transform-origin:left;transition:transform .7s var(--ease) .25s}
  .js .section-head.reveal.in .eyebrow::before,
  .js .section-head.reveal.in .eyebrow::after{transform:scaleX(1)}
  .js .steps .step::after{transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease)}
  .js .steps .step:nth-child(1)::after{transition-delay:.2s}
  .js .steps .step:nth-child(2)::after{transition-delay:.42s}
  .js .steps .step:nth-child(3)::after{transition-delay:.64s}
  .js .steps.reveal.in .step::after{transform:scaleX(1)}
  /* Why-JdS ledger rules draw in behind their own row's stagger, so line and
     row read as one gesture. Rows stagger .08s apart (see per-child block). */
  .js .values .value::before,
  .js .values .value:last-child::after{transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease)}
  .js .values .value:first-child::before{transition-delay:.2s}
  .js .values .value:nth-child(2)::before{transition-delay:.28s}
  .js .values .value:nth-child(3)::before{transition-delay:.36s}
  .js .values .value:nth-child(4)::before{transition-delay:.44s}
  .js .values .value:nth-child(5)::before{transition-delay:.52s}
  .js .values .value:last-child::after{transition-delay:.6s}
  .js .values.reveal.in .value::before,
  .js .values.reveal.in .value:last-child::after{transform:scaleX(1)}
}

/* ---------- Breadcrumb / page hero ---------- */
.page-hero{background:linear-gradient(158deg,#1b1a16 0%,#26241d 100%);color:var(--cream);position:relative;overflow:hidden}
.page-hero::before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(50deg,rgba(200,150,46,.04) 0 2px,transparent 2px 24px)}
.page-hero .wrap{position:relative;padding-block:clamp(2.8rem,6vw,4.5rem)}
.crumbs{font-size:.8rem;color:#a59f8f;margin-bottom:1rem;display:flex;gap:.5rem;flex-wrap:wrap}
.crumbs a{transition:color var(--dur-quick)}
.crumbs a:hover{color:var(--gold-bright)}
.crumbs .sep{color:var(--gold-deep)}
.page-hero h1{color:var(--cream);max-width:18ch}
.page-hero .lead{color:#c4bdac;margin-top:1.1rem;max-width:60ch}

/* ============================================================
   Signature moment 4 — "Cut in" (commission 2026-08-16, M1).
   The band's right portion becomes a photographic panel revealed
   behind a canted cut running bottom-left -> top-right, wider at
   the bottom, with a 1.5px gold hairline laid along it. The cut
   runs counter to the 50deg pinstripe — same 40deg family,
   mirrored — a deliberate counterpoint, not an echo.
   Entrance: one beat. A band-surface veil slides off along the
   cut, gold hairline leading, copy rising in its wake.
   CSS only, GPU transforms only, no JS. Composed static state
   under reduced motion and with JS absent.
   ============================================================ */
.page-hero--media{
  --jds-cut:48%;            /* cut's intercept at the band's bottom edge */
  --jds-wipe-x:calc(100% - 91px);  /* veil travel, panel-local */
  --jds-wipe-y:-180px;
  min-height:463px;
}
/* A-i: shared eyebrow at :87 keeps --gold-deep; the band needs AA over both
   gradient stops. Later-cascade override, page-hero only. */
.page-hero .eyebrow{color:var(--gold)}
.page-hero--media .wrap{position:relative;z-index:2}
/* the approved composition sets the lede at 46ch; 60ch runs into the cut */
.page-hero--media .lead{max-width:46ch}
.page-hero__cta{margin-top:1.6rem}

.page-hero__media{position:absolute;inset:0;z-index:1}
.page-hero__frame{
  position:absolute;top:0;bottom:0;left:var(--jds-cut);right:0;
  overflow:hidden;
  transform:skewX(-40deg);transform-origin:0 100%;
}
.page-hero__frame img{
  position:absolute;inset:0;width:100%;height:100%;max-width:none;
  object-fit:cover;object-position:50% 50%;
  transform:skewX(40deg);transform-origin:0 100%;
}
/* the permanent hairline, laid on the cut itself */
.page-hero__media::after{
  content:"";position:absolute;top:0;bottom:0;left:var(--jds-cut);width:1.5px;z-index:2;
  background:var(--gold);box-shadow:0 0 24px rgba(200,150,46,.25);
  transform:skewX(-40deg);transform-origin:0 100%;
}
/* proof chip — never skewed, only ever over real proof photography */
.page-hero__proof{
  position:absolute;z-index:3;bottom:28px;left:calc(var(--jds-cut) + 90px);
  font-family:var(--sans);font-size:.72rem;font-weight:700;line-height:1.2;
  letter-spacing:.22em;text-transform:uppercase;
  color:var(--cream);background:rgba(17,17,16,.82);
  padding:8px 14px;border-radius:var(--radius);border-left:2px solid var(--gold);
}
/* Veil — the band surface itself, so it is invisible at rest. Gated under
   html.js: with no script to remove it the photo must never be trapped. */
.page-hero__veil{display:none}
.js .page-hero__veil{
  display:block;position:absolute;inset:0;z-index:2;overflow:hidden;pointer-events:none;
  border-left:1.5px solid var(--gold);
  box-shadow:-24px 0 24px -24px rgba(200,150,46,.3);
  will-change:transform;
}
.js .page-hero__veil::before{
  content:"";position:absolute;top:0;bottom:0;
  left:-92.3077%;width:calc(192.3077% + 8px);   /* = the band's own box, so the gradient matches */
  transform:skewX(40deg);transform-origin:0 100%;
  background:repeating-linear-gradient(50deg,rgba(200,150,46,.04) 0 2px,transparent 2px 24px),
             linear-gradient(158deg,#1b1a16 0%,#26241d 100%);
}

/* Entrance — group reveal on the existing observer, same shape as .values:
   the group stays visible, the children carry the motion. */
.js .page-hero.reveal{opacity:1;transform:none}
.js .page-hero.reveal .wrap>*{
  opacity:0;transform:translateY(14px);
  transition:opacity var(--dur-slow) var(--ease),transform var(--dur-slow) var(--ease);
}
.js .page-hero.reveal.in .wrap>*{opacity:1;transform:none}
.js .page-hero.reveal.in .wrap>:nth-child(2){transition-delay:.08s}
.js .page-hero.reveal.in .wrap>:nth-child(3){transition-delay:.16s}
.js .page-hero.reveal.in .wrap>:nth-child(4){transition-delay:.24s}
.js .page-hero.reveal.in .wrap>:nth-child(5){transition-delay:.32s}
.js .page-hero--media.reveal.in .page-hero__veil{
  animation:jds-hero-wipe var(--dur-slow) var(--ease) .15s both;
}
/* the chip belongs to the photo, so it arrives on the same beat — the veil
   cannot cover it (it is nested inside the frame), so it fades on the wipe's
   own clock rather than sitting on the bare band before it starts */
.js .page-hero--media .page-hero__proof{opacity:0}
.js .page-hero--media.reveal.in .page-hero__proof{
  animation:jds-hero-chip var(--dur-slow) var(--ease) .15s both;
}
@keyframes jds-hero-wipe{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(var(--jds-wipe-x),var(--jds-wipe-y),0)}
}
@keyframes jds-hero-chip{from{opacity:0}to{opacity:1}}

/* M2 per-route framing. The C2 Buckingham derivative is portrait (745x900) in a
   landscape panel, so cover fills on width and only the vertical axis has slack;
   this lands the log burner and the glazed barn wall in the panel's visible wedge
   rather than riding its top edge. Class, never an inline style. */
.page-hero__frame img.page-hero__img--mid{object-position:50% 43%}

/* M2 intercept nudge. On the routes with no band CTA the lede is the lowest
   copy element, so it meets the cut at its narrowest point: /gallery/ measured
   57.6px and /about/ 79.6px of copy-to-hairline clearance at 1440, under the
   90px floor. The lede is already capped at 46ch, so the spec's other remedy
   applies — nudge that route's intercept right until it clears. */
.page-hero--cut51{--jds-cut:51%}

/* Compact variant — no media panel (for /areas/ and /privacy/, M3) */
.page-hero--compact .wrap{padding-block:clamp(2rem,4vw,3rem)}

/* M3 — privacy tonal cut. The pattern holds at zero photography: the cant
   reveals no photograph but a deeper tonal field, --black under the same
   50deg gold pinstripe at a higher alpha (.08 vs the band's .04). No new
   colour, no new geometry — the same frame, hairline and veil as the media
   routes. The fill is a counter-skewed pseudo-element using the veil's own
   offsets, so the pinstripe stays on the band's 50deg axis and its stripes
   run continuous across the cut, only deeper on the far side. */
.page-hero--tonal{
  min-height:0;                 /* compact wins over --media's 463px floor */
  --jds-panel-left:-92.3077%;   /* = -cut/(100-cut), so the counter-skewed */
  --jds-panel-w:192.3077%;      /*   fill lands on the band's own box at 48% */
}
.page-hero--tonal.page-hero--cut51{--jds-panel-left:-104.0816%;--jds-panel-w:204.0816%}
.page-hero--tonal .page-hero__frame{background:var(--black)}
.page-hero--tonal .page-hero__frame::before{
  content:"";position:absolute;top:0;bottom:0;
  left:var(--jds-panel-left);width:calc(var(--jds-panel-w) + 8px);
  transform:skewX(40deg);transform-origin:0 100%;
  background:repeating-linear-gradient(50deg,rgba(200,150,46,.08) 0 2px,transparent 2px 24px);
}

/* M3 — 404 centred variant (A-ii). Reproduces the four inline styles the
   band carried; the inline style attributes are removed. Its deliberate
   degenerate case: no cut, no media, no crumbs. */
.page-hero--center .wrap{text-align:center;padding-block:clamp(4rem,10vw,7rem)}
.page-hero--center h1{margin-top:1rem;color:var(--cream)}
.page-hero--center .lead{margin:1rem auto 0;max-width:46ch}
.page-hero--center .cta-actions{
  margin-top:2rem;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;
}

/* ---- Stacked band: the panel becomes a strip at the band's foot.
   The cut stays; the angle adapts to a shallow ~7deg in the same
   "/" direction — left end lower, photo deeper toward the right. ---- */
@media (max-width:899px){
  .page-hero--media{
    --jds-strip:190px;
    --jds-wipe-x:40px;--jds-wipe-y:240px;
    min-height:0;
  }
  .page-hero--media .wrap{padding-block:2.2rem calc(var(--jds-strip) + 1.1rem)}
  .page-hero--media .lead{max-width:none}
  .page-hero__frame{
    left:0;right:0;top:auto;bottom:0;height:var(--jds-strip);
    transform:skewY(-7deg);transform-origin:100% 100%;
  }
  .page-hero__frame img{transform:skewY(7deg);transform-origin:100% 100%}
  .page-hero__media::after{
    left:0;right:0;top:auto;bottom:var(--jds-strip);width:auto;height:1.5px;
    transform:skewY(-7deg);transform-origin:100% 100%;
  }
  .page-hero__proof{left:16px;bottom:16px;letter-spacing:.18em}
  .js .page-hero__veil{
    border-left:0;border-top:1.5px solid var(--gold);
    box-shadow:0 -24px 24px -24px rgba(200,150,46,.3);
  }
  .js .page-hero__veil::before{
    left:0;width:100%;bottom:auto;top:calc(var(--jds-strip) - 560px);height:560px;
    transform:skewY(7deg);transform-origin:100% 100%;
  }
  /* the tonal fill adapts with the strip, same counter-skew as the veil */
  .page-hero--tonal .page-hero__frame::before{
    left:0;width:100%;bottom:auto;top:calc(var(--jds-strip) - 560px);height:560px;
    transform:skewY(7deg);transform-origin:100% 100%;
  }
}
/* the sticky bar already carries the action below this width */
@media (max-width:760px){
  .page-hero__cta{display:none}
}
@media (prefers-reduced-motion:reduce){
  .js .page-hero.reveal .wrap>*{opacity:1;transform:none;transition:none}
  .js .page-hero--media .page-hero__proof,
  .js .page-hero--media.reveal.in .page-hero__proof{opacity:1;animation:none}
  .page-hero__veil,.js .page-hero__veil{display:none;animation:none}
}

/* ---------- Prose ---------- */
.prose p{margin-bottom:1.1rem;color:var(--ink)}
.prose h2{margin-top:2.5rem;margin-bottom:1rem}
.prose h3{margin-top:1.8rem;margin-bottom:.6rem}
.prose ul.bullets{margin:1rem 0 1.5rem;display:grid;gap:.6rem}
.prose ul.bullets li{padding-left:1.5rem;position:relative;color:var(--ink)}
.prose ul.bullets li::before{content:"";position:absolute;left:0;top:.62em;width:7px;height:7px;background:var(--gold);transform:rotate(45deg)}

.aside-box{background:var(--cream-2);border-left:3px solid var(--gold);border-radius:0 var(--radius) var(--radius) 0;padding:1.5rem 1.7rem}
.aside-box h3{font-size:1.15rem;margin-bottom:.6rem}
.aside-box p{font-size:.95rem;color:var(--muted)}

/* ---------- Responsive ---------- */
@media (max-width:899px){
  .case-row,.case-row.right{grid-template-columns:1fr}
  .case-feat,.case-sup,.case-stack,.case-cap,
  .case-row.right .case-feat,.case-row.right .case-sup,.case-row.right .case-cap{grid-column:1;grid-row:auto}
  .case-sup{aspect-ratio:4/3}
  .case-stack{grid-auto-rows:auto}
  .case-stack .case-fig{aspect-ratio:4/3}
}
/* Why-JdS ledger: rows at every width. Below 1080 the third column folds under
   the title, so it is numeral | (title + copy). Placed ahead of the 980/760
   blocks so those can still tune it. */
@media (max-width:1079.98px){
  .value{grid-template-columns:3.4rem 1fr;padding-block:1.3rem}
  .value .num{font-size:1.6rem;transform:translateY(.24rem)}
  /* h3 and p share column 2 and stack; without this the p auto-places into the
     narrow numeral column on row 2 */
  .value h3,.value p{grid-column:2}
  .value h3{font-size:1.2rem;margin-bottom:.45rem}
  /* R2 — capped: uncapped ran to ~89ch at 1024, a future-copy hazard */
  .value p{max-width:62ch}
}
@media (max-width:980px){
  .hero .wrap{grid-template-columns:1fr}
  .hero-art{aspect-ratio:16/10;max-width:640px}
  .grid-3{grid-template-columns:repeat(2,1fr)}
  .steps{grid-template-columns:repeat(2,1fr)}
  .step::after{display:none}
  /* Snag A18: the flip section is authored media-first in the DOM so line 308's
     order:2 can pull the text left at desktop. Resetting order:0 here let the
     photo follow DOM order at 390px and land above its own heading, reading as
     part of the previous section. Keep order:2 at every width — heading leads.
     Packet 03 (2026-08-19): service detail pages (`.svc`) restore desktop
     alternation at >=981px only — the <=980 stack below still leads with the
     heading everywhere, on every route. Do not "fix" that back. */
  .split{grid-template-columns:1fr}
  .split.flip .split-media{order:2}
  .quote-grid{grid-template-columns:1fr}
  .area-list{grid-template-columns:repeat(2,1fr)}
  /* auto rows so the CTA band sizes to its own content; with grid-auto-rows:1fr
     every row (band included) is forced to photo-row height */
  .gallery-grid{grid-template-columns:repeat(2,1fr);grid-auto-rows:auto}
  /* the CTA stops being a square cell and becomes a full-width band, which also
     keeps the photo count even (8 units / 2 cols = 4 clean rows, no orphan) */
  /* .gallery-grid prefix required — plain .tile-cta loses aspect-ratio to
     `.gallery-grid .tile{aspect-ratio:1}` and the band stays square */
  .gallery-grid .tile-cta{grid-column:1/-1;aspect-ratio:auto;
    grid-template-columns:auto auto 1fr;grid-template-rows:auto;align-items:center;
    padding:1.3rem 1.4rem}
  .gallery-grid .tile-cta .tile-cta-rule{grid-row:1;grid-column:1;margin-bottom:0;margin-right:1rem;align-self:center}
  .gallery-grid .tile-cta .tile-cta-label{grid-row:1;grid-column:2}
  .gallery-grid .tile-cta .arr{grid-row:1;grid-column:3;justify-self:end;margin-left:0}
  .footer-top{grid-template-columns:1fr 1fr}
}
@media (max-width:760px){
  body{font-size:17px}
  .header-cta{display:none}
  .nav-toggle{display:flex}
  /* drawer — always rendered; opacity/transform/visibility carry open+close
     so it animates instead of snapping (visibility keeps it unfocusable shut) */
  .nav{display:flex;position:absolute;top:74px;left:0;right:0;flex-direction:column;align-items:stretch;
    background:var(--cream);border-bottom:1px solid var(--line);padding:.8rem;gap:.2rem;box-shadow:var(--shadow-sm);
    opacity:0;transform:translateY(-8px);visibility:hidden;
    transition:opacity var(--dur-standard) var(--ease),transform var(--dur-standard) var(--ease),visibility 0s linear var(--dur-standard)}
  .site-header.open .nav{opacity:1;transform:none;visibility:visible;transition-delay:0s}
  .nav a{padding:.9rem 1rem}
  .nav a::after{display:none}
  .mobile-cta{display:flex}
  /* Snag A11: .mobile-cta computes to ~74px (.6rem padding + .85rem button
     padding + line-height); 64px let the bar clip the last footer row. */
  body{padding-bottom:82px}
  .grid-3,.grid-2{grid-template-columns:1fr}
  .value{grid-template-columns:2.9rem 1fr;padding-block:1.15rem}
  .steps{grid-template-columns:1fr}
  /* split from .area-list: the gallery keeps a two-up mosaic on mobile (the
     single column ran the section to 3,303px); .area-list stays one column */
  .area-list{grid-template-columns:1fr}
  .gallery-grid{grid-template-columns:repeat(2,1fr);gap:.75rem}
  .field-row{grid-template-columns:1fr}
  .footer-top{grid-template-columns:1fr;gap:2rem}
  .hero-meta{gap:1.2rem}
  /* Snag A9: footer links measured 19px tall (Instagram 25px) — the only place
     in the estate where the 44px touch floor was not carried through. The
     sticky bar, drawer and form controls were already correct. */
  .site-footer ul li{margin-bottom:0}
  .site-footer ul a,.site-footer .ig-link{display:flex;align-items:center;min-height:44px}
  .footer-bottom{gap:.25rem 1rem}
  .footer-bottom a{display:inline-flex;align-items:center;min-height:44px}
}

/* ---------- Town local-proof ---------- */
.intro-proof{display:grid;gap:var(--s4);align-items:center}
.intro-fig{display:flex;flex-direction:column;gap:var(--s2)}
.intro-fig .ph{aspect-ratio:4/3;border-radius:var(--radius);box-shadow:var(--shadow-sm)}
.ledger{display:grid}
.ledger a{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:baseline;gap:var(--s3);padding-block:var(--s3);border-top:1px solid var(--line);transition:background var(--dur-quick) var(--ease)}
.ledger a:last-child{border-bottom:1px solid var(--line)}
.ledger .lg-num{font-family:var(--serif);font-size:1.2rem;line-height:1;color:var(--gold-deep)}
.ledger h3{font-family:var(--serif);font-weight:500;font-size:1.3rem;color:var(--ink);margin-bottom:.25rem}
.ledger p{font-size:1rem;color:var(--muted)}
.ledger .textlink{white-space:nowrap}
@media (pointer:fine){.ledger a:hover{background:var(--cream-2)}}
.checklist-grid{grid-template-columns:1fr 1fr;gap:var(--s3) var(--s4)}
@media (min-width:900px){.intro-proof{grid-template-columns:5fr 7fr}}
/* no-figure case (town pages: the proof shot now lives in the band) — one
   column, reading measure held, no phantom grid cell */
.intro-proof--solo{max-width:68ch}
@media (min-width:900px){.intro-proof--solo{grid-template-columns:1fr}}
@media (max-width:899px){
  .ledger a{grid-template-columns:auto 1fr}
  .ledger .textlink{grid-column:2;justify-self:start;margin-top:.6rem}
  .checklist-grid{grid-template-columns:1fr}
}

/* Areas coverage map */
.areas-intro{display:grid;gap:var(--s4);align-items:center}
.area-map{width:100%;max-width:480px;margin-inline:auto}
.area-map svg{display:block;width:100%;height:auto;aspect-ratio:600/520}
@media (min-width:900px){
.areas-intro{grid-template-columns:repeat(12,1fr)}
.area-map{grid-column:1/span 6;max-width:none}
.areas-intro .prose{grid-column:7/span 6}
}
.jds-map .boundary{fill:var(--cream-2);fill-opacity:.35;stroke:var(--gold);stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1543;stroke-dashoffset:0}
.jds-map .county{fill:none;stroke:var(--gold);stroke-width:1;stroke-linecap:round;stroke-dasharray:3 5;opacity:.45}
.jds-map .seal-core{fill:var(--gold)}
.jds-map .seal-ring{fill:none;stroke:var(--cream);stroke-width:1}
.jds-map .seal-small{fill:var(--cream);stroke:var(--gold);stroke-width:1.5}
.jds-map .seal,.jds-map .seal circle{transform-box:fill-box}
.jds-map .seal circle{transition:transform var(--dur-quick) var(--ease),fill var(--dur-quick) var(--ease)}
.jds-map .label{font-family:var(--sans);font-weight:600;font-size:12.5px;letter-spacing:.08em;text-transform:uppercase;fill:var(--ink);transition:fill var(--dur-quick) var(--ease)}
.jds-map .label.core{font-family:var(--serif);font-weight:500;font-style:italic;font-size:15px;letter-spacing:normal;text-transform:none}
.jds-map .label.home{font-size:16px}
/* Snag A5: transparent hit rect enlarges the tap/click target well beyond the
   seal + label ink. Painted first so it sits under the artwork. */
.jds-map .hit{fill:transparent;stroke:none}
/* Snag A19: hover feedback gated to fine pointers, matching .card/.gallery-grid
   at line 208. Focus-visible stays ungated — the keyboard path needs it. */
.jds-map .town:focus-visible .seal-small{fill:var(--gold)}
.jds-map .town:focus-visible .label{fill:var(--black)}
@media (pointer:fine){
.jds-map .town:hover .seal-small{fill:var(--gold)}
.jds-map .town:hover .label{fill:var(--black)}
}
@media (prefers-reduced-motion:no-preference){
.jds-map .town:hover .seal circle,.jds-map .town:focus-visible .seal circle{transform:scale(1.12)}
.areas-intro.in .boundary{animation:jds-map-draw 950ms var(--ease) both,jds-map-wash 450ms var(--ease) 650ms both}
.areas-intro.in .county{animation:jds-map-county 400ms var(--ease) 600ms both}
.areas-intro.in .seal{animation:jds-map-seal 320ms var(--ease) var(--sd) both}
.areas-intro.in .label{animation:jds-map-label 320ms var(--ease) var(--sd) both}
}
@keyframes jds-map-draw{from{stroke-dashoffset:1543}to{stroke-dashoffset:0}}
@keyframes jds-map-wash{from{fill-opacity:0}to{fill-opacity:.35}}
@keyframes jds-map-county{from{opacity:0}to{opacity:.45}}
@keyframes jds-map-seal{from{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}
@keyframes jds-map-label{from{opacity:0}to{opacity:1}}

/* ============================================================
   Packet 03 — "Three anatomies": the service detail pages.
   Every rule below is scoped under `.svc` (on <main> of the three
   /services/<detail>/ pages only) so nothing can reach another
   route, and none of it reaches inside the untouchable `.page-hero`
   band (that section carries no `.section` class).
   Foundation: one text axis, one token-derived rhythm, one list
   language (check-circles = scope, gold numerals = method), real
   desktop alternation, text-first entrances, the gold line-draw on
   every section head.
   ============================================================ */

/* F1 — one axis: the intro band sits on the site text axis with a
   measure cap, instead of a centred 760px box floating off it. */
.svc .intro-prose{max-width:720px}

/* F7 — the "why it matters" measure, previously an inline style */
.svc .wrap--narrow{max-width:820px}

/* F2 — one rhythm. Within .svc the gap from any h2 to the block that
   follows is var(--s3), set here and nowhere else. Competing top
   margins on the following block are neutralised so the collapse
   resolves to the h2's own value. --s2 below is the same 1rem the
   `mt-10` shim carried; the number now derives from the scale.
   (This is the TRANSITIONAL decision the shim comment at line ~433
   deferred to the improvement packets.) */
.svc h2{margin-top:0;margin-bottom:var(--s3)}
.svc .eyebrow + h2{margin-top:var(--s2)}
.svc .checklist{margin-top:0}
.svc .cta-band p{margin-top:0}

/* F3 — one list language: `.stages`, the hub's numeral voice.
   Scope keeps its check-circles; method becomes numbered stages in
   the 01–04 process row's serif gold numerals. */
.svc .stages{counter-reset:stage;list-style:none;padding:0;margin:0;display:grid}
.svc .stages li{
  counter-increment:stage;position:relative;
  display:grid;grid-template-columns:2.6rem 1fr;align-items:baseline;column-gap:var(--s2);
  padding-block:.8rem;font-size:1rem;color:var(--ink);
}
.svc .stages li+li{border-top:1px solid var(--line-soft)}
.svc .stages li::before{
  content:"0" counter(stage);
  /* M3: --gold-deep measured 3.26–3.56:1 on the cream fields — under 4.5:1 at this
     size. --gold-text holds the voice and passes (4.55–4.97:1 measured). */
  font-family:var(--serif);font-size:1.35rem;line-height:1;color:var(--gold-text);
}
/* horizontal row form (exterior) — numeral above, connector rule across */
@media (min-width:981px){
  .svc .stages--row{grid-template-columns:repeat(5,1fr);gap:clamp(1.2rem,2.5vw,2rem)}
  .svc .stages--row li{display:block;padding:2.6rem 0 0;border:0}
  /* M3: the hub's washed --gold @.55 measures 1.61:1 here — fails even the 3:1
     large-text floor. Full-strength --gold-deep is 3.56:1 on --cream: AA at 2.3rem. */
  .svc .stages--row li::before{position:absolute;top:0;left:0;font-size:2.3rem;color:var(--gold-deep)}
  .svc .stages--row li::after{content:"";position:absolute;top:1.1rem;left:3.3rem;right:-1rem;height:1px;background:var(--line)}
  .svc .stages--row li:last-child::after{display:none}
}

/* F3 entrance — group stagger, mirroring the .values/.steps pattern
   at line ~455. The group reveal stays composed; the items carry the
   motion at the 80ms unit, capped at 6. */
.js .svc .stages.reveal{opacity:1;transform:none}
.js .svc .stages.reveal>li{
  opacity:0;transform:translateY(14px);
  transition:opacity var(--dur-slow) var(--ease),transform var(--dur-slow) var(--ease);
}
.js .svc .stages.reveal.in>li{opacity:1;transform:none}
.js .svc .stages.reveal.in>:nth-child(2){transition-delay:.08s}
.js .svc .stages.reveal.in>:nth-child(3){transition-delay:.16s}
.js .svc .stages.reveal.in>:nth-child(4){transition-delay:.24s}
.js .svc .stages.reveal.in>:nth-child(5){transition-delay:.32s}
.js .svc .stages.reveal.in>:nth-child(n+6){transition-delay:.4s}

/* F4 — alternation restored at desktop only. The estate-wide
   `.split.flip .split-media{order:2}` rules (lines ~311 and ~766) are
   untouched; the <=980 stack still leads with the heading (snag A18). */
@media (min-width:981px){.svc .split.flip .split-media{order:0}}

/* F6 — the approved gold line-draw, extended to every section-band
   eyebrow (not just `.section-head`). Section bands only: the hero
   band carries no `.section` class, so it cannot be reached. */
@media (prefers-reduced-motion:no-preference){
  .js .svc .section .reveal .eyebrow::before{transform:scaleX(0);transform-origin:right;transition:transform .7s var(--ease) .25s}
  .js .svc .section .reveal .eyebrow.center::after{transform:scaleX(0);transform-origin:left;transition:transform .7s var(--ease) .25s}
  .js .svc .section .reveal.in .eyebrow::before,
  .js .svc .section .reveal.in .eyebrow.center::after{transform:scaleX(1)}
}
/* F3 — the connector line-draw across the row form, mirroring the
   process connector at line ~526, extended to the 4th connector. */
@media (min-width:981px) and (prefers-reduced-motion:no-preference){
  .js .svc .stages--row li::after{transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease)}
  .js .svc .stages--row li:nth-child(1)::after{transition-delay:.2s}
  .js .svc .stages--row li:nth-child(2)::after{transition-delay:.42s}
  .js .svc .stages--row li:nth-child(3)::after{transition-delay:.64s}
  .js .svc .stages--row li:nth-child(4)::after{transition-delay:.86s}
  .js .svc .stages--row.reveal.in li::after{transform:scaleX(1)}
}

/* ---------- M2: the two remaining anatomies ---------- */

/* Exterior — the ledger band. The surfaces schedule is pinned to the
   elevation: photo across the left eight of twelve columns, panel
   across the right five, so the two overlap by exactly one column.
   The panel is first in the DOM, so the heading leads without JS and
   the <=980 stack needs no ordering rule. The only band in the
   cluster whose text sits right of its photograph. */
.svc .surfaces{display:grid;grid-template-columns:repeat(12,1fr);align-items:center}
.svc .surfaces-media{grid-column:1/span 8;grid-row:1;position:relative;
  aspect-ratio:4/3;border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.svc .surfaces-media>.ph{position:absolute;inset:0}
.svc .surfaces-panel{grid-column:8/span 5;grid-row:1;z-index:2;background:var(--paper);
  padding:clamp(1.6rem,3vw,2.4rem);border-radius:var(--radius);box-shadow:var(--shadow-sm)}
@media (max-width:980px){
  .svc .surfaces{display:block}
  .svc .surfaces-panel{margin-bottom:var(--s4)}   /* heading leads; no overlap */
  .svc .surfaces-media{aspect-ratio:4/3}
}

/* Wood — the chest framed portrait beside its five descending stages.
   `furniture-restoration.webp` is a 1200x800 (3:2) file, so a portrait
   frame crops its width, never its height: the full top-to-bottom of
   the photograph reads at either ratio, and 4/5 — the wider of the two
   the spec allows — is the one that still holds the piece's left
   corner, top moulding and drawer fronts inside the frame.
   object-position keeps the crop window off the workshop background. */
.svc .split-media--tall{aspect-ratio:4/5}
.svc .split-media--tall .ph>img{object-position:15% 50%}

/* Composed reduced-motion state, mirroring line ~466: everything
   lands drawn and in place, no transition — never merely disabled. */
@media (prefers-reduced-motion:reduce){
  .js .svc .stages.reveal>li{opacity:1;transform:none;transition:none}
  .js .svc .stages--row li::after{transform:none;transition:none}
  .js .svc .section .reveal .eyebrow::before,
  .js .svc .section .reveal .eyebrow.center::after{transform:none;transition:none}
}

/* ============================================================
   Packet 04 — "Held to the light": the photo lightbox.
   One law, estate-wide: proof responds, illustration stays mute.
   A real photograph lifts off the page (FLIP from its own tile),
   is held on a near-black field with the site's proof grammar —
   "Finished work" eyebrow, a gold hairline drawn once, one
   caption line in the site voice, a quiet counter — and returns
   to the exact frame it left. Nothing here invents grammar: the
   dark field and hairline are the hero/page-hero vocabulary, the
   line-draw is the section-head/ledger vocabulary. Tokens only:
   --dur-*, --ease, the palette, --radius, --shadow.
   Keyframes namespaced jds-lb-*. Markup is inert without JS
   (the [hidden] dialog stays hidden; the anchors open the file).
   ============================================================ */

/* --- enrolled photo anchors --- */
a[data-lb]{cursor:zoom-in}
.gallery-grid a.tile{display:block}
/* the gallery case photo's anchor fills its figure exactly — the img keeps its
   own absolute inset:0 rules, so the figure's box does not move (zero CLS) */
.case-fig .case-link{position:absolute;inset:0;display:block}
/* Enrolled anchors are clipped containers, so the global 3px-offset ring is
   cut off by the frame. Photo anchors alone take an inset ring; every other
   focusable element (a.tile-cta included) keeps the global one.

   Stress test 2026-08-25 (F1): the inset *outline* did not paint at all —
   measured 0 differing pixels on all 13 enrolled anchors, at 1x and 3x DPI,
   while :focus-visible matched and the outline computed correctly. A negative
   outline-offset on these clipped, photo-filled anchors is not rendered here.
   The ring is now a positioned ::after: it is a later-painted positioned
   sibling of the photograph, so it draws over it, and at inset 5px it sits
   inside the frame rather than being clipped by it. Geometry is identical to
   the outline it replaces (band from 5px to 7.5px inside the border box). */
a[data-lb]:focus-visible{outline:none}
a[data-lb]:focus-visible::after{
  content:"";position:absolute;inset:5px;
  border:2.5px solid var(--gold-deep);
  border-radius:var(--radius);
  pointer-events:none;
}

/* --- the field --- */
.lb{
  position:fixed;inset:0;z-index:300;
  display:grid;place-items:center;
  padding:clamp(1rem,4vw,3rem);
  overscroll-behavior:contain;
  /* the slider's grammar: vertical panning stays the browser's, horizontal
     travel is ours — without this the compositor claims the gesture and
     cancels the pointer stream mid-swipe */
  touch-action:pan-y;
}
.lb[hidden]{display:none}
/* The veil: near-opaque, with the mobile-cta's own blur grammar behind it, so
   the page reads as faint afterglow and never as legible text. Only opacity
   animates — the blur is a static property of the field. */
.lb-veil{position:absolute;inset:0;background:rgba(17,17,16,.97);opacity:0;
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  transition:opacity var(--dur-standard) var(--ease)}
.lb.is-open .lb-veil{opacity:1}

/* --- the photograph: a window and the print inside it ---
   .lb-frame is the window (clipped, radiused, shadowed); .lb-img is the
   photograph. The flight scales the window non-uniformly from the tile's
   crop box while the print counter-scales, so the photograph is never
   distorted — the frame opens to reveal it whole. Transforms only, both
   directions; the keyframes are written by the script. */
.lb-stage{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;
  gap:.95rem;max-width:min(86vw,1200px);margin:0}
.lb-frame{position:relative;overflow:hidden;line-height:0;
  border-radius:var(--radius);box-shadow:var(--shadow);
  transform-origin:50% 50%;will-change:transform}
.lb-img{
  display:block;
  max-width:min(86vw,1200px);max-height:72vh;width:auto;height:auto;
  transform-origin:50% 50%;will-change:transform;
  transition:opacity var(--dur-quick) var(--ease);
}
/* nav crossfade — snappy, not ceremonial */
.lb-img.is-swapping{opacity:0}

/* --- the caption bar: proof chip, eyebrow rule, caption, counter ---
   --lb-w is written by the script from the rendered image box so the bar is
   exactly as wide as the photograph it describes. */
.lb-capbar{width:var(--lb-w,100%);max-width:100%;
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;
  column-gap:.85rem;row-gap:.45rem}
.lb-proof{grid-row:1;grid-column:1;opacity:0;
  font-family:var(--sans);font-size:.72rem;font-weight:700;letter-spacing:.22em;
  text-transform:uppercase;color:var(--gold-bright)}
.lb-rule{grid-row:1;grid-column:2;justify-self:start;
  width:34px;height:1px;background:var(--gold);transform:scaleX(0);transform-origin:left}
.lb-count{grid-row:1;grid-column:3;justify-self:end;opacity:0;
  font-family:var(--sans);font-size:.78rem;color:#a59f8f;letter-spacing:.04em}
.lb-cap{grid-row:2;grid-column:1/-1;opacity:0;
  font-family:var(--serif);font-style:italic;font-size:1.05rem;color:var(--cream);
  max-width:46ch;min-height:calc(2 * 1.65em)}

/* --- controls: ghost-on-dark, 46px hit area ---
   .lb-nav is transparent to layout at desktop (the pair stays pinned to the
   field's sides); below 760px it becomes a real row that belongs to the
   content — 24px under the caption bar, in the thumb zone. */
.lb-nav{display:contents}
.lb-close,.lb-prev,.lb-next{
  position:absolute;z-index:2;opacity:0;
  display:flex;align-items:center;justify-content:center;
  width:46px;height:46px;padding:0;
  background:transparent;border:1px solid currentColor;color:var(--cream);
  border-radius:50%;
  transition:background var(--dur-quick) var(--ease),color var(--dur-quick) var(--ease),
             transform var(--dur-press) var(--ease);
}
.lb-close svg,.lb-prev svg,.lb-next svg{display:block}
.lb-close{top:clamp(.75rem,2.2vw,1.5rem);right:clamp(.75rem,2.2vw,1.5rem)}
.lb-prev{left:clamp(.75rem,2.2vw,1.5rem);top:50%;transform:translateY(-50%)}
.lb-next{right:clamp(.75rem,2.2vw,1.5rem);top:50%;transform:translateY(-50%)}
@media (pointer:fine){
  .lb-close:hover,.lb-prev:hover,.lb-next:hover{background:var(--cream);color:var(--black)}
}
.lb-close:active{transform:scale(.98)}
.lb-prev:active,.lb-next:active{transform:translateY(-50%) scale(.98)}

/* --- the entrance: caption bar arrives after the flight, on the 80ms unit.
   Flight lands at --dur-standard (.35s), so proof = +.08s, rule/caption/
   counter/controls = +.16s. The rule draws ONCE per open — nav never
   redraws it, because the class that starts it is never removed. --- */
.lb.is-open .lb-proof{animation:jds-lb-fade var(--dur-standard) var(--ease) .43s both}
.lb.is-open .lb-rule{animation:jds-lb-draw var(--dur-slow) var(--ease) .51s both}
.lb.is-open .lb-cap,
.lb.is-open .lb-count,
.lb.is-open .lb-close,
.lb.is-open .lb-prev,
.lb.is-open .lb-next{animation:jds-lb-fade var(--dur-standard) var(--ease) .51s both}
@keyframes jds-lb-fade{from{opacity:0}to{opacity:1}}
@keyframes jds-lb-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* --- mobile: shorter photo; prev/next belong to the content — a row 24px
   below the caption bar, not pinned to the screen edge; close stays
   top-right. The stage and the row are the two grid rows of the field, so
   the group centres as one. --- */
@media (max-width:759px){
  .lb{grid-auto-rows:min-content;align-content:center;justify-items:center;
    padding-bottom:calc(1rem + env(safe-area-inset-bottom,0px))}
  .lb-img{max-height:68vh}
  /* 18px is the exact gap the pinned pair had (−3.4rem / +.6rem about centre) */
  .lb-nav{display:flex;gap:18px;margin-top:24px}
  .lb-prev,.lb-next{position:static;top:auto;bottom:auto;left:auto;right:auto;
    margin:0;transform:none}
  .lb-prev:active,.lb-next:active{transform:scale(.98)}
}

/* --- reduced motion: composed, never merely disabled. The dialog appears
   whole — veil at full opacity, photograph in place, rule drawn, caption and
   controls present — with zero running transitions. The JS `reduce` gate
   skips the FLIP entirely; this is the other half of the same contract. --- */
@media (prefers-reduced-motion:reduce){
  .lb-veil,.lb-img,.lb-frame,.lb-close,.lb-prev,.lb-next{transition:none}
  .lb-frame,.lb-img{transform:none}
  /* .lb.is-open is what starts the beats above, so the composed state has to
     be written at the same specificity or it never wins. */
  .lb.is-open .lb-proof,.lb.is-open .lb-cap,.lb.is-open .lb-count,
  .lb.is-open .lb-close,.lb.is-open .lb-prev,.lb.is-open .lb-next,
  .lb .lb-proof,.lb .lb-cap,.lb .lb-count,
  .lb .lb-close,.lb .lb-prev,.lb .lb-next{animation:none;opacity:1}
  .lb.is-open .lb-rule,.lb .lb-rule{animation:none;transform:scaleX(1)}
  .lb-img.is-swapping{opacity:1}
}

/* ---------- Packet 04 — hero mini-tiles become wayfinding links ----------
   Quiet by contract: this is wayfinding, not a moment. No arrows, no
   transforms — the textlink underline and a one-step lift on the value.
   Colour-only, so it also applies under reduced motion. */
.hero-meta .hero-meta-link{display:flex;flex-direction:column;gap:.2rem}
/* The underline is a hover/focus state and nothing else: `transparent` is the
   rest value and no other rule may colour it, so no link can sit underlined
   while a sibling is hovered. */
.hero-meta .k{align-self:start;border-bottom:1px solid transparent;
  transition:border-color var(--dur-quick) var(--ease)}
.hero-meta .hero-meta-link .k{border-bottom-color:transparent}
.hero-meta .v{transition:color var(--dur-quick) var(--ease)}
@media (pointer:fine){
  .hero-meta .hero-meta-link:hover .k{border-bottom-color:var(--gold)}
  .hero-meta .hero-meta-link:hover .v{color:#c9c2b2}
}
.hero-meta .hero-meta-link:focus-visible .k{border-bottom-color:var(--gold)}
.hero-meta .hero-meta-link:focus-visible .v{color:#c9c2b2}
/* the triplet is a row, not a 2+1 break: at desktop the three links share it */
@media (min-width:981px){
  .hero-meta .hero-meta-link{flex:1 1 0;min-width:0}
}
@media (prefers-reduced-motion:reduce){
  .hero-meta .k,.hero-meta .v{transition:none}
}

/* ============================================================
   Grimsdale Web Design — site credit bar (externalised form of the
   package's footer-credit-dark.html; rules verbatim, tokens folded in
   because this estate has exactly one footer colour). Brand-fixed:
   the lockup and the #D5331E underline. Do not restyle.
   ============================================================ */
.gwd-credit{
  --gwd-bg:#111110; --gwd-fg:rgba(255,255,255,.70); --gwd-copy:rgba(255,255,255,.55);
  box-sizing:border-box;
  background:var(--gwd-bg);
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:18px 24px 16px; text-align:center;
  font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
}
.gwd-credit__link{ position:relative; display:inline-flex; align-items:center; gap:11px; text-decoration:none; }
.gwd-credit__by{ font-size:13px; font-weight:500; letter-spacing:.01em; color:var(--gwd-fg); }
.gwd-credit__link svg,
.gwd-credit__link img{ height:24px; width:auto; display:block; }
.gwd-credit__link::after{
  content:""; position:absolute; left:0; right:0; bottom:-6px; height:1.5px;
  background:#D5331E; transform:scaleX(0); transform-origin:left;
  transition:transform .3s cubic-bezier(.6,.1,.2,1);
}
.gwd-credit__link:hover::after{ transform:scaleX(1); }
.gwd-credit__copy{ margin:0; font-size:12px; color:var(--gwd-copy); }
/* touch floor (estate snag A9): 44px hit box on coarse pointers only; layout and the
   rendered bar are unchanged, the brand-fixed ::after is untouched */
@media (pointer:coarse){
  .gwd-credit__link::before{ content:""; position:absolute; left:0; right:0; top:-10px; bottom:-10px; }
}

/* ---- Grimsdale credit — the flourish ("Built"). Scoped to .gwd-credit; no bare selectors.
   Overlay is created lazily by main.js on first intent and removed after it lifts. ---- */
.gwd-credit{ position:relative; }
.gwd-credit__mark{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; overflow:hidden; display:block; }
.gwd-credit__mark *{ vector-effect:non-scaling-stroke; }
@media (prefers-reduced-motion:reduce){ .gwd-credit__mark{ /* JS also checks; nothing animates here */ } }
