/* ===========================================================================
   Kestrel website - marketing layer.

   THE COMPOSITIONAL IDEA, so that a change can be judged against it: the
   ledger row. Kestrel's subject is work that is recorded, so the page is built
   out of ruled rows with a narrow left rail carrying machine truth (a stage
   index, a service key, a scope, a unit) and a wide right column carrying
   language. The hairline between them is structure, not decoration: it is the
   column boundary. Nothing on this site is a floating card scattered over a
   gradient.

   MONO IS RESERVED. JetBrains Mono appears only where the text is something a
   machine produced or consumes: operation keys, scopes, capabilities, units,
   states, identifiers, rates. It is never used for emphasis.

   ACCENT IS RESERVED. Vermilion means one of three things and nothing else:
   the primary action, the current state, or the moving item. A heading is
   never accent-coloured for effect.
   =========================================================================== */

/* ============================================================ 1. structure */

.section {
    padding-block: var(--section-y);
    border-top: var(--rule);
}

.section--flush {
    border-top: 0;
}

.section--tint {
    background: var(--ks-raised);
}

/* The section head. Headline first, supporting line beneath it, both in the
   same column: a big headline on the left with a small paragraph floating in
   the top right is a layout tell and reads as two unrelated thoughts. */
.head {
    max-width: 46ch;
    margin-bottom: clamp(2rem, 3.6vw, 3rem);
}

.head--wide {
    max-width: 60ch;
}

/* Used where the head is a column of a split rather than the top of a section,
   so its own bottom margin would open a gap inside the grid. */
.head--flush {
    margin-bottom: 0;
}

.head__title {
    font-size: var(--step-title);
    font-weight: 800;
    letter-spacing: -.032em;
    line-height: 1.06;
    text-wrap: balance;
}

.head__lead {
    margin-top: 1.125rem;
    max-width: 58ch;
    font-size: var(--step-lead);
    color: var(--ks-text-muted);
    line-height: 1.58;
    text-wrap: pretty;
}

.head__meta {
    display: flex;
    align-items: center;
    gap: .625rem;
    margin-bottom: 1.25rem;
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    color: var(--ks-text-subtle);
}

.head__meta::before {
    content: '';
    width: 1.75rem;
    height: 1px;
    background: var(--ks-accent-ink);
    flex: none;
}

/* ================================================================ 2. header */

.header {
    position: sticky;
    top: 0;
    z-index: 100;
    background: color-mix(in oklab, var(--ks-canvas) 88%, transparent);
    backdrop-filter: saturate(140%) blur(14px);
    border-bottom: 1px solid transparent;
    transition: border-color .2s var(--ease), background-color .2s var(--ease);
}

/* The rule appears once the page has moved, so the header is part of the hero
   at rest and a separate bar once it is covering content. */
.header[data-scrolled='true'] {
    border-bottom-color: var(--ks-border);
    background: color-mix(in oklab, var(--ks-canvas) 96%, transparent);
}

@supports not (backdrop-filter: blur(1px)) {
    .header {
        background: var(--ks-canvas);
    }
}

.header__inner {
    display: flex;
    align-items: center;
    gap: 1rem;
    height: 4.25rem;
}

.brand {
    display: inline-flex;
    align-items: center;
    /* Tighter than the old square needed: a stroked K has its own optical
       side-bearing, so .5rem left a visible gap between mark and wordmark. */
    gap: .4375rem;
    flex: none;
    margin-right: auto;
}

.brand__name {
    font-size: 1.0625rem;
    font-weight: 800;
    letter-spacing: -.035em;
    color: var(--ks-ink);
}

.nav {
    display: none;
    align-items: center;
    gap: .125rem;
}

.nav__link {
    padding: .4375rem .6875rem;
    border-radius: var(--ks-radius-md);
    font-size: var(--step-small);
    font-weight: 600;
    color: var(--ks-text-muted);
    transition: background-color .16s var(--ease), color .16s var(--ease);
}

.nav__link:hover {
    background: var(--ks-sunken);
    color: var(--ks-text);
}

/* The current page is marked by weight and a rule as well as colour, so it is
   not carried by hue alone. */
.nav__link[aria-current='page'] {
    color: var(--ks-text);
    font-weight: 700;
    background: var(--ks-sunken);
    box-shadow: inset 0 -2px 0 var(--ks-accent-ink);
}

.header__actions {
    display: none;
    align-items: center;
    gap: .5rem;
    flex: none;
}

/* The theme control depends on script, so it is absent until script says
   otherwise rather than present and inert. */
.theme {
    display: none;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid var(--ks-border);
    border-radius: var(--ks-radius-md);
    background: var(--ks-surface);
    color: var(--ks-text-muted);
    cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease);
}

.js .theme {
    display: inline-flex;
}

/* The same control with a visible label, for the mobile panel where an icon
   alone in a stack of full-width buttons reads as a mistake. */
.theme-inline {
    display: none;
    border: 1px solid var(--ks-border);
    width: 100%;
}

.js .theme-inline {
    display: inline-flex;
}

.theme:hover {
    color: var(--ks-text);
    border-color: var(--ks-border-strong);
}

.theme svg {
    width: 1rem;
    height: 1rem;
}

.theme__moon {
    display: none;
}

:root[data-theme='dark'] .theme__moon {
    display: block;
}

:root[data-theme='dark'] .theme__sun {
    display: none;
}

/* --- mobile disclosure. A <details> so it opens with the keyboard, opens with
   no script at all, and announces itself as a disclosure rather than as a
   button with an invented role. */
.drawer {
    flex: none;
}

.drawer__toggle {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    min-height: 2.5rem;
    padding: .375rem .75rem;
    border: 1px solid var(--ks-border-strong);
    border-radius: var(--ks-radius-md);
    background: var(--ks-surface);
    font-size: var(--step-meta);
    font-weight: 700;
    color: var(--ks-text);
    cursor: pointer;
    list-style: none;
}

/* Three ways, because the disclosure triangle is painted by three different
   mechanisms across engines and any one of them left in place puts a stray
   caret next to the word Menu. */
.drawer__toggle::-webkit-details-marker {
    display: none;
}

.drawer__toggle::marker {
    content: '';
}

.drawer__toggle::before {
    content: none;
}

.drawer__bars {
    display: grid;
    gap: 3px;
    width: .875rem;
}

.drawer__bars span {
    height: 1.5px;
    background: currentColor;
    border-radius: 1px;
    transition: transform .2s var(--ease);
}

.drawer[open] .drawer__bars span:first-child {
    transform: translateY(4.5px) rotate(45deg);
}

.drawer[open] .drawer__bars span:nth-child(2) {
    opacity: 0;
}

.drawer[open] .drawer__bars span:last-child {
    transform: translateY(-4.5px) rotate(-45deg);
}

.drawer__panel {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    background: var(--ks-surface);
    border-top: var(--rule);
    border-bottom: var(--rule);
    box-shadow: var(--ks-shadow-3);
    max-height: calc(100dvh - 4.25rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.drawer__list {
    display: grid;
    padding: .5rem var(--gutter) 1rem;
}

.drawer__item {
    display: grid;
    gap: .125rem;
    padding: .875rem .25rem;
    border-bottom: var(--rule);
}

.drawer__item:last-of-type {
    border-bottom: 0;
}

.drawer__label {
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -.02em;
}

/* The current page is marked by a rule as well as by colour, so it is legible
   without depending on hue. */
.drawer__item[aria-current='page'] {
    padding-left: .75rem;
    margin-left: -.5rem;
    border-left: 2px solid var(--ks-accent-ink);
}

.drawer__item[aria-current='page'] .drawer__label {
    color: var(--ks-accent-ink);
}

.drawer__summary {
    font-size: var(--step-small);
    color: var(--ks-text-muted);
}

.drawer__actions {
    display: grid;
    gap: .625rem;
    padding: 1rem var(--gutter) 1.5rem;
    border-top: var(--rule);
    background: var(--ks-raised);
}

.drawer__actions .btn {
    width: 100%;
}

@media (min-width: 62rem) {
    .nav,
    .header__actions {
        display: flex;
    }

    .drawer {
        display: none;
    }
}

/* ================================================================== 3. hero */

.hero {
    padding-top: clamp(2.75rem, 4.8vw, 4.25rem);
    padding-bottom: clamp(2.5rem, 4vw, 3.5rem);
    position: relative;
    overflow: clip;
}

/* THE HERO HAS NO AMBIENT GRAPHIC, and that was a decision rather than an
   omission. It carried a hairline grid field for a while. Three things were
   wrong with it: a two-axis hairline field tiled on a fixed pixel cell is one
   of the most recognisable generated-interface signatures; it was the third
   such field on the page, and a device used three times is a habit rather than
   a choice; and it sat behind the headline, where it competed with the only
   thing in the hero that matters. The route instrument below the copy is the
   hero's visual. The field survives in exactly one place, on the closing slab,
   where it is masked to a single corner and is the brand mark at page scale. */

/* One column, with the space on the right left empty on purpose. Filling it
   with micro-facts is the reflex; the hero is stronger for holding a single
   thought, and the instrument below it is the visual. */
.hero__title {
    font-size: var(--step-display);
    font-weight: 800;
    letter-spacing: -.042em;
    line-height: .98;
    /* An italic word in the headline carries a descender, so the line box gets
       clearance and the wrapper reserves space beneath it. */
    padding-bottom: .125rem;
    text-wrap: balance;
    /* Two lines at every width from 1024px up, which is what keeps the actions
       inside the first viewport without a font-size override per breakpoint. */
    max-width: 19ch;
}

.hero__title em {
    font-style: italic;
    font-weight: 800;
    line-height: 1.1;
}

.hero__lead {
    margin-top: 1.5rem;
    max-width: 48ch;
    font-size: var(--step-lead);
    color: var(--ks-text-muted);
    line-height: 1.55;
    text-wrap: pretty;
}

.hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
    margin-top: 2rem;
}

/* Below the width where both fit on one line, they become a stack of equal
   full-width buttons. Two content-width buttons on separate lines read as a
   mistake rather than as a pair. */
@media (max-width: 32rem) {
    .hero__actions .btn {
        flex: 1 1 100%;
    }
}


/* ============================================================= 4. the route */

/* The hero instrument: one item entering Kestrel, passing through the services
   it needs, and leaving as a result with a record attached.

   It is an ordered list, not a picture. That is a deliberate engineering
   choice: a screen reader reads six numbered stations with their states, the
   layout reflows from a row to a column without a second implementation, and
   there is no alternative text to keep in step with a graphic. */
.route {
    margin-top: clamp(2rem, 3.6vw, 3rem);
    border: var(--rule);
    border-radius: var(--ks-radius-lg);
    background: var(--ks-surface);
    box-shadow: var(--ks-shadow-2);
    overflow: hidden;
}

.route__bar {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
    padding: .75rem 1.125rem;
    border-bottom: var(--rule);
    background: var(--ks-raised);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

.route__bar strong {
    color: var(--ks-text);
    font-weight: 500;
}

.route__bar-sep {
    width: 1px;
    height: .875rem;
    background: var(--ks-border-strong);
}

.route__body {
    padding: clamp(1.25rem, 3vw, 2rem) clamp(1.125rem, 3vw, 2rem);
    display: grid;
    gap: 1.5rem;
}

.route__endpoint {
    display: grid;
    gap: .375rem;
    align-content: start;
    padding: .875rem 1rem;
    border: var(--rule);
    border-radius: var(--ks-radius);
    background: var(--ks-raised);
}

.route__endpoint--out {
    border-color: color-mix(in oklab, var(--ks-success) 38%, var(--ks-border));
    background: color-mix(in oklab, var(--ks-success) 7%, var(--ks-surface));
}

.route__endpoint-label {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.route__endpoint-name {
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.01em;
    word-break: break-word;
}

.route__endpoint-note {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-muted);
}

/* --- the stations */

.route__track {
    container-type: inline-size;
    position: relative;
}

.route__stations {
    display: grid;
    gap: .125rem;
    position: relative;
}

/* On a narrow screen the route runs downwards, and the rule through the chips
   is what makes it read as a route rather than as six unrelated rows. */
.route__stations::before {
    content: '';
    position: absolute;
    top: 1rem;
    bottom: 1rem;
    left: .3125rem;
    width: 2px;
    border-radius: 1px;
    background: var(--ks-border);
}

.route__station {
    display: grid;
    grid-template-columns: .75rem minmax(0, 1fr) auto;
    grid-template-areas:
        'chip name state'
        '.    op   op';
    column-gap: .75rem;
    row-gap: .125rem;
    align-items: baseline;
    padding: .5rem 0;
}

.route__chip {
    grid-area: chip;
    width: .75rem;
    height: .75rem;
    border-radius: 2px;
    /* Success by default: the diagram's resting state is the completed one, so
       the chips agree with the state labels beside them without any motion.
       The desktop animation overrides this as the item passes. */
    background: var(--ks-success);
    align-self: center;
    position: relative;
}

.route__station-name {
    grid-area: name;
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.012em;
}

.route__station-op {
    grid-area: op;
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
    overflow-wrap: break-word;
}

.route__state {
    grid-area: state;
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-success);
    text-align: right;
}

/* On a wide viewport the stations become the route itself: a horizontal rail
   with the chips sitting on it. Below that they stay a vertical list, which is
   the same information in the shape a narrow screen can hold.

   THE BREAKPOINT IS 62rem, NOT 48rem. Six stations plus an operation key each
   need roughly 900px before the labels stop colliding. At tablet width the
   horizontal rail technically fits and is unreadable, which is the difference
   between a layout that is responsive and one that has merely been compressed:
   a tablet gets the vertical route, on purpose. */
@media (min-width: 62rem) {
    .route__body {
        grid-template-columns: minmax(8.5rem, .8fr) minmax(0, 3fr) minmax(9rem, .95fr);
        align-items: center;
        gap: clamp(1rem, 2vw, 1.75rem);
    }

    .route__stations {
        grid-template-columns: repeat(6, minmax(0, 1fr));
        gap: 0;
    }

    /* The rail turns through 90 degrees: same element, drawn at the height of
       the chip centres and inset by half a column so it starts and ends on a
       chip rather than in mid-air. */
    .route__stations::before {
        top: .375rem;
        bottom: auto;
        left: calc(100% / 12);
        right: calc(100% / 12);
        width: auto;
        height: 2px;
    }

    .route__station {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            'chip'
            'name'
            'op'
            'state';
        justify-items: center;
        text-align: center;
        row-gap: .375rem;
        padding: 0 .375rem;
        align-content: start;
    }

    .route__station-name {
        font-size: var(--step-meta);
        line-height: 1.25;
    }

    .route__state {
        text-align: center;
    }

    /* Two lines' worth of room whether the key needs it or not, so the state
       labels sit on one baseline across all six stations instead of stepping
       up and down with the length of an operation key. */
    .route__station-op {
        margin-top: .1875rem;
        line-height: 1.35;
        min-height: 2.7em;
    }
}

/* --- motion. One idea: the item moves along the route and each station turns
   over as it arrives. Nothing else on the page moves on a loop.

   Justified because it is the explanation. The diagram says "an item enters,
   passes through the services you chose, and leaves with a record"; the
   animation is that sentence performed once every nine seconds. With motion
   off, the diagram already shows every station and its final state, so nothing
   is lost. */
/* Both are inert until the layout is horizontal and motion is welcome. They
   carry no styles at all otherwise, so nothing renders and nothing shifts. */
.route__pulse,
.route__fill {
    display: none;
}

@media (prefers-reduced-motion: no-preference) and (min-width: 62rem) {
    .route__pulse {
        display: block;
        position: absolute;
        top: .125rem;
        left: calc(100% / 12);
        width: .625rem;
        height: .625rem;
        border-radius: 2px;
        background: var(--ks-accent);
        box-shadow: 0 0 0 4px color-mix(in oklab, var(--ks-accent) 18%, transparent);
        pointer-events: none;
        animation: route-run 9s var(--ease) infinite;
    }

    .route__fill {
        display: block;
        position: absolute;
        top: .375rem;
        left: calc(100% / 12);
        right: calc(100% / 12);
        height: 2px;
        border-radius: 1px;
        background: var(--ks-accent);
        transform-origin: left center;
        transform: scaleX(0);
        animation: route-fill 9s var(--ease) infinite;
    }

    .route__chip {
        animation: route-chip 9s var(--ease) infinite;
        animation-delay: calc(var(--i) * .55s);
    }

    .route__state {
        animation: route-state 9s var(--ease) infinite;
        animation-delay: calc(var(--i) * .55s);
    }
}

@keyframes route-run {
    0%,
    4% {
        transform: translateX(0);
        opacity: 0;
    }

    8% {
        opacity: 1;
    }

    62% {
        transform: translateX(calc(83.33cqw - .625rem));
        opacity: 1;
    }

    70%,
    100% {
        transform: translateX(calc(83.33cqw - .625rem));
        opacity: 0;
    }
}

@keyframes route-fill {
    0%,
    6% {
        transform: scaleX(0);
    }

    62%,
    100% {
        transform: scaleX(1);
    }
}

@keyframes route-chip {
    0%,
    6% {
        background: var(--ks-border-strong);
        box-shadow: none;
    }

    12% {
        background: var(--ks-accent);
        box-shadow: 0 0 0 3px color-mix(in oklab, var(--ks-accent) 20%, transparent);
    }

    22%,
    100% {
        background: var(--ks-success);
        box-shadow: none;
    }
}

/* The state label is present in the markup at its final value, so this only
   recolours it as the item passes. The word never changes under the reader,
   which would be a layout shift and a screen-reader announcement for nothing. */
@keyframes route-state {
    0%,
    6% {
        color: var(--ks-text-subtle);
    }

    12%,
    18% {
        color: var(--ks-accent-ink);
    }

    24%,
    100% {
        color: var(--ks-success);
    }
}

/* ================================================== 5. reveal on scroll */

/* One transform and one opacity, once, 14px. Applied by script so a page with
   scripting off is simply already in position. */
.js [data-reveal] {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity .55s var(--ease-out), transform .55s var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
}

.js [data-reveal].is-in {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .js [data-reveal] {
        opacity: 1;
        transform: none;
    }
}

/* ========================================================= 6. stage explorer */

/* The lifecycle. Six stages, one of which is open at a time.

   BUILT ON RADIO INPUTS, not on a script. The tabs are a real radio group, so
   they arrive keyboard-operable (arrow keys move between them, which is the
   correct interaction for a single-choice control), they work with scripting
   off, and they need no invented ARIA. Script adds nothing here at all. */
.stages {
    display: grid;
    gap: 0;
    border: var(--rule);
    border-radius: var(--ks-radius-lg);
    background: var(--ks-surface);
    overflow: hidden;
}

.stages__group {
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
    /* The fieldset exists for the legend and the grouping, not for layout, so
       its box gets out of the way and the rail and panels lay out against
       .stages directly. Selector matching is unaffected: it reads the DOM
       tree, not the box tree. */
    display: contents;
}

/* The rail of stage buttons. Horizontally scrollable on a narrow screen with
   snap points, because six labels do not fit and hiding two of them would be
   hiding content for convenience. */
.stages__rail {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    border-bottom: var(--rule);
    background: var(--ks-raised);
    scrollbar-width: thin;
}

/* The control sits inside its own label, so the association is native and two
   explorers on one page cannot collide over an id. It is not display:none,
   because a hidden input is not focusable and the group would drop out of the
   tab order; it is a 1px box behind its own label instead. */
.stages__radio {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}

.stages__tab {
    position: relative;
    display: grid;
    gap: .1875rem;
    flex: 1 0 auto;
    min-width: 7.75rem;
    padding: .9375rem 1rem 1rem;
    border-right: var(--rule);
    scroll-snap-align: start;
    cursor: pointer;
    background: transparent;
    box-shadow: inset 0 -2px 0 transparent;
    transition: background-color .18s var(--ease), box-shadow .18s var(--ease);
}

.stages__tab:last-of-type {
    border-right: 0;
}

.stages__tab:hover {
    background: var(--ks-sunken);
}

.stages__index {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
    letter-spacing: .04em;
}

.stages__name {
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.014em;
    color: var(--ks-text-muted);
}

/* Selection is carried by three things at once: the rule, the weight and the
   colour. Never colour alone. */
.stages__tab:has(.stages__radio:checked) {
    background: var(--ks-surface);
    box-shadow: inset 0 -2px 0 var(--ks-accent-ink);
}

.stages__tab:has(.stages__radio:checked) .stages__name {
    color: var(--ks-text);
}

.stages__tab:has(.stages__radio:checked) .stages__index {
    color: var(--ks-accent-ink);
}

/* The focus ring belongs on the label, because the input it wraps is a 1px box
   nobody can see. */
.stages__tab:has(.stages__radio:focus-visible) {
    outline: 2px solid var(--ks-accent-ink);
    outline-offset: -3px;
}

.stages__panels {
    display: grid;
}

/* Visible by default. A browser without :has() support therefore shows all six
   stages stacked, which is a longer page rather than a page with its content
   missing. @supports below is what turns it into a one-at-a-time control. */
.stages__panel {
    padding: clamp(1.5rem, 3.5vw, 2.5rem);
    border-top: var(--rule);
}

.stages__panel:first-of-type {
    border-top: 0;
}

.stages__panel-grid {
    display: grid;
    gap: clamp(1.5rem, 3vw, 2.5rem);
}

.stages__verb {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-accent-ink);
}

.stages__title {
    margin-top: .625rem;
    font-size: var(--step-heading);
    font-weight: 800;
    letter-spacing: -.024em;
    line-height: 1.18;
}

.stages__body {
    margin-top: .875rem;
    max-width: 52ch;
    color: var(--ks-text-muted);
    line-height: 1.62;
}

.stages__does {
    display: grid;
    gap: 0;
    margin-top: 1.25rem;
}

.stages__does li {
    display: grid;
    grid-template-columns: .875rem minmax(0, 1fr);
    gap: .75rem;
    align-items: baseline;
    padding: .5625rem 0;
    border-bottom: var(--rule);
    font-size: var(--step-small);
    line-height: 1.5;
}

.stages__does li:last-child {
    border-bottom: 0;
}

.stages__does li::before {
    content: '';
    width: .4375rem;
    height: .4375rem;
    border-radius: 1px;
    background: color-mix(in oklab, var(--ks-accent) 70%, transparent);
    transform: translateY(-.125rem);
}

.stages__side {
    display: grid;
    gap: 1.25rem;
    align-content: start;
    padding: 1.125rem 1.25rem;
    border: var(--rule);
    border-radius: var(--ks-radius);
    background: var(--ks-raised);
}

.stages__side-block {
    display: grid;
    gap: .5rem;
}

.stages__side-label {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.stages__side-value {
    font-size: var(--step-small);
    line-height: 1.5;
    color: var(--ks-text);
}

.stages__chips {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
}

/* Panel n follows tab n. Matched by position rather than by id so the markup
   carries no wiring: adding a stage is one entry in Stages::all(). */
@supports selector(:has(*)) {
    .stages__panel {
        display: none;
        border-top: 0;
    }

    .stages:has(.stages__tab:nth-of-type(1) .stages__radio:checked) .stages__panel:nth-of-type(1),
    .stages:has(.stages__tab:nth-of-type(2) .stages__radio:checked) .stages__panel:nth-of-type(2),
    .stages:has(.stages__tab:nth-of-type(3) .stages__radio:checked) .stages__panel:nth-of-type(3),
    .stages:has(.stages__tab:nth-of-type(4) .stages__radio:checked) .stages__panel:nth-of-type(4),
    .stages:has(.stages__tab:nth-of-type(5) .stages__radio:checked) .stages__panel:nth-of-type(5),
    .stages:has(.stages__tab:nth-of-type(6) .stages__radio:checked) .stages__panel:nth-of-type(6) {
        display: block;
    }
}

/* The panel arrives rather than appearing, which is the one thing that tells
   the reader the tab did something. 300ms, opacity and transform only, and it
   cannot shift the page because the panel is the only thing in its cell. */
@media (prefers-reduced-motion: no-preference) {
    @supports selector(:has(*)) {
        .stages__panel {
            animation: panel-in .3s var(--ease-out) both;
        }
    }
}

@keyframes panel-in {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
}

@media (min-width: 56rem) {
    .stages__panel-grid {
        grid-template-columns: minmax(0, 1.6fr) minmax(15rem, .9fr);
    }
}

/* ========================================================= 7. service ledger */

/* Eight services as a ruled index. A table would be the wrong element: these
   rows are navigational summaries of a product, not cells in a data set. */
.ledger {
    border-top: var(--rule);
}

.ledger__row {
    display: grid;
    gap: .875rem 1.5rem;
    padding: clamp(1.375rem, 2.6vw, 1.875rem) 0;
    border-bottom: var(--rule);
    transition: background-color .18s var(--ease);
}

.ledger__row:hover {
    background: color-mix(in oklab, var(--ks-accent) 3.5%, transparent);
}

.ledger__rail {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .5rem .75rem;
    align-content: start;
}

.ledger__key {
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    color: var(--ks-accent-ink);
}

.ledger__access {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

.ledger__main {
    min-width: 0;
}

.ledger__name {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .375rem .75rem;
    font-size: var(--step-heading);
    font-weight: 800;
    letter-spacing: -.026em;
    line-height: 1.2;
}

.ledger__duty {
    font-size: var(--step-small);
    font-weight: 600;
    color: var(--ks-accent-ink);
    letter-spacing: -.005em;
}

.ledger__summary {
    margin-top: .625rem;
    max-width: 62ch;
    color: var(--ks-text-muted);
    line-height: 1.62;
}

.ledger__composes {
    margin-top: .875rem;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: .625rem;
    align-items: baseline;
    max-width: 64ch;
    font-size: var(--step-small);
    color: var(--ks-text-muted);
}

.ledger__composes dt {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
    white-space: nowrap;
}

.ledger__meta {
    display: grid;
    gap: 1rem;
    align-content: start;
}

.ledger__facet {
    display: grid;
    gap: .4375rem;
}

.ledger__facet-label {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.ledger__ops {
    display: grid;
    gap: .375rem;
}

.ledger__op {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-muted);
}

.ledger__op span:first-child {
    overflow-wrap: break-word;
}

.ledger__rate {
    color: var(--ks-text);
    font-weight: 500;
    white-space: nowrap;
}

.ledger__none {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

@media (min-width: 52rem) {
    .ledger__row {
        grid-template-columns: 7.5rem minmax(0, 2.1fr) minmax(13rem, 1fr);
    }

    .ledger__rail {
        display: grid;
        gap: .375rem;
    }
}

/* ========================================================== 8. code section */

.code {
    display: grid;
    gap: 0;
    border: var(--rule);
    border-radius: var(--ks-radius-lg);
    background: var(--ks-surface);
    overflow: hidden;
    box-shadow: var(--ks-shadow-2);
}

.code__tabs {
    display: flex;
    overflow-x: auto;
    scrollbar-width: thin;
    border-bottom: var(--rule);
    background: var(--ks-raised);
}

.code__radio {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}

.code__tab {
    position: relative;
    padding: .8125rem 1.125rem;
    border-right: var(--rule);
    font-size: var(--step-meta);
    font-weight: 700;
    letter-spacing: -.005em;
    color: var(--ks-text-muted);
    cursor: pointer;
    white-space: nowrap;
    box-shadow: inset 0 -2px 0 transparent;
    transition: background-color .18s var(--ease), box-shadow .18s var(--ease), color .18s var(--ease);
}

.code__tab:hover {
    background: var(--ks-sunken);
    color: var(--ks-text);
}

.code__tab:has(.code__radio:checked) {
    background: var(--ks-surface);
    color: var(--ks-text);
    box-shadow: inset 0 -2px 0 var(--ks-accent-ink);
}

.code__tab:has(.code__radio:focus-visible) {
    outline: 2px solid var(--ks-accent-ink);
    outline-offset: -3px;
}

.code__panels {
    display: grid;
    min-width: 0;
}

/* Same degradation as the stage explorer: without :has() every sample is
   shown, one after another, each with its own file label and caption. */
.code__panel {
    min-width: 0;
    border-top: var(--rule);
}

.code__panel:first-of-type {
    border-top: 0;
}

@supports selector(:has(*)) {
    .code__panel {
        display: none;
        border-top: 0;
    }

    .code:has(.code__tab:nth-of-type(1) .code__radio:checked) .code__panel:nth-of-type(1),
    .code:has(.code__tab:nth-of-type(2) .code__radio:checked) .code__panel:nth-of-type(2),
    .code:has(.code__tab:nth-of-type(3) .code__radio:checked) .code__panel:nth-of-type(3),
    .code:has(.code__tab:nth-of-type(4) .code__radio:checked) .code__panel:nth-of-type(4) {
        display: block;
    }
}

.code__file {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .625rem 1.125rem;
    border-bottom: var(--rule);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
    overflow-wrap: anywhere;
}

.code__caption {
    padding: .875rem 1.125rem 1rem;
    border-top: var(--rule);
    background: var(--ks-raised);
    font-size: var(--step-small);
    color: var(--ks-text-muted);
    line-height: 1.55;
}

/* The block itself. Scrolls inside its own box so a long line can never widen
   the page, and keeps a tab size that matches the samples. */
pre.block {
    margin: 0;
    padding: clamp(1rem, 2.2vw, 1.5rem) clamp(1rem, 2.2vw, 1.375rem);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    background: var(--ks-surface);
    color: var(--ks-text);
    font-size: .8125rem;
    line-height: 1.72;
    tab-size: 4;
    scrollbar-width: thin;
    /* JetBrains Mono ligates -> into an arrow and => into a fat one. In prose
       that is a nice touch; in a PHP sample it means the reader is shown a
       character that is not in the code they would type. */
    font-variant-ligatures: none;
    font-feature-settings: 'calt' off, 'liga' off;
}

pre.block code {
    font-size: inherit;
    display: block;
    min-width: max-content;
}

pre.block--sunken {
    background: var(--ks-sunken);
}

/* Token colours. Derived from the platform's semantic tones rather than a
   borrowed editor theme, so the code sits inside the brand instead of beside
   it. All of them clear 4.5:1 on both surfaces. */
/* Comments are not decoration in these samples: they carry the reason the line
   below them is written that way, and they are meant to be read. Muted plus
   italic, not whispered. */
.tok-c { color: var(--ks-text-muted); font-style: italic; }
.tok-k { color: var(--ks-accent-ink); }
.tok-s { color: var(--ks-success); }
.tok-n { color: var(--ks-info); }
.tok-v { color: var(--ks-text); }
.tok-f { color: var(--ks-text); font-weight: 600; }
.tok-y { color: var(--ks-info); font-weight: 600; }
/* Object keys and punctuation are both deliberately quiet, and both sit on the
   sunken surface where --ks-text-subtle measures 4.36:1 in dark mode. They
   share the muted tone instead: the distinction worth drawing in a response
   body is quiet key against coloured value, not key against brace. */
.tok-a { color: var(--ks-text-muted); }
.tok-p { color: var(--ks-text-muted); }

/* --- the two-up arrangement used on the developers page */
.duo {
    display: grid;
    gap: clamp(1.25rem, 2.5vw, 1.75rem);
    align-items: start;
}

@media (min-width: 60rem) {
    .duo {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .duo--wide-first {
        grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    }
}

/* ============================================================== 9. panels */

.panel {
    display: grid;
    gap: .75rem;
    align-content: start;
    padding: clamp(1.25rem, 2.4vw, 1.75rem);
    border: var(--rule);
    border-radius: var(--ks-radius);
    background: var(--ks-surface);
    min-width: 0;
}

.panel--tint {
    background: var(--ks-raised);
}

.panel__title {
    font-size: var(--step-subheading);
    font-weight: 800;
    letter-spacing: -.022em;
    line-height: 1.25;
}

.panel__body {
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.6;
    max-width: 54ch;
}

.panel__note {
    margin-top: .25rem;
    padding-top: .75rem;
    border-top: var(--rule);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    line-height: 1.6;
    color: var(--ks-text-subtle);
}

/* The controls grid. Four cells, one of them double width, which is the
   arrangement the content actually has rather than three identical columns. */
.controls {
    display: grid;
    gap: 1px;
    background: var(--ks-border);
    border: var(--rule);
    border-radius: var(--ks-radius-lg);
    overflow: hidden;
}

.controls__cell {
    display: grid;
    gap: .625rem;
    align-content: start;
    padding: clamp(1.25rem, 2.5vw, 1.875rem);
    background: var(--ks-surface);
}

.controls__title {
    font-size: var(--step-subheading);
    font-weight: 800;
    letter-spacing: -.022em;
    line-height: 1.25;
}

.controls__body {
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
    max-width: 48ch;
}

.controls__mech {
    margin-top: .375rem;
    padding-top: .75rem;
    border-top: 1px dashed var(--ks-border-strong);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    line-height: 1.6;
    color: var(--ks-text-subtle);
}

/* A grid gets exactly as many cells as there is content for. A span that does
   not divide into the column count leaves a blank tile, which is a planning
   error rather than a design. */

/* Two columns is exact for four, six and eight cells alike, which is why it is
   the default rather than three. */
@media (min-width: 48rem) {
    .controls {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* Four cells: two by two, with the left column wider and one surface step
       darker. Variation by proportion and surface rather than by pattern: a
       decorative field in one tile of four is the kind of texture that reads as
       filler once you notice it is only there to break the repetition. */
    .controls--four {
        grid-template-columns: minmax(0, 1.24fr) minmax(0, 1fr);
    }

    .controls--four .controls__cell:nth-child(odd) {
        background: var(--ks-raised);
    }

    .controls--four .controls__cell:nth-child(even) {
        background: var(--ks-surface);
    }
}

/* Six cells: three by two at the width where three columns still hold a
   readable measure. Three cells: one row of three at the same width, and a
   single column below it rather than a 2 + 1 with a hole in it. */
@media (min-width: 64rem) {
    .controls--six,
    .controls--three {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (min-width: 48rem) and (max-width: 63.999rem) {
    .controls--three {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ============================================================ 10. use cases */

.work {
    display: grid;
    gap: 1px;
    background: var(--ks-border);
    border-block: var(--rule);
}

.work__item {
    display: grid;
    gap: 1rem;
    padding: clamp(1.25rem, 2.2vw, 1.75rem) 0;
    background: var(--ks-canvas);
}

.section--tint .work__item {
    background: var(--ks-raised);
}

.work__sector {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.work__title {
    margin-top: .5rem;
    font-size: var(--step-subheading);
    font-weight: 800;
    letter-spacing: -.024em;
    line-height: 1.24;
    max-width: 26ch;
}

.work__body {
    margin-top: .75rem;
    max-width: 56ch;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

/* The service chain. Reads as a sequence because the separator is an arrow
   between named services, not a decorative dot. */
.chain {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .375rem;
}

.chain__step {
    display: inline-flex;
    align-items: center;
    padding: .1875rem .4375rem;
    border: 1px solid color-mix(in oklab, var(--ks-accent) 26%, transparent);
    border-radius: var(--ks-radius-sm);
    background: color-mix(in oklab, var(--ks-accent) 9%, transparent);
    color: var(--ks-accent-ink);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    white-space: nowrap;
}

.chain__arrow {
    color: var(--ks-text-subtle);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
}

/* A hairline above it and a mono label, not a coloured stripe down one side.
   The word "Evidence" is what tells the reader what this is; a green tab on
   the left edge adds nothing except a very familiar look. */
.work__evidence {
    display: grid;
    gap: .375rem;
    align-content: start;
    padding-top: .875rem;
    border-top: var(--rule);
}

.work__evidence dt {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.work__evidence dd {
    font-size: var(--step-small);
    line-height: 1.5;
    color: var(--ks-text);
}

@media (min-width: 54rem) {
    .work__item {
        grid-template-columns: minmax(0, 1.9fr) minmax(14rem, 1fr);
        gap: clamp(1.5rem, 3vw, 3rem);
        align-items: start;
    }

    .work__aside {
        display: grid;
        gap: 1rem;
        align-content: start;
    }
}

/* ================================================================ 11. slab */

/* The brand mark at page scale: an ink field carrying an accent chip. Used
   exactly once per page, for the closing action, and never as a section
   theme flip. Its own tokens resolve to a raised surface in dark mode. */
.slab {
    position: relative;
    border-radius: var(--ks-radius-xl);
    background: var(--ks-slab);
    color: var(--ks-on-slab);
    padding: clamp(2rem, 5vw, 4rem);
    overflow: hidden;
    isolation: isolate;
}

/* THE ONE GRID FIELD ON THE SITE, and it is here on purpose.
   A tiled hairline field is a known generated-interface signature, so it is
   worth saying why this instance stays when the two others were deleted. It is
   used once per page rather than as a texture; it is masked to a single corner
   with a radial, so it falls away rather than covering the panel; and the panel
   it sits on IS the brand mark at page scale, an ink field with an accent chip,
   which is the one place on the site where a measured surface is the subject
   rather than the decoration. */
.slab::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(to right, var(--ks-slab-line) 1px, transparent 1px),
        linear-gradient(to bottom, var(--ks-slab-line) 1px, transparent 1px);
    background-size: 76px 76px;
    mask-image: radial-gradient(120% 100% at 88% 12%, rgba(0, 0, 0, .85), transparent 68%);
    z-index: -1;
}

.slab__grid {
    display: grid;
    gap: clamp(1.75rem, 3.5vw, 3rem);
    align-items: end;
}

.slab__title {
    font-size: var(--step-title);
    font-weight: 800;
    letter-spacing: -.032em;
    line-height: 1.04;
    max-width: 18ch;
    text-wrap: balance;
}

.slab__body {
    margin-top: 1.125rem;
    max-width: 46ch;
    color: var(--ks-on-slab-muted);
    line-height: 1.6;
}

.slab__actions {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
    margin-top: 2rem;
}

.slab__list {
    display: grid;
    gap: 0;
    align-content: end;
}

.slab__pair {
    display: grid;
    gap: .25rem;
    padding: .875rem 0;
    border-top: 1px solid var(--ks-slab-line);
}

.slab__pair:last-child {
    border-bottom: 1px solid var(--ks-slab-line);
}

.slab__list dd {
    margin: 0;
}

.slab__list-label {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ks-on-slab-muted);
}

.slab__list-value {
    font-size: var(--step-small);
    font-weight: 600;
    color: var(--ks-on-slab);
    line-height: 1.45;
}

@media (min-width: 58rem) {
    .slab__grid {
        grid-template-columns: minmax(0, 1.5fr) minmax(15rem, .8fr);
    }
}

/* ============================================================== 12. footer */

.footer {
    border-top: var(--rule);
    background: var(--ks-raised);
    padding-block: clamp(3rem, 6vw, 4.5rem) 2rem;
}

.footer__top {
    display: grid;
    gap: clamp(2rem, 4vw, 3.5rem);
    padding-bottom: clamp(2rem, 4vw, 3rem);
    border-bottom: var(--rule);
}

.footer__brand {
    display: grid;
    gap: .875rem;
    align-content: start;
    max-width: 30ch;
}

.footer__blurb {
    font-size: var(--step-small);
    color: var(--ks-text-muted);
    line-height: 1.6;
}

.footer__groups {
    display: grid;
    gap: 2rem 1.5rem;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
}

.footer__title {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
    margin-bottom: .875rem;
}

.footer__links {
    display: grid;
    gap: .0625rem;
}

.footer__link {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    padding: .3125rem 0;
    font-size: var(--step-small);
    color: var(--ks-text-muted);
    transition: color .16s var(--ease);
}

.footer__link:hover {
    color: var(--ks-accent-ink);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.footer__ext {
    color: var(--ks-text-subtle);
    font-size: .625rem;
}

.footer__bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding-top: 1.5rem;
    font-size: var(--step-meta);
    color: var(--ks-text-subtle);
}

.footer__legal {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.footer__legal a:hover {
    color: var(--ks-text);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.footer__by strong {
    color: var(--ks-text-muted);
    font-weight: 600;
}

@media (min-width: 56rem) {
    .footer__top {
        grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr);
    }
}

/* ========================================================= 13. page headers */

.masthead {
    padding-top: clamp(3rem, 6vw, 5rem);
    padding-bottom: clamp(2.5rem, 4.5vw, 3.5rem);
    border-bottom: var(--rule);
}

.masthead__grid {
    display: grid;
    gap: clamp(1.5rem, 3.5vw, 3rem);
    align-items: end;
}

.masthead__label {
    display: flex;
    align-items: center;
    gap: .625rem;
    margin-bottom: 1.125rem;
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    color: var(--ks-text-subtle);
}

.masthead__label::before {
    content: '';
    width: 1.75rem;
    height: 1px;
    background: var(--ks-accent-ink);
    flex: none;
}

.masthead__title {
    font-size: clamp(2.25rem, 5vw, 3.75rem);
    font-weight: 800;
    letter-spacing: -.038em;
    line-height: 1.02;
    max-width: 20ch;
    text-wrap: balance;
}

.masthead__lead {
    margin-top: 1.25rem;
    max-width: 56ch;
    font-size: var(--step-lead);
    color: var(--ks-text-muted);
    line-height: 1.56;
    text-wrap: pretty;
}

.masthead__aside {
    display: grid;
    gap: .875rem;
    align-content: end;
}

/* Top hairline, like every other labelled fact on the site. The site has ONE
   device for "a term and its value": a rule above it and a mono label. It is
   not a coloured tab on the left edge here and a hairline there. */
.masthead__stat {
    display: grid;
    gap: .1875rem;
    padding: .75rem 0;
    border-top: var(--rule);
}

.masthead__stat dt {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.masthead__stat dd {
    font-size: var(--step-small);
    font-weight: 600;
    line-height: 1.45;
}

@media (min-width: 58rem) {
    .masthead__grid {
        grid-template-columns: minmax(0, 1.7fr) minmax(14rem, .8fr);
    }
}

/* On-page anchor navigation for the longer pages. Sticky under the header,
   one line, scrollable rather than wrapped. */
.subnav {
    position: sticky;
    top: 4.25rem;
    z-index: 60;
    border-bottom: var(--rule);
    background: color-mix(in oklab, var(--ks-canvas) 92%, transparent);
    backdrop-filter: saturate(140%) blur(12px);
}

.subnav__list {
    display: flex;
    gap: .25rem;
    overflow-x: auto;
    scrollbar-width: none;
}

.subnav__list::-webkit-scrollbar {
    display: none;
}

.subnav__link {
    display: inline-block;
    padding: .8125rem .625rem;
    font-size: var(--step-meta);
    font-weight: 600;
    color: var(--ks-text-muted);
    white-space: nowrap;
    box-shadow: inset 0 -2px 0 transparent;
    transition: color .16s var(--ease), box-shadow .16s var(--ease);
}

.subnav__link:hover {
    color: var(--ks-text);
}

.subnav__link[aria-current='true'] {
    color: var(--ks-text);
    font-weight: 700;
    box-shadow: inset 0 -2px 0 var(--ks-accent-ink);
}

/* ================================================================ 14. table */

/* The rate card. A real table, because this is a data set: three axes and a
   row per operation. Scrolls in its own box on a narrow screen; the operation
   key is the row header so a horizontal scroll never orphans a rate. */
.table-wrap {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    border: var(--rule);
    border-radius: var(--ks-radius);
    background: var(--ks-surface);
    scrollbar-width: thin;
}

table.rates {
    width: 100%;
    min-width: 46rem;
    border-collapse: collapse;
    font-size: var(--step-small);
}

/* The legend sits ABOVE the scrolling box, not inside it. A caption inside an
   overflow-x container scrolls sideways with the table, so on a phone half the
   sentence is off screen with no way to reach it except dragging the table. */
.table-note {
    margin-bottom: .75rem;
    max-width: 68ch;
    font-size: var(--step-meta);
    color: var(--ks-text-muted);
    line-height: 1.55;
}

table.rates th,
table.rates td {
    padding: .75rem 1.125rem;
    text-align: left;
    vertical-align: top;
}

table.rates thead th {
    position: sticky;
    top: 0;
    background: var(--ks-surface);
    border-bottom: 1px solid var(--ks-border-strong);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    font-weight: 500;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
    white-space: nowrap;
}

table.rates tbody tr + tr th,
table.rates tbody tr + tr td {
    border-top: var(--rule);
}

/* Operation keys never break. They are identifiers, and a key split across two
   lines cannot be copied by eye. The table scrolls instead. */
table.rates tbody th {
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    font-weight: 400;
    color: var(--ks-text);
    white-space: nowrap;
}

.rates__service {
    font-weight: 600;
    white-space: nowrap;
}

.rates__unit {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-muted);
    white-space: nowrap;
}

.rates__rate {
    font-family: var(--ks-font-mono);
    font-weight: 500;
    color: var(--ks-text);
    white-space: nowrap;
}

.rates__note {
    color: var(--ks-text-muted);
    font-size: var(--step-meta);
    line-height: 1.5;
}

/* =============================================================== 15. steps */

/* The four things that happen between asking and going live. Numbered because
   the order is the content, and named by the action rather than by "Step 1". */
.steps {
    display: grid;
    gap: 0;
    counter-reset: step;
}

.steps__item {
    display: grid;
    gap: .5rem 1.5rem;
    padding: clamp(1.25rem, 2.5vw, 1.75rem) 0;
    border-top: var(--rule);
    counter-increment: step;
}

.steps__item:last-child {
    border-bottom: var(--rule);
}

.steps__marker {
    display: flex;
    align-items: baseline;
    gap: .625rem;
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    color: var(--ks-accent-ink);
}

.steps__marker::before {
    content: counter(step, decimal-leading-zero);
    color: var(--ks-text-subtle);
}

.steps__title {
    font-size: var(--step-subheading);
    font-weight: 800;
    letter-spacing: -.022em;
    line-height: 1.25;
}

.steps__body {
    margin-top: .5rem;
    max-width: 58ch;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

@media (min-width: 48rem) {
    .steps__item {
        grid-template-columns: 12rem minmax(0, 1fr);
    }
}

/* ============================================================= 16. questions */

.qa {
    display: grid;
    gap: 0;
}

.qa__item {
    border-top: var(--rule);
    padding: clamp(1.125rem, 2.2vw, 1.5rem) 0;
}

.qa__item:last-child {
    border-bottom: var(--rule);
}

.qa__q {
    font-size: var(--step-subheading);
    font-weight: 700;
    letter-spacing: -.018em;
    line-height: 1.3;
}

.qa__a {
    margin-top: .5rem;
    max-width: 62ch;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

/* ================================================================= 17. form */

.form {
    display: grid;
    gap: 1.25rem;
    padding: clamp(1.5rem, 3vw, 2.25rem);
    border: var(--rule);
    border-radius: var(--ks-radius-lg);
    background: var(--ks-surface);
    box-shadow: var(--ks-shadow-1);
}

.field {
    display: grid;
    gap: .5rem;
}

.field__label {
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.008em;
}

.field__req {
    color: var(--ks-accent-ink);
    font-weight: 700;
}

.field__hint {
    font-size: var(--step-meta);
    color: var(--ks-text-muted);
    line-height: 1.5;
}

.field__control {
    width: 100%;
    min-height: 2.875rem;
    padding: .6875rem .875rem;
    border: 1px solid var(--ks-border-strong);
    border-radius: var(--ks-radius-md);
    background: var(--ks-surface);
    color: var(--ks-text);
    font-size: var(--step-body);
    line-height: 1.45;
    transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}

textarea.field__control {
    min-height: 7.5rem;
    resize: vertical;
}

/* #8b91a4 on white is 3.3:1, which is enough for a placeholder that repeats
   the label but not enough for one that carries meaning. Nothing on this form
   uses a placeholder as a label. */
.field__control::placeholder {
    color: var(--ks-text-subtle);
}

.field__control:hover {
    border-color: var(--ks-text-subtle);
}

.field__control:focus {
    outline: none;
    border-color: var(--ks-accent-ink);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--ks-accent) 20%, transparent);
}

.field__control[aria-invalid='true'] {
    border-color: var(--ks-danger);
}

.field__error {
    display: flex;
    align-items: baseline;
    gap: .375rem;
    font-size: var(--step-meta);
    font-weight: 600;
    color: var(--ks-danger);
    line-height: 1.45;
}

.field__error::before {
    content: '!';
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: .9375rem;
    height: .9375rem;
    border-radius: 3px;
    background: var(--ks-danger);
    color: #fff;
    font-size: .625rem;
    font-weight: 800;
    flex: none;
    transform: translateY(.125rem);
}

/* A fieldset carries a user-agent border and notches its legend into it. The
   grouping is wanted; the box is not. */
.choices-wrap {
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
}

.choices {
    display: grid;
    gap: .5rem;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    min-width: 0;
}

.choices__legend {
    padding: 0;
    margin-bottom: .625rem;
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.008em;
}

.choice {
    display: flex;
    align-items: center;
    gap: .625rem;
    padding: .625rem .75rem;
    border: 1px solid var(--ks-border);
    border-radius: var(--ks-radius-md);
    background: var(--ks-raised);
    font-size: var(--step-small);
    cursor: pointer;
    transition: border-color .16s var(--ease), background-color .16s var(--ease);
}

.choice:hover {
    border-color: var(--ks-border-strong);
}

.choice input {
    accent-color: var(--ks-accent-ink);
    width: 1rem;
    height: 1rem;
    flex: none;
    margin: 0;
}

.choice:has(input:checked) {
    border-color: color-mix(in oklab, var(--ks-accent) 55%, transparent);
    background: color-mix(in oklab, var(--ks-accent) 8%, transparent);
}

.choice:has(input:focus-visible) {
    outline: 2px solid var(--ks-accent-ink);
    outline-offset: 2px;
}

.form__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    padding-top: .5rem;
    border-top: var(--rule);
}

.form__small {
    font-size: var(--step-meta);
    color: var(--ks-text-muted);
    line-height: 1.5;
    max-width: 40ch;
}

/* Outcome states. Both are announced, both are legible without colour, and
   both say what actually happened rather than thanking somebody for nothing. */
/* A full border in the tone rather than a stripe down the left edge. The tone
   is carried by the border, the tint and the title, which is three cues; the
   error variant adds a fourth in the badge on each field. */
.notice {
    display: grid;
    gap: .375rem;
    padding: 1rem 1.125rem;
    border: 1px solid color-mix(in oklab, var(--tone) 42%, transparent);
    border-radius: var(--ks-radius-md);
    background: color-mix(in oklab, var(--tone) 7%, var(--ks-surface));
}

.notice--ok {
    --tone: var(--ks-success);
}

.notice--bad {
    --tone: var(--ks-danger);
}

.notice__title {
    font-size: var(--step-small);
    font-weight: 700;
    color: var(--tone);
}

.notice__body {
    font-size: var(--step-small);
    color: var(--ks-text-muted);
    line-height: 1.55;
}

/* =============================================================== 18. layout */

.split {
    display: grid;
    gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
}

@media (min-width: 58rem) {
    .split {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .split--rail {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1.45fr);
    }

    .split--sticky > :first-child {
        position: sticky;
        top: 8.5rem;
    }
}

.stack {
    display: grid;
    gap: clamp(1rem, 2vw, 1.5rem);
}

.grid-2 {
    display: grid;
    gap: clamp(1rem, 2vw, 1.5rem);
}

@media (min-width: 46rem) {
    .grid-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.spread {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.actions {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
}

/* Definition rows used for facts that are genuinely paired. */
.facts {
    display: grid;
    gap: 0;
}

.facts__row {
    display: grid;
    gap: .25rem .5rem;
    padding: .8125rem 0;
    border-bottom: var(--rule);
}

.facts__row:first-child {
    border-top: var(--rule);
}

.facts dd,
.masthead__aside dd,
.work__evidence dd,
.ledger__composes dd {
    margin: 0;
}

.facts__term {
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ks-text-subtle);
}

.facts__value {
    font-size: var(--step-small);
    line-height: 1.55;
    color: var(--ks-text);
}

/* The stacked variant keeps the term above its value at every width, for rows
   whose term is a sentence rather than a label. */
.facts__row--stacked .facts__term {
    font-family: var(--ks-font);
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.008em;
    text-transform: none;
    color: var(--ks-text);
}

.facts__row--stacked .facts__value {
    color: var(--ks-text-muted);
}

@media (min-width: 34rem) {
    .facts__row:not(.facts__row--stacked) {
        grid-template-columns: 11rem minmax(0, 1fr);
        align-items: baseline;
    }
}

/* ============================================================ 19. not found */

.lost {
    display: grid;
    gap: 1.5rem;
    align-content: center;
    min-height: 52vh;
    padding-block: clamp(3rem, 8vw, 6rem);
    max-width: 44ch;
}

.lost__code {
    font-family: var(--ks-font-mono);
    font-size: var(--step-meta);
    color: var(--ks-accent-ink);
    letter-spacing: .08em;
}

.lost__title {
    font-size: var(--step-title);
    font-weight: 800;
    letter-spacing: -.032em;
    line-height: 1.05;
}

/* ======================================================= 20. legal documents

   THE LEDGER ROW, APPLIED TO A CONTRACT. A clause number is machine truth: it
   is how a clause is cited, in these documents and in the email where somebody
   argues about one. So it sits in the left rail in mono, as real text and not
   as a CSS counter. A counter looks identical and disappears from a copy, a
   print and a quotation, which are the three things anybody actually does with
   a legal page.

   PROSE IS ONE STEP LARGER HERE than in the rest of the marketing layer, and
   capped at 68 characters. Nothing else on this site asks to be read for
   fifteen minutes, and a contract set at 14px across the full shell is a
   contract nobody finishes.

   VERMILION STILL MEANS THREE THINGS. On these pages the third one - the
   moving item - is the unresolved fact: the value that is not settled yet. It
   is the only thing on a legal page that is going to change. */

.doc {
    border-top: var(--rule);
}

.doc__section {
    padding-block: clamp(1.75rem, 3.4vw, 2.75rem);
    border-bottom: var(--rule);
}

.doc__heading {
    display: grid;
    gap: .25rem .5rem;
    font-size: var(--step-heading);
    font-weight: 800;
    letter-spacing: -.026em;
    line-height: 1.2;
}

.doc__number {
    font-size: var(--step-meta);
    font-weight: 500;
    letter-spacing: .02em;
    color: var(--ks-accent-ink);
}

.doc__clause {
    display: grid;
    gap: .25rem .5rem;
    margin-top: clamp(1.125rem, 2vw, 1.5rem);
}

/* The rail is wide enough for "Annex A.10" and no wider. */
@media (min-width: 52rem) {
    .doc__heading,
    .doc__clause {
        grid-template-columns: 5.5rem minmax(0, 1fr);
        align-items: baseline;
    }
}

.doc__ref {
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

/* The clause number is its own anchor, so a clause can be sent to somebody as
   a link rather than as a page and an instruction to scroll. */
.doc__ref-link {
    color: inherit;
    text-decoration: none;
}

.doc__ref-link:hover {
    color: var(--ks-accent-ink);
}

.doc__body {
    min-width: 0;
}

.doc__text {
    max-width: 68ch;
    color: var(--ks-text-muted);
    line-height: 1.68;
}

.doc__list {
    counter-reset: sub;
    display: grid;
    gap: .5rem;
    margin-top: .875rem;
    max-width: 68ch;
}

.doc__list li {
    counter-increment: sub;
    position: relative;
    padding-left: 2.25rem;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

/* A generated marker is safe here in a way a generated clause number is not:
   the element is an <ol>, so the position is announced and carried by the
   element itself, and no document cites a sub-paragraph by its letter. */
.doc__list li::before {
    content: '(' counter(sub, lower-alpha) ')';
    position: absolute;
    left: 0;
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

.doc__defs {
    display: grid;
    margin-top: .875rem;
    max-width: 68ch;
}

.doc__def {
    display: grid;
    gap: .25rem .75rem;
    padding: .6875rem 0;
    border-bottom: var(--rule);
}

.doc__def:first-child {
    border-top: var(--rule);
}

.doc__def dt {
    font-weight: 700;
    font-size: var(--step-small);
    letter-spacing: -.008em;
    color: var(--ks-text);
}

.doc__def dd {
    margin: 0;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

@media (min-width: 40rem) {
    .doc__def {
        grid-template-columns: 9rem minmax(0, 1fr);
        align-items: baseline;
    }
}

.doc__summary {
    display: grid;
}

.doc__summary li {
    padding: .8125rem 0;
    border-bottom: var(--rule);
    font-size: var(--step-small);
    line-height: 1.55;
}

.doc__summary li:first-child {
    border-top: var(--rule);
}

.doc__count {
    font-size: var(--step-micro);
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ks-accent-ink);
}

/* --- the index. Four ruled rows, read in order, numbered because the order
   is the argument: what you agree to, what we do with data about you, what we
   do with data you send, what you may not do with it. */
.docs {
    border-top: var(--rule);
    margin-top: clamp(2rem, 4vw, 3rem);
}

.docs__row {
    display: grid;
    gap: .875rem 1.5rem;
    padding: clamp(1.375rem, 2.6vw, 1.875rem) 0;
    border-bottom: var(--rule);
    transition: background-color .18s var(--ease);
}

.docs__row:hover {
    background: color-mix(in oklab, var(--ks-accent) 3.5%, transparent);
}

@media (min-width: 52rem) {
    .docs__row {
        grid-template-columns: 8rem minmax(0, 1fr);
    }
}

.docs__rail {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    align-content: start;
    gap: .5rem .75rem;
}

.docs__index {
    font-size: var(--step-meta);
    color: var(--ks-accent-ink);
}

.docs__version {
    font-size: var(--step-micro);
    color: var(--ks-text-subtle);
}

.docs__main {
    min-width: 0;
}

.docs__name {
    font-size: var(--step-heading);
    font-weight: 800;
    letter-spacing: -.026em;
    line-height: 1.2;
}

.docs__link {
    color: inherit;
    text-decoration: none;
}

.docs__link:hover {
    color: var(--ks-accent-ink);
    text-decoration: underline;
    text-underline-offset: .2em;
}

.docs__lead {
    margin-top: .625rem;
    max-width: 62ch;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.62;
}

.docs__points {
    display: grid;
    gap: .375rem;
    margin-top: .875rem;
    max-width: 62ch;
}

.docs__points li {
    position: relative;
    padding-left: 1rem;
    font-size: var(--step-micro);
    line-height: 1.5;
    color: var(--ks-text-subtle);
}

.docs__points li::before {
    content: '';
    position: absolute;
    left: 0;
    top: .5em;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--ks-accent);
}

/* --- the draft state.

   It renders while any fact in any of the four documents is still a marker,
   and it is loud on purpose. The alternative is a legal page that reads as
   finished while containing an invented company number, which is a worse
   failure than an unfinished page that says so. */
.draft {
    padding-block: clamp(1rem, 2vw, 1.375rem);
    background: color-mix(in oklab, var(--ks-accent) 10%, var(--ks-canvas));
    border-bottom: 1px solid color-mix(in oklab, var(--ks-accent) 32%, transparent);
}

.draft__inner {
    display: grid;
    gap: .375rem 1.25rem;
    align-items: baseline;
}

@media (min-width: 40rem) {
    .draft__inner {
        grid-template-columns: 5.5rem minmax(0, 1fr);
    }
}

.draft__label {
    font-size: var(--step-micro);
    font-weight: 500;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ks-accent-ink);
}

.draft__title {
    font-size: var(--step-small);
    font-weight: 700;
    letter-spacing: -.008em;
}

.draft__body {
    margin-top: .25rem;
    max-width: 72ch;
    color: var(--ks-text-muted);
    font-size: var(--step-small);
    line-height: 1.6;
}

/* An unresolved fact, in the text where it belongs. <mark> is the right
   element: this is text marked for reference in another context, which is
   exactly what it is. */
.todo {
    padding: .0625rem .3125rem;
    border-radius: var(--ks-radius-sm);
    background: color-mix(in oklab, var(--ks-accent) 20%, transparent);
    color: var(--ks-text);
    font-family: var(--ks-font-mono);
    font-size: .92em;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

.todos {
    display: grid;
    margin-top: .75rem;
}

.todos__item {
    padding: .5rem 0;
    border-bottom: var(--rule);
    font-family: var(--ks-font-mono);
    font-size: var(--step-micro);
    line-height: 1.5;
    color: var(--ks-text-muted);
}

.todos__item:first-child {
    border-top: var(--rule);
}

/* ============================================================ 21. spacing

   Four spacing utilities, and no more. A section is built from components that
   own their own rhythm; these exist for the joins between them, where a
   one-off margin would otherwise become an inline style on the markup. They
   are named for the size rather than for a purpose on purpose: a class called
   .after-block would be a lie the first time it was reused. */

.gap-sm {
    margin-top: 1.5rem;
}

.gap-md {
    margin-top: 1.75rem;
}

.gap-lg {
    margin-top: 2.5rem;
}

.gap-none {
    margin-top: 0;
}

/* Sits inside a .chain, labelling the row of tags that follows it. */
.chain__label {
    margin-right: .25rem;
}

/* Used where a mechanism footnote is the only thing in its cell and therefore
   has nothing above it to be separated from. */
.controls__mech--bare {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

/* The header's scroll sentinel: a zero-height marker an IntersectionObserver
   watches, so the scrolled state costs no scroll handler. */
.sentinel {
    position: absolute;
    top: 0;
    width: 1px;
    height: 1px;
}

.actions--middle {
    align-items: center;
}

.notice--stacked {
    margin-bottom: 1.25rem;
}

/* ============================================================ 21. hero kicker

   The one small label above a heading on this site, and it is here because the
   brief asked for an orienting product descriptor. Everywhere else the heading
   carries itself. It is set as a rule plus a word rather than as wide-tracked
   caps floating in space, so it reads as part of the hero's structure. */

.hero__kicker {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin-bottom: 1.375rem;
    font-size: var(--step-small);
    font-weight: 600;
    letter-spacing: -.005em;
    color: var(--ks-text-muted);
}

.hero__kicker::before {
    content: '';
    width: 2rem;
    height: 2px;
    border-radius: 1px;
    background: var(--ks-accent);
    flex: none;
}

/* ============================================================== 22. roster

   The condensed catalogue. Eight services, the same three lines each, in a
   ruled index rather than eight identical cards. Two columns from 46rem, four
   rows of two, which is exactly eight cells for exactly eight services. */

.roster {
    display: grid;
    gap: 0;
    border-top: var(--rule);
}

.roster__item {
    display: grid;
    gap: .25rem;
    align-content: start;
    padding: .9375rem 0;
    border-bottom: var(--rule);
}

.roster__name {
    font-size: var(--step-subheading);
    font-weight: 800;
    letter-spacing: -.022em;
    line-height: 1.25;
}

.roster__duty {
    font-size: var(--step-small);
    line-height: 1.5;
    color: var(--ks-text-muted);
    max-width: 40ch;
}

.roster__pairs {
    margin-top: .1875rem;
    font-size: var(--step-meta);
    line-height: 1.45;
    color: var(--ks-accent-ink);
}

@media (min-width: 46rem) {
    .roster {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: clamp(2rem, 5vw, 4.5rem);
    }

}

/* --- the mark in the lockup.

   Two optical corrections, both measured rather than guessed.

   SIZE. The mark's cap-to-baseline is 26 of its 32 units. At the 24px default
   that is 19.5px against a wordmark whose cap height is 12.4px, which made the
   mark shout over the word it belongs to. 20px puts it at 16.3px, about 1.3x
   the cap height, which is where a mark sits beside a word without competing.

   VERTICAL. align-items:center centres the mark's box on the line box. The
   wordmark has no descenders, so its visual mass sits above that centre and the
   mark read as hanging low. 1.5px up puts the two optical centres together. */
.brand .mark {
    --mark-size: 20px;
    transform: translateY(-1.5px);
}

/* --- honeypot. Off-screen rather than display:none, because a bot that reads
   CSS skips display:none and fills in what it can see in the DOM. Nothing
   here is reachable: aria-hidden on the wrapper, tabindex -1 on the input. */
.honeypot {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* The list of what went wrong, each item a link to the field it belongs to. */
.notice__list {
    display: grid;
    gap: .25rem;
    margin-top: .5rem;
    padding-left: 1.125rem;
    list-style: disc;
    font-size: var(--step-small);
    color: var(--ks-text-muted);
}

.notice__list a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.notice__list a:hover {
    color: var(--ks-danger);
}

.notice:focus-visible {
    outline: 2px solid var(--ks-accent-ink);
    outline-offset: 3px;
}

/* A submit button mid-flight. The label swaps rather than a spinner appearing
   beside it, so the button does not change width and the page does not shift. */
.btn[aria-disabled='true'] {
    cursor: progress;
    opacity: .75;
    pointer-events: none;
}

/* --- touch targets for standalone links.

   `.link` and the footer's legal links are 21 to 22px tall, which is the
   height of their own text. They are not links inside a sentence, so WCAG 2.2
   asks for at least 24px and a thumb wants considerably more.

   The hit area is extended with a pseudo-element rather than with padding, so
   the target reaches 44px without moving anything on the page. The element
   itself keeps its exact position in the vertical rhythm. */
.link,
.footer__legal a {
    position: relative;
}

.link::after,
.footer__legal a::after {
    content: '';
    position: absolute;
    inset: -11px -4px;
}

/* Two adjacent footer links must not have overlapping hit areas, so the row
   gap is at least the horizontal bleed above, doubled. */
.footer__legal {
    gap: 1rem 1.25rem;
}
