/* ============================================================
   VELLEKOR - SHARED PAGE STYLES

   Extracted verbatim from index.html's inline style block on
   13 August 2026, when the download page became a second document
   that needed the same system. Not one declaration was changed in
   the move: the block was cut whole, and it still loads in the
   same cascade position it occupied inline, immediately after
   styles.css. Verified by capturing 30 selectors x 17 computed
   properties before and after - identical, and the page's
   scrollHeight was 4925 both times.

   WHY A FILE AND NOT A SECOND COPY: the capture and post-submit
   rules are shared by both pages. Duplicated, they would have
   forked on the next edit, and a fork that only shows on one of
   two pages is the kind of drift nobody notices until a reader
   does.

   NO url() APPEARS IN THIS FILE, checked at extraction, and that
   is a rule rather than an accident. Relative paths in an external
   sheet resolve against the SHEET, not the document, so adding one
   here is not the same as adding one inline. This file sits beside
   the pages in site/, so a path here would be written from site/ -
   the same as the pages use. Compare tokens/fonts.css, which is
   one level down and correctly says ../assets/fonts/.
   ============================================================ */


*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--surface-page); color: var(--text-body);
  font-family: var(--font-serif); font-size: var(--text-body-size);
  /* NO font-variant-numeric HERE - finding 20, and it is removed rather
     than scoped because measurement says no face on this page wants it.
     Rendered at 40px and measured:
       Source Serif 4   "000" and "111" are the same width already, with or
                        without the property. The serif ships tabular
                        figures by default, so the declaration was inert.
       Libre Franklin   "000" is 26.75px wider than "111" - AND STILL
                        26.75px WIDER with tabular-nums applied. The face
                        has no tnum table, so the property computes
                        correctly and changes nothing.
     Inherited by everything, it was doing nothing for the face that did
     not need it and nothing for the face that did. Anything that must be
     the same width across digits needs an explicit width - which is what
     the commitment nodes ended up doing. */
  line-height: var(--leading-body);
}
/* EVERY PARAGRAPH CARRIES THE CAP - finding 33. Page 08 requires it, and
   .measure was applied to exactly one paragraph, so the rule is moved onto
   the element instead of onto a class nobody remembers to add. Declared
   here, a paragraph added later is capped without anyone thinking about it,
   which is the only version of this that stays true.

   64ch, NOT 80. The 80ch allowance needs leading of 1.6 or looser AND a
   size of 18px or more, and it is BOTH conditions. Body here is 17px, so
   the page gets 64 everywhere it does not opt out. The hero lead is the one
   paragraph that qualifies for 80, and it renders at 58ch, so the cap never
   engages there.

   MEASURED BEFORE APPLYING, at 1440px: of twelve paragraphs, nine were
   already inside 64ch and the cap changes nothing about them. The three
   that are not are handled by the two exceptions below. */
p { text-wrap: pretty; margin: 0 0 var(--space-4); max-width: var(--measure-body); }
/* EXCEPTION ONE: the label voice. .cap is tracked uppercase, and a ch unit
   is meaningless against it - "0" in a proportional lowercase face is
   nothing like a tracked capital, so 60 characters of .cap measure far past
   64ch and the cap breaks a one-line label onto two. The measure clause is
   written for prose; this is a label. Left uncapped deliberately, and
   reported rather than forced. */
.cap { max-width: none; }
/* THE STAIRCASE KEEPS ITS OWN CAP, AND IT IS NOW LEGAL - see .step p. It
   was never uncapped; it carried 80ch, which the standard allows only at
   18px or more. Rather than tighten the measure, the step body moved to
   18px, so 80ch qualifies on both conditions. The argument is at the rule. */
p:last-child { margin-bottom: 0; }
img, svg { display: block; }
h1, h2, h3 {
  font-family: var(--font-serif); font-weight: var(--weight-regular);
  color: var(--text-heading); margin: 0; text-wrap: balance;
}

a {
  color: var(--link); text-decoration: underline;
  /* 3px holds the rule clear of Source Serif 4's descenders at body size.
     Not derived from anything - it is the smallest offset that does not
     touch a p or a g here, found by eye and left alone since. If the body
     size or the face changes it needs looking at again. */
  text-decoration-color: var(--link-underline); text-underline-offset: 3px;
  transition: color var(--transition-color), text-decoration-color var(--transition-color);
}
a:hover { color: var(--link-hover); text-decoration-color: var(--link-hover); }
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: var(--focus-ring) solid var(--clr-brown-black); outline-offset: var(--focus-offset);
}
.tile--ink a:focus-visible, .tile--clay a:focus-visible { outline-color: var(--clr-ivory-ground); }

/* ---------- scrollbar ----------
   The OS default is a rounded gray bar on a white gutter - the only thing
   on screen not drawn from the seven, and round in an identity whose
   spacing token file states outright that there are no radii.

   TWO SYNTAXES, AND THEY MUST NOT BE STACKED. From Chromium 121 the
   standard `scrollbar-color` takes precedence and silently switches OFF
   every ::-webkit-scrollbar rule - so declaring both would quietly cost
   the width and the gutter rule everywhere except Firefox. The @supports
   test hands each engine exactly one: Chromium and Safari take the
   pseudo-elements, Firefox falls through to the standard property.

   Warm Slate for the thumb, not Hairline. Hairline on Ivory Deep is about
   1.3:1 - it would be a scrollbar you cannot see. Warm Slate is 7.4:1,
   clears the 3:1 a non-text control owes with room to spare, and is the
   body-text color rather than a new value. Brown Black on hover, which
   is a color transition and the only kind motion.css permits.

   The track carries the same hairline the page uses to separate columns,
   so the gutter reads as an edge of the layout rather than as browser
   chrome bolted on. 12px is a little slimmer than the ~15px native bar
   and still an easy target; going thin() was rejected for that reason. */
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track {
  background: var(--surface-page);
  border-left: var(--rule-hairline) solid var(--clr-hairline);
}
::-webkit-scrollbar-thumb {
  background: var(--clr-warm-slate);
  border-radius: 0;
  transition: background-color var(--transition-color);
}
::-webkit-scrollbar-thumb:hover { background: var(--clr-brown-black); }
::-webkit-scrollbar-corner { background: var(--surface-page); }

@supports not selector(::-webkit-scrollbar) {
  html { scrollbar-color: var(--clr-warm-slate) var(--surface-page); }
}

.skip {
  position: absolute; left: -9999px;
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; text-decoration: none;
  background: var(--surface-reverse); color: var(--text-on-reverse);
  padding: var(--space-3) var(--space-4);
}
.skip:focus { left: var(--gutter-page); top: var(--space-3); z-index: 10; color: var(--text-on-reverse); }

.wrap { max-width: var(--measure-max); margin: 0 auto; padding: 0 var(--gutter-page); }
.measure { max-width: var(--measure-body); }

/* .eyebrow is gone with the hero kicker it existed for. The page now has
   no eyebrows at all, which is what both design skills asked for and what
   the frozen copy previously made impossible. */

/* The wordmark line, in the lockup where it belongs. Set as the FOOTER
   sets it - reading serif at small size, not the uppercase tracked label
   the old eyebrow used - because the header and footer lockups should be
   the same object seen twice, and because the nav beside it is already
   uppercase tracked sans. A second tracked line there would read as a
   fifth nav item.

   The hairline before it is the page's own divider, so the pair reads as
   one lockup rather than as a mark with a caption floating after it. */
.head-tag {
  margin: 0; font-size: var(--text-small); color: var(--text-label);
  white-space: nowrap;
  padding-left: var(--space-4);
  border-left: var(--rule-hairline) solid var(--clr-hairline);
}
/* Visible on arrival, gone once the page moves. A tagline that is always
   on screen stops being read - it becomes furniture. This way it belongs
   to the arrival moment and the stuck header is pure wayfinding.
   display, not opacity: the No Fade Rule says a change of state here is a
   surface swap, never a dissolve. */
.site-head--stuck .head-tag { display: none; }
.site-head__brand { display: flex; align-items: center; gap: var(--space-4); min-width: 0; }
/* ---------- two kinds of label, and they must not look alike ----------
   These were one class. "The problem" and "The tells" name a SECTION and
   mirror the nav; "One", "Two", "Three", "Five operating commitments" and
   "What we are against" label a TILE inside one. Setting both as the same
   italic serif lead-in flattened that - a reader scanning the page could
   not tell a new section from another card in the one they were already
   in.

   The split follows what each one is for. A section label is wayfinding,
   so it takes the nav's own voice - uppercase, tracked, sans - and sits
   under a full-width rule, which is the only horizontal rule in the
   content and therefore reads unambiguously as "new section". A tile label
   stays the italic serif with its short vertical tick.

   NOTE this reopens the eyebrow question the taste skill closed. Its
   resolution was to keep every frozen word but drop the uppercase
   micro-label signature, which is exactly what is being put back for two
   strings. Both are still frozen copy and neither loses a word; only the
   presentation moves, and it moves because the flattening was a real
   wayfinding cost. */
.section-label {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; line-height: var(--leading-body);
  /* --text-accent, AND IT IS THE TOKEN AGAIN. This read clay-deep for one
     reason: clay on Ivory Deep is 4.36:1, under AA at this size, and the
     page used to paint Ivory Deep. Conforming to Always 02 puts this label
     on Ivory Ground, where clay measures 5.03:1 and passes - so the
     substitution has nothing left to fix and the semantic token can say
     what it means. This is the clearest thing the inversion was costing:
     the page's accent color was unusable on the page's own surface.
     The RULE above never moved - a 2px bar is a graphic and owes 3:1. */
  color: var(--text-accent);
  margin: 0; padding-top: var(--space-4);
  border-top: var(--rule-strong-weight) solid var(--rule-accent);
}

/* Frozen label strings that would otherwise be eyebrows. */
.lead-in {
  font-family: var(--font-serif); font-size: var(--text-lead); font-style: italic;
  color: var(--text-accent); margin: 0 0 var(--space-4);
  padding-left: var(--space-4);
  border-left: var(--rule-hairline) solid var(--rule-accent);
  line-height: var(--leading-body);
}
.tile--ink .lead-in, .tile--clay .lead-in {
  color: var(--clr-ivory-ground); border-left-color: var(--clr-ivory-ground);
}
.cap {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; line-height: var(--leading-body); color: var(--text-label); margin: 0;
}
.tile--clay .cap { color: var(--clr-ivory-ground); }
.tile--ink .cap { color: var(--clr-ivory-deep); }

/* ---------- nav. One line, 72px, and it stays. ----------
   Sticky rather than fixed: fixed removes the header from flow, so the
   first screen would have started 72px too high and every anchor target
   would have needed the offset applied twice. Sticky keeps it in flow and
   the browser does the rest.

   --head-h is declared once here and read by scroll-margin-top below. If
   the header's height ever changes, that is the only number to move. */
:root { --head-h: 72px; }
.site-head {
  position: sticky; top: 0; z-index: 50;
  background: var(--surface-page);
  border-bottom: var(--rule-hairline) solid transparent;
  transition: border-color var(--transition-color);
}
/* Set by the observer at the foot of the file, once the page has moved. */
.site-head--stuck { border-bottom-color: var(--rule); }

/* Anchors land under a sticky header unless the target reserves the room.
   Every nav destination is a section id, so this is the whole fix. */
section[id] { scroll-margin-top: calc(var(--head-h) + var(--space-4)); }

/* Smooth scroll is a wayfinding aid, not decoration - it shows the reader
   the distance they just traveled instead of teleporting them. It is
   still motion, so it is off for anyone who has asked for less. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

.site-head__in {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-5); min-height: var(--head-h); flex-wrap: wrap;
}
/* ---------- the lockup ----------
   Mark plus wordmark. The mark is the plinth form: two columns on a base,
   which is the same figure the hero image elaborates.

   Both sizing relationships come from tokens/typography.css rather than
   being invented here:
     --wordmark-lockup-gap  0.173em  -> the gap, as a share of the wordmark
     --wordmark-cap-height  0.77em   -> what the mark aligns to

   Both tokens are in em, where 1em = 1000 font units. The wordmark master
   is viewBox -8 -780 6205 790: baseline at 0, cap top at -780, and 790 units
   tall overall (cap height 770 plus ~10 units of round-letter overshoot top
   and bottom). So the image is 0.79em tall, NOT 1em - which means the em has
   to be recovered from the rendered height before either token can be used:

       --wm-em = --wm-h / 0.79
       gap     = 0.173em  ->  0.173 x --wm-em
       mark    = 0.77em   ->  0.77  x --wm-em   (aligns to cap height)

   Applying 0.173 to --wm-h directly, as if the image were 1em, undershoots
   the gap by about 21%.

   40px, not 26px: at 26px on a 1x display Caslon's serifs and the 1.32px tie
   between the two L's fall below one device pixel and go soft. The header
   min-height is 72px and the lockup plus padding is 56px, so this costs no
   header height at all. */
.site-head__brand > a:first-child {
  --wm-h: var(--wm-h-screen);
  --wm-em: calc(var(--wm-h) / 0.79);
  display: inline-flex; align-items: center;
  gap: calc(0.173 * var(--wm-em));
  padding: var(--space-1) 0;
}
.lockup__mark { height: calc(0.77 * var(--wm-em)); width: auto; }
.wordmark { height: var(--wm-h); width: auto; }
.site-nav { display: flex; gap: var(--space-5); align-items: center; flex-wrap: wrap; }
.site-nav a {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; text-decoration: none; color: var(--text-label);
  padding: var(--space-2) 0;
}
/* Hover and "you are here" both underline. An underline is the hyperlink
   affordance and these ARE hyperlinks, so it is the right gesture here for
   the same reason it was the wrong one on a word in a sentence.

   6px of offset clears the labels without drifting: they are uppercase, so
   there are no descenders for the rule to cut, and no skip-ink is needed.
   Drawn in the current text color, so hover and current agree. */
.site-nav a:hover,
.site-nav a[aria-current="location"] {
  text-decoration: underline;
  text-decoration-thickness: var(--rule-strong-weight);
  text-underline-offset: 6px;
}
.site-nav a:hover { color: var(--link-hover); }
/* The section you are actually in. Set by the observer as aria-current, so
   the state is announced as well as drawn - a color-only "you are here"
   tells a screen reader nothing.

   Clay-deep for the same reason as the section label: plain clay on Ivory
   Deep is 4.36:1 and these labels are 11px. It also matches the hover
   color, so the nav has one accent rather than two nearly-identical
   clays. */
.site-nav a[aria-current="location"] { color: var(--link-hover); }

/* ---------- buttons ----------
   NO OFFSET BLOCK - finding 47. Every button used to carry one, drawn in
   the color the button was about to become so that hover traded the two
   places. It was a good mechanism and it is gone, because the standard
   permits ONE block per view on the object that view is about, and this
   page was rendering twelve. The block now lives on the closing mark
   alone.
   WHAT CARRIES THE STATE INSTEAD is what always carried it: each button
   is the opposite of the surface it stands on, and hover swaps that
   surface and its border on --transition-color. The block was a second
   answer to a question the fill had already answered.

   THE FOUR CASES SURVIVE UNCHANGED, and they are the reason the buttons
   still need four rules rather than one - a fill the same color as its
   tile is an invisible button, which is the failure this could quietly
   have shipped with:

     hero CTA        on Ivory Deep    ink    <-> clay
     closing CTA     on the ink tile  clay   <-> Ivory Ground
     book capture    on Ivory Ground  ink    <-> clay
     hero capture    on the clay tile ink    <-> Ivory Ground

   The last one is why the capture button cannot have a single hover: the
   default clay would land a clay button on a clay tile and dissolve it. */
.btn, .capture button {
  transition: background-color var(--transition-color), border-color var(--transition-color),
              color var(--transition-color);
}

.btn {
  display: inline-block; font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; text-decoration: none; white-space: nowrap;
  /* Declared rather than left at `normal`, which is a UA value and not on
     the published set. Snug is the single-line stop. */
  line-height: var(--leading-snug);
  padding: var(--space-4) var(--space-5);
  background: var(--surface-field); color: var(--text-on-field);
  border: var(--rule-hairline) solid var(--surface-field); border-radius: var(--radius);
}
/* The label MUST be re-declared here. The global `a:hover` sets color to
   clay-deep, and at (0,1,1) it outranks `.btn` at (0,1,0) - so a .btn:hover
   rule that only changes the background leaves a clay-deep label on a
   clay-deep fill, and the text vanishes. That was the bug.

   Hover now inverts the button: Ivory Ground fill, Brown Black label, at
   14.63:1. A surface swap, which is how this identity states changes. */
.btn:hover, .btn:active {
  background: var(--clr-ivory-ground);
  border-color: var(--clr-brown-black);
  color: var(--clr-brown-black);
}
/* INK AT REST, CLAY ON HOVER - the inverse of .btn, and the two are not
   arbitrary variants. Each button is the opposite of the surface it stands
   on: ink on the light hero, clay on the ink tile at the close. That is one
   rule stated twice, not two styles.

   Clay at rest was the loudest thing in the hero and it was competing with
   the hook, which is the line that should own the arrival. Ink puts the
   button in the same weight as the headline above it, and holds clay back
   as something the reader uncovers. */
.btn--ink { background: var(--surface-reverse); border-color: var(--surface-reverse); color: var(--text-on-reverse); }
.btn--ink:hover, .btn--ink:active { background: var(--surface-field); border-color: var(--surface-field); color: var(--text-on-field); }

/* ---------- tile grid ---------- */
.tiles { display: grid; grid-template-columns: repeat(12, minmax(0,1fr)); gap: var(--space-4); }
.tile { padding: var(--space-6); }
.tile--page { background: var(--surface-raised); }
.tile--outline { background: var(--surface-raised); border: var(--rule-hairline) solid var(--clr-hairline); }
.tile--clay { background: var(--surface-field); color: var(--text-on-field); }
.tile--clay h2, .tile--clay h3 { color: var(--text-on-field); }
.tile--ink { background: var(--surface-reverse); color: var(--clr-ivory-deep); }
.tile--ink h2, .tile--ink h3 { color: var(--text-on-reverse); }
.tile--flush { padding: 0; }

/* ---------- the closing tile ----------
   Sentence left, colonnade right, IN FLOW rather than absolutely
   positioned. That distinction is the whole reliability of this block: an
   absolute figure and a flowing sentence are two things measured against
   different boxes, and every viewport width between the breakpoints is a
   chance for them to meet. As flex children they cannot overlap at any
   width, whatever the statement's clamp() is doing to the type size.

   The negative margins are what let the figure keep the tile's 36px
   padding off its own three outer edges: the shafts have to leave through
   the top edge and the steps have to sit ON the bottom edge, or the
   building is a picture of a building sitting in a box. overflow:hidden on
   the tile is what does the cropping. */
.tile--close { display: flex; align-items: stretch; gap: var(--space-6); overflow: hidden; }
/* THE WIDTH IS SET BY THE SENTENCE, not by what looks right. At the 46px
   ceiling "You get back to what only you can do." measures 693px and the
   tile's content box is 996px, so with the 36px gap the mark has 267px
   before it starts folding the close onto four lines - and those two lines
   are display:block specifically so the break can only ever land on a full
   stop. A figure that undoes that is worse than no figure.

   IT IS NOT BLED, and that was tried first. Run to the tile's full 178px
   with negative margins the mark stops being a mark: the uprights come out
   at 39px wide and the base at 182x40, and three slabs that size read as
   an abstract stripe pattern that happens to be in the brand color. The
   ghost wordmark in the hero survives that treatment because a word stays
   legible at any size; three rectangles do not - they ARE the size.

   So it sits inside the padding at the height of the sentence beside it,
   which also gives the two things a shared top and bottom edge rather than
   an arbitrary one. 41:40 means the width follows at 109px, well inside
   the 267px ceiling. */
.close-mark {
  position: relative;
  flex: 0 0 auto;
  align-self: stretch;
  height: 100%;
  aspect-ratio: 41 / 40;   /* the master's own viewBox */
  margin-left: auto;
}
.close-mark img { position: absolute; inset: 0; width: 100%; height: 100%; }
/* THE PAGE'S ONE OFFSET BLOCK - findings 13, 46 and 49 all land here.
   This is now the only object on the page carrying one, which is what the
   standard permits: once per view, on the object that view is about. The
   closing mark is the page's terminal object.

   FOUR PIXELS, NOT EIGHT - finding 13. The silhouette stretches to the
   tile: 106px on its shorter side at 1280px, 109 at the 1100 breakpoint.
   4% of that range is 4.2 to 4.4, which clears the 3px floor and sits well
   under the 12px cap, so it rounds to 4px everywhere with no clamping.
   Eight was the staircase's offset borrowed wholesale, and a fixed offset
   behind an object this large is exactly what reads as a two-color press
   slipping rather than as a decision.

   DECLARED AS A COMPONENT CONSTANT - finding 46. CSS cannot express "4% of
   my own shorter side", so the proportion is computed once here and the
   measurement is written beside it. Any change to the mark's rendered size
   invalidates this number.

   ONE MECHANISM - finding 49, and this is the deviation worth reading. The
   block used to be a SECOND <img> of the ink master, translated 8px down
   and right under the ivory one. The standard sanctions exactly one
   implementation, `box-shadow: X Y 0 0 <color>`, and forbids a translated
   duplicate - but box-shadow casts from the element's BOX, and this object
   is a silhouette with transparent corners, so a box-shadow puts a solid
   rectangle behind three bars. That is not a second impression of the
   mark; it is a slab.
   drop-shadow with a zero blur radius and an opaque palette color is the
   same figure the standard describes - a second impression of the object,
   slipped down and right - and it is the only form of it that follows the
   silhouette. It carries no blur, no spread and no alpha, so the no-shadow
   clause is satisfied on its own terms. It also does what finding 49
   actually asks: the duplicate element and the transform are both gone,
   and one mechanism draws the block.

   IT APPEARS ON HOVER RATHER THAN SITTING THERE. The mark is asking for
   nothing; it is the page's last piece of furniture. Held permanently, an
   ink offset behind it reads as misregistration rather than as depth.

   THE OFFSET IS CONSTANT AND ONLY THE COLOR MOVES, so this is a color
   transition like everything else - the block is always drawn, in
   transparent, and hover paints it. Interpolating the offset instead would
   make it a geometry change.

   IT APPEARS ON HOVER RATHER THAN SITTING THERE, and that is the one place
   this parts company with the buttons. A button carries its block at rest
   because weight it already has reads as an affordance - the thing is
   asking to be pressed. The mark is asking for nothing; it is the page's
   last piece of furniture. Held permanently, an 8px ink offset behind a
   109px mark reads as misregistration, like a two-color print that has
   slipped, rather than as depth.

   It replaced a straight ivory-to-ink color swap. The swap worked but it
   was the same gesture the buttons and the commitments already make, and
   this is the one element on the page that is neither a control nor a
   list - it can afford its own move.

   THE MARK IS THE TRIGGER, not the tile. Hovering the whole tile fired it
   every time a cursor crossed 1068x178px on its way somewhere else, which
   made a deliberate detail read as a twitch. Bound to the mark it is
   something you find and then do on purpose.

   The trigger is the 109px box, not the ink inside it - the transparent
   corners around the uprights count. Tightening that to the glyph would
   mean a hit area with notches in it, which is a worse thing to hunt for
   than a square.

   No focus state and no touch equivalent: the mark is not interactive,
   and hiding nothing means costing nothing. Reduced motion collapses the
   duration, so the block arrives instantly rather than not at all. */
/* The silhouette measures 106px on its shorter side at 1280px, 109 at the
   1100px breakpoint - it stretches to the tile. 4% of either is 4.2-4.4,
   so it rounds to 4px across the range, clear of the 3px floor. */
.close-mark { --block-offset: 4px; }
.close-mark img {
  filter: drop-shadow(var(--block-offset) var(--block-offset) 0 transparent);
  transition: filter var(--transition-color);
}
.close-mark:hover img {
  filter: drop-shadow(var(--block-offset) var(--block-offset) 0 var(--clr-brown-black));
}
/* 1100px, AND IT IS NOT THE PAGE'S 900. Measured slack between the
   sentence and the mark:

     1120px viewport -> 87px      920px viewport -> 4px
     1020px viewport -> 36px

   1100 is where the statement's clamp() reaches its 46px ceiling. Above
   it the sentence is a fixed 693px and every extra pixel of viewport is
   slack; below it the type shrinks with the viewport but the tile shrinks
   faster, so the margin collapses - and by the page's own 900px
   breakpoint there are four pixels left. Four pixels is not a layout, it
   is a coincidence: any fallback metric while the webfont loads folds the
   close onto four lines, and those two lines are display:block precisely
   so the break can only ever land on a full stop.

   So the mark retires early, at the last width where it is comfortable
   rather than the last width where it fits. It is decoration, the header
   carries the mark at every width, and the close is the page's last
   sentence. */
@media (max-width: 1100px) {
  .tile--close { display: block; }
  .close-mark { display: none; }
}
.c12 { grid-column: span 12; } .c8 { grid-column: span 8; } .c7 { grid-column: span 7; }
.c6  { grid-column: span 6; }  .c5 { grid-column: span 5; } .c4 { grid-column: span 4; }
.c3  { grid-column: span 3; }

/* ---------- hero ---------- */
.hero { padding: var(--space-6) 0 0; position: relative; }
.hero h1 {
  font-size: clamp(34px, 4.8vw, 58px); line-height: var(--leading-hero);
  letter-spacing: var(--track-hero); margin: var(--space-5) 0; max-width: 21ch;
}
/* 42ch broke this 111-character line across three rows. 58ch lands it on
   two, and still sits inside the c7 column (615px) at the lead size. */
.hero__lead { font-size: var(--text-lead); margin-bottom: var(--space-6); max-width: 58ch; }

/* ---------- the dissolve ----------
   "invisible" thins out under the cursor and nowhere else, which is the
   headline performing its own sentence.

   Mechanism: a mask, not an opacity change on the element. A
   radial-gradient mask is parked far off-box until the pointer enters,
   then its center is driven to the cursor. Nothing moves - only the mask
   center changes - so there is no layout or transform work, just a repaint
   of one word.

   THE COLOR QUESTION IS SETTLED. This used to break the No Fade Rule:
   ghosting is meant to be a surface swap and never opacity, because
   opacity yields colors outside the published set, and at 15% the ink
   read as a warm gray that was not one of them. Standard v1.4 published
   that gray as Ivory Veil, and finding 52 required the page to PAINT it
   rather than keep mixing it. Both halves of the objection are now gone:
   the dissolved word is a named color, and it is a swap between two
   painted colors rather than a fade to transparency. The proposed
   alternative - stacking a Hairline copy underneath - is superseded and
   should not be revisited; Veil is the published answer.

   Still a deliberate exception: this tracks the pointer, which motion.css
   restricted to color transitions when it was written. Asked for on
   31 July 2026, and page 10 of the standard now permits motion outright.

   Guards: no hover support and reduced-motion both fall back to plain ink. */
.dissolve {
  --mx: -999px;
  --my: -999px;
  /* 62px on a ~206px word: the patch is clearly local, roughly three
     letters at full effect plus falloff. 88px reached half the word and
     read as dissolving the lot; 46px was a pinhole. Tune here. */
  --dissolve-r: 62px;
  display: inline-block;
  position: relative;
  color: var(--text-heading);
}
/* TWO PAINTED COLORS, NOT AN ALPHA RAMP - finding 52, and the mechanism is
   inverted from what it was. The mask used to thin the ink to 15% and let
   the page show through, which meant the dissolved word was a RUNTIME
   BLEND: ink mixed with whatever surface happened to be behind it. That
   color now has a name - Ivory Veil #D6D1C9, ink at 15% over Ivory Ground,
   resolved flat - and a named color that is still being blended drifts the
   moment the surface changes. This page's surface changed once already
   this month, which is the whole argument.

   HOW IT WORKS NOW. The element paints the word in Ivory Veil. An ink copy
   sits exactly on top, and the pointer-tracked radial masks the INK away
   rather than thinning it - so what the pointer uncovers is a painted
   veil, not transparency. The mask geometry is unchanged: same radius,
   same 18% plateau, same falloff. Only what it reveals is different.
   Change --surface-page now and the dissolved word does not move.

   GATED, AND THE GATE IS LOAD-BEARING. If the alt-text content syntax is
   unsupported the pseudo never generates - so the base rule above paints
   the real word INK, and an engine that cannot do this gets a plain
   headline rather than a veil-colored one nobody can read. Same pattern
   and the same reasoning as the fracture.

   The guards are unchanged and still do their job: the script bails on
   reduced motion and on no-hover, so --mx/--my stay off-box, the mask
   stays fully opaque, and the word is plain ink. */
@supports (content: "a" / "b") {
  .dissolve { color: var(--veil); }
  .dissolve::after {
    content: attr(data-word) / "";
    position: absolute; left: 0; top: 0;
    color: var(--text-heading);
    pointer-events: none; user-select: none;
    -webkit-mask-image: radial-gradient(circle var(--dissolve-r) at var(--mx) var(--my),
        transparent 0%, transparent 18%, #000 100%);
            mask-image: radial-gradient(circle var(--dissolve-r) at var(--mx) var(--my),
        transparent 0%, transparent 18%, #000 100%);
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
  }
}
/* ---------- the wordmark artwork ----------
   DELIBERATE BREAK OF THE MOTION RULE. tokens/motion.css states
   "color transitions only, no movement, no bounce". This slides.
   Jeff asked for it on 31 July 2026 after the constraint was put to
   him, so it is a decision, not an oversight.

   The mark is 128% of its container, so 28% of the container width
   hangs past the right edge. Translating the image by -21.875% of its
   own width (0.21875 x 128% = 28%) brings that tail exactly flush.

   THE OVERHANG IS DECLARED HERE, AS --wm-scrub, AND NOT IN THE SCRIPT.
   It used to be a hardcoded `img.style.width = '128%'`, and an inline
   style outranks a media query - so the mobile rule that set 190% was
   dead the whole time JS was running, and only took effect in the one
   case the fallback was written to keep unclipped. Two faults from one
   line. Declared as a custom property, the breakpoint below can move it
   and the script never argues with the stylesheet.

   Superseded: the hover reveal was replaced with a scroll-driven one.
   See the .ghost-mark rules further down. The mark is decorative and
   aria-hidden either way, so nothing is hidden behind it but a flourish.

   ---- A BROKEN COMMENT KILLED THIS RULE, AND THEN KILLED IT AGAIN ----
   When --wm-scrub was added, the comment above was closed early and the
   remaining three lines were left as BARE TEXT in the stylesheet, ending
   with an orphaned close-comment delimiter. CSS error recovery skips
   forward looking for a resync point, and what it swallowed was the whole
   .ghost-mark rule below - so --wm-scrub, overflow: hidden AND margin-top
   were all silently dead.
   WHAT THAT COST, all of it invisible: the band lost its air above and sat
   against the capture tile, which is the symptom that got noticed. It lost
   the clip that is the entire reason a cropped wordmark works. And with no
   --wm-scrub defined at desktop, `width: var(--wm-scrub)` was invalid at
   computed-value time, so the image fell back to `auto` and rendered at its
   natural 6205px, overflowing the page sideways.
   THE SECOND TIME WAS THIS COMMENT. The first repair described the bug by
   writing the two-character close sequence literally, inside a comment -
   which closed it early and swallowed the same rule a second time. Never
   write that sequence inside a comment; say the name of it instead.
   THE LESSON IS ABOUT COMMENTS, NOT MARGINS: in a file where comments run
   twenty lines, closing one in the wrong place does not produce a comment
   bug, it produces a silently missing RULE, and nothing reports it. */
/* --space-8, THE SECTION RHYTHM, and it is the value this rule always
   carried - the bug was that the rule was not applying, not that the number
   was wrong. Restored rather than re-chosen. Checked at 1440px: 88px above
   the band and 88px below it, because the next section's own padding-top is
   --space-8 too, so the band sits symmetrically in the rhythm rather than
   being pushed off it. --space-9 was tried first and is worse for exactly
   that reason: 112 above against 88 below reads as a gap that missed. */
.ghost-mark { --wm-scrub: 128%; overflow: hidden; margin-top: var(--space-8); }

/* FALLBACK, and the state anything without scroll-driven animation gets:
   the whole wordmark, no motion, nothing clipped. This is also what a
   reduced-motion reader sees. */
.ghost-mark img { width: 100%; max-width: none; height: auto; }

/* Hover is gone. It was an EVENT, so a cursor crossing this full-width band
   during a scroll fired a 700ms slide it never meant to, then fired again on
   the way back - which is what read as buggy.

   Scroll position is not an event. The offset is a function of how far the
   band has traveled through the viewport, so there is nothing to trigger:
   scroll down and it opens, scroll back and it closes, at exactly the rate
   you scroll. It cannot fire accidentally because it does not fire at all.
   It also works identically on touch, so the hover guard is unnecessary.

   THE TRAVEL IS MEASURED, NOT DECLARED. It used to be a constant -21.875%,
   which is correct for 128% and for nothing else: the offset that lands the
   tail flush is (imgW - bandW) / imgW, so it changes the moment the width
   does. With the width now set per breakpoint, a constant would leave the
   mobile pan either short of the edge or past it. Derived from the two
   measured widths it is right at any width, and it re-derives on resize and
   rotation for free. At 128% it still computes 21.875, so desktop is
   unchanged to the third decimal.

   Driven from JS rather than CSS `animation-timeline: view()`. That was the
   first implementation and it is the better one on paper - it runs off the
   main thread - but it cannot be relied on here: scroll-driven timelines
   tick on the compositor, and any context that does not composite frames
   leaves the animation frozen part-way, which looks like a bug rather than
   a static fallback. CSS.supports() reports true in exactly that case, so
   feature detection does not catch it.

   THE WIDTH IS APPLIED BY A CLASS THE SCRIPT ADDS, not by the stylesheet
   alone. So with no JS, or under reduced motion, the element keeps the 100%
   width above and simply shows the whole wordmark - a deliberate resting
   state, never a half-slid one. The class is what makes that true: put the
   overhang on `.ghost-mark img` directly and every engine that never runs
   the script gets a permanently clipped wordmark with no way to pan it,
   which is what the mobile rule was doing. */
.ghost-mark.is-scrub img { width: var(--wm-scrub); }
/* ---------- the hero image ----------
   ONE PHOTOGRAPH, NO SEQUENCE. Jeff's call, 2 August 2026: "I don't want any
   of the old hero image generation/blends, just this single image on the
   page." What went with it is written up at the top of this file - the mark,
   the drawn colonnade, the occluding backdrop, the reveal mask and five sets
   of keyframes. This is what is left, and it is deliberately almost nothing.

   THERE IS NO TILE AND NO BACKGROUND, which is now a property of the IMAGE
   rather than of the CSS. This was a Warm Slate panel with a 2px clay rule
   across its top; it went at v12 for congruence, because every other surface
   on this page is ivory and a dark rectangle in the hero was the one place
   the page argued with itself. The photograph is baked onto #F6F1E8 and
   feathered to it at every edge, so it has no boundary to see - the file is
   a rectangle, the picture is not.

   NOTHING HERE PAINTS A BACKGROUND, and that is load-bearing rather than
   tidy: if this box ever gets a fill, the image's own baked ivory becomes a
   visible panel on top of it and the whole blend is undone.

   aspect-ratio AND THE FILE MUST AGREE - the image is authored at exactly
   the box's ratio so `cover` crops nothing at any size. It also reserves
   the box before the image decodes, so the column cannot reflow. Change one
   and the other has to move with it. */
.hero-media {
  /* 1/1, WAS 3/4, AND THE CROP CHANGED WITH IT - the box and the file have
     to state the same ratio or `cover` starts throwing pixels away.
     WHY IT MOVED: measured on the 3:4 file, the stone filled 81% of the
     width but only 43% of the HEIGHT, sitting at y=512 with the entire top
     43% of the frame empty ivory. Only 11.6% of the pixels were subject.
     That is a subject shrunk twice - once by a c5 column, again by its own
     empty frame - which is why the hero read as weak.
     Cropping to 1:1 costs nothing: the width is untouched, so the file
     still resolves at 2.08x in this box, and only dead vertical space is
     removed. Measured after: 81% w, 57% h, 15.4% of pixels, with 212px of
     margin above the stone and 178 below. */
  aspect-ratio: 1 / 1; width: 100%;
  display: block; position: relative;
}
.hero-img {
  display: block; width: 100%; height: 100%; object-fit: cover;
}

/* ---------- figures ---------- */
.figure {
  font-family: var(--font-sans); font-weight: 700;
  font-size: clamp(46px, 7vw, 96px); line-height: 0.86;
  letter-spacing: -0.03em; display: block;   /* no tabular-nums: inert in Libre Franklin, see body */
}
.figure--sm { font-size: clamp(34px, 4vw, 58px); }

/* ---------- THE BLEND ----------
   Candidate 1's outlined numeral inside candidate 3's tile. Kept
   intact: it is the reason this candidate exists. Hairline stroke is
   1.54:1, so the numeral is decorative and hidden from assistive tech. */
/* ---------- THE TELLS, AS A CONTROL SURFACE ----------
   Three states and a color for each, which is what makes a counter
   readable: you have to be able to sweep the grid and know instantly which
   are down.

     REST      Ivory Ground, no block          the tile as it always was
     HOVER     Ivory Deep + a Warm Slate block this one is reachable
     SELECTED  Clay + an ink block             this one is true of me

   THE LADDER IS DELIBERATE. Ivory Ground to Ivory Deep is the smallest
   step the palette has - barely a state, which is right for a hover that
   only says "you can press this". Ground to Clay is the largest step it
   has short of ink. So the gap between "considering" and "chosen" is much
   wider than the gap between "resting" and "considering", and the grid
   reads at a glance from across the room.

   THE BLOCK IS THE PAGE'S OWN DEVICE, same 8px hard offset as the
   staircase and every button, and it follows the same two-surface rule:
   it has to clear the page it falls on AND the tile it falls from.
     hover     Warm Slate  7.59 vs the Ivory Deep page,  8.75 vs the tile
     selected  Brown Black 12.69 vs the page,            2.91 vs clay
   Ink on clay at 2.91 is the same figure as the clay step in the
   staircase, and acceptable for the same reason: it is a slab of color,
   not something being read. */
.tell {
  /* button reset. Everything else comes from .tile and .c4/.c5/.c7. */
  appearance: none; -webkit-appearance: none;
  border: 0; font: inherit; text-align: left; width: 100%; cursor: pointer;
  /* THE NUMERAL SCALES WITH THE TILE, and the column hugs the glyph rather
     than being declared. At a fixed 104px the numeral column was 43% of a
     c4 tile and 17% of a c7 - so 05, 06 and 07 had 139px of measure and ran
     to four lines while 01 and 04 sat on one. Measured, not guessed.
     Sizing the numeral off the span turns that from a defect into the point:
     the tiles are three widths, so they get three numeral sizes, and the
     asymmetry Jeff asked to keep now reads in the type as well as the grid.
     `auto` on the first track means the column is exactly as wide as the
     glyph at whatever size it lands, with no leftover gutter to tune. */
  display: grid; grid-template-columns: var(--tell-c) minmax(0,1fr);
  gap: var(--space-5); align-items: start;
  /* These two are ALIASES, not values - finding 37. The ladder itself lives
     in tokens/typography.css; --tell-n and --tell-c exist only so that the
     span classes below can select a rung without restating the grid and
     the numeral rule. They hold no number of their own. */
  --tell-n: var(--tell-num-1); --tell-c: var(--tell-col-1);
  /* THE HAIRLINE IS DECLARED AT REST, IN TRANSPARENT, and that is the
     whole reason hover can be color-only. Adding a border on hover would
     change the box; declared here and recolored there, the geometry never
     moves. box-sizing is border-box, so the 1px costs the content box and
     not the tile.
     It replaces the offset block this tile used to carry - finding 47.
     Seven blocked tiles in a grid is the standard's named failure state,
     and it was also the hover's only signal, so the block could not just
     be deleted. A hairline is the identity's other answer to the same
     question, and the tells are now allowed it precisely because they no
     longer have a block. */
  border: var(--rule-hairline) solid transparent;
  transition: background-color var(--transition-color), border-color var(--transition-color);
}
/* THE COLUMN IS DECLARED, NOT `auto`, and this is the tabular-figures trap
   again. Libre Franklin has no tnum table, so `auto` sized each column to
   its own digits and the three c4 tiles - which sit side by side and are
   read as one row - started their text at 69, 69 and 66. The declared
   widths are the widest glyph at each size plus a couple of pixels:
   102 -> 104, 84 -> 86, 69 -> 72. */
.tell.c6 { --tell-n: var(--tell-num-2); --tell-c: var(--tell-col-2); }
.tell.c5 { --tell-n: var(--tell-num-3); --tell-c: var(--tell-col-3); }
/* c4 CAME DOWN FROM 52/72 AND A 20px GAP, and every one of those three
   numbers is doing work. 07 is the longest sentence in the row - 60
   characters against 53 and 52 - and it was the only tile in the section
   running to four lines.
   Measured, the break back to three happens between 185px and 187px of
   measure. The old 52/72/20 gave 179. 48/64/14 gives 193, which clears it
   by eight rather than by two, because the webfont loads after first paint
   and a fallback metric that costs three pixels would have put 07 back on
   four lines with nothing in the CSS to explain why.
   The gap did the last six pixels so the numeral did not have to shrink
   further: at 44px it starts reading as a different size of thing rather
   than the same numeral scaled. */
.tell.c4 { --tell-n: var(--tell-num-4); --tell-c: var(--tell-col-4); gap: var(--space-3); }
/* THE HOVER IS COLOR ONLY, AND IT HAS TO BE, which is the tightest corner
   in this component. The tile cannot step its surface: it rests on Ivory
   Deep, the only lighter ivory is Ivory Ground, and Ivory Ground is the
   page - so a hovered tile would dissolve into the background it is
   supposed to sit on. The palette holds no mid-tone between them. And
   since finding 47 the tile has no offset block either, which was the
   hover's other signal.
   SO THE HAIRLINE CARRIES IT, recolored from transparent to Clay, with the
   sentence going Clay Deep beneath it. Two color changes on one 120ms
   transition, no geometry.
   MEASURED against the Ivory Deep tile: the clay rule is 4.36:1, which
   clears the 3:1 a non-text graphic owes; Clay Deep on the sentence is
   5.70:1 and clears text AA. The numeral keeps its own fill, declared with
   the other numeral states below - it is a color change too, so it costs
   this rule nothing.
   SCOPED OFF THE PRESSED STATE. A chosen tile is clay; a clay hairline on
   clay is not a hairline, and a Clay Deep sentence on clay is 1.31:1. The
   pressed state is the component's second level and it answers for itself
   below, unchanged. */
.tell:not([aria-pressed="true"]):hover { border-color: var(--rule-accent); }
.tell:not([aria-pressed="true"]):hover .tell__t { color: var(--clr-clay-deep); }
/* NO BLOCK HERE EITHER - finding 47. The pressed tile kept its ink block
   through the first draft of this fix, which would still have rendered up
   to seven of them; the acceptance test is one per page, so it goes. The
   full ink-on-clay inversion and the Ivory Ground numeral, which are what
   "pressed" actually reads as, are untouched. */
.tell[aria-pressed="true"] {
  background: var(--surface-field);
}
/* The pressed tile's own hover: clay deepens to Clay Deep. This is the
   second state level answering the pointer in its own vocabulary, and it
   is why the rule above is scoped off the pressed state rather than
   fighting it. */
.tell[aria-pressed="true"]:hover { background: var(--clr-clay-deep); }
.tell[aria-pressed="true"] .tell__t { color: var(--text-on-field); }
/* The selected numeral is set with the rest of the numeral states, down at
   .tell__n - it fills rather than just restroking, so keeping a second
   half-version of that rule up here would have been two places to change
   one thing, and the loser would have been silent. */
/* These ARE interactive now, so unlike the steps and the commitments they
   get a real focus ring. Keyboard reaches them in document order and Space
   toggles them, both for free from <button>. */
.tell:focus-visible { outline: var(--focus-ring) solid var(--clr-brown-black); outline-offset: var(--focus-offset); }
.tell[aria-pressed="true"]:focus-visible { outline-color: var(--clr-ivory-ground); outline-offset: -6px; }
/* THE NUMERAL FILLS IN AS YOU COMMIT, which is the clearest thing the
   three states do. Hollow at rest, solid and dark under the pointer, solid
   and light once chosen - a ghost, then a mark, then a mark on a fill.
   It also gives the hover a second signal beyond the tile's own color,
   which matters because Ground to Deep is deliberately the smallest step
   in the palette and on its own it is nearly subliminal.

   Warm Slate on Ivory Deep is 7.59:1, so the hovered numeral is genuinely
   dark rather than merely less faint - Hairline filled would have been
   1.34:1, which is the trap: filling it in is not the same as darkening it.

   The stroke color has to move with the fill or the glyph ends up solid
   with a hairline of a different color around it, which reads as a
   printing fault. */
.tell__n {
  font-family: var(--font-sans); font-weight: 700; font-size: var(--tell-n); line-height: 0.8;
  letter-spacing: -0.03em; color: transparent;
  -webkit-text-stroke: var(--rule-hairline) var(--clr-hairline);
  user-select: none;   /* no tabular-nums: inert in Libre Franklin, see body */
  transition: color var(--transition-color), -webkit-text-stroke-color var(--transition-color);
}
.tell:hover .tell__n {
  color: var(--clr-warm-slate); -webkit-text-stroke-color: var(--clr-warm-slate);
}
.tell[aria-pressed="true"] .tell__n,
.tell[aria-pressed="true"]:hover .tell__n {
  color: var(--clr-ivory-ground); -webkit-text-stroke-color: var(--clr-ivory-ground);
}
.tile--ink .tell__n, .tile--clay .tell__n { -webkit-text-stroke-color: var(--clr-ivory-deep); }
.tell__t { display: block; font-size: var(--text-lead); color: var(--text-heading); }

/* ---------- THE TALLY ----------
   Sentence left, count right, and the tile answers the count. */
.tally { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-6); }
.tally__line { max-width: 24ch; margin: 0; }
.tally__count { display: grid; justify-items: end; gap: var(--space-4); flex: 0 0 auto; }
/* Set at the same 76px as the tells' own numerals, and solid where those
   are outlined. The grid above is seven hollow numbers; this is the one
   filled numeral in the section, which is what makes it read as the sum of
   them rather than an eighth of them. */
/* IT CLAMPS NOW, BECAUSE IT IS A WORD AND NOT A DIGIT. At a flat 76px "seven"
   is 202px wide where "0" was about 40 - and the sentence beside it shrinks
   with the viewport while a fixed numeral does not. Measured at a 463px
   viewport: 161px of sentence + 36 gap + 202 of word against 319px of tile,
   so it ran 80px past its own edge. The clamp puts it on the same footing as
   the .statement it sits next to, which is the thing it has to stay in
   proportion with.
   tabular-nums is gone with the digits. It was inert anyway - Libre Franklin
   has no tnum table - but on a word it is not even the right intent.
   nowrap because "seven" breaking across two lines would move the pips. */
.tally__n {
  font-family: var(--font-sans); font-weight: 700;
  font-size: clamp(46px, 7vw, 76px); line-height: 0.8;
  letter-spacing: -0.03em; white-space: nowrap;
  color: var(--clr-ivory-ground);
}
.tally__pips { display: flex; gap: var(--space-2); }
.tally__pips i {
  /* An 11px square - the same figure as --text-label-size, and deliberately
     not that token: the pip is a mark, not type, and coupling it to the
     label size would mean a change to the label scale silently resized the
     denominator. It is a square because the identity has no radii, and it
     is small because seven of them sit under a 76px numeral without
     competing with it. */
  display: block; width: 11px; height: 11px;
  border: var(--rule-hairline) solid var(--clr-ivory-ground);
  transition: background-color var(--transition-color);
}
.tally__pips i.is-on { background: var(--clr-ivory-ground); }

/* THE TILE INVERTS AT THREE. Its sentence is "If three of those are true,
   that is the conversation." - so at three the sentence stops being a
   condition and the tile stops being clay. That is the entire reason this
   section is interactive; every other state change here exists to make
   this one land.
   Ink rather than any other color because it is the page's heaviest
   surface and this is the page's heaviest claim. Everything on it was
   already Ivory Ground, which is 14.63:1 on ink, so nothing needs
   recoloring to survive the swap.

   RESTORED AT v15, REVERSING v13's E - Jeff's call, and it puts this rule
   back to exactly what v12 shipped. v13 had replaced the inversion with the
   offset block the selected tells carry, on the argument that the block is
   the section's taught vocabulary for "chosen". That argument was right
   about the vocabulary and wrong about the job: the seven tiles say "you
   picked this one", and the tally is not a thing the reader picked - it is
   the page answering. A signal borrowed from the tiles makes the answer the
   eighth tile. The inversion is the one moment on the page where a surface
   changes what it IS rather than what it is wearing, and it is spent on the
   one line the whole section is built to reach.
   THE COUNT DOES NOT REVERT WITH IT - Jeff, same call. It keeps v13's
   spelled-out none/one..seven and the clamp; v12's fixed 76px digit capped
   at "3+" does not come back. So this file is v12's color and v13's count.
   THAT PAIRING IS BETTER THAN EITHER WHOLE VERSION, and worth saying why: at
   exactly three the sentence's "three" and the count's "three" become the
   same word, and now the tile turns over underneath them on the same beat.
   The digit could never do that - "3" and "three" are not the same word - so
   the moment the inversion exists to serve only became available when the
   digits went.

   WHAT WENT WITH IT. v13's .tally__word rule is gone: it turned the word
   "three" Brown Black, which was 2.91:1 on clay and would be 1.00:1 on this
   ink tile - the same color, invisible. The <span class="tally__word"> stays
   in the markup on purpose, unstyled, because the alt-three-block variant is
   still awaiting Jeff's verdict and needs the hook. THAT VARIANT DID NOT MOVE
   INTO THIS REPO - it is in the archived OneDrive folder at
   working/alt-three-block, named by FROZEN.md there.
   AND THAT VARIANT IS NOW MUTUALLY EXCLUSIVE WITH THIS RULE, which is the
   thing to know before merging either: its ink field behind the word only
   reads against a clay tile. If the ink-block "three" wins, this inversion
   goes; if the inversion stands, that variant is dead. They cannot both
   ship. */
.tally { transition: background-color var(--transition-color); }
.tally[data-hit="true"] { background: var(--surface-reverse); }

/* ---------- the tally's own CTA ----------
   Hidden until the tile inverts, and hung off the SAME attribute that
   inverts it. One state, so the button and the surface cannot disagree
   about whether three was reached.

   flex-basis 100% ON A WRAPPING ROW puts it on its own line under the
   sentence and the count rather than as a third item beside them. .tally
   is a two-item flex row and those two still fit, so adding wrap changes
   nothing at rest - it only gives this a line to land on.

   display:none IS THE POINT, not a shortcut. visibility:hidden would hold
   the space and leave a permanent gap under the sentence at rest, and
   off-screen positioning would leave a tab stop on a control nobody can
   see. display:none removes it from layout, the accessibility tree and the
   tab order at once, and gives all three back together. The cost is that
   it cannot be transitioned, which is the right trade: the tile it appears
   on is already changing colour under it, and a button fading in on top of
   an inverting surface is two motions competing to say one thing. */
.tally { flex-wrap: wrap; }
.tally__go { display: none; flex: 0 0 100%; margin: var(--space-5) 0 0; }
.tally[data-hit="true"] .tally__go { display: block; }
/* Clay on ink measures 2.91:1, under the 3:1 WCAG 1.4.11 wants for a
   control's own edge - the same finding that put a hairline on .tile--ink
   .btn. The tally is not .tile--ink, it is a clay tile wearing ink, so
   that rule cannot reach it and the border has to be stated again here. */
.tally[data-hit="true"] .btn { border-color: var(--clr-hairline); }

section { padding: var(--space-8) 0; }
/* ---------- three utilities, lifted out of style attributes ----------
   Finding 38. Five inline style attributes carried three distinct
   declarations between them; an inline style outranks every rule in this
   sheet, which is how the ghost band's mobile width sat dead for weeks.
   .section-head   the label row above a section's statement
   .statement--gap the statement's own top offset under that row
   .statement--tight  a 30ch cap, used once, on the tells statement */
.section-head { margin-bottom: var(--space-3); }
.statement--gap { margin-top: var(--space-5); }
/* 30ch, not the 21ch of .statement--pair: this sentence is longer and
   wants to break after "and the", which lands at 30. */
.statement--tight { max-width: 30ch; }
.statement {
  font-size: clamp(34px, 4.2vw, var(--text-display));
  line-height: var(--leading-tight); letter-spacing: var(--track-display);
}
/* Two sentences, one per line. "Nothing is broken." is 18 characters and
   "That is the problem." is 20, so a 21ch measure puts the break exactly on
   the full stop rather than wherever the column happens to run out. balance
   keeps the two lines from drifting apart if the copy is ever edited. */
.statement--pair { max-width: 21ch; text-wrap: balance; }
/* The same idea enforced rather than coaxed: each sentence is its own
   block, so the break cannot land anywhere but the full stop. */
.statement--lines > span { display: block; }

/* The offer line, third in #work's section head under the label and the h2.
   --text-lead under a display heading is exactly the .hero__lead pattern,
   which is the precedent this follows.

   ITS OWN DECLARATION, NOT .hero__lead ITSELF, and this is the third time
   the same trap has come up in this section - after .tally__line, whose
   24ch cap belongs to the tally's flex row, and .hero__lead's own 58ch cap
   and margin-bottom, which belong to the hero column. THE GENERAL FORM:
   sharing a size token is not a reason to share a class named for
   somewhere else. Copy the declaration, not the namespace.

   WHAT IT REPLACES: .offer-terms, which styled the second half of a
   two-line offer block that no longer exists. That rule is gone rather
   than left behind - a declaration with no caller is dead text that reads
   as live, and the next person cannot tell which.

   RELEASED FROM THE PROSE MEASURE, and the first version of this rule got
   it wrong twice - first by copying the hero's 58ch, then by leaning on the
   base `p` cap of --measure-body. Both capped it, and 64ch folded an 88
   character line onto two.

   64ch EXISTS TO KEEP RUNNING TEXT READABLE. That is not this line's job.
   It is one sentence under a heading, read as a unit, and a cap written for
   paragraphs was reaching an element that is a <p> but is not prose - the
   same failure the comment on .step p:not(.step__n) already names, and the
   third instance of it in this section after .tally__line and .hero__lead.
   THE GENERAL FORM, stated once more because it keeps recurring: `p` is a
   structural tag, not a semantic one. Every rule that selects it has to say
   which kind of `p` it means, and every cap has to say what it is capping
   FOR.

   THIS IS A DELIBERATE EXEMPTION FROM THE MEASURE-CAP RULE, not an
   oversight - the same class of exemption .cap already carries, and for the
   same reason: a measure in `ch` means nothing on a line that is not being
   read as a paragraph.

   text-wrap: balance, NOT the `pretty` inherited from `p`. Once released
   the line sets on one at desktop and only wraps when the column gets
   narrow, which is exactly where `pretty` would leave a short orphan on the
   second line. balance splits it evenly instead. */
.work-offer {
  font-size: var(--text-lead); margin: var(--space-4) 0 0;
  max-width: none; text-wrap: balance;
}

/* ---------- the fracture ----------
   "broken" splits along a horizontal seam and the halves slide apart. The
   line claims nothing is broken; the word disagreeing with the sentence is
   the point, so this is doing work rather than decorating.

   Structure: the real word sets the box and stays in the DOM for reading,
   selection, assistive tech and anything parsing the markup, but is drawn
   transparent. Two ::before / ::after copies of it sit exactly on top,
   clipped above and below a 52% seam, and together they render what you
   actually see. At rest both are at zero offset, so it is pixel-identical
   to plain text.

   The seam sits at 52% rather than 50%: the cap height of this serif puts
   the optical middle of a lowercase word slightly below center, so a true
   half splits it through the x-height and reads as a smudge.

   Breaks motion.css again, deliberately, same as the wordmark and the
   dissolve. Reduced motion holds it shut. Touch never triggers it, which
   costs nothing because it hides no information. */
/* ---------- the staircase ----------
   Structure from candidate-2/c: a 64px label column, then the content, and
   each step inset one --space-6 further than the last so the sequence
   reads as advancing.

   The surfaces run ink, ivory, clay - the same progression as the
   reference - but step one is FILLED ink here rather than an outlined
   transparent box. Candidate 2 sits on a dark page, where transparent
   already reads as ink; on this ivory page the same rule would have made
   step one disappear into the background and the staircase would have
   started with nothing. */
.steps { display: grid; gap: var(--space-5); }
.step {
  /* 64px, against a widest ordinal of 46.4px - "THREE" at 11px with 1.76px
     of tracking; ONE is 29.2 and TWO 32.4. So the column carries 17.6px of
     slack, which is deliberate: it is declared rather than `auto` so all
     three rows align down the staircase whatever word they hold, and the
     slack is what keeps the heading's left edge steady if an ordinal ever
     changes. */
  display: grid; grid-template-columns: 64px minmax(0,1fr);
  gap: var(--space-5); align-items: start;
  padding: var(--space-5) var(--space-6);
  /* NO OFFSET BLOCK - finding 47. Each step used to carry one, drawn in
     transparent at rest and colored on hover so that hovering changed a
     color rather than a geometry. Three of them, on a page allowed one.
     The whole apparatus went with it: a per-step --step-block, chosen
     against each step's own fill because a single shared color merged
     into two of the three tiles (Warm Slate measured 8.75:1 on the ivory
     step, 1.74 on clay, 1.67 on ink - so the effect only ever landed on
     step one, which is exactly what Jeff saw). That measurement is worth
     keeping even though the code it justified is gone: A BLOCK MUST CLEAR
     THE SURFACE IT FALLS FROM AS WELL AS THE ONE IT FALLS ON.
     The steps keep the hairline, which was always doing the structural
     work, and their descent is carried by the indent and the fill. */
}
.step__n {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; margin: 0;
  /* Nudged onto the heading's optical baseline. align-items:start puts the
     label's cap-line above the h3's, because 11px and 22px do not share a
     first baseline in a grid. */
  padding-top: 0.35em;
}
.step h3 { font-size: var(--text-h2); line-height: var(--leading-snug); margin: 0 0 var(--space-3); }
/* 80ch, NOT --measure-body's 64ch. 64ch resolves to 560px inside a 904px
   cell, so both short steps were being folded onto a second line with 340px
   of empty space beside them - the break was set by the cap, not by the
   sentence, which is exactly why it read as arbitrary. Measured: 78ch is
   the threshold where steps One and Two each fit their single sentence on
   one line; 80ch gives that a little slack. Step Three is genuinely long
   and still takes three lines, which is a wrap it has earned.
   `pretty` keeps that third line from ending on an orphan.

   OPEN AGAINST PAGE 08 - finding 33, and this is the one thing in that
   finding that is not mechanical. The standard allows 80ch only when the
   leading is 1.6 or looser AND the size is 18px or more. Both conditions.
   The leading here is 1.62 and qualifies; the size is 17px and does not.
   So the cap above is 16ch wider than this size is entitled to.
   THE FINDING SAID THIS PROSE HAS NO CAP. It has one - it has had one
   since the staircase was built, with the measurement above. What it has
   is the wrong number, which is a smaller problem and a different fix.
   MEASURED, so the decision can be made on numbers: dropping to 64ch takes
   step one from one line to two (its sentence is 72 characters) and step
   two from one line to two (88). Step three is three lines either way and
   would not notice.
   RESOLVED BY RAISING THE TYPE, NOT BY TIGHTENING THE MEASURE - Jeff's
   call, 7 August 2026. The step body is now --text-lead, 18px, which is
   already on the published scale. At 18px with 1.62 leading the 80ch above
   satisfies BOTH conditions and stops being an exception at all.

   WHY THIS AND NOT THE OTHER TWO. Capping at 64ch puts 340px of white
   space beside two short steps and sets the break at the cap rather than
   at the sentence - the exact fault the 80 was introduced to fix. Narrowing
   the container changes a layout that was measured. This changes one token
   reference, and it is the only option that makes the existing design legal
   rather than bending the design to the rule.

   NOTHING REFLOWS, and the reason is worth stating because it looks like
   luck: `ch` is a font-relative unit, so the 80ch box grows by exactly the
   same 18/17 the type does. The ratio of text width to box width is
   unchanged, so a sentence that fit on one line still fits.
   MEASURED AFTER, at 1440px: step one 72 characters on one line, step two
   88 on one line, step three 196 on three - identical to before. The box
   is 734.4px, which is 80.1ch, sitting in cells of 904, 868 and 832px as
   the staircase indents. The cap is doing the work, not the cell.

   IT DOES CHANGE RENDERED TYPE SIZE, which is why it needed a decision
   rather than a patch. */
/* :not(.step__n) IS LOAD-BEARING - DO NOT SIMPLIFY THIS BACK TO `.step p`.
   The step ordinal is also a <p>, so an unscoped `.step p { font-size }` at
   (0,1,1) outranks `.step__n`'s own (0,1,0) and silently takes the tracked
   11px label to 18px. Caught by measuring after, not by reading; it looks
   fine at a glance because the label is three characters long.

   IT IS THE SAME FAILURE THE AUDIT HIT ON FINDING 33: a rule written for
   prose reaching an element that is a <p> but is not prose. There it was
   the measure cap landing on .cap, the tracked uppercase label voice, where
   a ch unit means nothing. Here it is a size landing on the ordinal.
   THE GENERAL FORM: `p` is a structural tag, not a semantic one. On this
   page it carries prose, section labels, tile captions and step ordinals,
   and only the first of those wants prose rules. Anything selecting bare
   `p` has to say which of them it means. */
.step p:not(.step__n) {
  font-size: var(--text-lead);
  max-width: 80ch; margin: 0; text-wrap: pretty;
}
.step__n { margin: 0; }

/* SURFACES RUN LIGHT TO DARK: ivory, clay, ink. Reversed from the
   reference, and deliberately.

   Weight follows content. Step One is one short sentence and Step Three
   carries the longest paragraph, so putting ink first left most of the
   heaviest block empty. Ink last means the darkest field is also the
   fullest one. It also lets the sequence build rather than front-load, and
   resolving into solidity is what "Stay to build" means - the same claim
   the wordmark line makes in the header. */
.step--1 {
  background: var(--surface-raised); color: var(--text-body);
}
.step--1 h3 { color: var(--text-heading); }
/* Clay-deep, not clay: at 11px on Ivory Ground clay is 4.36:1 and under
   AA. Clay-deep is 6.03:1 and reads as the same accent. */
.step--1 .step__n { color: var(--clr-clay-deep); }

.step--2 {
  background: var(--surface-field); color: var(--text-on-field);
  margin-left: var(--space-6);
}
.step--2 h3, .step--2 .step__n { color: var(--text-on-field); }

.step--3 {
  background: var(--surface-reverse); color: var(--clr-ivory-deep);
  margin-left: calc(2 * var(--space-6));
}
.step--3 h3 { color: var(--text-on-reverse); }
.step--3 .step__n { color: var(--clr-hairline); }

/* ---------- the commitments ----------
   A spine with five drops. The rule across the top is the parent, each
   1px drop is a branch, and the sentence hangs off it - the org-chart
   shape asked for, built from the only two marks this identity has, a
   hairline and a square.

   grid rather than flex, so all five cells share one row height and the
   drops all start on the same line no matter how many lines each sentence
   runs to. Flex would have let each drop float to its own cell's top. */
.charter {
  list-style: none; margin: var(--space-5) 0 0; padding: 0;
  display: grid;
  /* The first column is wider on purpose. Commitment 01 is the only one
     carrying a second clause, and at five equal columns it ran to four
     lines while the rest sat at two - so the row height was set by one
     cell and the other four had a blank line under them.

     Measured across ratios: 1.25fr takes 01 to three lines and leaves
     every other cell at two. 1.5fr also works for 01 but starts pushing
     03 onto a third line, which just moves the problem. Chart height goes
     185px to 157px, and no copy changed - the alternative was trimming
     01, and every natural trim of it turns "not ONLY the people
     describing it" into "not the people describing it", which inverts the
     claim from "we talk to both" into "we exclude them". */
  grid-template-columns: 1.25fr repeat(4, minmax(0,1fr));
  gap: var(--space-5);
  position: relative;
}
/* THE SPINE RUNS NODE CENTER TO NODE CENTER, not edge to edge. As the ul's
   border-top it spanned the full 1068px, overhanging the outer branches by
   114px on the left and 91px on the right - so the chart had two lengths of
   rule connecting nothing, which is the one thing a connector must not do.

   The insets are the halves of the first and last columns. With
   1.25fr + 4 x 1fr the track total is 5.25fr across (100% - 4 gaps), so one
   fr is C/5.25, the first column is C/4.2 and the last is C/5.25 - half of
   each is C/8.4 and C/10.5. Measured against the rendered grid: 113.8px and
   91.0px, against node centers at 114px and 977px. It stays correct at any
   width because it is derived from the same track sizes the grid uses. */
.charter::before {
  content: ""; position: absolute; top: 0; height: var(--rule-hairline);
  left:  calc((100% - 4 * var(--space-5)) / 8.4);
  right: calc((100% - 4 * var(--space-5)) / 10.5);
  background: var(--connector);
  transition: background-color var(--transition-color);
}
.charter li {
  position: relative;
  /* Body size, not --text-small. At 13.5px five short sentences under a
     hairline read as a footnote row, and these are the firm's promises -
     they sit directly under a staircase of 22px headings and have to hold
     their own against it. */
  font-size: var(--text-body-size); line-height: var(--leading-body);
  color: var(--text-body);
  text-align: center;
  transition: color var(--transition-color);
}
/* THE NUMERAL IS THE NODE, and the drop is drawn by the same pseudo.

   The 5px square that used to sit here read as a tick rather than as a
   join. A numeral does the joining AND says which branch this is, and it
   rhymes with the outlined 01-07 in the tells - the page already numbers
   things this way, so the chart is not importing a new device.

   The drop is a 1px solid painted as a background stripe on this same
   element, sized 1px x --space-5 and pinned to its top center. One
   pseudo-element therefore draws both the branch and its node; with two,
   the numeral and the drop had to be positioned against each other and
   any change to one broke the other.

   NOTE ON THE COUNTER: this numbers 01-05 for the eye only. The list is
   unordered in the markup and stays that way, because these commitments
   have no sequence - the numerals count them, they do not rank them. */
.charter { counter-reset: charter; }
.charter li { padding-top: calc(var(--space-5) + var(--space-3)); }

/* THE ROOT NODE. Centered, boxed, and dropping into the spine, so the whole
   block reads as one diagram: parent, trunk, spine, five branches. Its
   drop and the gap above the spine are BOTH --space-5, which is what makes
   the trunk meet the rule instead of stopping short of it. */
.charter-head { text-align: center; }
.charter-root {
  position: relative; display: inline-block; margin: 0;
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--text-on-reverse);
  background: var(--surface-reverse);
  border: var(--rule-hairline) solid var(--surface-reverse);
  padding: var(--space-3) var(--space-5);
}
/* top:100% + the border. For an absolutely positioned child, 100% resolves
   against the PADDING box, not the border box - so a plain top:100% started
   the trunk one pixel above the bottom border and drew a hairline tick
   inside the ink box. Measured: padding box 46px, border box 48px. */
.charter-root::after {
  content: ""; position: absolute; left: 50%;
  transform: translateX(-50%);   /* same half-pixel as the drops */
  top: calc(100% + var(--rule-hairline));
  width: var(--rule-hairline); height: var(--space-5);
  background: var(--connector);
  transition: background-color var(--transition-color);
}
.charter { margin-top: var(--space-5); }

/* THE NUMERAL, IN A BOX. A bare numeral was a label; boxed, it is a node -
   and a box is the one shape this identity has, so five of them under a
   rule read as drawn rather than typeset.

   The box is outlined at rest and FILLS on hover, inverting the numeral.
   That is a surface swap, which is how this identity states a change, and
   it is the loudest thing the block can do without moving anything: five
   cells that shifted on hover would be unusable at this size. */
.charter li::before {
  counter-increment: charter;
  content: counter(charter, decimal-leading-zero);
  /* FIXED WIDTH, NOT fit-content, AND THIS WAS A REAL BUG. The boxes were
     sized to their digits, and the digits are not the same width: measured,
     01 came out 46.67px against 04's 48.66px, with 02 at 48.14 and 03/05
     inside 0.2px of each other. So five boxes that read as one repeated
     element were five different sizes, 01 nearly 2px narrower than the
     rest - which is exactly the way Jeff described it, 01 and 02 looking
     unlike the other three.

     THE DECLARATION THAT SHOULD HAVE PREVENTED IT IS INERT. body sets
     font-variant-numeric: tabular-nums and it computes as tabular-nums on
     these boxes - but Libre Franklin has no tnum table, so nothing applies
     it. Layout matches the proportional advances exactly. A declaration
     that computes correctly and does nothing is the worst kind to trust,
     so the width is stated here rather than assumed from the font.

     50px against a 48.66px widest leaves 1.34px of slack, which is enough
     for the metric differences between the webfont and the fallback while
     it loads. */
  display: block; width: 50px; text-align: center;
  /* Letter-spacing is added AFTER the last digit too, so centered text sits
     half a letter-space left of true center. 0.08em is half of the 0.16em
     tracking, put back. */
  text-indent: 0.08em;
  margin: 0 auto var(--space-3);
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  color: var(--clr-clay-deep);
  /* The box outline is a connector too. Left on flat Hairline while the
     spine and the drops strengthened, the five nodes read as faded holes
     in a diagram that had just been drawn - the lines arriving at nothing.
     Everything ruled in this block moves together. */
  border: var(--rule-hairline) solid var(--connector);
  padding: var(--space-2) var(--space-3);
  transition: background-color var(--transition-color),
              border-color var(--transition-color), color var(--transition-color);
}
/* The branch, drawn separately from the box so it can light with it.

   translateX(-50%) IS LOAD-BEARING, not tidiness. `left: 50%` puts the
   LEFT EDGE of the 1px rule on the node center, so every drop sat half a
   pixel right of the box it points at and half a pixel right of the spine
   endpoint it hangs from. Centered, the drop, the box and the spine all
   agree. The spine needs no matching change - it already runs center to
   center; it was the drops that were off it. */
.charter li::after {
  content: ""; position: absolute; top: 0; left: 50%;
  transform: translateX(-50%);
  width: var(--rule-hairline); height: var(--space-5);
  background: var(--connector);
  transition: background-color var(--transition-color);
}
/* ---------- TWO LEVELS OF HOVER, AND THEY STACK ----------
   Level one, the whole diagram: hovering anywhere in the block strengthens
   every rule in it at once - trunk, spine, five drops and the five node
   outlines. The structure draws itself in.
   Level two, unchanged: hovering one cell lights that branch - its drop,
   its node and its sentence - so the three read as one item.

   Together they are a sequence rather than two effects. The chart arrives
   as a faint arrangement, becomes a diagram when you approach it, and then
   resolves to the branch you are actually on.

   ONE CUSTOM PROPERTY, NOT CASCADING SELECTORS. Every connector reads
   --connector, and the two levels just set it at different depths: the
   block sets it on .chart, a cell sets it on its own <li>, and inheritance
   does the rest. Written as selectors, `.chart:hover .charter li::after`
   would have outweighed `.charter li:hover::after` on specificity and
   silently killed the per-branch state that was already there - which is
   exactly the kind of thing that does not announce itself.

   COLOR, NOT THICKNESS, and this is the part that had to be argued. The
   ask was depth and boldness, and the obvious move is 1px to 2px - safe
   here, since every one of these is absolutely positioned and nothing
   would reflow. It is still wrong: this identity has two marks, a hairline
   and a square, and a hairline that doubles has stopped being one. It also
   breaks the rule the rest of the page keeps, that a state change is a
   color change and nothing moves.
   It does not need the thickness. Hairline on Ivory Deep is 1.34:1 - a
   rule you can only just find. Warm Slate is 7.59:1. That is 5.7x the
   contrast, which lands as a line being drawn rather than a line being
   recolored, and Brown Black on top of it still leaves the selected
   branch clearly ahead of the other four. */
.chart { --connector: var(--clr-hairline); }
.chart:hover { --connector: var(--clr-warm-slate); }

.charter li:hover { color: var(--text-heading); --connector: var(--clr-brown-black); }
/* CLAY FILL, IVORY GROUND NUMERAL, at 5.03:1.

   THE FILL was ink, and an ink fill made the selected node the same object
   as the root box at the top of the chart - two ink boxes on one diagram,
   one of them the parent and one of them just the branch under the cursor.
   Clay separates them: the root stays the structure, the accent marks the
   selection, and it is the color this page already uses for every "you
   are here".

   THE NUMERAL went to Brown Black first and back to Ivory Ground once it
   was on screen. Brown Black on clay is 2.91:1 against the 4.5:1 an 11px
   numeral needs - the same figure that kept clay off the capture button's
   label - and it read as muddy rather than as dark-on-accent, which is the
   usual tell for a pair this close in luminance. Ivory Ground passes and
   looks better, which is not a coincidence: 2.91:1 is roughly where a
   contrast starts failing the eye as well as the metric.

   It is also now the same inversion the numeral makes everywhere else on
   this page - light figure on a filled accent - rather than a third
   treatment invented for one hover state. */
.charter li:hover::before {
  background: var(--surface-field); border-color: var(--surface-field);
  color: var(--clr-ivory-ground);
}

.nobreak { white-space: nowrap; }
.fracture {
  position: relative;
  display: inline-block;
}
.fracture::before, .fracture::after {
  /* The halves used to be two aria-hidden <span> duplicates. aria-hidden
     kept them out of the accessibility tree and user-select:none kept them
     out of a copied selection, but neither touches raw markup, and raw
     markup is what a crawler, a scraper and a reader-mode extractor read.
     The H2 was serving "Nothing is brokenbrokenbroken. That is the
     problem." to every one of them - including, now, the link unfurl this
     page just gained.

     Generated content fixes it at the source: the word is in the DOM once,
     and the two painted copies exist only in CSS.

     THE ` / ""` IS NOT DECORATION. Chromium and Firefox both put
     pseudo-element text into the accessibility tree, so moving the
     duplicates into CSS would have handed the tripled word straight back
     to screen readers - the one audience the old markup got right. The
     alt-text half of the content property overrides what is announced,
     and empty is exactly right: these two are a picture of a word that is
     already being read. */
  content: attr(data-word) / "";
  position: absolute;
  left: 0; top: 0;
  white-space: nowrap;
  color: var(--clr-ivory-ground);   /* the ink tile's heading color */

  /* ---- WHY THESE TWO LINES EXIST: THE HOVER USED TO GLITCH ----
     A clipped element that is transform-animated is the one combination
     that cannot be handed to the compositor as-is. Without a layer of its
     own, Chromium re-rasterizes the clip on every frame of the 180ms, and
     the seam is the worst possible place for that: the box is 52.89px
     tall, so 52% lands on 27.50px and the +0.5px overlap ends at 28.00px -
     a boundary that sits half-way through a device pixel and gets
     re-resolved 11 times on the way across. That is the shimmer.

     will-change promotes both halves once, up front, and the clip is then
     baked into each layer - the clip itself never changes, only the
     transform, so the layers are simply moved instead of redrawn.

     IT HAS TO BE DECLARED AT REST, not inside :hover. Promotion on hover
     IS the flash: the layer is created at the moment the pointer arrives,
     the text re-renders into it, and that re-render is the frame you see.
     Declared here it has already happened before anyone touches it.

     The explicit identity transform is the same argument one step earlier:
     interpolating from `none` makes the first frame a type conversion
     rather than a move, and it is free to just start from zero. */
  transform: translateX(0);
  will-change: transform;

  /* ---- THE HALVES ARE NOT PAINTED AT REST. See the long note below; this
     is the line that ends the hairline-seam bug rather than hiding it.
     opacity, NOT visibility or content:none. The layer has to stay alive
     and promoted through the rest state - that is the whole point of
     will-change above - and hidden/none discards it, which brings back the
     promote-on-hover flash this file already fixed once.
     THE OPACITY SWITCH IS INSTANT AND DELAYED, which is not a contradiction:
     0s duration, 180ms delay. It fires when the halves have finished
     travelling, so on the way OUT the word reassembles first and only then
     hands back to the real text. On the way IN the delay is overridden to 0
     so the swap happens on the same frame the movement starts. */
  opacity: 0;
  transition: transform var(--motion-quick) var(--ease-arrive),
              opacity 0s linear var(--motion-quick);
}
/* Matching half of the same swap, on the real word: transparent the instant
   the pointer arrives, back 180ms after it leaves - the frame the halves
   stop moving and stop being painted. Declared here so the delay belongs to
   the leave; :hover carries the 0s version for the arrival. */
.fracture { transition: color 0s linear var(--motion-quick); }
/* ============ THE HAIRLINE SEAM, AND WHY IT KEPT COMING BACK ============
   THIRD ATTEMPT, AND THE FIRST ONE THAT REMOVES THE CAUSE. The word looked
   faintly cracked when nobody was touching it - the exact opposite of the
   idea, which is that it is whole until it is touched.

   THE CAUSE, stated properly. Two clipped copies meeting at a shared edge
   cannot add up to solid. The boundary row is a partial-coverage edge for
   BOTH halves, so each antialiases it to roughly half alpha and the sum
   lands short. Worse here than in the general case, because will-change
   puts each half on its OWN COMPOSITOR LAYER: the two edge rows are
   rasterized separately and then composited, so they blend towards the tile
   instead of into each other. Two 50% rows over ink give about 75%, never
   100%. And the geometry guarantees the edge is fractional - the box is
   52.89px tall at a y ending in .625, so 52% lands at 27.50px and the seam
   sits mid-device-pixel at DPR 1.

   WHAT WAS TRIED AND WHY IT WAS NEVER GOING TO HOLD. First the seam was
   moved off 50%. Then the top half was given a +0.5px overlap so one of
   them would paint the boundary row solid. Both are attempts to make the
   artifact invisible, and both depend on how a particular engine rounds a
   fractional clip inside a promoted layer - which is exactly the kind of
   thing that changes between Chromium versions, between DPRs, and between
   this machine and a reader's. That is why it came back.

   THE FIX IS TO NOT BUILD THE RESTING WORD OUT OF TWO LAYERS AT ALL. At
   rest the halves are not painted (opacity: 0 above) and what you see is
   the real text node, set once, no clip, no seam, nothing to round. It is
   ordinary text and there is no artifact available to it. On hover the real
   word goes transparent and the two halves light up on the same frame, and
   because their color is identical to the h2's - both #F6F1E8, checked,
   not assumed - the handover cannot be seen.
   THE SEAM STILL EXISTS WHILE THE HALVES ARE ON SCREEN, and that is now
   harmless twice over: they are 6px apart for all of the hover, and the
   only moments they sit in register are the first and last frames of a
   180ms move. A hairline visible for one frame OF A WORD BREAKING APART is
   not a defect, it is the crack.

   THE OVERLAP STAYS ANYWAY. It costs nothing and it cleans up those same
   transitional frames; it is simply no longer load-bearing. If a future
   engine rounds it differently, the resting word - the thing everyone
   actually looks at - is not affected, because it is not made this way. */
.fracture::before { clip-path: polygon(0 0, 100% 0, 100% calc(52% + 0.5px), 0 calc(52% + 0.5px)); }
.fracture::after  { clip-path: polygon(0 52%, 100% 52%, 100% 100%, 0 100%); }

/* EVERY HOVER RULE IS GATED, and the gate now protects more than it did.
   An unsupported `content` value invalidates the whole declaration, so the
   pseudo-elements never generate. Previously `color: transparent` sat on
   .fracture unconditionally and would have deleted the word from the page
   outright on any engine too old for the alt-text syntax (Firefox below
   133). It is now on :hover, so the same failure would have hidden the word
   only while the pointer was over it - still wrong, and still gated.
   Ungated, an engine without pseudo support gets no fracture and a word
   that behaves. Losing an effect is the correct failure; losing a noun is
   not. */
@supports (content: "a" / "b") {
  .fracture:hover { color: transparent; transition: color 0s linear 0s; }
  .fracture:hover::before, .fracture:hover::after {
    opacity: 1;
    transition: transform var(--motion-quick) var(--ease-arrive),
                opacity 0s linear 0s;
  }
  .fracture:hover::before { transform: translateX(-3px); }
  .fracture:hover::after  { transform: translateX(3px); }
}

/* The statement tile is narrow on purpose, so the prose beside it can be
   wide and therefore short. At 46px the longer line measures 378px, so the
   tile needs 379px of text width. c5 is 433px, which leaves only 54px for
   padding - hence 20px sides here rather than the 36px the other tiles use.
   Vertical padding stays at 36px so it still reads as a tile.
   If the statement ever needs to grow past 46px, this tile has to widen to
   c6 or the type will fall to three lines. */
.tile--statement { padding-left: var(--space-4); padding-right: var(--space-4); }
.tile h3 { font-size: var(--text-h2); line-height: var(--leading-snug); margin: var(--space-3) 0; }

/* The `.terms` table styles went with the terms section. Nothing else on
   the page sets a table. */

/* ---------- capture ----------
   Briefly a bare mailto: link, now back to a real field + submit. The
   mailto was correct on the reasoning and wrong in the hand: an input is
   the affordance people read as "give me your address", and a lone button
   in an empty tile reads as an afterthought. Restored verbatim, including
   the two contrast decisions measured for it.

   Nothing receives the address yet - a valid submit is still stopped
   rather than faked. Wiring it up is a later job. */
/* Both lines stay in the tracked caps of .cap; the pair is separated by
   SIZE rather than by changing one of them to serif. The offer title steps
   up to 17px and the description sits at the 11px micro-label, so the
   order is read as title-then-detail instead of two equal labels.

   Color is inherited from .cap either way: Ivory Ground on the clay hero
   tile, which is the only one of the seven that clears 4.5:1 there, and
   warm slate on the ivory tile at the close. */
/* 17px tracked caps needs about 480px and the close tile gives it 450, so
   this line wraps there and only there. Left alone it broke at the hyphen
   in "one-page" - "A ONE-" / "PAGE CHECKLIST" - which reads as a typo. The
   .nobreak span around the second phrase leaves the separator as the only
   break opportunity, so the wrap lands on the "·" and the two halves stay
   whole. It is not shrunk to fit: the size is the distinction that was
   asked for, and a deliberate two-line title costs nothing. */
.capture__label { font-size: var(--text-body-size); line-height: var(--leading-body); }
.capture__desc { margin: var(--space-3) 0 0; }

/* A capture tile carries a permanently reserved error line under its
   submit, and that slot is invisible whitespace at rest - so the tile's
   own 36px bottom padding was landing UNDER 30px of reserved space and the
   button ended up with 72px beneath it, twice what its other three sides
   get. It read as a hole rather than as padding.

   The padding comes in to 14px, so the total under the button is 8 + 22 +
   14 = 44px, close to the 36px the tile uses elsewhere. The reserved slot
   itself is untouched: an error still lands in space already allocated,
   nothing reflows, and the message keeps 14px of clearance below it rather
   than sitting on the tile edge. */
.tile--capture { padding-bottom: var(--space-3); }

/* ---------- the post-submit screen ----------
   Shown in place of the whole offer, not just the form. The label and the
   description describe an offer the reader has just taken, and the fine
   print's first sentence - "You'll get this" - stops being true the moment
   it is sent. That is the same reason the download page uses only the
   second half of the standing privacy line.

   IT TAKES THE BODY VOICE, NOT THE TRACKED CAPS the offer above it uses.
   Same argument the form messages are set by: a tracked micro-caps line
   reads as a system alert, and this is a person telling you something
   arrived. Emphasis comes from size. */
.capture__swap[data-state="offer"] .capture__done  { display: none; }
.capture__swap[data-state="done"]  .capture__offer { display: none; }

/* THE HERO RESERVES ITS HEIGHT BY CONSTRUCTION, NOT BY A NUMBER. Both
   states are placed in the same grid cell, so the wrapper is always as
   tall as the taller of the two and the swap cannot change it - at any
   viewport, at any text size, with no constant that goes stale the next
   time the copy moves. This page has been bitten twice by hardcoded
   geometry that was right for one width and nothing else.

   `visibility`, not `display`: a display:none child contributes no height,
   which is the entire point of the stack. visibility:hidden keeps the box
   and still takes the hidden state out of the tab order and the
   accessibility tree, so the form behind a shown screen cannot be tabbed
   into. These rules must stay AFTER the pair above - equal specificity,
   later wins. */
.capture__swap--reserve { display: grid; }
.capture__swap--reserve > * { grid-area: 1 / 1; }
.capture__swap--reserve[data-state="offer"] .capture__done  { display: block; visibility: hidden; }
.capture__swap--reserve[data-state="done"]  .capture__offer { display: block; visibility: hidden; }

/* Focus is moved here on success so the swap is announced rather than
   silently replacing what the reader was looking at. No ring: this is a
   container the reader never tabbed to, and the ring belongs on controls. */
.capture__done:focus { outline: none; }

/* Sizes copied as declarations, not by borrowing .hero__lead - that class
   carries a 58ch cap and a margin belonging to the hero column. */
.done__lead { font-size: var(--text-lead); line-height: var(--leading-snug); margin: 0; }
.done__body { margin: var(--space-3) 0 0; }
.done__go   { margin: var(--space-4) 0 0; }

/* AN INLINE LINK ON THE CLAY TILE CANNOT USE --link. The link color is
   clay, and clay on clay is 1.00:1 - the same color, invisible. Ivory
   Ground is the only one of the seven that clears 4.5:1 against clay at
   body size, so it is the only value available here.
   THAT LEAVES NO SECOND COLOR FOR HOVER, so hover drops the underline
   instead: a real, visible state change that costs no contrast. Flagged
   for Jeff - this is a new link rule on a locked palette, not a derived
   one. The closing screen has no such problem; its link is a .btn--ink. */
.tile--clay .done__body a {
  color: var(--clr-ivory-ground);
  text-decoration-color: var(--clr-ivory-ground);
}
.tile--clay .done__body a:hover {
  color: var(--clr-ivory-ground);
  text-decoration-color: transparent;
}

/* The close is the last section, and 88px of section padding under it was
   stacking on top of the footer's own rule and 28px inset - 116px of empty
   ivory before anything was said. Between two sections 88px is the page's
   rhythm and it stays; against a ruled footer it is one separator too many.
   56px still reads as a section break, and the rule does the rest. */
#book { padding-bottom: var(--space-7); }

/* 520px is the row's own content, not a round number: the submit measured
   220px wide when it held "Send me the checklist" in tracked caps, and the
   email field took the remaining 300px, which is enough for a full address
   before it scrolls. Measured at 1280px. Both halves flex, so this is the
   ceiling rather than the layout - past it the row starts looking like a
   form on a page built for something else.
   THE SUBMIT NOW READS "Send it to me". Re-measured on a render,
   12 August 2026 at viewport 1400: the button is 146.1px and the field is
   373.9px, and they still sum to the 520px row. Every character that comes
   off the button goes to the field, so the address argument above keeps
   getting better as the label gets shorter. 220/300 and 188.5/331.5 are
   the historic pairs; 146.1/373.9 is current.

   THE LABEL IS NOT "Send me the check" ANY MORE, AND MUST NOT GO BACK.
   A bare "the check" reads as a restaurant bill or a bank draft in US
   English. "The Detection Check" survives because the modifier carries it;
   stripped of the modifier it lands somewhere else entirely. "Send it to
   me" leans on the label sitting directly above the field, which names the
   thing in full - so the referent is on screen and the button does not
   have to repeat it. See the naming rule at the MESSAGES object. */
.capture { max-width: 520px; }
.capture__row { display: flex; margin-top: var(--space-3); flex-wrap: wrap; }
.capture input[type=email] {
  /* 220px basis, and it is the submit's measured width rather than a guess
     - the two halves start equal and the field takes every pixel the row
     has spare, reaching 300px at the 520px cap. Below about 470px the row
     wraps and each takes a full line. */
  flex: 1 1 220px; font-family: var(--font-serif); font-size: var(--text-body-size);
  color: var(--text-heading); background: var(--clr-ivory-ground);
  border: var(--rule-hairline) solid var(--clr-hairline); border-right: 0;
  border-radius: var(--radius); padding: var(--space-4); min-width: 0;
  transition: border-color var(--transition-color);
}
.capture input[type=email]::placeholder { color: var(--text-label); opacity: 1; }

/* The field and the submit share an edge - they are one instrument, so the
   gap between them is exactly 0px. The global focus ring sits at
   outline-offset:3px, which therefore had nowhere to go but on top of the
   button. Both halves draw their ring INSIDE their own box instead.
   2px keeps it above the WCAG 2.2 focus-appearance minimum. */
.capture input[type=email]:focus-visible {
  outline: var(--focus-ring) solid var(--clr-brown-black);   /* 14.63:1 inside the ivory field */
  outline-offset: -2px;
}
/* The submit is Brown Black, so an ink ring inside it would be invisible.
   It takes an Ivory Ground ring - 14.63:1 the other way round.

   DRAWN IN currentColor RATHER THAN NAMED. The button now changes fill on
   hover, and on the clay tile it inverts to Ivory Ground - where a ring
   hard-coded to Ivory Ground would have disappeared into the fill for
   anyone holding a pointer over a keyboard-focused button. currentColor is
   the label's color, which is chosen for contrast against whatever the
   fill currently is, so the ring is correct in all three states by
   construction. On the ink rest state it resolves to the same Ivory Ground
   this rule always drew. */
.capture button:focus-visible {
  outline: var(--focus-ring) solid currentColor;
  outline-offset: -2px;
}
.capture input[type=email]:focus { border-color: var(--clr-brown-black); }

/* ---------- form messages ----------
   The browser's native validation bubble is an OS tooltip: gray, system
   font, arrow, no relation to any of this. It cannot be styled at all, so
   the forms carry `novalidate` and the message is built inline instead.

   Set in the reading serif rather than the uppercase tracked label. A
   tracked micro-caps line reads as a system alert; this is meant to sound
   like a person explaining something, so it takes the voice the body copy
   uses.

   One capture sits on Clay and the other on Ivory Ground, so the color
   follows the surface: Brown Black on the light tile at 14.63:1, Ivory
   Ground on the clay tile at 5.03:1. Both clear AA for normal text.

   The slot is ALWAYS in the flow, empty at rest, holding exactly one line.
   Nothing moves when a message appears - it lands in space already reserved.
   That matters more than the 22px it costs, because the message appears
   after a click, so any reflow would move content under a cursor that has
   already committed.

   No max-width. 48ch resolves to 344.1px (re-measured 12 August 2026) while
   the message it was originally fighting measured 410px, which is what was
   breaking it across two lines.
   THAT ARGUMENT IS NOW HISTORIC: the messages have only got shorter, and
   re-measured 12 August 2026 they are 294.9px (empty) and 320.4px
   (invalid), so 48ch would in fact clear both today. The rule stays off
   anyway. A cap here buys nothing (the note sits in 458.8px and the
   strings are authored short, right below), and adding one would only
   re-arm the two-line reflow the moment the wording grows. Leave it absent
   on purpose, not by oversight.

   THE BUDGET IS NOW SET BY THE INVALID MESSAGE, NOT THE EMPTY ONE. That
   swapped when "send the check" became "send it over" and took the empty
   string 21.4px below its neighbour. Anyone re-checking the fit should
   measure the invalid message, which is the long one at 320.4px. */
.form-note {
  display: flex;
  align-items: stretch;
  gap: var(--space-3);
  font-family: var(--font-serif);
  font-size: var(--text-small);
  line-height: var(--leading-body);
  color: var(--text-heading);
  margin: var(--space-2) 0 0;
  min-height: calc(var(--text-small) * var(--leading-body));
}
/* The attention gesture: a rule before the message, echoing the lead-in
   rule used elsewhere on the page. Transparent at rest so the reserved
   slot is invisible until there is something to say. Clay on the light
   tile; on the clay tile clay would vanish, so it takes Ivory Ground -
   the same color the text uses there, since at 13.5px Ivory Ground is
   the only one of the seven that clears 4.5:1 against clay. */
.form-note::before {
  content: "";
  flex: 0 0 var(--rule-strong-weight);
  background: transparent;
}
.form-note--shown::before { background: var(--rule-accent); }
.tile--clay .form-note { color: var(--clr-ivory-ground); }
.tile--clay .form-note--shown::before { background: var(--clr-ivory-ground); }

/* THE HANDLING NOTE, and it is the page's only privacy statement.
   DELIBERATELY NOT IN THE FOOTER. Two forms on this page handle an address
   differently - this one joins the mailing list, the discovery-call form
   will not - so a single footer line could not be true of both. It sits at
   the point of collection, where it is answerable for exactly one form.

   BELOW THE FIELD, NOT ABOVE IT. Above, it arrives before the ask and
   reads as a second pitch. Below, it answers the question the ask just
   raised. It is a note, so it takes the reading serif at --text-small and
   never the tracked caps the two lines above it use.

   THE SAME STRING IN BOTH BLOCKS, unlike the descriptions above them,
   which are deliberately different. Those are pitches and can differ; this
   is a fact about handling, and the handling is identical. If it ever
   stops being identical, that is a copy change, not a CSS one.

   "PUBLISH", NOT "WRITE", and it names the product in full rather than
   saying "the check" - see the naming rule at the MESSAGES object. What
   lands in an inbox is a published piece; "whatever we write next" quietly
   promises a drafting cadence that is not running yet, which is a claim
   the business would then owe. NO FREQUENCY CLAIM ANYWHERE IN THE LINE,
   deliberately - no weekly, no monthly, no "occasional". Every one of
   those is a promise the sender has to keep.

   NO COLOR OF ITS OWN, which is what lets one class serve both surfaces:
   .tile--clay sets Ivory Ground on its children and the outline tile
   inherits warm slate. Measured on the render, 12 August 2026: 5.03:1 on
   clay and 7.59:1 on Ivory Deep, both clear of the 4.5:1 that small text
   owes. Give this rule a color and one of the two breaks. */
/* .cta-fine SHARES THIS BLOCK RATHER THAN COPYING IT. Same role - fine
   print under a control - but it sits under the ask tile's button, where
   there is no form and no capture, so borrowing the capture__ name would
   have been a naming lie for the sake of one fewer selector. Two names,
   one declaration, and they are meant to move together: if the fine print
   treatment changes, it changes in both places or it is inconsistent. */
.capture__fine, .cta-fine {
  font-family: var(--font-serif); font-size: var(--text-small);
  line-height: var(--leading-body);
  margin: var(--space-3) 0 0;
}

/* The field states its own error too, not just the message beside it -
   color alone is never the only signal. Border color changes, not
   width, so nothing reflows when the message appears. */
.capture input[type=email][aria-invalid="true"] { border-color: var(--clr-brown-black); }

/* ---------- the discovery-call enquiry ----------
   A DIFFERENT FORM TO A DIFFERENT PLACE. It posts to Formspree, not to
   Kit, and it must never touch the mailing list: somebody asking to talk
   has not agreed to a newsletter, and an enquiry landing in a marketing
   dashboard is a lead read late.

   THE DECLARATIONS BELOW ARE COPIED FROM .capture, NOT INHERITED FROM
   IT. Borrowing that namespace would drag in a 520px cap built for a
   one-field row and a flex layout built for a field welded to a button.
   That mistake has been made on this page four times. What is shared is
   what should be shared - .form-note, .capture__swap and .capture__done
   are reused verbatim, so the error slot, the post-submit swap and the
   focus move behave identically to the capture. Same states, own layout.

   THE FIELD BORDERS ARE WHOLE. .capture's input drops its right border
   because it butts against the submit; these stand alone and need four
   sides. */
.enquiry { max-width: 640px; }

/* Two columns on the short fields, one on the long one. The pair that
   sits side by side is name and email - the two a person answers without
   thinking - and the two that take a moment get a full line each. */
.enquiry__grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--space-4); margin-top: var(--space-4);
}
.enquiry__field--wide { grid-column: 1 / -1; }

/* max-width:none IS LOAD-BEARING, and leaving it out shipped a visible
   defect. These rows are <p>, and every paragraph on this page carries
   the body measure cap - so the two full-width fields rendered at
   560.31px inside a 640px grid while the two half-width ones filled
   their 310px tracks exactly. A ragged right edge of 79.69px, which
   reads as three different field widths rather than as a measure.
   THE SAME TRAP AS .tally__line, .hero__lead AND --measure-body: `p` is
   a structural tag, not a semantic one, and a cap that means "prose
   should not run too wide" has no business on a form row. Fifth
   instance; it keeps arriving from a different direction. */
.enquiry__field {
  display: flex; flex-direction: column;
  gap: var(--space-2); min-width: 0; max-width: none;
}

/* VISIBLE LABELS, where the capture uses a screen-reader label and a
   placeholder. One field with a placeholder is legible; four are a
   memory test, because the placeholder disappears the moment anybody
   types. The label takes .cap's tracked micro-caps, which is the page's
   existing vocabulary for furniture, so this adds no new type. */
.enquiry__label {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--text-label); margin: 0;
}
.enquiry__optional { text-transform: none; letter-spacing: 0; font-family: var(--font-serif); }

.enquiry input {
  font-family: var(--font-serif); font-size: var(--text-body-size);
  color: var(--text-heading); background: var(--clr-ivory-ground);
  border: var(--rule-hairline) solid var(--clr-hairline);
  border-radius: var(--radius); padding: var(--space-4);
  min-width: 0; width: 100%;
  transition: border-color var(--transition-color);
}
.enquiry input::placeholder { color: var(--text-label); opacity: 1; }
.enquiry input:focus { border-color: var(--clr-brown-black); }

/* The ring sits INSIDE the box for the same reason the capture's does -
   the global focus ring is offset 3px and these fields sit in a grid
   gap of 20px, so an outside ring would collide with the neighbour. */
.enquiry input:focus-visible {
  outline: var(--focus-ring) solid var(--clr-brown-black);
  outline-offset: -2px;
}
.enquiry input[aria-invalid="true"] { border-color: var(--clr-brown-black); }

/* Full width at the point the two columns stop being readable. 560px is
   where the label plus a 20px gap stops leaving either column enough for
   an ordinary email address. */
@media (max-width: 560px) {
  .enquiry__grid { grid-template-columns: 1fr; }
}

.enquiry__go { margin-top: var(--space-4); }
/* Declared, not borrowed - same reasoning as the inputs. Ink on the
   Ivory Deep tile, hovering to clay, which is the pair the closing
   capture's submit already uses on this surface. */
.enquiry button {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; cursor: pointer; white-space: nowrap;
  line-height: var(--leading-snug);
  background: var(--surface-reverse); color: var(--text-on-reverse);
  border: var(--rule-hairline) solid var(--surface-reverse);
  border-radius: var(--radius); padding: var(--space-4) var(--space-6);
}
.enquiry button:hover, .enquiry button:active {
  background: var(--surface-field); border-color: var(--surface-field);
  color: var(--text-on-field);
}
.enquiry button:focus-visible {
  outline: var(--focus-ring) solid currentColor;
  outline-offset: -2px;
}
.capture button {
  font-family: var(--font-sans); font-weight: var(--weight-label);
  font-size: var(--text-label-size); letter-spacing: var(--track-label);
  text-transform: uppercase; cursor: pointer; white-space: nowrap;
  line-height: var(--leading-snug);   /* not the UA `normal` - see .btn */
  background: var(--surface-reverse); color: var(--text-on-reverse);
  border: var(--rule-hairline) solid var(--surface-reverse);
  border-radius: var(--radius); padding: var(--space-4);
}
/* THE FILL NOW MOVES, where before it deliberately did not. The old hover
   held the Brown Black and warmed the label to Hairline instead, because
   clay as a LABEL color on ink is 2.91:1 and fails. Clay as a FILL is a
   different move and passes: the label goes Ivory Ground on it at 5.03:1,
   which is also the only one of the seven that clears 4.5:1 on clay at
   this size. The warm-label treatment goes; a button that swaps surfaces
   is the same gesture every other button on this page makes. */
.capture button:hover, .capture button:active {
  background: var(--surface-field);
  border-color: var(--surface-field);
  color: var(--text-on-field);
}
/* THE HERO CAPTURE STANDS ON THE CLAY TILE, and this is the case the one
   rule would have got wrong twice over: a clay block would be invisible
   against the tile at rest, and a clay fill on hover would dissolve the
   button into it. Both flip to Ivory Ground, so the pair still trades two
   colors - just the other pair. The label goes Brown Black at 14.63:1.
   Same rule, checked against the surface rather than assumed. */
.tile--clay .capture button:hover, .tile--clay .capture button:active {
  background: var(--clr-ivory-ground);
  border-color: var(--clr-brown-black);
  color: var(--clr-brown-black);
}
.sr { position: absolute; left: -9999px; }

/* ---------- the close's ask tile ----------
   Ink surface with a clay button, so the last block before the footer has
   depth rather than being a third ivory panel. The qualifier leads at
   small size and the question answers it large - the reader is qualified
   first, then asked, and the size difference is what makes that an order
   rather than two equal lines. */
.tile h3.ask {
  font-size: var(--text-h1); line-height: var(--leading-tight);
  letter-spacing: var(--track-h1); margin: var(--space-3) 0 0;
}
.qualifier { font-size: var(--text-small); margin: 0; }
/* Clay on ink measures 2.91:1, which is under the 3:1 that WCAG 1.4.11
   wants for the edge of a control - the label inside is fine at 5.03:1,
   but the button's own shape has to be findable too. A Hairline border
   puts the edge at 9.47:1 without giving up the clay fill, and the ring is
   what makes the button sit ON the dark tile rather than in it. */
.tile--ink .btn { border-color: var(--clr-hairline); }


/* THREE GROUPS, NOT ONE ROW. This reverses the single-baseline-row note in
   the markup below, and that note was right when it was written: four items
   with one job read fine on one line. There are six items now doing three
   jobs - who we are, how to reach us, the legal pair - and a longer line
   cannot say which is which. Grouping can. Spatial only: no headings were
   added, because a heading is copy.

   STILL baseline-aligned, and the original reason survives the change. The
   groups are columns, so each contributes its FIRST line's baseline: the
   wordmark's bottom edge lands on the baseline of "contact@vellekor.com"
   and of the ownership sentence beside it. align-items:center would float
   a 22px image against 13.5px text and look a pixel wrong at every width. */
.site-foot__in {
  display: flex; align-items: baseline; gap: var(--space-5) var(--space-7);
  flex-wrap: wrap;
  border-top: var(--rule-strong-weight) solid var(--rule-strong);
  padding: var(--space-5) 0 var(--space-8);
}
/* The group is the unit now. space-2 inside, space-7 between - the inside
   gap has to be clearly smaller than the outside one or the grouping does
   no work, and 8px against 56px is not a judgement call at a glance. */
.foot-group { display: flex; flex-direction: column; gap: var(--space-2); }
.foot-group > * { margin: 0; }
/* 22px, which is the --text-h2 stop and roughly the cap-height of the
   13.5px tagline beside it once the wordmark's 0.79 ink crop is allowed
   for. Left as a literal rather than pointed at --text-h2: this is an
   image height that happens to coincide, and binding it to a type token
   would resize the mark whenever the heading scale moved. */
.foot-mark { height: 22px; width: auto; align-self: center; }
.foot-tag { margin: 0; font-size: var(--text-small); }
/* The pronunciation gloss. Quieter than the tagline above it by color
   rather than by size - both are --text-small, and a third size in a
   three-line stack would read as three ranks when there are two: the mark
   and its two glosses. --text-label is warm slate, 7.65:1 on the page. */
.foot-say {
  font-family: var(--font-serif); font-size: var(--text-small);
  line-height: var(--leading-body); color: var(--text-label);
  font-style: italic;
}
/* Pushes the legal group right. margin-left:auto rather than
   space-between, which would spread all three gaps evenly and pull the
   reach-us group away from the identity block it sits nearest. */
.foot-group--legal { margin-left: auto; }
.foot-legal__line {
  font-family: var(--font-serif); font-size: var(--text-small);
  line-height: var(--leading-body); color: var(--text-label);
}
.foot-copy { white-space: nowrap; }

/* ---------- mobile collapse, declared explicitly ---------- */
@media (max-width: 900px) {
  .c12, .c8, .c7, .c6, .c5, .c4, .c3 { grid-column: span 12; }
  /* One rung below 900px: every tile is full width, so the four-step
     ladder has nothing to distinguish. 64px is --tell-col-4, the ladder's
     narrowest column, paired with --tell-num-mobile above. */
  .tell { grid-template-columns: var(--tell-col-4) minmax(0,1fr); gap: var(--space-4); }
  .tell__n { font-size: var(--tell-num-mobile); }
  /* 30px on small screens: still well clear of --wordmark-min-mobile-header
     (19px), and the lockup scales with it because both halves read --wm-h. */
  .site-head__brand > a:first-child { --wm-h: var(--wm-h-mobile); }
  /* The header cannot fit its nav on one line here as it is; a tagline
     beside the lockup makes that worse. It belongs to the wide layout. */
  .head-tag { display: none; }


  /* The staircase flattens. The inset is what makes the sequence read as
     advancing, but at 375px it would eat 72px of a 263px column, so the
     third step would have a third less room than the first for no gain. */
  .step--2, .step--3 { margin-left: 0; }
  .step { grid-template-columns: 1fr; gap: var(--space-3); }
  .step__n { padding-top: 0; }

  /* Five columns at 375px is 40px each. The chart becomes a vertical
     spine: the rule moves to the left edge and the drops become ticks
     running off it, which is the same diagram rotated rather than a
     different one. */
  /* One column, so there is no span between outer nodes to inset - the
     spine becomes the left edge and the node-center pseudo is switched
     off rather than left drawing a stray horizontal. */
  .charter {
    grid-template-columns: 1fr; gap: var(--space-4);
    /* The spine becomes this border at one column, so it takes --connector
       like every other rule in the diagram - otherwise the one connector
       that matters most on a phone is the only one that never lights. */
    border-left: var(--rule-hairline) solid var(--connector);
    padding-left: var(--space-4);
    transition: border-color var(--transition-color);
  }
  .charter::before { display: none; }
  /* Stacked, the chart is one column, so centring would run five boxes
     down the middle of an otherwise left-aligned page. The root moves
     left, the spine becomes the left edge, and each numeral box sits
     beside its sentence with the branch running sideways into it. Same
     diagram, rotated a quarter turn. */
  .charter-head { text-align: left; }
  .charter-root::after { left: var(--space-5); }
  .charter li {
    text-align: left; padding-top: 0;
    display: grid; grid-template-columns: auto minmax(0,1fr);
    gap: var(--space-4); align-items: start;
  }
  .charter li::before { margin: 0; }
  /* THE BRANCH MUST MEET THE NUMERAL NODE ON ITS CENTRE LINE, and 0.9em
     did not. Measured at 375: the stub landed at y=15.3 against a box
     centre of 17.91 - 2.61px high on every one of the five rows, which
     on a 1px hairline meeting a bordered box reads as a mistake rather
     than as a diagram.
     0.9em was inherited from the vertical drop, where it was measuring
     down from the li's own text and had nothing to do with the numeral.
     Rotated a quarter turn, the thing it has to align to changed.
     DERIVED, NOT MEASURED IN. The node is border + padding + one line of
     the label face: 1 + 8 + (11 x 1.62)/2 = 17.91. Written from the
     tokens so it stays true if the label size or the leading moves - a
     hardcoded 17.91px would be right today and silently wrong after any
     type change. */
  .charter li::after {
    top: calc(var(--rule-hairline) + var(--space-2)
              + (var(--text-label-size) * var(--leading-body) / 2));
    left: calc(-1 * var(--space-4));
    width: var(--space-4); height: var(--rule-hairline);
  }

  /* THE HEADER STOPS STICKING HERE, and the measurement is why. The four
     nav labels sum to 346px of text before any gap, against 263px of
     content width at 375px - so the nav cannot be made to fit one line,
     and the header lands at 163px, a permanent 20% of an 812px screen.

     Every way out was worse than letting it scroll: dropping the wordmark
     for the mark alone hides the identity on the one device where it is
     the only branding on screen; scrolling the nav sideways hides
     destinations behind a gesture nobody is told about; and taking the
     labels below 11px breaks the type floor. Shortening the labels would
     work, but they are frozen copy.

     --head-h goes to 0 with it. It is only ever read by scroll-margin-top,
     and an anchor that reserves 72px for a header that is no longer
     hovering just drops the reader short of their target. */
  :root { --head-h: 0px; }
  .site-head { position: static; border-bottom-color: transparent; }
  /* The footer stops being two columns here, so the legal block is no
     longer sitting against the right edge - right-aligned text under a
     left-aligned wordmark just reads as a mistake. */
  /* The footer wraps here, so the auto margin that pushed the legal group
     right would strand it against the edge under a left-aligned mark. Once
     wrapped there is no right column to push to.
     WAS on .foot-legal__line, which is now inside a group and no longer
     the thing being pushed - the group is. */
  .foot-group--legal { margin-left: 0; }
  /* Baseline alignment is for items sitting SIDE BY SIDE. Once the groups
     stack it aligns nothing and only risks the 22px mark dropping onto a
     shared baseline with the group above it, so the groups go back to
     ordinary block flow and each keeps its own internal spacing. */
  .site-foot__in { align-items: flex-start; }
  .form-note { min-height: calc(var(--text-small) * var(--leading-body) * 2); }
  /* THE OVERHANG IS CAPPED AT 150% HERE, DOWN FROM 190%. Jeff's call.
     190% left only 53% of the word in the window at any one moment - at a
     375px viewport that is 335px of band showing 176px of a 636px wordmark,
     which is about three and a half letterforms out of eight. The band stops
     reading as a word being panned and starts reading as a texture of
     fragments, and the letter at each edge is a sliver rather than a shape.
     150% shows 67%, which is five and a half letterforms and keeps whole
     letters at both ends of the travel.
     THE PAN GETS LONGER, NOT SHORTER, which is the part that looks wrong in
     the number and is not: travel is (imgW - bandW) / imgW, so 128% pans
     21.9% of the image, 150% pans 33.3% and 190% pans 47.4%. Capping the
     width shortens the distance in pixels while lengthening it as a share of
     the image - and it is the share that decides how much word goes past. */
  .ghost-mark { --wm-scrub: 150%; }
  section { padding: var(--space-7) 0; }
}
/* 560px - the third and last of the published breakpoints (1100, 900, 560).
   No fourth exists anywhere in this file. */
@media (max-width: 560px) {
  .wrap { padding: 0 var(--space-5); }
  .skip:focus { left: var(--space-5); }
  .lead-in { padding-left: var(--space-3); }
  /* THE TALLY STACKS, and the font clamp alone could not have saved it. The
     sentence is a flex item, so it will not shrink below its own longest word
     however small the count gets - measured at a 386px viewport, 161px of
     sentence that refuses to narrow plus the word ran 39px past the tile's
     edge even with the word down to its 40px floor. Side by side stopped
     fitting; the fix is to stop asking it to.
     The count goes left with the sentence rather than staying right: stacked,
     a right-aligned number under a left-aligned line reads as two things that
     failed to line up rather than as one answering the other. */
  .tally { flex-direction: column; align-items: flex-start; gap: var(--space-5); }
  .tally__count { justify-items: start; }
  /* Below roughly 515px the tile is too narrow for the message to stay on
     one line - measured 247px of text width at 375px against the ~331px it
     needs. So the reserved slot holds two lines here instead of one,
     otherwise the tile still jumps by 22px on error, which is the exact
     problem the reservation exists to prevent. */
}
/* The wordmark's @media (hover: none) guard was removed with the hover
   itself - a scroll-driven reveal needs no pointer, so touch gets the same
   behavior as everything else. Its reduced-motion rule is gone too: the
   animation is now inside a `prefers-reduced-motion: no-preference` query,
   so it is never declared for readers who opted out, and the base rule
   already shows the mark whole. */

/* ---------- THE ARRIVAL ----------
   THE FIRST VIEWPORT RISES ONCE, TOGETHER. Everything named below shares
   one duration, one curve and no delay. What it says is simply "the page
   has arrived" - it no longer tries to say anything about reading order,
   because finding 02 removed the stagger that was carrying that and the
   standard does not allow it back.

   Bands of scroll hijack, pinning and GSAP were available when this was
   built and are wrong for this page: it is one screen of type, and a
   scroll-jacked hero on a services site reads as a portfolio showing off.

   TRANSFORM AND OPACITY ONLY, no top/left/width/height, so every frame is
   composited.

   14px, NOT 40px. The distance is the whole difference between an entrance
   and a slideshow. This identity is hairlines and square corners; things
   settle into place, they do not fly in.

   THE HEADER FADES BUT DOES NOT MOVE. It is the page's frame - a frame that
   slides in makes the whole thing feel unmoored - and the sticky observer
   is watching a sentinel behind it, which a transformed ancestor would put
   on a different plane.

   REDUCED MOTION: the entire block is inside `no-preference`, so it is
   never declared for readers who opted out. That is deliberate rather than
   tidy - written the other way round, with opacity:0 in the base rule and
   an override to switch it back, anyone whose browser did not run the
   animation would get a blank hero. Nothing here can fail to visible. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes vk-rise { from { opacity: 0; transform: translateY(14px); } }
  @keyframes vk-fade { from { opacity: 0; } }

  /* NO STAGGER - finding 02, and this deleted a considered piece of work,
     so what it was doing is recorded here rather than lost.
     There were seven delays, 0 to 360ms, and they encoded a reading order:
     hook, then the sentence explaining it, then the primary CTA, then the
     capture last because it is the secondary conversion. The image landed
     with the headline because they are one composition.
     THE STANDARD IS UNAMBIGUOUS: elements arrive together or not at all,
     and a cascade of delays reads as a template rather than as a decision.
     The reading order is now carried by what it should always have been
     carried by - position, size and weight on the page. If the order stops
     being legible without the stagger, that is a layout problem and the
     fix belongs in the layout.
     THE DURATION AND THE CURVE ARE TOKENS NOW (finding 36): 560ms and the
     one easing curve were written out by hand in two places. */
  .site-head__brand,
  .site-nav,
  .hero h1,
  .hero__lead,
  .hero .btn,
  .hero .tile--capture,
  .hero .tile--flush,
  .hero-media {
    animation: vk-rise var(--motion-arrive) var(--ease-arrive) both;
  }
  /* The frame fades in place rather than rising. A frame that slides makes
     the whole page feel unmoored, and the sticky sentinel sits behind the
     header where a transformed ancestor would put it on a different plane. */
  .site-head__brand,
  .site-nav { animation-name: vk-fade; }
}
/* A READER WITH REDUCED MOTION GETS THE SAME HERO AS EVERYONE ELSE, and that
   is now true by construction rather than by a rule. There is one image and
   it is never animated except by the entrance above, which lives inside
   `no-preference` and is therefore simply not declared for them.
   THIS USED TO NEED THREE RULES AND A DECISION. The sequence's backdrop was
   an opaque panel above the <img>, so with no animation to lift it that
   reader was served the drawn stage with the photograph hidden behind it -
   while still downloading the image to look at the back of it. Fixing that
   meant choosing which stage they should be left on. With one image there is
   no stage to choose and nothing to uncover.

   ENTRANCE MOTION IS STILL DECLARED INSIDE `no-preference` rather than as
   `opacity: 0` in the base rule with an override to restore it. The second
   form serves a blank hero to any engine that does not run the animation. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; }

  /* THE FRACTURE'S HANDOVER RUNS ON transition-delay, WHICH THE LINE ABOVE
     DOES NOT REACH - it overrides duration only. Left alone, the halves
     would snap apart instantly and then hang in register for 180ms after the
     pointer left before handing back to the real word: a stall, and exactly
     the kind of thing the reader who set this preference is avoiding. */
  .fracture, .fracture::before, .fracture::after { transition-delay: 0s !important; }
}

/* ============================================================
   THE MOBILE BAR - a condensed sticky, below 900px only

   WHAT IT SOLVES, measured at 375: the four nav labels are 345.75px of
   text plus three 28px gaps = 429.75px, against 319px of content width.
   "Book a call" therefore wraps to a second row and the header stands at
   162.63px - 20.03% of an 812px screen - which is why sticky was turned
   off below 900px in the first place. The wrap was the cause; the header
   scrolling away was the consequence.

   WHAT IT DOES INSTEAD. The header is left exactly as it is: full
   lockup, four labels, two rows, still not sticky, nothing about the
   resting page changes. Once the reader scrolls past it, a separate
   compact bar arrives carrying the mark and the one link that matters.
   Measured, that content is 124.52px wide and the bar stands at about a
   quarter of today's header.

   IT IS A SEPARATE ELEMENT, NOT THE HEADER RESTYLED, and that is the
   load-bearing decision. Switching .site-head from static to fixed on
   scroll would pull 162.63px out of the document flow, and everything
   below would jump up by exactly that much at the moment it engaged.
   A second element that is never in flow cannot move anything.

   NO NEW JAVASCRIPT. It keys off .site-head--stuck, which the sentinel
   observer already toggles for the desktop hairline, through a sibling
   selector - so the bar must stay a LATER SIBLING of the header in the
   markup. If it is ever moved inside a wrapper, this stops working
   silently and the bar simply never appears.

   VISIBILITY, NOT DISPLAY, for the hidden state - the same reason the
   capture swap uses it. display:none would drop the link out of the
   document and back in; visibility:hidden keeps the box, and still
   takes the link out of the tab order and the accessibility tree, so a
   keyboard reader at the top of the page cannot tab into a bar that is
   not on screen.
   ============================================================ */
.mobile-bar { display: none; }

@media (max-width: 900px) {
  .mobile-bar {
    display: block;
    position: fixed; top: 0; left: 0; right: 0;
    /* Above the page, below the skip link's 10 - a keyboard reader
       tabbing in must never find this in front of "Skip to content". */
    z-index: 9;
    /* NO HORIZONTAL PADDING OF ITS OWN. It carries an inner .wrap, the
       same as .site-head does, so its contents land on the page's
       gutters whatever they are. Writing var(--gutter-page) here looked
       right and was wrong: that token computes to 56px, while .wrap is
       overridden to 28px below 900px, so the bar would have sat inset
       28px from every other thing on the page. */
    padding: var(--space-2) 0;
    background: var(--surface-page);
    border-bottom: var(--rule-hairline) solid var(--clr-hairline);
    visibility: hidden;
    transform: translateY(-100%);
    /* Visibility flips instantly on the way in and waits for the slide
       on the way out, so the bar is never invisible while still moving. */
    transition: transform var(--motion-quick) var(--ease-arrive),
                visibility 0s linear var(--motion-quick);
  }
  .site-head--stuck ~ .mobile-bar {
    visibility: visible;
    transform: translateY(0);
    transition: transform var(--motion-quick) var(--ease-arrive),
                visibility 0s;
  }

  .mobile-bar__in {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-4);
  }
  .mobile-bar__home { display: flex; align-items: center; line-height: 0; }
  .mobile-bar__home img { height: 30px; width: auto; display: block; }

  /* NOT .btn. That carries 20px/28px padding and stands 54.97px tall,
     which in a bar this size is the bar. Declared instead at the same
     type as .btn - 11px tracked caps - on a shorter box.
     44px minimum tap target, which is what sets the padding rather than
     any visual preference. */
  .mobile-bar__cta {
    font-family: var(--font-sans); font-weight: var(--weight-label);
    font-size: var(--text-label-size); letter-spacing: var(--track-label);
    text-transform: uppercase; text-decoration: none; white-space: nowrap;
    line-height: var(--leading-snug);
    min-height: 44px; display: inline-flex; align-items: center;
    padding: 0 var(--space-4);
    background: var(--surface-reverse); color: var(--text-on-reverse);
    border: var(--rule-hairline) solid var(--surface-reverse);
    border-radius: var(--radius);
    transition: background-color var(--transition-color),
                border-color var(--transition-color),
                color var(--transition-color);
  }
  .mobile-bar__cta:hover, .mobile-bar__cta:active {
    background: var(--surface-field); border-color: var(--surface-field);
    color: var(--text-on-field);
  }
  .mobile-bar__cta:focus-visible {
    outline: var(--focus-ring) solid currentColor; outline-offset: -2px;
  }
  .mobile-bar__home:focus-visible {
    outline: var(--focus-ring) solid var(--clr-brown-black); outline-offset: 2px;
  }

  /* --head-h WAS 0 HERE, and it has to stop being 0 now that something
     hovers again. It is read only by scroll-margin-top, so at 0 an
     anchor target lands underneath the bar - the reader taps "The
     tells" and arrives at a heading with a bar sitting on it.
     61px is the bar's MEASURED height, not its arithmetic: 44px of tap
     target plus 8px of padding top and bottom is 60, and the hairline
     border-bottom is the 61st. Written as 60 first, which left an anchor
     target sitting exactly under the rule.
     It over-reserves only at the very top of the page, where the bar is
     not showing - and from there every anchor scrolls DOWN, which brings
     the bar in before the reader lands. */
  :root { --head-h: 61px; }
}
