/* GENERATED from components/ by pipeline/build/build-styles.py.
   DO NOT EDIT. Edit the component's own file and re-run the build; the gate fails if these
   disagree. Order comes from styles/_order.json, because cascade order is behaviour and a
   directory listing has none. */


/* ---- foundation/base ---- */
/* TWO BREAKPOINTS, AND ONLY TWO. 1100px is where the twelve-column register stops holding and a
   three-up becomes a stack; 760px is where a row of anything becomes a column. They are written
   as literals because a media query cannot read a custom property: `@media (max-width:var(--x))`
   is not valid CSS, so the value has to appear in the sheet. That makes them the one class of
   number the token source cannot own, and the only defence is that there are two of them.
   There were four. 700px and 720px were drift, one component each, sitting either side of 760
   and stacking at a width nobody had chosen. */
/* AKA prototype shell. Extracted verbatim from home-register.html (AKA-54), which stays the
   reference implementation. Tokens, type ladder, chrome and footer live here so every route page
   carries one register; page-specific sections stay in the page.
   Tokens are NOT here: assets/tokens.css is the universal set and the only definition site, and
   every route links it ahead of this file. This sheet reads those names and never declares one. */
*{box-sizing:border-box;margin:0}
/* AKA-103. `document.documentElement.scrollWidth` measured 105px wider than the viewport on
   several routes with nothing painting in the gap: every element in the flagged subtree bounds
   to the viewport's own width, and the only geometry that ever exceeds it is the fixed, blurred,
   `contain:strict` aurora background, which browsers already exclude from paint outside the
   viewport. The scrollable range existed with nothing to scroll to - a reader could drag the
   page 105px right into dead ground.

   `overflow-x:hidden` was the first attempt and it broke the sticky nav: `overflow` in EITHER
   axis on an ancestor takes a `position:sticky` descendant out of its scrolling context in
   Chrome, not only the axis it names, so the bar scrolled off with the page instead of holding
   at the top - measured live, `top:-600` at `scrollY:600` where a working sticky bar reads `0`.
   `overflow-x:clip` is the fix: newer than `hidden`, written for exactly this, it stops paint
   and scroll range in one axis without establishing the scroll container that sticky positioning
   requires to remain unbroken. Verified after: scrollWidth back to the viewport's width, and the
   nav's `top` reads `0` at `scrollY:600`, both on the same route. */
html{scroll-behavior:auto;overflow-x:clip}
body{overflow-x:clip;background:var(--colour-surface-page);color:var(--colour-text-primary);font:var(--type-body-weight) var(--type-body-size)/var(--type-body-leading) var(--type-family-text);-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
/* dev only: ?grid=1 lays the 12 columns over the page. The band is fluid like the grid it checks:
   one period is (content + gutter)/12, which is the live 120 at 1440, and the column is that period
   less the gutter, so the overlay still reads 90/30 at 1440 and stays true at every width below it. */
body.showgrid::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:98;
 width:min(var(--grid-content-width),calc(100% - var(--grid-page-inset)));margin:0 auto;
 --band:calc((100% + var(--space-gutter))/12);
 background:repeating-linear-gradient(to right,rgba(255,0,128,.14) 0 calc(var(--band) - var(--space-gutter)),
  transparent calc(var(--band) - var(--space-gutter)) var(--band))}
/* One family, three voices. Display is weight 800 on the narrow width axis, which is the cut that
   replaces a second display face. Text is 300 to 500 at width 100. */
/* THE HEADINGS ARE NOT STYLED BY TAG. The bare h1 to h6 rules that lived here - here and at the
   foot of this file - are the headline atom's level defaults now, in headline.form.css, matching
   only a headline that asks for no grade. Styling the tag made the element a design decision:
   every grade drawn on an h1 turned uppercase, and a 13px grade on an h1 took its -3px tracking. */
/* `p{color}` moved to component/text.skin.css: the atom is the seam, not the element */
/* one micro-label everywhere: mono, 12px, uppercase, dim. No brackets, no second size */
/* CTA. Rounded to the same 10px the panels and fields use, so the page has one corner and not a
   pill beside a card. Carries the motion page's own UI-FEEDBACK pattern: a 120ms depress on
   :active, which is the timing their Press me control uses, and the label's icon runs on hover. */
/* THE ATOM, NOT THE CLASS. Every one of these was keyed on `.btn`, and 20 of the site's 42 buttons
   do not carry that class: the contact form's submit and the subscribe form's, injected by
   shell.js and marked `data-component="button"` without it. They were getting their look from
   whichever pattern held them - `[data-component="formPanel"] .send`, `[data-component=
   "inlineForm"] button` - so a marked button outside a form panel had NO button appearance at
   all: the inventory drew two of them in the browser's own grey with the icon stacked under the
   label. The mark is the seam; the class is an accident of which markup happened to carry it.
   
   0 elements carry `.btn` without the mark, measured across ten routes, so nothing loses a rule
   here - 20 elements gain the one they should always have had. */
/* THREE STATES, NAMED ON THE ATOM. They were classes - `.btn`, `.btn.o`, `.t` - which is a
   nickname for a state rather than the state itself: an editor picking emphasis was picking a
   letter, and two of the three were invisible to the schema. `data-form` is where anatomy the
   frontend chooses lives (`data-variant` is reserved for the four editorial intents), so the
   grade reads the same in the document, the markup and this sheet.

     primary    white solid. The page's one action.
     secondary  white outline. The alternative standing beside it.
     tertiary   text only. What a card carries, and anything that is an offer rather than a push.

   AN ICON IS OPTIONAL AND HAS A SIDE. `data-icon="right"` is the default reading - the mark
   follows the label the way the action follows the sentence - and `left` puts it first, for a back
   or a download where the mark is the subject. Absent means no mark, which is most buttons.

   Every value here is the register's. The shared floor is the atom's own, and the three states
   change only what distinguishes them: ground, ink and border. */
/* THE BOX. 22px of horizontal padding was the last raw literal in this atom, and it was not even
   on the 8px unit: it sat between two steps, so a button was off the register on one axis while
   being on it on the other. With 24px of vertical padding on 16px type the control stood 75px
   tall, half again the height of the same atom in the nav, which is how one component came to
   have three sizes. The step below on the vertical and the step above on the horizontal gives a
   57px control, which is the size the nav was already drawing, so the site converges on one
   button rather than gaining a fourth.

   THE CORNER. The house draws square, and a control is the exception: it is the one element a
   reader is meant to read as pressable rather than as part of the page. 2px is the softest
   corner that still registers, and it is a token rather than a value so the exception is
   declared once and cannot spread to a card by somebody typing it again. */
/* the mark sits 8px from its label, not 16: at 16 the icon read as a second thing beside the
   word rather than part of it (owner, 2026-09-23) */
[data-component="button"]{display:inline-flex;align-items:center;gap:var(--space-step-1);
 /* A CONTROL HAS A HEIGHT, NOT A PADDING THAT ADDS UP TO ONE. The height was whatever 16px of
    inset plus a 24.8px leading plus two hairlines came to - 59px, a number nobody chose and one
    off the 8px unit the rest of the vertical rhythm sits on. `--size-control` is 50 and
    `--size-control-small` is 40; the label centres in the box rather than propping it open, so a
    change of leading no longer changes the size of every control on the site. */
 /* OPTICALLY CENTRED, NOT MATHEMATICALLY. A line box is the leading, and the descender space
    under a label with no descenders in it is empty - so a label centred on the box's true middle
    reads 2px low, which is what the eye sees before it can say why. 4px of padding under it
    moves the content box's centre up by half that. */
 min-height:var(--size-control);padding:0 var(--button-inset-inline) var(--button-optical-lift);
 align-items:center;
 /* THE WEIGHT IS IN THE SHORTHAND. `--type-button-font` opens with --type-button-weight, which
    is medium, and this said 500 again immediately after it - the same decision written twice,
    once as a name and once as a number, so a change to the role would have moved every button
    on the site except this declaration. */
 font:var(--type-button-font);
 border-radius:var(--radius-control);
 /* AND THE TRACKING IS THE ROLE'S. `.01em` was the one value on this element the register did
    not hold - and tracking is a property of a grade, not of a component: every other role says
    so in the register, from the label's 0.3 to the display's -1.5. `--type-button-tracking` is
    that value under its own name, 0.16px, which is what .01em resolved to at the button size. */
 letter-spacing:var(--type-button-tracking);cursor:pointer;white-space:nowrap;
 /* the atom sets its own case: in the nav it inherited the menu's capitals (tertiary sets them back) */
 text-transform:none;
 /* NO UA CHROME. A marked `<button>` with no pattern around it kept Chrome's own
    `2px outset ButtonBorder`, which is why the two form submits drew a bevelled grey outline on
    the inventory sheet while the nav's anchor drew none: an `<a>` has no such default and a
    `<button>` does, so the same atom arrived in two shapes depending on its tag. The 3px border
    the submits carry on a route is the CONTRACT's - section 5.1, the only 3px rules on the site -
    and it belongs to the form pattern that puts them there, which outranks this floor. */
 border:0}
/* TWO SIZES AND NO MORE. The default stands at 57px, which is the height the nav was already
   drawing, so a CTA and the nav control are one size rather than two that nearly match. `small`
   is one step down on BOTH axes, 41px, for a control inside something else: a card's action, a
   form's submit, a row's link. A third size would have to justify itself against these two. */
/* AND THE SMALL CONTROL LIFTS ITS LABEL TOO. This wrote the whole padding shorthand, which set
   the bottom back to zero and undid the optical lift above - so a 50px button read centred and
   a 40px one read low, which is the sort of difference a reader feels and cannot name. */
[data-component="button"][data-size="small"]{min-height:var(--size-control-small);
 padding:0 var(--button-small-inline) var(--button-optical-lift)}

/* THE GRADES ARE THE SAME HEIGHT. A secondary carries a hairline and a primary does not, so the
   two stood 2px apart: invisible at 75px and plainly wrong at 41, where they sit side by side in
   a pair. The primary takes a transparent border of the same width rather than the secondary
   losing its edge, because the edge is what the grade IS. */
[data-component="button"]:not([data-form="tertiary"]){
 border:var(--border-width-hairline) solid transparent}
/* THE SOLID GRADE'S EDGE IS ITS OWN GROUND, not nothing. A TRANSPARENT border still draws: the
   outer corner rounds at 4px and the inner at 3, so a white button on a dark page read as two
   nested shapes with a seam between them. The border is kept, because it is what holds the two
   grades at the same height, and it is painted the ground's colour so there is one shape. */
[data-component="button"][data-form="primary"]{border-color:var(--colour-action-surface)}

[data-component="button"] .icon{transition:transform var(--motion-exchange-mark) var(--motion-ease-settle)}
/* A MARK TRAVELS ONLY IF IT POINTS. Four pixels on hover is the arrow saying "this takes you
   somewhere", and it was written for every mark a button can carry: a phone slid sideways, and
   so did the copy mark on 172 specimen cards, where nothing travels at all because copying text
   moves nothing. `data-mark` names what the mark IS, so the motion can follow the meaning. The
   buttons that carry no `data-mark` keep the old reading, which is the arrow. */
[data-component="button"][data-icon="right"]:hover .icon,[data-component="button"][data-icon="both"]:hover .icon{transform:translateX(var(--motion-travel-mark))}
[data-component="button"][data-mark="copy-simple"]:hover .icon{transform:none}
[data-component="button"]:active{transform:scale(var(--motion-press-scale))}
/* THE MARK ON THE LEFT LEADS, so the travel agrees with it: a back control moves the way it
   points. Same rule the hero's download CTA needed and got as a one-off selector. */
/* NO ROW-REVERSE. `data-icon` says which side the mark is ON, and the markup puts it there - a
   leading mark is written before the label, a trailing one after it. Reversing the row instead
   made the attribute disagree with the DOM: the contact button writes its phone first, so it
   read `right`, took the trim on its trailing edge, and the imbalance moved to the side the mark
   was actually on. An attribute that describes the markup can be trusted by both. */
[data-component="button"][data-icon="left"]:hover .icon{transform:translateX(calc(-1 * var(--motion-travel-mark)))}
/* A PHONE RINGS, it does not travel (owner, 2026-09-23). It slid left with every leading mark,
   which read as "back". On hover it shakes about its own centre, three quick swings then a rest,
   for as long as the pointer stays: the handset ringing. Same swing angle as the wave. */
[data-component="button"][data-mark="phone"]:hover .icon:first-child{transform:none;
 animation:mark-ring var(--motion-duration-short) var(--motion-ease-settle) infinite}
@keyframes mark-ring{
 0%,48%,100%{transform:rotate(var(--angle-0))}
 8%,24%,40%{transform:rotate(calc(-1 * var(--angle-mark-wave)))}
 16%,32%{transform:rotate(var(--angle-mark-wave))}
}
@media (prefers-reduced-motion:reduce){[data-component="button"][data-mark="phone"]:hover .icon:first-child{animation:none}}
/* BOTH EDGES TAKE THE SAME INSET, mark or no mark (owner, 2026-09-23). The mark side used to drop
   to 8px, a trim tuned against a 16px label-to-mark gap; with the gap at 8 it left the mark 10px
   from its edge against the type's 24 and the button read lopsided. A mark carries about 2px of
   its own air, so equal padding lands within that of balanced. With a mark on both sides, only the
   trailing one (the arrow, the consequence) travels; the leading one keeps its own meaning. */
[data-component="button"][data-icon="both"]:hover .icon:first-child{transform:none}
/* THE MARK SIDE TAKES 16, not 24 (owner, 2026-09-23): a mark is drawn with its own air, so at the
   full inset its edge read wider than the type's. Not 8: that read cramped against a gap of 8. */
[data-component="button"][data-icon="left"]{padding-left:var(--space-step-2)}
[data-component="button"][data-icon="right"]{padding-right:var(--space-step-2)}
[data-component="button"][data-icon="both"]{padding-inline:var(--space-step-2)}
[data-component="button"][data-size="small"][data-icon="left"]{padding-left:var(--space-step-1)}
[data-component="button"][data-size="small"][data-icon="right"]{padding-right:var(--space-step-1)}
[data-component="button"][data-size="small"][data-icon="both"]{padding-inline:var(--space-step-1)}

[data-component="button"][data-form="primary"]{background:var(--colour-action-surface);color:var(--colour-action-text)}
/* the outline CTA fills on hover rather than nudging, so the pair reads as one control in two
   states instead of two unrelated buttons */
/* --line2, NOT --line. A hairline at 12 per cent white is the weight a card's edge or a table
   rule wants: it separates things that are already in their own boxes. A control standing beside
   a solid white button is doing something else - it has to read as pressable on its own - and at
   12 per cent it read as a dark panel with type in it. The register already names the heavier
   hairline, "a hairline that must carry weight", which is exactly this case. Changed on the ATOM,
   so every secondary on the site moves with it: six today, and whatever an editor makes secondary
   tomorrow. */
[data-component="button"][data-form="secondary"]{background:none;color:var(--colour-text-primary);
 border:var(--border-width-hairline) solid var(--colour-border-strong)}
[data-component="button"][data-form="secondary"]:hover{background:var(--colour-text-primary);border-color:var(--colour-text-primary);
 color:var(--colour-surface-page)}
/* an arrow that points down runs down, not right: the travel has to agree with the mark. Only an
   in-page jump (href="#...") draws the down arrow; a route draws the right arrow and keeps the
   atom's rightward travel. Scoped to every hero secondary, it dropped "See the work"'s right arrow
   4px below its label on hover (owner, 2026-09-23). */
.hero [data-component="button"][data-form="secondary"][href^="#"]:hover .icon{transform:translateY(var(--motion-travel-mark))}

/* TEXT ONLY, and at label grade rather than body: it is a signpost, not a sentence. No ground,
   no border and no padding of its own, so it sits on the baseline of whatever holds it. */
[data-component="button"][data-form="tertiary"]{background:none;color:var(--colour-text-primary);
 padding:0 0 var(--button-optical-lift);
 gap:var(--space-step-3);font:var(--type-label-font);
 letter-spacing:var(--type-label-tracking);text-transform:uppercase}
/* AN ARROW TRAVELS; A COPY MARK DOES NOT. The six-pixel slide is the arrow saying "this
   takes you somewhere", and it is the wrong gesture on an offer that copies text: nothing
   moves, so nothing should look like it is about to. The button names its mark, so the
   motion follows the mark rather than the grade. */
[data-component="mediaCard"]:hover [data-component="button"][data-form="tertiary"][data-mark="arrow-right"] .icon{transform:translateX(var(--motion-travel-reach))}

/* Phosphor light at one size across every page. Sizing them in em made every icon a different
   size, so a 15px button carried a 15px mark against a 21px heading's 18px one. One 20px box. */
.icon{width:var(--size-mark-base);height:var(--size-mark-base);flex:none;display:block;fill:currentColor}
/* A MARK THAT SAYS `fill="none"` IS A KEYLINE and nothing may paint it. The rule above is CSS
   and the attribute is presentation, so CSS won: the wave became a white blob of a hand, the
   venn became a solid trefoil, and the menu and the long arrow lost their strokes the same way.
   Four of the set are drawn as strokes in currentColor and the attribute is how they say so. */
svg[fill="none"]{fill:none}
.rnd{overflow:hidden}
.head{display:flex;flex-wrap:wrap;row-gap:var(--space-step-2);justify-content:space-between;align-items:baseline;gap:var(--space-step-9);margin-bottom:var(--space-step-12)}
/* NOWRAP UNTIL NOW. A section head is a title plus one or two mono labels on the same baseline,
   and at 1440 they fit; on the two specimen sheets - fourteen and nine instances a page - the
   labels pushed the row to 531px inside a 390px page. `flex-wrap` is the fix everywhere `.head`
   is used, not only there: a title and a label dropping to their own line beats a page that
   scrolls sideways, and `row-gap` keeps the dropped line off the baseline above it. */
/* the live site's own logo.svg. Its artwork is already white, so it is drawn as an image rather
   than a mask: a file:// mask is treated as cross-origin and paints nothing. Width is left to the
   227:40 artboard so the mark never squashes. */
.mark{display:block;width:auto}
@keyframes up{
to{transform:none}
}
/* lenis, the smooth scroll the shell loads on every route. These three rules are its own
   contract and were sitting in home-register, which meant every other route ran lenis with
   no stylesheet: the wrapper sets its own height and stops native scrolling, and a locked
   page has to actually stop scrolling. */
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
.lenis.lenis-stopped{overflow:hidden}
/* the ratio scale is the media atom's: atoms/media.form.css (2026-09-24) */
/* mediaCard media types. THREE, and the attribute value is the content type's own name rather
   than a nickname for it: data-media="image", data-media="video", data-media="embedCode", or the
   attribute absent for none. The same name in the schema, the frontend prop and the markup,
   because a seam whose vocabulary differs from the model's is a lookup table somebody has to
   maintain.
   The plate holds one of the three and the card does not care which. An image is a picture. A
   video is a picture that moves, and it is muted, looping and inline or it is an autoplaying
   advert. An embedCode is running code, which is what the two specimen sheets are: 172 examples
   whose visual is a live component, which is why they sat outside the component set and grew
   their own card. /drop's product art is the same choice made a third time - its linter was 108
   rules of page CSS drawing a screenshot, and it is now an embedCode in a slot.
   A plated tone exists for exactly these: a moving or running plate needs its own ground and
   edge because the thing inside it is drawn on a stage, where a case study or an article sits on
   the page. The colours are var(--colour-surface-card) and var(--colour-border-default), so a sheet that repoints those tokens
   per theme restyles the card with it rather than fighting it. */
/* the scroll-driven examples animate against a named view timeline the card publishes, which is
   what the sheets called --card. An embed card publishes it wherever it sits. */
/* `[data-kind="embed"]{view-timeline-name:--card}` stood here. The timeline is published by
   the CARD, which is what the scroll demos scrub against, and the specimen sheet carries that
   rule itself as its host CSS. Renaming this one to the media atom would have published a
   second --card on the plate inside the card and given every consumer the wrong one. */
/* The plate an embedCode runs in, and the caption a plated card carries. Both lived in
   thinking-specimen.html as six [data-component="mediaCard"] rules, which is a page reaching
   inside a component: the page that exists to prove nothing is page specific was styling the
   card's stage and every part of its caption. The stage gives the demo its aspect and its own
   query container, and --stage is the ground the register already names, not the #161616 the
   page had hard-coded. The caption is the record's fields in the three roles a specimen uses,
   title then alias then description, so a card renders a record without the page saying how. */
[data-kind="embed"] .stage{position:relative;aspect-ratio:3/2;overflow:hidden;
 container-type:inline-size;background:var(--colour-surface-stage)}
[data-kind="embed"] .stage .fill{position:absolute;inset:0;width:100%;height:100%}
/* video shares the embed plate: the same ground, the same containment, the same empty state, and
   the aspect is the file's rather than the card's because a 16:9 clip letterboxed into 3:2 is a
   picture with two black bars designed into it. Muted, looping, inline and no controls is not a
   style choice: an autoplaying clip with sound is the thing every reader hates, and a clip that
   needs controls is not decoration, it belongs in a player. */
[data-media="video"] .stage{position:relative;overflow:hidden;container-type:inline-size;background:var(--colour-surface-stage)}
[data-media="video"] .stage video{display:block;width:100%;height:100%;object-fit:cover}
@media (prefers-reduced-motion:reduce){

 [data-media="video"] .stage video{display:none}

 [data-media="video"] .stage.has-poster{background-size:cover;background-position:center}
}
@keyframes contest{

 0%,100%{font-variation-settings:"wght" 800,"opsz" 144,"wdth" 92;letter-spacing:-.015em}

 50%{font-variation-settings:"wght" 800,"opsz" 6,"wdth" 92;letter-spacing:.02em}
}
/* flowModule: stages inside a named boundary, arrows between them, outputs closing it. The
   demo /drop drew this as .flow .frame with thirteen page rules; it is a module the model names,
   so the rules are the component's. */
/* the band the head sits in: one rhythm and one rule between sections, so a page is a stack of
   equal bands rather than eight paddings somebody tuned individually */
/* the frames are stacked and only the live one is opaque, so nothing reflows as the reel advances */
/* ---- the embed kind's primitives
   SCOPED TO THE KIND, not to a card type and not to the atom. These were `[data-media="embedCode"]`, which
   named a kind of CARD - and there is no such thing: a card is one component and what fills its
   plate is a variant of it. The scope is the plate itself now, so an embed brings its medium
   with it wherever the atom is drawn.
 ---- original note below ---- -----------------------------------------------------------------
   The medium's own vocabulary, not any one example's: .fill is the full-bleed layer inside a
   stage, used by 16 demos, .hint the in-stage caption used by 17, .box the generic moving block
   used by 8, .floor its ground line, and .lbl, .ratios, .seg-btns and .on the in-stage labels
   and controls. 24 class names, 109 rules, 13KB, and every one of them was trapped in the two
   specimen sheets, which is why an embed lifted out of a sheet rendered as an empty stage: the
   record carried the markup and the example's own rules, and the primitives stayed behind.
   They belong to the media type, so they live here. Every selector is scoped under
   [data-kind="embed"], because .on, .a, .b and .p are far too generic to be let loose on a
   global sheet and inside a stage they are unambiguous. The token defaults below are the two
   sheets' own, so an embed on an ordinary page has a ground and a grid without the sheet.
   -------------------------------------------------------------------------------------------- */
[data-kind="embed"] .seg button:focus-visible, [data-kind="embed"] .seg-btns button:focus-visible{outline:var(--size-2) solid var(--colour-text-specimen);outline-offset:calc(-1 * var(--size-neg-2))}
[data-kind="embed"] .bar a.on{color:var(--colour-text-specimen)}
[data-kind="embed"] .seg button.on{background:var(--colour-text-specimen);color:var(--colour-surface-page)}
[data-kind="embed"] [data-theme="cartoon"] .seg button.on{background:var(--colour-accent-base);color:var(--colour-surface-page)}
[data-kind="embed"] .stage{position:relative;aspect-ratio:3/2;overflow:hidden;container-type:inline-size; background:var(--colour-surface-stage); background-image:radial-gradient(circle at var(--size-1) var(--size-1), var(--colour-border-grid) var(--size-1), transparent 0); background-size:var(--size-20) var(--size-20); display:flex;align-items:center;justify-content:center}
[data-kind="embed"] .card.wide .stage{aspect-ratio:auto;height:clamp(var(--size-230),29.5vw,var(--size-421))}
[data-kind="embed"] .card.wide.board .stage{aspect-ratio:var(--ar,16/9);height:auto}
[data-kind="embed"] .stage .ratios{position:absolute;top:var(--size-9);right:var(--size-9);z-index:6;display:flex; overflow:hidden;background:var(--chip-background)}
[data-kind="embed"] .stage .ratios button{font:400 var(--size-10)/1 var(--type-family-mono);letter-spacing:.04em; border:0;background:transparent;color:var(--colour-text-secondary); padding:var(--space-step-1) var(--size-8);cursor:pointer;transition:all var(--motion-duration-150) ease;white-space:nowrap}
[data-kind="embed"] .stage .ratios button + button{border-left:var(--size-1) solid var(--colour-border-strong)}
[data-kind="embed"] .stage .ratios button:hover{color:var(--colour-text-specimen)}
[data-kind="embed"] .stage .ratios button.on{background:var(--colour-text-specimen);color:var(--colour-surface-page)}
[data-kind="embed"] [data-theme="cartoon"] .stage .ratios{border-color:var(--colour-text-specimen)}
[data-kind="embed"] [data-theme="cartoon"] .stage .ratios button{color:var(--colour-text-specimen)}
[data-kind="embed"] [data-theme="cartoon"] .stage .ratios button.on{background:var(--colour-accent-base);color:var(--colour-surface-page)}
[data-kind="embed"] .stage[data-word]{cursor:text}
[data-kind="embed"] .hint{position:absolute;top:var(--size-9);right:var(--size-9);z-index:6; font:400 var(--size-9)/1 var(--type-family-mono);letter-spacing:.14em;text-transform:uppercase; color:var(--colour-text-tertiary);padding:var(--space-step-1) var(--size-6); background:var(--chip-background)}
[data-kind="embed"] [data-theme="cartoon"] .hint{color:var(--colour-text-specimen);background:var(--colour-accent-2);border-color:var(--colour-text-specimen)}
[data-kind="embed"] .box{width:9cqw;height:9cqw;background:var(--colour-text-specimen);}
[data-kind="embed"] .dot{width:8cqw;height:8cqw;background:var(--colour-text-specimen);}
[data-kind="embed"] .floor{position:absolute;left:6cqw;right:6cqw;bottom:14cqw;height:var(--border-width-hairline);background:var(--colour-border-strong)}
[data-kind="embed"] svg.fill, [data-kind="embed"] canvas.fill{position:absolute;inset:0;width:100%;height:100%;display:block}
[data-kind="embed"] [data-theme="cartoon"] .box{background:var(--colour-accent-base)}
[data-kind="embed"] [data-theme="cartoon"] .dot{background:var(--colour-accent-3)}
[data-kind="embed"] .stage{content-visibility:auto;contain:layout paint style}
[data-kind="embed"] .card .stage{contain-intrinsic-size:auto var(--size-300)}
[data-kind="embed"] .card.wide .stage{contain-intrinsic-size:auto var(--size-421)}
[data-kind="embed"] .card.runway-card .stage, [data-kind="embed"] .card.board .stage{content-visibility:visible}
[data-kind="embed"] .card:has(canvas) .stage{content-visibility:visible}
/* AN EMBED OFF SCREEN STOPS ANIMATING, and the class goes on the plate. It read
   `[data-media="embedCode"] .card.idle .stage *` - a card INSIDE the element that named the
   media, which is not a shape this markup has ever had, so the rule matched nothing and every
   embed on both sheets animated off screen for good. embed.js marks the atom now. */
[data-kind="embed"].idle *{animation-play-state:paused !important}
[data-kind="embed"] .appeal i.a{left:3.6cqw}
[data-kind="embed"] .appeal i.b{right:3.6cqw}
[data-kind="embed"] .ez-row{position:absolute;left:4cqw;right:4cqw;display:flex;flex-direction:column;gap:clamp(var(--space-step-2),1.8cqw,var(--size-17));top:var(--position-centre);transform:translateY(var(--offset-centre))}
[data-kind="embed"] .ez{display:flex;align-items:center;gap:var(--space-step-3)}
[data-kind="embed"] .ez small{width:clamp(var(--size-76),10cqw,var(--size-112));flex:none;color:var(--colour-text-tertiary); font:400 clamp(var(--size-9-5),1.2cqw,var(--size-11-5))/1 var(--type-family-mono);letter-spacing:.06em;text-transform:uppercase}
[data-kind="embed"] .ez .track{position:relative;flex:1;min-width:0;height:var(--size-14);border-bottom:var(--size-1) solid var(--colour-border-default)}
[data-kind="embed"] .ez .p{position:absolute;top:0;left:0;width:var(--size-13);height:var(--size-13);background:var(--colour-text-specimen); --x:calc(90cqw - var(--lbl) - var(--size-26));--lbl:clamp(var(--size-76),10cqw,var(--size-112))}
[data-kind="embed"] [data-theme="cartoon"] .ez .p{background:var(--colour-accent-base)}
[data-kind="embed"] .ez .linear{animation:run var(--motion-duration-2s) linear infinite alternate}
[data-kind="embed"] .ez .eo{animation:run var(--motion-duration-2s) cubic-bezier(.16,1,.3,1) infinite alternate}
[data-kind="embed"] .ez .ei{animation:run var(--motion-duration-2s) cubic-bezier(.7,0,.84,0) infinite alternate}
[data-kind="embed"] .ez .eio{animation:run var(--motion-duration-2s) cubic-bezier(.65,0,.35,1) infinite alternate}
[data-kind="embed"] .ez .spring{animation:run var(--motion-duration-2s) var(--motion-curve-6) infinite alternate}
[data-kind="embed"] .ez .crit{animation:run var(--motion-duration-2s) var(--motion-curve-7) infinite alternate}
[data-kind="embed"] .ez .under{animation:run var(--motion-duration-2s) var(--motion-curve-8) infinite alternate}
[data-kind="embed"] .ez .over{animation:run var(--motion-duration-2s) var(--motion-curve-9) infinite alternate}
[data-kind="embed"] .dur-row .dot{width:6.5cqw;height:6.5cqw;animation:dur-pulse var(--motion-duration-1s) ease-in-out infinite}
[data-kind="embed"] .dur-row .a{animation-duration:var(--motion-duration-100)}
[data-kind="embed"] .dur-row .b{animation-duration:var(--motion-duration-300)}
[data-kind="embed"] .dur-row .c{animation-duration:var(--motion-duration-500)}
[data-kind="embed"] .diss.a{background:var(--colour-text-specimen);animation:diss-a var(--motion-duration-2s) ease-in-out infinite alternate}
[data-kind="embed"] .diss.b{border:var(--border-width-hairline) solid var(--colour-text-specimen);animation:diss-b var(--motion-duration-2s) ease-in-out infinite alternate}
[data-kind="embed"] [data-theme="cartoon"] .diss.a{background:var(--colour-accent-base)}
[data-kind="embed"] .smear.g1{animation-delay:calc(-1 * var(--motion-duration-500))}
[data-kind="embed"] .smear.g2{animation-delay:calc(-1 * var(--motion-duration-100));background:var(--colour-border-strong)}
[data-kind="embed"] .plate .glint.b{width:var(--ratio-9);opacity:.5;animation-delay:var(--motion-duration-130)}
[data-kind="embed"] .twi.g1{color:var(--colour-text-tertiary);animation-delay:calc(-1 * var(--motion-duration-400))}
[data-kind="embed"] .twi.g2{color:var(--colour-border-strong);animation-delay:calc(-1 * var(--motion-duration-800))}
[data-kind="embed"] [data-theme="cartoon"] .twi.g1{color:var(--colour-accent-3)}
[data-kind="embed"] [data-theme="cartoon"] .twi.g2{color:var(--colour-accent-2)}
[data-kind="embed"] .selective .eye.a{left:5cqw}
[data-kind="embed"] .selective .eye.b{left:11cqw}
[data-kind="embed"] .jank-lane.a{top:var(--ratio-30)}
[data-kind="embed"] .jank-lane.b{top:var(--ratio-66)}
[data-kind="embed"] .budget .meter{flex:1;height:4.4cqw;position:relative;overflow:hidden}
[data-kind="embed"] .budget .meter i{position:absolute;inset:0 auto 0 0;background:var(--colour-text-specimen)}
[data-kind="embed"] .budget .meter .b60{width:100%}
[data-kind="embed"] .budget .meter .b120{width:var(--ratio-50)}
[data-kind="embed"] [data-theme="cartoon"] .budget .meter i{background:var(--colour-accent-base)}
[data-kind="embed"] .stage:hover .ktswl-w::after{opacity:1}
[data-kind="embed"] .stage:hover .ktswl-key{color:var(--colour-text-secondary)}
[data-kind="embed"] .ktvie-rig .hint{background:var(--colour-surface-card)}
[data-kind="embed"] .sd .meter{width:100%;height:4cqw;overflow:hidden}
[data-kind="embed"] .sd .meter i{display:block;height:100%;background:var(--colour-text-specimen);transform-origin:left;transform:scaleX(.35)}
[data-kind="embed"] .sd .lbl{margin-bottom:var(--space-step-4);font:400 clamp(var(--size-8-5),1.8cqw,var(--size-11))/1 var(--type-family-mono);letter-spacing:.18em;color:var(--colour-text-tertiary);text-transform:uppercase}
[data-kind="embed"] [data-theme="cartoon"] .sd .meter i, [data-kind="embed"] [data-theme="cartoon"] .sd .rot{background:var(--colour-accent-base)}
[data-kind="embed"] .sc{position:relative;width:72cqw;display:flex;flex-direction:column;align-items:center;gap:3cqw}
[data-kind="embed"] .sc .lbl{margin-bottom:var(--space-step-4);font:400 clamp(var(--size-8-5),1.8cqw,var(--size-11))/1 var(--type-family-mono);letter-spacing:.16em;color:var(--colour-text-tertiary);text-transform:uppercase}
[data-kind="embed"] .blk{width:46cqw;height:15cqw;background:var(--colour-text-specimen);}
[data-kind="embed"] [data-theme="cartoon"] .blk{background:var(--colour-accent-base)}
[data-kind="embed"] .pxl.a{top:var(--ratio-58);width:60cqw;height:5cqw;background:var(--colour-text-tertiary)}
[data-kind="embed"] .pxl.b{top:var(--ratio-44);width:44cqw;height:5cqw;background:var(--colour-text-secondary)}
[data-kind="embed"] .pxl.c{top:var(--ratio-30);width:28cqw;height:5cqw;background:var(--colour-text-specimen)}
[data-kind="embed"] .card.wide.runway-card .pin .stage{height:auto;width:100%;aspect-ratio:16/9; max-height:calc(100dvh - var(--size-200))}
[data-kind="embed"] .card.wide.runway-card .pin .stage{height:auto;width:100%;aspect-ratio:16/9;max-height:74vh}
[data-kind="embed"] .rwhint{position:absolute;bottom:var(--size-10);left:var(--position-centre);translate:calc(-1 * var(--ratio-neg-50)) 0;z-index:6; font:400 var(--size-9)/1 var(--type-family-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--colour-text-tertiary); background:var(--chip-background);padding:var(--space-step-1) var(--size-7);}
[data-kind="embed"] .interactive .stage{cursor:pointer}
[data-kind="embed"] .knob{position:absolute;width:13cqw;height:13cqw;background:var(--colour-text-specimen);touch-action:none;cursor:grab;z-index:2}
[data-kind="embed"] .knob:active{cursor:grabbing}
[data-kind="embed"] [data-theme="cartoon"] .knob{background:var(--colour-accent-base)}
[data-kind="embed"] .track-h{position:absolute;left:8cqw;right:8cqw;top:var(--position-centre);height:var(--border-width-hairline);background:var(--colour-border-strong)}
[data-kind="embed"] .seg-btns{display:flex;gap:var(--space-step-1);position:absolute;bottom:var(--size-9);left:var(--size-9);right:var(--size-9);justify-content:center;flex-wrap:wrap;z-index:6}
[data-kind="embed"] .seg-btns button{font:400 var(--size-10)/1 var(--type-family-mono);letter-spacing:.08em;text-transform:uppercase; background:var(--chip-background);color:var(--colour-text-secondary);padding:var(--space-step-1) var(--size-8);cursor:pointer;transition:all var(--motion-duration-150) ease}
[data-kind="embed"] .seg-btns button:hover{color:var(--colour-text-specimen);border-color:var(--colour-text-secondary)}
[data-kind="embed"] .seg-btns button.on{background:var(--colour-text-specimen);color:var(--colour-surface-page);border-color:var(--colour-text-specimen)}
[data-kind="embed"] [data-theme="cartoon"] .seg-btns button{border-color:var(--colour-text-specimen);color:var(--colour-text-specimen)}
[data-kind="embed"] [data-theme="cartoon"] .seg-btns button.on{background:var(--colour-accent-base);color:var(--colour-surface-page)}
[data-kind="embed"] .card.w12 .stage{aspect-ratio:auto;height:clamp(var(--size-300),26vw,var(--size-400))}
[data-kind="embed"] .stage{/* type — 11 steps, mono throughout */ --specimen-size-1:5px; --specimen-size-2:6px; --specimen-size-3:7px; --specimen-size-4:8px; --specimen-size-5:9px; --specimen-size-6:11px; --specimen-size-7:13px; --specimen-size-8:17px; --specimen-size-9:22px; --specimen-size-10:30px; --specimen-size-11:54px; /* px mirror of the type scale, for HTML/CSS demos. One stage width means one viewBox scale (448/300 = 1.4933), so --pN renders pixel-identical to --tN. These are LITERAL px on purpose. They were briefly expressed as calc(N*100cqw/300) to make them survive a change of stage width, which broke the sheet: an element with container-type cannot query ITSELF, so the `.stage` default-font rule below resolved 100cqw against the viewport (1440px) and set every HTML demo to 33.6px instead of 10.45px. Descendants resolved correctly, which is what made it easy to miss. */ --specimen-px-1:7.47px; --specimen-px-2:8.96px; --specimen-px-3:10.45px; --specimen-px-4:11.95px; --specimen-px-5:13.44px; --specimen-px-6:16.43px; --specimen-px-7:19.41px; --specimen-px-8:25.39px; --specimen-px-9:32.85px; --specimen-px-10:44.8px; --specimen-px-11:80.64px; /* letter-spacing — 5 steps */ /* tracked-out mono labels */ --specimen-track-1:.06em; --specimen-track-2:.1em; --specimen-track-3:.14em; --specimen-track-4:.2em; --specimen-track-5:.3em; /* tight tracking for prose and large numerals. The ladder above is all positive, so four demos were setting their own negative values. */ --specimen-tight-1:-.01em; --specimen-tight-2:-.02em; /* stroke weight — 8 steps, hairline to heavy */ --specimen-stroke-1:.5; --specimen-stroke-2:.75; --specimen-stroke-3:1; --specimen-stroke-4:1.4; --specimen-stroke-5:2; --specimen-stroke-6:2.8; --specimen-stroke-7:4; --specimen-stroke-8:5.5; /* opacity — 7 steps between the 0 and 1 endpoints */ --specimen-veil-1:.08; --specimen-veil-2:.15; --specimen-veil-3:.25; --specimen-veil-4:.4; --specimen-veil-5:.55; --specimen-veil-6:.7; --specimen-veil-7:.85; /* dash rhythm — decorative only; pathLength dasharrays are left alone because snapping one would desynchronise its dashoffset animation */ --specimen-dash-1:2 3; --specimen-dash-2:3 3; --specimen-dash-3:5 3; --specimen-dash-4:1 3;}
[data-kind="embed"] .stage{font:400 var(--specimen-px-3)/1.3 var(--type-family-mono);letter-spacing:var(--specimen-track-2)}
[data-kind="embed"] .stage svg{font:400 var(--specimen-size-3)/1 var(--type-family-mono)}
[data-kind="embed"] [data-theme="cartoon"] [data-theme="cartoon"]   .stage{content-visibility:auto;contain:layout paint style}
[data-kind="embed"] .rdar-st.s0{opacity:1;animation-name:rdar-n0}
[data-kind="embed"] .rdar-st.s1{animation-name:rdar-n1}
[data-kind="embed"] .rdar-st.s2{animation-name:rdar-n2}
[data-kind="embed"] .heat-s.s0{animation-delay:calc(-1 * var(--motion-duration-6s))}
[data-kind="embed"] .heat-s.s1{animation-delay:calc(-1 * var(--motion-duration-4s))}
[data-kind="embed"] .heat-s.s2{animation-delay:calc(-1 * var(--motion-duration-2s))}
/* THE DOUBLED CLASS IS DELIBERATE. `[data-component="field"]{width:100%}` carries the same
   specificity as a single class and is declared later, so a hidden label came out 390px wide,
   absolutely positioned, and dragged the page 74px sideways at 390. `.vh.vh` outranks it
   without reaching for !important, which nothing else in this sheet uses. */
.vh,.vh.vh{position:absolute;width:var(--size-1);height:var(--size-1);overflow:hidden;clip-path:inset(var(--clip-visually-hidden));white-space:nowrap}
/* keyboard focus: never removed, only styled */
/* §5.1: focus is architecture, so it takes the structural weight - 3px, accent, 2px
   offset. It was 2px on --fg at 3px offset with a 2px radius: an off-weight ring, an
   off-spec offset and a rounded corner, in one declaration. */
:focus-visible{outline:var(--border-width-rule) solid var(--colour-accent-base);outline-offset:var(--size-2)}
.m .r{opacity:0;transform:translateY(var(--size-32));transition:opacity var(--motion-duration-600) var(--motion-ease-settle),transform var(--motion-duration-600) var(--motion-ease-settle)}
.m .r.in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){

 .m .r{opacity:1;transform:none;transition:none}
}
/* the three paths are deliberately unequal in length and phase, so the fields never sit still
   together and the page never shows a loop point */
@keyframes aur1{
 0%,100%{transform:translate3d(0,0,0) scale(1)}
 50%{transform:translate3d(var(--aurora-amplitude),12vmax,0) scale(1.18)}}
@keyframes aur2{
 0%,100%{transform:translate3d(0,0,0) scale(1.08)}
 50%{transform:translate3d(calc(var(--aurora-amplitude) * -.8),18vmax,0) scale(.92)}}
@keyframes aur3{
 0%,100%{transform:translate3d(0,0,0) scale(.96)}
 50%{transform:translate3d(calc(var(--aurora-amplitude) * .55),-16vmax,0) scale(1.22)}}
@media (prefers-reduced-motion:reduce){

 /* a user setting, not a preference to shorten: the field holds its position and stops drifting */
 .aurora b{animation:none}
}
body.nomotion .aurora b{animation:none}
/* a link with no page behind it in the prototype: dotted underline, so a reviewer can tell an
   unbuilt route from a broken one. Never ships; there is no unbuilt route on the real site. */
a[data-unbuilt]{text-decoration:underline dotted rgba(255,255,255,.28);text-underline-offset:var(--size-4)}
/* ---- THE BUILD CHOREOGRAPHY -----------------------------------------------------------------
   Recovered from drop.html, where it was page CSS plus an inline driver and the port dropped
   both: a page built around a pipeline arriving in order rendered every card at full opacity on
   the first frame.

   ONE REVEAL MARKER, and it is the shell's `.in`. The first version of this shipped its own
   `.is-in`, which put two observers on the same elements: shell.js reveals every `.r`, and the
   build layer held those same rows at opacity 0 waiting for a class shell never adds. On
   /services all seven family rows sat invisible and kept their space, which is the gap.

   So the build layer adds ONLY the stagger. `.r` still decides when a thing has arrived;
   `data-build-child` and `--i` decide the order it arrives in, and a group sets `--i` from zero
   so every grid staggers on its own rather than inheriting a page-wide index. An element with no
   `.r` is never held at opacity 0, because nothing would ever reveal it. */
@keyframes build-in{
 from{opacity:0;transform:translateY(var(--build-shift,var(--size-14)))}
 to{opacity:1;transform:none}
}
[data-build-child]{--build-shift:var(--size-18)}
.r[data-build-child].in{animation:build-in var(--motion-duration-long) var(--motion-ease-settle)
 calc(var(--i,0) * var(--motion-duration-short)) both}
@media (prefers-reduced-motion:reduce){

 .r[data-build-child].in{animation:none;transform:none}
}

/* THE TYPE SCALES WITH THE PAGE (owner, 2026-09-24). The roles keep their names and step DOWN the
   same sixteen-step scale at the register's two breakpoints, so a heading keeps its place in the
   hierarchy relative to the body copy under it rather than dwarfing it on a narrow screen. Body
   roles, 13 to 17px, do not move: they are already at reading size. Token to token, so every
   component that names a role follows, and the other platforms keep their fixed desktop values.

   A ROLE THAT STEPS DOWN TAKES THE STEP'S LEADING WITH IT (2026-09-27). This block moved only the
   sizes, so every heading below 1100 kept its desktop leading: the h1 set 41px type on 90px lines,
   the h2 30px on 46.8, the lede 24px on 39. 0003's law is that leading is a range applied PER
   SIZE, so step N's size and step N's leading travel as a pair, and the display grades, which
   lead at 1.00, take their size as their leading.

   THE H1 IS FLUID BELOW 1100, not stepped. The scale has nothing between 41 and 90, so a tablet
   either set a five-line 90px title or a 41px one. It follows the same 8vw the hero and the page
   title already use, floored at step 14 and capped at step 15, so it meets the desktop size at
   the breakpoint instead of jumping to it. */
@media (max-width:1100px){
 :root{--grid-page-inset:var(--space-step-6);
  --type-h1-size:clamp(var(--type-step-14),8vw,var(--type-step-15));--type-h1-leading:var(--type-h1-size);
  --type-h2-size:var(--type-step-12);--type-h2-leading:var(--type-leading-12);
  --type-quote-size:var(--type-step-11);--type-quote-leading:var(--type-leading-11);
  --type-h3-size:var(--type-step-10);--type-h3-leading:var(--type-leading-10);
  --type-lede-size:var(--type-step-9);--type-lede-leading:var(--type-leading-9);
  --type-h4-size:var(--type-step-8);--type-h4-leading:var(--type-leading-8);
  --type-big-number-size:var(--type-step-13);--type-big-number-leading:var(--type-step-13)}
}
@media (max-width:760px){
 :root{--grid-page-inset:var(--space-step-4);
  --type-h2-size:var(--type-step-10);--type-h2-leading:var(--type-leading-10);
  --type-quote-size:var(--type-step-9);--type-quote-leading:var(--type-leading-9);
  --type-h3-size:var(--type-step-9);--type-h3-leading:var(--type-leading-9);
  --type-lede-size:var(--type-step-8);--type-lede-leading:var(--type-leading-8);
  --type-h4-size:var(--type-step-7);--type-h4-leading:var(--type-leading-7);
  --type-title-size:var(--type-step-7);--type-title-leading:var(--type-leading-7);
  --type-h5-size:var(--type-step-6);--type-h5-leading:var(--type-leading-6);
  --type-big-number-size:var(--type-step-11);--type-big-number-leading:var(--type-step-11)}
}

/* A TOUCH SCREEN GETS CONTROLS A THUMB CAN HIT (2026-09-27). Measured at 390 and 768 on a coarse
   pointer: every small button and field stood 40px, under the 44 a finger needs. The small control
   takes 48 there; a mouse keeps the 40 the register was drawn at. */
@media (hover:none) and (pointer:coarse){
 :root{--size-control-small:var(--size-px-48)}
}

/* THE DROP THEME (owner, 2026-09-28). /drop is the product's own page and carries the product's
   accent: mint where the rest of the site spends yellow, on the primary action, on every accented
   mark and in the aurora. A theme reassigns semantic names inside its scope and nothing else, so
   every component draws Drop's colour without knowing it is on Drop. */
/* A name bound to the accent at :root resolves there, so reassigning the accent here does not
   reach it: every such name is rebound in this scope too, or the aurora and the hovers stay yellow. */
[data-theme="drop"]{
 --colour-accent-base:var(--colour-mint-400);
 --colour-accent-warm:var(--colour-teal-900);
 --aurora-colour-1:var(--colour-mint-400);
 --colour-action-surface:var(--colour-mint-400);
 --colour-action-swipe-tertiary:var(--colour-mint-400);
 --colour-action-text-hover:var(--colour-mint-400);
 --field-border-focus:var(--colour-mint-400);
}
/* THE ACCENT'S SMALL PLACES (owner, 2026-09-28): a section's eyebrow and a question's number take
   the accent on every page, as /drop set them first. Yellow on the site, mint on Drop, through the
   same name. Small type only: headings take it on one phrase at most, marked in the document. */
[data-component="sectionHead"] > [data-component="label"],
[data-component="bigNumberItem"] .q{color:var(--colour-accent-base)}
/* DROP'S BODY SITS ON TEN COLUMNS (owner, 2026-09-28): every band between the card stack and the
   footer takes the middle ten of the twelve, one column empty either side, as the product's own
   site sets its body. Below 1100 the ten meet the page inset like any wrap. */
[data-theme="drop"] [data-template="band"]:not([data-band]) > [data-template="wrap"]{width:min(var(--grid-column-10),calc(100% - var(--grid-page-inset)))}
/* a section's own picture, under its heading and above its content, the grid's full measure */
.section-picture{margin:0 0 var(--space-step-6)}
.section-picture img{display:block;width:100%;aspect-ratio:3/2;object-fit:cover;border-radius:var(--radius-surface)}
.section-picture figcaption{margin-top:var(--space-step-1)}
.section-picture figcaption{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase;color:var(--colour-text-tertiary)}

/* ---- templates/band.skin ---- */
/* band: the paint. One hairline between sections, and nothing else: a band that painted a
   ground would decide for the patterns inside it. */
[data-template="band"],.band{border-top-color:var(--colour-border-default)}

/* ---- templates/band.form ---- */
/* A SECTION'S SPACE (owner, 2026-09-24): step 6 (48) above, step 12 (96) below - 72 at 1100, 48 at
   760 - so two sections meet 144 apart on desktop */
/* band: one horizontal section of a page, and the wrap that holds it to the measure.

   TEMPLATE ANATOMY, not a component. A template is made of patterns, and the band is HOW it is
   made of them: each pattern sits in a band, and the band decides the rhythm between them and
   where the page's measure falls. A component may not contain a pattern, so this cannot be one.

   `wrap` is the measure: the content width, or the viewport less one gutter each side, whichever
   is smaller. Everything a reader reads sits inside one.

   A BAND CARRIES NO PAINT AT ALL. It had a hairline on top, drawn to separate it from the band
   above, and the separation is already the rhythm: 72px of space above a section head, and the
   head's own type. The line added a second statement of the same boundary, which on a page of
   fourteen sections read as a table of fourteen rows. Space separates; a rule divides, and there
   is nothing here to divide. */

[data-template="band"]{padding:var(--space-step-6) 0 var(--space-step-12)}
[data-template="band"][data-form="narrative"]{padding-top:var(--space-step-12)}
[data-template="wrap"]{width:min(var(--grid-content-width),calc(100% - var(--grid-page-inset)));margin:0 auto}

/* The classes remain while the renderer carries both: the specimen records and eleven renderer
   sites name [data-template="band"] and [data-template="wrap"] directly, and a rename that leaves either behind is a page with no
   measure at all. */
.band{padding:var(--space-step-6) 0 var(--space-step-12)}
.wrap{width:min(var(--grid-content-width),calc(100% - var(--grid-page-inset)));margin:0 auto}

/* ---- A DOCUMENT READS IN A 3:7:2 -----------------------------------------------------
   The narrative band is the one a case study, an article and a reference entry render into,
   and it ran its prose the full twelve columns: 1,410px, 122 characters a line, against the
   45 to 75 a reader can hold. The live site sets long-form in the middle seven of a 3:7:2,
   which leaves three columns of approach on the left and two of air on the right, and the
   measure comes out where a reader can hold a line. The columns are the register's own, so
   the text edge lands on a column edge rather than near one. */
/* THE TRACKS ARE THE GRID'S PERIOD, NOT ITS 1440 PIXELS. They were 330, 810 and 210 - the three
   spans at 1440 - so from 1101 to 1439 the wrap narrowed and the tracks did not: at 1300 the
   document ran 126px past the viewport, and the body clips, so the right edge of every paragraph
   and figure was simply cut off. One period is (content + gutter) / 12, and a span of n columns is
   n periods less one gutter, which is exactly 330, 810 and 210 at 1440 and stays in proportion
   below it. */
[data-template="band"][data-form="narrative"] > [data-template="wrap"]{display:grid;
 grid-template-columns:calc((100% + var(--grid-gutter)) * 3 / 12 - var(--grid-gutter))
  calc((100% + var(--grid-gutter)) * 7 / 12 - var(--grid-gutter))
  calc((100% + var(--grid-gutter)) * 2 / 12 - var(--grid-gutter));
 column-gap:var(--grid-gutter)}
[data-template="band"][data-form="narrative"] > [data-template="wrap"] > *{grid-column:2}
/* ONE COLUMN BELOW 1100, AT THE SEVEN-COLUMN MEASURE. It went to the full wrap, so an article at
   1100 set 17px body 1,052px wide, 131 characters a line. Seven columns is the measure the page
   reads at on desktop; a narrower screen just meets it sooner. */
@media (max-width:1100px){
 [data-template="band"][data-form="narrative"] > [data-template="wrap"]{grid-template-columns:minmax(0,var(--grid-column-7))}
 [data-template="band"][data-form="narrative"] > [data-template="wrap"] > *{grid-column:1}
}

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){[data-template="band"]{padding:var(--space-step-6) 0 var(--space-step-9)}.band{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){[data-template="band"]{padding:var(--space-step-6) 0 var(--space-step-6)}.band{padding:var(--space-step-6) 0 var(--space-step-6)}}
/* a case study's themes, under the last of its body: chips in a wrapping row, one section step down */
[data-template="band"][data-form="narrative"] > [data-template="wrap"] > .themes{display:flex;flex-wrap:wrap;gap:var(--space-step-1);margin:var(--space-step-6) 0 0;padding:0;list-style:none}
/* NARRATIVE BANDS IN A ROW READ AS ONE ARTICLE (owner, 2026-09-25): /about keeps a section page's
   hero, but its body is set as an article's is - 64 before each heading, not a band's 96 below and
   48 above between every section */
[data-template="band"][data-form="narrative"]:has(+ [data-template="band"][data-form="narrative"]){padding-bottom:0}
[data-template="band"][data-form="narrative"] + [data-template="band"][data-form="narrative"]{padding-top:var(--space-step-8)}
/* and 16 after the heading, as an article's h2 has, not a section head's 40 */
[data-template="band"][data-form="narrative"] > [data-template="wrap"] > [data-component="sectionIntro"] [data-component="sectionHead"]{margin-bottom:var(--space-step-2)}
/* THE HOUSE RHYTHM ON THE ARTICLE GRID (owner, 2026-09-25): 16 after any heading, 24 between
   paragraphs, 48 around a picture or a table. A section head's intro sits 16 under its heading and
   24 above the text it opens; a picture or a table opening a section sits 48 under the heading. */
[data-template="band"][data-form="narrative"] [data-component="sectionHead"] > [data-component="text"]{margin-top:var(--space-step-2)}
[data-template="band"][data-form="narrative"] > [data-template="wrap"] > [data-component="sectionIntro"] [data-component="sectionHead"]:has(> [data-component="text"]){margin-bottom:var(--space-step-3)}
[data-template="band"][data-form="narrative"] [data-component="prose"] :is(h2,h3,h4) + *{margin-top:var(--space-step-2)}
[data-template="band"][data-form="narrative"] [data-component="prose"] > figure:first-child,
[data-template="band"][data-form="narrative"] > [data-template="wrap"] > [data-component="sectionIntro"] + :is([data-component="factTable"],[data-component="markedList"]){margin-top:var(--space-step-4)}
/* on the article grid a section's intro is running text, so it is set as the body is */
[data-template="band"][data-form="narrative"] [data-component="sectionHead"] > [data-component="text"]{font:var(--type-body-font)}
/* 64 INTO AND OUT OF THE ARTICLE GRID (owner, 2026-09-25): a section page's body on the article
   grid (/about) sat 192px under its hero and 144 above the next section, a page section's 96 below
   stacked on the next one's 48 or 96 above. The body takes the article rule instead - 64 before its
   heading, 64 after its last line - and the neighbour gives up its own padding on that side. */
:is(.hero[data-component="masthead"],[data-template="band"]:not([data-form="narrative"])):has(+ [data-template="band"][data-form="narrative"]){padding-bottom:0}
:is(.hero[data-component="masthead"],[data-template="band"]:not([data-form="narrative"])) + [data-template="band"][data-form="narrative"]{padding-top:var(--space-step-8)}
[data-template="band"][data-form="narrative"]:has(+ :is([data-template="band"]:not([data-form="narrative"]),[data-component="cardRail"])){padding-bottom:var(--space-step-8)}
[data-template="band"][data-form="narrative"] + :is([data-template="band"]:not([data-form="narrative"]),[data-component="cardRail"]){padding-top:0}
/* a section with no heading continues the one above it: an empty head takes no room, and the
   band opens 48 under the text, the rule around a table or a picture (owner, 2026-09-25) */
[data-template="band"][data-form="narrative"] [data-component="sectionIntro"]:not(:has(h2,h3,p,[data-component="label"])){display:none}
[data-template="band"][data-form="narrative"] + [data-template="band"][data-form="narrative"]:not(:has(h2)){padding-top:var(--space-step-6)}
[data-template="band"][data-form="narrative"]:not(:has(h2)) > [data-template="wrap"] > [data-component="factTable"]{margin-top:0}
[data-template="band"][data-form="narrative"]:not(:has(h2)) > [data-template="wrap"] > [data-component="sectionIntro"] + [data-component="factTable"]{margin-top:0}
[data-template="band"][data-form="narrative"]:has([data-component="factTable"]) + [data-template="band"][data-form="narrative"]:not(:has(h2)){padding-top:var(--space-step-2)}
/* A DETAILED PROSE OUTSIDE THE NARRATIVE BAND STILL READS AT THE MEASURE. `detailed` drops the
   prose cap because the narrative band's seven-column track sets it; in a standard band - the legal
   pages - nothing did, and the accessibility statement ran 175 characters a line at 1920. */
[data-template="band"]:not([data-form="narrative"]) [data-component="prose"][data-variant="detailed"]{max-width:var(--grid-column-7)}
/* A BAND THAT RUNS AN EMBED OPENS FLUSH (owner, 2026-09-28): /drop's card stack sits straight under
   the hero's controls, and its runway carries its own space around the frame */
[data-template="band"][data-band="embed"]{padding-top:0;padding-bottom:0}
/* and the section after it opens a step down, since the runway's pinned frame already leaves room
   beneath the stack */
[data-template="band"][data-band="embed"] + [data-template="band"]{padding-top:var(--space-step-4)}

/* ---- patterns/aurora.form ---- */
/* ---- aurora --------------------------------------------------------------------------------- */
[data-component="aurora"]{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;contain:strict}
[data-component="aurora"] span{position:absolute;display:block;width:70vmax;height:70vmax;border-radius:var(--radius-circle);will-change:transform}
[data-component="aurora"] span:nth-child(1){left:-8vmax;top:-24vmax;width:52vmax;height:52vmax;animation:aur1 var(--aurora-speed) cubic-bezier(.45,0,.55,1) infinite}
[data-component="aurora"] span:nth-child(2){right:-16vmax;top:-6vmax;animation:aur2 calc(var(--aurora-speed) * 1.37) cubic-bezier(.45,0,.55,1) infinite}
[data-component="aurora"] span:nth-child(3){left:22vmax;bottom:-34vmax;animation:aur3 calc(var(--aurora-speed) * 1.71) cubic-bezier(.45,0,.55,1) infinite}

/* ---- patterns/aurora.skin ---- */
[data-component="aurora"]{opacity:var(--aurora-opacity)}
[data-component="aurora"] span{filter:blur(var(--size-blur-lg));mix-blend-mode:var(--aurora-blend)}
[data-component="aurora"] span:nth-child(1){background:radial-gradient(circle at var(--ratio-50) var(--ratio-50),var(--aurora-colour-1),transparent var(--ratio-62))}
[data-component="aurora"] span:nth-child(2){background:radial-gradient(circle at var(--ratio-50) var(--ratio-50),var(--aurora-colour-2),transparent var(--ratio-64))}
[data-component="aurora"] span:nth-child(3){background:radial-gradient(circle at var(--ratio-50) var(--ratio-50),var(--aurora-colour-3),transparent var(--ratio-66))}

/* ---- components/breadcrumb.form ---- */
/* ---- the text components -------------------------------------------------------------------
   Six objects that every detail route drew for itself. Measured across work-case, legal,
   services-offer, services-group, thinking-article and thinking-reference before they moved, so
   what follows is the values those pages already carried rather than a redesign: where two pages
   agreed the value is theirs, and where they disagreed the difference is a data-variant with the
   reason stated. A page keeps one declaration only, which column it sits in. */
/* breadcrumb: the trail every detail route opens with. Four pages carried it, three of them
   without the container rule because they had inherited it from a copy that did. */
/* Four instances, four different values: gap 8 or 10, margin-bottom 22, 27 or 32, and /legal
   alone without the uppercase and the dim. One value each, the majority where there was one:
   gap 8 from three of the four, the 27px bottom margin from the middle of the three, and the
   uppercase dim register from three of the four. /legal's plain white trail was the deviation. */
/* THE GAP IS 8, NOT 16. The trail is one phrase - "work, financial services, Barclays" - and
   16px between every word and its slash read as three separate labels on a row rather than a
   path. The value is the same one the register calls `space.inset.tight`, which is the semantic
   name for the tightest space between two things that belong together; `--space-step-2` was the
   primitive underneath it, named directly, which is the rung the register says only the semantic
   tier may reach for. */
[data-component="breadcrumb"]{display:flex;gap:var(--space-step-1);align-items:center;margin-bottom:var(--space-step-6);font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
[data-component="breadcrumb"] a{transition:color var(--motion-duration-250) var(--motion-ease-settle)}
[data-component="breadcrumb"] b{font-weight:400}

/* THE RUNGS SIT ON ONE LINE. The separator and the current rung are the `label` atom now, and a
   label is a block with 32px under it - which is right where a label names the thing beneath it
   and wrong inside a row, so the trail broke into two baselines with the slashes riding above
   the words. The row is the container's decision, the same way prose decides a quote's size. */
[data-component="breadcrumb"] [data-component="label"]{display:inline;margin-bottom:0}
/* ON A TOUCH SCREEN each rung is at least 48px square - a family code like "B0" was 18px wide */
@media (hover:none) and (pointer:coarse){
 [data-component="breadcrumb"] a{display:inline-flex;align-items:center;min-block-size:var(--size-control-small);
  min-inline-size:var(--size-control-small)}
}
/* THE TRAIL IS ONE LINE AND THE LAST RUNG GIVES WAY. The page you are on is the h1 directly below,
   so its rung shortens to an ellipsis rather than wrapping: at 390 a case study's title ran two
   lines of capitals over the same title in the display grade, and the slash before it hung at the
   end of the line above. The rungs above it keep their words. */
[data-component="breadcrumb"] > *{flex:none}
[data-component="breadcrumb"] > [data-component="label"]:last-child{flex:1 1 auto;min-width:0;display:block;
 white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ---- components/breadcrumb.skin ---- */
[data-component="breadcrumb"]{color:var(--colour-text-tertiary)}
[data-component="breadcrumb"] a{color:var(--colour-text-tertiary)}
[data-component="breadcrumb"] a:hover{color:var(--colour-text-primary)}
/* THE PAGE YOU ARE ON IS THE LOUD ONE. It read the other way round - the steps behind you in
   the secondary and the page you are standing on dimmer than both - which says the trail matters
   more than the document under it. The rungs you can leave for are quiet, the one you are on is
   the text colour, and the separators are quieter still because they are punctuation. */
[data-component="breadcrumb"] [data-component="label"]{color:var(--colour-text-primary)}
[data-component="breadcrumb"] [data-component="label"][aria-hidden="true"]{color:var(--colour-text-tertiary)}
[data-component="breadcrumb"] b{color:var(--colour-text-primary)}

/* ---- atoms/button.form ---- */
/* button: what a press feels like.
   
   COMPONENT-SCOPED, because that is the seam this repository styles on: `.btn, button` reached
   past every component boundary it touched, and the gate said so. The three clickables that had
   no `data-component` were the nav's, the contact form's and the subscribe form's, all injected
   by shell.js - so they are marked as the atom they always were rather than styled by a second
   rule that happens to match them.
   
   The ATOM had no stylesheet at all. Its base rules sit in base.css - correctly, as the
   foundation every surface shares - but its INTERACTION lived in /drop's page head, applying to
   8 buttons on one route and unreachable from anywhere else.
   
   120ms for the press and 240ms for colour, two tokens rather than one: a pointer expects a press
   to land immediately and a colour to catch up. Both were `var(--dur-sm)`, resolved from custom
   properties /drop defined on its own `body` - a block that also REDEFINED --ease-out, so the 36
   component rules using it animated on a different curve on that one route. */
[data-component="button"]{transition:transform var(--motion-exchange-label) var(--motion-ease-settle),
 background-color var(--motion-exchange-base) var(--motion-ease-settle),border-color var(--motion-exchange-base) var(--motion-ease-settle),
 color var(--motion-exchange-base) var(--motion-ease-settle)}

/* the wipe's containing block: `isolation:isolate` composites it against the button rather than
   against the page.

   NOT ON THE TEXT-ONLY GRADE. A tertiary has no ground, so there is no wipe to contain and
   nothing to clip against - and it is the one grade whose mark MOVES on hover, six pixels to the
   right, which put the arrow's leading edge straight through the clip: every card on both
   specimen sheets sliced the arrowhead off the moment a reader hovered it. `.o` was the old
   secondary class and was already excepted here, which is what the state attribute replaced. */
[data-component="button"]:not([data-form="tertiary"]){position:relative;overflow:hidden;
 isolation:isolate}

/* THE SWIPE, ITS GEOMETRY AND ITS CLOCK. What the front IS, and what colour it arrives in, is the
   skin's: this file owns only the box that travels, the lean it travels on, and the time it
   takes. See button.skin.css for the three grades' colours. */
[data-component="button"]:not([data-form="tertiary"])::before{content:"";position:absolute;
 inset:0 var(--swipe-control-overhang);z-index:-1;
 transform:skewX(var(--angle-control-swipe)) scaleX(0);transform-origin:left center;
 transition:transform var(--motion-exchange-wipe) var(--motion-ease-swipe)}
[data-component="button"]:not([data-form="tertiary"]):hover::before,
[data-component="button"]:not([data-form="tertiary"]):focus-visible::before{
 transform:skewX(var(--angle-control-swipe)) scaleX(1)}
[data-component="button"]:not([data-form="tertiary"]):not(:hover)::before{transform-origin:right center}

/* THE TEXT GRADE UNDERLINES rather than taking a ground, which would make it a fourth grade on
   hover. Same lean and same clock, so the gesture still matches. `bottom:var(--offset-control-underline)` sits the rule
   clear of the descenders rather than through them. */
[data-component="button"][data-form="tertiary"]{position:relative}
[data-component="button"][data-form="tertiary"]::after{content:"";position:absolute;
 /* THE RULE FOLLOWS THE LABEL, FOUR PIXELS UNDER IT. `bottom` used to be measured from the
    button's own edge, which was right while a text grade hugged its words at 22px tall - the
    moment every control took a 50px height the label centred inside it and the rule stayed at
    the bottom, 17px below the thing it underlines. Half the box, less half a line, less the
    clearance: it sits under the label wherever the label sits, and follows the optical lift.
    The clearance is 4 rather than the 1px that first read as an underline ON the words. */
 left:0;right:0;height:var(--border-width-rule);
 bottom:calc(var(--position-centre) - 0.5lh - var(--offset-control-rule));
 transform:skewX(var(--angle-control-swipe)) scaleX(0);transform-origin:left center;
 transition:transform var(--motion-exchange-wipe) var(--motion-ease-swipe)}
[data-component="button"][data-form="tertiary"]:hover::after,
[data-component="button"][data-form="tertiary"]:focus-visible::after{
 transform:skewX(var(--angle-control-swipe)) scaleX(1)}
[data-component="button"][data-form="tertiary"]:not(:hover)::after{transform-origin:right center}
/* THE RULE UNDERLINES THE WORDS, NOT THE MARK (owner, 2026-09-24). It ran the whole box - the
   words, the 24px gap, the arrow and the 8px the small size puts on the mark side - so a hover drew
   a line under an arrow. A text grade has no ground, so it takes no inline padding at all, and the
   rule stops where the words stop: the mark and the gap to it are held off the side the mark is on. */
[data-component="button"][data-form="tertiary"][data-icon]{padding-inline:0}
[data-component="button"][data-form="tertiary"][data-icon="right"]::after{right:calc(var(--size-mark-base) + var(--space-step-3))}
[data-component="button"][data-form="tertiary"][data-icon="left"]::after{left:calc(var(--size-mark-base) + var(--space-step-3))}
[data-component="button"][data-form="tertiary"][data-icon="both"]::after{left:calc(var(--size-mark-base) + var(--space-step-3));right:calc(var(--size-mark-base) + var(--space-step-3))}

/* THE INK WAITS FOR THE FRONT, and the EDGE does not. The label flipped on its own clock while
   the ground took 440ms to cross, so for a fifth of a second the type was dark on a still-dark
   button and the words vanished; it is delayed rather than slowed, because a colour that crosses
   slowly is a smear. The border is part of the ground, so it changes immediately: held back with
   the label, it drew a white hairline around a button that had already gone yellow. */
[data-component="button"]:not([data-form="tertiary"]):hover,
[data-component="button"]:not([data-form="tertiary"]):focus-visible{
 transition:color var(--motion-exchange-label) var(--motion-ease-swipe) var(--motion-exchange-ink),
  background-color var(--motion-exchange-label) var(--motion-ease-swipe) var(--motion-exchange-ink),
  border-color var(--motion-exchange-label) var(--motion-ease-swipe),
  transform var(--motion-exchange-label) var(--motion-ease-settle)}

@media (prefers-reduced-motion:reduce){
 [data-component="button"]::before,[data-component="button"][data-form="tertiary"]::after{transition:none}
}

/* DISABLED STOPS THE GESTURE, it does not dim it. A control that still swipes under the pointer
   while refusing the press is telling the reader two things at once, so the front does not
   travel, the press does not scale, and the cursor says why. This lived in inlineForm.skin.css as
   an opacity on its own submit: a pattern styling an atom's state, which is the coupling the
   ladder exists to remove, and it meant the other forms' disabled submits looked enabled. */
[data-component="button"][disabled],[data-component="button"][aria-disabled="true"]{
 cursor:not-allowed;pointer-events:none}
[data-component="button"][disabled]:active,[data-component="button"][aria-disabled="true"]:active{transform:none}
/* `transform:none` here was a real bug, drawn twice on the library page: it did not hide the
   front, it removed the scaleX(0) that was HOLDING the front off, so a disabled primary rendered
   a full-size yellow ground and a disabled secondary a full-size white one with its label lost
   in it. The front is collapsed explicitly instead, and the clock stopped. */
[data-component="button"][disabled]::before,[data-component="button"][aria-disabled="true"]::before{
 transform:skewX(var(--angle-control-swipe)) scaleX(0);transition:none}
[data-component="button"][disabled]::after,[data-component="button"][aria-disabled="true"]::after{
 transform:skewX(var(--angle-control-swipe)) scaleX(0);transition:none}

/* THE MARK THE BUTTON BUILDS. A button's arrow is its own structure, written `.glyph` without the icon
   atom's name, because a control is an atom and holds no other. Its size lived in icon.form.css as a
   bare `.glyph svg`, which is the icon atom styling the button; the button states it here, at the
   20 a control's mark takes. */
[data-component="button"] .glyph svg{width:var(--size-mark-base);height:var(--size-mark-base);display:block}

/* ---- atoms/button.skin ---- */
/* THE THREE GRADES' COLOUR, AND THE COLOUR THE SWIPE ARRIVES IN.

   ONE GESTURE, THREE COLOURS. The grades differ in what they are FOR, and the colour says which:
   the solid grade swipes to the accent, the outline grade fills white, which is the state it
   already had arriving on the lean instead of appearing, and the text grade takes a rule rather
   than a ground. The motion is identical in all three, so three controls on one page read as one
   gesture rather than three effects. Geometry and timing are in button.form.css.

   THE SOLID GRADE'S EDGE IS ITS OWN GROUND, not nothing. A TRANSPARENT border still draws: the
   outer corner rounds at 4px and the inner at 3, so a white button on a dark page read as two
   nested shapes with a seam between them. The border is what holds the two grades at the same
   height, so it is kept and painted the ground's colour. */
/* EACH GRADE NAMES ITS OWN COLOUR. `--swipe` was a local custom property: the skin set it per
   grade and two rules below read it, which works and is invisible to the register - the token
   check walks declared tokens and a property invented in a stylesheet is not one. Two rules
   became four and every value is now a name the register holds. The indirection saved two lines
   and cost the one thing this system is for: being able to ask what a control is painted. */
[data-component="button"][data-form="primary"]{border-color:var(--colour-action-surface)}
[data-component="button"][data-form="primary"]::before{background:var(--colour-action-swipe-primary)}
[data-component="button"][data-form="secondary"]::before{background:var(--colour-action-swipe-secondary)}
[data-component="button"][data-form="tertiary"]::after{background:var(--colour-action-swipe-tertiary)}

/* both filled grades end up dark type on a light ground, and the edge takes the swipe's own
   colour so the control is one shape throughout the crossing */
[data-component="button"]:not([data-form="tertiary"]):hover,
[data-component="button"]:not([data-form="tertiary"]):focus-visible{
 color:var(--colour-surface-page);background-color:transparent}
[data-component="button"][data-form="primary"]:hover,
[data-component="button"][data-form="primary"]:focus-visible{border-color:var(--colour-action-swipe-primary)}
[data-component="button"][data-form="secondary"]:hover,
[data-component="button"][data-form="secondary"]:focus-visible{border-color:var(--colour-action-swipe-secondary)}
/* `:not([disabled])` because this rule restores the resting ground after a hover, and without it
   it also restored WHITE underneath a disabled control, which is why an unavailable primary was
   still a white button with grey type on it. */
[data-component="button"][data-form="primary"]:not(:hover):not(:focus-visible):not([disabled]):not([aria-disabled="true"]){
 background-color:var(--colour-action-surface)}

/* UNAVAILABLE IS A STATE, NOT A DIMMER. Opacity alone made a disabled control read as a MUTED
   one: the same button, quieter, as though it were further away or still loading. A reader cannot
   tell that apart from a de-emphasised control, and on the solid grade a 55 per cent white ground
   just reads as grey. So the state is painted rather than faded: a flat raised ground, the
   quietest legible ink, the faint hairline, and A HATCH: a control that is merely darker still
   reads as a live control turned down, because every live grade on this page is also dark. The
   hatch is the one treatment nothing else on the site uses, so it cannot be mistaken for
   emphasis, and it runs on the same 33 degree lean the swipe travels on, so the state belongs to
   the same system rather than arriving from a stylesheet somebody borrowed. */
[data-component="button"][disabled],[data-component="button"][aria-disabled="true"]{
 background-color:var(--colour-action-surface-disabled);
 background-image:repeating-linear-gradient(var(--angle-control-swipe),
  transparent 0 var(--hatch-disabled-pitch),
  var(--colour-action-hatch-disabled) var(--hatch-disabled-pitch) var(--hatch-disabled-rule));
 color:var(--colour-action-text-disabled);
 border-color:var(--colour-action-border-disabled)}
/* the text grade has no ground to flatten, so it is the ink alone that says so */
/* the text grade has no ground to hatch, so the ink alone says so, and the rule it would have
   underlined on hover is simply never drawn */
[data-component="button"][data-form="tertiary"][disabled],
[data-component="button"][data-form="tertiary"][aria-disabled="true"]{
 background-color:transparent;background-image:none;border-color:transparent}

/* ---- atoms/choice.form ---- */
/* choice: one answer a reader can give. A radio if only one may be true, a checkbox if any may.

   ONE ATOM, NOT TWO. The only genuine difference between a radio and a checkbox is the mark it
   draws and the rule about how many may be on; everything else - the hit target, the focus ring,
   the disabled state, the keyboard - is identical, and writing it twice means fixing it twice.
   The `row` form is the third: the lettered answer the diagnostic asks, which was markup invented
   inside `field` for one route with no name, no library page and no way for anything else to use
   it. It is the same atom with a bigger face.

   THE CONTROL IS THE INPUT, and the input is taken out of the FLOW rather than out of the
   accessibility tree. `display:none` would take the keyboard, the screen reader and a no-script
   submit with it; a clipped input keeps all three and needs nothing added for them. The label is
   the hit target, so the whole row is pressable.

   THE MARK IS DRAWN, not a glyph and not the browser's. A radio is a circle and a checkbox is a
   square with the control's own 4px corner, both at the hairline, and the fill arrives on the
   same 33 degree lean the button swipes on, so a checked answer and a pressed control are one
   gesture. The tick is a rotated box rather than a character: a glyph would need a font to have
   loaded before an answer could look answered. */
[data-component="choice"]{display:block;position:relative}
[data-component="choice"] > input{position:absolute;width:var(--border-width-hairline);height:var(--border-width-hairline);
 margin:calc(var(--border-width-hairline) * -1);padding:0;overflow:hidden;clip-path:inset(var(--clip-visually-hidden));white-space:nowrap}
[data-component="choice"] .face{display:grid;grid-template-columns:var(--size-mark-base) minmax(0,1fr);
 gap:var(--space-step-2);align-items:center;cursor:pointer;
 transition:border-color var(--motion-exchange-base) var(--motion-ease-settle),
  background-color var(--motion-exchange-base) var(--motion-ease-settle)}

/* THE MARK ITSELF. A box that holds the fill, and the fill is a child so it can travel. */
[data-component="choice"] .mark{position:relative;width:var(--size-mark-base);height:var(--size-mark-base);
 flex:none;border-width:var(--border-width-hairline);border-style:solid;overflow:hidden;
 transition:border-color var(--motion-exchange-base) var(--motion-ease-settle)}
/* NO RAW VALUES, INCLUDING THE SHAPES. These were briefly left as percentages on the argument
   that the drawing of a shape is not a register value. That argument is how a register erodes:
   it is a judgement about which numbers count, taken one sheet at a time, and the next person
   makes it differently. The tick's proportions are named, so a tick can be redrawn once rather
   than in whatever sheets have copied it. See docs/adr/0002. */
[data-component="choice"][data-form="radio"] .mark{border-radius:var(--radius-circle)}
[data-component="choice"][data-form="checkbox"] .mark{border-radius:var(--radius-control)}
[data-component="choice"] .mark::before{content:"";position:absolute;inset:0 var(--swipe-control-overhang);
 transform:skewX(var(--angle-control-swipe)) scaleX(0);transform-origin:left center;
 transition:transform var(--motion-exchange-wipe) var(--motion-ease-swipe)}
[data-component="choice"] > input:checked ~ .face .mark::before{
 transform:skewX(var(--angle-control-swipe)) scaleX(1)}
[data-component="choice"] > input:not(:checked) ~ .face .mark::before{transform-origin:right center}

/* the tick, drawn: two sides of a rotated box, revealed once the fill has arrived under it */
[data-component="choice"][data-form="checkbox"] .mark::after{content:"";position:absolute;
 /* border-width:0 FIRST. Setting only the right and bottom widths left the other two at the
    initial `medium`, so `border-style:solid` drew a thick box on all four sides and the tick
    rendered as a filled diamond. Two strokes of a rotated box is a tick; four is a shape. */
 left:var(--tick-control-left);top:var(--tick-control-top);
 width:var(--tick-control-width);height:var(--tick-control-height);border-style:solid;border-width:0;
 border-right-width:var(--border-width-rule);border-bottom-width:var(--border-width-rule);
 transform:rotate(var(--angle-control-tick)) scale(0);transform-origin:center;
 transition:transform var(--motion-exchange-label) var(--motion-ease-settle) var(--motion-exchange-ink)}
[data-component="choice"][data-form="checkbox"] > input:checked ~ .face .mark::after{
 transform:rotate(var(--angle-control-tick)) scale(1)}

[data-component="choice"] .said{font:var(--type-body-font)}

/* THE ROW SITS ON ONE BASELINE. Its face holds an answer and, once chosen, an insight below it,
   so it cannot centre: the letter would drift down as the row grew. `start` was the other
   mistake, aligning two different type sizes by the TOP of their line boxes, which left the
   letter's baseline 4.7px above the answer's and reading as a mis-set superscript. `baseline`
   puts the letter on the answer's own line, which is where a reader expects a label. */
[data-component="choice"][data-form="row"] .face{align-items:baseline}

/* THE LETTERED ROW. The diagnostic's own answer: a face with a border and its own padding, the
   letter where the mark would be, and an insight the answer reveals. */
/* THE LETTER'S COLUMN IS A MARK'S COLUMN. It took the page's 30px GUTTER, which is a value for
   separating columns of a layout, not a label from the thing it labels: the letter floated 30px
   off its own answer while every other form of this atom sits 16 from its mark. */
[data-component="choice"][data-form="row"] .face{grid-template-columns:var(--size-mark-l) minmax(0,1fr);
 gap:var(--space-step-2);padding:var(--space-step-2);border-width:var(--border-width-hairline);
 border-style:solid;border-radius:var(--radius-control)}
/* THE LETTER PICKS A ROLE. It stated a size and a leading of its own, which is the one thing a
   part may not do: `label` already carries both, and the mono family is the only thing genuinely
   different about this letter. */
[data-component="choice"][data-form="row"] .letter{font:var(--type-label-font);
 font-family:var(--type-family-mono);letter-spacing:var(--type-label-tracking);
 text-transform:uppercase}
/* the insight is a note under an answer, not a second paragraph: one step of air above the rule
   and one below it, where two steps read as a gap somebody forgot to fill */
[data-component="choice"] .insight{display:none;grid-column:2;margin-top:var(--space-step-1);
 padding-top:var(--space-step-1);border-top-width:var(--border-width-hairline);
 border-top-style:solid;font:var(--type-meta-font)}
[data-component="choice"] > input:checked ~ .face .insight{display:block}

[data-component="choice"] > input:focus-visible ~ .face{outline-width:var(--border-width-rule);
 outline-style:solid;outline-offset:var(--border-width-hairline)}

/* DISABLED STOPS, it does not slow down. No pointer, no travel, and the cursor says so. */
[data-component="choice"] > input:disabled ~ .face{cursor:not-allowed}
[data-component="choice"] > input:disabled ~ .face .mark::before{transition:none}

@media (prefers-reduced-motion:reduce){
 [data-component="choice"] .mark::before,
 [data-component="choice"][data-form="checkbox"] .mark::after{transition:none}
}

/* ---- atoms/choice.skin ---- */
/* THE ANSWER IS THE ACCENT. An unanswered mark is the page's own hairline; a chosen one fills
   with the accent on the same lean the button swipes on, and the tick is drawn in the page's
   ground so it reads against that fill. The row's face takes the field's border at rest and the
   accent once chosen, so a scanned list shows what has been answered without reading it. */
[data-component="choice"] .mark{border-color:var(--colour-border-strong)}
[data-component="choice"] .mark::before{background:var(--colour-accent-base)}
[data-component="choice"][data-form="checkbox"] .mark::after{border-color:var(--colour-surface-page)}
[data-component="choice"] > input:checked ~ .face .mark{border-color:var(--colour-accent-base)}
[data-component="choice"] .said{color:var(--colour-text-primary)}
[data-component="choice"] .insight{color:var(--colour-text-secondary);border-top-color:var(--colour-border-default)}

/* the row's paint, carried over unchanged from field.skin.css so the diagnostic looks exactly as
   it did: a card ground at rest, raised and accented once answered, and the letter picking up the
   accent with it. */
[data-component="choice"][data-form="row"] .face{border-color:var(--colour-border-default);
 background-color:var(--colour-surface-card)}
[data-component="choice"][data-form="row"] .face:hover{border-color:var(--colour-border-strong)}
[data-component="choice"][data-form="row"] .letter{color:var(--colour-text-tertiary)}
[data-component="choice"][data-form="row"] > input:checked ~ .face{border-color:var(--colour-accent-base);
 background-color:var(--colour-surface-raised)}
[data-component="choice"][data-form="row"] > input:checked ~ .face .letter{color:var(--colour-accent-base)}

[data-component="choice"] > input:focus-visible ~ .face{outline-color:var(--colour-accent-base)}

/* DISABLED READS AS UNAVAILABLE, not as broken: the whole row dims rather than any one part of
   it changing colour, so nothing about it looks like a different state of the control. */
[data-component="choice"] > input:disabled ~ .face{opacity:var(--opacity-disabled)}

/* ---- atoms/label.form ---- */
/* label: one short line of mono text, read above a heading or beside a value.

   THE ATOM THE WHOLE SITE HAD AND NEVER NAMED. `.lbl` was a bare class in base.css - the type
   metrics, the tracking, the case - belonging to nobody, and 1,009 elements across the built site
   wore it. Six components then added a line of their own on top of it (`display:block` and a
   margin), which is six places holding one atom's anatomy: sectionHead, titleStack, factRow,
   linkPanel, callBooker, siteDetails, siteFooter and noticeBar each said what a label is.

   It is the eyebrow over a headline, the byline on a card, the name of a value in a fact row, and
   the caption on a slot. One element, one job, and the same job every time: say in the register's
   smallest voice what the thing under it is. That is an atom, and it reads only tokens.

   A LABEL SITS ON ITS OWN LINE. That is what all six copies were saying, in three different gaps
   - 18px, 9px and 4.5px - for the same relationship: a label and the thing it names. One gap
   now, the register's own, and no second form beside it. `data-inline` was here for "the one
   case that genuinely runs in a line of text" and there was no such case: measured across every
   built route, no document asked for it once. The only markup that carried it was a card's
   placeholder, which wore `.lbl` to get the mono face and then cancelled the class it had just
   put on. A form with a specimen and no caller is a claim the system cannot support.

   THE CLASS IS GONE, AND THE ATOM IS NAMED BY EVERY WRITER. It stayed as an alias beside the
   attribute while the shell wrote the footer at runtime with the class alone, which left 53
   elements styled as this atom without being it: invisible to the library's "drawn on", to the
   tier count and to every gate that counts parts. Every renderer, the shell, consent.js and
   access.js now write `data-component="label"`. The code embeds' own `.lbl` is embed furniture,
   not this atom, and states in base.css the one thing it had been borrowing - its margin. */
/* THE WHOLE REGISTER, WEIGHT INCLUDED. This named the family, the size, the leading, the
   tracking and the case and stopped short of the one remaining part of it, so a label took
   whatever weight it happened to inherit: 195 of them drew at 300 and 14 at 500, on the same
   page, for the same job. `--type-label-font` - the shorthand the 40 rules that restate this
   register all use - has carried `--type-label-weight` the whole time. Declared here rather than
   through the shorthand because the shorthand would re-state the family and the size a second
   time in the same rule. */
[data-component="label"]{font-family:var(--type-family-mono);font-size:var(--type-label-size);
 font-weight:var(--type-label-weight);
 line-height:var(--type-label-leading);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
[data-component="label"]{display:block;margin-bottom:var(--space-step-4)}

/* ---- atoms/label.skin ---- */
/* THE LABEL'S VOICE IS DIM, which is the whole point of it: a label names the thing under it and
   must not compete with it. This was inside the bare `.lbl` rule in base.css together with the
   type metrics, so a reskin had to edit the foundation to change one colour. */
[data-component="label"]{color:var(--colour-text-tertiary)}

/* ---- patterns/cardRail.form ---- */
/* the rail's own frame is reached by the child path, never by class alone: an example inside a card
   draws on its own `.stage`, and a bare `.stage` rule made every demo a sticky full-height box */
/* ---- cardRail: the home page's scroll-scrubbed rail ----------------------------------------
   A tall track holds a sticky stage; the rail inside it is translated horizontally by the
   track's own scroll progress, so a reader moves through the cards by scrolling down the page.
   It was the home page's alone, written as .work, and it is a component now because /thinking
   wants two of them. The card is the mediaCard anatomy at portrait height: plate, title, meta.
   Every value is the home page's own, so that page is unchanged by the move.
   assets/rail.js drives it, reading Lenis's own scroll where Lenis is running. ------------- */
/* THE TRACK IS AS TALL AS THE RAIL IS WIDE, set by rail.js from the measured travel. It was a
   literal 2400px, which only happened to fit: eight cards need 1,473px of travel and 2400 leaves
   1,530px of scroll after the sticky stage - 57px of slack. A ninth card takes the travel past
   1,900 and the rail would simply never reach its end, stopping mid-card with no error anywhere.
   A number that is right for one card count is a number that is wrong for every other.
   
   This value is the no-JS floor only. Under ?motion=0 or reduced motion the CSS lays the cards
   out as a wrapped grid and the track's height stops mattering at all. */
[data-component="cardRail"] > .track{height:200vh}
/* THE PANE TAKES ITS CONTENT WHEN ITS CONTENT IS TALLER. `height` made this a fixed box that
   clipped whatever did not fit: at a 1000px viewport the head, its step and the rail came to
   seven pixels more than the pane, and seven pixels of card went under the edge. The amount
   moves with the viewport because the pane is measured in vh and the cards are not, so it is a
   fault at some heights and invisible at others. The house rule is that content yields to the
   frame; a frame that eats its own content is the same rule inverted. `min-height` keeps the
   full-height pane everywhere it fits and lets the pane grow where it does not. */
/* the stage is as tall as its heading and cards, pinned under the nav: it was a full screen with the
   cards centred in it, and the empty screen above and below them read as a gap between sections */
[data-component="cardRail"] > .track > .stage{position:sticky;top:var(--rail-stage-top,var(--rail-pin-clear));display:flex;flex-direction:column;justify-content:flex-start;overflow:hidden}
/* the rail's own measure element. A full-bleed stage gives its children no container, so the head
   inside it has to be handed one or it renders flush to the viewport while the cards sit on the
   grid - which is what it was doing, at x=0 against a first column starting at 79 on a 1568
   viewport. The rail owns this box; what sits in it is sectionHead's business. */
/* the rail places its sectionHead on the content column's left edge, where the first card starts;
   its width and its gap to the cards are the sectionHead's own */
[data-component="cardRail"] > .track > .stage > [data-component="sectionHead"]{margin-left:calc((100% - min(var(--grid-content-width),100% - var(--grid-page-inset)))/2)}
/* every card type tops out on one line, whatever its height: the plate is the rail's baseline,
   so a short card like view all work hangs from it rather than floating in the middle */
[data-component="cardRail"] > .track > .stage > .rail{display:flex;align-items:flex-start;gap:var(--space-gutter);padding-left:calc((100% - min(var(--grid-content-width),100% - var(--grid-page-inset)))/2);width:max-content;will-change:transform}
/* four columns, or the viewport less the page inset and a 64px glimpse of the next card, whichever
   is less: at 390 a 450px card ran off the screen and the rail read as clipped, not as a rail */
[data-component="cardRail"] .card{width:min(var(--grid-column-4),calc(100vw - var(--grid-page-inset) - var(--space-step-8)));flex:none}
/* cardRail card types. ONE rail, and `data-card` on the card inside it says which type it is.
   That is the whole API:

     data-card absent, or "media"  a plate, a title, a meta line. The default, and what the plate
                                   holds is `data-media` - an image, an embedCode, or nothing.
     a rail plate                  a moving ground with a slide sequence over it, 3:4, four columns.

   A type changes the card's own anatomy and nothing about the rail, so one rail carries any mix of
   them and a fourth type costs one value: the three things this site puts on a rail are the same
   object at different densities - a project, an article, and a running example. */
/* every card on the rail takes four columns (owner, 2026-09-24); what is inside it is the card's */
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
/* STILL: reduced motion, or ?motion=0, which rail.js marks `data-still` - the cards wrap as a grid
   and the track stops being a runway. The grid sits on the content column and a last
   odd card starts on its left edge, as a card grid's does, rather than centring between columns. rail.js always said ?motion=0 did this; only the media query
   did, so a frozen page kept a 200vh track and a rail running off the edge. */
@media (prefers-reduced-motion:reduce){
 [data-component="cardRail"] > .track{height:auto}
 [data-component="cardRail"] > .track > .stage{position:static;height:auto}
 [data-component="cardRail"] > .track > .stage > .rail{flex-wrap:wrap;width:min(var(--grid-content-width),calc(100% - var(--grid-page-inset)));margin-inline:auto;
 transform:none!important;padding-left:0;justify-content:flex-start}
}
[data-component="cardRail"][data-still] > .track{height:auto}
[data-component="cardRail"][data-still] > .track > .stage{position:static;height:auto}
[data-component="cardRail"][data-still] > .track > .stage > .rail{flex-wrap:wrap;width:min(var(--grid-content-width),calc(100% - var(--grid-page-inset)));margin-inline:auto;
 transform:none!important;padding-left:0;justify-content:flex-start}
/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
/* opacity only: a transform here would offset the sticky pin */
/* scroll-scrubbed word reveal */
.work .card img,.ins .rnd img{transition:transform var(--motion-duration-600) var(--motion-ease-settle)}

/* THE CARD AT THE END OF THE RAIL, which the demo had and the port dropped: after the last case
   study, one more card carrying "View more" and an arrow, so a reader who has scrolled the whole
   track has somewhere to go. It is a plate like the others - same width, same height - because a
   card that is a different size at the end of a rail reads as the rail breaking rather than as
   an invitation. */
/* SAME PLATE AS EVERY OTHER CARD: 330 wide and the plate's own 492px height. My first version
   left it at its content size - 450 x 76 against the others' 330 x 517 - which reads as the rail
   breaking at its end rather than as an invitation, and it also broke the travel measurement,
   because a card of a different width makes the rail's scrollWidth disagree with the count. */
/* slot seven, the way out: the button atom, floating in the track beside the six cards, centred on
   their height rather than filling a card of its own */
/* the way out sits on the middle of the cards' media, whatever setting they hold: rail.js measures the
   last card's plate and sets --rail-plate-centre, the plate's centre line from the rail's top */
[data-component="cardRail"] [data-card="more"]{flex:none;align-self:flex-start;margin-top:var(--rail-more-offset,0)}

/* a section owns only its foot (owner, 2026-09-24): 96, 72 at 1100, 48 at 760 */
section[data-component="cardRail"]{padding:var(--space-step-6) 0 var(--space-step-12)}
@media (max-width:1100px){section[data-component="cardRail"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){section[data-component="cardRail"]{padding:var(--space-step-6) 0 var(--space-step-6)}}
/* A CARD ON A RAIL IS ITS PICTURE AND ITS HEADLINE (owner, 2026-09-27). The rail pins at the
   viewport's height, and under a 3:4 plate the summary and the card's foot were pushed below the
   fold of the pinned stage, where they could not be read. The whole card is the link, so the
   headline says where it goes; the summary is on the page it opens. The same cards in a grid keep
   everything. */
[data-component="cardRail"] [data-component="mediaCard"] > :is([data-component="text"],.foot){display:none}
/* A TALL CARD IS SIZED BY THE SCREEN'S HEIGHT TOO. The rail pins at the viewport's height, and a
   3:4 plate four columns wide stands 600px, so on a 1280x844 laptop the headline sat below the
   pinned stage and never came into view. Such a card is no wider than three quarters of the height
   left once the bar and 320px of section head and headline are set aside. */
[data-component="cardRail"] .card:has([data-ratio="3:4"]){width:min(var(--grid-column-4),calc(100vw - var(--grid-page-inset) - var(--space-step-8)),
 calc((100svh - var(--bar-site-height) - var(--size-px-320)) * 3 / 4))}

/* ---- patterns/cardRail.skin ---- */


/* ---- components/factRow.form ---- */
/* factRow: client, period and role, taken in one pass. A label-and-value table reads as
   reference; three columns read as a masthead, which is what these three facts are. The first
   column is wider because a client is a DESCRIPTION where the name is under NDA - "a FTSE 100
   chemicals group" - and a period and a role are four words each.

   Lives here rather than on the case study page, which is where it was written: a page styling a
   component is the fork this seam exists to prevent, and the audit named all five rules. */
[data-component="factRow"]{padding:var(--space-step-5) 0 var(--space-step-11)}
[data-component="factRow"] .cols{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--space-gutter);padding-top:var(--space-step-4);border-top-width:var(--border-width-hairline);border-top-style:solid}
[data-component="factRow"] p{font:var(--type-citation-font);}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
/* ON THE GRID, IN THE LIST RHYTHM (owner, 2026-09-23). The columns were 2fr 1fr 1fr, which puts the
   second fact at 705px - on no grid line. Each fact now spans three of the twelve columns, so they
   start at 0, 360, 720 and 1080: four facts fill the row, fewer leave it open. Spacing follows
   bigNumberList and exhibitCard: 32px from the rule, 8px from label to value. */
[data-component="factRow"] .cols > *{grid-column:span 3;display:grid;gap:var(--space-step-1);align-content:start}
[data-component="factRow"] .cols [data-component="label"]{margin-bottom:0}
[data-component="factRow"] .cols p{margin:0}
@media (max-width:1100px){
 [data-component="factRow"] .cols{grid-template-columns:1fr;gap:var(--space-step-4)}
 [data-component="factRow"] .cols > *{grid-column:auto}
}

/* ---- components/factRow.skin ---- */
[data-component="factRow"] .cols{border-top-color:var(--colour-border-default)}
[data-component="factRow"] p{color:var(--colour-text-primary)}

/* ---- atoms/field.form ---- */
/* field: the one control a reader types into. GEOMETRY ONLY - colour is in field.skin.css.
   
   WHY THIS FILE EXISTS, AND WHY IT IS NOT IN shared/. These rules lived in
   `shared/field-contract.css`, the only sheet in that folder and the only atom whose rules sat
   outside the component layer. `shared/` is for a contract two components must agree on; a field
   is not that, it is an ATOM, and every other atom keeps its form beside its peers. That folder
   is now empty and gone.
   
   AND WHY THE SITE'S components.css CARRIES FIELD RULES AT ALL, where the house bundle does not: §5 says
   a presentation system has no controls - "no buttons, inputs or loaders" - and §13.1 makes the
   site the application class, whose definition is having them. The house `assets/components.css` styles
   no input on purpose. This one must.
   
   THE SELECTOR NAMES THE ATOM, AND ONLY THE ATOM. It also named any input inside formPanel or
   inlineForm, for controls written without the mark - the access gate, the shell's forms. Every
   writer names the field atom now (checked across the built pages, the runtime scripts and the
   renderers, 2026-09-23: none bare), so those halves matched nothing the atom did not, and they
   were the atom styling the components that hold it. They are gone.
   
   `:not([type="submit"])` because a submit is a BUTTON wearing an input's tag: §5.1 gives it the
   structural 3px border, not a field's hairline. */
/* THE BOX, ON THE CONTROL'S OWN BALANCE. A field stood 76px tall on 24px of vertical inset
   and 8px of horizontal, so its type sat against the left edge while a button beside it
   stood at 59. The two are the same kind of thing and now take the same insets: 16 and 24,
   which puts a field and a button on one line at one height. `small` is a step down on both
   axes, as it is on the button, for a control inside something else.
   The corner is the control corner, for the reason 0006 gives: a thing a reader acts on is
   the one thing the house rounds. */
/* THE CITATION ROLE, 15px (owner, 2026-09-24): what is typed in a field sits at the size of the
   words around the form - the address, the intro lines, the notes. It was the 17px body role. */
[data-component="field"]{width:100%;border:var(--border-width-hairline) solid var(--field-border);min-height:var(--size-control);padding:0 var(--field-inset-inline) var(--button-optical-lift);border-radius:var(--radius-control);font:inherit;font-size:var(--type-citation-size);line-height:var(--type-citation-leading);transition:border-color var(--motion-duration-250) var(--motion-ease-settle),background-color var(--motion-duration-250) var(--motion-ease-settle)}

/* A FIELD STANDS AT A CONTROL'S HEIGHT, which is the whole reason the tokens above said "the
   same 16 a control takes, so a field and a button beside it stand the same height": they did
   not, because both were adding a padding to a leading and the two leadings differ. Both are
   given the height now and the text centres in it. A textarea keeps its rows: a box you type
   paragraphs into is not a control standing in a row. */
[data-component="field"][data-size="small"]{min-height:var(--size-control-small);
 padding:0 var(--field-small-inline) var(--button-optical-lift)}textarea[data-component="field"]{min-height:0;padding:var(--field-inset-block) var(--field-inset-inline)}

/* FOCUS TAKES THE STRUCTURAL WEIGHT. DESIGN.md §5.1: "focus is a 3px accent outline at 2px offset
   - focus is architecture, so it takes the structural weight." It was a 2px outline at 3px offset
   with a 2px radius: three breaches of the contract in one declaration. */
/* FOCUS IS THE EDGE, NOT A RING AROUND IT. The contract asked for a 3px accent outline at 2px
   offset, which draws a second shape outside the field and reads as something stuck to it
   rather than as the field being live. The accent goes on the border itself, and the ring
   is drawn as a box-shadow with no offset so the edge gains the structural weight the
   contract wants WITHOUT a gap: one shape, thicker and yellow. The shadow is geometry
   here and its colour is the skin's, which is why it reads a variable the skin sets. */
[data-component="field"]:focus-visible{outline:none}

/* the browser's own focus ring is replaced rather than layered: `outline:none` is geometry, so it
   belongs here and not in the skin, which the gate is right to insist on */
/* THE SUPPRESSION IS FOR A POINTER ONLY. This rule was `:focus{outline:none}` against the
   `:focus-visible` ring three lines up. Both are pseudo-classes of equal weight, so the pair have
   identical specificity and the later one won: a keyboard-focused field matched both and the
   contract's 3px accent ring - section 5.1, focus is architecture and takes the structural weight
   - resolved to `outline-style:none`. The only remaining cue was a border-colour change, on every
   input, textarea and select on the site.
   
   `:focus:not(:focus-visible)` says what was meant: a mouse click on a text field should not draw
   a ring, a keyboard arriving at one must. It also outranks the ring rule now, which is fine,
   because the two no longer match the same element. */
[data-component="field"]:focus:not(:focus-visible){outline:none}

/* A SELECT IS A FIELD, and its caret is the field's, not the panel's. Every declaration that made
   a select look like one of this site's fields - the suppressed native appearance, the 42px of
   room on the right, the register's own chevron - was keyed on `[data-component="formPanel"]`, so
   a select anywhere else arrived with the platform's arrow jammed against its edge and no room
   for it. The inventory drew exactly that: a chevron with none of the field's 15px inset.
   
   THE CARET MIRRORS THE TEXT. It sits the field's own inline inset from the right edge, which is
   the inset the text keeps from the left, and the room reserved for it is that inset, the mark,
   and one step between the last letter and the caret: 24 + 16 + 8 = 48 at the default size. It
   was placed with `--space-step-1`, the PAGE's 8px inset, so the caret sat 8px from its edge
   while the text sat 24 from the other - the mirror this comment always promised and the rule
   never drew. The room was already 48, so no text moves; only the caret does.
   
   The selector names the atom alone, as every rule in this file does now. */
select[data-component="field"]{
 appearance:none;cursor:pointer;padding-right:calc(var(--field-inset-inline) + var(--size-mark-s) + var(--space-step-1))}
/* SMALL KEEPS ITS CARET. The small rule sets the whole `padding` shorthand and outranks the select
   rule above, so a small select lost its reserved room and a long option ran under the caret. It
   takes the same sum at the small inset: 16 + 16 + 8. */
select[data-component="field"][data-size="small"]{
 padding-right:calc(var(--field-small-inline) + var(--size-mark-s) + var(--space-step-1))}

/* The lettered answer row that used to live here is now the `choice` ATOM, with a radio, a
   checkbox and that row as its three forms. It was markup invented inside this file for one
   route: no name, no library page, and nothing else able to reuse it. See choice.form.css. */

/* The note that used to live here, and in field.skin.css, is the `fieldNote` ATOM with sheets of
   its own. A field says what is wrong on its own edge (field.skin.css); what it says in words is
   the note's. See fieldNote.form.css. */

/* ON A TOUCH SCREEN A FIELD READS AT BODY SIZE. iOS zooms the page into any field set under 16px the
   moment it takes focus, and the citation grade is 15, so every form on the site jumped on a phone. */
@media (hover:none) and (pointer:coarse){
 [data-component="field"]{font-size:var(--type-body-size)}
}
/* A PLACEHOLDER LONGER THAN ITS FIELD ENDS IN AN ELLIPSIS rather than a hard cut mid-word: the
   access gate's "Your email, or an access code you already have" lost its last words at 390 */
[data-component="field"]{text-overflow:ellipsis}

/* ---- atoms/field.skin ---- */
/* field: what a field is painted.
   
   The faint fill is the point: a field reads as a field inside a panel rather than dissolving into
   a border the same weight as the panel's own edge.

   THE FILL IS `background-color`, NEVER `background`. The focus rule wrote the shorthand, and
   `:focus` outranks the select's own rule, so focusing a select reset its background-image to
   none: the caret vanished the moment a reader clicked into the enquiry field on the contact
   form, and came back when they left it. This is the trap the caret's own comment below already
   records once; the rest fill is changed with it, because it only escapes today by losing on
   specificity. */
[data-component="field"]{background-color:var(--field-background);color:var(--colour-text-primary)}[data-component="field"]:hover{border-color:var(--field-border-hover)}[data-component="field"]:focus{border-color:var(--colour-text-primary);background-color:var(--field-background-focus)}[data-component="field"]::placeholder{color:var(--colour-text-tertiary)}

/* native validation, only after the reader has actually touched the field */
[data-component="field"]:user-invalid{border-color:var(--colour-feedback-warn)}

/* the caret's placement and size: the field's own inline inset from the right edge, the same inset
   the text keeps on the left, at the 16px mark size. See field.form.css for the room reserved. */
/* THE WHOLE CARET IS PAINT, image included. The image sat in field.form.css and this file's own
   `background:` SHORTHAND, which loads after it, reset background-image to none - so the caret
   vanished from every route while the exhibit still showed it. A shorthand is not a narrower
   property: it writes every longhand it owns, including the ones you did not mention.
   
   The comment also belongs OUT here rather than inside the block: the wired gate reads a
   declaration block's contents as declarations, so a comment in there was reported as a property
   named "THE WHOLE CARET IS PAINT". */
select[data-component="field"]{
 background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%238f8f8f"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></svg>');
 background-repeat:no-repeat;background-position:right var(--field-inset-inline) center;background-size:var(--size-mark-s)}select[data-component="field"][data-size="small"]{background-position:right var(--field-small-inline) center}

/* a choice: the state is on the whole face, because the face IS the control. A tick beside a row
   says the row is a label for something else. */

/* the ring the form draws: the accent while the field is live, and the warning when it is not */
/* THREE THINGS A FIELD CAN BE SAYING, and each says it on its own edge rather than through an
   icon or a ring floating beside it. `good` and `error` are set by the form and outrank focus, so
   a field that has been accepted still reads as accepted while the cursor is in it. */
[data-component="field"]:focus-visible{--field-ring:var(--field-border-focus);border-color:var(--field-border-focus)}[data-component="field"][data-state="error"]{--field-ring:var(--field-border-error);border-color:var(--field-border-error)}[data-component="field"][data-state="good"]{--field-ring:var(--field-border-good);border-color:var(--field-border-good)}

/* THE RING, WHOLE. It doubles the field's edge with no offset, so the edge thickens rather than a
   second shape floating outside it: one shape, in the colour of whatever the field is saying. The
   declaration lives here rather than in the form because a box-shadow is paint, which the gate is
   right to insist on even when the thing it draws is structural. */
[data-component="field"]:focus-visible,
[data-component="field"][data-state="error"],
[data-component="field"][data-state="good"]{
 box-shadow:0 0 0 var(--border-width-hairline) var(--field-ring,transparent)}

/* ---- atoms/fieldNote.form ---- */
/* fieldNote: the one line a field says about itself - what is wrong with it, or that it has been
   accepted - set under the field it belongs to.

   ITS OWN SHEETS. These rules lived inside field.form.css and field.skin.css, so an atom was
   styled from another atom's files: the library could not say where fieldNote was drawn from, and
   a change to the note meant editing a file named for something else.

   THE MESSAGE BELONGS TO THE FIELD rather than to the panel, because the panel does not know which
   field failed. So the note sits directly under its field, one step down, at the meta grade. */
[data-component="fieldNote"]{display:block;margin-top:var(--space-step-1);font:var(--type-meta-font)}

/* ---- atoms/fieldNote.skin ---- */
/* fieldNote: the paint. The note takes the state of the field it belongs to, so it asks for the
   field's own text colours rather than a colour of its own: if the field's warning changes, the
   note under it changes with it. On its own it is the warning, because a note with nothing wrong
   is a note nobody writes. */
[data-component="fieldNote"]{color:var(--field-text-error)}
[data-component="fieldNote"][data-state="good"]{color:var(--field-text-good)}

/* ---- patterns/factTable.form ---- */
/* factTable: a set of facts, in one of the two shapes the prototype actually drew.
     pairs    a label column and a value column, no header row. A case study's own facts.
     columns  a header row and three or four columns, which stacks into labelled rows under
              1100 because a four-column table on a phone is not a table.
   The first column's width is the one measured difference between the two `columns` instances,
   26 per cent on an service's phase table and 38 per cent on a group's service list, and it is a
   variant rather than a free value: a table whose first column is a name needs more room than
   one whose first column is a number. */
[data-component="factTable"]{width:100%;border-collapse:collapse}
/* ON THE GRID (owner, 2026-09-24): every row is the twelve columns with the gutter between, and a
   cell spans by how many the row holds - two: 4 and 8, three: 4 each, four: 3 each - so its edges
   land on the grid's lines rather than wherever the text pushes them. */
[data-component="factTable"] :is(thead,tbody){display:block}
[data-component="factTable"] tr{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--space-gutter)}
[data-component="factTable"] :is(th,td){grid-column:span 4}
[data-component="factTable"] :is(th,td):last-child:nth-child(2){grid-column:span 8}
[data-component="factTable"] :is(th,td):nth-last-child(n+4),[data-component="factTable"] :is(th,td):nth-last-child(n+4) ~ *{grid-column:span 3}
/* FIVE OR SIX COLUMNS DO NOT DIVIDE TWELVE (owner, 2026-09-25): spanning 3 each, a fifth cell
   wrapped onto a line of its own and the row stopped reading as a row. A row that long takes one
   equal track per cell, with the same gutter between. */
[data-component="factTable"] tr:has(> :nth-child(5):last-child){grid-template-columns:repeat(5,minmax(0,1fr))}
[data-component="factTable"] tr:has(> :nth-child(6):last-child){grid-template-columns:repeat(6,minmax(0,1fr))}
[data-component="factTable"] tr:has(> :nth-child(5)) > *{grid-column:auto}
[data-component="factTable"] th,[data-component="factTable"] td{text-align:left;vertical-align:top;font-weight:400}

/* A CELL OWNS ITS PADDING, because a table cell with none takes the browser's own 1px - which is
   off the 4.5px vertical ramp, and is exactly what the spacing audit caught the first time a
   document's own table reached a page. 560 tables across 74 documents render inside prose, and
   before this they were 1px cells in the UA's default grade.
   The value is factPair's, so a `pairs` table is unchanged and a `columns` table lands on the
   same grid. */
[data-component="factTable"] th,[data-component="factTable"] td{padding:var(--space-step-4) 0}
/* a header row is a row of TERMS, which is the mono grade every other label on the site takes */
[data-component="factTable"] thead th{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
[data-component="factTable"][data-form="columns"] thead th,[data-component="factTable"][data-form="columns"] tbody td{border-top-width:var(--border-width-hairline);border-top-style:solid}
[data-component="factTable"][data-form="columns"] tbody td{font:var(--type-citation-font);}

/* UNDER 1100 A COLUMNS TABLE IS NOT A TABLE. Four columns in 390px measured 529px wide and took
   the whole document sideways with it, which is the one thing a page may never do. The header row
   goes, every row becomes a card, and each cell prints the column it belongs to from `data-label`
   - written by prose.table, because a value with no header above it says nothing. `pairs` is
   untouched: two columns fit, and factPair already owns that row. */
@media (max-width:1100px){
  /* DISPLAY NONE, NOT A CLIP. Clipped out of sight the header row still measured 430px and
     still widened the document, which is the fault this rule exists to fix. Nothing is lost:
     every cell below prints its own column name. */
  [data-component="factTable"][data-form="columns"] thead{display:none}
  [data-component="factTable"][data-form="columns"] tbody tr{display:block;border-top-width:var(--border-width-hairline);border-top-style:solid}
  [data-component="factTable"][data-form="columns"] tbody td{display:block;border-top:0;padding:0 0 var(--space-step-2) 0}
  [data-component="factTable"][data-form="columns"] tbody tr td:first-child{padding-top:var(--space-step-4)}
  [data-component="factTable"][data-form="columns"] tbody tr td:last-child{padding-bottom:var(--space-step-4)}
  [data-component="factTable"][data-form="columns"] tbody td[data-label]::before{content:attr(data-label);display:block;font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
}

/* A FIGURE IS NOT A FACT. `metrics` is the case study's own numbers, "52", "four times", each
   beside what it measures. Set at the citation grade they read as another sentence in the table;
   the big-number role exists for exactly this and nothing had ever picked it. The label stays
   where it is: the figure is what a reader is meant to land on. */
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] td:first-of-type{
 font:var(--type-big-number-font)}

/* THE FIGURES SIT ON ONE GRID (owner, 2026-09-25): each row sized its own number column, so the
   labels started wherever a figure happened to end, and a fixed figure slot left a gap wider than
   any figure. Every row now shares the table's two tracks through subgrid: the figure column is
   as wide as the widest figure, the label takes the rest at full width, and the figure and the
   the label align. The rule is the row's, drawn once across. Below 760 the figure stacks
   over its label. */
[data-component="factTable"][data-form="metrics"] tbody{display:grid;grid-template-columns:max-content minmax(0,1fr);column-gap:var(--space-step-6)}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"]{display:grid;grid-column:1 / -1;grid-template-columns:subgrid;align-items:start;
 padding:var(--space-step-3) 0;border-top-width:var(--border-width-hairline);border-top-style:solid}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"]:first-child{border-top:0}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] > *{padding:0;border:0;text-align:left;width:auto;max-width:none}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] > * > *{max-width:none}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] td{grid-column:1;grid-row:1}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] td > *{font:inherit;margin:0}
/* THE TOPS ALIGN, NOT THE BASELINES (owner, 2026-09-25): on a shared baseline a 14px label hangs
   beside the foot of a 41px figure. Each text box is trimmed to its capital height, so the top of
   the figure and the top of the label land on one line whatever their sizes. */
:is([data-component="factTable"][data-form="metrics"] [data-component="factPair"] td > *,[data-component="factTable"][data-form="metrics"] [data-component="factPair"] th > *){text-box-trim:trim-start;text-box-edge:cap alphabetic}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"] th{grid-column:2;grid-row:1}
@media (max-width:760px){[data-component="factTable"][data-form="metrics"] tbody{grid-template-columns:minmax(0,1fr)}
 :is([data-component="factTable"][data-form="metrics"] [data-component="factPair"] td,[data-component="factTable"][data-form="metrics"] [data-component="factPair"] th){grid-column:1}
 [data-component="factTable"][data-form="metrics"] [data-component="factPair"] th{grid-row:2}}

/* a PAIRS table stacks below 760: one column, the term over its value (see factPair) */
@media (max-width:760px){
 [data-component="factTable"][data-form="pairs"] tr{grid-template-columns:minmax(0,1fr)}
 [data-component="factTable"][data-form="pairs"] :is(th,td){grid-column:1 / -1}
}
/* a token path or code value in a cell may break anywhere, so a long one keeps the gutter to the
   next column rather than running up against it */
[data-component="factTable"] :is(td,th) code{overflow-wrap:anywhere}

/* ---- patterns/factTable.skin ---- */
/* factTable: the paint the columns form needs, and nothing else. A `pairs` table takes its rules
   and its colours from factPair, which is the row; a `columns` table has no row component, so the
   rule between its rows belongs to the table. */
[data-component="factTable"] thead th{color:var(--colour-text-tertiary)}
[data-component="factTable"][data-form="columns"] thead th,[data-component="factTable"][data-form="columns"] tbody td{border-top-color:var(--colour-border-default)}
[data-component="factTable"][data-form="columns"] tbody td{color:var(--colour-text-secondary)}
[data-component="factTable"][data-form="metrics"] [data-component="factPair"]{border-top-color:var(--colour-border-default)}

/* ---- components/factPair.form ---- */
/* factPair: one row of a label-and-value table - the field's name, and its value.

   THE ROW HAD NO NAME, so factTable was a pattern whose only contents were text atoms: the table
   owned the width of the label column, the mono grade on it and the rule above every cell, and the
   repeated unit - the thing a document adds a row to - was a bare `<tr>`. Both callers now render
   this: the evidence table, and the licence's scale table, which until now drew three unnamed
   divs a stylesheet never mentioned.

   The label is a term and the value is a TEXT ATOM, which is where its body grade comes from. */
[data-component="factPair"] th,
[data-component="factPair"] td{border-top-width:var(--border-width-hairline);border-top-style:solid;padding:var(--space-step-4) var(--space-step-3) var(--space-step-4) 0}
/* ON THE GRID: two columns of label and one gutter, so the value starts on column 3. */
/* the term is the label atom inside the cell - a th stays a table cell, so the atom cannot be the
   cell itself - and the atom sets its type. The cell only places it on the grid. */
[data-component="factPair"] th{width:calc(var(--factpair-label-width) + var(--grid-gutter));padding-right:var(--grid-gutter)}
[data-component="factPair"] th [data-component="label"]{margin-bottom:0}
[data-component="factPair"] td{font:var(--type-citation-font);}
/* the first row carries no rule above it: the table's own top edge is the section's, not a cell
   border. The selector was `... tr:first-child th` when the table owned this; the pair IS the tr. */
[data-component="factPair"]:first-child th,
[data-component="factPair"]:first-child td{border-top:0}
/* BELOW 760 THE PAIR STACKS: the term over its value (factTable owns the row's grid and drops it to
   one column there). At 390 the term had four of twelve columns, 100px, and "Private Equity Value
   Creation" ran three lines deep beside - and at first over - its value. */
@media (max-width:760px){
 [data-component="factPair"] th,[data-component="factPair"] td{width:auto;padding-right:0}
 [data-component="factPair"] th{padding-bottom:var(--space-step-1)}
 [data-component="factPair"] td{border-top:0;padding-top:0}
}

/* ---- components/factPair.skin ---- */
[data-component="factPair"] th,
[data-component="factPair"] td{border-top-color:var(--colour-border-default)}
[data-component="factPair"] th{color:var(--colour-text-tertiary)}
[data-component="factPair"] td{color:var(--colour-text-secondary)}

/* ---- patterns/cardGrid.form ---- */
/* cardGrid: a set of cards, laid out on the count the module resolved.

   THE PATTERN THE CARD LISTS HAD NO NAME FOR. Seven modules render a grid of panelCards or
   mediaCards - the card list, the licence tiers, the case studies, the articles, the feed, the
   specimen panels, a single media card - and every one of them reached the COMPONENT directly:
   `cardListModule -> panelCard` in the contract's own table, with the grid that arranges them
   living in base.css as a bare `.grid` class belonging to nobody. So a template composed a
   component, the rung between them was a class in the foundation, and the tier table recorded
   the skip as if it were the model.

   The grid is that rung. It is a section's worth of cards and it holds components, which is what
   a pattern is; the cards hold atoms, which is what a component is. The rules are base.css's own,
   moved unchanged except for the selector and `54px`, which is `--lead-l` and was the one
   dimension here written as a number.

   A grid states its COLUMNS where the count is editorial and its COUNT where the count is the
   layout - data-count drove both, which failed the moment a list module handed it forty rows.
   `data-form="pair"` is the two-up form the licence tiers take, which was a second class. */
[data-component="cardGrid"]{display:grid;gap:var(--space-step-12) var(--space-gutter);grid-template-columns:1fr}
[data-component="cardGrid"][data-cols="2"]{grid-template-columns:repeat(2,minmax(0,1fr))}
[data-component="cardGrid"][data-cols="3"]{grid-template-columns:repeat(3,minmax(0,1fr))}
[data-component="cardGrid"][data-cols="4"]{grid-template-columns:repeat(4,minmax(0,1fr))}
/* THE DERIVED COUNT ONLY SPEAKS WHERE NOTHING IS STATED. These four sat after the `data-cols`
   rules at the same specificity, so they won: a module stating three columns and handing over
   four cards drew four, and one handing over two drew two. A specimen page's fourteen grids came
   out 3, 4 and 2 wide down one page. `data-cols` is the module's own instruction; the count is
   the fallback for a list that never gave one. */
[data-component="cardGrid"][data-count="2"]:not([data-cols]){grid-template-columns:repeat(2,minmax(0,1fr))}
[data-component="cardGrid"][data-count="3"]:not([data-cols]){grid-template-columns:repeat(3,minmax(0,1fr))}
[data-component="cardGrid"][data-count="4"]:not([data-cols]){grid-template-columns:repeat(4,minmax(0,1fr))}
[data-component="cardGrid"][data-count="6"]:not([data-cols]){grid-template-columns:repeat(3,minmax(0,1fr))}
[data-component="cardGrid"][data-form="pair"]{display:grid;gap:var(--space-gutter);grid-template-columns:repeat(2,minmax(0,1fr))}
@media (max-width:1100px){
 /* The stated columns collapse at the breakpoint. Three up survived to 1100 and pushed 208px past
    the viewport on five routes, because a column count that never changes is a column count that
    only works at one width. */
 [data-component="cardGrid"]:is([data-cols="3"],[data-cols="4"]){grid-template-columns:repeat(2,minmax(0,1fr))}
 [data-component="cardGrid"][data-count]:not([data-cols]),[data-component="cardGrid"][data-form="pair"]{grid-template-columns:minmax(0,1fr)}
}
@media (max-width:760px){
 [data-component="cardGrid"][data-cols="2"],
 [data-component="cardGrid"][data-cols="3"],[data-component="cardGrid"][data-cols="4"]{grid-template-columns:minmax(0,1fr)}
}

[data-template="band"][data-variant="detailed"] [data-component="cardGrid"]{grid-template-columns:repeat(2,minmax(0,1fr));
 gap:var(--space-gutter)}
@media (max-width:1100px){

 [data-template="band"][data-variant="detailed"] [data-component="cardGrid"]{grid-template-columns:minmax(0,1fr)}
}
/* THREE UP, A CARD'S TITLE STEPS DOWN (owner, 2026-09-24): at a third of the page the h4
   grade, 27px, crowded a 440px card. The grid names the h5 role, 21px, the size of the form and booker heads. */
/* a paged grid hides the cards not on the current page (page.js); the attribute beats a card's display */
[data-component="cardGrid"] > [hidden]{display:none}
/* the pageRow under a paged grid stands a block clear of the last row of cards */
[data-component="cardGrid"] + [data-component="pageRow"]{margin-top:var(--space-step-6)}
/* THE FEED LOADING (owner, 2026-09-25): while a band is aria-busy its cards hold their shape as
   blocks - the plate, a line for the byline, two for the title - and nothing inside them shows, so
   a reader sees the grid arriving rather than a page that has stopped */
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"] > :not([data-component="media"]){visibility:hidden}
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"] [data-component="media"] img{visibility:hidden}
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"]{position:relative}
/* two bars under the plate: a byline 16px tall, then a title 48px tall - drawn as one background of
   two gradients so the card needs no extra markup */
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"]::after{content:"";position:absolute;left:0;right:0;
 top:calc(var(--card-plate-height) + var(--space-step-3));height:var(--space-step-12)}
/* the plate's height, so the bars sit under it whatever the column width: a 3:2 plate is two
   thirds of the card's width */
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"]{container-type:inline-size;--card-plate-height:calc(100cqw * 2 / 3)}
[aria-busy="true"] [data-component="chipRow"] :is(button,[data-count]){visibility:hidden}
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"] [data-component="media"],
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"]::after{animation:skeleton var(--motion-duration-1100) var(--motion-ease-settle) infinite alternate}
@media (prefers-reduced-motion:reduce){[aria-busy="true"] [data-component="cardGrid"] *,[aria-busy="true"] [data-component="cardGrid"] > *::after{animation:none}}
/* NO ORPHAN ON A THREE-UP ROW (2026-09-27). Four, seven or ten cards in three columns end on
   a card alone - /private-equity's four closed on one - so those counts set four columns: 4, 4/3,
   4/4/2. Thirteen is left at three: four columns would end 4/4/4/1. Wide screens only; under 1100 the grid is two-up and these counts pair off or end on one
   the way any two-column list does. */
@media not (max-width:1100px){
 [data-component="cardGrid"][data-cols="3"]:is([data-count="4"],[data-count="7"],[data-count="10"]){grid-template-columns:repeat(4,minmax(0,1fr))}
}
/* A DETAILED GRID OF PANELS KEEPS ITS STATED THREE (owner, 2026-09-28): /drop's six modules and three
   steps are panels, each boxed on its own ground, so three to a row reads as a set where the plain
   detailed grid's two would leave a step alone on its row */
[data-template="band"][data-variant="detailed"] [data-component="cardGrid"][data-cols="3"]:has(> [data-component="mediaCard"][data-form="panel"]){grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:1100px){
 [data-template="band"][data-variant="detailed"] [data-component="cardGrid"][data-cols="3"]:has(> [data-component="mediaCard"][data-form="panel"]){grid-template-columns:minmax(0,1fr)}
}
/* A LEDGER IS ONE COLUMN WITH NO GAP: its lines meet at their hairlines, and the last closes it */
[data-component="cardGrid"][data-cols="1"]:has(> [data-form="row"]){grid-template-columns:minmax(0,1fr);gap:0;border-bottom:var(--border-width-hairline) solid}
/* SIX PANELS ARE ONE BOARD (owner, 2026-09-28): /drop's modules share one boxed surface, each cell
   ruled off from the next, rather than six boxes with gutters between them */
[data-component="cardGrid"][data-count="6"]:has(> [data-form="panel"]){gap:0;border:var(--border-width-hairline) solid;border-radius:var(--radius-surface);overflow:hidden}
[data-component="cardGrid"][data-count="6"] > [data-form="panel"]{border-radius:0;border-width:0 var(--border-width-hairline) var(--border-width-hairline) 0}
[data-component="cardGrid"][data-count="6"] > [data-form="panel"]:nth-child(3n){border-right-width:0}
[data-component="cardGrid"][data-count="6"] > [data-form="panel"]:nth-last-child(-n+3){border-bottom-width:0}
@media (max-width:1100px){
 [data-component="cardGrid"][data-count="6"] > [data-form="panel"]{border-right-width:0}
 [data-component="cardGrid"][data-count="6"] > [data-form="panel"]:nth-last-child(-n+3){border-bottom-width:var(--border-width-hairline)}
 [data-component="cardGrid"][data-count="6"] > [data-form="panel"]:last-child{border-bottom-width:0}
}

/* ---- patterns/cardGrid.skin ---- */
/* the skeleton's paint: the card ground on the plate and the lines, pulsing */
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"] [data-component="media"]{background-color:var(--colour-border-default)}
[aria-busy="true"] [data-component="cardGrid"] > [data-component="mediaCard"]::after{
 background-image:linear-gradient(var(--colour-border-default),var(--colour-border-default)),linear-gradient(var(--colour-border-default),var(--colour-border-default));
 background-size:var(--ratio-40) var(--space-step-2),var(--ratio-85) var(--space-step-6);background-position:0 0,0 var(--space-step-4);background-repeat:no-repeat}
@keyframes skeleton{from{opacity:1}to{opacity:.45}}
/* the ledger's closing rule and the board's edge */
[data-component="cardGrid"][data-cols="1"]:has(> [data-form="row"]),
[data-component="cardGrid"][data-count="6"]:has(> [data-form="panel"]){border-color:var(--colour-border-default)}

/* ---- components/formPanel.form ---- */
/* panels: the form takes 28px of padding at 810 wide, the rail cards 20px at 330, so the inner
   margin scales with the panel rather than every box sharing one cramped 15px */
/* 32px inside the edge, not 48 (owner, 2026-09-23): the form read as a box with a margin round it */
[data-component="formPanel"]{border-width:var(--border-width-hairline);border-style:solid;padding:var(--space-step-4)}
/* a mark leads each panel heading: the icon sits on the heading's own line, sized off its type */
/* THE HEAD IS THE CALL BOOKER'S (owner, 2026-09-23): the mark and the h5 heading on one line, 8px
   apart and flex-centred, with no optical drop (see callBooker: the drop sat the wave 2.88px under
   the capitals); the line under it is the text atom at the citation grade, 8px below. */
[data-component="formPanel"] .cap{display:flex;align-items:center;gap:var(--space-step-1)}
[data-component="formPanel"] .cap [data-component="headline"]{margin:0}
[data-component="formPanel"] .intro{font:var(--type-citation-font);margin:var(--space-step-1) 0 var(--space-step-6);max-width:var(--form-panel-measure)}
[data-component="formPanel"] .grid2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--space-gutter)}
/* SMALLER, TIGHTER (owner, 2026-09-23): the field label is the label register one step down, at
   the meta size, 8px above its box; fields stand 24px apart rather than 40. */
/* each field's name is the label atom, which sets its face, tracking and capitals; the panel keeps
   the meta size the owner chose for it (2026-09-23) and its 8px to the field */
[data-component="formPanel"] .f [data-component="label"]{font-size:var(--type-meta-size);margin-bottom:var(--space-step-1)}
[data-component="formPanel"] .f{margin-bottom:var(--space-step-3)}
[data-component="formPanel"] textarea{min-height:var(--form-panel-min);resize:vertical}
/* THE SEND BUTTON IS THE PRIMARY BUTTON ATOM (owner, 2026-09-23): the panel only places it. */
[data-component="formPanel"] .send{margin-top:var(--space-step-1)}
/* ---- components ----------------------------------------------------------------------------
   Three rules govern everything below, and they are the headless contract rather than taste.

   1. A component owns its appearance. Nothing here is scoped to an element or a page, so where a
      component sits cannot change how it looks. The rules above this block used to read
      `footer input` and `footer .send`, which made placement decide styling, and the cost was two
      verbatim forks: .nsub on /noods and .cform on /contact, both copying declarations they could
      not reach. The footer keeps placement only: which columns it spans and how it aligns.
   2. A second placement is a variant, never a copy. `[data-variant]` carries it.
   3. The selector is the seam. `data-component` is what becomes a React component at the port, so
      a page styling one is visibly wrong in review, where a page styling `.news` was not.
   ------------------------------------------------------------------------------------------- */
/* formPanel: the contact form. One implementation for the footer on every route and for the
   promoted panel on /contact. */
/* an empty status occupies nothing: the region exists for announcements, and reserving a line
   for it moved the panel 18px taller on all fifteen routes */
[data-component="formPanel"] .status:empty{display:none}
[data-component="formPanel"] .status{margin-top:var(--space-step-4);font:var(--type-meta-font);}

/* §5.1: "submit - 3px border, mono uppercase, chip fill on hover and on the pressed state." The
   structural weight, because a submit is the one control that commits. It carried 1px, which is
   the hairline weight - the one for separating repeating peers. */
/* THE CONSENT LINE under the submit (AKA-113): the contact form in HubSpot requires consent to
   process, and this is it, stated. The same grade and measure as the intro above the fields, one
   step under the button. */
/* the consent line reads at the citation grade across the form's full width, as the fields do (owner, 2026-09-23) */
[data-component="formPanel"] .consent{font:var(--type-citation-font);margin:var(--space-step-2) 0 0}
/* A FORM READS AT THE PROSE MEASURE wherever it stands. In the footer its column is already
   narrower; placed in a page's own band, as /contact places it, it ran the full 1,410px. */
[data-component="formPanel"]{max-width:var(--grid-column-7)}
/* PLACED UNDER ITS SECTION'S OWN HEADING, the form drops its own: /contact's band already says "Send a
   message" and the line under it, and the panel said both again one step down. */
[data-component="sectionIntro"] ~ [data-component="formPanel"] > :is(.cap,.intro){display:none}
/* BELOW 760 THE NAMES STACK, on the same 24px every field keeps (the grid's 30px gutter added to
   the field's own 24 set first and last name further apart than any other pair), AND THE SUBMIT
   TAKES THE ROW: a field column and a 130px button left
   the rest of the line empty under a full-width form. */
@media (max-width:760px){
 [data-component="formPanel"] .grid2{grid-template-columns:minmax(0,1fr);row-gap:0}
 [data-component="formPanel"] .send{inline-size:var(--ratio-100);justify-content:center}
}

/* ---- components/formPanel.skin ---- */
[data-component="formPanel"]{border-color:var(--colour-border-default)}
[data-component="formPanel"] .intro{color:var(--colour-text-tertiary)}
[data-component="formPanel"] label{color:var(--colour-text-tertiary)}
[data-component="formPanel"] select:invalid,[data-component="formPanel"] select[data-empty]{color:var(--colour-text-tertiary)}
[data-component="formPanel"] select option{background:var(--colour-surface-raised);color:var(--colour-text-primary)}
[data-component="formPanel"] .status{color:var(--colour-text-tertiary)}
[data-component="formPanel"] .status[data-state="error"]{color:var(--colour-feedback-warn)}
[data-component="formPanel"] .status[data-state="done"]{color:var(--colour-text-primary)}
[data-component="formPanel"] .consent{color:var(--colour-text-tertiary)}

/* ---- components/inlineForm.form ---- */
/* subscribe: the newsletter block. Default is the footer rail, plain and boxless, because the
   rail sits on the page like the meta column. */
/* THE FORM PANEL'S HEAD, MEASURE FOR MEASURE (2026-09-24). The mark is the icon atom at medium,
   8px from the title; the title is the headline atom at h5; the
   line under it is the text atom at the citation grade, 8px below and 24px above the field (owner,
   2026-09-24: the panel's 48 read as a break between the words and their form). It was a
   bare svg 16px from 24px title text over a 13px <small>: the two forms in the footer rail read
   as two different systems.

   No optical drop: the 2px the panel carried put the envelope 2.15px under the capitals and its
   foot below the baseline. Flex centring alone lands it 0.15px from the cap-height's centre. */
[data-component="inlineForm"] .cap{display:flex;align-items:center;gap:var(--space-step-1)}
[data-component="inlineForm"] .cap [data-component="headline"]{margin:0}
[data-component="inlineForm"] .intro{font:var(--type-citation-font);margin:var(--space-step-1) 0 var(--space-step-3);max-width:var(--form-panel-measure)}
/* ONE LINE, ONE HEIGHT (owner, 2026-09-24). The field and its submit share the row, both at
   `small`, 40px: the panel's send is small, and the field atom takes the same step so the pair
   stands level instead of a 40px button beside a 50px field. 8px apart, the gap between the parts
   of one item. The field takes the slack; the button keeps its own width. */
/* A FLEX ROW, NOT A GRID. Password managers put their own element beside the input after load (a
   0x0 div for LastPass's badge). In a two-column grid that element took the second cell and the
   button fell to a new line a second after the page drew. In a flex row a 0x0 child takes no room,
   and the 8px is the button's own margin rather than a gap, so an injected sibling cannot widen
   it either. */
[data-component="inlineForm"] .row{display:flex;align-items:center;gap:var(--space-step-1)}
[data-component="inlineForm"] .row [data-component="field"]{flex:1 1 auto;min-width:0}
[data-component="inlineForm"] .row [data-component="button"]{flex:none}
/* BELOW 760 THE FIELD TAKES THE WHOLE ROW and the button the row under it, the same width. Beside the button at 390 the
   access gate's field was 154px and cut its own placeholder: "Your email, or an acces". */
@media (max-width:760px){
 [data-component="inlineForm"] .row{flex-wrap:wrap}
 [data-component="inlineForm"] .row :is([data-component="field"],[data-component="button"]){flex-basis:var(--ratio-100)}
 [data-component="inlineForm"] .row [data-component="button"]{justify-content:center}
}
/* THREE COLUMNS OF TWELVE (owner, 2026-09-24). The form must hold one line in a 3-column cell,
   which is 255px at a 1141px window: the 132px labelled button left the field 115px and cut the
   placeholder to "Email addr". Under 320px of its OWN width - a container query, because the cell
   decides, not the window - the submit keeps only its arrow, a 40px square, and the word stays
   in the accessible name. The field gets 207px. */
[data-component="inlineForm"]{container-type:inline-size}
@container (max-width:320px){
 [data-component="inlineForm"] .row [data-component="button"]{width:var(--size-control-small);padding:0;justify-content:center}
 [data-component="inlineForm"] .row [data-component="button"] .say{position:absolute;width:var(--size-1);height:var(--size-1);overflow:hidden;clip-path:inset(var(--clip-visually-hidden));white-space:nowrap}
}
/* THE FIELD IS THE FIELD ATOM, untouched: it padded and sized its own input at 13px with 8px of
   inset, where the panel's fields are the atom's 17px and 16px. The note is the text atom at the
   citation grade, 16px under the button, the panel's consent line. */
[data-component="inlineForm"] .note{font:var(--type-citation-font);margin-top:var(--space-step-2)}
/* the panel variant: the same block promoted to a full-width bordered panel, copy left and form
   right, which is what /noods asks for above and below its feed */
/* THE PANEL IS A FEATURE (owner, 2026-09-25): the accent ground, no box, on the twelve columns -
   the name and its line across seven, the form across the last four, both on one top line. The
   field and its send stand at the regular control height, so the one ask on the page is big
   enough to be the one ask. */
[data-component="inlineForm"][data-form="panel"]{padding:var(--space-step-9);display:grid;
 grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--grid-gutter);align-items:end}
[data-component="inlineForm"][data-form="panel"] .copy{grid-column:1 / span 7}
[data-component="inlineForm"][data-form="panel"] .copy [data-component="headline"]{margin:0}
[data-component="inlineForm"][data-form="panel"] .copy .intro{margin:var(--space-step-2) 0 0;max-width:none;font:var(--type-body-font)}
[data-component="inlineForm"][data-form="panel"] .field{grid-column:9 / span 4;width:auto}
[data-component="inlineForm"][data-form="panel"] .row :is([data-component="field"],[data-component="button"]){block-size:var(--space-step-6)}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
@media (max-width:1100px){
 [data-component="inlineForm"][data-form="panel"]{grid-template-columns:minmax(0,1fr);row-gap:var(--space-step-4);padding:var(--space-step-6)}
 [data-component="inlineForm"][data-form="panel"] :is(.copy,.field){grid-column:1}
}

/* ---- components/inlineForm.skin ---- */
[data-component="inlineForm"] .intro{color:var(--colour-text-tertiary)}
[data-component="inlineForm"] .note{color:var(--colour-text-tertiary)}
[data-component="inlineForm"] .note[data-state="error"]{color:var(--colour-feedback-warn)}
[data-component="inlineForm"] .note[data-state="done"]{color:var(--colour-text-primary)}
/* the feature panel: ink on the accent, and the controls inverted to match */
[data-component="inlineForm"][data-form="panel"]{background-color:var(--colour-accent-base);color:var(--colour-ink-900)}
[data-component="inlineForm"][data-form="panel"] :is(.intro,.note,[data-component="headline"]){color:var(--colour-ink-900)}
[data-component="inlineForm"][data-form="panel"] [data-component="field"]{background-color:transparent;color:var(--colour-ink-900);border-color:var(--colour-ink-900)}
[data-component="inlineForm"][data-form="panel"] [data-component="field"]::placeholder{color:var(--colour-ink-a62)}
[data-component="inlineForm"][data-form="panel"] [data-component="field"]:focus{background-color:transparent;border-color:var(--colour-ink-900)}
/* the send inverts: the site's action tokens re-pointed inside the panel, so the button atom paints
   ink on the accent with its own rules, resting and swiping alike */
[data-component="inlineForm"][data-form="panel"]{--colour-action-surface:var(--colour-ink-900);--colour-action-swipe-primary:var(--colour-white-base)}
[data-component="inlineForm"][data-form="panel"] [data-component="button"][data-form="primary"]{color:var(--colour-accent-base)}
[data-component="inlineForm"][data-form="panel"] [data-component="button"][data-form="primary"]:is(:hover,:focus-visible){color:var(--colour-ink-900)}

/* ---- components/indexList.form ---- */
/* indexList: the scrollIndex's index - the list atom, unmarked, of link atoms, one per frame, each
   going to its frame's id. Its own component (owner, 2026-09-24) so scrollIndex is made of
   components: this beside a column of mediaCards. It pins while its frames scroll past, 8px
   between rows, and a row's arrow shows only while its frame is the one being read. */
[data-component="indexList"]{position:sticky;top:var(--index-pin-top)}
[data-component="indexList"] [data-component="list"]{row-gap:var(--space-step-1)}
[data-component="indexList"] [data-component="link"]{display:flex;align-items:flex-start;gap:var(--space-step-1)}
[data-component="indexList"] .icon{flex:none;width:var(--size-mark-base);height:var(--size-mark-base)}
[data-component="indexList"] .count{margin-top:var(--space-step-5)}
@media (max-width:1100px){[data-component="indexList"]{position:static}}
/* ON A TOUCH SCREEN each index entry is a 48px row */
@media (hover:none) and (pointer:coarse){
 [data-component="indexList"] [data-component="link"]{align-items:center;min-block-size:var(--size-control-small)}
}

/* ---- components/indexList.skin ---- */
/* the resting rows recede, the row being read holds the ink, and only it shows its arrow */
[data-component="indexList"] li:not(.on) [data-component="link"]{color:var(--colour-text-resting)}
[data-component="indexList"] li:not(.on) .icon{opacity:0}
[data-component="indexList"] .count{color:var(--colour-text-tertiary)}

/* ---- components/leadText.form ---- */
/* leadText: the opening paragraph on its own measure, one grade up from the body. It is the
   argument, not a caption, and the scale has a role for it - lead, 30/31.5 at weight 400. The
   first version asked for --fs-lede, which is not a token, so it rendered at body size. */
[data-component="leadText"]{padding:0 0 var(--space-step-12)}
/* THE STANDFIRST'S LEADING, LOOSENED. The lead role is 30/31.5 - ratio 1.05 - which is the
   contract's grade for a line of display type and too tight for what this actually is: four
   wrapping lines of 930px prose, where at 31.5px one line's descenders sit on the next line's
   caps. `--lead` is 36px, 8u, ratio 1.20.
   
   SCOPED HERE, not changed in the token. `--lh-lead` is also read by masthead's lede, by
   scrollIndex's item headings and by the register subline, and those three are one or two lines
   of display type where 31.5 is right. A token shared by four rules is not the place to fix one
   of them. This IS a departure from section 3 and is recorded as one. */
/* the lede starts on the body copy's left edge (owner, 2026-09-25): the narrative band sets its prose
   three columns and a gutter in, so the lede sits there too, at the body's seven-column measure */
[data-component="leadText"] p{max-width:var(--grid-column-7);margin-left:calc(var(--grid-column-3) + var(--grid-gutter));/* THE ROLE CARRIES ITS OWN LEADING. This overrode it with a spacing step - 64px on
   30px type, a ratio of 2.13 against the lede role's 1.30 - which is a stack
   value doing a leading's job. The house battery caught it on its first run. */
 font:var(--type-lede-font);}

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){[data-component="leadText"]{padding:0 0 var(--space-step-9)}[data-component="leadText"] p{margin-left:0}}
@media (max-width:760px){[data-component="leadText"]{padding:0 0 var(--space-step-6)}}

/* ---- components/leadText.skin ---- */
[data-component="leadText"] p{color:var(--colour-text-primary)}

/* ---- atoms/link.form ---- */
/* link: the atom a reader clicks. Its ARROW and its transitions - never its display.
   
   WHAT AN ATOM CAN OWN IS DECIDED BY MEASUREMENT, not by preference. Across seven routes the 141
   marked links render as: 128 flex rows inside linkList, 9 flex inside scrollIndex, 8 flex with an
   arrow inside ctaRow, 3 grid inside rankList, 2 INLINE inside prose and 1 as `display:contents`
   inside a headline. So `display` is the PATTERN's: a link in a sentence is inline and a link in a
   list is a row, and an atom that forced one of those would break the other. The contract draws
   the same line - a component is named for its form, and a link's form is "a thing you click",
   not "a row".
   
   What IS the atom's, in every one of those six contexts: the arrow, its size, and the movement it
   makes on hover. Those were written three times, in ctaRow, linkPanel and linkList, with the same
   values and the same 10px travel - which is the duplication that makes a fourth pattern wanting
   an arrow paste a fifth copy. */
[data-component="link"] .go{transition:transform var(--motion-duration-350) var(--motion-ease-settle)}
[data-component="link"] .go svg{width:var(--size-mark-base);height:var(--size-mark-base);display:block;flex:none}
[data-component="link"]:hover .go{transform:translateX(var(--motion-travel-base))}

@media (prefers-reduced-motion:reduce){
 [data-component="link"] .go,[data-component="link"]:hover .go{transition:none;transform:none}
}

/* the colour change on hover is the whole affordance, and it was written in ctaRow and linkList
   separately at two durations - .35s and .25s - one atom answering at two speeds depending on
   which list it sat in. A transition is form, which the gate is right about. */
[data-component="link"]{transition:color var(--motion-duration-250) var(--motion-ease-settle)}

/* THE LINK'S TWO LINES ARE ITS OWN ANATOMY. A link of this kind carries a lead line and a line
   of support - "Drop" over "Design to code in minutes, inside your own network" - and NOTHING in
   the atom said so: the stacking came from `[data-component="rankList"] .lead` and from ctaRow's
   own rules, and the supporting line had no size at all, so it fell to the browser's `small`
   default of 0.83em. Drawn on its own the pair ran together on one line at two sizes nobody
   chose - 16px against 13.33px - which is what the owner saw and named exactly: one font size
   for "Drop" and another for the rest.
   
   THE STACK AND THE SUPPORT ARE THE ATOM'S; THE LEAD'S SIZE IS THE PATTERN'S. Measured across
   eight routes, every instance stacks - so `display:block` is safe on the atom - but the lead
   renders at 104px in the home page's rank list, 32px in its smaller sibling and 36px in a
   ctaRow, because how loud the lead is depends on what the pattern is for. The atom sets the
   title grade as its floor, which every pattern rule outranks by specificity, and the support
   line takes the small-body grade because a supporting line is body copy, quieter. */
/* THE ROW'S TITLE IS AT h2, which is what every live row draws: ctaRow set it there itself, and the
   atom's own default - the title role, 24px - was a size no page showed, so the library drew the
   row at a size the site never had. Lowered with :where so a pattern that arranges the row its own
   way (rankList's interpolated sizes) still wins without having to out-rank it. */
[data-component="link"] .lead{display:block}
:where([data-component="link"][data-form="row"]) .lead{font:var(--type-h2-font);letter-spacing:var(--type-h2-tracking)}
[data-component="link"] small{display:block;font-size:var(--type-citation-size);line-height:var(--type-citation-leading)}

/* ---- atoms/link.skin ---- */
/* link: what a link's two lines are painted.
   
   THE SUPPORT LINE IS NOT TYPE COLOUR. Section 2 is binary plus a fill - a field, a type colour
   and a chip fill - and the quieter of two stacked lines takes the muted role, not the type role.
   Every pattern already did this: rankList and ctaRow both paint the support line `--dim`, so on
   a route it reads correctly and NOTHING in the atom said so. Lifted out of its pattern the
   support line came out at `--fg`, the same white as the lead above it, and the pair read as two
   equal lines of a sentence broken in half.
   
   `--dim` rather than `--muted`: it is what both patterns use, so the atom's floor is what the
   site actually renders and no instance shifts. --muted is body copy; --dim is the quieter still
   of a label or a support line under something louder. */
[data-component="link"] small{color:var(--colour-text-tertiary)}

/* THE HOVER IS THE ACCENT, and it is the atom's. A link under the cursor goes yellow: that is
   the one place the page spends the accent on words rather than on a ground, and it is the same
   answer in a sentence, a row and an index. It was not written anywhere - ctaRow and letterGroup
   each took their link to white, which is the colour it already is in most of its settings, so
   the affordance was a transition to nothing. Both of those rules are deleted: an atom that
   owns the arrow, the travel and the transition owns the colour they are for. */
/* TWO SELECTORS, BECAUSE A PATTERN PAINTS THE RESTING LINK. A link's resting colour is its
   setting's - ctaRow's rows are drawn at the border grade, letterGroup's index and the footer's
   legal row at the secondary - and each of those is written as `[data-component="X"] a`, which
   is (0,2,1) against this rule's (0,2,0). The base rule beat the hover, so the affordance
   measured as no change at all on every route: a cursor over a link and nothing happening.
   The second selector is the same statement at (0,3,0) - a link inside anything, hovered - so
   the atom's answer outranks the settings' resting colours rather than depending on file order. */
[data-component="link"]:hover,[data-component] [data-component="link"]:hover{
 color:var(--colour-action-text-hover)}

/* ---- components/linkPanel.form ---- */
/* linkPanel: a specimen sheet as a panel rather than a card - two up, the title at section grade,
   the sheet's own line under it, and the arrow pinned to the bottom so two panels of unequal prose
   still line up. Nine rules inside thinking.html under `.spec`, which is why /thinking lost the
   most when its page CSS went: the two panels ARE that route's middle. */
[data-component="linkPanel"]{display:flex;flex-direction:column;border-width:var(--border-width-hairline);border-style:solid;padding:var(--space-step-6);transition:border-color var(--motion-duration-350) var(--motion-ease-settle)}
/* THE LINK ROW'S SPACING (owner, 2026-09-24). The link atom's row stacks its title, its line and
   its arrow flush, each on the line box of the one above, and the panel now does the same: title
   to line 0, line to arrow 0. The row has no label; the one here follows the item rule, its own
   32px dropped and 8px to the title. The arrow keeps margin-top:auto, so panels of unequal prose
   still end on one line. It was 40px, 8px and 48px. */
[data-component="linkPanel"] [data-component="label"]{margin-bottom:0}
[data-component="linkPanel"] h3{margin-top:var(--space-step-1)}
[data-component="linkPanel"] p{margin-top:0;font:var(--type-citation-font)}
[data-component="linkPanel"] .go{margin-top:auto;transition:transform var(--motion-duration-350) var(--motion-ease-settle)}

/* ---- components/linkPanel.skin ---- */
[data-component="linkPanel"]{border-color:var(--colour-border-default)}
[data-component="linkPanel"]:hover{border-color:var(--colour-text-primary)}
[data-component="linkPanel"] p{color:var(--colour-text-secondary)}

/* ---- patterns/markedList.form ---- */
/* markedList: a list whose rows are LED BY A MARK, and the rhythm between them.

   Named for its form. `serviceLadder` would have named its content, and the same shape carries
   anything with a mark, a lead and a set of parts under it.

   WHAT IS LEFT HERE IS THE RHYTHM. The row's own twelve-column anatomy went to markedRow, which
   is the component this pattern is made of: the list had held every one of its rows' internals -
   the mark's column, the lead's column, the sublist, the type grades - so the thing an editor
   adds a family to was a `.row` class and the tier table said this pattern composed atoms. A
   separator and a padding between rows IS the list's decision; everything inside a row is not. */
[data-component="markedList"] [data-component="markedRow"]{border-top:var(--border-width-hairline) solid;padding:var(--space-step-9) 0}

/* ---- patterns/markedList.skin ---- */
[data-component="markedList"] [data-component="markedRow"]{border-top-color:var(--colour-border-default)}

/* ---- components/markedRow.form ---- */
/* markedRow: one row of a markedList - a mark, a lead and the parts under it, on twelve columns.

   The mark in columns 1 to 2, the lead in 3 to 7, the row's own sublist in 8 to 12, which is the
   shape the 2026 services page ran as `.pkg`, one row per service family. The port rendered it as
   a flat list of links, so seven families with a mark, a standfirst, a duration and four modules
   each collapsed into seven lines of text.

   ONE COLUMN BELOW 1100, and the breakpoints are the row's own: a twelve-column row is a row
   only while there are twelve columns to put it on. */
[data-component="markedRow"]{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
 gap:var(--space-gutter)}
/* THE FIRST TWO COLUMNS HOLD A SQUARE (owner, 2026-09-24), the family's picture. Until a family
   has one, its shape stands in, filling the cell's width at 1:1: shapes.js draws to the element's
   own box, so the solid scales with the square. It stood at a fixed 180px in a 210px cell. */
[data-component="markedRow"] .code{grid-column:1 / span 2;align-self:start}
[data-component="markedRow"] .code > [data-component="shape"]{width:100%;height:auto;aspect-ratio:1/1}
/* 2 / 6 / 4 (owner, 2026-09-24): the square, the text block, the parts with their durations. It
   was 2 / 5 / 5. */
[data-component="markedRow"] .lead{grid-column:3 / span 6}
/* THREE PARTS, 8px APART (owner, 2026-09-24): the title, the text block and the duration, each
   separated by the part gap. Inside the text block its two lines stay flush, on one another's
   line boxes. Both lines are the citation grade, as linkPanel's line is. */
[data-component="markedRow"] .stand{margin-top:var(--space-step-1);font:var(--type-citation-font);
 }
[data-component="markedRow"] .hook{margin:0;font:var(--type-citation-font)}
[data-component="markedRow"] .dur{display:inline-block;margin-top:var(--space-step-1);
 font:var(--type-label-font);letter-spacing:var(--type-label-tracking);
 text-transform:uppercase}
[data-component="markedRow"] ul{grid-column:9 / span 4;align-self:start}
/* THE PARTS ARE THE LIST ATOM'S ROWS: its 8px row gap and its type, as letterIndex's entries are.
   The row only sets its value at the far end of the line, in the same grade as the part's name.
   It drew hairlines and 24px of padding between rows, and the value in the 14px label grade. */
/* THE NAME KEEPS THREE FIFTHS OF THE ROW and the value wraps inside the rest, right-aligned. On
   `1fr auto` a long value - "Scoped to the brand count" - took its whole width first and set
   "Multi-Brand Scale" three lines deep at 390. */
[data-component="markedRow"] li{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:var(--space-gutter);align-items:baseline}
[data-component="markedRow"] li .d{text-align:right}
/* the whole row is one link target, so the anchor lays out as its children rather than as a box */
[data-component="markedRow"] .lead [data-component="headline"] a{display:contents}
/* THE ROW'S WAY IN (owner, 2026-09-26): a tertiary button under the duration, on its own line,
   the part gap above it like every other part of the text block */
/* flex, not the atom's inline box: the duration is inline-block, so an inline button sat on its
   line beside it rather than under it */
[data-component="markedRow"] .lead > .go{display:flex;margin-top:var(--space-step-2);width:fit-content}
@media (max-width:1100px){
 [data-component="markedRow"]{grid-template-columns:1fr}
 [data-component="markedRow"] .code,[data-component="markedRow"] .lead,
 [data-component="markedRow"] ul{grid-column:1}
 [data-component="markedRow"] .code > [data-component="shape"]{width:var(--size-plate-row);height:var(--size-plate-row)}
 [data-component="markedRow"] .code{margin-bottom:var(--space-step-4)}
 [data-component="markedRow"] ul{margin-top:var(--space-step-6)}
}
[data-component="markedRow"] [data-component="label"]{margin-bottom:0}
/* COMPACT (owner, 2026-09-28): a list that sits in a page's body column rather than across the
   grid - a family's own modules under its prose. No mark and no sublist, so the lead takes the
   whole column, and the rows breathe at the body's pace rather than the section's. */
[data-component="markedList"][data-variant="compact"] [data-component="markedRow"]{grid-template-columns:1fr;padding:var(--space-step-6) 0}
[data-component="markedList"][data-variant="compact"] [data-component="markedRow"] .lead{grid-column:1}

/* ---- components/markedRow.skin ---- */
[data-component="markedRow"] .stand{color:var(--colour-text-secondary)}
[data-component="markedRow"] .dur{color:var(--colour-text-tertiary)}
[data-component="markedRow"] li{color:var(--colour-text-secondary)}
[data-component="markedRow"] li:hover{color:var(--colour-text-primary)}
[data-component="markedRow"] li .d{color:var(--colour-text-tertiary)}

/* ---- atoms/media.form ---- */
/* media: ONE ATOM for every plate, whatever it holds.

   `data-kind` is none, placeholder, image, video or embed (contract.MEDIA_KINDS). One atom rather
   than five components because the plate is the same thing every time - the frame, the ratio,
   the way it sits in a column - and only its content differs. Five components would have been
   five copies of the plate and five places to change it, which is the duplication the knowledge
   base's own architecture entry warns component libraries into.

   494 on the built pages, 2026-09-23: 233 image, 172 embed, 81 placeholder, 8 video. `none` is
   declared and has no writer; its rule hides the plate for a card that carries no picture.

   The atom owns HOW THE CONTENT SITS IN THE PLATE. Where the plate sits on the page belongs to
   the organism around it: the hero's full-bleed image fill, which opened this file as
   `[data-component="masthead"] ... [data-component="media"] img`, is masthead.form.css's now. */
/* THE ATOM'S OWN FLOOR: media owned no unscoped rule at all - the only declaration was the hero's
   img-fill line (now in masthead.form.css), so a plate drawn anywhere else was an unclipped, statically positioned
   box with nothing to hold a ratio against. position:relative and overflow:hidden are what every
   instance of the plate already sets for itself before it picks a ratio - mediaCard's grid card,
   cardRail's rail and reel cards, and this hero's own frame all declare exactly these two
   properties - so this states the behaviour the site already renders rather than a new one.
   The RATIO NUMBER stays out of this floor on purpose. 16/10 is the site's default for an image
   plate (base.css says so), but it is not universal - embed is 3/2, reel is 3/4, this hero's own
   frame is 230/153 - and a plate here has no width to spare in every context. Setting
   aspect-ratio directly on the atom was tried and measured: a box with an explicit height and no
   width computes its width FROM the ratio, which would have narrowed the register hero's own
   sticky box away from the full width it needs and shifted the centring of a hero that has not
   even shipped a media slot yet. The number is each kind's or each pattern's own choice; holding
   it steady is the atom's. */
[data-component="media"]{position:relative;overflow:hidden}

/* THE THIRD KIND: markup that runs. An image fills the plate with a picture and a video with a
   moving one; an embed fills it with a running example, and the plate is the same thing in all
   three cases - the ratio, the ground, the rounding, the way it sits in a column.

   The plate keeps the ratio the embed asks for, so a card holding a 3:2 example is the same shape
   as a card holding a 3:2 photograph. `container-type:inline-size` is what the embeds inside are
   written against: their type and their geometry are set in cqw, so without a container they all
   resolve against the viewport and a small card draws a huge example. */
/* THE RATIO COMES OFF THE NAMED SCALE, which base.css already holds as `[data-ratio="3:2"]` and
   the schema already offers an editor. Five rules keyed on a slash form stood here and the gate
   refused them, correctly: nothing wrote those values, and a second spelling of a ratio the
   register has already named is a lookup table somebody maintains. Only the fallback and the
   container are the atom's. */
[data-component="media"][data-kind="embed"]{container-type:inline-size}
[data-component="media"][data-kind="embed"] > [data-embed]{position:absolute;inset:0}
/* AND THE EXAMPLE FILLS THE PLATE. An embed's own stage carries the ratio its demo was drawn
   against, and 26 of the 172 were drawn at 16:9 inside a plate the card holds at 3:2 - 47px of
   empty plate under the example, on a quarter of the cards, in a grid whose whole point is that
   every card is the same shape. The plate owns the shape and the example is written in container
   units, so it takes the space rather than letterboxing inside it. */
[data-component="media"][data-kind="embed"] > [data-embed],
[data-component="media"][data-kind="embed"] .stage{width:100%;height:100%;aspect-ratio:auto;
 max-height:none}

/* NO OUTLINES, APART FROM PLACEHOLDER AND CODE EMBED (owner, 2026-09-24). A picture, a film and a
   rail carry their own edge; a placeholder and a running example are drawn fields that would
   otherwise bleed into the page, so those two keep the hairline. */
[data-component="media"]:is([data-kind="placeholder"],[data-kind="embed"]){border-width:var(--border-width-hairline);
 border-style:solid}

/* AND THE TWO KINDS THE MARKUP DREW WITHOUT A NAME. `placeholder` is the stated empty state the
   thinking and noods cards have always used - the plate, holding the card's index, where a
   document carries no picture. It was written as `data-kind="image"` with a number inside it,
   which claims a picture that is not there and leaves the empty state unnameable in the Studio.
   `none` is a card with no plate at all: a title, its line and its foot. */
[data-component="media"][data-kind="placeholder"]{display:flex;align-items:flex-start;justify-content:flex-start}
[data-component="media"][data-kind="none"]{display:none}

/* AN EMPTY PLATE STILL HAS A SHAPE. A stage with nothing in it collapsed to the height of its own
   hairline - two pixels - while the label it was holding stood 64px tall and hung 63 of them
   outside the frame. A placeholder exists precisely so that the absence of a picture still
   occupies the space a picture would, so the plate keeps its ratio and centres what it holds. */
[data-component="media"].stage.empty:not([data-ratio]),
[data-component="media"] .stage.empty{aspect-ratio:16/10;min-height:var(--space-step-12);
 display:flex;align-items:center;justify-content:center}

/* A KIND HAS A DEFAULT SHAPE, and until now only `embed` did. The atom deliberately holds no
   single ratio - 16/10 is the image default, the reel is 3/4, the register hero's frame is
   230/153 - but "no ratio on the atom" was read as "no ratio anywhere", so a plate with nothing
   in it collapsed onto its own hairline: measured on the library page, placeholder and video
   drew 28px tall and image drew 2. A plate exists to hold a shape whether or not it has content
   yet, which is the whole argument for the placeholder kind.

   `:not([data-ratio])` is what keeps this a DEFAULT rather than a ruling. A named ratio in
   base.css is one attribute, (0,1,0), and this is two, (0,2,0) - so without the guard the atom
   would out-specify every ratio a pattern or an editor has chosen, the reel's three-by-four and
   the register hero's own frame among them. Named on the plate, the plate's number wins;
   silent, the kind's default applies.

   (And the ratio value is spelled in prose here rather than as a selector, because the dead-rule
   gate reads this file for selectors and a quoted one inside a comment reads to it as a rule
   nothing writes. It said so on the first run.) */
[data-component="media"][data-kind="image"]:not([data-ratio]),
[data-component="media"][data-kind="placeholder"]:not([data-ratio]),
[data-component="media"][data-kind="embed"]:not([data-ratio]){aspect-ratio:3/2}
[data-component="media"][data-kind="video"]:not([data-ratio]){aspect-ratio:16/9}

/* AND THE CONTENT FILLS THE PLATE IT IS GIVEN. A ratio on the figure does nothing for a picture
   inside a `.rnd` that has a height of its own, so the box held the shape and the image sat in
   the top of it. */
[data-component="media"][data-kind="image"] > img,
[data-component="media"][data-kind="image"] > .rnd,
[data-component="media"][data-kind="image"] > .rnd > img{width:100%;height:100%;object-fit:cover;display:block}

/* THE EMPTY PLATE'S FIELD IS A LAYER OF ITS OWN. The aurora colours are the skin's; the box
   that holds them is this file's, which is the split the gate enforces - a `content`, a
   `position` and an `inset` are form wherever they appear. */
[data-component="media"][data-kind="placeholder"]:not(img)::after,
[data-component="media"] .stage.empty::after,
[data-component="media"] .rnd.empty::after{content:"";position:absolute;inset:0;pointer-events:none}
[data-component="media"] .stage.empty,[data-component="media"] .rnd.empty{position:relative}

/* A STAGE FILLS THE PLATE. The plate holds the ratio and the box inside it held its own content
   height, so an empty stage was 0 to 26px tall inside a 628px plate - which is why the field
   painted on it could not be seen on four of the five kinds: it was there, in a strip under the
   top edge. The plate is the shape; whatever sits directly in it takes that shape. */
[data-component="media"] > .rnd,[data-component="media"] > .stage{width:100%;height:100%}

/* THE EMBEDS SPEAK THE OLD REGISTER. Every specimen on /thinking/motion and /thinking/data-viz is
   foreign code held in the dataset (`codeEmbed.html`), written against the short names the
   register carried before 82c8d3ba renamed them (2026-09-21): `--bw`, `--mono`, `--spec-size-3`.
   The rename reached every stylesheet and no embed, so 172 plates drew boxes with no width and
   animations with no duration - `var()` on an undeclared name is invalid at computed time and
   the declaration falls away silently. The old names are declared HERE, on the plate only, each
   as the token that replaced it: the embed keeps its vocabulary, the register keeps one source,
   and nothing outside an embed can reach them. `--r` and `--r-sm` are NOT restored: corners went
   square on purpose in 052a012b, and an undeclared radius is the square corner that asked for. */
[data-component="media"][data-kind="embed"]{--bw:var(--border-width-hairline);--mono:var(--type-family-mono);--display:var(--type-family-display);--font:var(--type-family-text);--pad:var(--grid-unit-2);--gutter:var(--space-gutter);--container:var(--grid-container-width);--sheet-cols:var(--grid-specimen-columns);--dur:var(--motion-duration-2400)}
[data-component="media"][data-kind="embed"]{--spec-dash-1:var(--specimen-dash-1);--spec-dash-2:var(--specimen-dash-2);--spec-dash-3:var(--specimen-dash-3);--spec-dash-4:var(--specimen-dash-4);--spec-px-1:var(--specimen-px-1);--spec-px-2:var(--specimen-px-2);--spec-px-3:var(--specimen-px-3);--spec-px-5:var(--specimen-px-5);--spec-px-6:var(--specimen-px-6);--spec-px-7:var(--specimen-px-7);--spec-px-8:var(--specimen-px-8);--spec-px-10:var(--specimen-px-10);--spec-size-1:var(--specimen-size-1);--spec-size-2:var(--specimen-size-2);--spec-size-3:var(--specimen-size-3);--spec-size-4:var(--specimen-size-4);--spec-size-5:var(--specimen-size-5);--spec-size-6:var(--specimen-size-6);--spec-size-7:var(--specimen-size-7);--spec-size-8:var(--specimen-size-8);--spec-size-9:var(--specimen-size-9);--spec-size-10:var(--specimen-size-10);--spec-stroke-1:var(--specimen-stroke-1);--spec-stroke-2:var(--specimen-stroke-2);--spec-stroke-3:var(--specimen-stroke-3);--spec-stroke-4:var(--specimen-stroke-4);--spec-stroke-5:var(--specimen-stroke-5);--spec-stroke-6:var(--specimen-stroke-6);--spec-stroke-7:var(--specimen-stroke-7);--spec-stroke-8:var(--specimen-stroke-8);--spec-tight-1:var(--specimen-tight-1);--spec-tight-2:var(--specimen-tight-2);--spec-track-1:var(--specimen-track-1);--spec-track-2:var(--specimen-track-2);--spec-track-3:var(--specimen-track-3);--spec-track-4:var(--specimen-track-4);--spec-track-5:var(--specimen-track-5);--spec-veil-1:var(--specimen-veil-1);--spec-veil-2:var(--specimen-veil-2);--spec-veil-3:var(--specimen-veil-3);--spec-veil-4:var(--specimen-veil-4);--spec-veil-5:var(--specimen-veil-5);--spec-veil-6:var(--specimen-veil-6);--spec-veil-7:var(--specimen-veil-7)}
/* THE RATIO SCALE, the three shapes the six settings take: 3:2 for a horizontal image and a code
   embed, 16:9 for video, 2:3 for a vertical image, 3:4 for a rail. Moved from base.css (2026-09-24):
   the ratio is the plate's, and nothing but the media atom carries one. */
[data-component="media"][data-ratio="3:2"]{aspect-ratio:3/2}
[data-component="media"][data-ratio="16:9"]{aspect-ratio:16/9}
[data-component="media"][data-ratio="2:3"]{aspect-ratio:2/3}
/* a rail stands 3:4 at the card's full four columns (owner, 2026-09-25): at 2:3 and three it read too narrow */
[data-component="media"][data-ratio="3:4"]{aspect-ratio:3/4}
/* ORIGINAL: the picture's own shape, uncropped, for a slide or a diagram whose edges carry words */
[data-component="media"][data-ratio="original"]{aspect-ratio:auto}
[data-component="media"][data-ratio="original"] > .rnd > img{height:auto}
/* EVERYTHING VERTICAL IS THREE COLUMNS WIDE AT MOST (owner, 2026-09-24): a 2:3 plate, vertical
   image or rail, stands no wider than three grid columns, wherever it is placed */
[data-component="media"][data-ratio="2:3"]{max-width:var(--grid-column-3)}
/* a video fills its plate, as a picture does */
[data-component="media"][data-kind="video"] > video{display:block;width:100%;height:100%;object-fit:cover}
/* RAIL (owner, 2026-09-24): 3:4, a muted looping video as the ground and, floating in its middle, a
   panel at the live site's 450:260 and 79.1% of the plate's width, through which the project's
   screens cut, no fade, in bursts of three 300ms apart then a 3s hold (reel.js). The atom's own
   anatomy, so it is whole wherever it is placed, in a card or on its own. */
[data-component="media"][data-kind="rail"]{position:relative;display:flex;align-items:center;justify-content:center}
[data-component="media"][data-kind="rail"] > video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0}
[data-component="media"][data-kind="rail"] > .panel{position:relative;z-index:1;width:var(--ratio-79-1);aspect-ratio:16/9;overflow:hidden}
/* THE SCREENS ARE SHOWN WHOLE (owner, 2026-09-25): 16:9 is what the case studies' screens are
   (most measure 1.78), so they fill the panel exactly. One of another shape still fills it, but
   pinned to its left edge rather than cropped from the centre, because a screen reads from the
   left and that is where its words start. At 3:2 and centred, every screen lost a sixth of its
   width, half of it off the left. */
[data-component="media"][data-kind="rail"] > .panel img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:left center}
/* A SLIDESHOW PLATE (owner, 2026-09-25): its images stacked in the one frame, the reel showing one
   at a time. The frame keeps its own box; every image fills it and crops, never stretches. */
[data-component="media"][data-slides]{position:relative}
[data-component="media"][data-slides] > img{position:absolute;inset:0;width:var(--ratio-100);height:var(--ratio-100);object-fit:cover}
/* ON A TOUCH SCREEN A DEMO'S CONTROLS TAKE A THUMB (owner, 2026-09-27). The sixteen embeds with
   buttons draw them 20 to 22px tall as part of the demo, and resizing them would redraw the demo.
   Each keeps its drawn size and gains an invisible target 12px taller above and below - 46px in all -
   and 2px wider, so neighbours in a row do not overlap; none of the
   embeds positions its buttons or draws on their pseudo-elements, so this adds reach and nothing
   else. */
@media (hover:none) and (pointer:coarse){
 [data-component="media"][data-kind="embed"] button{position:relative}
 [data-component="media"][data-kind="embed"] button::after{content:"";position:absolute;inset:calc(-1 * (var(--space-step-1) + var(--space-step-half))) calc(-1 * var(--space-step-quarter))}
}

/* ---- atoms/media.skin ---- */
/* THE ATOM'S OWN GROUND. This file used to say the plate paints nothing of its own and takes its
   ground from whatever places it - true only because no rule here ever gave it one, so a plate
   drawn on its own rendered with no fill at all rather than looking designed.
   var(--colour-surface-card) is not a new choice: mediaCard's own grid card and cardRail's own rail card both
   already paint their plate exactly this colour, independently of each other, which is what
   makes it the floor rather than a guess. The register hero repaints its own frame
   var(--frame-plate-cool) instead - a cooler, hero-specific tone - and that rule is more specific than
   this one, so its choice still wins there; nothing about it changes.
   `:not(img)` excludes the one instance where the atom IS the content rather than a box around
   it: nameLane tags its partner marks `data-component="media"` directly on the <img>, and those
   are transparent SVGs sitting straight on the page - painting a ground behind them would put a
   faint fill behind every logo in the strip, which is not what a mark on a page is. */
[data-component="media"]:not(img){background:var(--colour-surface-card)}

/* the hairline's colour, on the two kinds that keep one: placeholder and code embed */
[data-component="media"]:is([data-kind="placeholder"],[data-kind="embed"]){border-color:var(--colour-border-default)}

/* AN EMPTY PLATE IS NOT A GREY BOX. A placeholder is the plate standing where a picture has not
   arrived, and it was painting the card ground - the same fill as the surface behind it, so the
   absence read as a hole rather than as a frame waiting for something.

   THE AURORA'S OWN THREE COLOURS, on the plate rather than behind the page. The pattern itself
   is fixed to the viewport, animated and full-bleed, which is not a thing that can live inside a
   card; what carries the identity is the three-colour field, and those are tokens - the same
   `--aurora-colour-1/2/3` the pattern names. Three radial gradients, placed the way the
   pattern's own three spans sit, at the pattern's own opacity. Still rather than moving: a
   placeholder is the quiet state of a card, and 137 of them drifting on a grid is a page that
   will not sit still. */
/* (0,2,1) rather than (0,2,0): mediaCard paints its own plate and its sheet loads after the
   atoms, so at equal weight the card's flat ground won and this measured as `background:none`. */
[data-component="media"][data-kind="placeholder"]:not(img){
 background:var(--colour-surface-card)}
/* AT THE AURORA'S OWN OPACITY, which is the difference between the identity and a poster. The
   pattern paints its three fields through `--aurora-opacity` and a blur; drawn at full strength
   inside a card the same three colours read as a loud gradient rather than as the site's own
   light. The field goes on a pseudo-element so the opacity applies to it alone and not to the
   plate's edge. */
[data-component="media"][data-kind="placeholder"]:not(img)::after,
[data-component="media"] .stage.empty::after,
[data-component="media"] .rnd.empty::after{
 opacity:var(--opacity-field);
 background:
  radial-gradient(circle at var(--ratio-22) var(--ratio-30),var(--aurora-colour-1),transparent var(--ratio-62)),
  radial-gradient(circle at var(--ratio-78) var(--ratio-22),var(--aurora-colour-2),transparent var(--ratio-64)),
  radial-gradient(circle at var(--ratio-64) var(--ratio-78),var(--aurora-colour-3),transparent var(--ratio-66))}

/* the embeds' old colour names, as the tokens that replaced them. The reason is in media.form.css.
   The ten canvas embeds read theirs through getComputedStyle, off the plate rather than the root
   since 2026-09-24, under the even older short names: --mut, --acc, --acc2 to --acc4. */
[data-component="media"][data-kind="embed"]{--ink:var(--colour-text-specimen);--line:var(--colour-border-default);--line2:var(--colour-border-strong);--dim:var(--colour-text-tertiary);--muted:var(--colour-text-secondary);--accent:var(--colour-accent-base);--accent-2:var(--colour-accent-2);--accent-3:var(--colour-accent-3);--accent-4:var(--colour-accent-4);--card:var(--colour-surface-card);--bg:var(--colour-surface-page);--stage:var(--colour-surface-stage);--plate:var(--colour-surface-plate);--plate-ink:var(--frame-plate-ink);--shine:var(--colour-accent-shine);--grid:var(--colour-border-grid);--warn:var(--colour-feedback-warn);--chipbg:var(--chip-background);--barbg:var(--bar-site-background);--fg:var(--colour-text-primary);--mut:var(--colour-text-secondary);--acc:var(--colour-accent-base);--acc2:var(--colour-accent-2);--acc3:var(--colour-accent-3);--acc4:var(--colour-accent-4)}
/* the rail: the plate's ground before the film loads, the floating panel's own ground, and the
   slideshow - every slide stacked and hidden, the live one shown (reel.js sets .on) */
[data-component="media"][data-kind="rail"]{background:var(--reel-background)}
[data-component="media"][data-kind="rail"] > .panel{background:var(--colour-surface-card)}
[data-component="media"][data-kind="rail"] > .panel img{opacity:0}
[data-component="media"][data-kind="rail"] > .panel img.on{opacity:1}
/* the slideshow shows the frame the reel has marked; the first stands until the script runs */
[data-component="media"][data-slides] > img{opacity:0}
[data-component="media"][data-slides] > img.on,[data-component="media"][data-slides] > img:first-child:not(.on ~ img):not([class]){opacity:1}
/* A PICTURE THAT FAILS TO LOAD SHOWS ITS PLATE, not its alt text as body copy across the frame. The
   words stay for a screen reader; only their paint goes. */
[data-component="media"] img{color:transparent}

/* ---- atoms/headline.form ---- */
/* headline: ONE ATOM for every heading on the site, at any level.

   `data-level` is not a field - the LEVEL IS THE ELEMENT, h1 to h6, because an outline a screen
   reader walks cannot be an attribute. `data-size` is the scale, and the two are separate because
   they answer to different people: the level is the document's structure and the size is the
   design's. Collapsing them is how a heading becomes an h3 because 28px looked right.

   The sizes are named for PURPOSE rather than appearance - display, section, subsection, minor,
   label - which the knowledge base's design-systems-architecture entry is explicit about:
   `colour.text.primary` over `colour.dark-grey`. The first version of this map called the h2 size
   "h2", which is the level spelled twice and breaks the moment a section heading wants the
   display grade.

   A HEADING WITH NO SIZE TAKES ITS LEVEL'S DEFAULT, from this sheet and not from the tag. 472
   headings once took their scale from the organism around them, so the defaults sit at the
   specificity a bare tag had - (0,0,1) - and every component that restyles one still wins
   exactly as it did. What moved is WHERE the default lives: base.css styled every h1 to h6 on
   the site by tag, which made the element a design decision. */
/* THE GRADE IS THE ROLE. 0003 already names the type scale: sixteen roles, each carrying size,
   leading, weight and tracking together. A headline that took its own vocabulary - display,
   section, subsection, minor - was a second set of names over the same decisions, so a component
   asking for a grade had to know both and the two could disagree. `data-size` is the role now.

   There are no aliases. `section`, `subsection` and `minor` were a second set of names over the
   same decisions, and every heading that wrote one now writes the role it always meant.

   A grade states nothing of its own: it picks a role, and the role carries everything. */
[data-component="headline"][data-size="display"]{font:var(--type-display-font);letter-spacing:var(--type-display-tracking)}
[data-component="headline"][data-size="h1"]{font:var(--type-h1-font);letter-spacing:var(--type-h1-tracking);
 text-transform:uppercase;font-variation-settings:"wdth" 92,"opsz" 96}
[data-component="headline"][data-size="h2"]{font:var(--type-h2-font);letter-spacing:var(--type-h2-tracking);
 font-variation-settings:"opsz" 72}
[data-component="headline"][data-size="h3"]{font:var(--type-h3-font);letter-spacing:var(--type-h3-tracking)}
[data-component="headline"][data-size="h4"]{font:var(--type-h4-font)}
[data-component="headline"][data-size="h5"]{font:var(--type-h5-font)}
[data-component="headline"][data-size="h6"]{font:var(--type-h6-font)}
[data-component="headline"][data-size="title"]{font:var(--type-title-font)}
[data-component="headline"][data-size="lede"]{font:var(--type-lede-font)}
/* a stage's or an item's index, 01 02 03: the big-number role, light */
[data-component="headline"][data-size="number"]{font:var(--type-big-number-light-font);letter-spacing:normal}
[data-component="headline"][data-size="label"]{font:var(--type-label-font);letter-spacing:var(--type-label-tracking)}
[data-component="headline"][data-size="meta"]{font:var(--type-meta-font)}
/* THREE GRADES THAT WERE COMPONENTS' OWN TYPE (owner, 2026-09-23: "name them as grades"). Each
   was set inside the component that used it - the hero and the page title in titleStack, the frame
   title in the scrollIndex frame - so the register could not see them and a second use would have copied the
   declarations. The declarations are the ones those components set, unchanged.
     hero   the home hero: display, in capitals, on the narrow cut, fluid from its floor
     page   a route's title: the h1 size and tracking, sentence case, full width
     frame  a frame card's title: the lede's size at the display weight, narrow cut
     statement  the home manifesto: the h1 grade in sentence case (pullQuote set the case itself) */
[data-component="headline"][data-size="hero"]{font:var(--type-display-font);
 font-size:clamp(var(--hero-title-min),8vw,var(--type-display-size));
 line-height:clamp(var(--hero-title-min),8vw,var(--type-display-leading));text-transform:uppercase;
 letter-spacing:var(--type-display-tracking);font-variation-settings:"wdth" 92,"opsz" 96}
[data-component="headline"][data-size="page"]{font:var(--type-h1-font);letter-spacing:var(--type-h1-tracking);
 text-transform:none;font-variation-settings:"wdth" 100,"opsz" 72}
[data-component="headline"][data-size="statement"]{font:var(--type-h1-font);letter-spacing:var(--type-h1-tracking);
 text-transform:none;font-variation-settings:"wdth" 92,"opsz" 96;
 /* thin at this size (owner, 2026-09-24): the light weight, as the big numbers take it */
 font-weight:var(--type-weight-light)}
[data-component="headline"][data-size="frame"]{font:var(--type-lede-font);font-weight:800;
 letter-spacing:var(--type-h3-tracking);text-transform:none;font-variation-settings:"wdth" 92,"opsz" 96}

/* A GRADE STATES EVERYTHING, SO NOTHING IS INHERITED. The grades above set a font and, for five
   of them, a tracking; the rest of a heading's voice - its tracking, its case, its variation - was
   filled in by whatever tag it was drawn on. Measured on the library: every grade turned uppercase
   on an h1 and on no other level, and `meta` at 13px took the h1's -3px. A grade picked for a
   design reason was being overruled by a structural one, which is the collapse this atom exists
   to prevent.

   Zero specificity: any explicit rule anywhere still beats it, including a component that has
   chosen a tracking for its own heading. All this does is stop inheritance - `letter-spacing` and
   `text-transform` are inherited properties, so a grade inside a tracked, uppercase container
   used to take the container's voice as well. */
:where([data-component="headline"][data-size]){letter-spacing:normal;text-transform:none;
 font-variation-settings:normal}

/* THE LEVEL DEFAULTS, moved from the bare `h1`-`h6` rules in base.css, matching only a headline
   that has asked for no grade. TRACKING IS THE GRADE'S OWN: each level takes its own role's
   tracking token, so a heading tracks the same whether it arrived by level or by grade. They
   borrowed one rung up - the h1 a literal -3px, the h2 the h1's token, the h3 the h2's - which
   is how a page title and an h1 grade came to disagree. EACH LEVEL'S DEFAULT IS NOW ITS GRADE,
   to the declaration: the h1 grade took the capitals and the narrow cut, the h2 grade the optical
   size, so an h1 is the same heading whether content asked for the level or design for the
   grade. Change one, change both. */
h1:where([data-component="headline"]:not([data-size])){font:var(--type-h1-font);
 letter-spacing:var(--type-h1-tracking);text-transform:uppercase;
 font-variation-settings:"wdth" 92,"opsz" 96}
h2:where([data-component="headline"]:not([data-size])){font:var(--type-h2-font);
 letter-spacing:var(--type-h2-tracking);font-variation-settings:"opsz" 72}
h3:where([data-component="headline"]:not([data-size])){font:var(--type-h3-font);
 letter-spacing:var(--type-h3-tracking)}
h4:where([data-component="headline"]:not([data-size])){font:var(--type-h4-font)}
h5:where([data-component="headline"]:not([data-size])){font:var(--type-h5-font)}
h6:where([data-component="headline"]:not([data-size])){font:var(--type-h6-font)}

[data-component="headline"] span[data-line]{display:block}

/* THE PER-LINE REVEAL LIVES WITH THE BLOCK, NOT THE ATOM. It was written here as
   `.hero h1.ttl [data-line]` - a page-era class pair naming the one place it happened to run -
   and it is titleStack's entrance, so it is in titleStack.form.css now. The atom keeps only what
   is true of a headline anywhere: an authored break renders one line per span.
   It also shipped with no release. Nothing anywhere set the clip back to zero, so the resting
   state was `inset(0 0 100%)` and every hero headline outside the home route was a band of
   nothing where the page title belongs. Fixed with the move: gated on `.m`, released on `.m.go`. */

/* THE FLUID DISPLAY SIZES BELONG TO THE ATOM. They were written inside titleStack, which is
   a component setting a type size - the rung below it owns that decision, and the same
   clamp was then restated for the register hero. The component still says WHICH grade it
   wants, by attribute; the atom says what that grade measures at any viewport. */

/* A HEADING BALANCES ITS LINES (2026-09-27). Measured on every page at 390, 69 headings ended on one
   short word - a case study card's "...for a division / mid-restructure", a reference entry's h2
   with its last word alone. The browser evens the lines instead, on every heading at every width,
   so no page and no component has to break one by hand. */
[data-component="headline"]{text-wrap:balance}
/* the accented phrase keeps the headline's own weight; the accent is its colour, in the skin */
[data-component="headline"] strong{font-weight:inherit}

/* ---- atoms/headline.skin ---- */
/* headline has no paint of its own: it inherits the register's foreground, which is the point -
   a headline that set its own colour would have to be told again for every surface. The pair
   exists because a component is a form and a skin, and an empty skin is a statement that the
   colour belongs upstream rather than an omission. */

/* THE ACCENTED WORD (owner, 2026-09-28): a heading may carry one phrase in the accent - "Inside
   your network.", "twenty days." - written `**like this**` in the document. It is the accent and
   nothing else: the weight and the face stay the headline's own */
[data-component="headline"] strong{color:var(--colour-accent-base)}

/* ---- atoms/icon.form ---- */
/* icon: the mark's own size, and its placement inside a stage.
   
   Three rules from /drop's page head, where they styled 20 marks on that route and nothing
   elsewhere could reach them. `.glyph` is the row's PLACEMENT of a mark and `.ic` is the mark, so
   both names survive the move - the component owns how big a mark is.
   
   FORM ONLY. `fill` is colour and colour is the skin's, which the gate is right to insist on:
   the first version of this file painted the mark here. */

/* THE WRAPPER SIZES THE MARK, NOT THE CLASS INSIDE IT. Sizing on `.ic` meant the size a mark
   drew at was whichever class its artwork happened to be imported with: the seventeen Phosphor
   marks carry `.ic` and drew at 26, the seventeen later ones carry `.icon` and drew at 20, and
   `lock`, which carries neither, drew at 847 - its viewBox against a box nothing had bounded.
   The class is in the artwork and the artwork is data, so it cannot decide presentation. A mark
   that IS the part draws at the atom's `large`, 26, which is why the default asks for that size
   by the atom's own name rather than for the mark ladder's `--size-mark-l` it resolves to.

   A MARK A CONTROL BUILDS IS NOT THIS ATOM, and is not sized here. A button's arrow is written
   as `.glyph` without `data-component="icon"` (a control is an atom, and an atom holds no other),
   and this sheet used to size it with a bare `.glyph svg` - the icon atom styling the button's
   own structure. That rule is button.form.css's now, at the same 20.

   NO `flex:none` HERE. A panelCard holds its mark in a 32px flex box and asks for 41 inside it,
   and what made that draw at 30 rather than overflow was the mark shrinking to fit. `.icon`
   carries `flex:none` in the foundation because a mark in a button row must not shrink; a mark
   in a box that sizes it has the opposite need, and one rule cannot hold both. */
[data-component="icon"] svg{width:var(--size-icon-large);height:var(--size-icon-large);display:block;
 transition:fill var(--motion-exchange-base) var(--motion-ease-settle),transform var(--motion-exchange-base) var(--motion-ease-settle)}

@media (prefers-reduced-motion: reduce){
 [data-component="icon"] svg{transition:none}
}

/* FOUR SIZES, NAMED FOR THE JOB. A mark had one size, 26, which is the size of a mark that IS the
   subject of its row; the same rule drew the 16px sign inside a line of text and the 32px figure
   on a card. `data-size` is the mark's own axis, and the default stays where it was so nothing
   already drawn moves. */
[data-component="icon"][data-size="small"] .ic,[data-component="icon"][data-size="small"] svg{
 width:var(--size-icon-small);height:var(--size-icon-small)}
[data-component="icon"][data-size="medium"] .ic,[data-component="icon"][data-size="medium"] svg{
 width:var(--size-icon-medium);height:var(--size-icon-medium)}
[data-component="icon"][data-size="large"] .ic,[data-component="icon"][data-size="large"] svg{
 width:var(--size-icon-large);height:var(--size-icon-large)}
[data-component="icon"][data-size="display"] .ic,[data-component="icon"][data-size="display"] svg{
 width:var(--size-icon-display);height:var(--size-icon-display)}

/* THE MARKS THAT MOVE. Three of the set are drawn to animate rather than to sit still, and each
   says so on itself with `data-motion` rather than being animated by whatever holds it: a mark
   that only waves inside a nav is a mark nobody can reuse.

   A WAVE TURNS ABOUT THE WRIST, not about the middle of its box, which is why the origin is the
   bottom corner the hand sits on. Turning about the centre swings the whole hand like a pendulum
   and reads as a metronome. */
[data-component="icon"][data-mark="wave"] svg{transform-origin:var(--origin-mark-wrist-x) var(--origin-mark-wrist-y);
 animation:mark-wave var(--motion-duration-2s) var(--motion-ease-settle) infinite}
@keyframes mark-wave{
 0%,60%,100%{transform:rotate(var(--angle-0))}
 70%{transform:rotate(calc(-1 * var(--angle-mark-wave)))}
 80%{transform:rotate(var(--angle-mark-wave))}
 90%{transform:rotate(calc(-1 * var(--angle-mark-wave)))}
}

/* THE VENN CLOSES AND OPENS: three circles that meet at the centre and part again, which is the
   mark's whole argument. The circles move by their own centres, so each keyframe names a cx and a
   cy rather than a translate, and an SVG geometry property animates like any other. */
[data-component="icon"][data-mark="venn"] circle{
 animation:mark-venn var(--motion-duration-2s) var(--motion-ease-settle) infinite}
@keyframes mark-venn{40%,60%{cx:8;cy:8}}

/* the two rules of a menu mark meet and cross when whatever holds it is open, and the state is
   the holder's to set, because only it knows. Each rule turns about its own middle, so the cross
   is centred on the mark rather than on the corner of its box. */
[data-component="icon"][data-mark="menu"] line{transform-box:fill-box;transform-origin:center;
 transition:transform var(--motion-exchange-base) var(--motion-ease-settle)}
[aria-expanded="true"] [data-mark="menu"] .m1{transform:translateY(var(--size-mark-travel)) rotate(var(--angle-mark-cross))}
[aria-expanded="true"] [data-mark="menu"] .m2{transform:translateY(calc(-1 * var(--size-mark-travel))) rotate(calc(-1 * var(--angle-mark-cross)))}

/* AN EYE BLINKS, on the mark itself rather than on whatever holds it. The first version closed
   once for 200ms in every five seconds, which is a 4% duty at 26px: it was running the whole
   time and could not be seen, which is the same as not running. A blink is two closes, because
   one squash reads as a glitch and a pair reads as an eye. The clock's tick was written here too
   and is gone: the set has no clock mark, so the rule was a capability nothing could ask for. */
[data-component="icon"][data-mark="eye"] svg{
 animation:mark-blink var(--motion-duration-5s) var(--motion-ease-settle) infinite;transform-origin:center}
@keyframes mark-blink{
 0%,86%,94%,100%{transform:scaleY(1)}
 89%,97%{transform:scaleY(.08)}
}

/* STILLNESS HAS TO OUTRANK THE MOTION, and it did not. The guard named `[data-motion]`, which is
   (0,2,0), while every rule above it is (0,2,1) or more - so the wave kept waving and the eye
   kept blinking for a reader who had asked the operating system for neither. The venn was worse:
   its animation is on the circles and the guard only ever reached the svg. Same shape as the
   rules it answers, plus their children, so it wins on specificity rather than on file order. */
@media (prefers-reduced-motion:reduce){
 [data-component="icon"][data-mark] svg,[data-component="icon"][data-mark] svg *{animation:none}
 [data-component="icon"][data-mark="menu"] line{transition:none}
}

/* ---- atoms/icon.skin ---- */
/* icon: what a mark is painted. The form sheet says how big a mark is; this says what colour it
   takes, and it says it once for the whole set rather than for the half of it whose artwork
   happens to carry `.ic`.

   `lock` is why. It carries no class at all, so no rule painted it and it drew in the SVG
   default, which is black - a black padlock on a near-black ground, which is a mark nobody can
   see. The other sixteen unclassed marks drew white from the foundation's `.icon` rather than
   the secondary the atom declares.

   COLOUR ON THE WRAPPER, FILL ON THE MARK. A keyline mark strokes in `currentColor` and a solid
   one fills in it, so the one thing both need is the colour - and `fill` is then only set where
   the artwork has not already said `none`. */
[data-component="icon"]{color:var(--colour-text-secondary)}
[data-component="icon"] svg:not([fill="none"]){fill:currentColor}

/* ---- atoms/subline.form ---- */
/* subline: the line under the headline. `.tag` in the register hero, `.lede` everywhere else,
   one component either way - the form is the same and the class was a leftover of which page
   wrote it first. */

/* THE ATOM'S OWN GRADE. Every rule that touched a subline was keyed on the register hero below,
   so the component owned no floor of its own: a subline drawn anywhere else would render as
   plain body text with no vertical proportion of its own at all. --fs-lead/--lh-lead/--w-lead
   is the exact value the hero's own rule already gave it, so this states the grade the site
   already renders rather than choosing a new one - the hero's rule below no longer repeats it. */
[data-component="subline"]{font-size:var(--type-lede-size);line-height:var(--type-lede-leading);font-weight:var(--type-lede-weight)}

/* ---- atoms/subline.skin ---- */
/* the subline is dimmer than the headline on purpose: it is the second thing read. */

/* THE ATOM'S OWN COLOUR. The only place a subline was ever painted was the hero's own rule
   below, so a subline drawn anywhere else inherited the page's foreground instead of reading as
   the dimmer, second thing it is meant to be. var(--colour-text-secondary) is the exact tone that rule already
   gives it, so this states the render that already exists rather than picking a new one. */
[data-component="subline"]{color:var(--colour-text-secondary)}

/* ---- components/buttonGroup.form ---- */
/* buttonGroup: two or three buttons side by side, wrapping when they will not fit.
   
   A COMPONENT NAMED "GROUP" THAT DID NOT OWN BEING A GROUP. Every one of its four instances sits
   inside a masthead, and `[data-component="masthead"] [data-component="buttonGroup"]` supplied
   the display, the gap and the wrap - so outside a masthead it was a block with no gap and its
   buttons touched edge to edge. The component inventory is where that showed: the exhibit drew
   two buttons with their borders against each other, which reads as a broken button rather than
   as a group that was never told it was one.
   
   THE GAP IS 32 AND IT IS NAMED FOR ITS JOB. It was `--space-step-4`, the primitive, named
   directly by a component - the rung the register says only the semantic tier may reach for -
   and the comment here still said 18px, which the type and space re-base made untrue. The name
   is `space.row`: the space between two controls standing side by side, far enough apart to be
   two targets and close enough to read as one group. The register masthead keeps its own tighter
   pairing below, because that is a decision about that hero. The MARGIN stays the masthead's
   too: distance from what came before is rhythm.

   AND A GROUP OF SMALL BUTTONS SITS CLOSER. `data-size="small"` on the group takes
   `space.row.tight`, 16px: a smaller control needs less air around it to read as separate, and
   the same 32px around a 15px button leaves the row looking scattered rather than grouped. */
[data-component="buttonGroup"]{display:flex;flex-wrap:wrap;gap:var(--space-step-3)}
[data-component="buttonGroup"][data-size="small"]{gap:var(--space-step-2)}
/* A PAIR STAYS ON ONE LINE ON A PHONE (owner, 2026-09-27). Two buttons at their natural widths need
   377px with the gap, so at 390 the second wrapped under the first. Below 760 a pair shares the row
   half each, label centred, with the inline padding a step down so "Start a project" still fits
   its half at 360. Three or more still wrap: a row cannot promise them the room. */
@media (max-width:760px){
 [data-component="buttonGroup"]:has(> :nth-child(2):last-child){flex-wrap:nowrap;gap:var(--space-step-1);inline-size:var(--ratio-100)}
 [data-component="buttonGroup"]:has(> :nth-child(2):last-child) > [data-component="button"]{flex:1 1 0;min-width:0;
  justify-content:center;padding-inline:var(--space-step-1)}
}

/* ---- components/buttonGroup.skin ---- */


/* ---- components/titleStack.form ---- */
/* titleStack: the block a route opens with - an eyebrow, a headline, a subline under it, and the
   row of controls beneath that. ONE component, on every route that has a title, which is what
   being headless is supposed to buy: the ladder is templates of patterns, patterns of components,
   components of atoms, atoms of tokens, and a change to the title block reaches every title.

   IT WAS ONLY HALF TRUE UNTIL NOW. The first split named the home route's title block and left the
   other ten heroes as loose atoms sitting directly in the template wrap, with masthead - a pattern
   - setting their grade, their measure and their spacing by reaching through nothing at all. So
   the same block existed twice: once as a component and once as four selectors in the pattern that
   held it. That is the fault the split was supposed to remove, still standing on ten routes out of
   eleven, and a change to the title block reached exactly one page.

   The two seams the contract already has carry the difference, so there is no second component:
     data-form="register"   the home route's anatomy: centred, hero grade, fixed 450px block
     data-size  lg | xl     the scale, which widens the measure and at xl lifts the grade
   Everything else - the eyebrow, the subline, the controls, the per-line reveal - is the same
   block on all eleven, and is written once, here. */

/* ---- THE DEFAULT: every index and detail hero ------------------------------------------------
   Block flow, because the stack IS the flow: an eyebrow, a title, a lede, a row of buttons. The
   measures are the block's decision and the grades are the block's decision, which is why they
   are here and not on the headline atom - the same argument the register's copies always made. */
[data-component="titleStack"] [data-component="headline"]{max-width:var(--grid-column-9)}
[data-component="titleStack"][data-size="lg"] [data-component="headline"]{max-width:var(--grid-column-10)}

/* THE PART GAP (owner, 2026-09-24): label, title and subline 8px apart, as every other head is.
   The label dropped its own 32px and the subline stood 48px under the title. */
[data-component="titleStack"] > [data-component="label"]{margin-bottom:var(--space-step-1)}
[data-component="titleStack"] [data-component="subline"]{margin-top:var(--space-step-1);max-width:var(--grid-column-7)}
[data-component="titleStack"][data-size="lg"] [data-component="subline"]{max-width:var(--grid-column-8)}

/* THE PER-LINE REVEAL, SPC-06: each line wipes up from its own clip, staggered by index. It read
   `.hero h1.ttl [data-line]` in headline.form.css - a page-era class pair naming the one place it
   happened to be used - and it is the BLOCK's entrance, not the atom's and not the route's.
   Gated on `.m`, which shell.js sets only when it intends to animate, so a visitor with no
   JavaScript gets the headline rather than a gap; released on `.m.go`, with the stagger intact. */
.m [data-component="titleStack"]:not([data-form="register"]) [data-component="headline"] [data-line]{display:block;clip-path:inset(0 0 100% 0);transform:translateY(.14em);
 transition:clip-path var(--motion-build-long) var(--motion-ease-settle),transform var(--motion-build-long) var(--motion-ease-settle);
 transition-delay:calc(var(--i,0) * var(--motion-duration-110))}
.m.go [data-component="titleStack"]:not([data-form="register"]) [data-component="headline"] [data-line]{clip-path:inset(0 0 0 0);transform:none}
@media (prefers-reduced-motion:reduce){
 .m [data-component="titleStack"] [data-component="headline"] [data-line],
 .m.go [data-component="titleStack"] [data-component="headline"] [data-line]{clip-path:none;transform:none;transition:none}
}

/* ---- data-form="register": the home route's front door ---------------------------------------
   A fixed 450px block on the viewport's centre line, centred and set at hero grade, because this
   is a front door rather than an index head. A wrapping flex row: the headline and the subline
   take a full line each and the buttonGroup lands on its own beneath them.

   A COMPONENT COMPOSING A COMPONENT is composition, not a tier violation. A row of buttons is a
   real thing with a real name and that name owns the row's spacing; refusing the nesting would
   force either a nameless wrapper or the group's layout pasted into every block holding two
   buttons, which is the duplication the component layer exists to prevent. */
/* THE REGISTER HERO STATES A FLOOR. Its height is the stage the title is optically centred in,
   and a fixed one clipped a title that wrapped to a third line at narrow widths. */
[data-component="titleStack"][data-form="register"]{position:relative;z-index:1;min-height:var(--grid-column-4);display:flex;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;text-align:center;max-width:var(--grid-content-width);margin:0 auto;gap:var(--space-step-4) var(--space-step-3)}
[data-component="titleStack"][data-form="register"]>[data-component="headline"],
[data-component="titleStack"][data-form="register"]>[data-component="subline"]{flex:0 0 100%}
[data-component="titleStack"][data-form="register"] [data-component="headline"]{max-width:none}
[data-component="titleStack"][data-form="register"] [data-component="subline"]{margin-top:0;max-width:none}

/* the register's lines rise out of a clip instead of wiping: home-hero.js rewrites the headline's
   own `data-lines` into `.ln > .mv` spans to do it. The clip is this block's, the words are the
   document's. Two mechanisms because two entrances, not because two owners. */
.m [data-component="titleStack"][data-form="register"] [data-component="headline"] .ln{display:block;overflow:hidden}
.m [data-component="titleStack"][data-form="register"] [data-component="headline"] .ln .mv{display:block;font-style:normal;transform:translateY(var(--ratio-105));transition:transform var(--motion-duration-900) var(--motion-ease-draw)}
.m.go [data-component="titleStack"][data-form="register"] [data-component="headline"] .ln .mv{transform:none}

/* the subline and the control row arrive after the headline has landed. These were `.m .intro
   .tag` and `.m .intro .ctas` - page classes on the home route - and they are this block's own
   choreography wherever it is drawn. Scoped to the register: every other hero reveals on scroll
   through the shared `.r` seam, which is the chrome's job rather than this component's. */
.m [data-component="titleStack"][data-form="register"] [data-component="subline"],
.m [data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{transform:translateY(var(--motion-travel-base));transition:opacity var(--motion-duration-1s) var(--motion-ease-draw),transform var(--motion-duration-1s) var(--motion-ease-draw)}
.m.go [data-component="titleStack"][data-form="register"] [data-component="subline"],
.m.go [data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{transform:none}
@media (prefers-reduced-motion:reduce){
 .m [data-component="titleStack"][data-form="register"] [data-component="subline"],
 .m [data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{transform:none;transition:none}
}

/* THE HOME ROUTE OPENS AT DISPLAY, and it is the only one that does. `display` is the grade
   a page opens with once, and seventeen routes all opening at 120px is not a hierarchy, it
   is a font size. Every other page title is h1. */

/* THE xl FORM, a route document's `variant: xl`: moved verbatim from headline.form.css, where
   the atom was styling the component that holds it. No route asks for it today. */
[data-component="titleStack"][data-size="xl"] [data-component="headline"]{
 font:var(--type-h1-font);font-size:clamp(var(--hero-title-min),8vw,var(--type-h1-size));
 line-height:clamp(var(--hero-title-min),8vw,var(--type-h1-leading));
 letter-spacing:var(--type-display-tracking);font-variation-settings:"wdth" 92,"opsz" 96}
/* the register's title stack is centred, and so is its row of calls to action - at every width */
[data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{justify-content:center}

/* ---- components/titleStack.skin ---- */
/* titleStack's paint. The subline is the quieter of the two voices in the block on every route;
   the register's build-in brings its subline and its controls up from nothing. The headline takes
   the ground's ink and the buttons are the button atom's, so neither is coloured here. */
[data-component="titleStack"] [data-component="subline"]{color:var(--colour-text-secondary)}
.m [data-component="titleStack"][data-form="register"] [data-component="subline"],
.m [data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{opacity:0}
.m.go [data-component="titleStack"][data-form="register"] [data-component="subline"],
.m.go [data-component="titleStack"][data-form="register"] [data-component="buttonGroup"]{opacity:1}

/* ---- components/growFrame.form ---- */
/* growFrame: a plate that pins to the centre of the viewport and grows as the page scrolls past
   it. One component, holding the sticky box and the plate inside it; the plate itself is the
   media atom and keeps its own surface.

   EXTRACTED FROM masthead[data-form="register"], where it was a bare `div.sizzle` whose
   data-component was `media` - so the sticky 1410x100vh positioning shell and the 230x153 plate
   were the SAME element's job, and the atom was being asked to be a layout. That is what made
   the container paint the plate's ground behind the whole hero: media.skin's floor landed on the
   shell. Splitting the two removes the cause rather than overriding it, so masthead.skin's
   `background:none` patch is gone with it.

   The end size is NOT here. home-hero.js scrubs the transform from the plate's own layout box to
   63.9 per cent of the viewport's width and 68.1 per cent of its height, measured off the live
   site at 1440x900, because a pixel rate reads correctly at one size and nowhere else. This sheet
   holds the resting state; the run is behaviour. */
/* ITS OWN SECTION (owner, 2026-09-24): the frame is two viewports tall, which is the scroll the
   plate grows over. The stage pins just under the nav and is exactly the height left beneath it;
   the plate starts at the stage's top edge - the masthead's 96px under the controls - and grows
   downward until it fills the stage, so no empty stage is left under it when it releases */
[data-component="growFrame"]{height:200vh}
[data-component="growFrame"] .stage{position:sticky;top:calc(var(--bar-site-height) + var(--space-step-1));height:calc(100vh - var(--bar-site-height) - var(--space-step-1));display:flex;justify-content:center;align-items:flex-start}

/* AT REST THE PLATE IS SMALL AND ON THE CENTRE LINE. 15.97vw on a 230:153 box: the pattern's own
   top padding is derived from half this plate's height, which is why the number lives beside the
   ratio rather than as a magic value in two files. Radius 0, measured against the reference.
   transform-origin is the centre because the growth is about the centre line, and will-change
   because every frame of the scrub writes a transform. */
/* THREE COLUMNS AT REST, TEN AT FULL, NO KEYLINE (owner, 2026-09-23): the plate starts at the
   grid's 3-column width and home-hero.js grows it to the 10-column width; the media atom's
   hairline is dropped because a picture growing to fill the stage has no edge to draw. */
[data-component="growFrame"] [data-component="media"]{width:var(--grid-column-3);border:0;aspect-ratio:230/153;position:relative;overflow:hidden;transform-origin:var(--position-centre) 0;will-change:transform}
[data-component="growFrame"] [data-component="media"] img{width:100%;height:100%;object-fit:cover;display:block}

/* the plate fades in once, with the build. The growth is scroll, the arrival is the build: two
   different clocks, so two different rules. */
.m [data-component="growFrame"]{transition:opacity var(--motion-duration-1s) var(--motion-ease-draw)}
.m.go [data-component="growFrame"]{transition-delay:var(--motion-duration-600)}
@media (prefers-reduced-motion:reduce){
 [data-component="growFrame"] [data-component="media"]{will-change:auto}
}

/* BELOW 760 THE PICTURE DOES NOT GROW (owner, 2026-09-27): no 200vh runway, no pinned stage, the
   plate at the column's full width. home-hero.js leaves it unscaled at the same width. */
@media (max-width:760px){
 [data-component="growFrame"]{height:auto}
 [data-component="growFrame"] .stage{position:static;height:auto}
 [data-component="growFrame"] [data-component="media"]{width:var(--ratio-100)}
}

/* ---- components/growFrame.skin ---- */
/* growFrame's paint: the plate's own cool ground, and the component's fade to it.

   The ground sits on the PLATE - the media atom - and not on the sticky shell around it, which
   is the whole point of the split: a shell that paints is a shell that draws a viewport-tall
   rectangle behind everything the pattern also holds. */
[data-component="growFrame"] [data-component="media"]{background:var(--frame-plate-cool)}
.m [data-component="growFrame"]{opacity:0}
.m.go [data-component="growFrame"]{opacity:1}

/* ---- patterns/sectionIntro.form ---- */
/* sectionIntro: a band's opening: the sectionHead component, alone.

   A PATTERN SO THE PAGE HOLDS PATTERNS ONLY (owner, 2026-09-23). A band is the template's region,
   and the component this holds sat in it directly. It adds no layout of its own: a block around
   one block, with no margin, padding or border, so the component draws exactly as it did. */
[data-component="sectionIntro"]{display:block}

/* ---- templates/followRun.form ---- */
/* followRun (template): a run of page body with a mark that follows the reader down it (owner, 2026-09-28),
   as /approach's shape does. The mark sits in the grid's first two columns, pinned under the nav
   while the run scrolls, and its track ends with the run, so it stops above the footer. The
   run's own content keeps the grid it already had; the mark takes no room in it. */
[data-template="followRun"]{position:relative}
[data-template="followRun"] > .follow{position:absolute;top:0;bottom:0;z-index:0;pointer-events:none;
 left:calc((100% - min(var(--grid-content-width),100% - var(--grid-page-inset))) / 2);width:var(--grid-column-3)}
[data-template="followRun"] > .follow > [data-component="shape"]{position:sticky;top:var(--space-step-12);width:var(--grid-column-3);height:var(--grid-column-3)}
[data-template="followRun"] > :not(.follow){position:relative;z-index:1}
@media (max-width:1100px){[data-template="followRun"] > .follow{display:none}}

/* ---- templates/followRun.skin ---- */
/* followRun holds no paint: the shape atom paints itself */

/* ---- patterns/pageFacts.form ---- */
/* pageFacts: a document page's facts line: the factRow component, alone.

   A PATTERN SO THE PAGE HOLDS PATTERNS ONLY (owner, 2026-09-23). A band is the template's region,
   and the component this holds sat in it directly. It adds no layout of its own: a block around
   one block, with no margin, padding or border, so the component draws exactly as it did. */
[data-component="pageFacts"]{display:block}
/* A WRITING PAGE'S FACTS SIT ON THE BODY COLUMN (owner, 2026-09-25): the narrative band sets its prose
   three columns and a gutter in, at seven columns wide, so the row - its rule, its facts - starts
   and ends there too, each fact a quarter of that measure */
[data-component="pageFacts"][data-form="narrative"] [data-component="factRow"] .cols{max-width:var(--grid-column-7);margin-left:calc(var(--grid-column-3) + var(--grid-gutter));grid-template-columns:repeat(4,minmax(0,1fr))}
[data-component="pageFacts"][data-form="narrative"] [data-component="factRow"] .cols > *{grid-column:span 1}
@media (max-width:1100px){[data-component="pageFacts"][data-form="narrative"] [data-component="factRow"] .cols{margin-left:0;max-width:none}}
/* A WRITING PAGE'S OPENING IS ONE PIECE (owner, 2026-09-25): the hero, the facts, the lede and the
   body read in one run, so each sits one step apart - 48 from the hero to the facts' rule, 32 from
   the facts to the lede, 48 from the lede to the body - where each had a section's full space */
[data-component="pageFacts"][data-form="narrative"] [data-component="factRow"]{padding:0 0 var(--space-step-4)}
[data-component="masthead"]:is(:has(> [data-template="wrap"] > .hero-plate),:has(+ [data-component="pageFacts"][data-form="narrative"])){padding-bottom:var(--space-step-6)}
/* a case study's picture, edge to edge between its facts and its standfirst: 48 from each */
[data-component="pageFacts"] + .hero-bleed{padding:var(--space-step-2) 0 var(--space-step-6)}
.hero-bleed + [data-gate-slot] + [data-component="leadText"],.hero-bleed + [data-component="leadText"]{padding-top:0}
[data-component="pageFacts"] + .hero-bleed > .hero-plate{width:100%}
[data-component="pageFacts"][data-form="narrative"] ~ [data-component="leadText"]{padding-bottom:0}
/* only the body's FIRST band: a narrative band after another keeps the article's 64 before its heading */
[data-component="pageFacts"][data-form="narrative"] ~ [data-template="band"][data-form="narrative"]:not([data-template="band"][data-form="narrative"] + *){padding-top:var(--space-step-6)}
/* A CASE STUDY'S FACTS SPAN THE GRID (owner, 2026-09-25): the rule runs the full content width and
   the four facts sit on the twelve columns at 2, 2, 3 and 3 - client, sector, period, role */
[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols{margin-left:0;max-width:none;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--grid-gutter)}
/* CLIENT TAKES THREE, PERIOD TWO (2026-09-27): at 2, 2, 3, 3 a client name over twenty characters
   ran three or four lines in its two columns while period, the shortest fact, had three. Now 3, 2,
   2, 3 - client, sector, period, role - on the same ten columns. */
[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols > *{grid-column:span 2}
[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols > :is(:nth-child(1),:nth-child(4)){grid-column:span 3}
@media (max-width:1100px){[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols{grid-template-columns:repeat(2,minmax(0,1fr))}[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols > :is(*,:nth-child(1),:nth-child(4)){grid-column:auto}}
@media (max-width:760px){[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols{grid-template-columns:1fr}[data-component="pageFacts"][data-form="narrative"][data-align="start"] [data-component="factRow"] .cols > :is(*,:nth-child(1),:nth-child(4)){grid-column:auto}}
/* BELOW 1100 THE FACTS PAIR UP. Four to a row at 390 gave each fact 70px, so a family name like "AI
   Operating Model Design" ran four lines in its column while the code beside it sat on one; two to
   a row gives each 171px. At 768 four facts left a client name 95px, ten characters a line. */
@media (max-width:1100px){[data-component="pageFacts"][data-form="narrative"]:not([data-align="start"]) [data-component="factRow"] .cols{grid-template-columns:repeat(2,minmax(0,1fr))}}

/* ---- patterns/masthead.form ---- */
/* masthead[data-form="register"]: the register's opening piece, and the one layout on the site that
   is a scroll composition rather than a block. Recovered from home-register.html, where it was 19
   page rules and an inline script: a 100vh ground carrying the wordmark three times in clipped
   bands, and the scroll run the pieces inside it are choreographed against. The route asks for it
   by being the home route; what home looks like is the frontend's.

   IT IS A PATTERN AND IT NOW COMPOSES COMPONENTS. It used to hold a bare `div.title` and a bare
   `div.sizzle`, and reach through both to the atoms inside - the headline's display grade, the
   plate's size, their choreography - so the pattern owned the anatomy of two blocks that had no
   name. They are named now: `titleStack` is the headline, subline and control row, `growFrame`
   is the plate that pins and grows. What is left here is what only the pattern can know: the
   length of the scroll run, and the ground the whole thing is set over. */
[data-component="masthead"][data-form="register"]{position:relative}
/* THE CONTAINING BLOCK HAS TO SPAN THE WHOLE RUN, or the plate inside has nothing to stick
   against. This rule read `>.wrap` - a CLASS - and the wrap is `[data-template="wrap"]`, an
   attribute, so it matched nothing: the wrap took its content height, 1023.5px against the
   hero's 2553px, and growFrame's sticky had 123.5px of travel before it released and scrolled
   away with the page. That is why the plate read as a small marooned thumbnail rather than a
   frame that pins and grows - the grow was running, on an element that had already left. */
[data-component="masthead"][data-form="register"]>[data-template="wrap"]{height:100%}
/* the ground is the first screen, not the masthead: the text block is shorter than the viewport and
   the wordmark is set to fill it, so it runs on behind the picture below rather than being cut */
[data-component="masthead"] .ground{position:absolute;inset:0 0 auto;height:100vh;overflow:hidden;pointer-events:none;z-index:0}
[data-component="masthead"] .ground .word{position:absolute;left:var(--position-centre);top:var(--position-centre);transform:translate(var(--offset-centre),var(--offset-centre));font-family:var(--type-family-display);font-size:145vh;line-height:1;font-weight:800;text-transform:uppercase;white-space:nowrap;font-variation-settings:"wght" 800,"opsz" 144,"wdth" 92;letter-spacing:-.015em;animation:contest var(--motion-duration-7s) cubic-bezier(.6,0,.4,1) infinite}
[data-component="masthead"] .ground .word:nth-child(1){clip-path:inset(0 0 var(--ratio-70) 0);animation-delay:var(--motion-duration-0)}
[data-component="masthead"] .ground .word:nth-child(2){clip-path:inset(var(--ratio-30) 0 var(--ratio-38) 0);animation-delay:calc(-1 * var(--motion-duration-456))}
[data-component="masthead"] .ground .word:nth-child(3){clip-path:inset(var(--ratio-62) 0 0 0);animation-delay:calc(-1 * var(--motion-duration-912))}
[data-component="masthead"] .ground .rule{position:absolute;left:0;right:0;height:var(--size-1)}
[data-component="masthead"] .ground .rule:nth-child(4){top:var(--ratio-30)}
[data-component="masthead"] .ground .rule:nth-child(5){top:var(--ratio-62)}
/* THE GROUND IS THE MASTHEAD'S, not only the register's (owner, 2026-09-25): any hero that carries
   one holds it behind its title, which sits above it */
[data-component="masthead"]:has(> [data-template="wrap"] > .ground){position:relative}
[data-component="masthead"] .ground ~ [data-component="titleStack"]{position:relative;z-index:1}
/* THE TITLE BLOCK IS A COMPONENT ON EVERY ROUTE, and this pattern composes it rather than
   setting it: titleStack.form.css owns the eyebrow, the headline's grade and measure, the
   subline and the controls, under `data-size` for the scale and `data-form="register"` for the
   home route's anatomy. The plate the register grows is growFrame.

   FOUR RULES LEFT HERE WHEN THEY SHOULD HAVE LEFT WITH THE SPLIT. `[data-component="masthead"]
   [data-component="headline"]` and its lg and xl measures, and the two `.lede` rules, were the
   pattern reaching past nothing to atoms on ten routes, while the eleventh had the same block
   named and owned. A component that reaches one of eleven instances is a component in name only:
   the point of the ladder is that changing the title block changes every title. It does now.

   What is left is what only a masthead can know: the band's padding, the family mark it draws,
   the ground the register is set over, and the length of the register's scroll run - the
   relationship BETWEEN titleStack and growFrame, which neither component can know alone. */
/* masthead: the title block every index and detail route opens with. Nine pages carried these
   four rules, three of them with a padding 12 to 20px off the other six, which is drift rather
   than intent: one value now, 120px over and 64px under. Genuine deviations are a data-variant,
   and there are none yet.
   Note for the port: /drop uses a `.idx` class for something else entirely, a mono index label
   inside its cards. Two meanings for one name is exactly what the attribute seam prevents. */
/* THE ONE TOP PADDING: the masthead opens the page under the fixed nav, so its head is the nav's
   clearance, not the gap between sections, which every other section owns at its foot only */
[data-component="masthead"]{padding:var(--space-step-12) 0 var(--space-step-12)}

/* a family route's mark, in the hero: the shape atom, which owns its box, canvas and failure.
   The masthead says only where it sits and how far it steps down on a narrow screen. */
[data-component="masthead"] [data-component="shape"]{margin-bottom:var(--space-step-6)}
@media (max-width:1100px){
 [data-component="masthead"] [data-component="shape"]{width:var(--size-plate-row);height:var(--size-plate-row)}
}

/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
@keyframes up{to{transform:none}}
/* ---- AND ONE RULE DELETED RATHER THAN MOVED ---------------------------------------------
   `.hero{padding-top:calc(50vh - var(--hero-plate-centre) - 2.655vw);height:calc(100vh + var(--hero-plate-travel))}` was here,
   lifted verbatim out of home-register.html's head along with everything else. On the home page
   it was a duplicate of the `[data-form="register"]` rule above, which is harmless. Everywhere
   else it was a leak: `class="hero"` is on EVERY masthead the renderer emits, so /work/,
   /services/, /about/ and every index and detail route with them were all 2553px tall - a
   viewport and a half of empty ground under each page's title, carrying a scroll run those
   routes do not have. Measured at 1440x900: three routes sampled, all three 2553px.
   This is what a page class costs when it moves into a component sheet without being read: the
   selector stops describing one page and starts describing a component that appears on 217.
   The register form owns the home hero's geometry, so there is nothing to replace it with. */
/* TYP-04 Contested Measure, run as the hero's ground. The word is set so its CAPS fill the
   viewport's height and is cropped full bleed left and right: whether the whole word fits is not
   the point, the tear across the frame is. Three bands, hairlines at the 30% and 62% seams, each
   band re-cutting its optical size on a 7.6s loop phase-shifted ~6%, so the slices stop agreeing.
   Low contrast: this is ground, and the h1 has to stay the thing you read.
   Measured, not assumed: 139vh put the cap ink at 861 of 900, so the size is 145vh. */
@keyframes contest{0%,100%{font-variation-settings:"wght" 800,"opsz" 144,"wdth" 92;letter-spacing:-.015em}50%{font-variation-settings:"wght" 800,"opsz" 6,"wdth" 92;letter-spacing:.02em}}
@media (prefers-reduced-motion:reduce){.hero .ground .word{animation:none}}
@media (prefers-reduced-motion:reduce){.m .r,.m .method .card{transform:none;transition:none}}

/* THE HERO PICTURE FILLS ITS PLATE. This was the first rule in media.form.css - the atom styling
   itself inside one pattern - which is the ladder upside down: where a plate sits and how the
   hero fills it is the masthead's decision. Same rule, same specificity, its own sheet. */
[data-component="masthead"][data-form="register"] [data-component="media"] img{width:100%;height:100%;object-fit:cover}

/* THE STANDFIRST IN THE HERO: its entrance and its margin. These were in subline.form.css,
   the atom deciding how one pattern uses it; the masthead owns them now, verbatim. */


.m [data-component="masthead"][data-form="register"] [data-component="subline"],
.m [data-component="masthead"][data-form="register"] [data-component="subline"]{transform:translateY(var(--motion-travel-base));transition:opacity var(--motion-duration-1s) var(--motion-ease-draw),transform var(--motion-duration-1s) var(--motion-ease-draw)}
.m.go [data-component="masthead"][data-form="register"] [data-component="subline"]{transform:none;transition-delay:var(--motion-duration-200)}
/* MARGIN IS RHYTHM, NOT GRADE: 22.5px is the gap this hero wants between its headline and this
   line, which is a decision about the hero's own vertical spacing rather than about what a
   subline is everywhere. It stays scoped to the register hero for that reason; the grade above
   no longer needs restating here. */
[data-component="masthead"][data-form="register"] [data-component="subline"]{margin-top:var(--space-step-2)}

/* HOW THIS PATTERN USES buttonGroup: moved verbatim from buttonGroup.form.css, where the
   part was deciding how its parent places it. */
/* The MARGIN is the masthead's: distance from what came before is rhythm, and the register hero
   sets both a tighter pairing and a longer drop under its form. Everything about being a group -
   the flex, the wrap, the 18px - is above, once.
   
   Two identical `[data-form="register"]` rules stood here, written on separate days for separate
   reasons, which is what a component sheet collects when the same fix arrives twice. The history
   below is worth keeping because it names the trap: this gap was PAGE css, so it loaded after
   every component and won. Moved into masthead.form.css it loaded BEFORE this file, buttonGroup's
   own gap beat it, and every hero button shifted 6px. Then, unscoped, home's 12px applied to
   about, drop and private-equity, which shifted each of those 6px the other way. A rule that
   lived in one page's head carries that page's scope implicitly, and the scope has to be made
   explicit when the rule moves. */
[data-component="masthead"] [data-component="buttonGroup"]{margin-top:var(--space-step-8)}
[data-component="masthead"][data-form="register"] [data-component="buttonGroup"]{gap:var(--space-step-3);margin-top:var(--space-step-4)}
.m.go [data-component="masthead"][data-form="register"] [data-component="buttonGroup"]{transform:none;transition-delay:var(--motion-duration-400)}

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){[data-component="masthead"]{padding:var(--space-step-9) 0 var(--space-step-9)}}
@media (max-width:760px){[data-component="masthead"]{padding:var(--space-step-6) 0 var(--space-step-6)}}
/* a piece of writing's hero, under its headline (owner, 2026-09-25): 48px below the h1, full width */
[data-component="masthead"] > [data-template="wrap"] > .hero-plate{margin-top:var(--space-step-6)}
/* A BOOKING PAGE'S MASTHEAD (owner, 2026-09-25): the title and the scheduler under it take seven
   columns, the call's brief the last four from the headline's own top edge, a column of air
   between. The brief spans both rows, so a tall brief lengthens the band rather than pushing the
   scheduler away from the title. One column below 1100, in source order: title, steps, brief. */
[data-component="masthead"]:has(> [data-template="wrap"] > [data-component="callBrief"]) > [data-template="wrap"]{
 display:grid;grid-template-columns:repeat(var(--count-12),minmax(0,1fr));grid-template-rows:auto 1fr;
 column-gap:var(--space-gutter);align-items:start}
[data-component="masthead"] > [data-template="wrap"]:has(> [data-component="callBrief"]) > :is([data-component="titleStack"],[data-component="callBooker"]){grid-column:1 / span 7}
[data-component="masthead"] > [data-template="wrap"] > [data-component="callBooker"]{margin-top:var(--space-step-8);position:relative;z-index:1}
[data-component="masthead"] > [data-template="wrap"] > [data-component="callBrief"]{grid-column:9 / span 4;grid-row:1 / span 2;position:relative;z-index:1}
/* BELOW 1100 EVERY PART TAKES THE FULL ROW. This said `> *`, which the seven-column rule above
   outranks through its :is(), so at 390 the title, the standfirst and the dates stood in seven
   twelfths of the phone - 196px - with the rest of the screen empty. */
@media (max-width:1100px){
 [data-component="masthead"] > [data-template="wrap"]:has(> [data-component="callBrief"]) > :is([data-component="titleStack"],[data-component="callBooker"],[data-component="callBrief"]){grid-column:1 / -1}
 [data-component="masthead"] > [data-template="wrap"] > [data-component="callBrief"]{grid-row:auto;margin-top:var(--space-step-8)}
}
/* a section page's own picture sits 48 under the standfirst, the pictures rule (owner, 2026-09-25):
   it was drawn flush against the last line of text */
[data-component="masthead"] > [data-template="wrap"] > .prod-art{margin-top:var(--space-step-6)}
/* the picture crops to its frame, never stretches: a 16:9 photograph in the 3:2 plate was drawn 16% too tall */
[data-component="masthead"] > [data-template="wrap"] > .prod-art > img[data-component="media"]{object-fit:cover;inline-size:var(--ratio-100)}
/* A register hero followed by a running embed hands its foot to it (owner, 2026-09-28): /drop's card
   stack sits under the controls, and its runway carries the space above its frame */
[data-component="masthead"][data-form="register"]:has(+ [data-band="embed"]){padding-bottom:0}

/* ---- patterns/masthead.skin ---- */
[data-component="masthead"] .ground .word{color:var(--colour-text-primary);opacity:.028}
[data-component="masthead"] .ground .rule{background:linear-gradient(var(--angle-90),transparent,var(--colour-border-faint) var(--ratio-22),
 var(--colour-border-faint) var(--ratio-78),transparent)}
[data-component="masthead"] .lede{color:var(--colour-text-secondary)}
/* THE PLATE'S PAINT LEFT WITH THE PLATE. Its ground, its fade and the `background:none` that had
   to sit beside them are all growFrame.skin.css's now.
   That patch existed because one element was both the sticky viewport-tall shell and the plate:
   media.skin's floor - `[data-component="media"]:not(img){background:var(--colour-surface-card)}` - landed on
   the shell, so the hero drew a 5 per cent white rectangle behind the headline, the subline, the
   buttons and the plate alike, and the only fix available was to override the atom's own floor.
   The split removes the cause: the shell is a growFrame and is not a media atom at all, so the
   floor never lands on it and there is nothing left to override. A patch that can be deleted by
   naming a thing correctly was a naming problem wearing a paint problem's clothes.
   The remaining rules here are the ground's, which is the only thing this pattern still paints. */
/* paint that came off the home page with its geometry: the form sheet holds where these
   sit, this holds how they look, and the gate refuses the two in one file. */
.m .r,.m .method .card{opacity:1}

/* THE STANDFIRST IN THE HERO: its entrance and its margin. These were in subline.skin.css,
   the atom deciding how one pattern uses it; the masthead owns them now, verbatim. */


.m [data-component="masthead"][data-form="register"] [data-component="subline"],
.m [data-component="masthead"][data-form="register"] [data-component="subline"]{opacity:0}
.m.go [data-component="masthead"][data-form="register"] [data-component="subline"]{opacity:1}

/* HOW THIS PATTERN USES buttonGroup: moved verbatim from buttonGroup.skin.css, where the
   part was deciding how its parent places it. */
/* the group paints nothing but its own arrival: the intro fades it in after the headline and the
   subline, which is why the opacity lives here rather than in the form sheet. */

.m.go [data-component="masthead"][data-form="register"] [data-component="buttonGroup"]{opacity:1}

/* ---- components/mediaCard.form ---- */
/* mediaCard: the canonical card, and the one the work archive established. Four routes drew it,
   in four stylesheets, with the same computed result reached four ways: /work set a 6px plate
   radius where everyone else inherited the 12px token, and the home page put the 20px gap under
   the plate instead of over the title. Identical output, four places to change. One now.
   The plate's empty state belongs here too, per headless-conventions rule 10: an absent image is
   a designed state, so the component names the publisher and admits the placeholder rather than
   leaving a reader a hole they cannot account for. */
[data-component="mediaCard"]{display:block}
/* THE CARD'S RATIO IS A DEFAULT, NOT A DIKTAT. 16:10 is the shape a card takes when nothing
   states otherwise, and it was written here as an unconditional rule - so an embed plate
   declaring 3:2, which is the geometry its example was drawn against, computed 281px tall
   inside a 300px stage and had 19px of the demo clipped off the bottom on every one of the
   172 cards that hold one. A plate that states its ratio keeps it; a plate that states
   nothing gets the card's. */
[data-component="mediaCard"] .rnd:not([data-ratio]){aspect-ratio:16/10}
[data-component="mediaCard"] .rnd img{transition:transform var(--motion-duration-600) var(--motion-ease-settle)}
[data-component="mediaCard"]:hover .rnd img{transform:scale(1.03)}
/* the 20px belongs to whatever follows the plate, not to the title: /work puts a sector label
   between the two, and pinning the gap to h3 pushed all 31 archive cards 11px taller. The plate
   is .rnd or .stage, per the media types above. */
/* A WRAPPING HEADLINE NEEDS ITS LEADING BACK. The title role is 18/18 - ratio 1.0 - which is
   right for the one-line label it was designed as and wrong for a card title that runs to two or
   three lines: at 18/18 the descenders of one line touch the caps of the next. 22.5px is 5u, on
   the contract's unit, and gives the wrapping case a 1.25 ratio. The SIZE is untouched: this is
   the leading, not the grade. */
/* THE CARD'S OWN LEADING, on a grade the atom sets. The card titles take the `minor` grade and
   this one line deviates from it - 22.5px rather than 18px, because a card title wraps to two
   lines and 18px leading closes them up. It names the ATOM rather than the element so it ties the
   grade rule's specificity and wins on order; `h3` alone loses to `[data-size="title"]` and the
   deviation would silently vanish. */
/* THE CARD NO LONGER OVERRIDES A HEADLINE'S LEADING. It set the title one role's leading and
   another role's size, which is a pair split in half: the type scale now ships size, leading and
   weight together per role, and a component that takes two of the three from different roles is
   the reason 31 card titles rendered 24px type on a 23.25px line. */
[data-component="mediaCard"] p{margin-top:var(--space-step-1);font:var(--type-citation-font);}
[data-component="mediaCard"] .meta{display:block;margin-top:var(--space-step-1)}
/* the empty state belongs to both media, not just the image one. A slot waiting for an embed is
   the same designed absence as a slot waiting for a picture, and /drop's product art proved it:
   an un-filled embed plate rendered as a silent 939px black rectangle with no placeholder,
   because .ph was scoped to .rnd. A hole a reader cannot account for is the thing rule 10 is for. */
[data-component="mediaCard"] .rnd.empty::after,[data-component="mediaCard"] .stage.empty::after{content:"PLACEHOLDER";position:absolute;right:var(--type-step-1);top:var(--type-step-1);font:var(--type-label-font);letter-spacing:var(--type-label-tracking)}
[data-component="mediaCard"][data-tone="plate"]{overflow:hidden}
/* PICTURE TO WORDS, 16px (owner, 2026-09-24): --space-step-2 is the register's gap from an
   item's picture to its words. It was 32px, twice the gap anything else in the card uses. */
[data-component="mediaCard"] .stage + *,[data-component="mediaCard"] .rnd + *{margin-top:var(--space-step-2)}
[data-component="mediaCard"][data-tone="plate"] .stage + *{margin-top:0}
[data-component="mediaCard"] figcaption{padding:var(--space-step-2) var(--type-step-2) var(--space-step-3)}
[data-component="mediaCard"] figcaption b{display:block;font:var(--type-title-font);}
[data-component="mediaCard"] figcaption .alias{display:block;margin-top:var(--space-step-1);font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
[data-component="mediaCard"] figcaption span:not(.alias){display:block;margin-top:var(--space-step-1);font:var(--type-citation-font);}
/* THE BESPOKE SPECIMEN CAPTION IS GONE. `.cap-t` held a code chip and a bold title inside a
   figcaption, which was a second card design on identical type: the card reads byline, headline
   and text now, exactly as a thinking or noods card does, and the citation code is the byline.
   the citation code a specimen card carries: every example is addressable as PRN-01, so the code
   is a link to itself and lives in the caption beside the title. Rendered into the markup, not
   patched on afterwards, so the reference exists whether or not any script runs. */
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
@media (prefers-reduced-motion:reduce){
 [data-component="mediaCard"] .rnd img,[data-component="mediaCard"]:hover .rnd img{transition:none;transform:none}
}
/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
.ins a,.work .card{cursor:pointer}

/* THE CARD'S FOUR NEW PARTS, in the order a reader uses them: who and when, the title, the
   summary, what it is filed under, and the row that says this is a link.
   
   The byline is the DROP above the title - half the labelled type's size, so 9px for an 18px
   headline, per the contract's ladder.
   
   The tags are chips, because a tag is a thing you could filter by rather than another line of
   prose. They hug their text and wrap, since a card carries up to three and the third can be one
   word or four.
   
   The foot is pushed to the bottom of the card by `margin-top:auto`, so a row of cards has its
   arrows on one line whatever their titles do - which is the whole reason a card grid reads as a
   grid rather than as five separate columns. */
[data-component="mediaCard"]{display:flex;flex-direction:column}
[data-component="mediaCard"] .tags{display:flex;flex-wrap:wrap;gap:var(--space-step-1);margin-top:var(--space-step-1)}
[data-component="mediaCard"] .tag{padding:var(--space-step-1) var(--space-step-2)}
/* NO RULE ABOVE THE FOOT. The hairline was doing the work the action atom does better: the row
   is an offer, not a section boundary, and a divider above it reads as the end of the card
   rather than the way out of it. The affordance is a tertiary button now - ONE button holding its
   label and its mark, not a mono label pushed to one edge and an arrow to the other. A row with
   an action at each end reads as two controls; a reader should see one thing to press. So the
   foot packs to the left and the label travels inside the button that follows it.
   THE PART GAP IS THE BUTTON'S OWN (owner, 2026-09-24): the action is the button atom at small, a
   40px control whose words sit in its centre, so the box stands directly under the line and its
   inset puts the words 9px below it - markedRow's 8px part gap, with the full 40px still to press.
   8px of padding above it (owner, 2026-09-24: the words read tight under a long body) sets the
   words about 17px under the line. */
[data-component="mediaCard"] .foot{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:flex-start;gap:var(--space-step-2);margin-top:auto;padding-top:var(--space-step-1)}
/* the foot's two atoms share a BASELINE, 16px apart. Centring the boxes left the label 2px under
   the button's words, which the button lifts by its optical 2px; the label's own 8px top margin had
   pushed it 4px further, and 48px stood between them. */
[data-component="mediaCard"] .foot .meta{margin-top:0}
[data-component="mediaCard"] .foot .go svg{width:var(--size-mark-base);height:var(--size-mark-base);display:block}
[data-component="mediaCard"] .foot .go svg{transition:transform var(--motion-duration-350) var(--motion-ease-settle)}
/* THE MARK MOVES, NOT THE LABEL. These two rules translated the whole button six pixels on
   hover, which is what the foot needed when `.go` WAS the arrow - a bare mark at the card's far
   edge. It is a labelled tertiary button now, and sliding a word sideways reads as the label
   coming loose. base.css moves the mark inside it, and only where the mark is an arrow: an
   arrow travels because it means "this takes you somewhere", and a copy mark does not move
   because copying text moves nothing. */

@media (prefers-reduced-motion:reduce){
 [data-component="mediaCard"] .foot .go svg,
 [data-component="mediaCard"]:hover .foot .go svg{transition:none;transform:none}
}

/* ---- A CARD THAT HOLDS A SCROLL TRACK -------------------------------------------------------
   A pinned scroll demo is a runway some 2,300px tall with one frame held sticky inside it, and
   the two specimen sheets used to give it the page's own scroll: the card grew to the height of
   the runway and the reader scrolled the document to scrub it. In a three-up grid that strands
   its two neighbours at the top of 2,400px of empty column, and the grid is three up everywhere.

   So the PLATE is the scrollport. It keeps the card's shape, the runway travels inside it, and
   sticky and the view timelines that scrub against it work exactly as they did - a scrollport is
   a scrollport whether it is the document or a 450px box. `overscroll-behavior:contain` stops the
   page taking over when the runway reaches its end, which is what makes a scroll demo inside a
   card readable rather than a trap. */
[data-component="mediaCard"][data-scroll="runway"] [data-component="media"][data-kind="embed"]{
 overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-width:none}
[data-component="mediaCard"][data-scroll="runway"] [data-component="media"][data-kind="embed"]::-webkit-scrollbar{display:none}
/* AND ITS CONTENT FLOWS. An embed normally fills its plate absolutely, because the plate states
   the shape and the example draws into it. A runway states its own height, and an absolutely
   positioned child reports none: the plate had nothing to scroll. */
[data-component="mediaCard"][data-scroll="runway"] [data-component="media"][data-kind="embed"] > [data-embed]{position:static;inset:auto}
/* AND THE PIN IS THE PLATE'S HEIGHT. Every runway embed pins its frame at `top:56px`,
   `height:calc(100dvh - 56px)`: the sheet it came from scrolled the document, so its scrollport
   was the viewport under a 56px bar. Inside a plate that pin stood 844px tall in a 300px box and
   centred its stage 353px down, below the visible plate, so a timeline scrubbing 0 to 100 drew
   into nothing. The plate IS the scrollport now, so the pin is the plate: its width at 3:2,
   measured off the plate's own inline-size container. */
[data-component="mediaCard"][data-scroll="runway"] [data-component="media"][data-kind="embed"] .pin{
 top:0;height:calc(100cqw * 2 / 3)}
[data-component="mediaCard"] [data-component="label"]{margin-bottom:0}
/* a card holding a vertical plate is as narrow as its plate: three columns at most, so its words
   run the plate's width, in a rail, a grid or on its own */
[data-component="mediaCard"]:has(> [data-component="media"][data-ratio="2:3"]){max-width:var(--grid-column-3)}
/* the byline is a part of the item, so the part gap to the title */
[data-component="mediaCard"] > .byline{display:block;margin-bottom:var(--space-step-1)}

/* A SHAPE PLATE (owner, 2026-09-24): a card whose picture is an animated mark from the shapes engine
   holds it in a 3:2 plate of its own, the mark centred at the plate's height */
[data-component="mediaCard"] > .rnd.plate{aspect-ratio:3/2;display:flex;align-items:center;justify-content:center;overflow:hidden}
[data-component="mediaCard"] > .rnd.plate > [data-component="shape"]{width:auto;height:100%;aspect-ratio:1}
/* A LOCKED CARD ASKS IN PLACE (owner, 2026-09-25). The title's link is stretched over the whole
   card, so the card still clicks through; the foot sits above that layer. "Request access" slides
   the foot out and the form in on the same line: the field and send at small, a close beside them,
   and the answer line under them replacing the field once it is sent. */
[data-component="mediaCard"][data-request]{position:relative}
[data-component="mediaCard"] .stretch{text-decoration:none}
[data-component="mediaCard"] .stretch::after{content:"";position:absolute;inset:0}
/* the foot and the form share one cell and swap in it, clipped to the card's width. CLIP, not
   hidden (owner, 2026-09-26): a hidden box is still scrollable, so focusing the field while it was
   still sliding scrolled the cell sideways under it, 168px and back, which was the jolt on open.
   And the clip stands a step OUTSIDE the cell on three sides, paid back by the margin, because
   the field's focus ring sits 2px out from it and a clip at the cell's edge cut the ring off. */
[data-component="mediaCard"] .act{position:relative;z-index:1;display:grid;overflow:clip;margin-top:auto;
 padding:var(--space-step-1);margin-inline:calc(-1 * var(--space-step-1));margin-bottom:calc(-1 * var(--space-step-1))}
[data-component="mediaCard"] .act .foot{margin-top:0;padding-top:0}
/* the close is a square the height of the field, so it is a target and not a glyph */
[data-component="mediaCard"] .ask .shut{inline-size:var(--size-control-small);block-size:var(--size-control-small);justify-content:center;padding:0}
[data-component="mediaCard"] .act > *{grid-area:1 / 1}
[data-component="mediaCard"] .act .foot{transition:transform var(--motion-duration-short) var(--motion-ease-settle),opacity var(--motion-duration-short) var(--motion-ease-settle)}
[data-component="mediaCard"] .ask{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-step-1);align-self:start;
 transform:translateX(var(--space-step-3));visibility:hidden;
 transition:transform var(--motion-duration-short) var(--motion-ease-settle),opacity var(--motion-duration-short) var(--motion-ease-settle),visibility var(--motion-duration-0) var(--motion-duration-short)}
[data-component="mediaCard"] .ask [data-component="field"]{flex:1 1 0;min-width:0}
[data-component="mediaCard"] .ask .note{flex:0 0 100%;margin:0}
[data-component="mediaCard"] .ask .note:empty{display:none}
[data-component="mediaCard"] .ask[data-sent] :is([data-component="field"],[type="submit"]){display:none}
[data-component="mediaCard"] .ask[data-sent] .note{flex:1 1 0}
[data-component="mediaCard"][data-asking] .act .foot{transform:translateX(calc(-1 * var(--space-step-3)));visibility:hidden}
[data-component="mediaCard"][data-asking] .ask{transform:none;visibility:visible;transition-delay:var(--motion-duration-0)}
@media (prefers-reduced-motion:reduce){[data-component="mediaCard"] :is(.act .foot,.ask){transition:none}}
/* THE PANEL FORM (owner, 2026-09-28): a card read on its own, standing on a raised surface with
   its words inset from a hairline edge - /drop's faults, modules and steps. A grid of panels keeps
   the grid's own gap; the panel only draws the box each card sits in. */
[data-component="mediaCard"][data-form="panel"]{padding:var(--space-step-4);border-radius:var(--radius-surface);border:var(--border-width-hairline) solid}
/* THE ROW FORM (owner, 2026-09-28): a card laid along one line of a ledger - its code, its name,
   what it does and its figure hung at the line's end - between hairlines rather than in a box.
   /drop's four faults. The mark is the panel's, so a ledger line carries none. */
[data-component="mediaCard"][data-form="row"]{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,2.4fr) minmax(0,3.6fr) minmax(0,1.4fr);
 column-gap:var(--space-gutter);align-items:baseline;padding:var(--space-step-4) 0;border-top:var(--border-width-hairline) solid}
[data-component="mediaCard"][data-form="row"] > .glyph{display:none}
[data-component="mediaCard"][data-form="row"] > p{margin-top:0}
[data-component="mediaCard"][data-form="row"] .foot{margin-top:0;justify-self:end;text-align:right}
[data-component="mediaCard"][data-form="row"] .meta b{display:block;margin-top:var(--space-step-1);font:var(--type-h5-font)}
@media (max-width:1100px){
 [data-component="mediaCard"][data-form="row"]{grid-template-columns:minmax(0,1fr) auto;row-gap:var(--space-step-1)}
 [data-component="mediaCard"][data-form="row"] > :is(.byline,p){grid-column:1 / -1}
}
/* the figure is set as words, not as a label: its own case and tracking */
[data-component="mediaCard"][data-form="row"] .meta b{text-transform:none;letter-spacing:0}

/* ---- components/mediaCard.skin ---- */
[data-component="mediaCard"] p{color:var(--colour-text-secondary)}
[data-component="mediaCard"] .meta{color:var(--colour-text-tertiary)}
[data-component="mediaCard"] .meta .pin{color:var(--colour-text-primary)}
[data-component="mediaCard"] .meta .dot{color:var(--colour-text-tertiary)}
[data-component="mediaCard"] .rnd.empty::after,[data-component="mediaCard"] .stage.empty::after{color:var(--colour-text-tertiary)}
[data-component="mediaCard"][data-tone="plate"]{background:var(--colour-surface-card)}
[data-component="mediaCard"] figcaption .alias{color:var(--colour-text-tertiary)}
[data-component="mediaCard"] figcaption span:not(.alias){color:var(--colour-text-secondary)}

/* the byline and the tags are the card's furniture: dim for the byline, a chip fill for a tag,
   which is section 2's one fill doing the job it exists for */
[data-component="mediaCard"] .byline{color:var(--colour-text-tertiary)}
/* the tag takes the FIELD's fill, not `--chipbg`: that token is rgba(18,18,18,.72), a dark ground
   for a chip sitting over an image, and a tag on the card's own near-black ground would disappear
   into it. --field-bg is the site's one subtle raise off the ground, which is what a chip on the
   page's own surface needs, and reusing it keeps the card and the form reading as one system. */
[data-component="mediaCard"] .tag{background:var(--field-background);color:var(--colour-text-secondary)}
[data-component="mediaCard"] > .rnd.plate{background:var(--colour-surface-card)}
[data-component="mediaCard"] .stretch{color:inherit}
[data-component="mediaCard"] .ask{opacity:0}
[data-component="mediaCard"][data-asking] .act .foot{opacity:0}
[data-component="mediaCard"][data-asking] .ask{opacity:1}
/* the panel's ground and edge, and its byline in the accent: a step's number, a fault's code */
[data-component="mediaCard"][data-form="panel"]{background:var(--colour-surface-card);border-color:var(--colour-border-default)}
[data-component="mediaCard"][data-form="panel"] .byline{color:var(--colour-accent-base)}
/* THE ACCENT PANEL: the lead tier stands on the accent with ink on it, the same inversion the
   feature panel of inlineForm makes, text and controls alike */
[data-component="mediaCard"][data-form="panel"][data-tone="accent"]{background:var(--colour-accent-base);border-color:var(--colour-accent-base);color:var(--colour-ink-900);
 --colour-text-primary:var(--colour-ink-900);--colour-text-secondary:var(--colour-ink-900);--colour-text-tertiary:var(--colour-ink-900)}
[data-component="mediaCard"][data-form="panel"][data-tone="accent"] :is(.byline,[data-component="headline"],p,li,.go){color:var(--colour-ink-900)}
/* the ledger line: its hairline, its code in the accent, its figure in the accent */
[data-component="mediaCard"][data-form="row"]{border-color:var(--colour-border-default)}
[data-component="mediaCard"][data-form="row"] :is(.byline,.meta b){color:var(--colour-accent-base)}
/* the list's marks are the accent, which vanishes on the accent panel: they take the ink there */
[data-component="mediaCard"][data-form="panel"][data-tone="accent"] [data-component="list"][data-marker="mark"] > li::before{background-color:var(--colour-ink-900)}

/* ---- patterns/nameLane.form ---- */
/* nameLane: the client roster as NAMES. It was `logoLane`, which was wrong twice - it carries no
   logos, and a logo wall is a rights question where a description is not. Names wrap in a lane at
   mono grade, separated by the gutter rather than by a rule. */
[data-component="nameLane"] ul{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:var(--space-step-3) var(--space-gutter)}
[data-component="nameLane"] li{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
/* The `#000` stops in the mask below are MECHANICS, not palette: a mask reads the alpha channel,
   so the colour is only there to mean "opaque" and a token would imply the lane's edges could be
   themed. Every other colour in this file comes from the token set.

/* `compact` is the intent the home page's roster declares - more names in less height than a
   wrapped list - and a RUNNING LANE is this component's answer to it. The demo ran one on home,
   as `.logos .lane` with a 90s linear loop, and the port rendered the wrapped list on every
   route, so the band sat still.

   The run is rendered TWICE and the lane travels exactly one run's width, which is why the loop
   has no seam: at -50% of a doubled run the second copy sits exactly where the first began.
   Hover pauses it, because a reader who stops to read a name should not have it move away. */
/* the fade is the logo track's, so the heading above it reads whole */
[data-component="nameLane"][data-variant="compact"] .lane{overflow:hidden;-webkit-mask-image:linear-gradient(var(--angle-90),transparent,#000 var(--ratio-6),#000 var(--ratio-94),transparent);mask-image:linear-gradient(var(--angle-90),transparent,#000 var(--ratio-6),#000 var(--ratio-94),transparent)}
[data-component="nameLane"][data-variant="compact"] ul{flex-wrap:nowrap;width:max-content;animation:name-lane-loop var(--motion-duration-90s) linear infinite}
[data-component="nameLane"][data-variant="compact"]:hover ul{animation-play-state:paused}
[data-component="nameLane"][data-variant="compact"] li{flex:none}
@keyframes name-lane-loop{from{transform:translateX(0)}to{transform:translateX(calc(-1 * var(--ratio-neg-50)))}}
/* the setting means stop, not slow down: a lane that never rests cannot be read by someone who
   asked for no motion, so it becomes the wrapped list it is by default */
@media (prefers-reduced-motion:reduce){
 [data-component="nameLane"][data-variant="compact"] ul{animation:none;flex-wrap:wrap;width:auto}
}

/* a mark in the lane. The live site ran each asset at its own 170x96 artboard, flex:none so the
   lane does not squeeze them, and the duplicate run is a plain span so the loop has no seam. */
[data-component="nameLane"] img{width:var(--lane-cell-width);min-height:var(--lane-row-height);flex:none;object-fit:contain}
[data-component="nameLane"] .dup{display:flex;align-items:center;gap:var(--space-step-3) var(--space-gutter)}
[data-component="nameLane"][data-variant="compact"] .dup{flex-wrap:nowrap}

/* the mark's own fade is motion, so it lives with the form even though the property it animates
   is paint: a reskin changes which ink, never how long. */
[data-component="nameLane"] img{transition:opacity var(--motion-duration-300) var(--motion-ease-settle)}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
/* ---- SECTION RHYTHM ------------------------------------------------------------------------
   The demo page gave every section its own vertical measure and the port gave every section one
   `[data-template="band"]` padding, so the home page read evenly spaced: a manifesto that needs air and a fact row
   that does not were set identically. These are the demo's own values, keyed on the COMPONENT the
   section carries rather than on a page class, so a section takes its rhythm from what it is.

   A band with no component named here keeps the default, which is the common case. */
section[data-component="nameLane"]{padding:var(--space-step-6) 0 var(--space-step-12)}
/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
/* -183px was their gap under the title against a 613px box; the box is now 100vh, so the same
   pre-pin position is -183 + 306.5 - 50vh. Once pinned the centre sits at 50vh by construction. */
/* 230x153 at their 1440 is 15.97vw on a 3:2 box. Held as a fraction of the viewport so the start
   state, and therefore the growth ratio, is the same on any screen instead of only at 1440. */

/* the client strip carries all thirty marks the live site carries, on the same continuous loop:
   two identical runs travelling one run's width, so the seam never shows. Each mark gets the same
   box so a wide wordmark and a tall roundel read at one weight, and the strip fades at both edges
   rather than being cut by the container. */
/* the live site's own metrics: each mark is its native 170x96 artboard with a 30px gap, which is
   why the marks read at full weight there and looked shrunk here at 44px */
@keyframes loop{to{transform:translateX(calc(-1 * var(--ratio-neg-100)))}}
/* its heading stands close over the logos: 24px, the gap between a group's heading and its members */
[data-component="nameLane"] > [data-component="sectionHead"]{margin-bottom:var(--space-step-3)}

/* ---- patterns/nameLane.skin ---- */
[data-component="nameLane"] li{color:var(--colour-text-secondary)}

/* the marks arrive in their own brand colours, and the register is one ink: inverted to the page's
   foreground and held at rest, coming to full strength under a pointer. The live site did exactly
   this - `filter:invert(1) grayscale(1); opacity:.7` - which is what makes thirty marks from
   thirty brands read as one row rather than as a colour chart. */
[data-component="nameLane"] img{filter:invert(1) grayscale(1);opacity:.7}
[data-component="nameLane"] img:hover{opacity:1}

/* ---- patterns/noticeBar.form ---- */
/* noticeBar: the question, asked once, at the bottom of the page. Component seam like every
   other, from the register: a notice styled unlike the site it interrupts is a notice people
   dismiss without reading. It is NOT modal - it takes no focus and blocks nothing, because a
   reader who wants the cookie policy first must be able to reach it. */
[data-component="noticeBar"]{position:fixed;left:0;right:0;bottom:0;z-index:60;border-top-width:var(--border-width-hairline);border-top-style:solid;transform:translateY(100%);transition:transform var(--motion-duration-500) var(--motion-ease-settle)}
[data-component="noticeBar"].in{transform:none}
[data-component="noticeBar"] .wrap{display:flex;gap:var(--space-gutter);align-items:center;justify-content:space-between;flex-wrap:wrap;padding:var(--space-step-4) 0}
[data-component="noticeBar"] .say{max-width:66ch}
[data-component="noticeBar"] .say p{font:var(--type-citation-font);}
[data-component="noticeBar"] .acts{display:flex;gap:var(--space-step-3);align-items:center}
[data-component="noticeBar"] .pol{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
@media (prefers-reduced-motion:reduce){
 [data-component="noticeBar"]{transition:none;transform:none}
}
/* BELOW 760 THE POLICY LINK TAKES ITS OWN LINE and the two answers share the next one, half each.
   On one line the three measured 363px against a 358px wrap at 390, so the accept button ran off
   the edge - clipped, not scrolled, because the body clips its overflow - and the link broke
   onto two lines beside it. */
@media (max-width:760px){
 [data-component="noticeBar"] .acts{width:100%;flex-wrap:wrap;gap:var(--space-step-2)}
 [data-component="noticeBar"] .pol{flex-basis:var(--ratio-100)}
 [data-component="noticeBar"] .acts > [data-component="button"]{flex:1}
}

/* ---- patterns/noticeBar.skin ---- */
[data-component="noticeBar"]{background:var(--colour-surface-page);border-top-color:var(--colour-border-default)}
[data-component="noticeBar"] .say p{color:var(--colour-text-secondary)}
[data-component="noticeBar"] .pol{color:var(--colour-text-secondary)}
[data-component="noticeBar"] .pol:hover{color:var(--colour-text-primary)}

/* ---- patterns/noticePanel.form ---- */
/* noticePanel: what a reader sees instead of a closed case study. Deliberately NOT a full-page
   interstitial - the hero above it stays, so the client description, the sector and the headline
   are still readable and the reader can tell whether the request is worth making. That split is
   the live site's and it is the right one. */
/* A REAL TOKEN, NOT A FALLBACK. This read `var(--space-section,96px)` and `--sec` was defined nowhere, so
   every access gate padded itself with 96px - a number off the 4.5px unit, arriving by the exact
   route §13.1 warns about: "a token that does not exist fails silently to an inherited value".
   `--sec` is 94.5px now, the contract's own margin.y, and it is defined. */
[data-component="noticePanel"]{padding:var(--space-step-6) 0 var(--space-step-12)}
/* what the gate holds is a formPanel, spaced as the contact form is; the panel sets only its measure */
[data-component="noticePanel"] .wrap{max-width:var(--grid-column-6)}

@media (max-width:1100px){[data-component="noticePanel"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){[data-component="noticePanel"]{padding:var(--space-step-6) 0 var(--space-step-6)}}

/* ---- patterns/noticePanel.skin ---- */
/* noticePanel holds no paint of its own: the formPanel inside it is painted by its own sheets */

/* ---- patterns/preloader.form ---- */
/* preloader: a yellow panel with a black wordmark that slides away. The page behind it is black,
   so the yellow-to-black change is the panel LEAVING rather than a colour animation.

   There is no cross-fade, because every version of one was dirty: interpolating the ground
   yellow-to-black cuts through olive in sRGB, and `invert(1)` to `invert(0)` takes the white
   artwork through mid grey on the way. Nothing interpolates a colour here now. One transform. */
[data-component="preloader"]{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;transition:transform var(--motion-duration-620) cubic-bezier(.65,0,.35,1)}
[data-component="preloader"] span{overflow:hidden;display:inline-block}
[data-component="preloader"] .mark{height:var(--bar-nav-height);transform:translateY(var(--ratio-110));animation:up var(--motion-duration-600) cubic-bezier(.22,1,.36,1) forwards}
/* once it has run the preloader is spent: it keeps its box for the transition but must not take
   the pointer, which it was doing, swallowing every hover on the page below it */
[data-component="preloader"][data-done]{transform:translateY(calc(-1 * var(--ratio-neg-100)));pointer-events:none}
@media (prefers-reduced-motion:reduce){
 [data-component="preloader"]{transition:none}
 [data-component="preloader"] .mark{animation:none;transform:none}
}

/* ---- patterns/preloader.skin ---- */
[data-component="preloader"]{background:var(--colour-accent-base)}
[data-component="preloader"] .mark{filter:invert(1)}

/* ---- atoms/quote.form ---- */
/* quote: a blockquote - a passage in someone else's words.

   IT OWNED NOTHING, and was excepted from the ownership rule on the grounds that no document held
   one. A document does: the JioBlackRock case study carries a pull quote, so the atom was
   rendering while its exception said it could not. What the two containers do with it differs -
   prose insets it behind a rule, pullQuote sets it at the subsection grade in a wide measure - and
   those are the containers' decisions.

   WHAT IS THE ATOM'S IS THE RESET. A blockquote arrives with 40px of UA margin on all four sides,
   so every container that held one had to cancel it first; an atom that needs its container to
   stop looking like a browser default owns nothing. Same rule as list.form.css, same reason. */
[data-component="quote"]{margin:0}

/* AND THE GRADE IT IS NAMED FOR. The register has carried `--type-quote-*` the whole time - 36px
   at the light weight on a 1.20 leading - and nothing read it, so a quote standing on its own
   drew at body: 17px at 300, indistinguishable from the paragraph before it. A grade named for
   one part and used by none is a decision made and then dropped.
   The two containers still decide otherwise, and both out-specify this: prose sets a quoted line
   at the prose size behind a rule, and pullQuote sets its own subsection grade. This is the
   floor for a quote with no container to speak for it. */
[data-component="quote"]{font:var(--type-quote-font)}

/* AND ITS MARKS. A blockquote is a passage in someone else's words and nothing on the page said
   so except its indent, which prose owns and pullQuote does not use - so lifted out of either
   container the quote read as a large paragraph.

   `open-quote` and `close-quote` rather than characters typed into the content: the marks are
   presentation, the words are the document's, and a curly mark pasted into a case study is one
   an editor can delete by accident. `quotes` names the pair, so the atom decides which glyph
   rather than inheriting whatever the locale hands it. */
[data-component="quote"]{quotes:"\201C" "\201D" "\2018" "\2019"}
[data-component="quote"]::before{content:open-quote}
[data-component="quote"]::after{content:close-quote}

/* ---- atoms/quote.skin ---- */
/* quote: the marks are the accent and the words are not. A quotation mark is punctuation doing
   a job - it says these words are borrowed - and the accent is how this system marks the thing
   that does the work, the way a list's dot and its numerals do. The passage itself stays at the
   text colour it inherits, because colouring a whole quote would make it a pull quote. */
[data-component="quote"]::before,[data-component="quote"]::after{color:var(--colour-accent-base)}

/* ---- patterns/prose.form ---- */
/* THE MEASURE IS EIGHT COLUMNS. Prose ran the full content width - 1,410px, 122 characters a
   line - against the 45 to 75 a reader can hold, and loosening the leading made that worse
   rather than better: the eye travels further across AND further down. Eight columns is what
   the live site sets body copy to. */
/* prose: a richText field rendered.
   `detailed` is the intent a clause set declares - a document read BY SECTION rather than start to
   finish - and ruled sections at bodyS are this component's answer to it. The variant used to be
   called `sectioned`, which named its own appearance: one of exactly four values a variant may
   take, and never a layout. So the rules were unreachable, because no document could legally ask
   for them. One measure, one rhythm, and the body copy sits at --muted
   with emphasis at --fg, which is the house rule that weight is not emphasis stated in colour.
   The legal variant is the same prose cut into ruled sections at bodyS, which is a document that
   is read by section rather than start to finish, not a second prose style. */
[data-component="prose"]{max-width:var(--grid-column-7)}
/* THE RHYTHM (owner, 2026-09-24), every gap a step: 64 above a heading that opens a new passage,
   16 from a heading to its first paragraph, 24 between paragraphs and before a list, nothing above
   the first thing in the prose */
[data-component="prose"] h2{margin-top:var(--space-step-8)}
[data-component="prose"] > :first-child{margin-top:0}
/* the measure is the grid's where a page puts prose in a column: a max-width inside a seven
   column track constrains it twice and the copy gets taller for no reason */
/* AND A LONG DOCUMENT IS EIGHT COLUMNS, not uncapped. `detailed` released the measure entirely,
   so a case study or a reference entry - the longest reading on the site - ran the full 1,410px
   at 122 characters a line, against the 45 to 75 a reader can hold. Eight columns is what the
   live site sets body copy to. */
[data-component="prose"][data-variant="detailed"]{max-width:none}
[data-component="prose"] p{margin-top:var(--space-step-3);font:var(--type-body-font);}
[data-component="prose"] h2 + :is(p,ul,ol){margin-top:var(--space-step-2)}
[data-component="prose"] strong{font-weight:500}
/* the list sits on the column: its markers on the text's left edge, the words one marker column in
   (owner, 2026-09-25), so nothing hangs off the grid */
[data-component="prose"] :is(ul,ol){margin-top:var(--space-step-3);font:var(--type-body-font);}
[data-component="prose"][data-variant="detailed"] p a{text-decoration:underline;text-underline-offset:var(--size-3);transition:text-decoration-color var(--motion-duration-250) var(--motion-ease-settle)}
[data-component="prose"] figcaption{margin-top:var(--space-step-3);font:var(--type-meta-font);}
[data-component="prose"][data-variant="detailed"] .sec{padding:var(--space-step-7) 0;border-top-width:var(--border-width-hairline);border-top-style:solid}
[data-component="prose"][data-variant="detailed"] .sec:first-child{padding-top:0}
/* A LONG DOCUMENT'S RUNNING TEXT IS BODY. The detailed variant set it one step down,
   which was the old `bodyS` role: 15px for 174 case studies and reference entries,
   the longest reading on the site set smaller than the shortest. The tighter stack
   stays, because a document with forty paragraphs needs it; the size does not. */
[data-component="prose"][data-variant="detailed"] p{margin-top:var(--space-step-3);font:var(--type-body-font);}
/* a section's rule already separates it, so its heading carries no top margin: prose's own 54px
   above an h2 is the space an article needs between passages of one argument */
/* THE RHYTHM (owner, 2026-09-25), the same steps as the default prose: 64 above a section heading,
   32 above an inline heading, 16 from any heading to what it heads, 24 between paragraphs */
[data-component="prose"][data-variant="detailed"] h2{margin-top:var(--space-step-8)}
[data-component="prose"][data-variant="detailed"] :is(h3,h4){margin-top:var(--space-step-4)}
[data-component="prose"][data-variant="detailed"] :is(h2,h3,h4) + *{margin-top:var(--space-step-2)}
[data-component="prose"][data-variant="detailed"] > :first-child{margin-top:0}
[data-component="prose"][data-variant="detailed"] dt{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase}
[data-component="prose"][data-variant="detailed"] dd{margin:var(--space-step-1) 0 0;font:var(--type-citation-font);}
[data-component="prose"][data-variant="detailed"] .sec:first-child{border-top:0}
/* a code span inside prose. The reference shelf is written in file and token names - 22 of them
   on one page - and they arrived as markdown that the port escaped and printed, backticks and
   all. Rendered now, so they need the mono face: the same one the labels use, at the prose
   size, because an inline token sits in a sentence rather than on its own line. */
[data-component="prose"] code{font-family:var(--type-family-mono)}
/* a quote inside prose. The case studies carry 60 quote blocks - what was said to leadership,
   the finding a number rests on - and the port printed the marker and set them as paragraphs, so
   a quotation read as a typo. Indented against a rule, at the prose size: this is a quoted line
   in an argument, not a pull quote, which is its own component and its own scale. */
/* AND PROSE SAYS ITS SIZE OUT LOUD. The comment above has always said "at the prose size", and
   that was true only because the atom set no size at all; now that it carries the quote grade,
   a quoted line inside an argument would have jumped to 36px in 60 places. The size is the
   container's decision and it is written here rather than assumed. */
[data-component="prose"] blockquote{padding-left:var(--space-step-3);border-left-width:var(--size-2);border-left-style:solid;font:var(--type-body-font)}
[data-component="prose"] blockquote + blockquote{margin-top:var(--space-step-4)}
/* the dot on the column's edge itself, not centred in its marker column */
[data-component="prose"] [data-component="list"][data-marker="mark"] > li::before{left:0}
/* A PICTURE IN THE PROSE (owner, 2026-09-25): 48 above and below; on the body column, or bleeding from
   the column's right edge out to the page's left edge */
[data-component="prose"] > figure.plate{margin:var(--space-step-6) 0 0}
/* a picture stands one section step from whatever follows it, as from what precedes it */
[data-component="prose"] > figure.plate + *{margin-top:var(--space-step-6)}
/* A TABLE IS SPACED LIKE A PICTURE (owner, 2026-09-25): a paragraph ran straight into its top
   rule at 0. It stands 48 clear of the text on both sides; under its own heading it keeps the 16 a
   heading gives what it introduces, and a heading after it keeps its own spacing. */
[data-component="prose"] > [data-component="factTable"]{margin-top:var(--space-step-6)}
[data-component="prose"] > :is(h2,h3,h4) + [data-component="factTable"]{margin-top:var(--space-step-2)}
[data-component="prose"] > [data-component="factTable"] + :not(h2,h3,h4){margin-top:var(--space-step-6)}
/* the offset picture (owner, 2026-09-25): on the grid, ten columns from the grid's left edge, 3:2 - it
   sits left of the body column and ends on column 10.
   BOTH BREAKOUTS COUNT IN THE BODY COLUMN'S OWN PERIOD. The body is seven columns, so one period
   is (100% + gutter) / 7 of it; three periods back is the grid's left edge, and ten or twelve
   periods less a gutter is the picture. At 1440 that is 360, 1170 and 1410, the pixel values these
   held, and between 1101 and 1439 the picture shrinks with the grid instead of running off it. */
[data-component="prose"] > figure.plate[data-form="offset"]{margin-left:calc((100% + var(--grid-gutter)) * -3 / 7);
 width:calc((100% + var(--grid-gutter)) * 10 / 7 - var(--grid-gutter))}
@media (max-width:1100px){[data-component="prose"] > figure.plate[data-form="offset"]{margin-left:0;width:auto}}
/* full width: the page's whole content width, from the grid's left edge to its right */
[data-component="prose"] > figure.plate[data-form="full"]{margin-left:calc((100% + var(--grid-gutter)) * -3 / 7);
 width:calc((100% + var(--grid-gutter)) * 12 / 7 - var(--grid-gutter))}
@media (max-width:1100px){[data-component="prose"] > figure.plate[data-form="full"]{margin-left:0;width:auto}}
[data-component="prose"] > figure.plate > figcaption{margin-top:var(--space-step-1)}
/* A CASE STUDY'S FIGURES OPEN ITS BODY (owner, 2026-09-25): each numeral on the column's edge, what it
   measures beside it, and the source under that */
/* A PAIR (owner, 2026-09-25), after the live case study: two pictures five columns each with the
   gutter between, flush with the grid's left edge, or ending on its right edge. One column below 760 */
[data-component="prose"] > figure.plate > .pair{display:grid;grid-template-columns:1fr 1fr;column-gap:var(--grid-gutter);align-items:start}
@media (max-width:760px){[data-component="prose"] > figure.plate > .pair{grid-template-columns:1fr;row-gap:var(--space-step-2)}}
/* the sign-off: 48 under the last paragraph, the signature three columns wide at its own shape,
   the role 8 under it (owner, 2026-09-25) */
[data-component="prose"] > .signoff{margin-top:var(--space-step-6)}
[data-component="prose"] > .signoff > img[data-component="media"][data-kind="image"]{inline-size:var(--grid-column-3);block-size:auto;aspect-ratio:auto}
[data-component="prose"] > .signoff > figcaption{margin-top:var(--space-step-1);display:grid;row-gap:var(--space-step-1)}

/* ---- patterns/prose.skin ---- */
[data-component="prose"] p{color:var(--colour-text-secondary)}
[data-component="prose"] strong{color:var(--colour-text-primary)}
[data-component="prose"] ul{color:var(--colour-text-secondary)}
[data-component="prose"] li::marker{color:var(--colour-text-tertiary)}
[data-component="prose"][data-variant="detailed"] p a{color:var(--colour-text-primary);text-decoration-color:var(--colour-border-default)}
[data-component="prose"][data-variant="detailed"] p a:hover{text-decoration-color:var(--colour-text-primary)}
[data-component="prose"] figcaption{color:var(--colour-text-tertiary)}
[data-component="prose"][data-variant="detailed"] .sec{border-top-color:var(--colour-border-default)}
[data-component="prose"][data-variant="detailed"] dt{color:var(--colour-text-primary)}
[data-component="prose"][data-variant="detailed"] dd{color:var(--colour-text-secondary)}
/* the quote's rule and its voice: dimmer than the prose around it, because a quotation is
   somebody else speaking inside the argument */
[data-component="prose"] blockquote{border-left-color:var(--colour-border-default);color:var(--colour-text-secondary)}
/* the signature is drawn in black; inverted it takes the page's light text, as the live page masks it */
[data-component="prose"] > .signoff > img{filter:invert(1)}
[data-component="prose"] > .signoff .name{color:var(--colour-text-primary)}
[data-component="prose"] > .signoff .role{color:var(--colour-text-tertiary)}

/* ---- patterns/pullQuote.form ---- */
/* the statement reads a word at a time as it passes: it is one sentence and it is the page's
   argument, so it earns the attention a fade does not ask for. */
[data-component="pullQuote"][data-form="statement"] [data-component="headline"]{max-width:var(--grid-column-8)}
[data-component="pullQuote"][data-form="statement"] [data-component="headline"] .w{transition:color var(--motion-duration-250) var(--motion-ease-settle)}
/* NOT RECOVERED, deliberately: `.manifesto .fade`, a gradient that ran the statement from --fg to
   --dim through background-clip:text. It is in the demo's stylesheet and NO demo markup carries the
   class, so it was dead there too. Porting it would have been recovering an intention rather than
   a design, and the word-by-word lighting in home-hero.js is what that sentence actually does.

/* pullQuote: a testimonial or a client line, at h3 on nine columns. Two pages carried it
   byte-identically, which is two chances to drift rather than consistency. */
/* THE QUOTE ROLE, not the h2. A pull quote borrowed the heading grade, which is a medium weight
   saying THIS IS A SECTION; the quote role is lighter and a grade smaller, which is a voice
   rather than a structure. It existed and nothing picked it. */
/* THE GRADE IS THE ATOM'S NOW. This named `--type-quote-font` itself, which is the quote atom's
   own floor since it started reading the grade it is named for - so the pattern was restating
   the voice and keeping only the measure. The measure is genuinely the pattern's: nine columns
   is where a testimonial sits on this page, and that is a layout decision. */
[data-component="pullQuote"] blockquote{max-width:var(--grid-column-9)}
[data-component="pullQuote"] cite{display:block;font-style:normal;margin-top:var(--space-step-6);font:var(--type-meta-font);}
[data-component="pullQuote"] cite b{font-weight:400;display:block}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
section[data-component="pullQuote"][data-form="statement"]{padding:var(--space-step-6) 0 var(--space-step-12)}

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){section[data-component="pullQuote"][data-form="statement"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){section[data-component="pullQuote"][data-form="statement"]{padding:var(--space-step-6) 0 var(--space-step-6)}}

/* ---- patterns/pullQuote.skin ---- */
/* the words not yet read rest at the resting text colour; a read word takes the headline's own */
[data-component="pullQuote"][data-form="statement"] [data-component="headline"] .w{color:var(--colour-text-resting)}
[data-component="pullQuote"][data-form="statement"] [data-component="headline"] .w.on{color:inherit}
[data-component="pullQuote"] cite{color:var(--colour-text-tertiary)}
[data-component="pullQuote"] cite b{color:var(--colour-text-primary)}

/* ---- patterns/rankList.form ---- */
/* rankList: TYP-03 Hierarchy Reflow. A stack of destinations where ONE of them is promoted at a
   time, and the promotion is a single number - `data-rank` on the stack - that drives the grid
   rows, the type size, the variable-font axes and the colour through CSS transitions. So the
   clock's move and a pointer's promotion are the same interpolation rather than two systems
   arguing about the same element.

   Lifted from home-register.html, where it was 14 page rules and an inline driver, and lost
   entirely in the headless pass: the home page's close became three links in a ctaRow. This is
   the most distinctive interaction on the site and nothing could ask for it.

   The type scales in `cqw` against the stack's own container, which is why the stack declares
   `container-type`: the promoted line is 7.4% of the stack's width and the resting lines are
   2.3%, so the proportion holds at any measure rather than at 1440 alone.

   2.88s is the reflow duration and `--ez-reflow` is its curve, both already in the token set -
   TYP-03's own, used by the specimen sheet that documents this behaviour. */
[data-component="rankList"] h3{margin-bottom:var(--space-step-4)}
[data-component="rankList"] .rows{container-type:inline-size;display:grid;min-height:var(--rank-stage-height);grid-template-rows:minmax(max-content,1fr) minmax(max-content,1fr) minmax(max-content,1fr);transition:grid-template-rows var(--motion-duration-2s) var(--motion-ease-reflow)}
[data-component="rankList"] .rows[data-rank="0"]{grid-template-rows:minmax(max-content,4fr) minmax(max-content,1fr) minmax(max-content,1fr)}
[data-component="rankList"] .rows[data-rank="1"]{grid-template-rows:minmax(max-content,1fr) minmax(max-content,4fr) minmax(max-content,1fr)}
[data-component="rankList"] .rows[data-rank="2"]{grid-template-rows:minmax(max-content,1fr) minmax(max-content,1fr) minmax(max-content,4fr)}
[data-component="rankList"] a{display:grid;grid-template-columns:1fr auto;align-items:start;column-gap:var(--space-step-7);padding:var(--space-step-4) var(--type-step-1) var(--space-step-4) 0;border-top-width:var(--border-width-hairline);border-top-style:solid;cursor:pointer;overflow:hidden;transition:color var(--motion-duration-2s) var(--motion-ease-reflow)}
[data-component="rankList"] a:last-of-type{border-bottom-width:var(--border-width-hairline);border-bottom-style:solid}
[data-component="rankList"] a .lead{display:block;font-family:var(--type-family-display);font-weight:var(--type-body-weight);line-height:var(--rank-lead-leading);font-size:max(var(--rank-lead-size-rest),var(--type-h5-size));font-variation-settings:"wght" 400,"opsz" 12,"wdth" 100;letter-spacing:0;transition:font-size var(--motion-duration-2s) var(--motion-ease-reflow),font-variation-settings var(--motion-duration-2s) var(--motion-ease-reflow),
  letter-spacing var(--motion-duration-2s) var(--motion-ease-reflow)}
[data-component="rankList"] a small{display:block;font:var(--type-citation-font);font-weight:var(--type-body-weight);text-transform:none;overflow:hidden;max-height:0;margin-top:0;transition:opacity var(--motion-duration-1s) var(--motion-ease-reflow),max-height var(--motion-duration-2s) var(--motion-ease-reflow),
  margin-top var(--motion-duration-2s) var(--motion-ease-reflow)}
/* the promoted line: full weight, display optical size, and its note revealed */
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1) .lead,
[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2) .lead,
[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3) .lead{font-size:max(var(--rank-lead-size-promoted),var(--type-h2-size));font-variation-settings:"wght" 700,"opsz" 144,"wdth" 96;letter-spacing:-.03em}
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1) small,
[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2) small,
[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3) small{max-height:var(--space-step-5);margin-top:var(--space-step-3)}
[data-component="rankList"] a .go{display:flex;margin-top:var(--space-step-1);transition:transform var(--motion-duration-400) var(--motion-ease-settle)}
[data-component="rankList"] a:hover .go{transform:translateX(var(--motion-travel-base))}
/* reduced motion: the rank still promotes, it simply does not interpolate, and the driver does
   not run its clock. A stack that reflows every 7 seconds under that setting is the fault the
   setting exists to prevent. */
@media (prefers-reduced-motion:reduce){
 [data-component="rankList"] .rows,[data-component="rankList"] a,
 [data-component="rankList"] a .lead,[data-component="rankList"] a small{transition:none}
}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
section[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-12)}
/* ---- /* SCOPED TO rankList. These were page CSS on the home route, so `.end` meant home's close
   and nothing else. In a component sheet `.end` also matched ctaRow, whose close carries the
   same class - so `.end a{display:grid}` turned /drop's and /private-equity's single close
   button from 144px into the full 1410px measure. A selector carries its page's scope
   implicitly; moving it makes that scope explicit or it leaks. */
MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
/* manifesto */

/* services: pinned list left, a column of 835x470 mockups scrolling right, var(--size-845) apart */

/* work: sticky stage, portrait cards 420x492, rail enters from +var(--size-200) */
/* the arrow runs on hover, and the rule under the label follows the label, not the arrow */

/* testimonial */

/* method: stacked cards */

/* insights */

/* cta rows, on the TYP-03 Hierarchy Reflow motion: three lines share one fixed frame and rank
   rotates among them. The frame's row heights re-apportion, 4fr 1fr 1fr through 1fr 1fr 4fr, and
   whichever line holds the tall row grows to display size and full weight while the other two
   compress to caption grade and grey down. Nothing slides: the type resizes in place on the same
   var(--motion-duration-21-6) clock, one var(--motion-duration-7-2) turn each, var(--ratio-60) hold and var(--ratio-40) move, with the lines offset a turn apart. */
[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-12)}/* one state, one ease. The frame carries a rank on data-rank and everything else is a transition
   off it, so the loop's move and a hover promotion are literally the same 2.88s interpolation.
   Keyframes could not do both: a running animation outranks author rules, so hover had to fight
   it with !important, and a property under an animation ignores its transition, which is what
   made the hover snap while the loop eased. */
/* house style: content hangs off its rule rather than floating in the row. The line sits a fixed
   18px drop below the hairline and the row's slack falls to the bottom, so a row reads as a
   hanging label whatever height the reflow has given it. The row clips vertically so the
   description can collapse, which also clips the arrow's hover travel, hence 12px of right pad. */
[data-component="rankList"] .rows > a:last-of-type{border-bottom-width:var(--border-width-hairline);border-bottom-style:solid}
@media (prefers-reduced-motion:reduce){.end .rows{height:auto;grid-template-rows:none;transition:none}}
@media (prefers-reduced-motion:reduce){.end a{padding:var(--space-step-6) 0}}
@media (prefers-reduced-motion:reduce){.end a .lead,[data-component="rankList"] a small,[data-component="rankList"] .rows{transition:none}}
@media (prefers-reduced-motion:reduce){.end a .lead{font-size:var(--rank-index-size);font-variation-settings:"wght" 500,"opsz" 32}}
@media (prefers-reduced-motion:reduce){.end a small{max-height:none;margin-top:var(--space-step-2)}}

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){section[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-9)}[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){section[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-6)}[data-component="rankList"]{padding:var(--space-step-6) 0 var(--space-step-6)}}
/* THE REFLOW SIZES HAVE A FLOOR, AND A PHONE HAS NO STAGE. The leads are set in container widths,
   2.3 and 7.4 per cent, which is 32 and 104px at 1410 and 8 and 26px in a 358px column - "Start a
   project" at 8px under a 26px "Browse services". Neither falls below its grade now: h5 at rest, h2
   promoted. And below 760 - on mobile only (owner, 2026-09-27) - the 392px stage the promoted row
   stretches into is dropped, so the
   rows stand at their own height instead of 300px of dark under the first. */
@media (max-width:760px){
 [data-component="rankList"] .rows,[data-component="rankList"] .rows[data-rank]{min-height:0;grid-template-rows:none}
}

/* ---- patterns/rankList.skin ---- */
[data-component="rankList"] a{border-top-color:var(--colour-border-default);color:var(--colour-text-tertiary)}
[data-component="rankList"] a:last-of-type{border-bottom-color:var(--colour-border-default)}
[data-component="rankList"] a small{color:var(--colour-text-tertiary);opacity:0}
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1),
[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2),
[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3){color:var(--colour-text-primary)}
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1) small,
[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2) small,
[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3) small{opacity:1}
/* PAINT THAT CAME OFF THE HOME PAGE, and scoped to rankList while it moved.
   These were page CSS on the home route, so `.end` meant home's close and nothing else. In a
   component sheet `.end` also matches ctaRow, whose close carries the same class, so the
   moved geometry turned /drop's and /private-equity's single close button from 144px into
   the full 1410px measure. A selector carries its page's scope implicitly, and moving it has
   to make that scope explicit or it leaks.
   The form sheet holds where these sit; this holds how they look. */
/* one state, one ease. The frame carries a rank on data-rank and everything else is a transition
   off it, so the loop's move and a hover promotion are literally the same 2.88s interpolation.
   Keyframes could not do both: a running animation outranks author rules, so hover had to fight
   it with !important, and a property under an animation ignores its transition, which is what
   made the hover snap while the loop eased. */
/* house style: content hangs off its rule rather than floating in the row. The line sits a fixed
   18px drop below the hairline and the row's slack falls to the bottom, so a row reads as a
   hanging label whatever height the reflow has given it. The row clips vertically so the
   description can collapse, which also clips the arrow's hover travel, hence 12px of right pad. */
[data-component="rankList"] a{color:var(--colour-text-tertiary)}
/* demoted is the resting state; promotion is the exception, so the caption grade is the base */
/* the promoted line, whether the clock chose it or the pointer did */
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1),[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2),[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3){color:var(--colour-text-primary)}
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1) small,[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2) small,[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3) small{opacity:1}
[data-component="rankList"] a{color:var(--colour-text-primary)}
[data-component="rankList"] a small{opacity:1}
/* the promoted row's own colour. Its geometry stayed in the form sheet; only the paint is
   here, which is the split the gate enforces and my splitter missed because the rule sits
   behind its own comment and the comment read as the selector. */
[data-component="rankList"] .rows[data-rank="0"] a:nth-child(1),[data-component="rankList"] .rows[data-rank="1"] a:nth-child(2),[data-component="rankList"] .rows[data-rank="2"] a:nth-child(3){color:var(--colour-text-primary)}
[data-component="rankList"] .rows > a:last-of-type{border-bottom-color:var(--colour-border-default)}

/* ---- components/reviewNote.form ---- */
/* the review view's own banner. A page that looks open without saying why is a screenshot waiting
   to be filed as a broken gate. */
/* A FIELD'S BOX, IN THE ALERT'S COLOURS (owner, 2026-09-24). The note is shaped as the field atom
   is, from the field's own tokens - its hairline, its 50px control height, its inline inset and
   optical lift, its control corner - so it reads as the same family as the form it stands in for.
   The paint is the register's alert: the refused edge a field takes, and the faint warning ground.
   Its label and its line sit on one baseline, 8px apart. */
[data-component="reviewNote"]{display:flex;align-items:center;min-height:var(--size-control);
 padding:0 var(--field-inset-inline) var(--button-optical-lift);
 border-width:var(--border-width-hairline);border-style:solid;border-radius:var(--radius-control)}
[data-component="reviewNote"] .wrap{display:flex;gap:var(--space-step-1);align-items:baseline;flex-wrap:wrap}
[data-component="reviewNote"] [data-component="label"]{margin-bottom:0}
[data-component="reviewNote"] [data-component="text"]{margin:0}
[data-component="reviewNote"] code{font-family:var(--type-family-mono)}

/* ---- components/reviewNote.skin ---- */
[data-component="reviewNote"]{background:var(--colour-feedback-warn-quiet);border-color:var(--field-border-error)}
[data-component="reviewNote"] [data-component="label"]{color:var(--field-text-error)}
[data-component="reviewNote"] [data-component="text"]{color:var(--colour-text-primary)}

/* ---- patterns/scrollIndex.form ---- */
/* scrollIndex: a sticky list of titles beside a column of frames, the row for whichever frame is
   crossing the line marked and pointed at. It was 26 page rules in work.html under `.feat`, so the
   most distinctive layout on the site lived in one page and could not be asked for by a document.
   Asked for now by a module's variant, which is the field every module already carries.

   THE FRAME ITSELF IS A mediaCard (frameCard until 2026-09-23, retired as a second card). Sixteen of this file's rules described what is inside one frame -
   its plate, its chip, its heading grade, its meta row, its lock - so the pattern held a column of
   unnamed groups and the tier table recorded it as composed of atoms. What stays here is the two
   columns, the sticky index, and the RHYTHM between frames, which is the column's decision. */
/* ON THE GRID (owner, 2026-09-24): the index four columns, the frames eight, the gutter between */
[data-component="scrollIndex"] .cols{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--space-gutter);row-gap:var(--space-step-6);align-items:stretch}
[data-component="scrollIndex"] .side{grid-column:span 4}
[data-component="scrollIndex"] .items{grid-column:span 8}
[data-component="scrollIndex"] .side{position:relative;height:100%}
[data-component="scrollIndex"] [data-component="mediaCard"]{padding-bottom:var(--space-step-12)}
[data-component="scrollIndex"] [data-component="mediaCard"]:last-child{padding-bottom:0}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. A component's own breakpoints
   belong in the component's own sheet. */
@media (max-width:1100px){
 [data-component="scrollIndex"] :is(.side,.items){grid-column:1 / -1}
  [data-component="scrollIndex"] [data-component="mediaCard"]{padding-bottom:var(--space-step-9)}
}
section[data-component="scrollIndex"]{padding:var(--space-step-6) 0 var(--space-step-12)}

/* THE ROW IS A BUTTON NOW, so it has to look like the row it replaced rather than like a button:
   a button brings a UA border, a UA background, centred text and its own font, and none of that
   belongs on a line of an index. What it brings that the `<li>` could not is the tab order, Enter
   and Space, and a focus ring - which is the whole point.

   The focus ring is the contract's, section 5.1: 3px of accent at 2px offset, because focus is
   architecture and takes the structural weight. */

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){[data-component="scrollIndex"] [data-component="mediaCard"]{padding-bottom:var(--space-step-9)}section[data-component="scrollIndex"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){[data-component="scrollIndex"] [data-component="mediaCard"]{padding-bottom:var(--space-step-6)}section[data-component="scrollIndex"]{padding:var(--space-step-6) 0 var(--space-step-6)}}
/* A FRAME'S SUMMARY READS AT THE MEASURE. The frame is the content width, so at 1920 its paragraph
   ran 114 to 125 characters a line; six columns holds it near the prose measure at any width. */
[data-component="scrollIndex"] [data-component="mediaCard"] > [data-component="text"]{max-width:var(--grid-column-6)}

/* ---- patterns/scrollIndex.skin ---- */
/* scrollIndex holds no paint of its own: indexList and the mediaCards carry theirs */

/* ---- patterns/sectionHead.form ---- */
/* sectionHead: the label, heading and standfirst a section opens with. /drop drew it eight times
   as `[data-template="band"] .sec-head` and it was the last shared object still living in a page: nine other
   routes open a section the same way and reached for `.head`, which is a different thing - a
   baseline-aligned row with something on the right. This is the stacked one.
   h2 is sized off the h3 grade rather than the tag's house default of 58.5px, because eight
   sections at hero scale flatten a page's hierarchy: the page keeps one big statement, in its
   masthead, and every section below it speaks one grade quieter. */
[data-component="sectionHead"]{max-width:var(--grid-column-9);margin-bottom:var(--space-step-5)}
/* THE PART GAP (owner, 2026-09-24): label, title and line are the head's three parts, 8px apart,
   as linkPanel's and markedRow's are. The label dropped its own 32px and the line stood 32px under
   the title. The line keeps the citation role; the atom draws it. */
[data-component="sectionHead"] > [data-component="label"]{margin-bottom:var(--space-step-1)}
[data-component="sectionHead"] > [data-component="subline"]{margin-top:var(--space-step-1)}
[data-component="sectionHead"] > [data-component="text"]{margin-top:var(--space-step-1);max-width:var(--grid-column-7);font:var(--type-citation-font)}

/* ---- components/callBooker.form ---- */
/* callBooker: the "book a call" panel in the footer rail - a heading, what it costs you in time,
   and the slots on offer.
   
   IT WAS AN UNMARKED DIV inside siteFooter, so every rule that made it a panel was written as
   `[data-component="siteFooter"] .booker` and the component inventory could not draw it at all:
   the owner looked at the footer exhibit and correctly said the booking form was missing. It
   renders on all 17 routes and always has. A thing with its own heading, its own explanation and
   its own affordance is a component, not a class inside another one.
   
   The rules below are the ones that were in siteFooter, with the parent selector taken off.
*/
/* A STACK, NOT A SEQUENCE OF MARGINS. The panel set no layout of its own and its four parts held
   themselves apart with whatever margin each happened to carry, so its height was the sum of four
   unrelated decisions and nothing could be resized without chasing all of them. One column, one
   gap: the parts stack, the panel is as tall as what it holds, and a state that adds a row costs
   exactly one gap.

   `padding:0` and `border:0` were here cancelling rules that no longer exist - the panel was a
   `.booker` div inside siteFooter when it was written, and the last footer rule that reached into
   it went when it became a component. */
[data-component="callBooker"]{display:grid;gap:var(--space-step-1);align-content:start}
/* THE CAP HOLDS A MARK AND A HEADING, and now says only how they sit together. `font:` here was
   the component drawing a heading itself - the title grade, restated - which is what kept the
   library reading this whole panel as one atom. */
[data-component="callBooker"] .cap{display:flex;align-items:center;gap:var(--space-step-1)}
/* THE MARK IS NOT DROPPED. A 2px `mark.optical.drop` sat here, written from a reading that the
   cap-height's centre was 1.6px ABOVE the mark's - and then moved the mark DOWN. Measured on the
   drawn paths rather than the svg box (2026-09-24): with the drop the phone sat 1.84px under the
   capitals' centre and its foot 2.56px below the baseline; flex centring alone lands it 0.16px
   off. The token is gone with it. */
/* the time cost, and the waiting line that replaces the dates while the scheduler is asked */
/* THE TIME COST IS A CITATION. It drew at the meta grade - 13px in the register's smallest voice
   - which is the size a timestamp or a byline takes, and this is a sentence a reader is meant to
   read before deciding. The citation grade, 15px light, is what the rest of the site uses for the
   line that qualifies a heading: sectionHead's paragraph, panelCard's sub, the notice bar's say. */
/* THE COST LINE KEEPS THE PANEL'S OWN LEFT EDGE, with the row label and the dates under it,
   rather than indenting to the heading's text: the mark leads the panel and everything below it
   reads off one margin. */
[data-component="callBooker"] .cost{font:var(--type-citation-font)}
[data-component="callBooker"] .wait{display:flex;align-items:center;gap:var(--space-step-2)}
/* THE SLOTS ROW, AND THE AIR AROUND IT. 40px above the rule and 24 below it, with the label then
   holding its own 32px off its dates: 96px of air in a panel 253 tall, and a label reading as the
   heading of a section rather than the caption on the row beneath it. A label belongs to the thing
   under it, so it sits with it. */
[data-component="callBooker"] .slots{display:grid;gap:var(--space-step-2);
 margin-top:var(--space-step-2);
 border-top-width:var(--border-width-hairline);border-top-style:solid;
 padding-top:var(--space-step-2)}
/* THE LABEL'S OWN MARGIN IS THE STACK'S NOW. The atom holds a 32px gap under itself, which is
   right for a label that heads a section and wrong for the caption on the row beneath it - and a
   margin inside a grid is a second opinion about a gap the grid already states. */
[data-component="callBooker"] .slots [data-component="label"]{margin-bottom:0}

/* the dates, when there are any: chips in a row that wraps */
[data-component="callBooker"] .dates{display:flex;flex-wrap:wrap;gap:var(--space-step-1)}
/* THE LIVE BOOKER (owner, 2026-09-25): /book's scheduler, brought across from akanoodles-2026, where
   its columns are bookingPanel's. Its four steps are blocks booker.js shows in turn, so a
   step's own display must yield to `hidden` rather than override it. The details form borrows
   formPanel's field rhythm: two names side by side, 24 between fields, labels at the meta size. */
[data-component="callBooker"] [hidden]{display:none}
[data-component="callBooker"][data-live] .grid2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--space-gutter)}
[data-component="callBooker"][data-live] .f [data-component="label"]{display:block;font-size:var(--type-meta-size);margin-bottom:var(--space-step-1)}
[data-component="callBooker"][data-live] .f{margin-bottom:var(--space-step-3)}
[data-component="callBooker"][data-live] .chosen{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-step-2)}
[data-component="callBooker"][data-live] .more{justify-self:start;margin-top:var(--space-step-1)}
[data-component="callBooker"][data-live] .send{justify-self:start}
@media (max-width:760px){
 [data-component="callBooker"][data-live] .grid2{grid-template-columns:minmax(0,1fr)}
}

/* ---- components/callBooker.skin ---- */
/* the booker's paint, moved off its parent for the same reason */
/* `small` went when the time cost became a `text` at the citation grade; the rule that painted it
   outlived the element by one edit. */
[data-component="callBooker"] .slots{border-top-color:var(--colour-border-default)}

/* THE QUIET LINES. The time cost and the waiting line both speak under the heading, which is a
   colour decision and belongs here rather than beside the box rules that place them. */
[data-component="callBooker"] .cost,[data-component="callBooker"] .wait{
 color:var(--colour-text-tertiary)}

/* ---- components/callBrief.form ---- */
/* callBrief: what a reader wants to know before giving up thirty minutes - who they will speak to,
   how the call runs (owner, 2026-09-25). Its parts sit 40px apart; inside a part,
   16 between a label and what it names. Every word is the module's, so a part with nothing to say
   is not drawn at all. */
[data-component="callBrief"]{display:grid;row-gap:var(--space-step-5)}
[data-component="callBrief"] .part{display:grid;row-gap:var(--space-step-2)}
/* a label here names the list under it, 16px off, not a section: the atom's own trailing margin
   is the prose's, and is cancelled */
[data-component="callBrief"] [data-component="label"]{margin-bottom:0}
/* a plain list, as nameLane's: the list atom may hold no atoms, and these rows hold two */
[data-component="callBrief"] .agenda{list-style:none;margin:0;padding:0}
[data-component="callBrief"] .part > [data-component="headline"]{margin:0}
/* THE PORTRAIT TAKES THE BRIEF'S FULL WIDTH at the media atom's own 3:2, cropped rather than
   letterboxed: a face is the page's strongest signal, and at two columns it read as an avatar */
[data-component="callBrief"] .portrait{inline-size:var(--ratio-100);object-fit:cover}
[data-component="callBrief"] .role{font:var(--type-citation-font)}
/* THE AGENDA: the minutes in a column of their own, so three rows read down one edge; a hairline
   over each row, 16px of air inside it */
[data-component="callBrief"] .agenda li{display:grid;grid-template-columns:var(--grid-column-1) minmax(0,1fr);
 column-gap:var(--space-step-2);align-items:baseline;padding-block:var(--space-step-2);
 border-top-width:var(--border-width-hairline);border-top-style:solid}

/* ---- components/callBrief.skin ---- */
/* callBrief's paint: the role and the minutes recede, the agenda's rules are the page's default
   hairline */
[data-component="callBrief"] .role,[data-component="callBrief"] .agenda [data-component="label"]{color:var(--colour-text-tertiary)}
[data-component="callBrief"] .agenda li{border-top-color:var(--colour-border-default)}

/* ---- components/clockRow.form ---- */
/* clockRow: what time it is where the studio is. The old site's nav trio, in its order: the dial,
   the running time to the second, then the city. Everything on Europe/London, so it reads my clock
   rather than the reader's; shell.js turns the hands and writes the digits.

   Three atoms in a fixed arrangement: `clock`, then two `label`s. It was a bare `.clock` span
   styled by siteFooter, and before that one label wrapped around all three, which put an atom
   inside an atom. the footer places it; nothing here reaches up to the footer. */
[data-component="clockRow"]{display:flex;align-items:center;gap:var(--space-step-2)}
/* A LABEL IN A ROW DROPS ITS MARGIN. The label atom carries 32px under it for when it names the
   block below; here it names nothing below, and the margin made each label 54px tall, so centring
   put the dial 16px under the digits. */
[data-component="clockRow"] [data-component="label"]{margin-bottom:0}
/* THE CITY DOES NOT MOVE. The face's figures are proportional, so "12:41:11" is narrower than
   "12:40:08" and the city beside it shifted every second the width changed. Tabular figures give
   every digit one advance, so the time holds one width and the row holds still. */
[data-component="clockRow"] time{font-variant-numeric:tabular-nums}

/* ---- components/studioAddress.form ---- */
/* studioAddress: where the practice is. One text atom in the citation role, the size of the call
   booker's "30mins free consultation" beside it in the footer (owner, 2026-09-24; it was the 13px
   meta role), the studio's name leading and the lines under it. */
[data-component="studioAddress"] [data-component="text"]{font:var(--type-citation-font);margin:0}

/* ---- components/studioAddress.skin ---- */
/* the name leads in primary ink; the lines take the text atom's own */
[data-component="studioAddress"] .name{color:var(--colour-text-primary)}

/* ---- components/socialLinks.form ---- */
/* socialLinks: a label and the platforms, each a link atom holding the platform's mark.
   THE PART GAP (owner, 2026-09-24): the label stands 8px over the row, not its own 32px, and the
   marks sit 16px apart, the gap between items in a row, where they stood 32px apart. */
[data-component="socialLinks"] > [data-component="label"]{margin-bottom:var(--space-step-1)}
[data-component="socialLinks"] [data-component="list"]{display:flex;flex-wrap:wrap;gap:var(--space-step-2);align-items:center}
[data-component="socialLinks"] [data-component="link"]{display:block;transition:color var(--motion-duration-250) var(--motion-ease-settle)}
[data-component="socialLinks"] svg{display:block;width:var(--size-mark-base);height:var(--size-mark-base)}
/* ON A TOUCH SCREEN each mark owns a 48px square, drawn at its left so the row still starts on the
   text edge: the 20px mark alone was the whole target */
@media (hover:none) and (pointer:coarse){
 [data-component="socialLinks"] [data-component="list"]{gap:0}
 [data-component="socialLinks"] [data-component="link"]{display:flex;align-items:center;
  inline-size:var(--size-control-small);block-size:var(--size-control-small)}
}

/* ---- components/socialLinks.skin ---- */
[data-component="socialLinks"] [data-component="link"]{color:var(--colour-text-secondary)}
[data-component="socialLinks"] [data-component="link"]:hover{color:var(--colour-text-primary)}

/* ---- components/siteMap.form ---- */
/* siteMap: the site's own pages - the legal shelf - and the copyright under them. The pages are
   link atoms in the label role, capitals and tracking; the copyright is the label atom, keeping
   zero tracking so its 47 characters fit a 330px column. */
/* the pages are set as studioAddress is (owner, 2026-09-24): the citation role, 15px, in the words'
   own case. They were the 14px label role in capitals. */
[data-component="siteMap"] li{font:var(--type-citation-font)}
[data-component="siteMap"] [data-component="link"]{transition:color var(--motion-duration-250) var(--motion-ease-settle)}
[data-component="siteMap"] > [data-component="label"]{display:block;margin-top:var(--space-step-4);margin-bottom:0;letter-spacing:0}
/* ON A TOUCH SCREEN each page link is a 48px row, so a thumb takes one and not its neighbour: at the
   citation leading they stood 21px apart, measured at 390. */
@media (hover:none) and (pointer:coarse){
 [data-component="siteMap"] [data-component="link"]{display:flex;align-items:center;min-height:var(--size-control-small)}
}
/* the 48px rows stand edge to edge: the list's own 8px gap on top of them read as a loose stack */
@media (hover:none) and (pointer:coarse){
 [data-component="siteMap"] [data-component="list"]{row-gap:0}
}

/* ---- components/siteMap.skin ---- */
[data-component="siteMap"] > [data-component="label"]{color:var(--colour-text-tertiary)}

/* ---- patterns/siteFooter.form ---- */
/* footer, ported from the live akanoodles.com: 810px form panel + 330px rail.
   Geometry and field metrics are theirs (1px border, 10px radius, 15px pad); colour is this register's. */
[data-component="siteFooter"]{padding:var(--space-step-6) 0 var(--space-step-12)}
/* footer row on the 12 columns: 1-3 address and socials, 4-9 the form, 10-12 booker and newsletter */
[data-component="siteFooter"] .cols{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--space-gutter);align-items:start}
/* the column fills the row rather than stopping short: the gap between connect and legal is the
   slack, so legal and the copyright sit on the same baseline as the foot of the form panel */
/* THE COLUMNS DO NOT STRETCH. Both side columns used to fill the row - `align-self:stretch`
   with a `1fr` track, so legal sat on the foot of the form and the newsletter on the foot of the
   rail. That reads as alignment only while the middle column is about as tall as they are, and
   the form is 780px: it left a 400px void in the meta column between CONNECT and the legal
   links, and a 500px void in the rail between the booker and Noods. Slack is not alignment. The
   content sits at the top of its own column now and the row is as tall as the form. */
/* WHERE the details column sits: columns 1 to 3, top-aligned. How it reads - that it is a grid of
   its own with 27px between its parts - is the component's, in siteDetails.form.css. This line
   used to carry both, which is why the column came out as a plain block when drawn on its own. */
[data-component="siteFooter"] .meta{grid-column:1 / span 3;align-self:start}
/* THE LEFT COLUMN IS THE FOOTER'S (2026-09-24): four components - clockRow and studioAddress read
   together, socialLinks, siteMap - stacked 48px apart in the column's meta type. It was the
   siteDetails component, which held nothing of its own; retired. */
[data-component="siteFooter"] .meta{display:grid;grid-template-rows:auto;gap:var(--space-step-6);font:var(--type-meta-font)}

[data-component="siteFooter"] form.r{grid-column:4 / span 6}
[data-component="siteFooter"] .rail{grid-column:10 / span 3}
/* the rail fills the row like the meta column: booker at the top, newsletter on the form's foot */
[data-component="siteFooter"] .rail{align-self:start;display:grid;grid-template-rows:auto;gap:var(--space-step-9)}

/* the rail is plain: no box, no border, the content sits on the page like the meta column */
/* the meta column is four labelled groups: clock, studio address, connect, legal. Every group is
   label then content, 8px apart. The first three sit 28px apart; the gap above legal is the
   column's slack, so the block lands on the foot of the form. */
/* socials are the live site's own 24px icons, drawn in currentColor so they take the register's
   ink; legal pages stay words and stack. Both sit at mute and come to white on hover. */
/* the studio name leads the address block, the rest of the lines sit back */
/* 47 characters of Fragment Mono at 11px with the label's .3px tracking measure 334px against a
   330px column, so this line drops the tracking rather than the words */
/* every section sits above the field, and the chrome above that. Two elements are deliberately
   left out of the position:relative sweep, because both already carry their own positioning and
   overwriting it broke them. The preloader is fixed and full-bleed, so relative dropped it into the flow
   as a 100vh block above the nav, where it could neither cover the page nor slide away. nav is
   position:sticky, and relative unstuck it: the bar scrolled away with the page on every route.
   Both only ever needed the z-index, which they get on their own lines below. */
body>section,body>[data-component="siteFooter"]{position:relative;z-index:1}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
@media (max-width:1100px){
 /* ---- the chrome below 1100px -------------------------------------------------------------- */
 /* the footer is a 12-column row of three panels. The row is fractional, so between 1100 and 1440 it
    narrows with the container rather than holding its 1440 width. Below 1100 three panels side by
    side stop reading, so they stack in order - form, rail, meta - and the nav drops to logo and CTA. */
 [data-component="siteFooter"] .cols{grid-template-columns:1fr}
 /* ---- the chrome below 1100px -------------------------------------------------------------- */
 /* the footer is a 12-column row of three panels. The row is fractional, so between 1100 and 1440 it
    narrows with the container rather than holding its 1440 width. Below 1100 three panels side by
    side stop reading, so they stack in order - form, rail, meta - and the nav drops to logo and CTA. */
 [data-component="siteFooter"] form.r,[data-component="siteFooter"] .rail,[data-component="siteFooter"] .meta{grid-column:1}
 /* ---- the chrome below 1100px -------------------------------------------------------------- */
 /* the footer is a 12-column row of three panels. The row is fractional, so between 1100 and 1440 it
    narrows with the container rather than holding its 1440 width. Below 1100 three panels side by
    side stop reading, so they stack in order - form, rail, meta - and the nav drops to logo and CTA. */
 [data-component="siteFooter"] .meta{align-self:auto;grid-row:3}
 /* ---- the chrome below 1100px -------------------------------------------------------------- */
 /* the footer is a 12-column row of three panels. The row is fractional, so between 1100 and 1440 it
    narrows with the container rather than holding its 1440 width. Below 1100 three panels side by
    side stop reading, so they stack in order - form, rail, meta - and the nav drops to logo and CTA. */
 [data-component="siteFooter"] .rail{grid-row:2;align-self:auto;grid-template-rows:auto}
 /* ---- the chrome below 1100px -------------------------------------------------------------- */
 /* the footer is a 12-column row of three panels. The row is fractional, so between 1100 and 1440 it
    narrows with the container rather than holding its 1440 width. Below 1100 three panels side by
    side stop reading, so they stack in order - form, rail, meta - and the nav drops to logo and CTA. */
 [data-component="siteFooter"] .news,[data-component="siteFooter"] [data-component="siteMap"]{align-self:auto}
}
/* THE FOOTER NO LONGER PADS ITS FORMS BELOW 760. `footer form` was meant for the message panel,
   which pads itself, and it also caught the newsletter's form - so on a phone the email field and
   Subscribe sat 32px in from the column the text above them started on. The panel's own two-up
   row is its own business now (formPanel.form.css). */
/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */
/* THE BOOKER'S REVEAL IS DELETED, not moved: it held the panel at `opacity:0` with a transform,
   waiting for a class that NOTHING adds. The home page's own script used to add it and does not
   exist any more, so the booking panel has been invisible on all 17 routes - it measured
   330x125 at the right place and painted nothing. That is why the footer read as missing its
   booking form. A reveal with no revealer is not animation, it is a hidden component. */

/* at the breakpoints the section padding switches token: 96, then 72 at 1100, then 48 at 760 */
@media (max-width:1100px){[data-component="siteFooter"]{padding:var(--space-step-6) 0 var(--space-step-9)}}
@media (max-width:760px){[data-component="siteFooter"]{padding:var(--space-step-6) 0 var(--space-step-6)}}

/* ---- patterns/siteFooter.skin ---- */
/* paint that came off the home page with its geometry: the form sheet holds where these
   sit, this holds how they look, and the gate refuses the two in one file. */
/* ---- MOVED OFF THE HOME PAGE -------------------------------------------------------------
   These were page CSS in home-register.html's head, which is the last thing keeping that
   route from owning no styling at all. Selectors unchanged: they already matched this
   component's own markup, which is what made them page rules rather than page design. */

/* HOW THE FOOTER USES siteDetails: moved verbatim from siteDetails.skin.css, where the
   part was deciding how its parent places it. */
/* THE SOCIAL MARKS KEEP THIS; THE LEGAL LINKS DO NOT. `.legal a` is the link atom - the shell
   writes it with `data-component="link"` - so its hover is the atom's accent and a rule here
   taking it to white was the component answering a question the atom now answers. A social mark
   is an anchor holding an icon rather than a link atom, so it keeps a hover of its own. */
/* the left column's ink, which its address and links read */
[data-component="siteFooter"] .meta{color:var(--colour-text-secondary)}

/* ---- patterns/siteNav.form ---- */
/* siteNav: the site chrome bar. It was scoped to the nav element, which made every other nav
   on the site into site chrome: the two specimen sheets carry their own nav and their own
   in-page bar, and both were being given this component's 75px height and its type. Same
   defect as the footer-scoped fields, same fix. */
/* the bar floats over the page: a wash of the background with the content blurred behind it,
   no edge of any kind.
   THE WASH IS A LAYER UNDER THE BAR, NOT THE BAR. A backdrop filter on an element makes it the
   containing block of everything fixed inside it, so the menu's panel and veil, which are fixed
   to the viewport, were drawn inside the 76px bar. On a layer of its own the blur is the same and
   the bar contains nothing it should not. */
[data-component="siteNav"]{position:sticky;top:0;z-index:10;height:var(--bar-site-height);display:flex;align-items:center}
[data-component="siteNav"]::before{content:"";position:absolute;inset:0;z-index:-1;-webkit-backdrop-filter:blur(var(--size-blur-base)) saturate(var(--ratio-140))}
[data-component="siteNav"] .wrap{display:flex;justify-content:space-between;align-items:center;gap:var(--space-step-3)}
/* the mark never gives way: squeezed by the routes it shrank to 74px at 700 before anything else moved */
[data-component="siteNav"] .logo{display:block;flex:none}
[data-component="siteNav"] .logo .mark{height:var(--bar-nav-height)}
/* the nav items are links: they sit back at mute and come forward on hover, so the row has a
   state instead of reading as nine equally loud words */
/* uppercase at 13px with tracking, so the caps do not read as shouting at 15px sentence size */
[data-component="siteNav"] ul{display:flex;gap:var(--space-step-4);font:var(--type-label-font);letter-spacing:.06em;text-transform:uppercase;align-items:center}
[data-component="siteNav"] ul li{cursor:pointer;transition:color var(--motion-duration-250) var(--motion-ease-settle)}
/* the route you are on is the one item already forward, so the bar says where you are without a second device */
[data-component="siteNav"] ul li.on a{cursor:default}
/* THE HEIGHT IS THE CONTROL'S. This set its own vertical padding, so the nav's button stood at
   59px while every other button on the site stood at 50 - the same "a component re-deciding what
   a control is" the gap and the label both had. What stays here is what is genuinely the nav's:
   the label must not wrap in a row that has no room to grow. */
/* the CTA sits inside the 75px bar: nothing in the row may wrap and push the bar open */
[data-component="siteNav"] ul li{white-space:nowrap}
[data-component="siteNav"]{z-index:10}
/* ---- RULES THAT CAME HOME FROM base.css ------------------------------------------------
   These were in base.css, which loads FIRST, while the rules they override are here at
   segment 57 of 63. A media query adds no specificity, so the responsive clause lost to the
   base rule every time and the component simply never reflowed. The footer proved it: its
   stack-at-1100 rule sat at char 33,441 of the bundle and the 12-column rule at 118,606, so
   the rail stayed 3 columns wide at 1100 and the booker's 314px floor pushed the document
   54px past the viewport on all eighteen routes. A component's own breakpoints belong in the
   component's own sheet. */
/* THE MENU IS HIDDEN UNTIL THE BAR SAYS OTHERWISE, and this rule has to sit ABOVE the ones that
   reveal it: they are one attribute more specific, which is what lets them win. */
[data-component="siteNav"] .menu,[data-component="siteNav"] .scrim,[data-component="siteNav"] .panel{display:none}
/* the control is not a native button to look at, shown or not: no UA padding, no UA border */
[data-component="siteNav"] .menu{padding:0;border:0}

/* THE BAR GOES COMPACT WHEN ITS ROUTES WOULD MEET THE MARK, OR ON A TOUCH SCREEN (owner,
   2026-09-27). Not a breakpoint: shell.js measures the seven routes against the room beside the
   logo and sets `data-compact` on the bar the moment they would touch, and sets it on any device
   with no hover and a coarse pointer whatever its width. The bar then drops to logo, the book
   link and the menu. */
/* compact, the bar is the mark and the menu only: Book lives in the menu as its call (owner, 2026-09-27) */
[data-component="siteNav"][data-compact] .wrap > ul{display:none}
/* THE MENU CONTROL IS A MARK, as on the live site: two rules that cross when the menu is open.
   A 48px square so a thumb finds it, and it stays above the panel it opens, so the same control
   closes it from the same place. */
[data-component="siteNav"][data-compact] .menu{display:inline-flex;align-items:center;justify-content:center;
 width:var(--size-px-48);height:var(--size-px-48);cursor:pointer;position:relative;z-index:3}
/* THE PANEL OPENS FROM THE RIGHT EDGE (owner, 2026-09-27), as the live site's does, full
   height, and the page behind it dims under a veil. It was a list hung under the bar at the
   label grade, which read as a dropdown, not a menu: the live site's menu is a surface of its
   own, routes set large and set left, the call to book under them and the studio at the foot.
   Four columns wide where there is room for the page to show beside it, the whole screen on a
   phone. It slides in under the control that opened it. */
[data-component="siteNav"][data-compact] .scrim{display:block;position:fixed;inset:0;z-index:1;visibility:hidden;-webkit-backdrop-filter:blur(var(--size-blur-sm));
 transition:opacity var(--motion-duration-500) var(--motion-ease-settle),visibility var(--motion-duration-500)}
[data-component="siteNav"][data-compact] .panel{display:flex;flex-direction:column;gap:var(--space-step-5);
 position:fixed;top:0;bottom:0;right:0;z-index:2;width:min(var(--ratio-100),var(--grid-column-4));
 padding:0 calc(var(--grid-page-inset) / 2) var(--space-step-6);overflow-y:auto;overscroll-behavior:contain;
 border-left-width:var(--border-width-hairline);border-left-style:solid;
 transform:translateX(var(--ratio-100));visibility:hidden;
 transition:transform var(--motion-duration-500) var(--motion-ease-settle),visibility var(--motion-duration-500)}
/* ONE MARK, AND IT DOES NOT MOVE (owner, 2026-09-27). The panel has no logo of its own: its top is
   the bar's height, left empty, and the bar's own mark and control sit above the panel and the
   veil, where they were before it opened. */
/* and the strip holds while the panel scrolls, so a route never slides under the mark */
[data-component="siteNav"][data-compact] .panel::before{content:"";flex:none;height:var(--bar-site-height);
 position:sticky;top:0;z-index:1;margin:0 calc(var(--grid-page-inset) / -2)}
[data-component="siteNav"][data-compact] .wrap > .logo{position:relative;z-index:3}
/* the routes at the h3 grade in the display voice - heavy, capitals, the narrow cut - so the
   menu speaks as the page titles do. Each arrives from the right, in turn, following the panel. */
[data-component="siteNav"][data-compact] .panel .links{display:grid;row-gap:var(--space-step-1);font:var(--type-h3-font);
 font-weight:var(--type-weight-bold);letter-spacing:var(--type-h3-tracking);text-transform:uppercase;
 font-variation-settings:"wdth" 92,"opsz" 96}
[data-component="siteNav"][data-compact] .panel .links li{transform:translateX(var(--size-px-24));
 transition:opacity var(--motion-duration-500) var(--motion-ease-settle),transform var(--motion-duration-500) var(--motion-ease-settle)}
[data-component="siteNav"][data-compact] .panel .links a{display:inline-block}
/* the call to book: a bordered row, pushed to the foot with the studio under it */
[data-component="siteNav"][data-compact] .panel .book{margin-top:auto;display:flex;align-items:center;gap:var(--space-step-3);
 padding:var(--space-step-3);border-width:var(--border-width-hairline);border-style:solid}
[data-component="siteNav"][data-compact] .panel .book .say{display:grid;flex:1}
[data-component="siteNav"][data-compact] .panel .meta{display:flex;flex-wrap:wrap;gap:var(--space-step-4) var(--space-step-6)}
/* OPEN, the bar rises above the cookie bar, which would otherwise cover the panel's foot */
[data-component="siteNav"][data-compact][data-open]{z-index:70}
/* visible AT ONCE on opening, so focus can land on the first route in the same frame; closing
   keeps the base rule's visibility transition, so the panel stays visible while it slides out */
[data-component="siteNav"][data-compact][data-open] .scrim{visibility:visible;
 transition:opacity var(--motion-duration-500) var(--motion-ease-settle)}
[data-component="siteNav"][data-compact][data-open] .panel{transform:none;visibility:visible;
 transition:transform var(--motion-duration-500) var(--motion-ease-settle)}
[data-component="siteNav"][data-compact][data-open] .panel .links li{transform:none;
 transition-delay:calc(var(--motion-duration-80) * var(--i,0) + var(--motion-duration-130))}
@media (prefers-reduced-motion:reduce){
 [data-component="siteNav"] .scrim,[data-component="siteNav"] .panel,[data-component="siteNav"] .panel .links li{transition:none}
}
/* BELOW 760 THE MARK STEPS DOWN A SIZE. At 40px it is 227px wide, which left the book link and the
   menu 131px of a 358px row at 390: the link sat against the mark with no space between them. At
   32px the mark is 182px and the row keeps a gap at 360. */
@media (max-width:760px){
 [data-component="siteNav"] .logo .mark{height:var(--size-px-32)}
}
/* the product link's pulse: a 4s cycle, held while hovered, and none under reduced motion */
[data-component="siteNav"] a[href="/drop"]{animation:nav-drop-pulse var(--motion-duration-4s) ease-in-out infinite}
[data-component="siteNav"] a[href="/drop"]:hover{animation:none}
@media (prefers-reduced-motion:reduce){[data-component="siteNav"] a[href="/drop"]{animation:none}}
/* ON A TOUCH SCREEN THE MARK IS A 48px TARGET, the way home: the logo image alone stood 32px tall */
@media (hover:none) and (pointer:coarse){
 [data-component="siteNav"] .wrap > .logo{display:flex;align-items:center;min-block-size:var(--size-control-small)}
}
/* the wordmark as a mask: its box is the logo's own 227:40, the mask is the file */
[data-component="siteNav"] .logo .mark{display:block;aspect-ratio:227/40;-webkit-mask:url(/assets/logo.svg) center/contain no-repeat;mask:url(/assets/logo.svg) center/contain no-repeat;transition:background-color var(--motion-duration-400) var(--motion-ease-settle)}

/* ---- patterns/siteNav.skin ---- */
[data-component="siteNav"]::before{background:var(--bar-site-background);backdrop-filter:blur(var(--size-blur-base)) saturate(var(--ratio-140))}
[data-component="siteNav"] ul{color:var(--colour-text-secondary)}
/* a link goes to the accent yellow on hover (owner, 2026-09-24) */
[data-component="siteNav"] ul li:hover{color:var(--colour-accent-base)}
[data-component="siteNav"] ul li.on{color:var(--colour-text-primary)}

/* the menu control is the mark alone, in the bar's own ink, and full white when it is open */
[data-component="siteNav"] .menu{color:var(--colour-text-secondary);background:none}
[data-component="siteNav"] .menu:hover,[data-component="siteNav"] .menu[aria-expanded="true"]{color:var(--colour-text-primary)}
/* THE VEIL DIMS THE PAGE and blurs it, so the open menu is the one thing in focus */
[data-component="siteNav"] .scrim{background:var(--colour-surface-veil-strong);backdrop-filter:blur(var(--size-blur-sm));opacity:0}
[data-component="siteNav"][data-open] .scrim{opacity:1}
/* the routes fade in as they slide, one after another */
[data-component="siteNav"] .panel .links li{opacity:0}
[data-component="siteNav"][data-open] .panel .links li{opacity:1}
/* AKA-91. `--barbg` is the specimen control bar's translucent token, 62% alpha, and the panel
   borrowed it. Page content behind kept reading through the blur - a reader on /work saw the
   page's own "WORK" kicker ghosting behind the nav's "WORK" link, in the one surface the form
   sheet's own comment says has to "read as a surface the bar opened" rather than as an overlay.
   `--bg` is the opaque page ground; a panel occluding the page takes the page's own colour. */
[data-component="siteNav"] .panel{background:var(--colour-surface-page);border-left-color:var(--colour-border-default)}
[data-component="siteNav"] .panel::before{background:var(--colour-surface-page)}
[data-component="siteNav"] .panel a{color:var(--colour-text-secondary)}
[data-component="siteNav"] .panel a:hover,[data-component="siteNav"] .panel .on a{color:var(--colour-text-primary)}
/* the routes rest back at the tertiary ink, so the one you are on comes forward; the one under
   the pointer takes the accent, as it does in the bar */
[data-component="siteNav"] .panel .links a{color:var(--colour-text-tertiary)}
[data-component="siteNav"] .panel .links a:hover{color:var(--colour-accent-base)}
[data-component="siteNav"] .panel .links .on a{color:var(--colour-text-primary)}
[data-component="siteNav"] .panel .book{border-color:var(--colour-border-default);color:var(--colour-text-primary)}
[data-component="siteNav"] .panel .book:hover{border-color:var(--colour-text-primary)}
/* DROP PULSES (owner, 2026-09-24): the colours of the product link's slow pulse, white to the accent
   yellow and back; the motion is in the form sheet. Yellow while hovered, and yellow and still under
   reduced motion, so it stands out either way */
@keyframes nav-drop-pulse{0%,100%{color:var(--colour-text-primary)}50%{color:var(--colour-accent-base)}}
[data-component="siteNav"] a[href="/drop"]:hover{color:var(--colour-accent-base)}
@media (prefers-reduced-motion:reduce){[data-component="siteNav"] a[href="/drop"]{color:var(--colour-accent-base)}}
/* the wordmark takes the text colour, and the accent under the pointer or keyboard focus */
[data-component="siteNav"] .logo .mark{background-color:var(--colour-text-primary)}
[data-component="siteNav"] .logo:is(:hover,:focus-visible) .mark{background-color:var(--colour-accent-base)}

/* ---- patterns/subNav.form ---- */
/* subNav: the section index a long index page carries under the site chrome. One row of links to
   the page's own sections, the section nearest the top marked as the page scrolls, and one slot at
   the head of the row for a page-level control — the specimen sheets put their Freeze motion
   toggle there. It sticks under the 75px site bar rather than to the viewport top, because two
   bars fighting for the same 75px is how the sheets' own index ended up hidden behind the nav.
   The register does the work: mono at label size, the accent for the section you are in, and one
   hairline under the row. No pills, no second background. */
[data-component="subNav"]{position:sticky;top:var(--bar-site-height);z-index:40;border-bottom-width:var(--border-width-hairline);border-bottom-style:solid}
/* THE ROW IS THE TEMPLATE'S WRAP, and this said `.wrap` - a class the wrap element has not
   carried since the templates moved to `data-template`. So the row lost its flex, its gap
   and its 48px height and rendered as 24px of stacked links; the rule beneath it, which
   already used the attribute, kept working and hid how wrong the selector above it was. */
[data-component="subNav"] [data-template="wrap"]{display:flex;align-items:center;gap:var(--space-step-5);height:var(--bar-section-height);overflow-x:auto;scrollbar-width:none}
[data-component="subNav"] [data-template="wrap"]::-webkit-scrollbar{display:none}
[data-component="subNav"] a{font:var(--type-label-font);letter-spacing:var(--type-label-tracking);text-transform:uppercase;white-space:nowrap;transition:color var(--motion-duration-300) var(--motion-ease-settle)}

/* THE ROW SAYS IT GOES ON. It scrolls sideways when its sections outrun the screen - /thinking/motion's
   ten at every width - and the last one was cut mid-word at the edge with nothing to say there was
   more. The last 64px fade out, so a cut label reads as a row that continues. */
[data-component="subNav"] [data-template="wrap"]{-webkit-mask-image:linear-gradient(to right,var(--colour-surface-page) calc(var(--ratio-100) - var(--space-step-8)),transparent);
 mask-image:linear-gradient(to right,var(--colour-surface-page) calc(var(--ratio-100) - var(--space-step-8)),transparent)}
/* ON A TOUCH SCREEN a section link is the bar's full height, 48px, rather than its 22px of words */
@media (hover:none) and (pointer:coarse){
 [data-component="subNav"] a{display:flex;align-items:center;align-self:stretch}
}

/* ---- patterns/subNav.skin ---- */
[data-component="subNav"]{background:var(--bar-site-background);backdrop-filter:blur(var(--size-blur-sm));border-bottom-color:var(--colour-border-default)}
[data-component="subNav"] a{color:var(--colour-text-tertiary)}
[data-component="subNav"] a:hover{color:var(--colour-text-primary)}
[data-component="subNav"] a[aria-current="true"]{color:var(--colour-accent-base)}

/* ---- atoms/clock.form ---- */
/* clock: an analogue dial, the time in London. An atom: a ring and two hands, nothing in it to
   name. It is not an icon mark, because a mark is static artwork and these hands are turned every
   second by clock() in shell.js. It was a `.dial` class in foundation/base.css, then in siteFooter,
   and it had no rung either way. The digits beside it are a label atom, not part of this one: an
   atom may not hold an atom. */
[data-component="clock"]{display:block;width:var(--size-14);height:var(--size-14);flex:none;
 border-width:var(--size-1-5);border-style:solid;border-radius:var(--radius-circle);position:relative}
[data-component="clock"] .hr,[data-component="clock"] .mn{position:absolute;left:var(--position-centre);
 bottom:var(--position-centre);width:var(--size-1-5);margin-left:calc(-1 * var(--size-0-75));
 transform-origin:var(--position-centre) 100%;border-radius:var(--size-1)}
[data-component="clock"] .hr{height:var(--size-3-2)}
[data-component="clock"] .mn{height:var(--size-4-6)}

/* ---- atoms/clock.skin ---- */
/* clock: the paint. Tertiary ink, the label's own, so the dial reads as one line with the time
   and the city beside it; ring and hands follow it through currentColor. */
[data-component="clock"]{color:var(--colour-text-tertiary);border-color:currentColor}
[data-component="clock"] .hr,[data-component="clock"] .mn{background:currentColor}

/* ---- atoms/code.form ---- */
/* code: a term a reader should take literally - a token, a path, a value - inside a sentence
   rather than in a block.
   
   22 instances, one shape, and no rule named the atom: the mono face came from
   `[data-component="prose"] code`, so a code span inside anything that is not prose rendered in
   the body face. The atom carries it now.
   
   THE GRADE IS INHERITED. My first version set the mono ROLE's 12/18, which shrank every inline
   term from 16px to 12px inside a 16px sentence. The contract's `mono` role is for chips, meta
   and counters - a standalone label - and an inline code span is not one: it sits in a line of
   body text and has to share that line's size, or the baseline breaks around it.
   
   THE MARK IS WEIGHT, NOT FACE. This atom used to say it changed the face, and it stopped doing
   so on 19 September, when the house went to one typeface: `type.family.mono` resolves to the
   same sans as the sentence round it, so the atom drew nothing a reader could see. Of the three
   axes one variable family offers, weight is the one that marks a term without changing its size
   - the width axis was tried, and at body grade a condensed term reads as a smaller one, which is
   the fault the paragraph above exists to prevent. It borrows the label's medium, and the two do
   not collide, because a label also takes uppercase and tracking and a term takes neither.
   
   The family is still named: it states the role, so if `mono` is ever given a face of its own,
   every term follows without an edit here. */
[data-component="code"]{font-family:var(--type-family-mono);font-weight:var(--type-weight-medium)}

/* ---- atoms/list.form ---- */
/* list: an ordered set of peers - a ul, an ol or a dl.
   
   THE GROUPS NEED AIR BETWEEN THEM, which is what the owner spotted in the /drop FAQ: seven
   question-and-answer pairs ran together as one wall of text because the only spacing was the
   hairline between rows, and a `dl` has no gap of its own. A set of peers is read by its grouping,
   and grouping is the gutter - §7, "group with proximity, alignment and the gutter".
   
   `row-gap` on the ATOM rather than a margin on the children, so a pattern can still set its own
   rhythm on top without fighting a margin it has to zero out first.

   AND THE PITCH IS 2.0, MEASURED ON THE TYPE. The gap was `--space-step-5`, 40px - a section's
   air used between two lines of a sentence - which put items 41px apart, 2.4 times the type
   size, and three of them read as three blocks. Zero gap was the other end: items at the
   paragraph's own 1.55 and too tight to scan as a set. The gap is `calc(1lh * 0.29)`, which puts
   the pitch at 34px on 17px type - 2.0 - and it is expressed against the item's own line so it
   holds at whatever size a list is drawn at. */
/* AND ITS OWN RESET. The marker and the 40px of indent are the browser's, and on a route they
   were being taken off by an ancestor - so the list atom drawn on its own came out with discs and
   an indent nobody authored. A list that needs its container to stop looking like a browser
   default owns nothing. `list-style:none` because this system marks a set with a rule, a rank or
   a mark - `data-marker` - and never with the UA's bullet. */
[data-component="list"]{display:grid;row-gap:var(--list-row-gap);list-style:none;padding-left:0;margin:0}

/* A GRID COLUMN IS max-content UNTIL IT IS TOLD OTHERWISE, and one source in the thinking corpus
   is a 107-character URL. The implicit column sized itself to that token, the list measured 892px
   inside a 390px page, and the whole document scrolled sideways. `minmax(0,1fr)` lets the column
   be narrower than its content and `overflow-wrap` breaks the token rather than the page. */
[data-component="list"]{grid-template-columns:minmax(0,1fr)}
[data-component="list"] > li{overflow-wrap:anywhere}

/* THREE MARKERS, AND UNTIL NOW ALL THREE DREW THE SAME THING. `data-marker` was documented in
   the comment above, named in the contract, written onto 236 lists across the built routes - 197
   `mark`, 30 `rank`, 9 `none` - and no stylesheet anywhere carried a rule for it. The library
   drew its three exemplars as three identical unmarked lists, which is the shape of an axis that
   exists in the markup and nowhere else.

   THE MARKER SITS IN ITS OWN COLUMN rather than in the text flow, so the second line of a long
   item aligns under the first word and not under the dot. That is the one thing the UA's own
   `list-style` gets right and a `::before` in the flow gets wrong.

   THE COLUMN IS PADDING AND AN ABSOLUTE MARKER, NOT A GRID. A grid on the `li` was the obvious
   way to write it and it destroyed the reference page: an item is a text node beside inline
   elements, every one of which becomes a grid ITEM, so a bulleted paragraph with a link in it
   broke into a column per fragment and the list measured 7,166px tall. Padding leaves the item's
   contents as one flow and the marker sits beside it. */
[data-component="list"][data-marker="mark"] > li,
[data-component="list"][data-marker="rank"] > li{position:relative;padding-left:var(--list-marker-column)}
/* BOTH MARKERS RIGHT-ALIGN IN THE SAME COLUMN, which is what makes a dotted list and a
   numbered one sit on the same two edges. Left-aligned they did not: a 3px dot left 21px of air
   before the words and an 18px numeral left 6px, so the two lists read as two different indents
   for the same structure. The marker box is the column less the gap, the marker sits at its
   right edge, and the gap after it is one value for both.

   EXCEPT THE DOT, WHICH CENTRES IN THAT BOX. A numeral is two characters wide and reads as a
   block, so its right edge is the edge to hold. A dot is 3px: right-aligned it hangs off the
   text like an afterthought, with all the air on its left and none on its right. Centred, it
   sits in the middle of the same column the numerals occupy, which is what the eye reads as the
   two lists sharing an indent. */
[data-component="list"][data-marker="mark"] > li::before,
[data-component="list"][data-marker="rank"] > li::before{
 position:absolute;left:0;width:calc(var(--list-marker-column) - var(--space-step-1));
 text-align:right;white-space:nowrap}

/* A DOT, NOT A BULLET. Punctuation-sized and at the quietest text grade: the list is read by its
   items and the mark is there to say where each one starts. */
[data-component="list"][data-marker="mark"] > li::before{
 content:"";width:var(--list-dot-size);height:var(--list-dot-size);border-radius:var(--radius-circle);
 left:calc((var(--list-marker-column) - var(--space-step-1) - var(--list-dot-size)) / 2);
 top:calc((1lh - var(--list-dot-size)) / 2)}

/* TWO DIGITS, in the mono label grade every other numeral on the site takes - stepPanel's step,
   bigNumberItem's row. `counter-reset` on the list and `counter-increment` per item, so the numbers are
   the browser's own count rather than something a builder has to write into the content. */
[data-component="list"][data-marker="rank"]{counter-reset:rank}
[data-component="list"][data-marker="rank"] > li{counter-increment:rank}
/* THE NUMERAL SITS ON THE ITEM'S BASELINE. `--type-label-font` is a shorthand and it carries
   the label's own leading, 21.7px against the item's 26.35 - so every number rode 4.6px high
   above the words it counts. The family, the size and the weight are the label's; the LEADING is
   the line it sits on, which is the item's. */
[data-component="list"][data-marker="rank"] > li::before{
 content:counter(rank,decimal-leading-zero);
 font-family:var(--type-family-mono);font-size:var(--type-label-size);
 font-weight:var(--type-label-weight);line-height:inherit;
 letter-spacing:var(--type-label-tracking)}

/* and `none` is the atom's own reset, which is already the rule above: no marker, no column. */

/* ---- atoms/list.skin ---- */
/* list: what a list is painted. */

/* THE MARKER IS THE ACCENT, dot and numeral alike. A marked list is a set the reader is meant
   to count or scan, and the mark is the thing doing that work - so it takes the yellow, the same
   one a link takes under the cursor, and the words stay white. Here rather than in the form
   sheet, which the gate is right about: a fill and a colour are paint. */
[data-component="list"][data-marker="mark"] > li::before{background-color:var(--colour-accent-base)}
/* THE NUMERAL TAKES THE ACCENT. A dot is punctuation and stays quiet; a number is a position in
   a sequence, which is information the reader is meant to use, so it is the one place a list
   spends the accent. */
[data-component="list"][data-marker="rank"] > li::before{color:var(--colour-accent-base)}

/* ---- atoms/text.form ---- */
/* text: a paragraph. The atom's own grade, not the page's.
   
   664 instances across five routes and FOUR shapes, which is the whole argument for this file: the
   type is identical everywhere - 16/22.5 - and only the MARGIN differs, 22.5px in prose, 13.5px in
   a frame, 9px in a card, 0 where a pattern stacks its own. Margin is rhythm and rhythm belongs to
   the pattern; size and leading belong to the atom, and they had no rule naming the atom at all.
   
   The size and leading are the screen ramp's `body` role, 16/22.5, which DESIGN.md §13.1 hands
   this class unchanged. Written as tokens rather than numbers so the ramp stays one definition. */
[data-component="text"]{font-size:var(--type-body-size);line-height:var(--type-body-leading)}

/* running text avoids a lone last word on its final line, where the browser can */
[data-component="text"]{text-wrap:pretty}

/* ---- atoms/text.skin ---- */
/* text: what a paragraph is painted. `p{color:var(--colour-text-secondary)}` in base.css was doing this by ELEMENT
   - so a paragraph that is not the text atom took it too, and the atom that is not a `p` did not.
   The atom is the seam. */
[data-component="text"]{color:var(--colour-text-secondary)}

/* ---- patterns/letterIndex.form ---- */
/* letterIndex: a long shelf of entries, grouped by first letter, read in columns.
   
   THE DEMO HAD THIS DESIGN AND IT WAS BETTER, so these are its rules, from
   archive/demo/thinking.html: a CSS multi-column list rather than a grid, because the entries are
   one alphabetical sequence folded into three - a reader goes down column one and continues at
   the top of column two, which is how an index reads. A grid would order them across the row and
   break the alphabet. `break-inside:avoid` keeps a lettered group whole where it fits, so a
   letter is never left behind at the foot of a column.
   
   WHAT MY FIRST VERSION GOT WRONG, and it is all spacing: a `ul` keeps the UA's 40px left pad
   and its markers, so every topic sat indented under its letter with a bullet in front of it and
   the letter floating in space above. The demo zeroes the list, and each entry is a BLOCK link
   with 4.5px of padding top and bottom - so the row is the click target, the column edge is one
   straight line from the letter down through every topic, and the rhythm inside a group is the
   padding rather than a margin.
   
   One column below 1100: three columns of a 128-entry shelf on a phone is a column 90px wide. */
/* A GRID (owner, 2026-09-24): the twelve columns with the gutter, a letter group four columns wide,
   so three groups a row and the alphabet reads across */
[data-component="letterIndex"] .cols{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--space-gutter);margin-top:var(--space-step-2)}
[data-component="letterIndex"] [data-component="letterGroup"]{grid-column:span 4}
[data-component="letterIndex"] .count{margin-top:var(--space-step-7)}

/* TWO GROUPS A ROW BELOW 1100, ONE BELOW 760. A whole row per letter at 1024 set a list of short
   entries down a third of the width and left two thirds empty for the length of the shelf. */
@media (max-width:1100px){
 [data-component="letterIndex"] [data-component="letterGroup"]{grid-column:span 6}
}
@media (max-width:760px){
 [data-component="letterIndex"] [data-component="letterGroup"]{grid-column:1 / -1}
}

/* ---- patterns/letterIndex.skin ---- */
/* the letter is the shelf's furniture, not its content: dim, so the topics read first. The paint
   is the demo's - --dim for the letter, --muted for a topic, --fg on hover. */

/* ---- components/letterGroup.form ---- */
/* letterGroup: one letter of the shelf and the topics filed under it.

   THE SHELF IS 27 OF THESE, and the group was a `.grp` class in the index's own stylesheet - so
   the index read as a pattern composed of link atoms, and the unit a reader actually scans had no
   name. `break-inside:avoid` is the group's own rule for the same reason: keeping a letter whole
   across a column break is a fact about the group, not about the shelf. */
[data-component="letterGroup"]{break-inside:avoid;padding-top:var(--space-step-6)}
/* BIG NUMBER'S TYPE (owner, 2026-09-24). The letter is the index's figure, set as bigNumberItem sets
   its Q01: the big-number light grade, capitals, no tracking. It was a 12px label.

   THE ENTRIES ARE THE LIST ATOM, UNTOUCHED: `list[data-marker="none"]` of link atoms, drawn exactly
   as the library draws that form - its type, its 8px row gap, the link's own ink and hover. This
   sheet used to restyle them (15px citation links, then a block display, 8px of padding and a
   grey), which is how an index came to look like no other list on the site. It only places the
   list, 8px under its letter. */
[data-component="letterGroup"] ul{margin:var(--space-step-1) 0 0}
/* the label atom's own 32px margin is the stack's here: letter to list is one part-gap */
[data-component="letterGroup"] [data-component="label"]{margin-bottom:0}
/* ON A TOUCH SCREEN each entry is a 40px row: its words' 21px with 8px of padding above and below,
   and the list's own 8px gap between rows keeps neighbouring targets apart */
@media (hover:none) and (pointer:coarse){
 [data-component="letterGroup"] [data-component="link"]{display:block;padding-block:var(--space-step-1)}
}

/* ---- components/letterGroup.skin ---- */
/* the letter in primary ink, as bigNumberItem's figure is */
[data-component="letterGroup"] .letter{color:var(--colour-text-primary)}

/* ---- patterns/flowFrame.form ---- */
/* flowFrame: /drop's boundary diagram - a labelled boundary around three stages, each a mark, an
   index, a heading and a line of prose, with arrows between them.
   
   IT WAS `.frame` IN base.css AND MARKED AS `media`. Both were wrong the same way: the rules sat
   in the foundation every surface shares though one route draws them, and the markup claimed to
   be a plate though it holds nine atoms, which made the `media` atom look like a component.
   
   EXTRACTED BLOCK BY BLOCK, not line by line. The first attempt moved matching LINES, which cut
   two multi-line rules in half: `.outputs` and `.outputs span` lost their closing braces, so from
   that point the shipped bundle was one unterminated block and every later rule was discarded -
   ten of this component's seventeen, including the whole skin file, on a production route. A
   stylesheet is a tree, and a line is not a node in it. Found by an audit parsing the bundle. */
/* ON THE GRID (owner, 2026-09-24): the stages sit on the twelve columns with the gutter between,
   four columns each, so three stages fill the row; the order reads from the index, 01 02 03. The
   boundary and the outputs are label atoms; every gap is a step on the scale. */
[data-component="flowFrame"] .stages{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--space-gutter);row-gap:var(--space-step-6)}
[data-component="flowFrame"] [data-component="flowStage"]{grid-column:span 4}
[data-component="flowFrame"] .outputs{display:flex;flex-wrap:wrap;column-gap:var(--space-gutter);row-gap:var(--space-step-1);margin-top:var(--space-step-6)}
[data-component="flowFrame"] .outputs [data-component="label"]{margin-bottom:0}
@media (max-width:1100px){[data-component="flowFrame"] [data-component="flowStage"]{grid-column:span 6}}
@media (max-width:760px){[data-component="flowFrame"] [data-component="flowStage"]{grid-column:1 / -1}}
/* THE BOUNDARY IS DRAWN (owner, 2026-09-28): the frame is a ruled box with its name set into the
   top edge, the stages boxed inside it and the outputs as chips, the diagram /drop's own site draws.
   It was a labelled row of three columns with nothing marking the network the page is about. */
[data-component="flowFrame"]{position:relative;padding:var(--space-step-6) var(--space-step-4) var(--space-step-4);border:var(--border-width-hairline) solid;border-radius:var(--radius-surface)}
[data-component="flowFrame"] > .boundary{position:absolute;top:0;left:var(--space-step-3);transform:translateY(calc(var(--ratio-50) * -1));padding:0 var(--space-step-1);margin:0}
[data-component="flowFrame"] [data-component="flowStage"]{padding:var(--space-step-3);border:var(--border-width-hairline) solid;border-radius:var(--radius-surface)}
[data-component="flowFrame"] .outputs{margin-top:var(--space-step-3);column-gap:var(--space-step-1)}
[data-component="flowFrame"] .outputs [data-component="label"]{padding:var(--space-step-1) var(--space-step-2);border:var(--border-width-hairline) solid;border-radius:var(--radius-surface)}
/* ONE COLUMN ON A PHONE (owner, 2026-09-28): the twelve columns' eleven gutters alone are 330px, so
   inside the boxed frame the stages overran its edge at 390. The phone takes a single track and a
   tighter frame. */
@media (max-width:760px){
 [data-component="flowFrame"]{padding:var(--space-step-5) var(--space-step-2) var(--space-step-2)}
 [data-component="flowFrame"] .stages{grid-template-columns:minmax(0,1fr);row-gap:var(--space-step-2)}
}

/* ---- patterns/flowFrame.skin ---- */
/* the boundary's rule and name in the accent, its name sitting on the page's ground to break the
   edge; the stages on the card ground; the outputs as quiet chips */
[data-component="flowFrame"]{border-color:var(--colour-accent-base)}
[data-component="flowFrame"] > .boundary{color:var(--colour-accent-base);background:var(--colour-surface-page)}
[data-component="flowFrame"] [data-component="flowStage"]{background:var(--colour-surface-card);border-color:var(--colour-border-default)}
[data-component="flowFrame"] [data-component="flowStage"] .idx{color:var(--colour-accent-base)}
[data-component="flowFrame"] .outputs [data-component="label"]{border-color:var(--colour-border-default);color:var(--colour-text-secondary)}

/* ---- components/flowStage.form ---- */
/* flowStage: one stage of a flow - its mark, its index, its heading, its sub label and its line.

   THE RUNG BETWEEN THE FLOW AND ITS ATOMS. flowFrame held nine atoms per stage with nothing named
   between them, so the flow was a pattern composed of atoms three times over and the repeated
   group - the thing an editor adds a fourth of - was a `.stage` class inside another component's
   stylesheet. The stage is that group.

   ITS SMALLER MARK COMES WITH IT. `.stage .glyph .ic` at 22px lived in icon.form.css, which is an
   ATOM's file scoping itself to another component's class: the flow's mark is smaller than the
   site's because the stage is dense, and that is the stage's decision to make, not the icon's.
   The mark scale is a token now - `--icon-m` - so the two files agree on the number by reading
   the same name rather than by both spelling 22. */

[data-component="flowStage"] .glyph .ic{width:var(--size-mark-m);height:var(--size-mark-m)}

/* THE bigNumberItem VOICE AND THE LIST RHYTHM (owner, 2026-09-23). One stack, 8px between parts, no
   part carrying a margin of its own. The index is the big number, light, as a bigNumberItem's is;
   the heading is the title grade; the line is the text atom at body size, not the citation grade.
   The mark sits one step (16px) above the number, the one gap that separates a picture from words. */
[data-component="flowStage"]{display:grid;gap:var(--space-step-1);align-content:start}
[data-component="flowStage"] .glyph{display:block;border:0;width:auto;height:auto;margin-bottom:var(--space-step-1)}
[data-component="flowStage"] [data-component="label"]{margin-bottom:0}
[data-component="flowStage"] [data-component="headline"],[data-component="flowStage"] p{margin:0}

/* ---- components/flowStage.skin ---- */
/* the stage's paint, split off its form as every component here is a pair */
[data-component="flowStage"] .glyph{color:var(--colour-text-secondary)}

/* ---- patterns/bigNumberList.form ---- */
/* bigNumberList: a question-and-answer set, one bigNumberItem per pair.

   NOTHING STYLED THIS BEFORE. The module rendered a `list` atom holding raw `dt`/`dd` pairs and a
   `.qtag` span no stylesheet in the repository mentions, so /drop's seven questions drew at the
   browser's defaults: a bold term, an indented definition, no rhythm and no rule between pairs.
   The set is the pattern the module renders and the pair is the component it composes.

   THE RULE BETWEEN PAIRS IS THE SET'S, the same way markedList owns the rule between its rows:
   separation is a fact about a list, not about a row. */
/* ONE RULE, ABOVE THE SET (owner, 2026-09-23): the rule opens the list rather than separating
   every pair. The pairs sit 48px apart - they were 64px above and below each, 128 between - and
   the first sits 32px under the rule. */
[data-component="bigNumberList"]{display:grid;row-gap:var(--space-step-6);margin:0;
 padding-top:var(--space-step-4);border-top-width:var(--border-width-hairline);border-top-style:solid}

/* ---- patterns/bigNumberList.skin ---- */
[data-component="bigNumberList"]{border-top-color:var(--colour-border-default)}

/* ---- components/bigNumberItem.form ---- */
/* bigNumberItem: one question and its answer, led by its number.

   THE NAME WAS IN THE CONTRACT AND NOTHING WORE IT. `faqModule -> bigNumberItem` sat in the module table
   pointing at a component that existed in no tier, no stylesheet and no page - the module drew a
   list atom instead - so the table described a rung that was never built. This is that rung.

   TWO COLUMNS: the numeral in its own, so every question starts on the same line whatever the
   number, and the pair in the second. The question takes the title grade and the answer is a text
   atom, which is where its body grade comes from. */
[data-component="bigNumberItem"]{display:grid;grid-template-columns:var(--grid-column-1) minmax(0,1fr);
 gap:var(--space-gutter);align-items:start}
/* THE NUMBER IS THE BIG-NUMBER ROLE, LIGHT (owner, 2026-09-23): it counts the questions, so it
   takes the figure the register keeps for a count, at the light weight so it sits beside the
   question rather than shouting over it. */
[data-component="bigNumberItem"] .q{font:var(--type-big-number-light-font);letter-spacing:normal;text-transform:uppercase}
/* the question is the headline atom at the title grade; the atom sets its type */
[data-component="bigNumberItem"] dd{margin:var(--space-step-1) 0 0;max-width:var(--grid-column-8)}
@media (max-width:760px){
 [data-component="bigNumberItem"]{grid-template-columns:minmax(0,1fr);gap:var(--space-step-2)}
}

/* ---- components/bigNumberItem.skin ---- */
[data-component="bigNumberItem"] .q{color:var(--colour-text-primary)}
[data-component="bigNumberItem"] dd{color:var(--colour-text-secondary)}

/* ---- atoms/meter.form ---- */
/* meter: how far through something a reader is. One element, one job.

   An atom: its fill is its own content, the way a plate's frame is the media atom's. `--at` is
   the only thing that moves it, and it is set as an inline custom property by whatever owns the
   sequence - so the atom holds no state and nothing needs a second class per position. */
[data-component="meter"]{display:block;height:var(--border-width-hairline);width:100%;overflow:hidden}
[data-component="meter"]::after{content:"";display:block;height:100%;
 width:calc(var(--at, 0) * var(--ratio-1));transition:width var(--motion-duration-long) var(--motion-ease-settle)}
/* A static meter takes a step: `data-at="40"`. `--at` itself is runtime state, set by the
   behaviour as a reader moves. */
[data-component="meter"][data-at="0"]{--at:0}
[data-component="meter"][data-at="40"]{--at:40}
[data-component="meter"][data-at="50"]{--at:50}

/* ---- atoms/meter.skin ---- */
/* meter: the paint. The track is the hairline every rule on the site takes; the fill is the
   accent, because progress is the one thing on the page asking to be noticed. */
[data-component="meter"]{background-color:var(--colour-border-default)}
[data-component="meter"]::after{background-color:var(--colour-accent-base)}

/* ---- components/stepPanel.form ---- */
/* stepPanel: one step of a stepFlow - its number, what it asks, and the answers to it.

   The unit a stepFlow repeats. The 2026 site called it QuizQuestion and gave the answer row no
   name at all, so the row's anatomy lived inside the question's own styles and nothing else
   could ever draw one.

   THE NUMBER IS A COLUMN, the way bigNumberItem sets its numeral: every step starts on the same line
   whatever the count reaches, which is what stops a two-digit step shifting the question. */
[data-component="stepPanel"]{display:grid;grid-template-columns:var(--grid-column-1) minmax(0,1fr);
 gap:var(--space-gutter);align-items:start}
/* SET AS bigNumberItem IS (owner, 2026-09-24): the step's number is the big-number figure, light,
   capitals, no tracking, in primary ink; the question is the headline atom at the title grade;
   the prompt the text atom 8px under it. The fieldset draws no frame of its own. It was a 14px
   accent label over an h3. */
[data-component="stepPanel"]{border:0;margin:0;padding:0;min-width:0}
[data-component="stepPanel"] .n{font:var(--type-big-number-light-font);letter-spacing:normal;text-transform:uppercase;margin-bottom:0}
[data-component="stepPanel"] legend{padding:0}
[data-component="stepPanel"] .ask{max-width:var(--grid-column-8)}
[data-component="stepPanel"] .ask > p{margin-top:var(--space-step-1)}
[data-component="stepPanel"] .options{list-style:none;padding:0;display:grid;gap:var(--space-step-2);margin-top:var(--space-step-5);max-width:var(--grid-column-8)}
@media (max-width:760px){
 [data-component="stepPanel"]{grid-template-columns:minmax(0,1fr);gap:var(--space-step-2)}
}

/* ---- components/stepPanel.skin ---- */
/* stepPanel: the paint. The number takes the accent, because it is the one mark that says where
   in the sequence a reader is standing; the prompt under the question is secondary to it. */
[data-component="stepPanel"] .n{color:var(--colour-text-primary)}
[data-component="stepPanel"] .ask > p{color:var(--colour-text-secondary)}

/* ---- patterns/stepFlow.form ---- */
/* stepFlow: a sequence a reader moves through one step at a time, with a meter saying how far in
   they are and a total at the end.

   NAMED FOR ITS FORM, not for the assessment it happens to run. The 2026 site called this object
   DiagnosticQuiz, which meant it could never carry anything that was not a quiz - and the same
   file held the questions, the scoring and the layout, so a re-scope was a deploy.

   It is not flowFrame. That draws a diagram of stages a reader looks AT; this is a sequence a
   reader moves THROUGH, so one step is visible at a time and the rest are marked done or waiting.

   WITHOUT SCRIPT EVERY STEP IS VISIBLE, which is the resting state written here: `hidden` is set
   by the behaviour, not by the stylesheet, so a reader with no JavaScript gets the whole
   assessment as a readable document rather than one step and no way past it. */
[data-component="stepFlow"]{display:grid;gap:var(--space-step-12)}
[data-component="stepFlow"] .steps{display:grid;gap:var(--space-step-12)}
/* the controls: back, and the restart the outcome offers */
/* THE ROW IS THE GROUP'S. `display:flex` and a gap of its own were this pattern deciding again
   what buttonGroup already decides - and at the grid gutter, 30px, against the group's 24, so
   the assessment's controls sat further apart than every other pair of buttons on the site. What
   is left here is genuinely the pattern's: the rule above the row, and the height it keeps. */
[data-component="stepFlow"] .controls{
 min-height:var(--space-step-10)}
[data-component="stepFlow"] .controls [hidden]{display:none}
/* AN OUTCOME IS A SIBLING SECTION, not a child: it holds a factTable and a ctaRow, and a pattern
   inside a pattern is the ladder broken. The flow reveals them by removing `hidden`; it does not
   contain them. */
[data-component="stepFlow"] .steps > *[hidden]{display:none}
[data-component="stepFlow"] .controls{border-top-width:var(--border-width-hairline);border-top-style:solid;
 padding-top:var(--space-step-5)}

/* ---- patterns/stepFlow.skin ---- */
/* stepFlow: the paint, and nothing else. The flow itself draws no colour - its steps and its
   meter carry their own - so what is left here is the rule that separates the controls from the
   sequence above them. */
[data-component="stepFlow"] .tally{color:var(--colour-text-tertiary)}
[data-component="stepFlow"] .controls{border-top-color:var(--colour-border-default)}

/* ---- components/exhibitCard.form ---- */
/* exhibitCard: one component shown as itself - what it is called, what tier it sits on, where the
   instance came from, and the instance.

   THE SHEET HAS TO OBEY THE RULE IT DOCUMENTS. Every exhibit was an `<article data-exhibit>` in
   an unnamed grid, with its own rules in the page's <style> block: a page styling an object that
   has no name, on a sheet whose whole subject is that objects have names and tiers. The card is
   a component now, it is made of atoms - a headline, two text atoms and a label - and the grid
   it sits in is the cardGrid pattern every other set of cards uses.

   The stage keeps its page-level rules, and that is correct rather than an exception: the frame
   around an exhibit is the workbench, not the site. */
/* THE CARD IS THE HEADER, not the frame around the exhibit. It wrapped the stage for one
   commit, which made a component the container of 25 patterns - masthead, siteNav, stepFlow and
   the rest - and the gate was right to refuse it: a pattern is a section and a component sits
   inside one, never the other way round. The stage is the workbench; this is the label. */
[data-component="exhibitCard"]{display:grid;gap:var(--space-step-1);
 padding-top:var(--space-step-4);border-top-width:var(--border-width-hairline);border-top-style:solid}
/* THE SAME RHYTHM AS bigNumberList AND callBooker (owner, 2026-09-23): 32px under the rule, 8px
   between the parts. The tier is the label atom and the description the text atom, so the card
   sets no type of its own; the label's own 32px margin is the stack's here. */
[data-component="exhibitCard"] [data-component="label"]{margin-bottom:0}

/* ---- components/exhibitCard.skin ---- */
/* exhibitCard: the paint. Only the rule is the card's; the label, headline and text atoms paint
   themselves. The tier label took the accent until 2026-09-23, when the owner took the yellow out. */
[data-component="exhibitCard"]{border-top-color:var(--colour-border-default)}

/* ---- atoms/shape.form ---- */
/* shape: one wireframe solid, named by `data-shape` and drawn by assets/shapes.js into a canvas
   it sizes to this box. An atom: the canvas is its content, the way a plate is the media atom's.

   It was a bare `.shape` div. masthead and markedRow each gave it a box, each told its canvas to
   be a block and each hid it on failure - three rules written twice for a part with no name. The
   atom owns them now, and a parent says only how big the mark is where it sits.

   The box is the feature plate by default: shapes.js reads the element's own width, so an
   element with no box draws nothing. `data-shape-failed` is set by the script when there is no
   2D context; the box gives up its space rather than holding a gap. */
[data-component="shape"]{display:block;position:relative;width:var(--size-plate-feature);height:var(--size-plate-feature)}
[data-component="shape"] canvas{display:block}
[data-component="shape"][data-shape-failed]{display:none}

/* ---- atoms/shape.skin ---- */
/* shape: the paint. shapes.js strokes in the element's own `color`, so the ink is set here and
   nowhere in the script: text-primary, or the accent where the element asks for it. */
[data-component="shape"]{color:var(--colour-text-primary)}
[data-component="shape"][data-shape-accent]{color:var(--colour-accent-base)}

/* ---- atoms/spinner.form ---- */
/* spinner: work is happening and nothing can be said about how much. One element, one job.

   An atom: the ring is drawn with a border and turned, so there is nothing inside it to name.
   Where the amount IS known, the answer is `meter`, which says how far along rather than that
   something is running - two different statements, two atoms.

   IT STOPS UNDER REDUCED MOTION and says so instead: a reader who has asked for stillness gets
   a static ring, which still reads as "waiting" because it is where a spinner would be. */
[data-component="spinner"]{display:inline-block;width:var(--size-mark-m);height:var(--size-mark-m);
 border-width:var(--border-width-hairline);border-style:solid;border-radius:var(--radius-circle);
 animation:spinner-turn var(--motion-duration-short) linear infinite}
@keyframes spinner-turn{to{transform:rotate(var(--angle-360))}}
@media (prefers-reduced-motion:reduce){[data-component="spinner"]{animation:none}}

/* ---- atoms/spinner.skin ---- */
/* spinner: the paint. The ring is the hairline, one quarter of it in the accent, which is what
   makes the turn legible without a second element. */
[data-component="spinner"]{border-color:var(--colour-border-default);border-top-color:var(--colour-accent-base)}

/* ---- components/pageRow.form ---- */
/* pageRow: where a reader is in a set too long for one page, and how to move.

   A component made of atoms: link atoms for the pages, button atoms for the two ends, and a
   label for the count. The 2026 site drew this as FeedPagination and owned every one of those
   shapes inside itself, so nothing else could page anything.

   `aria-current="page"` marks where you are. A visual mark alone tells a screen reader nothing,
   and the current page is the one fact this row exists to state. */
/* THE SPACING RULES (owner, 2026-09-24): 32px from the rule to the row, the register's gap under a
   rule; the row's parts - the two buttons, the pages, the count - 16px apart on one centre line,
   as mediaCard's foot is; the pages the list atom's own 8px apart. It was the 30px gutter, the
   pages 16px apart, and a count that restated the label atom's type. */
[data-component="pageRow"]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-step-2);
 padding-top:var(--space-step-4);border-top-width:var(--border-width-hairline);border-top-style:solid}
[data-component="pageRow"] .pages{grid-auto-flow:column;grid-template-columns:none;column-gap:var(--list-row-gap)}
/* ONE TYPE, ONE CENTRE LINE. The pages take the label role the two buttons and the count are set
   in, as siteMap's links do, so every word in the row is the same size; and the row centres its
   parts rather than aligning baselines, because a button with its mark on the left has no text
   first, and its baseline was taken from the arrow's foot - "Previous" rode high while "Next"
   did not. */
[data-component="pageRow"] .pages li{font:var(--type-label-font);letter-spacing:var(--type-label-tracking)}
/* the button lifts its words by --button-optical-lift to centre them optically; the pages and the
   count take the same lift, so all six words stand on one line rather than the buttons' 1px high */
/* the 2px optical lift the pages and count once took is gone (owner, 2026-09-25): off the 8px unit
   on a live route, and the row already centres its parts on one line */
[data-component="pageRow"] .count{margin-left:auto}
/* the count is the label atom; inside a row it drops the 32px it carries for naming a block below */
[data-component="pageRow"] .count{margin-bottom:0}
/* the row ships hidden and page.js reveals it: the attribute has to beat the row's own display */
[data-component="pageRow"][hidden]{display:none}
[data-component="pageRow"] [aria-disabled]{pointer-events:none}
/* the gap between runs of numbers is a mark, not a link, in the tertiary ink the skin gives it */
[data-component="pageRow"] .pages .gap{pointer-events:none}
/* ON A TOUCH SCREEN each number is a 48px square a thumb can take */
@media (hover:none) and (pointer:coarse){
 [data-component="pageRow"] .pages{column-gap:0}
 [data-component="pageRow"] .pages [data-component="link"]{display:flex;align-items:center;justify-content:center;
  min-inline-size:var(--size-control-small);block-size:var(--size-control-small)}
}

/* ---- components/pageRow.skin ---- */
/* pageRow: the paint. The page you are on is the only one at full strength; the rest are dim
   until hovered, which is the same rule the letter index uses. */
[data-component="pageRow"]{border-top-color:var(--colour-border-default)}
[data-component="pageRow"] .count{color:var(--colour-text-tertiary)}
[data-component="pageRow"] [data-component="link"]{color:var(--colour-text-secondary)}
[data-component="pageRow"] [data-component="link"]:hover{color:var(--colour-text-primary)}
[data-component="pageRow"] [aria-current="page"]{color:var(--colour-text-primary)}
[data-component="pageRow"] [aria-disabled]{opacity:.4}
[data-component="pageRow"] .pages .gap{color:var(--colour-text-tertiary)}

/* ---- atoms/chip.form ---- */
/* chip: one short label a reader can read, and sometimes press. One element, one job.

   POLYMORPHIC ON ITS ELEMENT rather than on an axis: a `<span>` states a topic on a card and a
   `<button>` filters by it, and both are the same object - a word in a box - so they are one
   atom. The 2026 site had `Tag` and `FilterButton`, two components with two stylesheets and the
   same appearance, which is how they drifted.

   `data-on` carries the pressed state, and the button also carries `aria-pressed`, because a
   ring a sighted reader can see is not a state a screen reader is told about. */
[data-component="chip"]{display:inline-block;padding:var(--space-step-1) var(--space-step-2);border-width:var(--border-width-hairline);
 border-style:solid;font-family:var(--type-family-mono);font-size:var(--type-label-size);line-height:var(--type-label-leading);
 letter-spacing:var(--type-label-tracking);text-transform:uppercase}
button[data-component="chip"]{cursor:pointer;
 transition:border-color var(--motion-duration-long) var(--motion-ease-settle),color var(--motion-duration-long) var(--motion-ease-settle)}
/* THE LARGE CHIP (owner, 2026-09-25): a choice that IS the page's task, as /book's dates and times
   are, reads at the button grade with a 48px target rather than as a label-sized filter. */
[data-component="chip"][data-size="large"]{padding:var(--space-step-2) var(--space-step-3);
 font-size:var(--type-button-size);line-height:var(--type-label-leading)}

/* ---- atoms/chip.skin ---- */
/* chip: the paint. Off is the hairline and the dim grade; on takes the accent, which is the one
   colour on the site that means "this is doing something". */
[data-component="chip"]{border-color:var(--colour-border-default);color:var(--colour-text-tertiary)}
/* a button's own ground is paint, and the browser gives it one: the chip takes the page's */
button[data-component="chip"]{background:none}
button[data-component="chip"]:hover{border-color:var(--colour-border-strong);color:var(--colour-text-primary)}
[data-component="chip"][data-on="true"]{border-color:var(--colour-accent-base);color:var(--colour-accent-base)}
/* the large chip is the page's task, not a filter: it rests at the secondary text and the strong
   hairline, so an unchosen date reads as available rather than disabled (owner, 2026-09-25) */
[data-component="chip"][data-size="large"]{border-color:var(--colour-border-strong);color:var(--colour-text-secondary)}
[data-component="chip"][data-size="large"][data-on="true"]{border-color:var(--colour-accent-base);color:var(--colour-accent-base)}

/* ---- patterns/chipRow.form ---- */
/* chipRow: a row of chips that narrows the set below it, and says how much is left.

   A pattern made of chip atoms and a label - which is one of the five patterns that legitimately
   composes atoms, for the same reason ctaRow is: a row of words IS its words, and inventing a
   component per chip would be an atom in a wrapper.

   THE TOPICS ARE THE DOCUMENTS' OWN. Nothing here holds a list of them: the row is built from
   the topics the resolved feed cards carry, so a new topic on a document is a new chip and a
   topic nobody uses disappears. The 2026 FeedFilters held its categories in the component. */
[data-component="chipRow"]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-step-2);
 padding-bottom:var(--space-step-5)}
[data-component="chipRow"] .count{margin-left:auto}

/* THE PANEL FORM (owner, 2026-09-25): the feed's filter as the live site had it. A bar - the
   Filters control with its active count, Clear all, the item count - and under it a panel that
   opens: Date (presets and a from/to range) then Topic, each a labelled group of chips. The bar's
   parts sit 16px apart as the row's chips do; the panel stands 24px under the bar, its groups 24px
   apart, a group's label 8px over its chips. */
[data-component="chipRow"][data-form="panel"]{display:block}
[data-component="chipRow"][data-form="panel"] .bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-step-2)}
[data-component="chipRow"][data-form="panel"] .bar .count{margin-left:auto;margin-bottom:0}
[data-component="chipRow"][data-form="panel"] .open .n{margin-left:var(--space-step-1)}
[data-component="chipRow"][data-form="panel"] .open .n[hidden]{display:none}
[data-component="chipRow"][data-form="panel"] .panel{display:grid;gap:var(--space-step-3);margin-top:var(--space-step-3)}
[data-component="chipRow"][data-form="panel"] .panel[hidden]{display:none}
[data-component="chipRow"][data-form="panel"] .group{display:grid;gap:var(--space-step-1)}
[data-component="chipRow"][data-form="panel"] .group > [data-component="label"]{margin:0}
[data-component="chipRow"][data-form="panel"] .chips{display:flex;flex-wrap:wrap;gap:var(--space-step-1)}
[data-component="chipRow"][data-form="panel"] .ranges{display:flex;flex-wrap:wrap;gap:var(--space-step-2);margin-top:var(--space-step-1)}
[data-component="chipRow"][data-form="panel"] .range{display:flex;align-items:center;gap:var(--space-step-1)}
[data-component="chipRow"][data-form="panel"] .range [data-component="label"]{margin:0}
[data-component="chipRow"][data-form="panel"] .clear[hidden]{display:none}

/* ---- patterns/chipRow.skin ---- */
/* chipRow: the paint. The count is secondary to the chips it describes. */
[data-component="chipRow"] .count{color:var(--colour-text-tertiary)}

/* ---- atoms/swatch.form ---- */
/* GENERATED from design-tokens/tokens.json by pipeline/build/build-tokens.py.
   DO NOT EDIT. One rule per token, so the library can show a token without any
   page setting a property. */
[data-component="swatch"]{display:inline-block}
[data-component="swatch"][data-of="colour"]{width:var(--size-mark-l);height:var(--space-step-3);border-width:var(--border-width-hairline);border-style:solid}
[data-component="swatch"][data-of="measure"]{height:var(--space-step-2);max-width:100%}
[data-component="swatch"][data-token="grid.unit.1"]{width:var(--grid-unit-1)}
[data-component="swatch"][data-token="grid.unit.2"]{width:var(--grid-unit-2)}
[data-component="swatch"][data-token="space.step.quarter"]{width:var(--space-step-quarter)}
[data-component="swatch"][data-token="space.step.half"]{width:var(--space-step-half)}
[data-component="swatch"][data-token="space.step.1"]{width:var(--space-step-1)}
[data-component="swatch"][data-token="space.step.2"]{width:var(--space-step-2)}
[data-component="swatch"][data-token="space.step.3"]{width:var(--space-step-3)}
[data-component="swatch"][data-token="space.step.4"]{width:var(--space-step-4)}
[data-component="swatch"][data-token="space.step.5"]{width:var(--space-step-5)}
[data-component="swatch"][data-token="space.step.6"]{width:var(--space-step-6)}
[data-component="swatch"][data-token="space.step.7"]{width:var(--space-step-7)}
[data-component="swatch"][data-token="space.step.8"]{width:var(--space-step-8)}
[data-component="swatch"][data-token="space.step.9"]{width:var(--space-step-9)}
[data-component="swatch"][data-token="space.step.10"]{width:var(--space-step-10)}
[data-component="swatch"][data-token="space.step.11"]{width:var(--space-step-11)}
[data-component="swatch"][data-token="space.step.12"]{width:var(--space-step-12)}
[data-component="swatch"][data-token="grid.column.width"]{width:var(--grid-column-width)}
[data-component="swatch"][data-token="grid.gutter"]{width:var(--grid-gutter)}
[data-component="swatch"][data-token="size.mark.base"]{width:var(--size-mark-base)}
[data-component="swatch"][data-token="size.mark.s"]{width:var(--size-mark-s)}
[data-component="swatch"][data-token="size.mark.m"]{width:var(--size-mark-m)}
[data-component="swatch"][data-token="size.mark.l"]{width:var(--size-mark-l)}
[data-component="swatch"][data-token="grid.container.width"]{width:var(--grid-container-width)}
[data-component="swatch"][data-token="grid.column.1"]{width:var(--grid-column-1)}
[data-component="swatch"][data-token="grid.column.2"]{width:var(--grid-column-2)}
[data-component="swatch"][data-token="grid.column.3"]{width:var(--grid-column-3)}
[data-component="swatch"][data-token="grid.column.4"]{width:var(--grid-column-4)}
[data-component="swatch"][data-token="grid.column.5"]{width:var(--grid-column-5)}
[data-component="swatch"][data-token="grid.column.6"]{width:var(--grid-column-6)}
[data-component="swatch"][data-token="grid.column.7"]{width:var(--grid-column-7)}
[data-component="swatch"][data-token="grid.column.8"]{width:var(--grid-column-8)}
[data-component="swatch"][data-token="grid.column.9"]{width:var(--grid-column-9)}
[data-component="swatch"][data-token="grid.column.10"]{width:var(--grid-column-10)}
[data-component="swatch"][data-token="grid.column.11"]{width:var(--grid-column-11)}
[data-component="swatch"][data-token="grid.column.12"]{width:var(--grid-column-12)}
[data-component="swatch"][data-token="type.face.sans"]{font-family:var(--type-face-sans)}
[data-component="swatch"][data-token="border.stroke.1"]{width:var(--border-stroke-1)}
[data-component="swatch"][data-token="border.stroke.3"]{width:var(--border-stroke-3)}
[data-component="swatch"][data-token="size.px.8"]{width:var(--size-px-8)}
[data-component="swatch"][data-token="size.px.40"]{width:var(--size-px-40)}
[data-component="swatch"][data-token="size.px.32"]{width:var(--size-px-32)}
[data-component="swatch"][data-token="size.px.120"]{width:var(--size-px-120)}
[data-component="swatch"][data-token="size.px.180"]{width:var(--size-px-180)}
[data-component="swatch"][data-token="size.px.520"]{width:var(--size-px-520)}
[data-component="swatch"][data-token="size.px.492"]{width:var(--size-px-492)}
[data-component="swatch"][data-token="size.px.48"]{width:var(--size-px-48)}
[data-component="swatch"][data-token="size.px.80"]{width:var(--size-px-80)}
[data-component="swatch"][data-token="size.px.24"]{width:var(--size-px-24)}
[data-component="swatch"][data-token="size.px.76"]{width:var(--size-px-76)}
[data-component="swatch"][data-token="size.px.220"]{width:var(--size-px-220)}
[data-component="swatch"][data-token="size.px.380"]{width:var(--size-px-380)}
[data-component="swatch"][data-token="size.px.320"]{width:var(--size-px-320)}
[data-component="swatch"][data-token="size.px.136"]{width:var(--size-px-136)}
[data-component="swatch"][data-token="size.px.168"]{width:var(--size-px-168)}
[data-component="swatch"][data-token="size.px.96"]{width:var(--size-px-96)}
[data-component="swatch"][data-token="size.px.392"]{width:var(--size-px-392)}
[data-component="swatch"][data-token="size.px.100"]{width:var(--size-px-100)}
[data-component="swatch"][data-token="size.px.124"]{width:var(--size-px-124)}
[data-component="swatch"][data-token="radius.2"]{width:var(--radius-2)}
[data-component="swatch"][data-token="radius.4"]{width:var(--radius-4)}
[data-component="swatch"][data-token="hatch.pitch"]{width:var(--hatch-pitch)}
[data-component="swatch"][data-token="hatch.rule"]{width:var(--hatch-rule)}
[data-component="swatch"][data-token="size.1"]{width:var(--size-1)}
[data-component="swatch"][data-token="size.1-5"]{width:var(--size-1-5)}
[data-component="swatch"][data-token="size.3-2"]{width:var(--size-3-2)}
[data-component="swatch"][data-token="size.4-6"]{width:var(--size-4-6)}
[data-component="swatch"][data-token="size.6"]{width:var(--size-6)}
[data-component="swatch"][data-token="size.7"]{width:var(--size-7)}
[data-component="swatch"][data-token="size.9"]{width:var(--size-9)}
[data-component="swatch"][data-token="size.10"]{width:var(--size-10)}
[data-component="swatch"][data-token="size.14"]{width:var(--size-14)}
[data-component="swatch"][data-token="size.30"]{width:var(--size-30)}
[data-component="swatch"][data-token="size.50"]{width:var(--size-50)}
[data-component="swatch"][data-token="size.70"]{width:var(--size-70)}
[data-component="swatch"][data-token="size.230"]{width:var(--size-230)}
[data-component="swatch"][data-token="size.421"]{width:var(--size-421)}
[data-component="swatch"][data-token="size.neg-1"]{width:var(--size-neg-1)}
[data-component="swatch"][data-token="size.neg-9"]{width:var(--size-neg-9)}
[data-component="swatch"][data-token="size.3"]{width:var(--size-3)}
[data-component="swatch"][data-token="size.2"]{width:var(--size-2)}
[data-component="swatch"][data-token="size.845"]{width:var(--size-845)}
[data-component="swatch"][data-token="size.200"]{width:var(--size-200)}
[data-component="swatch"][data-token="size.neg-12"]{width:var(--size-neg-12)}
[data-component="swatch"][data-token="size.neg-8"]{width:var(--size-neg-8)}
[data-component="swatch"][data-token="size.neg-3"]{width:var(--size-neg-3)}
[data-component="swatch"][data-token="size.8"]{width:var(--size-8)}
[data-component="swatch"][data-token="size.4"]{width:var(--size-4)}
[data-component="swatch"][data-token="size.75"]{width:var(--size-75)}
[data-component="swatch"][data-token="size.neg-4"]{width:var(--size-neg-4)}
[data-component="swatch"][data-token="size.neg-2"]{width:var(--size-neg-2)}
[data-component="swatch"][data-token="size.20"]{width:var(--size-20)}
[data-component="swatch"][data-token="size.300"]{width:var(--size-300)}
[data-component="swatch"][data-token="size.17"]{width:var(--size-17)}
[data-component="swatch"][data-token="size.76"]{width:var(--size-76)}
[data-component="swatch"][data-token="size.112"]{width:var(--size-112)}
[data-component="swatch"][data-token="size.9-5"]{width:var(--size-9-5)}
[data-component="swatch"][data-token="size.11-5"]{width:var(--size-11-5)}
[data-component="swatch"][data-token="size.13"]{width:var(--size-13)}
[data-component="swatch"][data-token="size.26"]{width:var(--size-26)}
[data-component="swatch"][data-token="size.8-5"]{width:var(--size-8-5)}
[data-component="swatch"][data-token="size.11"]{width:var(--size-11)}
[data-component="swatch"][data-token="size.400"]{width:var(--size-400)}
[data-component="swatch"][data-token="size.32"]{width:var(--size-32)}
[data-component="swatch"][data-token="size.80"]{width:var(--size-80)}
[data-component="swatch"][data-token="size.neg-40"]{width:var(--size-neg-40)}
[data-component="swatch"][data-token="size.18"]{width:var(--size-18)}
[data-component="swatch"][data-token="size.0-75"]{width:var(--size-0-75)}
[data-component="swatch"][data-token="size.4-5"]{width:var(--size-4-5)}
[data-component="swatch"][data-token="size.120"]{width:var(--size-120)}
[data-component="swatch"][data-token="size.210"]{width:var(--size-210)}
[data-component="swatch"][data-token="type.family.text"]{font-family:var(--type-family-text)}
[data-component="swatch"][data-token="type.family.display"]{font-family:var(--type-family-display)}
[data-component="swatch"][data-token="type.family.mono"]{font-family:var(--type-family-mono)}
[data-component="swatch"][data-token="motion.travel.1"]{width:var(--motion-travel-1)}
[data-component="swatch"][data-token="motion.travel.2"]{width:var(--motion-travel-2)}
[data-component="swatch"][data-token="size.blur.sm"]{width:var(--size-blur-sm)}
[data-component="swatch"][data-token="size.blur.base"]{width:var(--size-blur-base)}
[data-component="swatch"][data-token="size.blur.lg"]{width:var(--size-blur-lg)}
[data-component="swatch"][data-token="size.mark.xs"]{width:var(--size-mark-xs)}

/* ---- atoms/swatch.skin ---- */
/* GENERATED from design-tokens/tokens.json by pipeline/build/build-tokens.py.
   DO NOT EDIT. */
[data-component="swatch"][data-of="colour"]{border-color:var(--colour-border-default)}
[data-component="swatch"][data-of="measure"]{background-color:var(--colour-accent-base)}
[data-component="swatch"][data-token="colour.ink.900"]{background-color:var(--colour-ink-900)}
[data-component="swatch"][data-token="colour.ink.850"]{background-color:var(--colour-ink-850)}
[data-component="swatch"][data-token="colour.ink.800"]{background-color:var(--colour-ink-800)}
[data-component="swatch"][data-token="colour.ink.700"]{background-color:var(--colour-ink-700)}
[data-component="swatch"][data-token="colour.grey.500"]{background-color:var(--colour-grey-500)}
[data-component="swatch"][data-token="colour.grey.400"]{background-color:var(--colour-grey-400)}
[data-component="swatch"][data-token="colour.grey.300"]{background-color:var(--colour-grey-300)}
[data-component="swatch"][data-token="colour.tint.cool"]{background-color:var(--colour-tint-cool)}
[data-component="swatch"][data-token="colour.white.base"]{background-color:var(--colour-white-base)}
[data-component="swatch"][data-token="colour.yellow.400"]{background-color:var(--colour-yellow-400)}
[data-component="swatch"][data-token="colour.violet.900"]{background-color:var(--colour-violet-900)}
[data-component="swatch"][data-token="colour.teal.900"]{background-color:var(--colour-teal-900)}
[data-component="swatch"][data-token="colour.coral.400"]{background-color:var(--colour-coral-400)}
[data-component="swatch"][data-token="count.12"]{background-color:var(--count-12)}
[data-component="swatch"][data-token="opacity.20"]{background-color:var(--opacity-20)}
[data-component="swatch"][data-token="opacity.12"]{background-color:var(--opacity-12)}
[data-component="swatch"][data-token="blend.screen"]{background-color:var(--blend-screen)}
[data-component="swatch"][data-token="size.2-3cqw"]{background-color:var(--size-2-3cqw)}
[data-component="swatch"][data-token="size.7-4cqw"]{background-color:var(--size-7-4cqw)}
[data-component="swatch"][data-token="leading.1"]{background-color:var(--leading-1)}
[data-component="swatch"][data-token="colour.grey.100"]{background-color:var(--colour-grey-100)}
[data-component="swatch"][data-token="colour.green.400"]{background-color:var(--colour-green-400)}
[data-component="swatch"][data-token="colour.mint.400"]{background-color:var(--colour-mint-400)}
[data-component="swatch"][data-token="colour.ink.a40"]{background-color:var(--colour-ink-a40)}
[data-component="swatch"][data-token="colour.ink.a62"]{background-color:var(--colour-ink-a62)}
[data-component="swatch"][data-token="colour.ink.a72"]{background-color:var(--colour-ink-a72)}
[data-component="swatch"][data-token="colour.white.a04"]{background-color:var(--colour-white-a04)}
[data-component="swatch"][data-token="colour.white.a05"]{background-color:var(--colour-white-a05)}
[data-component="swatch"][data-token="colour.white.a055"]{background-color:var(--colour-white-a055)}
[data-component="swatch"][data-token="colour.white.a06"]{background-color:var(--colour-white-a06)}
[data-component="swatch"][data-token="colour.white.a07"]{background-color:var(--colour-white-a07)}
[data-component="swatch"][data-token="colour.white.a08"]{background-color:var(--colour-white-a08)}
[data-component="swatch"][data-token="colour.white.a12"]{background-color:var(--colour-white-a12)}
[data-component="swatch"][data-token="colour.white.a20"]{background-color:var(--colour-white-a20)}
[data-component="swatch"][data-token="colour.white.a22"]{background-color:var(--colour-white-a22)}
[data-component="swatch"][data-token="colour.white.a45"]{background-color:var(--colour-white-a45)}
[data-component="swatch"][data-token="colour.white.a95"]{background-color:var(--colour-white-a95)}
[data-component="swatch"][data-token="colour.surface.page"]{background-color:var(--colour-surface-page)}
[data-component="swatch"][data-token="colour.surface.raised"]{background-color:var(--colour-surface-raised)}
[data-component="swatch"][data-token="colour.text.primary"]{background-color:var(--colour-text-primary)}
[data-component="swatch"][data-token="colour.text.secondary"]{background-color:var(--colour-text-secondary)}
[data-component="swatch"][data-token="colour.text.tertiary"]{background-color:var(--colour-text-tertiary)}
[data-component="swatch"][data-token="colour.border.default"]{background-color:var(--colour-border-default)}
[data-component="swatch"][data-token="colour.border.strong"]{background-color:var(--colour-border-strong)}
[data-component="swatch"][data-token="colour.border.faint"]{background-color:var(--colour-border-faint)}
[data-component="swatch"][data-token="colour.text.specimen"]{background-color:var(--colour-text-specimen)}
[data-component="swatch"][data-token="colour.text.resting"]{background-color:var(--colour-text-resting)}
[data-component="swatch"][data-token="colour.accent.base"]{background-color:var(--colour-accent-base)}
[data-component="swatch"][data-token="colour.accent.2"]{background-color:var(--colour-accent-2)}
[data-component="swatch"][data-token="colour.accent.3"]{background-color:var(--colour-accent-3)}
[data-component="swatch"][data-token="colour.accent.4"]{background-color:var(--colour-accent-4)}
[data-component="swatch"][data-token="colour.surface.card"]{background-color:var(--colour-surface-card)}
[data-component="swatch"][data-token="colour.accent.shine"]{background-color:var(--colour-accent-shine)}
[data-component="swatch"][data-token="colour.surface.stage"]{background-color:var(--colour-surface-stage)}
[data-component="swatch"][data-token="colour.surface.plate"]{background-color:var(--colour-surface-plate)}
[data-component="swatch"][data-token="colour.border.grid"]{background-color:var(--colour-border-grid)}
[data-component="swatch"][data-token="colour.feedback.warn"]{background-color:var(--colour-feedback-warn)}
[data-component="swatch"][data-token="colour.action.surface"]{background-color:var(--colour-action-surface)}
[data-component="swatch"][data-token="colour.action.text"]{background-color:var(--colour-action-text)}
[data-component="swatch"][data-token="colour.action.surface.hover"]{background-color:var(--colour-action-surface-hover)}
[data-component="swatch"][data-token="colour.action.text.hover"]{background-color:var(--colour-action-text-hover)}
[data-component="swatch"][data-token="colour.action.surface.disabled"]{background-color:var(--colour-action-surface-disabled)}
[data-component="swatch"][data-token="colour.action.text.disabled"]{background-color:var(--colour-action-text-disabled)}
[data-component="swatch"][data-token="colour.action.border.disabled"]{background-color:var(--colour-action-border-disabled)}
[data-component="swatch"][data-token="colour.action.hatch.disabled"]{background-color:var(--colour-action-hatch-disabled)}
[data-component="swatch"][data-token="hatch.disabled.pitch"]{background-color:var(--hatch-disabled-pitch)}
[data-component="swatch"][data-token="hatch.disabled.rule"]{background-color:var(--hatch-disabled-rule)}
[data-component="swatch"][data-token="colour.surface.veil"]{background-color:var(--colour-surface-veil)}
[data-component="swatch"][data-token="colour.surface.veil.strong"]{background-color:var(--colour-surface-veil-strong)}
[data-component="swatch"][data-token="colour.surface.veil.light"]{background-color:var(--colour-surface-veil-light)}
[data-component="swatch"][data-token="colour.surface.measured"]{background-color:var(--colour-surface-measured)}
[data-component="swatch"][data-token="colour.surface.cool"]{background-color:var(--colour-surface-cool)}
[data-component="swatch"][data-token="colour.surface.field"]{background-color:var(--colour-surface-field)}
[data-component="swatch"][data-token="colour.surface.field.focus"]{background-color:var(--colour-surface-field-focus)}
[data-component="swatch"][data-token="colour.border.field"]{background-color:var(--colour-border-field)}
[data-component="swatch"][data-token="colour.border.field.hover"]{background-color:var(--colour-border-field-hover)}
[data-component="swatch"][data-token="colour.accent.cool"]{background-color:var(--colour-accent-cool)}
[data-component="swatch"][data-token="colour.accent.warm"]{background-color:var(--colour-accent-warm)}
[data-component="swatch"][data-token="size.expanse"]{background-color:var(--size-expanse)}
[data-component="swatch"][data-token="opacity.veil"]{background-color:var(--opacity-veil)}
[data-component="swatch"][data-token="opacity.field"]{background-color:var(--opacity-field)}
[data-component="swatch"][data-token="motion.exchange.drift"]{background-color:var(--motion-exchange-drift)}
[data-component="swatch"][data-token="colour.blend.light"]{background-color:var(--colour-blend-light)}
[data-component="swatch"][data-token="grid.measure.prose"]{background-color:var(--grid-measure-prose)}
[data-component="swatch"][data-token="type.reflow.rest"]{background-color:var(--type-reflow-rest)}
[data-component="swatch"][data-token="type.reflow.promoted"]{background-color:var(--type-reflow-promoted)}
[data-component="swatch"][data-token="type.leading.solid"]{background-color:var(--type-leading-solid)}
[data-component="swatch"][data-token="size.control"]{background-color:var(--size-control)}
[data-component="swatch"][data-token="size.control.small"]{background-color:var(--size-control-small)}
[data-component="swatch"][data-token="colour.feedback.warn.quiet"]{background-color:var(--colour-feedback-warn-quiet)}
[data-component="swatch"][data-token="colour.feedback.good"]{background-color:var(--colour-feedback-good)}
[data-component="swatch"][data-token="size.icon.small"]{background-color:var(--size-icon-small)}
[data-component="swatch"][data-token="size.icon.medium"]{background-color:var(--size-icon-medium)}
[data-component="swatch"][data-token="size.icon.large"]{background-color:var(--size-icon-large)}
[data-component="swatch"][data-token="size.icon.display"]{background-color:var(--size-icon-display)}
[data-component="swatch"][data-token="angle.mark.wave"]{background-color:var(--angle-mark-wave)}
[data-component="swatch"][data-token="angle.mark.cross"]{background-color:var(--angle-mark-cross)}
[data-component="swatch"][data-token="origin.mark.wrist.x"]{background-color:var(--origin-mark-wrist-x)}
[data-component="swatch"][data-token="origin.mark.wrist.y"]{background-color:var(--origin-mark-wrist-y)}
[data-component="swatch"][data-token="size.mark.travel"]{background-color:var(--size-mark-travel)}
[data-component="swatch"][data-token="bar.site.background"]{background-color:var(--bar-site-background)}
[data-component="swatch"][data-token="chip.background"]{background-color:var(--chip-background)}
[data-component="swatch"][data-token="reel.background"]{background-color:var(--reel-background)}
[data-component="swatch"][data-token="frame.plate.cool"]{background-color:var(--frame-plate-cool)}
[data-component="swatch"][data-token="field.background"]{background-color:var(--field-background)}
[data-component="swatch"][data-token="field.background.focus"]{background-color:var(--field-background-focus)}
[data-component="swatch"][data-token="field.border"]{background-color:var(--field-border)}
[data-component="swatch"][data-token="field.border.hover"]{background-color:var(--field-border-hover)}
[data-component="swatch"][data-token="aurora.colour.1"]{background-color:var(--aurora-colour-1)}
[data-component="swatch"][data-token="aurora.colour.2"]{background-color:var(--aurora-colour-2)}
[data-component="swatch"][data-token="aurora.colour.3"]{background-color:var(--aurora-colour-3)}
[data-component="swatch"][data-token="frame.plate.ink"]{background-color:var(--frame-plate-ink)}
[data-component="swatch"][data-token="field.inset.block"]{background-color:var(--field-inset-block)}
[data-component="swatch"][data-token="field.inset.inline"]{background-color:var(--field-inset-inline)}
[data-component="swatch"][data-token="field.small.block"]{background-color:var(--field-small-block)}
[data-component="swatch"][data-token="field.small.inline"]{background-color:var(--field-small-inline)}
[data-component="swatch"][data-token="field.border.focus"]{background-color:var(--field-border-focus)}
[data-component="swatch"][data-token="field.border.error"]{background-color:var(--field-border-error)}
[data-component="swatch"][data-token="field.text.error"]{background-color:var(--field-text-error)}
[data-component="swatch"][data-token="field.border.good"]{background-color:var(--field-border-good)}
[data-component="swatch"][data-token="field.text.good"]{background-color:var(--field-text-good)}
[data-component="swatch"][data-token="specimen.veil.1"]{background-color:var(--specimen-veil-1)}
[data-component="swatch"][data-token="specimen.veil.2"]{background-color:var(--specimen-veil-2)}
[data-component="swatch"][data-token="specimen.veil.3"]{background-color:var(--specimen-veil-3)}
[data-component="swatch"][data-token="specimen.veil.4"]{background-color:var(--specimen-veil-4)}
[data-component="swatch"][data-token="specimen.veil.5"]{background-color:var(--specimen-veil-5)}
[data-component="swatch"][data-token="specimen.veil.6"]{background-color:var(--specimen-veil-6)}
[data-component="swatch"][data-token="specimen.veil.7"]{background-color:var(--specimen-veil-7)}

/* ---- components/linkRow.form ---- */
/* linkRow: one record in an index, and the whole row is the link.

   Three cells on the register - a code, a title, and a note about the record's source. A
   component made of atoms: the code atom for the identifier, a headline or text for the title,
   and a label for the note.

   The ROW is the target, not a word inside it. A list of records where only the title is
   clickable gives a reader a small target beside a large one that does nothing, and a keyboard
   reader an extra stop per row.

   Below 1100px the three cells stack, as factRow's do. */

[data-component="linkRow"]{display:grid;
 grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--space-gutter);
 align-items:baseline;padding:var(--space-step-4) 0;
 border-top-width:var(--border-width-hairline);border-top-style:solid;text-decoration:none}
[data-component="linkRow"]:last-child{border-bottom-width:var(--border-width-hairline);border-bottom-style:solid}
/* FACTROW'S GRID (owner, 2026-09-24): twelve tracks, each cell three wide from the left edge, so a
   record's code, title and source stand in the same columns as a fact row's label and value above
   or below it. It was its own three widths - three columns, the slack, four columns - with the
   source pushed to the right edge. */
[data-component="linkRow"] > *{grid-column:span 3}
[data-component="linkRow"] .ti{min-width:0;margin:0}
[data-component="linkRow"] .src{text-transform:uppercase}

/* the row steps aside on approach: a movement, so it is form. The paint file carries the
   colour change that travels with it. */
[data-component="linkRow"]{transition:padding-left var(--motion-exchange-base) var(--motion-ease-settle),
 color var(--motion-exchange-base) var(--motion-ease-settle)}
[data-component="linkRow"]:hover,
[data-component="linkRow"]:focus-visible{padding-left:var(--size-px-8)}

@media (max-width:1100px){
 [data-component="linkRow"]{grid-template-columns:minmax(0,1fr);gap:var(--space-step-1)}
 [data-component="linkRow"] > *{grid-column:auto}
}

/* ---- components/linkRow.skin ---- */
/* linkRow: the paint. A record reads at the secondary ink and comes forward on approach; the
   code stays at full strength because it is the thing a reader scans down. The movement and its
   timing are in the form file, which is where a transition belongs. */
[data-component="linkRow"]{color:var(--colour-text-secondary);
 border-top-color:var(--colour-border-default);border-bottom-color:var(--colour-border-default)}
[data-component="linkRow"]:hover,
[data-component="linkRow"]:focus-visible{color:var(--colour-text-primary)}
[data-component="linkRow"] .src{color:var(--colour-text-tertiary)}

/* ---- patterns/linkIndex.form ---- */
/* linkIndex: a set of records, listed.

   A pattern made of components: a sectionHead states what the set is, and one linkRow per
   record. The pattern owns the vertical rhythm between the head and the rows and nothing else -
   a row's own anatomy is the row's. */
[data-component="linkIndex"]{display:block}
[data-component="linkIndex"] .rows{display:block;margin-top:var(--space-step-5)}

/* ---- patterns/linkIndex.skin ---- */
/* linkIndex: no paint of its own. The head and the rows carry theirs, and a pattern that
   repaints its children is a pattern deciding for them. */
