/* ==========================================================================
   TrueOak — the reel
   Scenes 01–03. Loaded on the home page only.

   Every visible state below is a function of custom properties that reel.js
   writes onto the stage: --p (damped scroll progress), --v (velocity), and one
   named value per beat. Nothing here animates on a timer, so the picture is
   always exactly where the visitor's scroll put it — and with the script
   absent every var falls back to 0, which is a correct, readable opening frame.
   ========================================================================== */

/* ---------- intro ---------- */

.intro {
  position: fixed; inset: 0; z-index: 300;
  display: grid; place-items: center;
  background: var(--grad-forest);
  animation: intro-clear .5s cubic-bezier(.65,0,.35,1) 1.15s both;
}
.intro::before { content: ""; position: absolute; inset: 0; background: var(--courses) repeat; opacity: .5; }
.intro::after {
  content: ""; position: absolute; inset: 0;
  background: var(--glow-leaf); opacity: 0;
  animation: intro-glow 1.15s ease-out both;
}
.intro-mark { position: relative; width: min(230px, 46vw); animation: intro-drop 1.15s cubic-bezier(.34,1.4,.5,1) both; }
.intro-mark img { width: 100%; height: auto; }
@keyframes intro-drop {
  0%   { transform: translateY(-46vh) rotate(-190deg) scale(.42); opacity: 0; }
  38%  { opacity: 1; }
  62%  { transform: translateY(14px) rotate(6deg) scale(1.06); }
  78%  { transform: translateY(-6px) rotate(-2deg) scale(.99); }
  100% { transform: none; opacity: 1; }
}
@keyframes intro-glow { 0%,55% { opacity: 0; } 100% { opacity: 1; } }
@keyframes intro-clear { to { clip-path: inset(50% 0 50% 0); opacity: 0; visibility: hidden; } }
.intro-skip {
  position: absolute; right: 18px; bottom: 16px;
  background: transparent; border: 1px solid rgba(249,249,230,.32);
  color: var(--cream-dim); border-radius: 2px; cursor: pointer;
  font: 700 .68rem/1 var(--f-body); letter-spacing: .16em; text-transform: uppercase; padding: 10px 15px;
}
.intro-skip:hover { color: var(--cream); border-color: var(--cream); }
.intro.gone { display: none; }

/* ---------- scene shell ----------
   A scene is one screen tall unless <html> carries .reel-on, which a tiny
   inline script in the head sets before the first paint. Deciding this later —
   from the deferred script, once the page has already been drawn — pushes
   everything below down by several screens and costs half a point of
   cumulative layout shift. That script also removes the class again if
   reel.js never turns up, so a failed request degrades to a normal hero
   instead of to screens of dead scrolling. */

.scn { position: relative; height: 100vh; padding: 0; background: #010A05; }
.stage {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: grid; place-items: center;
  perspective: 1400px; perspective-origin: 52% 46%;
  background: #010A05;
}
.stage::after {              /* dissolve into whatever follows */
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 18vh; z-index: 8;
  background: linear-gradient(180deg, rgba(1,10,5,0) 0%, rgba(1,10,5,.8) 64%, #010A05 100%);
  pointer-events: none;
}

/* ---------- depth planes ---------- */

.plane { position: absolute; inset: 0; pointer-events: none; will-change: transform; }
.plane svg { width: 100%; height: 100%; display: block; }

/* ---------- SC 02 · APPROACH ----------
   Five planes, five rates. The foreground overtakes the camera and leaves
   frame, the oak swings out to the left, the house grows, the sky barely
   moves. That difference is the whole illusion of travel. */

.reel-on #sc-approach { height: 380vh; }

#sc-approach .pl-sky   { transform: scale(calc(1 + var(--push,0) * .30)); }
#sc-approach .pl-far   { transform: translateY(calc(var(--push,0) * 5vh)) scale(calc(1 + var(--push,0) * .85)); }
#sc-approach .pl-oak   { transform: translate3d(calc(var(--push,0) * -30vw), calc(var(--push,0) * 12vh), 0) scale(calc(1 + var(--push,0) * 2.1)); }
#sc-approach .pl-house { transform: translateY(calc(var(--push,0) * -7vh)) scale(calc(1 + var(--push,0) * 1.5)); }
#sc-approach .pl-fore  {
  transform: translateY(calc(var(--push,0) * 28vh)) scale(calc(1 + var(--push,0) * 3.2));
  opacity: calc(1 - var(--push,0) * 1.7);
}

/* The frame closes down to the roof plane as the camera arrives. */
#sc-approach .gate {
  position: absolute; inset: 0; z-index: 7; pointer-events: none;
  background: #010A05;
  clip-path: inset(calc(var(--land,0) * 50%) 0 calc(var(--land,0) * 50%) 0);
  opacity: calc(var(--land,0) * .96);
}

/* ---------- SC 03 · ANATOMY ---------- */

.reel-on #sc-anatomy { height: 560vh; }
#sc-anatomy .stage { perspective: 1500px; perspective-origin: 56% 44%; }

/* The camera rig. Scale is the dolly; rotateZ across the whole scene is the
   orbit, and the orbit is what makes the separated layers show parallax
   against each other — which is what tells the eye they are at real depths
   rather than stacked in a picture. */
.rig {
  position: absolute; width: 520px; height: 340px;
  transform-style: preserve-3d;
  transform:
    translate3d(calc(8% + var(--ex,0) * 3%), calc(4% + var(--ex,0) * 5%), 0)
    scale(calc(.46 + var(--settle,0) * .46 - var(--asm,0) * .18))
    rotateX(calc(66deg - var(--step,0) * 4deg - var(--ex,0) * 9deg))
    rotateZ(calc(-54deg + var(--step,0) * 13deg + var(--ex,0) * 17deg));
}

/* A slab is a real box: a top face plus two extruded side faces. When the rig
   orbits you see the thickness of every layer, which is the entire difference
   between an architectural section and five rectangles. */
.slab {
  position: absolute; inset: 0; transform-style: preserve-3d;
  transform: translate3d(
    calc(var(--sx, 0) * 1px * var(--step,0) * (1 - var(--ex,0))),
    calc(var(--sy, 0) * 1px * var(--step,0) * (1 - var(--ex,0))),
    calc(var(--sep, 0) * 1px * var(--ex,0) * (1 - var(--asm,0)))
  );
}
/* The second background-size is the material tile. At 150px it fitted about
   thirty shingle courses across the slab, which is roughly three times what a
   real roof shows at this distance and is why it read as fabric rather than
   roofing. 260px puts it at about seventeen. */
.face { position: absolute; background-repeat: repeat;
        background-size: 100% 100%, 260px 260px;   /* gradient fills; only the texture tiles */
        backface-visibility: hidden; }
.face.top { inset: 0; box-shadow: inset 0 0 0 1px rgba(0,0,0,.4); }
.face.fr  { left: 0; top: 100%; width: 100%; height: calc(var(--th,8) * 1px);
            transform-origin: 50% 0%; transform: rotateX(-90deg); background-size: 100% 100%, 260px 40px; }
.face.rt  { left: 100%; top: 0; width: calc(var(--th,8) * 1px); height: 100%;
            transform-origin: 0% 50%; transform: rotateY(90deg); background-size: 100% 100%, 40px 260px; }
/* Edges are the same material seen end-on, so they read darker. */
.face.fr, .face.rt { filter: brightness(.62); }

.sl-deck    { --sep: 0;   --th: 22; --sx: 0;   --sy: 0; }
.sl-ice     { --sep: 66;  --th: 7;  --sx: 34;  --sy: 20; }
.sl-under   { --sep: 132; --th: 6;  --sx: 68;  --sy: 40; }
.sl-flash   { --sep: 198; --th: 5;  --sx: 102; --sy: 60; }
.sl-shingle { --sep: 272; --th: 15; --sx: 136; --sy: 80; }

.sl-deck    .face { background-image: linear-gradient(148deg, rgba(255,246,220,.20), rgba(0,0,0,.16)), url("/assets/tex/deck.webp"); }
.sl-ice     .face { background-image: linear-gradient(148deg, rgba(255,255,255,.18), rgba(0,0,0,.20)), url("/assets/tex/ice.webp"); }
.sl-under   .face { background-image: linear-gradient(148deg, rgba(255,255,255,.22), rgba(0,0,0,.16)), url("/assets/tex/under.webp"); }
.sl-shingle .face { background-image: linear-gradient(148deg, rgba(226,255,236,.22), rgba(0,0,0,.18)), url("/assets/tex/shingle.webp"); }
.sl-flash   .face {
  background-image: linear-gradient(126deg, rgba(255,248,222,.52) 8%, rgba(255,255,255,0) 36%, rgba(0,0,0,.16) 72%), url("/assets/tex/flash.webp");
}
.sl-flash .face.top { box-shadow: inset 0 0 0 1px rgba(228,206,142,.6), 0 0 26px rgba(201,174,107,.22); }

/* Fasteners, on the layer that actually gets nailed. */
.sl-shingle .face.top::after {
  content: ""; position: absolute; inset: 0;
  background-image: radial-gradient(2.5px 2.5px at 50% 50%, rgba(0,0,0,.55) 0 40%, transparent 60%);
  background-size: 76px 42px; opacity: .8;
}

/* The ventilation path: intake at the eave, exhaust at the ridge. */
/* Lives inside the rig so it travels with the stack, sitting at the depth
   the airflow actually occupies. */
.vent { position: absolute; inset: 0; pointer-events: none;
        opacity: calc(min(var(--vent,0), (0.72 - var(--asm,0)) * 4));
        transform: translateZ(calc(126px * var(--ex,0) * (1 - var(--asm,0)))); }
.vent svg { width: 100%; height: 100%; display: block; overflow: visible; }
.vent path {
  fill: none; stroke: var(--oak-bright); stroke-width: 2.2; stroke-linecap: round;
  stroke-dasharray: 9 13;
  stroke-dashoffset: calc((1 - var(--vent,0)) * 340);
  filter: drop-shadow(0 0 6px rgba(201,174,107,.65));
}

/* ---------- callouts ----------
   A leader line is only doing its job if its dot touches the layer it names.

   The earlier version placed each label at a fixed percentage of the stage,
   which is wrong: the layers are drawn by a 3D rig whose projected position
   depends on perspective, not on the stage's proportions. Those percentages
   happened to look right at one window shape and drifted by up to 330px at
   others, and the labels were spread roughly three times wider than the layers
   actually separate.

   So the anchors below are measured rather than guessed. Each layer's real
   on-screen position was sampled at seven points through the separation, at
   four window shapes, and fitted to a quadratic in --ex. Relative to the stage
   centre the fits agree across those window shapes to within 8px and track the
   layer to within 5px, against a 47-63px gap between neighbours — so the dot
   lands on its layer at any viewport, which fixed percentages cannot do.

   Still no JavaScript: the fit is three constants per axis, evaluated by the
   compositor from the same --ex the slabs move on. */

.calls { position: absolute; inset: 0; z-index: 5; pointer-events: none; }

/* The labels cannot simply sit at their layer's height. Early in the
   separation the layers are only about 17px apart, so five 33px labels held at
   those heights collide for most of the reveal. Instead each label sits at a
   fixed offset from its layer — evenly spread, so they can never touch — and
   an elbow leader runs back from the text to a dot that is still exactly on
   the layer. Which is how a technical illustration has always solved this. */
.leader {
  position: absolute;
  right: 24px;                     /* the label runs to the stage edge, so it
                                      can never overflow a narrower window */
  padding-left: 36px; max-width: 330px;
  display: grid; gap: 3px;
  opacity: calc(min((var(--ex,0) - var(--due,0)) * 9, (0.42 - var(--asm,0)) * 7));
}
.leader b { font: 700 .68rem/1.2 var(--f-body); letter-spacing: .17em; text-transform: uppercase; color: var(--oak-bright); }
.leader span { font: 400 .82rem/1.3 var(--f-body); color: var(--cream-dim); }
/* the elbow: a horizontal into the text, a vertical back to the layer */
.leader::before {
  content: ""; position: absolute; left: 3px; width: 27px;
  border-left: 1px solid var(--oak-bright);
  filter: drop-shadow(0 0 4px rgba(201,174,107,.55));
}
/* the dot, pulled back onto the layer itself */
.leader::after {
  content: ""; position: absolute; left: 0; top: calc(6px - var(--bias, 0px));
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--oak-bright); box-shadow: 0 0 10px rgba(201,174,107,.9);
}
.c-shingle::before { top: 9px;   height: 48px; border-top: 1px solid var(--oak-bright); }
.c-flash::before   { top: 9px;   height: 24px; border-top: 1px solid var(--oak-bright); }
.c-under::before   { top: 9px;   height: 0;    border-left: 0; border-top: 1px solid var(--oak-bright); }
.c-ice::before     { top: -15px; height: 24px; border-bottom: 1px solid var(--oak-bright); }
.c-deck::before    { top: -39px; height: 48px; border-bottom: 1px solid var(--oak-bright); }

/* left = the layer's right edge; top = its mid height. Both measured. */
.c-shingle {
  left: calc(50% + (452.3 - 38.7 * var(--ex,0) - 35.1 * var(--ex,0) * var(--ex,0)) * 1px);
  top:  calc(50% + (24.5 - 159.5 * var(--ex,0) - 24.2 * var(--ex,0) * var(--ex,0)) * 1px + -48px - 9px);
  --bias: -48px; --due: .10;
}
.c-flash {
  left: calc(50% + (419.4 - 23.6 * var(--ex,0) - 27.0 * var(--ex,0) * var(--ex,0)) * 1px);
  top:  calc(50% + (28.2 - 111.3 * var(--ex,0) - 13.3 * var(--ex,0) * var(--ex,0)) * 1px + -24px - 9px);
  --bias: -24px; --due: .32;
}
.c-under {
  left: calc(50% + (386.2 -  5.0 * var(--ex,0) - 20.4 * var(--ex,0) * var(--ex,0)) * 1px);
  top:  calc(50% + (31.9 -  68.7 * var(--ex,0) -  7.0 * var(--ex,0) * var(--ex,0)) * 1px + 0px - 9px);
  --bias: 0px; --due: .42;
}
.c-ice {
  left: calc(50% + (352.8 + 16.1 * var(--ex,0) - 15.6 * var(--ex,0) * var(--ex,0)) * 1px);
  top:  calc(50% + (35.7 -  25.8 * var(--ex,0) -  4.0 * var(--ex,0) * var(--ex,0)) * 1px + 24px - 9px);
  --bias: 24px; --due: .58;
}
.c-deck {
  left: calc(50% + (319.0 + 39.6 * var(--ex,0) - 12.5 * var(--ex,0) * var(--ex,0)) * 1px);
  top:  calc(50% + (39.5 +  17.2 * var(--ex,0) -  3.8 * var(--ex,0) * var(--ex,0)) * 1px + 48px - 9px);
  --bias: 48px; --due: .72;
}

/* The finished roof, revealed as the assembly closes. */
#sc-anatomy .pl-final { opacity: calc((var(--asm,0) - .50) * 2.8); z-index: 1; }
#sc-anatomy .rig      { z-index: 3; opacity: calc(1 - max(0, (var(--asm,0) - .50)) * 3.4); }

/* ---------- copy ---------- */

.scrim {
  position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: linear-gradient(94deg, rgba(1,12,6,.95) 0%, rgba(1,12,6,.86) 26%, rgba(1,12,6,.42) 50%, rgba(1,12,6,0) 68%);
}
.cap { position: absolute; z-index: 7; width: min(540px, 86vw); left: clamp(18px, 6vw, 88px); }
.cap h1, .cap h2 { color: var(--cream); }
.cap h2 { font-size: clamp(1.5rem, 1rem + 1.9vw, 2.15rem); }
.cap p { color: var(--cream-dim); font-size: 1.08rem; margin-top: 14px; }
.cap .btnrow { margin-top: 22px; }
.eyebrow-o { font: 700 .72rem/1 var(--f-body); letter-spacing: .2em; text-transform: uppercase; color: var(--oak-bright); margin-bottom: 14px; }

/* The headline does not fade out. The camera passes it, so it recedes. */
.cap-lead  { top: 21vh; transform: scale(calc(1 - var(--lift,0) * .26)); opacity: calc(1 - var(--lift,0) * 1.25); }
.cap-spec  { top: 25vh; opacity: calc(min(var(--step,0) * 2.6, (0.5 - var(--asm,0)) * 5)); }
.cap-close { top: 24vh; opacity: calc((var(--asm,0) - .55) * 3); pointer-events: auto; }

.cue {
  position: absolute; left: 50%; bottom: clamp(96px, 14vh, 150px); transform: translateX(-50%);
  display: grid; gap: 8px; justify-items: center; z-index: 7;
  font: 700 .64rem/1 var(--f-body); letter-spacing: .2em; text-transform: uppercase;
  color: var(--oak-bright); text-shadow: 0 2px 10px rgba(1,10,5,.9);
  opacity: calc(1 - var(--p,0) * 7);
}
.cue i { display: block; width: 1px; height: 30px; background: linear-gradient(var(--oak-bright), transparent); }

.rail { position: absolute; right: 22px; top: 50%; transform: translateY(-50%); width: 2px; height: 130px; background: rgba(249,249,230,.16); z-index: 7; }
.rail i { position: absolute; inset: 0 0 auto 0; background: var(--oak-bright); height: calc(var(--p,0) * 100%); box-shadow: 0 0 10px rgba(201,174,107,.8); }

/* ---------- narrow screens ----------
   The orbit is dropped, the extruded edges come off, and the stack plays flat
   and larger. Storytelling survives; the expensive parts do not. */

@media (max-width: 860px) {
  .reel-on #sc-approach { height: 300vh; }
  .reel-on #sc-anatomy  { height: 440vh; }
  .rig {
    width: 300px; height: 205px;
    transform:
      translate3d(0, calc(9vh + var(--ex,0) * -2vh), 0)
      scale(calc(.55 + var(--settle,0) * .42 - var(--asm,0) * .12))
      rotateX(calc(58deg - var(--ex,0) * 8deg))
      rotateZ(-34deg);
  }
  .face.fr, .face.rt { display: none; }
  .rail { display: none; }
  .cap { left: 18px; right: 18px; width: auto; }
  .cap-lead, .cap-spec, .cap-close { top: 12vh; }
  .scrim { background: linear-gradient(180deg, rgba(1,12,6,.95) 0%, rgba(1,12,6,.8) 26%, rgba(1,12,6,.16) 48%, rgba(1,12,6,.1) 62%, rgba(1,12,6,.88) 100%); }
}

/* Leader lines need a free column beside the artwork. The measured anchors put
   the outermost one about 450px right of the stage centre, so a readable label
   needs roughly 1440px of window before there is anywhere to put it. Below
   that the sequenced captions take over. That is a width question rather than
   a phone question, so it gets its own breakpoint. */
@media (max-width: 1440px) {
  .calls { display: none; }
}

/* ---------- the callouts, on a narrower window ----------
   A leader line needs a free column beside the artwork, and at 390px there
   isn't one. So the five captions play in sequence in a single fixed slot
   instead: as each layer pulls away from the stack its name and its
   explanation fade in, and the previous one fades out.

   Every window below is a function of the same --ex the slabs are moving on,
   so a caption cannot arrive early or linger after its layer has gone. The
   third term in each min() retires the whole list once reassembly starts.

   NOTE: this block MUST stay after the narrow-screen media query above.
   `display: none` and `display: grid` are the same specificity, so whichever
   is written last wins — and when the base rule sat below the media query it
   won at every width, which is why nothing appeared on a phone at all. */

.mobseq { display: none; margin: 0; padding: 0; }

/* The same dot the desktop leader lines use, sitting on the rule at the label's
   own line, so it reads as part of the caption rather than as a marker
   wandering on its own. Shared by the sequenced and the printed versions. */
.mobseq li::before {
  content: ""; position: absolute; left: -21px; top: 4px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--oak-bright); box-shadow: 0 0 9px rgba(201,174,107,.9);
}

@media (max-width: 1440px) {
  /* Sized to the longest of the five captions, so the rule never dangles
     past the text and the block never resizes as they swap. */
  .mobseq {
    display: block; position: absolute; z-index: 7;
    left: clamp(18px, 6vw, 88px); right: 18px; max-width: 520px;
    bottom: 12vh; height: 4.2rem;
    border-left: 1px solid rgba(201,174,107,.30); padding-left: 17px;
  }
  .mobseq li {
    list-style: none; position: absolute; left: 17px; right: 0; top: 0;
    display: grid; gap: 5px;
    opacity: calc(min(
      (var(--ex,0) - var(--a,0)) * 12,
      (var(--b,1) - var(--ex,0)) * 12,
      (0.45 - var(--asm,0)) * 7));
  }
  .mobseq b { font: 700 .7rem/1.2 var(--f-body); letter-spacing: .17em;
              text-transform: uppercase; color: var(--oak-bright); }
  .mobseq span { font: 400 .95rem/1.4 var(--f-body); color: var(--cream); }

  .m-shingle { --a: .04; --b: .30; }
  .m-flash   { --a: .22; --b: .48; }
  .m-under   { --a: .40; --b: .66; }
  .m-ice     { --a: .58; --b: .84; }
  .m-deck    { --a: .76; --b: 1.4; }   /* holds to the end of the separation */
}

/* ---------- no engine ----------
   If the script never runs there is no --ex, so every opacity expression above
   evaluates to zero and the section becomes a picture of a stack with nothing
   said about it. The leader lines can't rescue it either — they point at
   separations that never happen. So the same five definitions are simply
   printed out, which is what this section was always really for. */

html:not(.reel-on) #sc-anatomy .calls { display: none; }
html:not(.reel-on) #sc-anatomy .scrim { background: rgba(1,12,6,.72); }
html:not(.reel-on) #sc-anatomy .cap-spec { opacity: 1; top: 8vh; }
html:not(.reel-on) #sc-anatomy .stage { overflow: visible; }

html:not(.reel-on) .mobseq {
  display: grid; gap: 13px; position: absolute; z-index: 7;
  left: clamp(18px, 6vw, 88px); top: 46%; width: min(460px, 86vw); height: auto;
  border-left: 1px solid rgba(201,174,107,.34); padding-left: 17px;
}
/* relative, not static — otherwise all five dots resolve against the list and
   land on top of each other at its first line. */
html:not(.reel-on) .mobseq li { position: relative; left: 0; top: 0; opacity: 1;
                                display: grid; gap: 4px; list-style: none; }
html:not(.reel-on) .mobseq li::before { top: 3px; }
html:not(.reel-on) .mobseq b { font: 700 .68rem/1.2 var(--f-body); letter-spacing: .17em;
                               text-transform: uppercase; color: var(--oak-bright); }
html:not(.reel-on) .mobseq span { font: 400 .9rem/1.4 var(--f-body); color: var(--cream-dim); }

/* ---------- reduced motion ----------
   No pinning, no camera. Each scene collapses to one honest still frame.
   Reduced motion never sets .reel-on, so the printed definitions above are
   already in play here; these rules only let them flow instead of float. */

@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
  .scn, .reel-on .scn { height: auto; }
  .stage { position: static; height: auto; min-height: 80vh; padding: 12vh 0; }
  .plane { position: absolute; }
  .rig { position: relative; transform: scale(.78) rotateX(58deg) rotateZ(-38deg); }
  .slab { transform: none; }
  .cap { position: static; opacity: 1; transform: none; margin: 0 auto 30px; }
  .cap-close, .cue, .rail, .calls, .vent, .gate { display: none; }
  .cap-spec { display: block; order: -1; }
  #sc-anatomy .pl-final { display: none; }

  html:not(.reel-on) .mobseq {
    position: static; top: auto; left: auto;
    width: min(460px, 86vw); margin: 4px auto 0;
  }
}

@media print {
  .intro, .rig, .cue, .rail, .calls, .vent, .gate { display: none !important; }
  .scn, .reel-on .scn { height: auto; }
  .stage { position: static; height: auto; }
  .cap { position: static; opacity: 1 !important; transform: none !important; }
  /* On paper the five layers are the one thing worth keeping from this scene. */
  .mobseq { display: grid !important; position: static !important; gap: 8px;
            height: auto !important; opacity: 1 !important; padding-left: 14px;
            border-left: 1px solid #999; }
  .mobseq li { position: static !important; opacity: 1 !important; list-style: none; }
  .mobseq::after { display: none !important; }
  .mobseq b { font-weight: 700; text-transform: uppercase; letter-spacing: .12em; font-size: .7rem; }
}

/* ==========================================================================
   SC 05 · WHY AN OAK — THE ROOTS
   Deliberately on the cream ground the section already used. Turning it dark
   would have made it a second hero; as line work on paper it reads as an
   architectural section, which is both more on-brand and more premium.
   ========================================================================== */

.reel-on #sc-roots { height: 360vh; }
#sc-roots .stage {
  background: var(--grad-cream);
  perspective: none;
  display: grid; place-items: center;
}
#sc-roots .stage::after { background: linear-gradient(180deg, rgba(241,242,222,0) 0%, var(--cream-2) 100%); }

.roots-art {
  position: absolute; left: 4%; top: 50%;
  width: min(52vw, 680px); aspect-ratio: 1;
  /* We do not zoom the tree. The camera descends, following the roots down,
     which is why the trunk stays put while the ground line leaves the frame. */
  transform: translateY(calc(-50% - var(--roots,0) * 9%)) scale(calc(1 + var(--grow,0) * .06));
}
.roots-art svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* canopy lifts and opens a little; the trunk does not move, which is what
   makes the growth read as growth rather than as a zoom */
.canopy { fill: #16371F; transform-origin: 442px 214px;
  transform: translateY(calc(var(--grow,0) * -16px)) scale(calc(1 + var(--grow,0) * .07)); }
.canopy ellipse:nth-child(2) { transform: translateX(calc(var(--grow,0) * -14px)); }
.canopy ellipse:nth-child(3) { transform: translateX(calc(var(--grow,0) * 14px)); }
.limbs { opacity: .9; }

.gline {
  stroke: var(--oak-deep); stroke-width: 1.5; stroke-dasharray: 5 7;
  opacity: calc(.85 - var(--morph,0) * .85);
}

/* Roots draw outward from the trunk. Each carries its own --d so the thick
   structural roots are established before the fine ones appear. */
.rt {
  stroke: var(--oak-deep);
  stroke-dasharray: 1; stroke-dashoffset: 1;
  stroke-dashoffset: calc(1 - min(1, max(0, (var(--roots,0) - var(--d,0)) * 2.4)));
  opacity: calc(.92 - var(--morph,0) * .34);
}

/* The strata the roots run through resolve into the five roof materials.
   No caption says so. It just happens under the sentence that says it. */
/* Held under full strength so the roots still read across them — the roots
   running THROUGH the materials is the metaphor; the materials alone are just
   a section drawing. */
.strata .bd { opacity: calc(var(--morph,0) * .82); }

#sc-roots .cap { top: 22vh; left: auto; right: clamp(18px, 6vw, 80px); width: min(460px, 42vw); }
#sc-roots .cap h2, #sc-roots .cap blockquote { color: var(--forest); }
#sc-roots .cap p { color: var(--ink-soft); }
#sc-roots .eyebrow-o { color: var(--oak-deep); }
#sc-roots blockquote {
  margin: 0; padding-left: 22px; border-left: 4px solid var(--oak);
  font-family: var(--f-display); font-size: clamp(1.16rem, 1rem + .8vw, 1.5rem);
  font-weight: 600; line-height: 1.35; color: var(--forest);
}
/* The quote is the centrepiece: it arrives first and holds while the roots
   draw. The supporting copy follows, by line, once they are established. */
.oak-quote { opacity: calc(min(var(--grow,0) * 2.4, 1)); }
.oak-body  { opacity: calc(max(0, (var(--roots,0) - .28) * 2.6)); }
.oak-cta   { opacity: calc(max(0, (var(--morph,0) - .35) * 2.4)); }

/* On a phone this is recomposed rather than scaled: the copy takes the top of
   the frame and the tree is anchored to the bottom of it, so the two never
   share space. The type tightens to buy the artwork room. */
@media (max-width: 860px) {
  .reel-on #sc-roots { height: 260vh; }
  .roots-art {
    left: 50%; top: auto; bottom: 1vh; width: min(94vw, 360px); aspect-ratio: 1;
    transform: translate(-50%, calc(var(--roots,0) * -5%)) scale(calc(1 + var(--grow,0) * .04));
  }
  #sc-roots .cap { top: 9vh; left: 18px; right: 18px; width: auto; }
  #sc-roots blockquote { font-size: 1.08rem; padding-left: 16px; }
  #sc-roots .cap p { font-size: .93rem; margin-top: 10px; }
  #sc-roots .oak-cta { margin-top: 14px !important; }
}

@media (prefers-reduced-motion: reduce) {
  .roots-art { position: relative; left: auto; top: auto; transform: none; width: min(88vw, 460px); }
  .rt { stroke-dashoffset: 0; }
  .strata .bd { opacity: 0; }
  .oak-quote, .oak-body, .oak-cta { opacity: 1; }
}

/* ==========================================================================
   SC 07 · STORM — INVISIBLE FROM THE GROUND
   The headline is the interaction premise. The camera travels from a ground
   view to macro in one continuous push across three cross-faded scales, and
   the damage only resolves at the last one — so the visitor discovers it at
   the distance an inspector would, rather than being told about it.
   ========================================================================== */

.reel-on #sc-storm { height: 440vh; }
#sc-storm .stage { background: #030F08; }

/* Weather grade. Darkens toward the middle of the scene and clears again —
   no cartoon clouds, just the light going out of the sky and coming back. */
.st-sky {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background: linear-gradient(180deg, rgba(4,14,22,.92) 0%, rgba(6,20,14,.6) 46%, rgba(3,15,8,.88) 100%);
  opacity: calc(min(var(--weather,0) * 2.4, (1 - var(--weather,0)) * 2.8) * .62);
}

/* Rain as two sheared sheets at different angles and rates. Particles would
   read as decoration; sheets read as weather seen through a window. */
.st-rain {
  position: absolute; inset: -30% -20%; z-index: 3; pointer-events: none;
  opacity: calc(min(var(--weather,0) * 2.6, (1 - var(--weather,0)) * 3) * .5);
}
.st-rain.a {
  background: repeating-linear-gradient(74deg, rgba(214,228,220,.30) 0 1px, transparent 1px 7px);
  animation: st-fall-a 1.1s linear infinite;
}
.st-rain.b {
  background: repeating-linear-gradient(70deg, rgba(214,228,220,.18) 0 1px, transparent 1px 13px);
  animation: st-fall-b 1.7s linear infinite;
}
@keyframes st-fall-a { to { transform: translate3d(-90px, 300px, 0); } }
@keyframes st-fall-b { to { transform: translate3d(-140px, 460px, 0); } }

/* Three scales, cross-faded, so the push never cuts. */
.st-wide {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  transform: scale(calc(1 + var(--climb,0) * 4.2));
  opacity: calc(1 - var(--climb,0) * 1.35);
}
.st-wide svg { width: 100%; height: 100%; display: block; }

.st-roof, .st-macro { position: absolute; inset: -8%; z-index: 2; pointer-events: none; }
.st-roof {
  background: linear-gradient(160deg, rgba(226,255,236,.14), rgba(0,0,0,.34)), url("/assets/tex/shingle.webp");
  background-size: 100% 100%, 210px 210px;
  transform: rotate(-2deg) scale(calc(.55 + var(--close,0) * 1.7));
  opacity: calc(min(var(--close,0) * 2.6, (0.86 - var(--macro,0)) * 3.2));
}
.st-macro { transform: rotate(-2deg) scale(calc(.72 + var(--macro,0) * .8)); opacity: calc(var(--macro,0) * 2.4); }
.st-macro i {
  position: absolute; inset: 0; display: block;
  background-size: 100% 100%, 430px 430px;
}
.st-macro .sound { background-image: linear-gradient(160deg, rgba(226,255,236,.14), rgba(0,0,0,.30)), url("/assets/tex/shingle.webp"); }
/* The damage is the last thing to arrive, and only at the closest scale. */
.st-macro .dmg   { background-image: linear-gradient(160deg, rgba(226,255,236,.14), rgba(0,0,0,.30)), url("/assets/tex/shingle-dmg.webp");
                   opacity: calc(max(0, var(--macro,0) - .42) * 2.8); }

/* Inspection markers: a hairline, a small filled circle, a label. Architectural
   annotation, not a HUD. */
.st-marks { position: absolute; inset: 0; z-index: 5; pointer-events: none; }
.mk {
  position: absolute; display: grid; gap: 2px; width: 190px;
  opacity: calc(min((var(--marks,0) - var(--due,0)) * 7, 1));
}
.mk b { font: 700 .64rem/1.2 var(--f-body); letter-spacing: .16em; text-transform: uppercase;
        color: var(--oak-bright); text-shadow: 0 1px 6px rgba(2,12,6,.95); }
.mk span { font: 400 .8rem/1.3 var(--f-body); color: var(--cream);
           text-shadow: 0 1px 7px rgba(2,12,6,.95); }
.mk::before { content: ""; position: absolute; left: -30px; top: 7px; width: 24px; height: 1px;
  background: var(--oak-bright); box-shadow: 0 0 7px rgba(201,174,107,.7); }
.mk::after { content: ""; position: absolute; left: -35px; top: 4px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--oak-bright); box-shadow: 0 0 10px rgba(201,174,107,.9); }
.mk-1 { left: 56%; top: 24%; --due: .04; }
.mk-2 { left: 62%; top: 42%; --due: .22; }
.mk-3 { left: 57%; top: 60%; --due: .40; }
.mk-4 { left: 61%; top: 76%; --due: .58; }

.st-cap-a { top: 20vh; opacity: calc(1 - var(--close,0) * 1.5); }
.st-cap-b { top: 19vh; opacity: calc(max(0, var(--macro,0) - .30) * 3); }
#sc-storm .cap ul { margin: 14px 0 0; padding: 0; list-style: none; display: grid; gap: 9px; }
#sc-storm .cap li { display: grid; grid-template-columns: 22px 1fr; gap: 4px; color: var(--cream); font-size: .95rem; }
#sc-storm .cap li::before { content: "✓"; color: var(--oak-bright); font-weight: 700; }
#sc-storm .fine { font-size: .84rem; color: #93A096; margin-top: 12px; }

/* ==========================================================================
   SC 15 · CLOSE — THE PULL-BACK
   The inverse of the opening, with the same artwork run backwards. After
   twenty-odd screens of movement, the page stops, and the phone number is the
   last thing standing.
   ========================================================================== */

.reel-on #sc-close { height: 270vh; }
#sc-close .stage { background: #04180B; }
#sc-close .pl-sky   { transform: scale(calc(1 + (1 - var(--back,0)) * .5)); }
#sc-close .pl-far   { transform: translateY(calc(var(--back,0) * 4vh)) scale(calc(.8 + (1 - var(--back,0)) * 1.3)); }
#sc-close .pl-oak   { transform: translate3d(calc((1 - var(--back,0)) * -26vw), calc(var(--back,0) * 5vh + (1 - var(--back,0)) * 10vh), 0) scale(calc(.74 + (1 - var(--back,0)) * 2.1)); }
#sc-close .pl-house { transform: translateY(calc(var(--back,0) * 6vh)) scale(calc(.66 + (1 - var(--back,0)) * 2.9)); }
#sc-close .pl-fore  { transform: translateY(calc((1 - var(--back,0)) * 22vh)) scale(calc(1 + (1 - var(--back,0)) * 3)); opacity: calc(var(--back,0) * 1.2); }

.cl-mark {
  position: absolute; z-index: 5; width: min(148px, 21vw); left: 50%; top: 11%;
  transform: translateX(-50%) scale(calc(.86 + var(--mark,0) * .14));
  opacity: calc(var(--mark,0) * 1.2);
}
.cl-mark img { width: 100%; height: auto; display: block; }
.cl-cap {
  left: 50%; right: auto; top: 53%; width: min(620px, 88vw);
  transform: translateX(-50%);
  text-align: center; z-index: 6;
  opacity: calc(var(--cta,0) * 1.3);
}
.cl-cap .btnrow { justify-content: center; }
.cl-cap h2 { font-size: clamp(1.7rem, 1.1rem + 2.4vw, 2.8rem); }
.cl-cap .fine { font-size: .86rem; color: var(--oak-bright); margin-top: 16px; }

/* ==========================================================================
   Supporting motion — restrained on purpose
   ========================================================================== */

/* Stats: the figures are uncovered left to right, not counted up. Only two of
   the five are numeric; an odometer would work on "100+" and look broken on
   "Lifetime". */
#stats-band .stat .n, #stats-band .stat .l {
  clip-path: inset(0 calc((1 - min(1, max(0, (var(--p,0) - var(--i,0) * .07) * 3.4))) * 100% 0 0));
}
#stats-band .stat { --i: 0; }
#stats-band .stat:nth-child(2) { --i: 1; }
#stats-band .stat:nth-child(3) { --i: 2; }
#stats-band .stat:nth-child(4) { --i: 3; }
#stats-band .stat:nth-child(5) { --i: 4; }

/* Services: each card takes the floor in turn. The inactive ones stay legible
   — this is a reading section, so nothing moves, only emphasis changes. */
#services .card { --i: 0; transition: opacity .4s ease, border-color .4s ease; }
#services .cards > .card:nth-child(2) { --i: 1; }
#services .cards > .card:nth-child(3) { --i: 2; }
#services .cards > .card:nth-child(4) { --i: 3; }
#services .card {
  /* Activation runs across the middle of the section's pass, not its whole
     travel, so no card is "current" before the section is even readable. */
  --dist: calc((var(--p, 0) - .18) * 5 - var(--i));
  --act: clamp(0, calc(1 - max(var(--dist), calc(var(--dist) * -1)) * 1.5), 1);
  /* Dimming exists ONLY while the section is actually being read. Outside that
     window every card is at full strength, so a visitor who lands mid-page or
     scrolls back never meets permanently faded body copy. A card left dimmed
     is a readability bug, not a design decision. */
  --win: clamp(0, calc(min((var(--p, 0) - .10) * 8, (.92 - var(--p, 0)) * 8)), 1);
  --dim: calc(1 - var(--act));
  position: relative; overflow: hidden;
}
#services .card {
  opacity: calc(1 - var(--win) * var(--dim) * .18);
  border-bottom-color: color-mix(in srgb, var(--oak) calc(var(--act) * var(--win) * 100%), var(--shingle));
}
#services .card:hover { opacity: 1; }
.svc-mat {
  position: absolute; left: 0; right: 0; top: 0; height: 7px; display: block;
  background-size: 100% 100%, 150px 150px;
  clip-path: inset(0 calc((1 - var(--act) * var(--win)) * 100%) 0 0);
}
.card.sv-replace .svc-mat { background-image: linear-gradient(90deg, rgba(226,255,236,.2), rgba(0,0,0,.2)), url("/assets/tex/shingle.webp"); }
.card.sv-repair  .svc-mat { background-image: linear-gradient(90deg, rgba(255,248,222,.3), rgba(0,0,0,.2)), url("/assets/tex/flash.webp"); }
.card.sv-storm   .svc-mat { background-image: linear-gradient(90deg, rgba(226,255,236,.2), rgba(0,0,0,.2)), url("/assets/tex/shingle-dmg.webp"); }
.card.sv-ext     .svc-mat { background-image: linear-gradient(90deg, rgba(255,246,220,.2), rgba(0,0,0,.2)), url("/assets/tex/deck.webp"); }

/* Comparison: the row label, then our column, then — a beat later — the cheap
   quote. That delay is the whole point of the interaction. */
#compare tbody tr { --i: 0; }
#compare tbody tr:nth-child(2) { --i: 1; } #compare tbody tr:nth-child(3) { --i: 2; }
#compare tbody tr:nth-child(4) { --i: 3; } #compare tbody tr:nth-child(5) { --i: 4; }
#compare tbody tr:nth-child(6) { --i: 5; } #compare tbody tr:nth-child(7) { --i: 6; }
#compare tbody th          { opacity: calc(min(1, max(0, (var(--p,0) - var(--i) * .055) * 6))); }
#compare tbody td.us       { opacity: calc(min(1, max(0, (var(--p,0) - .02 - var(--i) * .055) * 6))); }
#compare tbody td.them     { opacity: calc(min(1, max(0, (var(--p,0) - .07 - var(--i) * .055) * 5))); }

/* Process: the hero takes a roof apart, so this one puts one on. Courses lay
   in from the eave up as each step becomes current. */
.pr-roof {
  position: relative; height: 92px; margin: 0 0 26px; border: 1px solid var(--cream-3);
  border-bottom: 4px solid var(--forest); border-radius: 2px; overflow: hidden;
  background: linear-gradient(148deg, rgba(255,246,220,.22), rgba(0,0,0,.14)), url("/assets/tex/deck.webp");
  background-size: 100% 100%, 130px 130px;
}
.pr-roof i {
  position: absolute; left: 0; right: 0; display: block; height: 28px;
  background-size: 100% 100%, 118px 100%;   /* tile across, never squash down */
  box-shadow: 0 -2px 0 rgba(0,0,0,.42);
  clip-path: inset(0 calc((1 - min(1, max(0, (var(--p,0) - .12 - var(--s) * .19) * 7))) * 100% 0 0));
}
.pr-roof i:nth-child(1) { --s: 0; bottom: 0;    background-image: linear-gradient(180deg, rgba(255,255,255,.16), rgba(0,0,0,.22)), url("/assets/tex/under.webp"); }
.pr-roof i:nth-child(2) { --s: 1; bottom: 22px; background-image: linear-gradient(180deg, rgba(226,255,236,.22), rgba(0,0,0,.24)), url("/assets/tex/shingle.webp"); }
.pr-roof i:nth-child(3) { --s: 2; bottom: 44px; background-image: linear-gradient(180deg, rgba(226,255,236,.22), rgba(0,0,0,.24)), url("/assets/tex/shingle.webp"); }
.pr-roof i:nth-child(4) { --s: 3; bottom: 66px; height: 24px; background-image: linear-gradient(180deg, rgba(255,248,222,.42), rgba(0,0,0,.2)), url("/assets/tex/flash.webp"); }

@media (max-width: 860px) {
  .reel-on #sc-storm { height: 320vh; }
  .reel-on #sc-close { height: 190vh; }
  .st-marks { display: none; }
  #sc-storm .cap { left: 18px; right: 18px; width: auto; }
  .st-cap-a, .st-cap-b { top: 10vh; }
  .st-rain { animation-duration: 1.6s, 2.2s; }
  .cl-cap { top: 44%; }
  .pr-roof { height: 74px; }
}

@media (prefers-reduced-motion: reduce) {
  .st-rain { display: none; }
  .st-wide, .st-roof, .st-macro { transform: none; }
  .st-roof { display: none; }
  .st-macro { opacity: 1; position: relative; inset: auto; height: 46vh; }
  .st-cap-a, .st-cap-b { position: static; opacity: 1; }
  .st-marks { display: none; }
  .cl-mark, .cl-cap { position: static; opacity: 1; transform: none; }
  #stats-band .stat .n, #stats-band .stat .l { clip-path: none; }
  #services .card { opacity: 1; }
  .svc-mat { clip-path: none; }
  #compare tbody th, #compare tbody td { opacity: 1 !important; }
  .pr-roof i { clip-path: none; }
}

@media print {
  .st-rain, .st-marks, .cl-mark { display: none !important; }
  #stats-band .stat .n, #stats-band .stat .l { clip-path: none !important; }
  #services .card, #compare tbody th, #compare tbody td { opacity: 1 !important; }
  .pr-roof i { clip-path: none !important; }
}
