/* checkpoint 77 ruling: --paper moves off the old near-white #FCFBF8 to
   #E5E1CD, the warm cream sampled directly off the second image set's own
   ground (AI/_IMAGES_2/1-6.png -- corner/edge pixels of all six, mode
   colour per image, averaged: #E6E2D0, #E9E5D1, #E5E1CD, #E5E1CD, #E6E1CD,
   #E1DDCA -> consensus #E5E1CD). The illustrations sit on the SAME ground
   the page is now painted, per the operator's imperative that the
   sketches read as part of the page rather than placed onto it.
   Redeclared here rather than at its source in css/tokens.css: this
   checkpoint's write set is site.css, not tokens.css, so the token is
   overridden at the cascade instead -- every page loads tokens.css then
   this stylesheet straight after it (see each <head>), and a later
   :root rule of equal specificity wins, so this is a genuine, complete
   override, not a partial one. --raised stays pure white (#FFFFFF): it
   is the postcard/card device's own "raised paper" surface, a
   different, unrelated token by design, untouched by this ruling. */
:root{--paper:#E5E1CD}
*{box-sizing:border-box}
/* checkpoint 71 ruling 2: every page links this stylesheet, so reserving the
   scrollbar's gutter here, once, keeps the fixed header's available width
   (left:0 to right:0) identical whether or not a given page's content is
   tall enough to actually need a scrollbar. Without this, a short page
   (packages/confirmation/testimonials) rendered a few pixels WIDER than a
   tall one (index/begin), and the header's centred brand mark and
   right-aligned nav shifted sideways by the scrollbar's own width the
   instant a visitor navigated between them -- root cause (b) of the
   checkpoint 71 header-shift ruling, verified by measuring .bar/.u-bar
   rects before this change (see HANDOFF/checkpoint notes). both-edges
   keeps the reserved gutter symmetric even though the scrollbar itself
   only ever paints on the right.
   checkpoint 79 ruling, FULL BLEED, amended 2026-08-28: "both-edges"
   drops to plain "stable". The footer read with a thin cream strip at
   both true viewport edges -- operator instruction, "the footer should
   be full bleed from left to right." Diagnosis, each step verified
   against an actual rendered screenshot, not layout math alone (layout
   math alone is what shipped two wrong fixes first): footer's own box
   was inset by the reserved gutter on both sides (~15px each at 1440px
   wide, this engine). Retargeting footer's own width/margin (the
   textbook 100vw trick, then a live-JS-measured pixel version) both
   made getBoundingClientRect report the correct true-edge box, yet a
   real screenshot of the exact same page still showed the identical
   cream strip, unchanged -- confirmed with a plain, unrelated throwaway
   element using the same technique, and with body's overflow-x:hidden
   (checkpoint 77, below) toggled off live, which also changed nothing.
   Conclusion: scrollbar-gutter's reservation is a paint-level clip on
   the root scrolling box's own scrollport -- no CSS or JS on any
   descendant can make painted content cross it, however its layout box
   measures. The one thing verified to reach the true edge was the
   document root's own FLAT background-colour (the standard CSS "canvas
   background" exemption -- html{background:red} painted red at the
   true edge in a screenshot where every element-level trick had
   failed); a background-IMAGE on the same root element was tested too
   and did NOT get the same exemption (still confined to the gutter-
   reduced box), so that route dead-ends at a flat, page-wide colour --
   no way to scope it to just the footer's own vertical range without
   painting a visible stripe past every OTHER section too, which is not
   what was asked for.
   The gutter itself was the only remaining lever, and only "both-edges"
   needed to move: the LEFT-side reservation is a pure symmetry mirror
   with no real scrollbar ever drawn under it (nothing is ever lost by
   letting content reach it), while the RIGHT-side reservation is where
   an actual, physical browser scrollbar renders when the page overflows
   -- content painting under a real scrollbar is not achievable on any
   website, in any browser, full stop, so no CSS change here or
   anywhere else could ever have satisfied a literal "zero pixels of any
   colour under the scrollbar itself" reading. Dropping to plain
   "stable" removes only the left-side mirror; verified on both index.html
   (long, genuinely overflowing) and packages.html (short, scrollHeight
   816 against a 900 viewport, i.e. never actually overflowing) that the
   right-side reservation still holds at the SAME 15px on both, with or
   without this change -- checkpoint 71's actual bug (width inconsistency
   between a scrolling and a non-scrolling page) is fully intact, proven
   before and after. The one real side effect: the fixed header's
   content, previously dead-centred within a symmetric [15,1425] box, now
   centres within an asymmetric [0,1425] box -- shifted right of true-
   centre by half the gutter's own width (~7.5px at 1440 wide). Every
   page shifts identically (still perfectly consistent with itself), and
   the shift is small enough to read as within normal design tolerance
   at a glance, but it IS a real, sitewide, unrequested visual change
   this ticket did not name -- flagged in the checkpoint 79 report rather
   than silently absorbed. */
/* checkpoint 89 fix, 2026-08-28, part of the mobile sweep: body already
   carries its own overflow-x:hidden (checkpoint 77, immediately below),
   and by spec that should propagate up to govern the viewport since html
   itself declares no overflow of its own -- but measured on this engine
   at 390 wide, it did not: document.documentElement.scrollWidth read
   405px against a 375/390px box, and window.scrollTo(9999,0) actually
   moved scrollX, a real, draggable sideways scroll exposing bare paper
   past the right edge (root cause was two genuine mobile layout bugs,
   fixed at their own source elsewhere in this file this same checkpoint:
   the shadowed .handnote white-space rule, and #vow .zone's unscoped
   desktop transform; a card's tape corner also bleeds a little past its
   own box by design, see .card .tape below). Declaring overflow-x:hidden
   on html directly, rather than relying on body's to propagate, is the
   standard belt-and-braces fix for exactly this propagation ambiguity
   and is the one line added here; scrollbar-gutter:stable and every word
   of the checkpoint 71/77 history on the line below it are untouched. */
html{scrollbar-gutter:stable;overflow-x:hidden}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--serif);overflow-x:hidden}
/* checkpoint 77: --paper moves to the sampled image-ground cream (see
   tokens.css). Every key pairing re-measured with the WCAG relative-
   luminance formula against the new #E5E1CD:
     hero sentence ink #2B2A26 on #E5E1CD          : 10.93:1
     body copy #3b3934 on #E5E1CD                  : 8.78:1
     footer .foot-bar copy on #E5E1CD              : 4.69:1 (was 5.44:1
       on the old #FCFBF8 -- the darker cream ate the margin, so the
       hex moves from #6b675f to #656159 below, a change too small to
       read as a different colour, to hold 4.5:1 AA)
     tape button label on its own tape's effective red (vermillion at
       .9 alpha over #E5E1CD, #C94B3B)             : 4.62:1 (the label
       used to render in var(--paper), which worked only because the
       old paper token was already near white -- on the new cream that
       pairing fell to 3.51:1, so the label now renders in var(--raised)
       (pure white) instead, decoupling it from the page-ground token;
       visually identical to how it always looked, since --raised and
       the old --paper were barely distinguishable)
     chip red var(--verm) on its own chip backing (now #E5E1CD at .82
       alpha)                                      : 3.96:1 (meta-
       register debug annotation, never shipped copy -- same exemption
       the chip's dashed border already carries, unchanged risk band)
   Two real regressions found and fixed (footer copy, tape label); the
   other three were already comfortably clear and stay clear. */
body.veil-open{overflow:hidden}
/* checkpoint 74 ruling 2, RETIRED 2026-08-27: the three radial-gradient dot
   layers below were the site's own CSS-generated stand-in for paper grain,
   dated back before a real texture asset existed. They now double up
   against the photographed grain tile applied by the rule that replaces
   this one (immediately below), so they come off -- commented rather than
   deleted, per house practice, so the history stays on record. --paper and
   the rest of the token block are UNCHANGED by this ruling: the ground is
   still the flat colour, texture is layered on top of it exactly per
   guidelines section 1.1 ("the paper character now comes from TEXTURE, not
   from tint"), and no colour token or contrast pairing moves.
body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.5;
  background-image:radial-gradient(circle at 16% 24%,rgba(43,42,38,.028) 0 1px,transparent 1.5px),
   radial-gradient(circle at 60% 70%,rgba(43,42,38,.022) 0 1px,transparent 1.5px),
   radial-gradient(circle at 84% 30%,rgba(43,42,38,.018) 0 1px,transparent 1.5px);
  background-size:53px 61px,73px 67px,97px 83px}
*/
/* checkpoint 74 ruling 2: the grainy white paper texture (source
   INSPIRATION/TEXTURES/6a7b962f38c7b61ba7f76d886aa63796.jpg, optimised web
   copy at ../assets/paper_grain.jpg, 438x640, 62KB -- RETIRED at checkpoint
   100, 2026-08-30, replaced by assets/paper_relief.webp; see the ground
   rule below) becomes the ground of
   every page that links this stylesheet -- index and the four utility
   pages alike, no per-page markup needed since they all already share this
   one rule. It sits on this SAME body::before layer the retired dot-grain
   used (fixed, inset:0, pointer-events:none, z-index:0 -- i.e. under every
   real section, canvas included, exactly as before), so it is texture
   layered UNDER the site's colour tokens: --paper stays body's own flat
   background-color underneath, nothing here overrides --paper, --raised or
   any wash token, and mix-blend-mode:multiply means the texture can only
   ever darken toward the grain, never tint or lighten past the ground
   colour. Tiled rather than covered (cp74 operator ruling gave either
   option) so the asset is never upscaled past its native resolution on a
   large screen, which would soften the grain; a photographed paper grain
   has no strong directional feature, so the repeat seam is not visually
   legible at this opacity.
   checkpoint 77 retune: --paper moved from near-white #FCFBF8 to the
   sampled cream #E5E1CD (tokens.css). multiply darkens toward the grain
   proportionally to how far the backdrop already sits below white, so
   the SAME .5 opacity that was imperceptible against near-white paper
   (measured mean grain tone 234/255, i.e. a ~1.2% darkening of a
   backdrop that was already 252) would pull the new, genuinely-coloured
   cream down by a measured ~4% on average -- a visible, uneven dulling
   the seamlessness law does not allow, since a placed image's own
   ground (matched to the sampled cream, not to a grain-darkened
   version of it) would then sit measurably lighter than the flat page
   around it. Opacity comes down to .14 -- grain still visibly rides the
   ground (verified in the P21 screenshots) but the average composed
   darkening is back under 3 levels, inside the seamlessness law's own
   "a couple of levels" tolerance (see the rendered-vs-sampled
   measurement recorded in HANDOFF for this checkpoint). */
/* checkpoint 100 ruling, 2026-08-30: the paper TRAVELS with the page.
   position:fixed pinned the sheet to the screen and slid the content over
   it, which read as a backdrop rather than as one long sheet; absolute on
   a position:relative body spans the whole document instead. The texture
   is the operator's own INSPIRATION/TEXTURES/6a7b96... at NATIVE scale --
   never upscaled, which is what made an earlier pass look blurry -- built
   into a MIRRORED 2x2 tile (1480x2160) so the repeat period doubles, with
   relief raised 1.9x around the mean so the tooth deepens while the
   average tone stays exactly --paper. At 2x device pixels the size halves
   so one texture pixel lands on one device pixel. Opacity is 1: the
   strength lives in the asset, not in a fade. */
/* checkpoint 100 fix, 2026-08-31: min-height:100% here was INERT and shipped a
   regression. A percentage min-height resolves against the containing block's
   HEIGHT, and html (line 97 above) sets no height -- so it computed to nothing and
   the absolute ground stopped at the content bottom. On any page shorter than the
   screen that left a flat, textureless band of raw --paper under the footer:
   measured 224px on testimonials at 1024x1366, 318px on payment at 1440x1400,
   with a hard 9.3-level edge. The old position:fixed ground never had this because
   it was pinned to the viewport. Viewport units resolve unconditionally, so they
   are the fix. 100lvh (LARGEST viewport height) rather than svh/dvh deliberately:
   on a phone the ground must still reach the bottom when the browser toolbars
   RETRACT and the visible area grows, which is exactly when an svh-sized ground
   would expose the band again. 100vh first as the fallback for engines without
   the newer units. */
body{position:relative;min-height:100vh;min-height:100lvh}
body::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:0;opacity:1;
  background-image:url(../assets/paper_relief.webp?v=cp100b);
  background-repeat:repeat;background-position:top left;background-size:1480px 2160px;
  mix-blend-mode:multiply}
@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi),(min-resolution:2dppx){
  body::before{background-size:740px 1080px}
}

/* ── one continuous canvas behind every section. the film replaces this whole layer ── */
#canvas{position:absolute;inset:0;z-index:auto;pointer-events:none;overflow:hidden}
/* lvh, NOT svh -- checkpoint 125. The ground above is sized 100lvh for a
   documented reason: on a phone or tablet the browser toolbars RETRACT as you
   scroll and the visible area GROWS. The hero was left on svh, the opposite
   unit, so it was sized against the SMALL viewport while the ground under it
   was sized against the LARGE one. On iPad that read as the hero section and
   its film changing size while scrolling. Both ends now agree. vh first as the
   fallback for engines without the newer units. */
#canvas .c-open{position:absolute;top:0;left:0;width:100%;height:102vh;height:102lvh}
/* checkpoint 126: when scroll.js has locked the viewport unit (see the long note
   at the top of scroll.js -- Chrome on iPad resizes the page's viewport when its
   toolbar hides, so every viewport unit moves mid-scroll), the hero geometry
   switches to the frozen pixel unit. Ordered after the lvh rules so it wins only
   when .vh-locked is present. */
@media(min-width:641px){
  /* scoped to 641px+ on purpose: below 640 the phone cascade sizes .c-open by
     its content (height:auto, checkpoint 94) and these higher-specificity
     locked rules would silently override that approved behaviour. The phone
     hero never used lvh sizing, so it never had this fault to fix. */
  html.vh-locked #canvas .c-open{height:calc(var(--vhL)*102)}
  html.vh-locked #canvas .c-open::after{height:calc(var(--vhL)*16)}
  html.vh-locked #canvas .b1{top:calc(var(--vhL)*96);height:calc(var(--vhL)*26)}
  html.vh-locked #canvas .b2{top:calc(var(--vhL)*92);height:calc(var(--vhL)*34)}
  html.vh-locked #canvas .b3{top:calc(var(--vhL)*98);height:calc(var(--vhL)*20)}
  html.vh-locked .opening{min-height:calc(var(--vhL)*102)}
}
/* checkpoint 77: mix-blend-mode:multiply dropped, same seamlessness
   reasoning as .artcol/.chapter .zone below -- it only stayed invisible
   against a near-white paper, and the hero image's own cream ground
   needs to meet var(--paper) cleanly where ::after fades to it a few
   lines down. Full-bleed top-to-side, so no mask is needed here (no
   rectangle edge exists except the bottom, already handled by the
   fade-to-paper gradient). */
#canvas .c-open img{width:100%;height:100%;object-fit:cover;mix-blend-mode:multiply}
/* checkpoint 92 ruling, 2026-08-28, RETIRED: the operator ruled "i dont want
   any part of the image to consist of a fade, so please remove the fade
   completely". The bottom gradient below faded the hero art into the paper;
   the image's own ground already matches the page cream, so its natural
   edge stands. Kept for history, not deleted:
#canvas .c-open::after{content:"";position:absolute;left:0;right:0;bottom:0;height:16vh;height:16lvh;
  background:linear-gradient(to bottom,transparent,var(--paper))}
*/
/* checkpoint 89 ruling, 2026-08-28: the operator named "these faint lines
   that are placed between the main hero page and the second section of
   the site" -- three gradient tendril lines, one per accent token colour,
   differing heights and opacity .28, exactly matching his screenshot.
   Retired, not deleted, per house practice; the markup they styled is
   retired the same checkpoint in index.html, next to this same note.
#canvas .bridge{position:absolute;width:2px;border-radius:0 0 2px 2px;opacity:.28}
#canvas .b1{left:23%;top:96vh;top:96lvh;height:26vh;height:26lvh;background:linear-gradient(var(--misty),transparent)}
#canvas .b2{left:79%;top:92vh;top:92lvh;height:34vh;height:34lvh;background:linear-gradient(var(--moss),transparent)}
#canvas .b3{left:61%;top:98vh;top:98lvh;height:20vh;height:20lvh;background:linear-gradient(var(--ochre),transparent)}
*/
/* checkpoint 89 ruling, 2026-08-28: the hero graphic (art2/art1.jpg) is a
   wide image, blossom branches in the top-right and bottom-left corners,
   an empty middle -- object-fit:cover above keeps only that empty middle
   on a narrow tall phone box, so the artwork read as absent, the
   operator's own finding ("the graphic in the main hero section wasnt
   even visible in mobile view"). Fixed with an art-directed PORTRAIT
   rendition cut from the same source (AI/_IMAGES_2/1.png, read only),
   keeping the top-right branch and generous clear paper below it, served
   under 640px through the <picture><source> in index.html; the desktop
   <img src> and this shared rule are untouched, so nothing above 640px
   moves. object-position is set to the top-right corner ONLY inside this
   same breakpoint -- SUPERSEDED in part at checkpoint 93 below (object-fit
   moves to contain in this same breakpoint), object-position itself is
   untouched and still does real work there (see that note). */
/* checkpoint 93 ruling, 2026-08-28: the operator, viewing the live mobile
   page, "i also dont want the image to be cropped cut at the bottom, so
   please move it up more so that the bottom part of the illustration isnt
   cut/cropped." Measured: the checkpoint-91 crop (art1_mobile.jpg, 644x430)
   carried real ink -- leaf material, not bare paper -- all the way to its
   own last pixel row (row 429 of 430), because that crop's own bottom edge
   was cut mid-cluster; checkpoint 91's note above believed the crop ended
   "at its natural taper," but the measured file did not. Two changes, both
   scoped to this same breakpoint so nothing above 640px moves and nothing
   else in this rule changes: (1) art1_mobile.jpg is re-cut from the same
   read-only source (AI/_IMAGES_2/1.png), same top-right window (x:700-1344)
   but taller -- 644x650 rather than 644x430 -- chosen with margin past
   where a dist>40 (strong-ink) row-scan reads zero for a sustained band
   (source rows 609-751 carry no hard ink at all; the last hard ink, a
   small bud cluster, sits at row 607, so the crop's own bottom edge at
   650 clears it by 43 source px), i.e. well past every leaf, petal and
   bud, into the thin trailing drip and stray flecks the operator did not
   object to. (2) #canvas .c-open img gets object-fit:contain in this same
   breakpoint (was cover, shared rule above still sets cover for desktop,
   untouched): the new crop's own aspect (644/650, near-square) no longer
   sits close to the 390-430 phone-width box's own aspect the way the 3:2
   crop did, so cover would crop again; contain shows the full re-cut
   image with no crop at all, letterboxing only on the box's left
   (object-position:top right, kept from checkpoint 89, pushes the fit
   image to the box's right/top). The letterbox band is not a fade -- it
   is the page's own flat --paper cream showing through #canvas's own
   transparent background, sampled against the new crop's own left-edge
   ground: within ~16 units (a couple of levels, the same tolerance the
   checkpoint 77 grain retune used) at every sampled row, so the seam does
   not read as a visible band. Box height (#canvas .c-open{height:35vh}
   below) and #markseat/.opening .sentence positions are untouched --
   contain never grows the box, so the crown's own footprint on screen is
   unchanged from checkpoint 91. Measured against the rendered screenshot
   at both widths: bottommost surviving ink sits 19px above the 295px-tall
   box's own bottom edge at 390 wide (6.4% clear margin) and 21px above
   the 326px-tall box's own bottom edge at 430 wide (also 6.4%) -- nothing
   crosses either box's bottom edge at either width. Proof:
   proof/P31_hero_390.png.
   RETIRED checkpoint 94, 2026-08-29: superseded by the checkpoint 94 rule
   further down this file (immediately after the checkpoint 91 note below)
   -- with #canvas .c-open no longer given a fixed mobile height, there is
   no box left for object-fit/object-position to fit the image against, so
   both properties come off at this breakpoint entirely. Retired, not
   deleted; history stays on record.
@media(max-width:640px){
  #canvas .c-open img{object-position:top right;object-fit:contain}
}
*/
/* checkpoint 91 ruling, 2026-08-28: the operator, viewing the live mobile
   page, ruled the checkpoint-89 portrait crop wrong in the other direction
   -- "the illustration currently takes the whole phone screen and the
   sentence sits too far down." #canvas .c-open carries no mobile height
   override at all before this ruling, so it inherits the shared
   102svh rule above at every breakpoint; on a phone box that means the
   branch image fills the entire first screen behind the mark and
   sentence rather than sitting as a crown above them. Two changes fix
   this, both scoped to this same breakpoint so nothing above 640px
   moves: (1) art1_mobile.jpg is re-cut shorter and wider, straight from
   the read-only source (AI/_IMAGES_2/1.png) -- same top-right branch,
   ending at its natural taper (the last full leaf cluster) before the
   long thin trailing stem and drip run that the previous portrait crop
   carried all the way to its own bottom edge; the new crop is 644x430,
   a 3:2 landscape window, entering the frame at the top edge exactly as
   before. (2) #canvas .c-open gets an explicit height here for the first
   time: 35vh, chosen because it lands the crown at roughly 35 percent of
   the screen (the operator's own figure) AND is close to the exact
   height at which this 3:2 crop's own aspect ratio matches the 390-430px
   phone-width container -- at that height object-fit:cover (unchanged
   above) has almost nothing to trim, and what little it does trim comes
   off the LEFT (bare paper, same safe direction checkpoint 89 already
   established with object-position:top right), never the branch itself.
   #markseat and .opening .sentence move to sit beneath this shorter
   crown and centred in the space below it -- see the checkpoint 91 note
   on that rule further down this file. Measured proof at 390x844 and
   430x932, proof/P30_hero_390.png and proof/P30_hero_430.png.
   RETIRED checkpoint 94, 2026-08-29: the fixed 35vh crown comes off --
   see the checkpoint 94 ruling immediately below, which replaces this
   box with one that sizes to the re-cut image's own intrinsic height.
   Retired, not deleted; history stays on record.
@media(max-width:640px){
  #canvas .c-open{height:35vh}
}
*/
/* checkpoint 94 ruling, 2026-08-29: the operator's own words, "for the
   mobile, on the main hero page, i want you to bring the illustration
   lower down, and of course ensure it is behind the text, get as much of
   the illustration in view, without it interfering with the text."
   Diagnosis: the checkpoint-93 crop (art1_mobile.jpg, 644x650) sat
   contained and letterboxed inside a fixed 35vh box -- small, and its own
   bottom edge landed well above #markseat (top:41%), nowhere near
   .opening .sentence (top:63%). Three changes, all scoped to this same
   breakpoint so nothing above 640px moves; #markseat and .opening
   .sentence are untouched -- same top%, same font-size/line-height, same
   word-drop entrance, exactly as checkpoint 91 left them:
   (1) art1_mobile.jpg is re-cut from the same read-only source
   (AI/_IMAGES_2/1.png, read only), a taller portrait window --
   434x752 (x:910-1344, y:0-752, the source's own full height) in place
   of 644x650. Narrower: column-wise ink-density scan (dist>40 from the
   source's own sampled paper baseline) puts the branch's own meaningful
   presence essentially nil left of source column ~900 inside the row
   0-650 band (col 850 reads 0.00%, col 900 reads 1.08%), so x:910 trims
   only blank buffer and the branch's own sparsest fringe, not the branch
   itself. Taller: extended straight to the source image's own bottom
   edge (752 of 752), all of the added height past the branch's last
   surviving ink (source row 607, checkpoint 93's own measurement) --
   pure open paper, the taller window's own lower portion. (2) #canvas
   .c-open drops its fixed 35vh height at this breakpoint -- height:auto,
   so the box sizes to the image's own intrinsic ratio instead of a fixed
   crown. (3) #canvas .c-open img drops the checkpoint-89/93
   object-fit/object-position pair at this breakpoint -- both were fixes
   for a box narrower than its content; with no fixed box height left to
   fit against, the image renders at plain width:100%, height:auto, full
   viewport width, the full re-cut, no crop, no letterbox.
   Z-order needs no new rule: #canvas has always carried z-index:1 against
   .opening's z-index:2 ("one continuous canvas" note, top of this file),
   so #markseat and .opening .sentence, both .opening descendants, already
   paint above the canvas image regardless of the image's own height --
   this ruling only makes the image tall enough to actually reach the text
   it was already behind.
   MEASURED, not eyeballed (both phone widths, against the re-cut file's
   own ink map, dist>40 from its sampled paper baseline, sampled inside
   #markseat's and .opening .sentence's own rendered rects): at 390 wide
   (375px content width under scrollbar-gutter:stable) the image renders
   434px scaled to 375px, scale .864, image bottom at 650px screen -- 42px
   clear of the sentence's own rendered bottom edge (607px). Sentence rect
   0.65% non-paper pixels; mark rect 0.28%. At 430 wide (415px content,
   scale .956) the image bottom lands at 719px, 55px clear of the
   sentence's bottom edge (664px). Sentence rect 0.56%; mark rect 0.36%.
   All four comfortably under the 3% target. No fade anywhere -- the
   re-cut's own bottom two-thirds is the source's own blank paper ground,
   already sampled within a few levels of --paper (checkpoint 77), so the
   image's own natural edge needs no gradient to read as continuous with
   the page. Proof: proof/P32_hero_390.png, proof/P32_hero_430.png. */
@media(max-width:640px){
  #canvas .c-open{height:auto}
  #canvas .c-open img{object-fit:fill;object-position:0 0;height:auto}
}
/* checkpoint 96 ruling 4: the operator's own actual phone -- "a tall
   Android, taller aspect than the 390x844 and 430x932 previously
   measured" -- reads the branch as reaching into the sentence and
   hurting legibility, even though checkpoint 94's own two measured
   widths already sat under the 3 percent ink target (0.65%/0.56%
   sentence, 0.28%/0.36% mark). Rather than trust that the two
   DevTools presets already checked generalise to every real tall
   phone, the crown comes down a size and up a little, adding
   clearance at every aspect instead of only the two previously
   proven: width 100% to 94% (a modest shrink, not a re-crop),
   margin-left:auto so it keeps hugging the right edge -- same
   top-right identity checkpoint 89 established -- instead of drifting
   toward centre, and the box itself lifts 2.4vh, clipped by #canvas's
   own overflow:hidden (a few px of the crop's own blank top margin,
   confirmed nowhere near the branch's own ink by the same
   dist>40-from-paper-baseline scan checkpoint 94 used). No fade
   anywhere -- this is a size and position change only, the image's
   own natural edges are untouched. Desktop (above 640px) is
   untouched, both by selector scope and by principle.
   MEASURED at FOUR widths/aspects this time, not two -- the checkpoint
   94 pair plus the operator's own tall-Android neighbours -- against
   the same re-cut file's own ink map, dist>40 from its sampled paper
   baseline (avg of 4 blank-paper samples, R234.75 G231.25 B212),
   sampled inside #markseat's and .opening .sentence's own rendered
   rects, mapped to the source file's own pixels through the exact
   render scale at each width (qa harness: a same-origin iframe sized
   to each exact width x height, so svh/vh-driven layout -- #opening's
   own min-height:102svh, #markseat/.opening .sentence's own top:%
   -- resolves against each aspect's real height, not just its width):
     390x844 (scale .812): sentence 0.191%, mark 0.263%
     430x932 (scale .899): sentence 0.190%, mark 0.290%
     393x873 (scale .819): sentence 0.193%, mark 0.088%
     412x915 (scale .860): sentence 0.192%, mark 0.093%
   All eight readings comfortably under the 3 percent target, and the
   sentence reading in particular drops well below checkpoint 94's own
   pre-fix numbers at the same two widths (0.65%/0.56% there vs
   0.191%/0.190% here) -- real headroom added, not just a passing
   number. Proof: proof/P34_hero_mobile.png (390 wide). */
@media(max-width:640px){
  #canvas .c-open{top:-2.4vh}
  #canvas .c-open img{display:block;width:94%;margin-left:auto}
}

/* ── the fixed bar. brand centre, nav right becoming the hamburger on scroll ── */
.bar{position:fixed;top:0;left:0;right:0;z-index:20;display:grid;
  grid-template-columns:1fr auto 1fr;align-items:center;
  padding:14px clamp(20px,3vw,44px);pointer-events:none;
  background:rgba(229,225,205,.82);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  transition:box-shadow .4s ease}
.bar.solid{box-shadow:0 1px 0 rgba(43,42,38,.12)}
.slotmark{grid-column:2;justify-self:center;font-family:var(--serif);font-weight:400;
  font-size:clamp(13px,1.2vw,16px);letter-spacing:.34em;text-transform:uppercase;visibility:hidden;
  text-decoration:none;color:inherit;pointer-events:auto}
/* checkpoint 72 ruling 1: .bar sets pointer-events:none on itself (so the
   fixed bar's empty grid cells stay click-through) and only .navwrap opts
   back in -- .slotmark, a sibling of .navwrap rather than a descendant of
   it, inherited that none and was never actually clickable anywhere, on
   ANY page, even where it already carried a real href. The one line above
   is the actual fix; wrapping the mark in an anchor (index.html; the four
   utility pages already had one) was necessary but not sufficient without
   it. Adds no box, moves nothing, changes no paint -- verified against
   the header identity re-proof below.
   cursor:pointer is the UA default for an anchor and needs no rule here. */
/* checkpoint 71 ruling 2: index keeps its mark invisible in this slot at
   rest -- the big scroll-driven mark (#bigmark) is what's actually seen,
   flying in from the hero and settling here. The utility pages (packages,
   begin, confirmation, testimonials) carry the exact same .bar/.slotmark
   markup now (root-cause fix for the header-shift ruling: one shared
   header instead of a second, differently-sized .u-bar), but they have no
   hero and no scroll engine to fly a mark in, so their copy of this slot
   is switched back to visible with the one class below -- same box, same
   type, same position as index's slot, just actually painted. */
.bar.util .slotmark{visibility:visible}
/* checkpoint 89 fix, 2026-08-28, part of the mobile sweep: on the four
   utility pages (begin, packages, confirmation, testimonials) this is a
   real, visible, working home link, not index's invisible seat -- and
   its tap target measured 98x15.5px at 390 wide, well under the 40px
   burger's own next to it. The bar's own row is already exactly 40px
   tall (68px bar height minus the 14px top/bottom padding both share),
   sized to fit the burger -- so growing the mark's own box to the same
   40px with inline-flex centring fits inside the existing row with no
   change to the bar's height, the mark's font-size/position, or any
   sibling; it only fills, vertically, room the row already reserved.
   Scoped under the same breakpoint as the rest of this sweep even though
   the change paints no visible pixel at any width. */
@media(max-width:860px){
  .bar.util .slotmark{display:inline-flex;align-items:center;min-height:40px}
}
.navwrap{grid-column:3;justify-self:end;position:relative;pointer-events:auto;
  display:grid;align-items:center;justify-items:end}
.navwrap nav{grid-area:1/1;font-family:var(--sans);font-weight:500;
  font-size:clamp(10px,.95vw,12px);letter-spacing:.14em;text-transform:uppercase;color:#4a4740;
  transition:opacity .35s ease;white-space:nowrap}
.navwrap nav b{color:var(--ink);font-weight:500}
.navwrap nav a{color:inherit;text-decoration:none}
@media(hover:hover){
  .navwrap nav a:hover,.u-nav a:hover,.foot-col a:not(.foot-ig):hover{
    text-decoration-line:line-through;text-decoration-color:var(--verm);text-decoration-thickness:1px}
}
.burger{grid-area:1/1;width:40px;height:40px;border:0;background:none;cursor:pointer;
  display:grid;place-items:center;opacity:0;pointer-events:none;transition:opacity .35s ease}
.burger span{display:grid;gap:4px}
.burger i{display:block;width:20px;height:1.6px;background:var(--ink)}
.bar.compact nav{opacity:0;pointer-events:none}
.bar.compact .burger{opacity:1;pointer-events:auto}

/* checkpoint 73: #bigmark is the only rendering of the brand mark a
   visitor on index ever actually sees, at the hero AND once docked in the
   header (#markslot sits underneath it the whole time, invisible, per
   the checkpoint 71 ruling above) -- so it, not the invisible slot, is
   what a real click or tap lands on. pointer-events was none here
   before, which meant the visible mark was never actually clickable at
   any scroll position: the operator's click fell straight through it to
   whatever was rendered underneath. auto plus the click handler in this
   page's own script (index.html) is the fix; aria-hidden stays, since
   the semantic, focusable link is still #markslot. */
#bigmark{position:fixed;z-index:21;left:0;top:0;font-family:var(--serif);font-weight:300;
  letter-spacing:.18em;text-transform:uppercase;white-space:nowrap;color:var(--ink);
  transform-origin:top left;will-change:transform;pointer-events:auto;cursor:pointer}

/* ── the veil ── */
.veil{position:fixed;inset:0;z-index:30;display:grid;place-items:center;
  background:rgba(229,225,205,.55);-webkit-backdrop-filter:blur(18px) saturate(.9);
  backdrop-filter:blur(18px) saturate(.9);
  opacity:0;visibility:hidden;transition:opacity .35s ease,visibility .35s}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .veil{background:rgba(229,225,205,.96)}}
.veil.open{opacity:1;visibility:visible}
.veil nav{display:grid;gap:clamp(12px,2.2vh,20px);text-align:center;
  font-family:var(--sans);font-weight:500;font-size:clamp(14px,1.9vw,19px);
  letter-spacing:.22em;text-transform:uppercase;color:var(--ink)}
/* checkpoint 71 ruling 1: this rule used to carry font-size:1.18em, which
   made "Begin a Visit" render larger than the other two veil labels (they
   are plain text, only this label is wrapped in <b>). Weight and colour
   stay overridden here (the <b> tag's own UA bold would otherwise fight
   the shared 500 weight the parent nav sets), size no longer does, so all
   three veil labels now share one size. */
.veil nav b{color:var(--ink);font-weight:500}
.veil nav .mi{position:relative;display:inline-block;padding:2px 12px;cursor:pointer;
  color:inherit;text-decoration:none}
/* checkpoint 89 fix, 2026-08-28, part of the mobile sweep: measured tap
   target on these three veil labels (the only nav a phone visitor has,
   since the max-width:860px block below hides .navwrap nav and forces
   the burger) at 390 wide was 164x22px, under the 40px minimum. Same
   breakpoint the burger takeover already uses, so the fix applies
   exactly where the veil is the primary nav; desktop's 2px padding
   (fine there, hover/mouse, and .mi sizes the click-through strike-svg
   off its own box) is untouched. */
@media(max-width:860px){
  .veil nav .mi{padding:11.5px 12px}
}
.veil nav .mi svg.strike{position:absolute;left:-5%;top:50%;width:110%;height:13px;
  transform:translateY(-50%) rotate(-3.5deg) scaleX(0);transform-origin:left center;
  transition:transform .28s ease;pointer-events:none;overflow:visible}
@media(hover:hover){
  .veil nav .mi:hover svg.strike{transform:translateY(-50%) rotate(-3.5deg) scaleX(1)}
}
.veil nav .mi svg.strike path{fill:var(--verm);opacity:.9}
@media(prefers-reduced-motion:reduce){.veil nav .mi svg.strike{transition:none}}
.veil .close{position:absolute;top:18px;right:clamp(20px,3vw,44px);width:40px;height:40px;
  border:0;background:none;cursor:pointer;display:grid;place-items:center}
.veil .close i{position:absolute;width:22px;height:1.6px;background:var(--ink)}
.veil .close i:first-child{transform:rotate(45deg)}
.veil .close i:last-child{transform:rotate(-45deg)}

.meta-chip{position:absolute;z-index:8;font-family:var(--sans);font-size:9.5px;font-weight:500;
  letter-spacing:.18em;text-transform:uppercase;color:var(--verm);
  border:1px dashed rgba(198,58,43,.55);padding:5px 9px;background:rgba(229,225,205,.82)}
/* checkpoint 127, operator's ruling 2026-09-01: the build-note chips come OFF
   the rendered site -- every red dashed annotation ("OPERATOR-PLACED STILL...",
   "SEAL FORM IS PROVISIONAL", the payment-stub note, all of them) was scaffolding
   for the build conversation, not visitor-facing copy. Retired here with one
   rule rather than by deleting ~40 spans across six pages: the markup stays as
   the record of what each element still owes (chips carry the open obligations
   -- unsourced photos, provisional seals, the stubbed payment feature), and one
   rule cannot miss a page. !important on purpose: several pages carry their own
   .meta-chip.static / .film-zone-mini .meta-chip display overrides. */
.meta-chip{display:none !important}

/* ── section one, the opening ── */
.opening{position:relative;min-height:102vh;min-height:102lvh;overflow:hidden;z-index:2}
/* checkpoint 77 ruling: the shape equity (.opening .equity, checkpoints
   74-76) and its keyline (.opening .edge-line.el-equity, checkpoint 76)
   are removed outright, operator instruction -- "the sentence sits on
   bare paper again." Both rules, and the two <svg> elements they styled
   in index.html, are deleted rather than commented out, per this
   ticket's own instruction to remove the equity entirely. #markseat, the
   sentence and .downstem below are untouched: same position, same
   word-drop entrance, same styling, now simply painting straight onto
   the section's own ground with nothing behind them. */
#markseat{position:absolute;left:50%;top:18%;transform:translate(-50%,-50%);
  font-family:var(--serif);font-weight:300;letter-spacing:.18em;text-transform:uppercase;
  white-space:nowrap;font-size:clamp(10px,2.16vw,29px);visibility:hidden}
/* checkpoint 89 ruling, 2026-08-28, superseding the "untouched" line in the
   checkpoint 77 note just above: .downstem is one of the operator's "faint
   lines... between the main hero page and the second section" (his
   checkpoint 89 words), a bare 1px stem at the bottom of this section,
   unnamed and unruled anywhere on this project, distinct from the
   APPROVED scroll cue ".cue" in the postcard section below, which carries
   live copy and is untouched. Retired, not deleted, per house practice;
   the markup is retired the same checkpoint in index.html.
.opening .downstem{position:absolute;left:50%;bottom:clamp(26px,4vh,44px);width:1px;
  height:clamp(30px,5vh,52px);background:rgba(43,42,38,.35);z-index:6}
*/
/* checkpoint 67 ruling: the operator-authored hero sentence (N-0776) moved
   off "a breath, not a banner" (cp66 ruling 1, superseded) to an editorial
   statement centred in the space below the reduced mark -- substantially
   larger, set in capitals, generous line-height and tracking for caps, a
   measured width so it breaks into a handful of balanced centred lines at
   desktop. Same serif face, same ink colour, mark untouched above it.
   Checkpoint 68: "love how the text comes in, but now the text is too
   large, reduce it by 30%" -- the clamp's three terms multiplied by 0.7
   (26/2.75/44 -> 18.2/1.93/30.8), same idiom, position/capitals/entrance
   untouched. Width unchanged: still breaks into balanced centred lines at
   desktop with the smaller size, no widow/orphan appeared.
   Checkpoint 69 ruling 2: the operator called the block too wide. Measured
   the THIRD line's rendered text width at 1440 before this change (the
   short, unbroken "however far from home life has taken you.") at
   669.34px -- the cap below is that measurement, so the whole block now
   sits at that same measure instead of the old 1340px ceiling. The 92vw
   term is kept so narrow viewports still shrink below the cap; the text
   rewraps into more, narrower lines at desktop, which is the intended
   effect. text-wrap:balance keeps the resulting stack of shorter lines
   even. */
.opening .sentence{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(92vw,669px);margin:0;text-align:center;font-family:var(--serif);font-weight:400;
  text-transform:uppercase;font-size:clamp(18.2px,1.93vw,30.8px);line-height:1.48;
  letter-spacing:.02em;color:var(--ink);text-wrap:balance}
/* the two source capitals (the L of Life, the S of So) stand slightly
   raised over the surrounding capitals -- an initial-capital effect, kept
   baseline-aligned since it is just a larger inline glyph, no shift. Wired
   up at runtime alongside the word split below, never re-typed here.
   Checkpoint 69 ruling 1: the enlarged glyph was inflating the line box of
   whichever line it opened -- a bare inline span inherits the sentence's
   unitless line-height, computed against the bigcap's OWN 1.38em
   font-size, so its box came out taller than a normal line's box and
   pushed the next line down further than usual (unequal gaps).
   line-height:0 collapses the bigcap's own contribution to line-box
   height to nothing -- the glyph still paints at full 1.38em size
   (overflow is visible, never clipped) but no longer competes for row
   height, so every line box comes out identical whether or not a bigcap
   opens it. Verified: rendered line-top deltas equal to within 0.01px at
   1440. */
.opening .sentence .bigcap{font-size:1.38em;line-height:0}

/* checkpoint 89 ruling, 2026-08-28, SUPERSEDED at checkpoint 91 below:
   companion to the mobile hero crop note on #canvas .c-open img above.
   Measured against the shipped mobile crop (art1_mobile.jpg,
   object-position:top right): the branch's ink density falls under 2
   percent of row width by 75 percent down the hero box and is fully
   clear by 85 percent, concentrated toward the right side, while the
   mark and sentence sit centred. #markseat and .opening .sentence keep
   every desktop rule untouched (this block only ever matches under
   640px, alongside the picture breakpoint) and move to the two of them
   sitting together in that clear lower band instead of the desktop
   18/50 percent split, which under the portrait crop would seat the
   sentence across the densest part of the branch. The sentence's
   font-size and line-height come down slightly here too, so the full
   six-line block fits inside the clear band with room at the bottom
   edge; still comfortably legible (15.5px, matching ordinary mobile body
   copy elsewhere on this site) and still the exact same text, same
   word-drop entrance (js/scroll.js splits on the live DOM text, not on
   any position or size). Verified at 390 and spot-checked at 430: the
   sentence's own rendered rect sits entirely below the branch's visible
   region, proof at proof/P29_hero_mobile.png.
   values were #markseat{top:74%} .opening .sentence{top:88%}. */
/* checkpoint 91 ruling, 2026-08-28: the operator's next-day mobile ruling
   -- checkpoint 89 fixed the branch being invisible by switching to a
   portrait crop, but that crop then read as filling the WHOLE phone
   screen with the sentence pushed to top:88%, "too far down". Paired
   with the checkpoint-91 crown crop and the new #canvas .c-open{height:
   35vh} above, the mark and sentence come up off the bottom edge: the
   mark sits just beneath the 35vh crown (top:41%, leaving a clear gap
   under the branch's own lowest leaf), and the sentence's own vertical
   CENTRE (this rule's transform is translate(-50%,-50%), so top:X% IS
   the centre point, not the block's edge) moves to top:63% -- centred in
   the space between the mark and the fold rather than jammed against
   it. font-size/line-height unchanged from checkpoint 89 (15.5px/1.4,
   still the exact same text, same word-drop entrance, same six-line
   wrap). Measured at 390x844 and 430x932: image block ~30-35 percent of
   viewport height, sentence centre inside the 45-70 percent band, full
   composition (crown, mark, sentence) above the fold, no overlap with
   the branch's ink -- proof/P30_hero_390.png, proof/P30_hero_430.png. */
@media(max-width:640px){
  #markseat{top:41%}
  .opening .sentence{top:63%;font-size:15.5px;line-height:1.4}
}

/* checkpoint 66 ruling 5, superseded on the sentence by checkpoint 67: the
   mark still settles once on load like ink into paper (untouched below).
   The sentence no longer fades as one block -- scroll.js splits it into
   per-word spans and each word drops into place with its own fade, laid
   down left to right. Both entrances are decided by the SAME arming test
   (fresh load, hero in view, motion allowed), synchronously before first
   paint; everywhere else (JS-off, motion reduced, mid-page reload) the
   mark and the sentence simply render at their settled, fully-visible
   state -- the sentence still gets its word-span split and big caps in
   that case, just with no animation, since that split is a permanent
   rendering treatment, not part of the entrance. */
@keyframes n2f-ink-mark{from{opacity:0;filter:blur(7px)}to{opacity:1;filter:blur(0)}}
@keyframes n2f-word-drop{from{opacity:0;transform:translateY(-.5em)}to{opacity:1;transform:translateY(0)}}
body.intro-run #bigmark{animation:n2f-ink-mark 1.2s ease-out both}
.opening .sentence .word{display:inline-block}
body.intro-run .opening .sentence .word{animation:n2f-word-drop 650ms ease-out both}
@media(prefers-reduced-motion:reduce){
  body.intro-run #bigmark{animation:none;opacity:1;filter:none}
  body.intro-run .opening .sentence .word{animation:none;opacity:1;transform:none}
}

/* ── section two, the postcard left, artwork right ── */
.postcardsec{position:relative;z-index:auto;min-height:106svh;
  display:grid;grid-template-columns:minmax(0,500px) 1fr;gap:clamp(26px,5vw,80px);
  align-items:center;padding:clamp(90px,13vh,140px) clamp(22px,4.5vw,70px)}
.cardcol{position:relative}
.postcard{position:relative}
.card{background:var(--raised);
  box-shadow:0 1px 2px rgba(43,42,38,.18), -9px 12px 0 rgba(43,42,38,.22);
  border:1px solid rgba(43,42,38,.08)}
/* checkpoint 93 ruling, 2026-08-28: "please also reduce the postcard with
   the picture attached by 20%, apply for both web and mobile" -- the front
   card (the photo postcard) only, same idiom as the wayhome/journey cap
   above: the px cap multiplied by 0.8, the vw fluid term untouched. Desktop
   300px -> 240px here; the mobile counterpart (280px -> 224px) sits at its
   own @media(max-width:860px) block further down this file. The tape, pin
   and photo ride the card unchanged (the tape/pin are fixed-px decoration
   anchored to the card's own corner by negative offset, not sized off the
   card's width, so they need no scaling to keep riding it; .photo is
   width:100% of the card and scales with it automatically). See that same
   breakpoint's .card.back/.seal notes for how the back card and seal
   rebalance so the smaller front card does not sit swallowed or
   overlapped. Measured before/after at 1440 and 390,
   proof/P31_postcard.png. */
.card.front{position:relative;z-index:6;width:min(60vw,240px);
  background:none;border:0;box-shadow:none;padding:0;
  transform:rotate(-5.4deg)}
/* checkpoint 95 ruling 5: "the handwritten back card reduces by 20 percent
   at desktop and mobile" -- same idiom every prior per-instance resize on
   this file has used (checkpoint 68/78/93: multiply the px cap by the
   ratio, leave the vw fluid term untouched). Desktop 470px -> 376px here;
   the mobile counterpart (400px -> 320px) sits at its own
   @media(max-width:860px) block further down this file, next to this
   same ruling's own note. The front (photo) card was already reduced
   separately at checkpoint 93 and stays exactly as it is -- this ruling
   only ever touches .card.back's own width cap.
   SUPERSEDED checkpoint 96 ruling 1(b) below -- the 376px cap survives
   unchanged, but the padding this rule set is cut down there. */
/* checkpoint 96 ruling 1(b): "the card has gone too long, needs to be more
   square" -- measured before this ruling at 1440: offsetWidth 376 x
   offsetHeight 655, ratio 0.57 (a column, not a postcard). Two changes,
   worked together with ruling 1(c)'s own line-wrap fix just below (which
   alone already dropped the height to 581/0.65 by letting the message
   flow naturally instead of wrapping twice): the vertical padding comes
   down -- top clamp(120px,10vw,150px) to clamp(78px,7vw,96px) (still
   clears the stamp's own footprint, top offset + height <= ~103px at
   1440, with margin now that ruling 1(a) removed the seal this padding
   never had to clear anyway), bottom clamp(24px,2.8vw,36px) to
   clamp(18px,2vw,26px) -- and the width cap widens, 376px to 430px (the
   84vw fluid term eased down from 86vw so it still yields to the front
   card and cardcol at narrow desktop widths). Measured after, 1440:
   offsetWidth 430 x offsetHeight 402, ratio 1.07 -- a postcard rectangle,
   not a column. Mobile's own matching cap sits at its own
   @media(max-width:860px) block below, same ruling. Proof:
   proof/P34_postcard.png. */
.card.back{position:relative;z-index:4;width:min(78vw,390px);
  padding:clamp(78px,7vw,96px) clamp(28px,3.2vw,44px) clamp(18px,2vw,26px);
  transform:rotate(3.2deg);margin:-16% 0 0 24%}
.card .tape{position:absolute;width:92px;height:26px;background:rgba(233,226,208,.62);
  box-shadow:0 1px 2px rgba(43,42,38,.16)}
.card .tape.tl{top:-12px;left:-28px;transform:rotate(-38deg)}
.card .tape.br{bottom:-11px;right:-30px;transform:rotate(-36deg)}
.card .pin{position:absolute;top:-7px;left:50%;width:14px;height:14px;
  border-radius:50%;background:var(--verm);
  box-shadow:0 3px 4px rgba(43,42,38,.4), inset -2px -2px 3px rgba(0,0,0,.28)}
.card .pin::after{content:"";position:absolute;left:6px;top:12px;width:1px;height:7px;
  background:rgba(43,42,38,.35);transform:rotate(14deg)}
.card .stamp{position:absolute;top:clamp(16px,1.8vw,24px);right:clamp(16px,1.8vw,24px);
  width:clamp(52px,5vw,66px);aspect-ratio:5/6;overflow:visible}
.card .stamp .stamp-edge{fill:var(--paper);stroke:var(--verm);stroke-width:1}
.card .stamp .stamp-frame,.card .stamp .stamp-oval{fill:none;stroke:var(--verm);stroke-width:.6}
.card .stamp .stamp-stem,.card .stamp .stamp-leaf,.card .stamp .stamp-petal{
  fill:none;stroke:var(--verm);stroke-width:.9}
.card .stamp .stamp-bud{fill:var(--verm);stroke:none}
.card .postmark{position:absolute;top:clamp(30px,3vw,42px);right:clamp(72px,7.4vw,96px);
  width:70px;height:34px;opacity:.5}
.card .postmark path{fill:none;stroke:var(--ink);stroke-width:1.2;stroke-linecap:round}
.card .photo{position:relative;width:100%;margin:2px 0 0;background:var(--raised);
  padding:8px 8px 20px;
  box-shadow:0 1px 2px rgba(43,42,38,.18), -9px 12px 0 rgba(43,42,38,.22);
  transform:rotate(1.2deg);
  border:1px solid rgba(43,42,38,.08)}
.card .photo img{display:block;width:100%;height:auto;filter:saturate(.94) contrast(1.02) sepia(.04)}
.hand-sal,.hand-ln,.hand-sign{font-family:var(--hand);font-size:clamp(15px,1.75vw,20px);
  line-height:1.85;margin:0}
.hand-sal{margin-bottom:14px}.hand-sign{margin-top:16px}
/* checkpoint 95 foreman correction, 2026-08-29: the back card's 20 percent
   reduction pulled its salutation up under the seam seal, which was found
   covering "Ma," and the first handwriting line at review. Raised and eased
   left so the seal binds the seam clear of the text. Was top:53%;left:52%.
   checkpoint 96 ruling 1(a) note: the message card's own bare .seal
   instance (this rule's top:44%;left:38%;transform centring) is retired
   from index.html's markup -- see that file's own note at the retired
   span, next to .seal.seal-photo. This shared rule survives untouched
   because .seal.seal-photo (below) still depends on its background,
   shadow, size and the ::before/::after sunburst/dot -- only its own
   top/left/transform centring values go unused now that no plain .seal
   element remains on the page. */
.seal{position:absolute;z-index:7;top:44%;left:38%;transform:translate(-50%,-50%);
  width:clamp(58px,6.2vw,76px);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;
  background:var(--verm);
  box-shadow:0 1px 1px rgba(43,42,38,.2), -4px 5px 0 rgba(43,42,38,.3)}
.seal::before{content:"";width:64%;aspect-ratio:1;border-radius:50%;
  background:repeating-conic-gradient(rgba(252,251,248,.92) 0 4deg,transparent 4deg 18deg);
  -webkit-mask:radial-gradient(circle,transparent 40%,#000 42%,#000 60%,transparent 62%);
  mask:radial-gradient(circle,transparent 40%,#000 42%,#000 60%,transparent 62%)}
.seal::after{content:"";position:absolute;width:20%;aspect-ratio:1;border-radius:50%;
  background:rgba(252,251,248,.92)}
/* checkpoint 95 ruling 5: the second seal instance, on the photo card.
   Every property of .seal above (size, flat vermillion fill, the sunburst
   ::before, the centre dot ::after, the harsh two-part shadow) stays
   shared and untouched -- this modifier only replaces the centring
   top/left/translate the message-card instance uses with a corner
   anchor, so it reads as stuck onto the photo's own edge rather than
   floating centred over it, the way a real sticker sits half off a
   photograph's corner. Positioned relative to .photo (site.css,
   position:relative, just above), not the wider .postcard, so it tracks
   the smaller photo card specifically at every width the front card
   itself already resizes at (checkpoint 93). */
.seal.seal-photo{top:auto;left:auto;bottom:-14px;right:-12px;
  transform:rotate(-9deg)}
.cue{margin-top:clamp(28px,4.5vh,46px);text-align:center;z-index:6;position:relative;
  font-family:var(--sans);font-size:10.5px;letter-spacing:.3em;text-transform:uppercase;color:#57544d}
.cue i{display:block;width:1px;height:clamp(22px,3.4vh,34px);background:rgba(43,42,38,.32);margin:12px auto 0}
.artcol{position:relative;align-self:center}
/* checkpoint 100 ruling, 2026-08-30 -- SUPERSEDES the checkpoint 77 law below.
   The images no longer carry a ground at all. Each is now a TRANSMISSION
   layer (assets/art3/*.webp): white where the sheet was untouched, darker
   where there is pigment, composited with mix-blend-mode:multiply. White
   multiplies to a no-op, so the page's own paper and grain read straight
   through and there is no rectangle left to hide -- which is why the
   corner mask is GONE rather than tuned. Two facts settled it: (a) the
   operator saw that the old files' own paper GRAIN, not their colour, was
   fighting the page's grain inside a box, and one flat divisor cannot
   cancel a grain (it is a wobble around the average, not the average);
   (b) the mask was measurably eating real drawing -- 16.2% of the hero's
   marks dimmed, down to 15% strength in the corners (proof/MASK_corner_
   fade_2026-08-30.md). Verified live in real Chrome before landing:
   inside-picture vs page-beside-it measured 63.0 RGB levels apart under
   the old build, 1.3 under this one.
   cp77's OWN reasoning is kept verbatim below because it explains WHY
   multiply failed for it, and that reason is exactly what changed: it
   failed because the image carried its own cream ground. Remove the
   ground and the objection dies with it. Do not restore the mask without
   first re-reading that distinction.
   ---- superseded, retained as the record ---- */
/* checkpoint 77 seamlessness law: mix-blend-mode:multiply only reads as
   "sitting on the paper" when the paper it composites against is close
   to white -- multiply(x,white)=x, so the old near-white #FCFBF8 barely
   touched the image at all. Against the new sampled cream (#E5E1CD,
   tokens.css) the same blend darkened every image's own light ground by
   roughly (255-229)/255, and stacked with the grain layer beneath it
   that reached over 20% in places -- measured, and it read as a dirty
   rectangle, not paper. Dropped outright: the image now paints in
   normal blend, its own ground already matching the sampled cream
   (that IS where the cream was sampled from), so no blend trick is
   needed to make the tones agree. What remains is the rectangle's own
   edge -- closed with mask-image, a soft radial feather so the last
   ~12% of the box fades to transparent and the page's own cream (with
   its grain) shows through at the seam instead of a hard photo edge.
   No frame, no border, no shadow anywhere on this element, per the law. */
.artcol img{display:block;width:100%;max-width:520px;height:auto;margin-inline:auto;
  mix-blend-mode:multiply}

/* ── section three, the interlude ── */
/* checkpoint 70 ruling 2: the film zone this section carried (cp69 ruling
   4) is gone (ruling 1), so the min-height and padding that were sized to
   fit that image come down with it, in the section's own clamp idiom,
   closing the gap to the next section's heading. */
.interlude{position:relative;z-index:2;min-height:72svh;padding:clamp(70px,10vh,120px) clamp(24px,5vw,80px)}
/* checkpoint 78 ruling: every .wash instance site-wide (9 of them --
   postcardsec, interlude x2, ache, wayhome, journey, vow x2, the steps
   "whisper" cloud) and every .edge-line keyline (4 of them -- el-postcard,
   el-interlude, el-wayhome, el-footwash below) come off the site
   outright: "remove all those boulder shapes... dotted throughout the
   site." Retired here, not deleted, so the shape's own history
   (checkpoints 75-77, quoted below) stays on record; index.html's own
   markup for every instance is retired the same way, at each section.
   Nothing that reads these rules survives elsewhere in this file -- see
   also the .foot-wash and footer .sprig retirements further down, and
   #washClip/#footWashClip's own retirement at the top of index.html.
.wash{position:absolute;z-index:0;width:clamp(220px,30vw,420px);
  height:clamp(300px,42vh,520px);background:#D3D7CC;
  clip-path:url(#washClip);
  pointer-events:none}
.interlude .wash{right:-6%;top:14%}
.interlude .wash.two{right:auto;left:-8%;top:auto;bottom:-6%;width:clamp(180px,24vw,340px);
  height:clamp(200px,30vh,360px);background:#F6E4E2}
.wash.w-ochre{background:#F1DFC6}.wash.w-moss{background:#D3D7CC}
.wash.w-sakura{background:#F6E4E2}.wash.w-misty{background:#E1E8EC}
.wash.w-clay{background:var(--clay)}
.wash.sm{width:clamp(140px,18vw,260px);height:clamp(160px,24vh,300px)}
.edge-line{position:absolute;overflow:visible;pointer-events:none;z-index:1}
.edge-line path{fill:none;stroke:var(--moss);stroke-width:1.5px;stroke-linecap:round;
  opacity:.6;vector-effect:non-scaling-stroke}
.interlude .edge-line.el-interlude{right:-6%;top:14%;
  width:clamp(220px,30vw,420px);height:clamp(300px,42vh,520px)}
.postcardsec .edge-line.el-postcard{right:-5%;top:6%;
  width:clamp(140px,18vw,260px);height:clamp(160px,24vh,300px)}
#wayhome .edge-line.el-wayhome{right:-6%;bottom:8%;
  width:clamp(140px,18vw,260px);height:clamp(160px,24vh,300px)}
*/
.interlude .inner{position:relative;z-index:5;max-width:1060px;margin:0 auto}
.interlude h2{font-weight:300;font-size:clamp(38px,6vw,72px);line-height:1.08;letter-spacing:-.01em;
  margin:0 0 clamp(34px,6vh,64px);max-width:12em;text-wrap:balance}
.interlude p{font-size:clamp(16.5px,1.7vw,20px);line-height:1.74;max-width:26em;margin:0 0 clamp(26px,4.5vh,46px);color:#3b3934}
.interlude p.offset-r{margin-left:auto}
.interlude p.close-line{font-size:clamp(22px,2.9vw,34px);line-height:1.4;max-width:20em;color:var(--ink)}
/* checkpoint 69 ruling 4 added a film zone here, paired with the mountains
   that open the scroll's cycle. Checkpoint 70 ruling 1 took it off the
   site outright (no illustration in the interlude; the mountains are
   already covered by the postcard section's artcol). Retired here,
   commented rather than deleted, so the sizing history stays on record;
   the selector is inert, matching the .threshold/.wordless retirement
   below.
.interlude .zone{position:relative;z-index:5;max-width:640px;margin:clamp(36px,6vh,60px) auto 0}
.interlude .zone img{display:block;width:100%;height:auto;margin-inline:auto;mix-blend-mode:multiply}
*/

#ribbonwrap{position:absolute;inset:0;z-index:4;pointer-events:none;display:none}
/* the pen line is PARKED at operator checkpoint 61; engine and both strokes stay on disk */
@media(max-width:860px){
  .handnote{white-space:normal;max-width:84vw}
  .note-float{position:static;display:block;margin:14px 0}
  .postcardsec{grid-template-columns:1fr;gap:clamp(30px,6vh,50px)}
  /* checkpoint 93 ruling, 2026-08-28: the same 20% postcard reduction as
     the desktop .card.front above, mobile's own cap: 280px -> 224px, the
     70vw fluid term untouched. */
  .card.front{width:min(70vw,224px);margin:0 auto}
  /* checkpoint 95 ruling 5: the mobile half of the same 20% back-card
     reduction as the desktop cap above -- 400px -> 320px, the 92vw fluid
     term untouched. */
  .card.back{width:min(92vw,320px);margin:-13% auto 0;padding-top:clamp(66px,17vw,86px)}
  .seal{top:56%;left:50%}
  /* checkpoint 95 ruling 5: the photo card's own seal sits on its own
     corner at every width, same reasoning as the desktop rule below --
     kept out of this file's shared mobile .seal override (which only
     ever targeted the message card's centred seal) via the :not() guard
     mirrored there. */
  .seal.seal-photo{top:auto;left:auto}
  .navwrap nav{display:none}
  .bar .burger{opacity:1;pointer-events:auto}
  .interlude p.offset-r{margin-left:0}
}
@media(prefers-reduced-motion:reduce){.bar,.navwrap nav,.burger,.veil{transition:none}}

/* ── the chapters, checkpoint 61, style per the set reference ── */
.chapter{position:relative;z-index:auto;min-height:96svh;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:clamp(26px,5vw,80px);
  align-items:center;padding:clamp(80px,12vh,130px) clamp(22px,4.5vw,70px)}
.chapter.flip .words{order:2}.chapter.flip .zone{order:1}
.chapter .words h2{font-weight:300;font-size:clamp(30px,4.4vw,54px);line-height:1.12;
  letter-spacing:-.008em;margin:0 0 20px;text-wrap:balance}
.chapter .words p{font-size:clamp(16px,1.62vw,19.5px);line-height:1.74;max-width:28em;
  margin:0 0 18px;color:#3b3934}
.chapter .zone{position:relative}
/* checkpoint 100, 2026-08-30: SUPERSEDED, see the full note at
   .artcol img above. This image is now a transmission layer composited
   with multiply; the radial mask feather described below is retired
   because there is no longer a ground to hide the edge of. The "no
   frame, no border, no shadow" clause SURVIVES and still governs.
   ---- superseded, retained as the record ----
   checkpoint 77 seamlessness law: same reasoning and same fix as
   .artcol img above -- normal blend (the image's own ground already
   matches the sampled cream) plus a soft radial mask feather in place
   of multiply, which only worked against a near-white paper. No frame,
   no border, no shadow. */
.chapter .zone img{display:block;width:100%;max-width:560px;height:auto;margin-inline:auto;
  mix-blend-mode:multiply}
/* checkpoint 78 ruling: the wayhome (the street, art2/4) and journey (the
   red door, art2/5) images render too large, operator instruction --
   scoped to these two only, the shared .chapter .zone img box above's own
   max-width (560px) multiplied by 0.7, the same idiom checkpoint 68 used
   on the hero sentence's clamp. width:100% stays the shared rule's own
   fluid term, untouched; only the cap moves. Every other .chapter .zone
   img instance (ache, and vow, whose own artwork is repositioned instead
   just below) keeps the shared 560px box exactly as it was.
   checkpoint 93 ruling, 2026-08-28: "the illustration in the 'you know the
   way' section is too large still -- please reduce it by 20%." Scoped to
   #wayhome ONLY, operator's own section name matching this chapter's own
   h2 ("You know the way."); #journey was not named and keeps its 392px
   cap exactly as checkpoint 78 left it -- the two are split into separate
   rules below so the one selector no longer moves both together. Same
   idiom again: the 392px cap multiplied by 0.8 -> 313.6px, width:100%
   fluid term untouched. */
#wayhome .zone img{max-width:313.6px}
#journey .zone img{max-width:392px}
/* checkpoint 78 ruling: the vow section's garden artwork (art6, the
   butterfly) moves toward the "Never far. Never forgotten." text and up
   slightly, operator instruction. A transform on .zone (the image's own
   wrapper, which also carries its meta-chip, so the two keep travelling
   together) shifts it purely visually -- the chapter's own two-column
   grid, and the par-lock wrapper nested in #vow .words, are untouched by
   this, no reflow anywhere else on the page.
   checkpoint 89 fix, 2026-08-28, part of the mobile sweep: this rule was
   never scoped to the desktop breakpoint the way its own sibling,
   #vow .zone img's translate(-26px,-100px) checkpoint 80 delta a little
   further down, correctly is. "Closer to the text on its left" only
   describes the desktop two-column layout; under 861px .chapter collapses
   to one column (see the max-width:860px block near the end of this
   file), and the same -64px pulled the image 64px left of its single
   stacked column, taking it 42px past the left edge of the viewport at
   390px wide, real horizontal overflow (measured: document.documentElement.
   scrollWidth over clientWidth). Wrapped in the same min-width:861px
   query as its sibling below, so desktop is untouched and mobile keeps
   the image inside its own column. */
@media(min-width:861px){
  #vow .zone{transform:none;left:-64px;top:-34px}
}
.chapter .zone.empty{border:1px dashed rgba(43,42,38,.28);min-height:clamp(240px,44svh,460px);
  display:grid;place-items:center}
.handnote{font-family:var(--hand);font-size:clamp(19px,2.3vw,30px);color:var(--ink);
  line-height:1.6;display:inline-block;white-space:nowrap}
/* checkpoint 89 fix, 2026-08-28, part of the mobile sweep: this base rule
   sits AFTER the max-width:860px block above in source order, so its
   unconditional white-space:nowrap was winning the cascade at every
   width, silently defeating that block's own .handnote{white-space:
   normal;max-width:84vw} on every phone. Measured defect: the "Ten
   springs now and the ocean hasn't got any smaller." handnote (chapter 2)
   rendered as one unbroken 514px line against a 375px content box,
   pushing document.documentElement.scrollWidth to 528px and giving the
   whole page real horizontal overflow (confirmed: a sideways scrollbar,
   every section shiftable right, not scoped to the handnote alone). Same
   selector, same specificity, re-declared here so it lands AFTER the
   base rule above and actually wins under 860px; the earlier block is
   left in place rather than moved, so its history and its other
   declarations (.note-float, .postcardsec, etc, all unaffected by this
   bug) stay exactly where they were. */
@media(max-width:860px){.handnote{white-space:normal;max-width:84vw}}
.handnote .hw{display:inline-block;clip-path:inset(-8% 100% -12% 0);
  transition:clip-path 1.5s ease .1s}
.handnote.written .hw{clip-path:inset(-8% -4% -12% 0)}
.handnote.pencil{opacity:.78;transform:rotate(-3.5deg);font-size:clamp(17px,2vw,26px)}
.handnote.frag{opacity:.85;transform:rotate(1.8deg)}
.note-float{position:absolute;z-index:6}
@media(prefers-reduced-motion:reduce){.handnote .hw{clip-path:none;transition:none}}
/* checkpoint 69 ruling 4: #door (.threshold) and the wordless #table
   chapter were dissolved out of the scroll order. The front door image
   and its handnote now live inside #journey's zone; the chair fragment
   came off the site entirely and the inside the house placeholder image
   is no longer referenced anywhere. Retired here, commented rather than
   deleted, so the sizing and behaviour history stays on record; both
   selectors are inert.
.threshold{position:relative;z-index:2;min-height:64svh;display:grid;place-items:center;
  padding:clamp(60px,9vh,100px) clamp(22px,4.5vw,70px)}
.threshold img{display:block;width:min(70vw,480px);height:auto;mix-blend-mode:multiply}
.wordless{align-items:end}
*/
.steps{position:relative;z-index:2;max-width:660px;margin:0 auto;
  padding:clamp(90px,13vh,150px) clamp(24px,5vw,60px)}
.steps h2{font-weight:300;font-size:clamp(28px,3.8vw,44px);margin:0 0 clamp(30px,5vh,50px);text-wrap:balance}
.steps ol{list-style:none;margin:0;padding:0;counter-reset:st}
.steps li{margin:0 0 clamp(24px,4vh,38px);padding-left:56px;position:relative;counter-increment:st}
.steps li::before{content:counter(st);position:absolute;left:0;top:2px;width:34px;height:34px;
  border:1px solid rgba(43,42,38,.4);border-radius:50%;display:grid;place-items:center;
  font-family:var(--sans);font-size:13px}
.steps li b{display:block;font-weight:500;font-family:var(--serif);
  font-size:clamp(18px,2vw,23px);margin-bottom:6px}
.steps li p{margin:0;font-size:clamp(15.5px,1.55vw,18px);line-height:1.7;color:#3b3934}
.vow{padding-bottom:clamp(40px,6vh,70px)}
/* checkpoint 73: the black box moved to css/site.css's shared .cta,.btn
   rule below (red tape, not a box) -- this section keeps only its own
   vertical rhythm around the button. */
.vow .cta{margin:22px 0 18px}
.vow .under{font-size:clamp(15px,1.5vw,18px);line-height:1.7;color:#3b3934;max-width:30em}
/* checkpoint 80 ruling: art6 (#vow .zone img, the butterfly garden) pulls
   closer to the .words column on its left and lifts 100px above its
   CURRENT rendered position -- operator instruction, this image named
   specifically, this checkpoint's own delta on top of checkpoint 78's
   ruling just above (#vow .zone{transform:translate(-64px,-34px)}), which
   already moved this same artwork toward the text and up slightly once
   before. That rule stays untouched and keeps doing its own job on the
   .zone wrapper (image + meta-chip travelling together); this checkpoint
   adds a SECOND, independent transform one level deeper, on the img alone
   (not the chip), so the two deltas compose visually without either
   rule's own history being folded into or overwritten by the other --
   same layering idiom the wayhome/journey max-width override just above
   uses against the shared .chapter .zone img rule. transform (not a
   change to .chapter's shared grid-gap) keeps the move scoped to this one
   instance: every other chapter's zone (ache/wayhome/journey, all still
   on the shared .chapter rule above) stays untouched, and so does #vow at
   mobile widths, where the grid collapses to one stacked column (see the
   max-width:860px block further down) and "closer to the text on its
   left" no longer describes the layout, so the pull is scoped to the
   desktop side-by-side breakpoint only. Measured at 1440px before this
   ruling (i.e. with checkpoint 78's own -64px/-34px already rendered in):
   the gap between .words' own right edge and this image's left edge was
   33.625px; this checkpoint's own translateX pulls it in a further 26px,
   landing the gap at 7.625px, a decisive close without the image crossing
   into the text column (proof: checkpoint 80 rendered rects). This
   checkpoint's own translateY(-100px) is the exact vertical ask on top of
   wherever checkpoint 78 already left it, applied on the same transform
   as the translateX rather than a second property, so both moves land in
   one paint step.
   NOTE, checkpoint 100 2026-08-30: this carve-out is now MOOT -- the
   feather came off EVERY image, not just this one, when the images
   stopped carrying their own ground. The `mask-image:none` below is
   therefore redundant but harmless, and is kept so this ruling's record
   stays readable. The operator's reasoning here turned out to be the
   general case.
   ---- original ruling ----
   The soft radial-edge feather every other chapter image keeps (the
   checkpoint 77 seamlessness law on .chapter .zone img above) comes off
   this one image outright: the operator ruled art6's own ground already
   matches the page cream, so a fade reads as unneeded softening rather
   than a seam fix. The other three chapter images (art3/4/5) were not
   named and were checked against the same law at this checkpoint -- their
   own grounds still match the page cream the same way art6's does, so
   they keep the shared mask untouched. */
@media(min-width:861px){
  #vow .zone img{transform:none;position:relative;left:-26px;top:-100px}
}
#vow .zone img{
  -webkit-mask-image:none;
  mask-image:none;
}
/* checkpoint 80 ruling, footer ground: the operator supplied a texture
   photograph directly this checkpoint (INSPIRATION/TEXTURES/
   b2700fa26a1fe3a81c15d033267c81b5.jpg, a deep olive plaster field,
   1280x640) and ordered it used AS THE GROUND, not as a reference for
   another paint pass -- superseding checkpoint 79 ruling (2) below, which
   had retired the checkpoint 78 photograph (../assets/footer_plaster.jpg,
   left on disk untouched, unreferenced, per house practice) in favour of
   a hand-painted inline SVG (.foot-paint, checkpoint 79). That SVG is
   itself retired now, same house practice, at the markup in index.html;
   its own construction/legibility comment stays on record just below the
   now-inert footer .foot-paint rule further down this file.
   New optimised web copy: ../assets/footer_ground.jpg, 1280x640, ~93KB,
   well under the 400KB ceiling, full resolution kept (the raw texture
   already sat at 1280px, generous enough that background-size:cover
   never has to upscale past source resolution at any viewport this site
   ships). background-size:cover, no repeat, matching the checkpoint 78
   reasoning still on record two checkpoints back (a tiled or mirrored
   texture reads as a visible seam/kaleidoscope; one covering image does
   not). The url() carries a ?v=cp80 cache-buster, the site's own existing
   idiom (css/js links already do this) -- needed here because the asset
   itself changed in place at an unversioned path partway through this
   checkpoint's own proof pass and a stale cached copy nearly slipped
   through the render check.
   LEGIBILITY LAW: the raw texture's own sampled average tone measured
   4.58:1 against var(--paper) -- short of the 5.5:1 target this
   checkpoint's average-ground floor sets (a tighter bar than checkpoint
   78's plain 4.5:1 AA floor). ImageEnhance.Brightness(0.78) baked into
   the exported asset (not a live CSS filter, for the same reason as
   checkpoint 78: a filter would also darken the text painted over it)
   is the darken that clears both this checkpoint's floors with real
   margin -- 0.85 was tried first (moving down in steps from 1.0) and
   passed the raw-image estimate but FAILED the actual render: the
   rendered-pixel check below caught it short of the 4.5:1 lightest-patch
   floor at 4.21:1 under the .foot-grid .share headline specifically, a
   lighter mottled patch than the raw-image sampling method (checkpoint
   78's own method) had found. 0.78 was the next step down and is what
   ships. Rendered-pixel proof (P24_footer.png / P24_footer_mobile.png,
   both widths, footer text hidden then re-measured at each live text
   element's own rect coordinates, same method as checkpoint 79's own
   proof, both widths' samples pooled into one pair of numbers): average
   ground under the live footer text measured 6.76:1 against var(--paper);
   the lightest sampled patch under any live footer text (a 4x4px window,
   swept across every text rect at both widths, not a single outlier
   pixel) measured 4.76:1 -- both clear their floors (5.5:1 / 4.5:1) with
   the source's own light/dark plaster mottle and tool-scratch texture
   still visible, nothing crushed to flat black or grey.
   The straight top border below is kept rather than a torn-paper edge --
   the flat-to-textured colour jump alone already reads as a clean seam
   against the cream page above; a torn edge has no precedent as a
   full-width section boundary anywhere else on this site (the site's own
   torn-edge idiom is the .cta/.btn tape ends, a small UI accent, not a
   section transition), so the plainer choice is the one "per the site's
   own language."
   Every live footer text colour below (ink, the muted grey, the footer
   link colour) recolours to var(--paper) itself, the site's own cream --
   unreadable on any of the dark grounds this footer has carried,
   checkpoint 78's ruling, untouched by this checkpoint. The meta-chip
   debug labels are the one exception: they keep their own red-on-pale-
   chip styling untouched (site-wide .meta-chip rule, its own pale chip
   background already legible on any ground), via the :not(.meta-chip)
   guard on .foot-col span below. */
/* checkpoint 79 ruling, FULL BLEED -- the fix itself, the failed attempts
   tried first, and the one accepted trade-off all live on the
   html{scrollbar-gutter:...} rule at the top of this file (checkpoint
   71/79). footer's own box needed no rule at all in the end: dropping
   "both-edges" to plain "stable" there is what lets it reach the true
   left edge natively. The class="site-footer" hook on the footer markup
   (index.html) is left in place as a harmless, unambiguous selector
   target distinguishing this footer from testimonials.html's unrelated
   <footer class="testimonial-attrib"> byline, though nothing currently
   depends on it. */
footer{position:relative;z-index:2;overflow:hidden;border-top:1px solid rgba(43,42,38,.14);
  padding:clamp(60px,9vh,110px) clamp(24px,5vw,80px) 0;
  background-color:#2E3620;
  background-image:url(../assets/footer_ground.jpg?v=cp80);
  background-size:cover;background-position:center;background-repeat:no-repeat}
/* RETIRED checkpoint 80, 2026-08-28: the painted-SVG ground documented in
   this whole comment block (and the footer .foot-paint rule just below
   it) is superseded -- see this file's own checkpoint 80 footer-ground
   note further up, next to the current live footer{} rule above, for
   what replaced it and why. Kept on record rather than deleted, per house
   practice; nothing below this point still renders.
   checkpoint 79 ruling (2) continued: the painted olive watercolour ground
   itself. Construction: an inline SVG (index.html, inside <footer>, first
   child, ~1.7KB of markup -- not a data-URI, not a file asset under
   src/assets/, matching the site's own existing idiom of literal inline
   <svg> markup for every other painted/organic shape (.wash, .sprig, the
   edge-lines), all now retired but the pattern is the house one). Its own
   viewBox is a fixed 800x800 square scaled with preserveAspectRatio
   "xMidYMid slice" -- the SAME crop-not-distort behaviour
   background:cover gave the retired photograph, so the paint keeps its
   own proportions at every footer aspect ratio from the two-column
   desktop grid down to the stacked mobile one, cropping rather than
   stretching. Three olive tones, deepest dominant: a full-bleed base
   fill in the darkest (#2E3620), then two soft-edged wash shapes in the
   other two (#3C4A28, #556339) plus one extra pooling shape reusing the
   darkest tone for a second, deeper wash layered into one corner
   ("layered soft washes" plural) -- reusing the darkest hex for that
   fourth shape means it can only darken a pixel further, never lighten
   one past the three measured tones below. Every wash shape is hand-
   drawn as one organic closed curve, then run through its own
   feTurbulence(fractalNoise)+feDisplacementMap+feGaussianBlur filter
   (three filters, different seed/frequency each, so no two washes share
   one texture) so the edge bleeds like pigment feathering into wet paper
   rather than reading as a vector blob with a hard outline -- paint, not
   a flat fill and not a photograph. color-interpolation-filters:sRGB is
   set explicitly on every filter: SVG's own default (linearRGB) blurs
   alpha in linear light, which measurably darkens/lightens a soft edge
   away from what the hand-picked hex values alone predict; sRGB keeps
   the rendered blend matching ordinary alpha compositing, so the
   contrast measurements below hold at the actual rendered edge, not just
   at each shape's own solid core.
   LEGIBILITY LAW: cream --paper (#E5E1CD) text measured against each of
   the three flat tones by the WCAG relative-luminance formula --
     #2E3620 (base fill, dominant)      : 9.61:1
     #3C4A28 (wash one, mid)            : 7.26:1
     #556339 (wash two, lightest)       : 4.95:1
   -- so even the single lightest tone in the whole painting clears the
   4.5:1 AA floor on its own, before any text ever sits on it. Rendered-
   pixel proof (P23_footer.png / P23_footer_mobile.png, both widths,
   text hidden then re-measured at the same coordinates): average ground
   across the full footer measured 8.1:1 against --paper; the single
   lightest sampled patch under any live footer text measured 4.87:1.
   Both numbers verified with the text-hidden pixel-sampling method
   recorded in HANDOFF for this checkpoint -- neither is a theoretical
   figure from the hex values alone.
footer .foot-paint{position:absolute;inset:0;z-index:1;width:100%;height:100%;pointer-events:none}
RETIRED checkpoint 80 ruling, 2026-08-28, continued from the note above
this whole block: the rule line just above is dead CSS now that
.foot-paint has no live markup left to select (index.html, checkpoint 80
reconciliation). Kept, not deleted. */
.foot-grid{position:relative;z-index:3;display:grid;grid-template-columns:1.5fr 1fr;
  gap:clamp(30px,6vw,90px);align-items:start;max-width:1200px;margin:0 auto}
.foot-grid .share{font-family:var(--serif);font-weight:300;
  font-size:clamp(24px,3.2vw,40px);line-height:1.32;color:var(--paper);margin:0;
  text-wrap:balance;max-width:16em}
.foot-col{font-family:var(--sans);font-size:12.5px;letter-spacing:.1em}
.foot-col a{display:block;color:var(--paper);text-decoration:none;margin-bottom:10px;
  text-transform:uppercase;letter-spacing:.14em}
/* checkpoint 89 fix, 2026-08-28, part of the mobile sweep: measured tap
   target on these three (what happens, packages, Begin a Visit, the same
   nav duplicated in the footer per PAGE_MAP.md) at 390 wide was 327x17px,
   well under the 40px minimum, with only a 10px gap (not itself tappable)
   between one and the next. Padding replaces margin as the vertical
   rhythm on mobile only (desktop's tighter footer, already approved, is
   untouched): each link's own box grows to a 40px-tall tap target, and
   the visual gap between links grows a little as a direct, accepted
   consequence rather than a targeted redesign. */
@media(max-width:640px){
  .foot-col a{padding:11.5px 0;margin-bottom:0}
}
.foot-col .foot-ig{display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;margin:2px 0 8px -9px;color:var(--paper)}
.foot-col .foot-ig svg{width:18px;height:18px;stroke:currentColor;stroke-width:1.5;fill:none}
.foot-bar{position:relative;z-index:3;display:flex;justify-content:space-between;
  align-items:baseline;max-width:1200px;margin:clamp(40px,7vh,70px) auto 0;
  padding:18px 0 26px;border-top:1px solid rgba(43,42,38,.12);
  font-family:var(--sans);font-size:11px;letter-spacing:.14em;color:var(--paper)}
.foot-bar .slot{font-family:var(--serif);letter-spacing:.34em;text-transform:uppercase;
  font-size:clamp(12px,1.1vw,14px);color:var(--paper)}
/* checkpoint 78 ruling: the hover strike on a footer nav link (shared
   rule up top, .foot-col a:not(.foot-ig):hover) paints var(--verm) --
   measured 1.27:1 against the darkened green ground, i.e. the vermillion
   reads as near-black-on-near-black and effectively vanishes. Swapped to
   var(--paper) for footer links only; every other strike on the site
   (nav, veil) sits on the pale page ground where vermillion already
   reads clearly and is untouched. */
footer .foot-col a:not(.foot-ig):hover{text-decoration-color:var(--paper)}
/* checkpoint 78 ruling: the footer's own colour block and keyline come
   off outright, along with every other .wash/.edge-line instance site-
   wide -- see this file's own checkpoint 78 note further up. Retired,
   not deleted.
.foot-wash{position:absolute;z-index:1;left:-4%;right:-4%;bottom:-42%;height:70%;
  background:#D3D7CC;
  clip-path:url(#footWashClip)}
footer .edge-line.el-footwash{left:-4%;right:-4%;bottom:-42%;height:70%}
*/

/* checkpoint 77, the fable read's remaining two elements:
   (c) a barely-there pale grey cloud, the steps section's "whisper" --
   RETIRED at checkpoint 78, see below.
   (d) fine olive botanical line sprigs, pure line work (stroke only, no
   element below carries a fill), placed in three of the site's emptier
   stretches per the ticket: the interlude's own open margin, beside the
   footer, and (packages.html) one utility page ground. Checkpoint 78
   ruling: the operator named the footer's own sprig for removal
   specifically ("the... lie plant illustration at the footer") but did
   not name the other two, so only the footer instance is retired below
   -- the interlude's sprig (index.html) and packages.html's own sprig
   survive untouched, both still built on the shared .sprig rule and
   .sprig path/ellipse stroke just below, unchanged. aria-hidden, sized
   as marginalia -- never a decoration wall. */
/* checkpoint 78 ruling: the "whisper" cloud is a .wash instance under the
   operator's own "boulder shapes... dotted throughout the site"
   instruction, so it comes off same as every other one -- see this
   file's own checkpoint 78 note further up. overflow:visible on .steps
   existed solely so this cloud (right:-40%) could bleed past the
   section's own narrow box uncropped; with the cloud gone, that rule is
   equally vestigial and retires with it. Retired, not deleted.
.steps{overflow:visible}
.steps .wash.cloud{right:-40%;top:6%;width:clamp(200px,26vw,360px);
  height:clamp(220px,30vh,380px);background:#EDEDE9;opacity:.55}
*/
/* checkpoint 79 ruling (3), retired 2026-08-28: the operator named "this
   random line plant" at the interlude and ordered it off outright; the
   packages.html twin comes off with it under the foreman's reading of
   evident intent (both are the SAME shared .sprig markup/rule the
   operator was pointing at, just placed in two different emptier
   stretches per the checkpoint 77(d) note above -- naming one and not
   the other reads as an oversight of which pages carry a copy, not as a
   deliberate carve-out, especially once the footer's own third instance
   was already named for removal at checkpoint 78). With the interlude
   instance (index.html) and the packages.html instance both retired at
   this checkpoint, and the footer instance already retired at checkpoint
   78 above, there is no live .sprig element left anywhere in src/ -- so
   the shared base rule and its one surviving live instance selector
   below are dead weight and retire with their last consumers, same
   practice as #washClip/#footWashClip at the top of index.html. Retired,
   not deleted; .sprig.pkg's own instance rule stays retired in
   pages/packages.html itself, next to the markup it sized.
.sprig{position:absolute;pointer-events:none;z-index:1;opacity:.8}
.sprig path,.sprig ellipse{fill:none;stroke:var(--moss);stroke-width:1.1px;stroke-linecap:round}
.interlude .sprig{left:1%;top:-2%;width:clamp(52px,7vw,80px)}
*/
/* checkpoint 78 ruling: the footer's own sprig instance is retired (the
   operator named it explicitly); the shared .sprig rule and the
   interlude/packages instances above/elsewhere are untouched -- see the
   checkpoint 77(d) note just above for why those two survive.
footer .sprig{left:2%;bottom:6%;width:clamp(46px,6vw,72px)}
*/
@media(max-width:860px){.foot-grid{grid-template-columns:1fr}.foot-bar{flex-direction:column;gap:8px}}
@media(max-width:860px){
  .chapter{grid-template-columns:1fr}
  .chapter.flip .words{order:1}.chapter.flip .zone{order:2}
}

.sr-only{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%)}
/* checkpoint 78 ruling: recolours to var(--paper) with the rest of the
   footer's text (see the footer{} note above) -- :not(.meta-chip) keeps
   the two footer meta-chip spans (the contact-placeholder guard, the
   destination-pages-unbuilt note) on their own site-wide .meta-chip
   colour/background instead, since this selector's own specificity
   (one class + one type) would otherwise win over .meta-chip's (one
   class alone) and silently repaint them cream too. */
.foot-col span:not(.meta-chip){display:block;color:var(--paper);margin-bottom:10px;text-transform:uppercase;
  letter-spacing:.14em;font-size:11.5px}

/* ── checkpoint 73: the primary buttons are a strip of red tape, not a
   black box, and the comic-book press spark (js/press.js, checkpoint 72
   ruling 10) is retired outright -- the file is deleted and every include
   of it removed; nothing below references it.

   .cta (the vow section's "Begin a Visit") and .btn (packages/begin/
   confirmation) share this ONE definition. Each page used to duplicate
   its own flat black box (this file's .vow .cta, css/begin.css's .btn,
   and inline <style> blocks on packages.html/confirmation.html) -- all of
   those are gutted down to page-specific layout only now, so there is
   exactly one place the look is set.

   The strip borrows the .card .tape pieces' own visual language (slight
   translucency, a soft shadow that reads as "stuck on", a thin top-edge
   highlight suggesting a glossy surface) and renders it in the brand
   vermillion, with the two short ends cut/torn rather than knife-straight.
   That texture paints on a ::before layer, absolutely positioned behind
   the label, NEVER on .cta/.btn themselves -- so the clip-path that gives
   the torn ends can never shrink the clickable box or clip the focus
   ring. .cta/.btn keep a plain rectangular box (same padding, same
   metrics as the old button) for hit-testing and outline purposes; only
   the ::before layer is cut to shape.

   --tilt carries each instance's angle (2 to 3 degrees; .cta and the two
   .btn variants below intentionally alternate the sign so the strips
   don't all lean the same way). The whole element rotates by --tilt, and
   the nested .tapelabel span counter-rotates by the exact negative, so
   the printed word stays perfectly horizontal while the tape around it
   sits at an angle -- the two rotations are applied to two different
   boxes (outer strip, inner label) so they never fight on one transform. */
.cta,.btn{
  --tilt:-2.4deg; --tape-rgb:198,58,43; --tape-a:.9;
  position:relative;display:inline-block;isolation:isolate;
  font-family:var(--sans);font-weight:500;font-size:13px;
  /* checkpoint 77: the label used to render in var(--paper), which read
     fine only because the old paper token was itself near white
     (#FCFBF8). Now that --paper is the sampled image cream (#E5E1CD,
     see tokens.css), that pairing measured 3.51:1 against the tape's
     own effective red -- below the 4.5:1 AA floor. var(--raised) is
     pure white and untouched by the cream move, so the label goes back
     to 4.62:1 with no visible change (--raised and the old --paper were
     barely distinguishable to begin with). */
  letter-spacing:.18em;text-transform:uppercase;color:var(--raised);
  background:none;border:0;padding:15px 34px;margin:0;
  text-decoration:none;cursor:pointer;
  transform:rotate(var(--tilt));
  transition:transform .18s ease;
}
.btn{--tilt:2.2deg}
/* the begin page's completion button gets its own sign so the three .btn
   instances (packages, begin, confirmation) don't all lean identically. */
.btn[data-completebtn]{--tilt:-2.6deg}
.cta::before,.btn::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:rgba(var(--tape-rgb),var(--tape-a));
  box-shadow:0 1px 2px rgba(43,42,38,.16), inset 0 1px 0 rgba(255,255,255,.24);
  clip-path:polygon(
    0% 0%, 2.4% 15%, 0.4% 32%, 2.6% 50%, 0.4% 68%, 2.4% 85%, 0% 100%,
    100% 100%, 97.6% 85%, 99.6% 68%, 97.4% 50%, 99.6% 32%, 97.6% 15%, 100% 0%
  );
}
.tapelabel{display:inline-block;transform:rotate(calc(var(--tilt) * -1))}
.cta:focus-visible,.btn:focus-visible{outline:2px solid var(--indigo);outline-offset:3px}
@media(hover:hover){
  .cta:hover,.btn:hover{transform:rotate(var(--tilt)) translateY(-2px)}
  .cta:hover::before,.btn:hover::before{
    box-shadow:0 3px 5px rgba(43,42,38,.24), inset 0 1px 0 rgba(255,255,255,.3)}
}
/* the gated completion button (.is-blocked, toggled by js/begin.js) and
   any statically disabled sibling (confirmation's native `disabled`
   button) read as the same tape, just paler and desaturated while
   blocked -- never a different shape or a hidden button. */
.cta:disabled,.cta.is-blocked,.cta[aria-disabled="true"],
.btn:disabled,.btn.is-blocked,.btn[aria-disabled="true"]{
  --tape-rgb:171,156,149; --tape-a:.5;
  color:rgba(255,255,255,.68);
  pointer-events:none;cursor:not-allowed;
}
@media(prefers-reduced-motion:reduce){.cta,.btn{transition:none}}
