/*
 * ═══════════════════════════════════════════════════════════
 *  1confirmed — the public landing page
 * ═══════════════════════════════════════════════════════════
 *
 * Depends on tokens.css and brand.css, linked before it. Every colour, space
 * and duration below is a token; there is no hex in this file.
 *
 * ## What this borrows, and what it does not
 *
 * What is borrowed is the *composition*: the argument hard left, the product
 * hard right, an email pill as the one filled control, and a faded marquee
 * underneath. Also one typographic ratio — their display line is 64px at a 400
 * weight with 1.0 leading, which is the detail that makes it read as *large*
 * rather than as *loud*. That ratio is kept here in Space Grotesk.
 *
 * What is deliberately not borrowed is their palette, and that includes their
 * darkness. This product is a light interface: paper, ink, one green accent.
 * An earlier pass painted the hero in the ink token to get their near-black
 * ground, and it was wrong for a reason worth writing down — ink is the *text*
 * colour, and a palette that names no dark surface is a palette telling you it
 * does not have one. The page is the colours the product is, so a visitor who
 * signs up is not handed something that looks like a different piece of
 * software.
 *
 * ## Depth without darkness
 *
 * A light page still needs a foreground and a background or it reads as a
 * document:
 *
 *   1. the ground     paper, with a barely-there wash toward paper-raised, so
 *                     the hero is a surface rather than a sheet of nothing
 *   2. the surfaces   the message and the email pill are paper-raised with a
 *                     hairline and a long soft shadow — they lift off the page
 *                     instead of being drawn on it
 *   3. the edge masks on the marquee, so names enter and leave rather than
 *                     being clipped at a hard border
 *
 * ## Motion
 *
 * Two entrances, one loop, one argument.
 *
 * The hero entrance is a staggered rise driven by `--i` on each element — no
 * observer, because it is above the fold. The service linework is different:
 * its routes carry signals and its destinations pulse while each board is in
 * view. The loop is the marquee. The argument is .ln-track, whose
 * four states tick through in order because that sequence *is* the product.
 *
 * All of it stops under prefers-reduced-motion, where the end state is what
 * you get immediately — never a blank page waiting for an animation that will
 * not run.
 */

/*
 * ── Edges ───────────────────────────────────────────────────────────────────
 *
 * This page is squared, not rounded.
 *
 * The reference's controls sit at four to eight pixels — a button is a
 * rectangle with the corners taken off, not a capsule — and that single choice
 * is most of why the page reads as a tool rather than as a consumer app. An
 * earlier pass made every control a 999px pill, which is the default shape of
 * a generated landing page and the opposite of what was asked for.
 *
 * Three values and nothing else, so a corner is never invented at the point of
 * use. The portal's own --radius-* tokens are deliberately not used here: the
 * application is rounder on purpose, and this page is the one place that
 * departs from it.
 */
/*
 * ── Easing ──────────────────────────────────────────────────────────────────
 *
 * tokens.css names three curves and they are the right three for controls:
 * --ease-out is an expo (0.16, 1, 0.3, 1), which lunges out of the gate and
 * stops hard. That is what a button wants and it is wrong for something
 * crossfading in place — a panel on an expo curve reads as arriving *from*
 * somewhere rather than settling *into* view.
 *
 * This is the gentler cousin, for transitions where nothing should look
 * thrown. Named here rather than inlined, so the next transition that needs it
 * borrows the same curve instead of inventing a fourth.
 */
:root {
    --ln-ease-soft: cubic-bezier(0.33, 0, 0.2, 1);
}

:root {
    --ln-edge-sm: 4px;
    --ln-edge: 6px;
    --ln-edge-lg: 10px;
}

/* ── Base ─────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

html, body {
    /* clip, not hidden: hidden breaks position: sticky further down the page */
    overflow-x: clip;
}

.ln-skip {
    position: fixed;
    inset-block-start: 8px;
    inset-inline-start: 8px;
    z-index: 9999;
    padding: 12px 18px;
    background: var(--color-ink);
    color: var(--color-paper-raised);
    transform: translateY(-160%);
}
.ln-skip:focus { transform: none; }

/*
 * Anchors glide rather than cut.
 *
 * One declaration covers every in-page link — the header's, the footer's, and
 * anything added later — which is the argument for doing it in CSS rather than
 * intercepting clicks in JavaScript. A handler would also have to reimplement
 * what the browser already does correctly: honouring the reduced-motion
 * setting, keeping the URL fragment, and leaving focus on the target so a
 * keyboard reader carries on from the right place.
 */
html {
    scroll-behavior: smooth;
}

/*
 * Smooth scrolling (resources/js/smooth-scroll.js) moves the page itself,
 * a little each frame. The browser's own smooth behaviour on top of it would
 * ease every one of those little moves again, so it stands down while the
 * script is driving; touch screens and anchor jumps without the script keep it.
 */
html.lenis.lenis-smooth { scroll-behavior: auto !important; }
html.lenis.lenis-stopped { overflow: clip; }


/*
 * Where a section stops.
 *
 * The header floats over the page, so without this a target scrolls until its
 * top edge meets the viewport's — and then the bar covers the heading the
 * reader just asked to see. The offset clears the settled bar's 60px plus its
 * 16px gap, with air left over so the heading arrives under the header rather
 * than jammed against it.
 */
:target,
#product,
#services,
#api {
    scroll-margin-top: clamp(5.5rem, 9vw, 7rem);
}

/*
 * Somebody who has asked for less motion means this too.
 *
 * A smooth scroll across four screens is one of the larger movements this page
 * can make, and it is the kind that causes trouble rather than the kind that
 * decorates. The jump is the accessible behaviour, not a degraded one.
 */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
}

html {
    /* The page's ground is the product's paper. The dark is the hero's, and it
       belongs to the hero — see .ln-hero. Tying the light-to-dark ramp to the
       viewport instead left the strip underneath it dark on a short screen and
       light on a tall one, which is a design that depends on the reader's
       window. */
    background: var(--color-paper);
    font-family: var(--font-sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/*
 * The dot grid.
 *
 * The texture that keeps a light page from reading as a blank document, and the
 * thing an archive of the reference does not show — their current page is white
 * over a faint grid of dots, and it is doing most of the work.
 *
 * One radial-gradient tiled at 24px. A dot per tile, at 1px, in the rule token
 * at a low mix: any more and it is a pattern you look at instead of a surface
 * you look through. `background-attachment: fixed` so the grid stays put while
 * the page moves over it, which is what makes it read as paper rather than as
 * wallpaper printed on the content.
 */
body {
    margin: 0;
    min-height: 100vh;
    color: var(--color-ink);
    font-size: 15px;
    line-height: 1.55;
    background-color: var(--color-paper);
    background-image: radial-gradient(
        circle at 1px 1px,
        color-mix(in oklab, var(--color-rule-track) 70%, transparent) 1px,
        transparent 0
    );
    background-size: 24px 24px;
    background-attachment: fixed;
}

h1, h2, p, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }

a { color: inherit; text-decoration: none; }

:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
    border-radius: 4px;
}

.ln-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

[x-cloak] { display: none !important; }

/*
 * ── The backdrop ──────────────────────────────────────────────────────────
 *
 * Fixed behind the page: the dot grid above is the still ground, and this is
 * what moves over it. Two layers — the canvas the charges run on, and the
 * marks a charge leaves where it lands.
 *
 * z-index 0 rather than -1. A negative child of <body> paints *below* body's
 * own background, which is where the dot grid lives, so the canvas would have
 * been drawn and then covered by the very grid it is lighting up. At 0 it
 * paints above the grid, and `main` is lifted to sit above it in turn.
 */
.ln-back {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
}

/* The other half of that: an unpositioned element paints *below* a positioned
   sibling whatever the source order, so the page's content has to say it is
   above the thing behind it. */
main {
    position: relative;
    z-index: var(--z-base);
}

.ln-back__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

/*
 * The identity watermark used to live here and has been removed.
 *
 * Worth a line so it is not re-added by somebody reading the backdrop and
 * thinking it looks bare: a scaled-up copy of the rail's engine was a second
 * thing moving behind the page, and two competing backgrounds is one more than
 * a background should be. The grid is the one carrying meaning. The identity is
 * still on the page, in the header, at the size it was drawn for.
 */

/* ── The marks ─────────────────────────────────────────────────────────── */

.ln-back__marks {
    position: absolute;
    inset: 0;
}

/*
 * What a charge leaves behind.
 *
 * Two forms from one list — the glyph alone, or the word alone. Snapped to a
 * grid node, so a mark sits on the lattice rather than floating next to it,
 * and drawn in the accent on the page's own paper so it reads as part of the
 * product rather than as a sticker.
 *
 * Removed by the script when the animation ends; `forwards` is what holds the
 * last frame in the gap before that happens.
 */
/*
 * Small on purpose, and smaller than it looks like it should be.
 *
 * The grid it sits on is a 24px lattice, and that is the scale a mark has to
 * belong to — anything that spans four tiles reads as UI that escaped from the
 * product rather than as something the grid produced. A word ends up about one
 * tile tall; an icon fits inside one.
 */
/*
 * The words are the wordmark's orange.
 *
 * --color-warm-ink is the exact token brand.css paints .cl-logo__numeral with,
 * so the "1" in the header and every word on the grid are the same colour by
 * reference rather than by a value copied between two files. Change the palette
 * in App Settings and both move together.
 *
 * It measures 4.72:1 on paper, which clears the 4.5:1 floor — worth stating
 * because it is the one place on this page where the accent is not the colour
 * and the substitution had to be checked rather than assumed.
 */
.ln-mark {
    position: absolute;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 2px 5px;
    border: 1px solid var(--color-warm-line);
    border-radius: var(--ln-edge-sm);
    background: color-mix(in oklab, var(--color-paper-raised) 86%, transparent);
    color: var(--color-warm-ink);
    font-size: 8.5px;
    font-weight: 600;
    letter-spacing: 0.03em;
    white-space: nowrap;
    opacity: 0;
    animation: ln-mark-bloom var(--ln-mark-life) var(--ease-out) forwards;
}

/*
 * The icons stay the engine's green.
 *
 * Which is the identity's own split: the numeral is warm, the C is accent, the
 * word is ink. The backdrop carries the same two colours for the same two
 * kinds of thing, so it reads as the mark taken apart rather than as two
 * unrelated colours on one surface.
 *
 * No border and no ground either — at 13px a box around the glyph is mostly
 * box.
 */
.ln-mark--icon {
    padding: 0;
    border-color: transparent;
    background: none;
    color: var(--color-accent);
}

.ln-mark svg { width: 9px; height: 9px; stroke-width: 2; }
.ln-mark--icon svg { width: 13px; height: 13px; stroke-width: 2; }

/* Every keyframe carries the -50% centring: a transform declared elsewhere is
   overwritten wholesale by an animated one, and the mark would jump to the
   node's corner on its first frame. */
/* Peaks at 0.5, not 1. A mark is something you catch out of the corner of your
   eye on the way to the headline; at full strength it was a label competing
   with the page it sits behind. */
@keyframes ln-mark-bloom {
    0%   { opacity: 0;   transform: translate(-50%, -46%) scale(0.86); }
    12%  { opacity: 0.5; transform: translate(-50%, -50%) scale(1); }
    76%  { opacity: 0.5; transform: translate(-50%, -50%) scale(1); }
    100% { opacity: 0;   transform: translate(-50%, -58%) scale(0.97); }
}

/*
 * Reduced motion takes the whole backdrop off.
 *
 * Not slowed, not faded — removed. Everything in it is decoration: the page
 * still has its dot grid from <body>, which is the part that was doing the
 * work. The canvas is also not created at all in that case; this is the rule
 * that covers a preference changed after load.
 */
@media (prefers-reduced-motion: reduce) {
    .ln-back__canvas,
    .ln-back__marks { display: none; }
}

/*
 * The notice strip.
 *
 * Ink, full width, above everything — and in normal flow, so it scrolls away.
 * It is news rather than navigation, and a permanent dark band across the top
 * of every screen costs every other line on the page some contrast.
 */
.ln-notice {
    position: relative;
    z-index: var(--z-raised);
    padding: 9px clamp(1rem, 4vw, 2.5rem);
    background: var(--color-ink);
    color: var(--color-paper);
    text-align: center;
}

.ln-notice__line { font-size: 13.5px; }

.ln-notice[style] {
    background: var(--cl-banner-bg, var(--color-ink));
    color: var(--cl-banner-ink, var(--color-paper));
}
.ln-notice__content { overflow-wrap: anywhere; }
.ln-notice__content :is(p, h1, h2, h3, h4, h5, h6, ul, ol, li) { display: inline; margin: 0; padding: 0; font: inherit; }
.ln-notice__content :is(p, li, h1, h2, h3, h4, h5, h6) + :is(p, li, h1, h2, h3, h4, h5, h6)::before { content: ' · '; }
.ln-notice__content br { display: none; }
.ln-notice__content img { display: inline; max-height: 1.5em; width: auto; vertical-align: middle; }
.ln-notice__content a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

.ln-notice__link {
    color: inherit;
    margin-inline-start: var(--space-2xs);
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    font-weight: 500;
}

.ln-notice__link:hover { text-decoration-thickness: 2px; }

/* ── The shared width ─────────────────────────────────────────────────── */

.ln-nav__bar,
.ln-strip {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2.5rem);
}

/* ── Buttons ──────────────────────────────────────────────────────────── */

/* Hallmark · component: header action group · genre: modern-minimal · theme: existing identity
 * states: default · hover · focus · active · disabled · loading · error · success
 * contrast: pass (40–41)
 */

.ln-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    min-height: 44px;
    padding: 13px 22px;
    border: 0;
    border-radius: var(--ln-edge);
    font-family: var(--font-sans);
    font-size: 14.5px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: transform var(--dur-short) var(--ease-out),
                background var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out);
}

.ln-btn--sm { padding: 10px 18px; font-size: 14px; }

/* The one filled control. Its colour is the product's accent, and the page
   spends it here and nowhere else. */
.ln-btn--accent {
    background: var(--color-accent);
    color: var(--color-accent-ink);
    box-shadow: 0 1px 0 color-mix(in oklab, var(--color-paper-raised) 22%, transparent) inset;
}

@media (hover: hover) and (pointer: fine) {
    .ln-btn--accent:hover,
    .ln-btn--accent.is-hover {
        background: var(--color-accent-hover);
        transform: translateY(-1px);
    }
}

.ln-btn--accent:active,
.ln-btn--accent.is-active { transform: translateY(1px); }

.ln-btn:focus-visible,
.ln-btn.is-focus {
    outline: 2px solid var(--color-ink);
    outline-offset: 3px;
}

.ln-nav__signin:focus-visible,
.ln-nav__signin.is-focus,
.ln-nav__link:focus-visible,
.ln-sheet__link:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
}

.ln-btn[aria-disabled='true'],
.ln-btn.is-disabled,
.ln-nav__signin[aria-disabled='true'],
.ln-nav__signin.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}

.ln-btn[aria-busy='true'],
.ln-btn.is-loading { cursor: progress; }

.ln-btn[aria-busy='true']::before,
.ln-btn.is-loading::before {
    content: '';
    width: 0.75rem;
    height: 0.75rem;
    flex: none;
    border: 2px solid currentColor;
    border-inline-end-color: transparent;
    border-radius: 50%;
    animation: ln-btn-spin 0.8s linear infinite;
}

@keyframes ln-btn-spin {
    to { transform: rotate(1turn); }
}

.ln-btn[data-state='error'],
.ln-btn.is-error {
    outline: 2px solid var(--color-halt);
    outline-offset: 2px;
}

.ln-btn[data-state='error']::before,
.ln-btn.is-error::before {
    content: '!';
    font-weight: 700;
}

.ln-btn[data-state='success'],
.ln-btn.is-success {
    box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-live) 44%, transparent);
}

.ln-btn[data-state='success']::before,
.ln-btn.is-success::before {
    content: '✓';
    font-weight: 700;
}

/*
 * The solid one.
 *
 * Ink, not accent: the reference's third control is black beside a coloured
 * one, and the pair works because they are different *weights* of commitment
 * rather than two shouts in two colours. This is the page's only other filled
 * control and it is the last thing on the right.
 */
.ln-btn--ink {
    background: var(--color-ink);
    color: var(--color-paper);
}

.ln-btn--ink:hover {
    background: var(--color-ink-soft);
    transform: translateY(-1px);
}

.ln-btn--ink:active { transform: none; }

/* ── The header ───────────────────────────────────────────────────────── */

/*
 * The header.
 *
 * Sticky, not fixed. Fixed took it out of flow and parked it on top of the
 * notice strip above it; sticky starts it where it belongs — under the notice —
 * and pins it to the top only once the notice has scrolled away. Which is the
 * reference's own behaviour, and it needs no offset anywhere else on the page.
 */
.ln-nav {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    /* The header stays transparent at every scroll position and always spans
       the page. The ground, the hairline and the shadow belong to the bar
       inside it, because that is the thing that floats. */
    background: transparent;
    /*
     * flow-root, and it is load-bearing — do not drop it back to block.
     *
     * (It also scopes the ::before below: .ln-nav carries z-index, so it forms
     * a stacking context, and a z-index: -1 child sits behind the bar while the
     * whole context still paints above main.)
     *
     * The floating bar's gap is a margin-top on .ln-nav__bar. With this header
     * as a plain block — no border, no padding — that margin collapses through
     * it and out the top rather than pushing the bar down inside it, and since
     * the header is pinned at top: 0 the escaped margin has nowhere to go. The
     * declared 16px computed as 16px and rendered as a 0px gap.
     *
     * flow-root makes the header a block formatting context, which is the one
     * thing that stops a child's margin collapsing with its parent's.
     */
    display: flow-root;
}

/*
 * The wash under the header, at rest.
 *
 * A band of paper starting at the header and fading out before the eyebrow —
 * so the bar sits on clean ground rather than directly on the dot grid, and
 * reads as a header instead of as four links floating on texture. This is the
 * page's *initial* state, which is the one a visitor judges it by.
 *
 * absolute, not fixed. Fixed would pin it to the viewport top, and at rest the
 * notice strip is up there — a paper wash over a dark ink band greys it out.
 * Anchored to .ln-nav instead, it begins exactly where the header begins,
 * wherever the notice has pushed that to.
 *
 * It fades out once the page moves, because by then the bar has lifted off and
 * carries its own ground, hairline and shadow. Two grounds under one bar is
 * one too many.
 *
 * z-index: -1 keeps it behind the bar while staying inside .ln-nav's stacking
 * context — which sits above both main and the backdrop, so the wash covers
 * the dot grid and the charges running on it, which is the entire point.
 */
.ln-nav::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    z-index: -1;
    /*
     * Fitted to the gap, not guessed at, and the gap moves with the viewport:
     * the hero's padding is much tighter on a phone. Measured at rest, header
     * top to eyebrow top —
     *
     *     390px wide   60 -> 172   =  112px
     *     1440px wide  39 -> 221   =  182px
     *
     * which is the line below. A flat 182px was tried first and hung 70px past
     * the eyebrow on mobile, washing out the top of the headline.
     */
    height: clamp(112px, 5.4rem + 6.6vw, 182px);
    pointer-events: none;
    background: linear-gradient(
        to bottom,
        var(--color-paper) 0%,
        var(--color-paper) 38%,
        color-mix(in oklab, var(--color-paper) 74%, transparent) 66%,
        transparent 100%
    );
    transition: opacity var(--dur-mid) var(--ease-out);
}

.ln-nav.is-settled::before { opacity: 0; }

/*
 * Settled, the bar lifts off the page.
 *
 * The header itself stays transparent and full width; what changes is the bar
 * inside it, which pulls in to 1040px, takes a ground, a hairline and a
 * shadow, and rounds to --ln-edge-lg — the same corner as the stage panel and
 * the email pill, so the floating bar is made of the page's own shapes rather
 * than being a capsule imported from somewhere else.
 *
 * Every property here is on .ln-nav__bar and every one of them is reversible,
 * which is the whole behaviour: scroll back to the top and the class comes off
 * and the bar settles back into the page. There is no second state to unwind
 * and nothing is measured in JavaScript — the transition runs both ways
 * because it is the same declaration in both directions.
 *
 * max-width and padding animate; that is a layout-driven transition rather
 * than a compositor one, on one element, at 260ms. It is the cost of the bar
 * physically narrowing, and there is no transform that does the same thing
 * without also squashing the type inside it.
 */
.ln-nav__bar {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    min-height: 72px;
    border: 1px solid transparent;
    border-radius: 0;
    transition: max-width var(--dur-mid) var(--ease-out),
                min-height var(--dur-mid) var(--ease-out),
                margin-top var(--dur-mid) var(--ease-out),
                padding var(--dur-mid) var(--ease-out),
                border-radius var(--dur-mid) var(--ease-out),
                border-color var(--dur-mid) var(--ease-out),
                background var(--dur-mid) var(--ease-out),
                box-shadow var(--dur-mid) var(--ease-out);
}

.ln-nav.is-settled .ln-nav__bar {
    /*
     * Inset on every side, not just the top.
     *
     * max-width alone floats it on a desktop and does nothing on a phone, where
     * the viewport is narrower than the cap: the bar ran the full 390px and its
     * corners were cut off against the screen edges. The calc is what holds the
     * side gap at any width; max-width still takes over once there is room for
     * it.
     */
    width: calc(100% - var(--space-md) * 2);
    max-width: 1040px;
    min-height: 60px;
    /* The gap above it is what makes it an object rather than a header that
       lost its corners. Content does scroll through that gap, because the
       sticky header behind the bar stays transparent — that is what floating
       over a page looks like, and closing the gap to avoid it just wedges the
       bar back against the edge. */
    margin-top: var(--space-md);
    padding-inline: var(--space-md);
    border-radius: var(--ln-edge-lg);
    border-color: var(--color-rule-soft);
    background: color-mix(in oklab, var(--color-paper-raised) 86%, transparent);
    box-shadow: 0 12px 34px -18px color-mix(in oklab, var(--color-ink) 40%, transparent);
    backdrop-filter: saturate(1.4) blur(10px);
}

/* The gap between the menu and the bar's new edge is smaller than the gap it
   had when the bar was the page's full width. */
.ln-nav.is-settled .ln-nav__menu { gap: var(--space-md); }

/*
 * A reader who has asked for less motion still needs to be able to find the
 * header — they get the ground and the corner, they just do not get the
 * quarter-second of the bar sliding into it.
 */
@media (prefers-reduced-motion: reduce) {
    .ln-nav__bar,
    .ln-nav::before { transition: none; }
}

.ln-nav__brand { flex: none; display: flex; align-items: center; }

/* The identity is drawn at rail scale by brand.css; the header wants it
   smaller, and the wordmark's own type sizes come down with it. */
.ln-nav__mark { --cl-logo-size: 34px; width: auto; height: 34px; font-size: 25px; }
.ln-nav__mark .cl-logo__engine { flex-basis: 34px; width: 34px; height: 34px; }
.ln-nav__mark .cl-logo__word { font-size: 19px; }

/* The mark keeps its own colours from brand.css — warm-ink numeral, ink word —
   because the page is paper and those are what they were drawn for. */

.ln-nav__menu {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    margin-inline: auto;
}

.ln-nav__link,
.ln-nav__signin {
    position: relative;
    padding: 6px 2px;
    color: var(--color-ink-soft);
    font-size: 14.5px;
    transition: color var(--dur-short) var(--ease-out);
}

.ln-nav__signin {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
}

.ln-nav__cta { min-height: 44px; }

@media (hover: hover) and (pointer: fine) {
    .ln-nav__link:hover,
    .ln-nav__signin:hover,
    .ln-nav__signin.is-hover { color: var(--color-ink); }
}

.ln-nav__signin:active,
.ln-nav__signin.is-active { transform: translateY(1px); }

/* A drawn underline on hover, which is the one flourish the bar gets. Scaled
   from the left rather than animating width, so it never reflows the label. */
.ln-nav__link::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1.5px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-short) var(--ease-out);
}

.ln-nav__link:hover::after { transform: scaleX(1); }

.ln-nav__end {
    flex: none;
    display: flex;
    align-items: center;
    gap: var(--space-lg);
}

.ln-nav__burger {
    display: none;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge);
    background: transparent;
    color: var(--color-ink);
    cursor: pointer;
}

/* Three bars from one element: the middle is the box, the outer two are its
   pseudo-elements. Fewer nodes than three spans, and it cannot get out of
   alignment. */
.ln-nav__burger-bars,
.ln-nav__burger-bars::before,
.ln-nav__burger-bars::after {
    display: block;
    width: 17px;
    height: 1.5px;
    margin-inline: auto;
    background: currentColor;
    border-radius: 2px;
}

.ln-nav__burger-bars { position: relative; }
.ln-nav__burger-bars::before,
.ln-nav__burger-bars::after { content: ''; position: absolute; left: 0; }
.ln-nav__burger-bars::before { top: -5.5px; }
.ln-nav__burger-bars::after { top: 5.5px; }

/* ── The mobile sheet ─────────────────────────────────────────────────── */

.ln-sheet {
    display: none;
    flex-direction: column;
    gap: var(--space-2xs);
    padding: var(--space-sm) clamp(1rem, 4vw, 2.5rem) var(--space-lg);
    background: var(--color-paper-raised);
    border-bottom: 1px solid var(--color-rule-soft);
}

.ln-sheet__link {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 12px 4px;
    color: var(--color-ink);
    font-size: 16px;
    border-bottom: 1px solid var(--color-rule-soft);
}

.ln-sheet__cta {
    min-height: 44px;
    margin-top: var(--space-sm);
}

/* ── The hero ─────────────────────────────────────────────────────────── */

/*
 * The hero.
 *
 * No grid: the argument is one block at the left with the top of the page to
 * itself, and the product is on a stage below it. An earlier pass put them side
 * by side, which halves the width the headline can use and makes the two
 * compete — the reference gives the line the whole measure and then hands over.
 *
 * No background of its own. The page's dot grid shows through, which is the
 * point of having one.
 */
.ln-hero {
    position: relative;
    z-index: var(--z-base);
    /* No header offset: the header is in flow above this now. */
    padding-top: clamp(3rem, 7vw, 6rem);
}

.ln-hero__say,
.ln-stage {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2.5rem);
}

/* Wide enough for the lede to be two comfortable lines, and no wider — the
   headline above it runs to the full measure on purpose. */
.ln-hero__say { max-width: min(1240px, 100%); }

/*
 * The eyebrow: a small-caps label, letterspaced, in the muted token.
 *
 * The reference's is a label followed by a live statistic in a mono chip. Ours
 * carries the label alone — see LandingViewModel::eyebrowFigure for why, and
 * for where a real figure would go. `.ln-eyebrow__figure` is styled for it, so
 * adding one is a one-line change in the view model.
 */
.ln-eyebrow {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-bottom: var(--space-lg);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.ln-eyebrow__figure {
    padding: 3px 8px;
    border-radius: 5px;
    background: var(--color-paper-sunken);
    color: var(--color-ink);
    font-family: ui-monospace, Menlo, monospace;
    font-size: 11.5px;
    letter-spacing: 0;
    text-transform: none;
}

/*
 * The headline.
 *
 * 400 weight at this size, not 700. That is the reference's own choice and the
 * reason its hero reads as considered rather than as shouting — at 64px the
 * letterforms carry the emphasis and weight on top of them is just ink. Leading
 * is 1.0 flat, so the two lines lock into a block.
 *
 * Roman, never italic: an italicised display line is the most reliable tell of
 * a generated page.
 */
.ln-display {
    font-family: var(--font-display);
    /* Larger than before, because it now has the whole measure rather than half
       of it. The reference sets 64px; this tops out a little above that because
       Space Grotesk runs narrower than their Lausanne at the same size. */
    font-size: clamp(2.5rem, 1.2rem + 4.4vw, 4.5rem);
    font-style: normal;
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--color-ink);
    /* A pasted product name can be one long unbroken word; the page must not
       scroll sideways because of it. */
    overflow-wrap: anywhere;
}


.ln-lede {
    max-width: 30rem;
    margin-top: var(--space-lg);
    color: var(--color-muted);
    font-size: 16.5px;
    line-height: 1.6;
}

/* ── The email pill ───────────────────────────────────────────────────── */

.ln-catch {
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    width: min(30rem, 100%);
    margin-top: var(--space-xl);
    padding: 5px;
    border-radius: var(--ln-edge-lg);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    box-shadow: 0 14px 34px -20px color-mix(in oklab, var(--color-ink) 26%, transparent);
}

.ln-catch__field {
    flex: 1;
    min-width: 0;
    padding: 11px 16px;
    border: 0;
    border-radius: var(--ln-edge);
    background: transparent;
    color: var(--color-ink);
    font-family: inherit;
    font-size: 15px;
}

.ln-catch__field::placeholder { color: var(--color-muted); }

/* Qualified by the pill so it outranks studio.css's site-wide ring, which
   scores the same (0,2,0) as the bare field selector and loads later. Losing
   that tie put a second, square ring inside the pill's own. */
.ln-catch .ln-catch__field:focus { outline: 0; }

/* The focus ring goes on the pill, not the bare field: the pill is what reads
   as the control. */
.ln-catch:focus-within {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/*
 * The pill's replacement, for a reader who is already signed in.
 *
 * The capsule is a field and a button in one container; with nothing to type
 * there is only the button, so the container goes rather than staying on as an
 * empty frame around it. It keeps the pill's own top margin so the hero's
 * rhythm is identical in both states — the eye should not be able to tell that
 * a different element is in the slot.
 */
.ln-catch-out {
    margin-top: var(--space-xl);
}

.ln-catch__note {
    margin-top: var(--space-sm);
    /* muted, not muted-soft: on paper the softer of the two measures 2.6:1,
       which is below the 4.5:1 this size needs. */
    color: var(--color-muted);
    font-size: 12.5px;
}

.ln-quiet {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: var(--space-lg);
    color: var(--color-accent);
    font-size: 14.5px;
    font-weight: 600;
}

.ln-quiet svg { width: 14px; height: 14px; transition: transform var(--dur-short) var(--ease-out); }
.ln-quiet:hover svg { transform: translate(2px, -2px); }

/* ── The message ──────────────────────────────────────────────────────── */

/*
 * The stage.
 *
 * A bordered panel across the measure, holding the product — the reference's own
 * device for it. Paper-raised inside its own border so the frame reads as a
 * window onto the software rather than as a card containing a picture, and the
 * dot grid continues underneath it at a lower opacity so the panel sits *on*
 * the page rather than replacing a rectangle of it.
 */
/* The gap between the argument and the stage under it.
   Tightened from clamp(2.5rem, 6vw, 4.5rem): at the old value the film had
   drifted far enough from the sentence above it to read as a separate section
   rather than as the evidence for it. */
.ln-stage { margin-top: clamp(1.25rem, 3vw, 2.25rem); }

/* Hallmark · component: journey switcher · genre: modern-minimal · theme: 1confirmed
 * states: default · hover · focus · active · disabled · loading · error · success
 * contrast: pass (accent and muted copy remain on paper surfaces)
 */
/*
 * Small bookmark labels attached directly to the shared film.
 *
 * There is deliberately no containing bar: a framed rail made the control
 * read as a second navigation header. Inactive journeys are quiet text labels;
 * only the current journey becomes a raised paper tab with one short green
 * mark. The selected label rises two pixels, not six.
 */
.ln-film-switcher {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: flex-end;
    gap: 3px;
    min-height: 27px;
    padding-block: 3px 0;
    /*
     * The first tab starts where the panel's top edge stops curving.
     *
     * A tab is only a tab because it is joined to a straight run of the
     * panel's edge. Started at a flat 6px against a 20px corner, its
     * bottom-left sits over the arc with nothing under it to join, and the
     * whole control lifts off and reads as a button parked above the section.
     * Offsetting by exactly the panel's radius is the smallest value at which
     * the edge beneath the tab is horizontal.
     *
     * Expressed as the token the panel itself rounds on, so the two cannot
     * drift apart — this went wrong once already by being written as a number.
     */
    padding-inline: var(--ln-stage-edge, var(--ln-edge-lg));
    margin-bottom: -1px;
}

.ln-film-switcher__tab {
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    min-width: max-content;
    height: 24px;
    padding: 0 8px;
    border: 0;
    border-radius: var(--ln-edge-sm) var(--ln-edge-sm) 0 0;
    background: transparent;
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--dur-short) var(--ease-out),
                transform var(--dur-tick) var(--ease-out);
}

.ln-film-switcher__tab::before,
.ln-film-switcher__tab::after {
    content: "";
    position: absolute;
    pointer-events: none;
}

.ln-film-switcher__tab::before {
    inset: 0;
    z-index: -1;
    border: 1px solid var(--color-rule);
    border-bottom: 0;
    border-radius: inherit;
    background: var(--color-paper-raised);
    opacity: 0;
    transform: scaleY(0.94);
    transform-origin: bottom;
    transition: opacity var(--dur-short) var(--ln-ease-soft),
                transform var(--dur-short) var(--ln-ease-soft);
}

/*
 * The mark that says which one is selected, along the tab's top edge.
 *
 * It sat along the bottom, and a 2px line there draws exactly the boundary
 * the tab is trying not to have: the paper merges into the panel at that edge,
 * and a rule across the join cuts the tab back out into its own closed
 * rectangle. Every browser tab strip puts this mark at the top for the same
 * reason — the bottom edge belongs to the join.
 *
 * Inset 8px either side so it reads as a mark on the tab rather than as the
 * tab's own border, and it rides the 2px the active tab grows by.
 */
.ln-film-switcher__tab::after {
    right: 8px;
    top: 0;
    left: 8px;
    height: 2px;
    border-radius: 2px;
    background: var(--color-accent);
    opacity: 0;
    transform: scaleX(0.4);
    transition: opacity var(--dur-short) var(--ln-ease-soft),
                transform var(--dur-short) var(--ln-ease-soft);
}

@media (hover: hover) and (pointer: fine) {
    .ln-film-switcher__tab:hover { color: var(--color-ink); }

    .ln-film-switcher__tab:hover::before {
        opacity: 0.5;
        transform: none;
    }
}

/*
 * The selected tab rises — by growing, not by moving.
 *
 * It used to be translateY(-2px) on the whole control, and that is what put
 * the tab in the air. The switcher's -1px bottom margin exists so the tab's
 * bottom edge lands *inside* the panel and covers its top border, which is the
 * entire trick that makes a bookmark look cut from the panel rather than laid
 * on top of it. Lifting the tab 2px spent that 1px and then took another,
 * leaving a hairline of page showing between the two and an unbroken panel
 * border running underneath — a button parked above a box.
 *
 * So the paper grows upward instead. The flex row is aligned to flex-end, the
 * bottom edge never moves, and the join survives. The label does not travel
 * with it either, which is the better reading: seven labels in a row share a
 * baseline, and the one that is selected should not be the one sitting off it.
 */
.ln-film-switcher__tab.is-active {
    color: var(--color-accent);
}

.ln-film-switcher__tab.is-active::before,
.ln-film-switcher__tab.is-active::after {
    opacity: 1;
    transform: none;
}

/* Both, so the mark rides the top edge of the paper it belongs to rather than
   floating 2px down inside it. */
.ln-film-switcher__tab.is-active::before,
.ln-film-switcher__tab.is-active::after {
    top: -2px;
}

.ln-film-switcher__tab:active { transform: translateY(1px) scale(0.985); }

/* Pressing the tab that is already selected does nothing, so it does nothing
   visibly either. The press animation is a nudge and a shrink, and both would
   pull the paper off the panel edge it is welded to. */
.ln-film-switcher__tab.is-active:active { transform: none; }

.ln-film-switcher__tab:disabled,
.ln-film-switcher__tab[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}

.ln-film-switcher__tab[aria-busy="true"] {
    color: var(--color-muted);
    cursor: progress;
}

.ln-film-switcher__tab[aria-busy="true"]::after {
    opacity: 1;
    animation: ln-film-switcher-busy 800ms var(--ease-in-out) infinite alternate;
}

.ln-film-switcher__tab[aria-invalid="true"],
.ln-film-switcher__tab[data-state="error"] {
    color: var(--color-halt);
    box-shadow: inset 0 0 0 1px var(--color-halt-line);
}

.ln-film-switcher__tab[aria-invalid="true"]::after,
.ln-film-switcher__tab[data-state="error"]::after {
    background: var(--color-halt);
    opacity: 1;
}

.ln-film-switcher__tab[data-state="success"] {
    color: var(--color-accent);
    box-shadow: inset 0 0 0 1px var(--color-accent-line);
}

/*
 * The focus ring, open along the bottom.
 *
 * `outline` draws a closed rectangle, and a closed 2px ring around a 24px tab
 * is a pill — it seals the one edge that has to stay open, and the focused tab
 * stops looking attached to anything. Three inset shadows draw the same ring
 * on the three edges that are actually edges.
 *
 * The transparent outline is not decoration: forced-colours mode discards
 * box-shadow and repaints outline in the system colour, so it is what keeps
 * this focusable in high contrast.
 *
 * It supersedes the error and success hairlines below while focused, which is
 * the right precedence — where the keyboard is takes priority over what the
 * control is reporting, and the colour still says which.
 */
.ln-film-switcher__tab:focus-visible {
    position: relative;
    z-index: 3;
    outline: 2px solid transparent;
    outline-offset: -2px;
    box-shadow: inset 0 2px 0 var(--color-accent),
                inset 2px 0 0 var(--color-accent),
                inset -2px 0 0 var(--color-accent);
}

@keyframes ln-film-switcher-busy {
    from { transform: scaleX(0.25); }
    to { transform: scaleX(1); }
}

@media (max-width: 48rem) {
    .ln-film-switcher {
        gap: 2px;
        /* padding-inline is deliberately not tightened here. The corner it
           clears is the same 20px on a phone as on a desktop, and the strip
           scrolls, so the start offset costs nothing but buying it back puts
           the first tab over the arc again. */
        overflow-x: auto;
        overscroll-behavior-inline: contain;
        scroll-padding-inline: 3px;
        scroll-snap-type: inline proximity;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }

    .ln-film-switcher::-webkit-scrollbar { display: none; }

    .ln-film-switcher__tab {
        padding-inline: 7px;
        font-size: 10px;
        scroll-snap-align: nearest;
    }

    /*
     * A tab a thumb can hit.
     *
     * The bookmark is 24px tall, which is right for how it looks and well
     * under what a finger needs: on a phone the strip was eight targets 24px
     * high. Each tab takes a 16px transparent border above it, so the button
     * — what a tap lands on — is 40px. The paper, its mark and the focus ring
     * are drawn against the padding box, which has not changed, so the tab
     * looks exactly as it did. The strip is pulled up by the same 16px: the
     * extra comes out of the empty gap above the stage, and nothing below it
     * moves.
     */
    .ln-film-switcher { margin-top: -16px; }

    .ln-film-switcher__tab {
        height: 40px;
        border-top: 16px solid transparent;
    }
}

/* Forced colours paint a transparent border in the system colour, which
   would draw a 16px bar over every tab. There the tab keeps its own size. */
@media (max-width: 48rem) and (forced-colors: active) {
    .ln-film-switcher { margin-top: 0; }

    .ln-film-switcher__tab {
        height: 24px;
        border-top: 0;
    }
}

/*
 * The scrolling strip says that it scrolls.
 *
 * With the scrollbar hidden, a phone showed the first four journeys and the
 * fifth cut through the middle of a word at the screen edge — which reads as
 * the layout breaking, not as more tabs. The edge that has more beyond it
 * fades out instead, driven by the strip's own scroll position: faded on the
 * right at the start, on both sides in the middle, on the left at the end,
 * and on neither when everything fits (an inactive timeline applies nothing).
 *
 * Only where scroll-driven animation exists; elsewhere the strip is as before.
 */
@property --ln-fade-start {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

@property --ln-fade-end {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

@keyframes ln-film-switcher-fade {
    0%   { --ln-fade-start: 0px;  --ln-fade-end: 32px; }
    10%  { --ln-fade-start: 32px; }
    90%  { --ln-fade-end: 32px; }
    100% { --ln-fade-start: 32px; --ln-fade-end: 0px; }
}

@supports (animation-timeline: scroll()) {
    @media (max-width: 48rem) {
        .ln-film-switcher {
            -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            animation: ln-film-switcher-fade linear both;
            animation-timeline: scroll(self inline);
        }
    }
}

.ln-stage__inner {
    position: relative;
    aspect-ratio: 1600 / 760;
    overflow: hidden;
    border: 1px solid var(--color-rule);
    /* Its own step, falling back to the panel radius.
       The stage is not a card: it is a window onto a moving image, and the
       corner has to cut the film itself rather than a page surface. At the
       panel's full radius the arc eats visibly into the frame and the box stops
       reading as a screen. A skin that wants a tighter corner here than on its
       content panels sets --ln-stage-edge; everything else is unchanged. */
    border-radius: var(--ln-stage-edge, var(--ln-edge-lg));
    background: color-mix(in oklab, var(--color-paper-raised) 62%, transparent);
}

/*
 * The film.
 *
 * width/height on the element as well as in CSS, so the panel reserves the
 * right box before a byte of video arrives — without them the page reflows when
 * it loads, which on a hero is the jump everybody notices and nobody can name.
 *
 * `poster` is the last frame, so the panel shows the finished state immediately
 * and the video only has to take over.
 */
.ln-stage__film {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    /* The composition's own ratio. Stated so the reserved box survives a change
       of render size without a matching change here going unnoticed. */
    aspect-ratio: 1600 / 760;
    transition: opacity 180ms var(--ease-in-out),
                visibility 0s linear 180ms;
}

.ln-stage[data-film-ready="true"] .ln-stage__film.is-active {
    opacity: 1;
    visibility: visible;
    transition-delay: 0s;
}

.ln-stage__poster {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}
.ln-stage__poster:not([src]) { visibility: hidden; }
.ln-stage[data-film-ready="true"] .ln-stage__poster { visibility: hidden; }
.ln-film-loading {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(8px, 2vw, 24px);
    color: var(--color-muted);
    font-size: clamp(11px, 1.2vw, 15px);
    background: linear-gradient(120deg, #f7fbfa, #fff 55%, #f4f8fc);
}
.ln-film-loading__scene {
    width: min(62%, 520px);
    stroke: #cddfdc;
    stroke-width: 4;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.ln-film-loading__scene rect,
.ln-film-loading__scene circle { fill: #fff; stroke-width: 2; }
.ln-film-loading__scene .ln-film-loading__accent { fill: #d4eee5; stroke: none; }
.ln-film-loading__link,
.ln-film-loading__check { stroke: #008367; animation: ln-preview-pulse 1.8s ease-in-out infinite; }
.ln-film-loading__check { animation-delay: .3s; }
.ln-stage[data-poster-ready="true"] .ln-film-loading,
.ln-stage[data-film-ready="true"] .ln-film-loading { visibility: hidden; }
@keyframes ln-preview-pulse { 50% { opacity: .25; } }

/*
 * Reduced motion: the poster, and nothing moving.
 *
 * The video element is removed rather than paused — a paused <video> is still a
 * decoder and still a download. What is left is the still it was already
 * showing, which is the finished state and therefore the whole argument, minus
 * the four seconds it takes to make it.
 */
@media (prefers-reduced-motion: reduce) {
    .ln-film-switcher__tab,
    .ln-film-switcher__tab::before,
    .ln-film-switcher__tab::after {
        animation: none;
        transition: none;
    }
    .ln-stage__film { display: none; }
    .ln-film-loading__link,
    .ln-film-loading__check { animation: none; }
    .ln-stage[data-film-ready="true"] .ln-stage__poster { visibility: visible; }

    .ln-stage__inner {
        aspect-ratio: 1600 / 760;
        background-image: var(--ln-stage-poster);
        background-size: cover;
        background-position: center;
    }
}

/*
 * The bubble.
 *
 * The portal's own .cl-wa in this page's colours: a raised sheet, the tail
 * corner squared off, the body then the small print then the time then the
 * buttons as divided rows. No phone around it — see the page's docblock.
 */
.ln-wa {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(24rem, 100%);
    padding: var(--space-md) var(--space-md) var(--space-xs);
    border-radius: 16px 16px 16px 4px;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    color: var(--color-ink);
    box-shadow: 0 24px 60px -28px color-mix(in oklab, var(--color-ink) 30%, transparent);
}

.ln-wa__body { font-size: 14.5px; line-height: 1.5; white-space: pre-line; }

.ln-wa__footer {
    margin-top: var(--space-xs);
    color: var(--color-muted);
    font-size: 12px;
}

.ln-wa__time {
    align-self: flex-end;
    margin-top: 2px;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 10.5px;
    color: var(--color-muted);
}

.ln-wa__buttons {
    display: flex;
    flex-direction: column;
    margin: var(--space-xs) calc(-1 * var(--space-md)) 0;
}

.ln-wa__btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 10px;
    border-top: 1px solid var(--color-rule-soft);
    color: var(--color-accent);
    font-size: 13.5px;
    font-weight: 500;
}

.ln-wa__btn > svg { flex: none; width: 14px; height: 14px; }

/* ── What happened to it ──────────────────────────────────────────────── */

.ln-track {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    width: 100%;
}

.ln-track__step {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px 6px 8px;
    border: 1px solid var(--color-accent-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-accent-wash);
    font-size: 12.5px;
}

.ln-track__mark {
    display: grid;
    place-items: center;
    width: 16px;
    height: 16px;
    /* Squared with the rest. A round badge on a squared chip is the one circle
       that gives the whole page away. */
    border-radius: 3px;
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

.ln-track__mark svg { width: 10px; height: 10px; }
.ln-track__label { color: var(--color-ink); font-weight: 500; }

.ln-track__at {
    color: var(--color-muted);
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
}

/* ── A section ────────────────────────────────────────────────────────── */

.ln-section {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding: clamp(3.5rem, 9vw, 7rem) clamp(1rem, 4vw, 2.5rem) 0;
}

.ln-section--film {
    padding-top: clamp(2.5rem, 6vw, 4.5rem);
}

.ln-section--film > .ln-stage {
    margin-top: 0;
    padding-inline: 0;
}

/* ── The strip ────────────────────────────────────────────────────────── */

/* Continues the hero's paper, so there is no seam between them. */
.ln-strip {
    padding-top: clamp(1.5rem, 3vw, 2.5rem);
    padding-bottom: clamp(3rem, 6vw, 5rem);
}

.ln-strip__lead {
    text-align: center;
    color: var(--color-ink-soft);
    font-size: 15px;
    line-height: 1.5;
}

/* The second line, one step quieter and on its own row. Two claims of equal
   weight side by side read as one long sentence; stacked and stepped down,
   they read as a statement and its qualifier. */
.ln-strip__second {
    display: block;
    color: var(--color-muted);
    font-size: 13.5px;
}

/* Customer marks drift right to left through soft fades at both edges. */
.ln-clients {
    --ln-clients-gap: clamp(2rem, 5vw, 4rem);
    margin-top: var(--space-lg);
    padding-block: 12px;
    overflow: hidden;
    direction: ltr;
    mask-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-ink) 18%, transparent) 3%, color-mix(in srgb, var(--color-ink) 72%, transparent) 8%, var(--color-ink) 14%, var(--color-ink) 86%, color-mix(in srgb, var(--color-ink) 72%, transparent) 92%, color-mix(in srgb, var(--color-ink) 18%, transparent) 97%, transparent);
    -webkit-mask-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-ink) 18%, transparent) 3%, color-mix(in srgb, var(--color-ink) 72%, transparent) 8%, var(--color-ink) 14%, var(--color-ink) 86%, color-mix(in srgb, var(--color-ink) 72%, transparent) 92%, color-mix(in srgb, var(--color-ink) 18%, transparent) 97%, transparent);
}

.ln-clients:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

/* Both runs have identical widths and half-gap padding at their boundaries.
   The second run therefore meets the first exactly when the loop resets. */
.ln-clients__track {
    display: flex;
    width: max-content;
    animation: ln-clients-scroll var(--ln-clients-duration, 64s) linear infinite;
}

.ln-clients:hover .ln-clients__track,
.ln-clients:focus-within .ln-clients__track {
    animation-play-state: paused;
}

.ln-clients__run {
    display: flex;
    flex: none;
    align-items: center;
    gap: var(--ln-clients-gap);
    margin: 0;
    padding-inline: calc(var(--ln-clients-gap) / 2);
    list-style: none;
}

@keyframes ln-clients-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

.ln-clients__item {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: clamp(108px, 11vw, 128px);
    height: 44px;
}

.ln-clients__mark {
    width: 100%;
    height: clamp(34px, 3.4vw, 40px);
    object-fit: contain;
    opacity: .6;
    filter: grayscale(1);
    mix-blend-mode: multiply;
    transition: opacity var(--dur-short) var(--ease-out);
}

.ln-clients__item:hover .ln-clients__mark { opacity: 1; }

/*
 * ── The ground ────────────────────────────────────────────────────────────
 *
 * Where the backdrop ends.
 *
 * The dot grid and the charges are both fixed to the viewport, so they cover
 * the whole page for as long as nothing is painted over them. This is what
 * paints over them: everything from the services index down sits on an opaque
 * sheet of paper, which simply scrolls across the backdrop and hides it. No
 * scroll maths and nothing to keep in sync — the backdrop ends exactly where
 * this element starts.
 *
 * The top 150px is a fade rather than an edge, so the grid dissolves instead of
 * stopping at a seam. A linear-gradient holds its last stop for the rest of the
 * box, so one declaration gives both the fade and the solid paper under it.
 *
 * 150px is chosen against the section's own top padding — clamp(3.5rem, 9vw,
 * 7rem), so 56px to 112px — plus the head's own leading. The fade lands inside
 * that padding at every width, which is why there is no visible band of empty
 * page: the dots are gone by the time the heading arrives.
 */
.ln-ground {
    position: relative;
    z-index: var(--z-base);
    /* The closing block owns the final vertical rhythm through its own
       padding. Extra ground padding here would open a gap between it and the
       footer, which is the one junction on the page where the surface change
       has to happen on the hairline and nowhere else. */
    padding-bottom: 0;
    background-image: linear-gradient(
        to bottom,
        transparent 0,
        color-mix(in oklab, var(--color-paper) 70%, transparent) 64px,
        var(--color-paper) 150px
    );
}

/*
 * ── The services ──────────────────────────────────────────────────────────
 *
 * Hallmark · pre-emit critique: P5 H5 E4 S5 R4 V4
 * genre: editorial · structure: scrolling name rows over a ruled 12-cell grid
 * tokens: pass — warm family on the badges, accent on the icons, the two
 *         colours the wordmark is built from; no literal in here
 * honest: pass — twelve services, the business's own copy and order
 * italic headers: none · chrome: none re-drawn
 * mobile: 320 / 375 / 768 / 1440 measured — 1/1/2/3 cells, no horizontal
 *         scroll, no clipped service name
 *
 * Two halves. The names go past, then each service gets a cell of its own.
 * The rows are built from the same array as the grid, so the two can never
 * disagree about what is on offer.
 */

/* ── The scrolling names ───────────────────────────────────────────────── */

.ln-swarm {
    margin-top: clamp(1.75rem, 3vw, 2.5rem);
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    /* Faded at both ends with a mask rather than two paper overlays: a mask
       works whatever is behind it, and behind this the ground's fade is a
       different colour at every scroll position. */
    mask-image: linear-gradient(90deg, transparent, black 9%, black 91%, transparent);
    -webkit-mask-image: linear-gradient(90deg, transparent, black 9%, black 91%, transparent);
}

.ln-swarm__row { display: flex; overflow: hidden; }

.ln-swarm__track {
    display: flex;
    gap: var(--space-sm);
    width: max-content;
    animation: ln-swarm-scroll var(--ln-swarm-time, 40s) linear infinite;
}

/* The middle row runs the other way. Three rows travelling together read as
   one thick band sliding past; one opposed row is what makes it read as
   three. */
.ln-swarm__track.is-reverse { animation-direction: reverse; }

/* Held still while a name is being read. */
.ln-swarm:hover .ln-swarm__track { animation-play-state: paused; }

.ln-swarm__run {
    display: flex;
    gap: var(--space-sm);
}

/*
 * A name.
 *
 * The source sets these in orange and that was carried over literally at
 * first. It was wrong here for a reason the source does not have: their page
 * is dark, so an orange chip is the only warm thing on it. Ours is paper, and
 * twelve orange chips became the loudest thing in the section — louder than
 * the headline above them and than the one accent button in the header.
 *
 * The page spends warm on exactly two things: the wordmark's numeral and the
 * keywords drifting on the backdrop. Neither is a block of twelve.
 *
 * So they are neutral: raised paper, a hairline, soft ink. The colour in this
 * section belongs to the twelve icon tiles below, and a quiet row of names is
 * what lets them keep it.
 */
.ln-swarm__badge {
    flex: none;
    padding: 6px 12px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
}

/* Exactly -25%: the track holds four passes, so one quarter is one full pass
   and the loop lands back on its own beginning with no seam. */
@keyframes ln-swarm-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-25%); }
}

/*
 * ═══════════════════════════════════════════════════════════════════════════
 *  Services — the product-led direction
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * ## The argument
 *
 * A card grid has been rejected three times, and the reason is worth stating
 * once so it is not rebuilt by accident: twelve boxes at identical weight give
 * a reader nothing to look at. Every cell is the same size, so every service
 * claims the same importance, so none of them do — and the only thing left
 * carrying meaning is the words, which is a list with borders drawn round it.
 *
 * The fix is not tidier cards. It is to stop naming the product and start
 * showing it. So this section is built from six SCENES, each one an artefact
 * drawn in HTML and CSS out of the real interface — a WhatsApp message with
 * quick replies, a delivery-state readout, a routing flow, a template with its
 * variables still in it, a six-digit code with its fallback ladder, an order
 * moving along its stepper, a star row, a campaign readout. The twelve services
 * are all still named and all still described, but they sit BESIDE the evidence
 * for them rather than instead of it.
 *
 * ## Why bands, and why they alternate
 *
 * Each scene owns a band; each band owns one to three services. The band is not
 * a container — it has a hairline above it and nothing else, so what a reader
 * sees is a drawn artefact and a short ruled index next to it, not a panel.
 * Alternating the scene left / right down the page does two things: it stops
 * the six bands reading as six rows of the same table, and it keeps the eye
 * travelling instead of scanning a single column of thumbnails.
 *
 * Band sizes deliberately vary (2, 3, 2, 2, 2, 1). A perfectly even split is
 * the thing that makes a grid feel like a grid even after you have removed the
 * borders.
 *
 * ## Where the one dark object goes
 *
 * The last band is ink. Everything above it is paper on paper — the scenes are
 * lifted a half-step but they are still light — and a page of nothing but light
 * surfaces has no anchor. Analytics is the natural place to spend it: it is the
 * one service that is *about* the eleven above it, so the section closes on a
 * readout of everything it has just shown you. On ink the accent finally reads
 * as a colour rather than as a tint.
 *
 * ## Honesty
 *
 * The only figures on the page are in the campaign readout, and that scene
 * carries a "Sample data" flag. Nothing else states a number that is not a
 * timestamp, a code, or an order reference — there are no invented statistics,
 * no testimonials, no customer names and no logos anywhere in this section.
 *
 * ## Tokens
 *
 * Every colour below is a var(--color-*) or a color-mix of one. Every corner is
 * --ln-edge-sm / --ln-edge / --ln-edge-lg, which is the landing page's squared
 * scale, with exactly one exception: .ln-pd-wa, the WhatsApp bubble, at
 * 24px 24px 24px 6px — because that is the shape a real recipient sees, and
 * squaring it off would be drawing something the product does not produce.
 * (.ln-pd-typing and .ln-pd-ask are the same bubble at a different size, so they
 * carry the same corner rather than a second invented one.)
 *
 * Below the scale there are a handful of 2px and 3px corners, and they are
 * *more* squared than --ln-edge-sm, not less. They sit on marks smaller than
 * 16px — a 6px eyebrow dot, a 5px typing dot, an 8px bar, an 11px stepper node,
 * a 16px tick badge — where 4px is most of the box and the mark rounds into a
 * circle. landing.css makes the same call for the same reason at
 * .ln-track__mark and .ln-nav__burger-bars; the note there is the one that
 * matters, and it is that a round badge on a squared chip is the single circle
 * that would give the whole page away.
 */

/* ── The head ──────────────────────────────────────────────────────────────
 *
 * Title left, lede right, both sitting on the same baseline band.
 *
 * Centring the head was the other option and it is the wrong one here: the
 * bands underneath are hard-left editorial rows, and a centred head above a
 * left-aligned page announces a symmetry the section then does not have. Ending
 * the head on a single hairline is what makes the first band read as a
 * continuation rather than as a new start.
 */
.ln-pd-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 27rem);
    align-items: end;
    gap: clamp(1rem, 3vw, 3rem);
    padding-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

.ln-pd-eyebrow {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-bottom: var(--space-md);
    color: var(--color-muted);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.11em;
    text-transform: uppercase;
}

/* The eyebrow's one mark. Squared, like everything else on this page — a dot
   here would be the single circle that gives the geometry away. */
.ln-pd-eyebrow::before {
    content: '';
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 2px;
    background: var(--color-warm);
}

/* 400 weight, roman, tight leading — the landing page's own display ratio, so
   this section's heading is the same voice as the hero's. Never italic. */
.ln-pd-title {
    font-family: var(--font-display);
    font-size: clamp(2.25rem, 1.4rem + 3vw, 3.75rem);
    font-style: normal;
    font-weight: 400;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--color-ink);
}

.ln-pd-lede {
    color: var(--color-muted);
    font-size: 16.5px;
    line-height: 1.6;
}

/* ── A band ────────────────────────────────────────────────────────────────
 *
 * One hairline, two columns, no box. The hairline is the only chrome in the
 * whole section and it belongs to the rhythm, not to the content: it says
 * "next" rather than "this is a thing".
 *
 * align-items: center rather than start, because the scenes are all different
 * heights on purpose and pinning them to the top of a short copy column leaves
 * a ragged trough down the middle of the page.
 */
.ln-pd-band {
    display: grid;
    grid-template-columns: minmax(0, 1.04fr) minmax(0, 1fr);
    align-items: center;
    gap: clamp(1.5rem, 4vw, 3.5rem);
    padding-block: clamp(2.5rem, 5vw, 4rem);
    border-top: 1px solid var(--color-rule-soft);
}

.ln-pd-band:first-of-type { border-top: 0; padding-top: 0; }

/* The flip. Explicit grid placement rather than `order`, so the DOM order stays
   scene-then-copy at every width — which is exactly what the single-column
   layout wants, and means the mobile stack needs no rule of its own. */
.ln-pd-band--flip > .ln-pd-scene-wrap { grid-column: 2; grid-row: 1; }
.ln-pd-band--flip > .ln-pd-say { grid-column: 1; grid-row: 1; }

/* The closing band breaks the two-column rhythm on purpose: its ink panel runs
   the full measure and does its own splitting inside. Five bands of the same
   width and then one that spans everything is what makes the last one read as
   a conclusion rather than as a sixth of the same thing. */
.ln-pd-band--final { grid-template-columns: minmax(0, 1fr); }

.ln-pd-band__kicker {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
    color: var(--color-muted);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* Tabular figures, in the wordmark's orange — the same treatment the live
   landing page gives its service index, so the numbering is continuous between
   the two surfaces rather than a new idea invented here. */
.ln-pd-band__nums {
    flex: none;
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
}

.ln-pd-band__title {
    max-width: 20ch;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(1.3rem, 1rem + 1.1vw, 1.75rem);
    font-style: normal;
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: -0.02em;
}

/* ── The service rows ──────────────────────────────────────────────────────
 *
 * Ruled rows, not cards: a top hairline each and nothing on the sides. Three of
 * these stacked read as an index — one continuous object — where three bordered
 * boxes would read as three separate ones that happen to be adjacent.
 *
 * The icon is a small squared tile rather than a bare glyph, because at 15px on
 * paper a 1.6-weight line drawing disappears; the tile gives it a ground and a
 * measurable size without turning it into a badge.
 */
.ln-pd-list { margin-top: clamp(1.25rem, 2.5vw, 1.75rem); }

.ln-pd-item {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-2xs);
    border-top: 1px solid var(--color-rule-soft);
    transition: background var(--dur-short) var(--ease-out);
}

.ln-pd-item:last-child { border-bottom: 1px solid var(--color-rule-soft); }

/* A wash on hover, so reading down the index has some life in it. Accent-wash
   at 55% rather than full: the row is being pointed at, not selected. */
.ln-pd-item:hover { background: color-mix(in oklab, var(--color-accent-wash) 55%, transparent); }

.ln-pd-item__tile {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border: 1px solid var(--color-accent-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-accent-wash);
    color: var(--color-accent);
}

.ln-pd-item__tile svg { width: 18px; height: 18px; stroke-width: 1.6; }

.ln-pd-item__name {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 15px;
    font-style: normal;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
}

.ln-pd-item__note {
    margin-top: 4px;
    max-width: 46ch;
    color: var(--color-muted);
    font-size: 13.5px;
    line-height: 1.55;
}

.ln-pd-item__num {
    flex: none;
    padding-top: 2px;
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
}

/* ── The scene ─────────────────────────────────────────────────────────────
 *
 * The workbench the artefacts are drawn on: a half-step sunken ground carrying
 * the landing page's own 22px dot lattice, so a scene reads as a piece of the
 * page with something built on it rather than as a screenshot pasted into a
 * frame.
 *
 * No border on three sides and no shadow — deliberately. The moment this thing
 * takes a full hairline and a lift it becomes a card, and six of them become
 * the grid this direction exists to avoid. It is a surface, and the objects
 * ON it are what get borders and shadows.
 */
.ln-pd-scene {
    position: relative;
    padding: clamp(1.1rem, 2.4vw, 1.75rem);
    border-radius: var(--ln-edge-lg);
    background-color: color-mix(in oklab, var(--color-paper-sunken) 62%, var(--color-paper));
    background-image: radial-gradient(
        circle at 1px 1px,
        color-mix(in oklab, var(--color-rule-track) 62%, transparent) 1px,
        transparent 0
    );
    background-size: 22px 22px;
}

/* The caption. Small, muted, and directly under the drawing — it says what you
   are looking at, which is the one thing a drawn artefact cannot say for
   itself, and it is where the sample-content disclosure lives. */
.ln-pd-scene__cap {
    margin-top: var(--space-sm);
    padding-inline: var(--space-2xs);
    color: var(--color-muted);
    font-size: 12px;
    line-height: 1.5;
}

/* ── Scene 1 · the auto-reply ──────────────────────────────────────────────
 *
 * Trigger, then a wire, then the typing state, then the message, then what
 * happened to the message. Read top to bottom it is the whole product in one
 * column, which is why it is the section's first scene.
 */
.ln-pd-trigger {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 11px;
    border: 1px solid var(--color-warm-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-warm-wash);
    color: var(--color-warm-ink);
    font-size: 12px;
    font-weight: 500;
}

.ln-pd-trigger svg { flex: none; width: 13px; height: 13px; stroke-width: 1.8; }

/* The wire. One pixel, indented to the middle of the trigger chip so the two
   objects are visibly connected rather than merely stacked. */
.ln-pd-wire {
    width: 1px;
    height: 18px;
    margin-left: 20px;
    background: color-mix(in oklab, var(--color-rule-control) 55%, transparent);
}

/* Typing. The one moving thing in the section, and it earns it: the whole claim
   of "responds instantly" is a state you can only show with time. */
.ln-pd-typing {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-bottom: var(--space-xs);
    padding: 7px 11px;
    border-radius: 24px 24px 24px 6px;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
}

.ln-pd-typing__dot {
    width: 5px;
    height: 5px;
    border-radius: 2px;
    background: var(--color-muted-soft);
    animation: ln-pd-typing 1.25s var(--ease-in-out) infinite;
}

.ln-pd-typing__dot:nth-child(2) { animation-delay: 0.16s; }
.ln-pd-typing__dot:nth-child(3) { animation-delay: 0.32s; }

@keyframes ln-pd-typing {
    0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
    30%           { opacity: 1;    transform: translateY(-2px); }
}

/*
 * The bubble — the one rounded object on a squared page.
 *
 * 24 24 24 6 is not a stylistic choice, it is a measurement: it is the shape
 * WhatsApp gives a business message, tail bottom-left, and drawing it any other
 * way would be showing a message the product never sends. Everything else here
 * — the tile, the chips, the cells, the scene itself — stays on --ln-edge-*.
 *
 * Raised paper with a long, soft, downward shadow, so the message lifts off the
 * workbench. No phone drawn around it: a device frame is decoration, and the
 * message is the product.
 */
.ln-pd-wa {
    display: flex;
    flex-direction: column;
    width: min(25rem, 100%);
    padding: var(--space-md) var(--space-md) var(--space-2xs);
    border: 1px solid var(--color-rule-soft);
    border-radius: 24px 24px 24px 6px;
    background: var(--color-paper-raised);
    color: var(--color-ink);
    box-shadow: 0 22px 46px -26px color-mix(in oklab, var(--color-ink) 32%, transparent);
}

.ln-pd-wa__body { font-size: 14.5px; line-height: 1.5; }

.ln-pd-wa__time {
    align-self: flex-end;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 3px;
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-variant-numeric: tabular-nums;
}

/* The read receipt keeps its real colour logic: two ticks, and they go accent
   once the message has been read. It is the product's own semantics, not a
   decoration. */
.ln-pd-wa__time svg { width: 13px; height: 13px; stroke-width: 2; color: var(--color-accent); }

/* Quick replies as full-bleed divided rows, pulled out to the bubble's edge —
   which is how WhatsApp actually draws them, and the reason the bubble's bottom
   padding is nearly nothing. */
.ln-pd-wa__acts {
    display: flex;
    flex-direction: column;
    margin: var(--space-xs) calc(-1 * var(--space-md)) 0;
}

.ln-pd-wa__act {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 10px;
    border-top: 1px solid var(--color-rule-soft);
    color: var(--color-accent);
    font-size: 13.5px;
    font-weight: 500;
}

.ln-pd-wa__act svg { flex: none; width: 14px; height: 14px; stroke-width: 1.8; }

/*
 * The delivery readout.
 *
 * Three states, and they are deliberately NOT three identical chips: sent and
 * delivered are neutral, read is accent-filled. Identical chips describe a
 * list; a progression describes a message travelling, which is the only reason
 * to draw this at all.
 */
.ln-pd-track {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    margin-top: var(--space-md);
}

.ln-pd-track__step {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 11px 6px 8px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    font-size: 12.5px;
}

.ln-pd-track__step.is-final {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
}

.ln-pd-track__mark {
    display: grid;
    place-items: center;
    width: 16px;
    height: 16px;
    /* Squared, at 3px. A round badge on a squared chip is the single circle
       that would give the whole page away. */
    border-radius: 3px;
    background: var(--color-paper-sunken);
    color: var(--color-muted);
}

.ln-pd-track__step.is-final .ln-pd-track__mark {
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

.ln-pd-track__mark svg { width: 11px; height: 11px; stroke-width: 2.2; }
.ln-pd-track__label { color: var(--color-ink); font-weight: 500; }

.ln-pd-track__at {
    color: var(--color-muted);
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
}

/* ── Scene 2 · the routing flow ────────────────────────────────────────────
 *
 * A rail with four stops rather than a branching tree.
 *
 * A true two-branch diagram needs diagonal connectors, which at 327px either
 * collapse into a smudge or force a horizontal scroll. The branch is real and
 * it still has to be shown, so it lives INSIDE the third stop as three
 * destination chips — the fork is expressed as choice, not as geometry, and the
 * rail stays one column at every width.
 */
.ln-pd-flow { display: flex; flex-direction: column; }

.ln-pd-node {
    position: relative;
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr);
    gap: var(--space-sm);
    padding-bottom: var(--space-md);
}

.ln-pd-node:last-child { padding-bottom: 0; }

/* The rail, drawn per-node from below its own marker to the next one. Doing it
   this way rather than as one absolutely positioned line means the rail cannot
   drift out of sync when a stop grows a second line of text. */
.ln-pd-node::before {
    content: '';
    position: absolute;
    left: 12.5px;
    top: 28px;
    bottom: 2px;
    width: 1px;
    background: var(--color-rule);
}

.ln-pd-node:last-child::before { display: none; }

.ln-pd-node__mark {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-muted);
}

/* Filled markers are the steps the automation performs by itself; the outlined
   last one is the handover to a person. The difference is the argument. */
.ln-pd-node.is-auto .ln-pd-node__mark {
    border-color: var(--color-accent);
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

.ln-pd-node__mark svg { width: 14px; height: 14px; stroke-width: 1.8; }

.ln-pd-node__name {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 13.5px;
    font-style: normal;
    font-weight: 600;
    line-height: 1.3;
}

.ln-pd-node__meta {
    margin-top: 2px;
    color: var(--color-muted);
    font-size: 12.5px;
    line-height: 1.45;
}

.ln-pd-branch {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: var(--space-xs);
}

.ln-pd-branch__chip {
    padding: 4px 9px;
    border: 1px solid var(--color-accent-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-accent);
    font-size: 11.5px;
    font-weight: 500;
}

/* ── Scene 3 · the template ────────────────────────────────────────────────
 *
 * A template with its variables still unresolved on top, and the same template
 * delivered in three languages underneath. Showing only the composer would be
 * showing a form; showing only the renders would be showing three sentences.
 * The pair is what makes the mechanism legible — one template, many recipients.
 */
.ln-pd-tpl {
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge);
    background: var(--color-paper-raised);
    box-shadow: 0 14px 34px -24px color-mix(in oklab, var(--color-ink) 28%, transparent);
}

.ln-pd-tpl__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: 9px var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
}

.ln-pd-tpl__name {
    min-width: 0;
    color: var(--color-ink-soft);
    font-family: var(--font-display);
    font-size: 12px;
    letter-spacing: 0.01em;
    overflow-wrap: anywhere;
}

.ln-pd-tpl__flag {
    flex: none;
    padding: 3px 8px;
    border: 1px solid var(--color-live-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-live-wash);
    color: var(--color-live);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.ln-pd-tpl__body {
    padding: var(--space-md);
    color: var(--color-ink);
    font-size: 14px;
    /* 1.9, not 1.5. The variable chips have a border and vertical padding, so
       at normal leading they collide with the line above. */
    line-height: 1.9;
}

/* A variable, drawn as the product draws it: accent, squared, and set in the
   display face so it reads as a token rather than as emphasised prose.
   nowrap because a chip broken across two lines loses both its borders. */
.ln-pd-var {
    padding: 2px 6px;
    border: 1px solid var(--color-accent-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-accent-wash);
    color: var(--color-accent);
    font-family: var(--font-display);
    font-size: 12.5px;
    font-weight: 500;
    white-space: nowrap;
}

.ln-pd-tpl__rule {
    padding: 7px var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
    background: var(--color-paper-sunken);
    color: var(--color-muted);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.11em;
    text-transform: uppercase;
}

.ln-pd-langs { padding: var(--space-2xs) var(--space-md) var(--space-sm); }

.ln-pd-langs li {
    display: grid;
    grid-template-columns: 30px minmax(0, 1fr);
    align-items: start;
    gap: var(--space-sm);
    padding: 9px 0;
    border-top: 1px solid var(--color-rule-soft);
}

.ln-pd-langs li:first-child { border-top: 0; }

.ln-pd-lang {
    padding: 2px 0;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-align: center;
}

.ln-pd-langs p { color: var(--color-ink-soft); font-size: 13px; line-height: 1.5; }

/* ── Scene 4 · the code, and the alarm ─────────────────────────────────────
 *
 * Two artefacts on one bench, divided by a hairline, because the two services
 * they stand for are the same promise made twice: a message that has to arrive
 * right now. One is expected by the recipient, the other is not.
 */
.ln-pd-otp__lead {
    color: var(--color-muted);
    font-size: 12.5px;
}

/* Six cells that share the row equally and cap out — flex-basis 0 keeps them
   even at 375px, and the max-width stops them stretching into six posters at
   1440px. Tabular figures so the digits sit on a real grid. */
.ln-pd-otp__cells {
    display: flex;
    gap: 6px;
    margin-top: var(--space-xs);
}

.ln-pd-otp__cell {
    flex: 1 1 0;
    min-width: 0;
    max-width: 52px;
    display: grid;
    place-items: center;
    aspect-ratio: 5 / 6;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(17px, 4.4vw, 21px);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 6px 16px -12px color-mix(in oklab, var(--color-ink) 40%, transparent);
}

.ln-pd-otp__note {
    margin-top: var(--space-xs);
    color: var(--color-muted);
    font-size: 11.5px;
}

/* The fallback ladder. The first channel is live-coloured and the rest are
   neutral, which is the honest picture: three of the four are standing by. */
.ln-pd-chain {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: var(--space-md);
}

.ln-pd-chain__step {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 9px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-muted);
    font-size: 11.5px;
    white-space: nowrap;
}

.ln-pd-chain__step svg { width: 12px; height: 12px; stroke-width: 2; }

.ln-pd-chain__step.is-on {
    border-color: var(--color-live-line);
    background: var(--color-live-wash);
    color: var(--color-live);
    font-weight: 600;
}

/* The link between channels: a two-pixel-tall arrowless segment. A chevron
   glyph here would be a fourth typeface doing a line's job. */
.ln-pd-chain__link {
    flex: none;
    width: 12px;
    height: 1px;
    background: var(--color-rule-track);
}

.ln-pd-split {
    height: 1px;
    margin: var(--space-md) 0;
    background: var(--color-rule);
}

/*
 * The incident line.
 *
 * Halt-coloured, and the colour is never doing the work alone — there is a
 * glyph and the word "Incident" beside it. A 3px left edge rather than a full
 * border, so it reads as a log line flagged in the margin, which is what an
 * alert actually is.
 */
.ln-pd-alert {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr);
    gap: var(--space-sm);
    padding: 11px 13px;
    border: 1px solid var(--color-halt-line);
    border-left: 3px solid var(--color-halt);
    border-radius: 0 var(--ln-edge) var(--ln-edge) 0;
    background: var(--color-halt-wash);
}

.ln-pd-alert__icon {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: var(--ln-edge-sm);
    background: color-mix(in oklab, var(--color-halt) 12%, transparent);
    color: var(--color-halt);
}

.ln-pd-alert__icon svg { width: 15px; height: 15px; stroke-width: 1.8; }

.ln-pd-alert__head {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
}

.ln-pd-alert__meta {
    margin-top: 2px;
    color: var(--color-muted);
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
}

/* ── Scene 5 · the order, and the ask ──────────────────────────────────────
 *
 * A stepper on top, a star row underneath, and the sequence between them is the
 * argument: the review request is not a separate campaign, it is the last step
 * of the delivery. That is the whole reason these two services share a band.
 */
.ln-pd-order {
    padding: var(--space-md);
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge);
    background: var(--color-paper-raised);
}

.ln-pd-order__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}

.ln-pd-order__ref {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.ln-pd-order__state {
    padding: 3px 8px;
    border: 1px solid var(--color-live-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-live-wash);
    color: var(--color-live);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    white-space: nowrap;
}

/*
 * The stepper.
 *
 * Equal flex columns with the connector drawn as a ::before on each step except
 * the first, so the track can never be longer or shorter than the steps it
 * joins. Labels are short by necessity — at 375px each column is about 75px,
 * and "Out for delivery" would take four lines.
 */
.ln-pd-steps { display: flex; }

.ln-pd-steps li {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    padding-top: 26px;
    text-align: center;
    color: var(--color-muted);
    font-size: 10.5px;
    line-height: 1.3;
}

.ln-pd-steps li::before {
    content: '';
    position: absolute;
    top: 8px;
    right: 50%;
    left: -50%;
    height: 1px;
    background: var(--color-rule-track);
}

.ln-pd-steps li:first-child::before { display: none; }

.ln-pd-steps li::after {
    content: '';
    position: absolute;
    top: 3px;
    left: calc(50% - 5.5px);
    width: 11px;
    height: 11px;
    border: 1px solid var(--color-rule-control);
    border-radius: 3px;
    background: var(--color-paper-raised);
}

.ln-pd-steps li.is-done { color: var(--color-ink-soft); font-weight: 500; }
.ln-pd-steps li.is-done::before { background: var(--color-accent); }
.ln-pd-steps li.is-done::after { border-color: var(--color-accent); background: var(--color-accent); }

/* The step the order is on right now: filled like the completed ones, but
   ringed so it is distinguishable without relying on colour alone. */
.ln-pd-steps li.is-now { color: var(--color-ink); font-weight: 600; }
.ln-pd-steps li.is-now::before { background: var(--color-accent); }
.ln-pd-steps li.is-now::after {
    border-color: var(--color-accent);
    background: var(--color-accent);
    box-shadow: 0 0 0 3px var(--color-accent-wash);
}

.ln-pd-ask {
    margin-top: var(--space-md);
    padding: var(--space-sm) var(--space-md) var(--space-md);
    border: 1px solid var(--color-rule-soft);
    border-radius: 24px 24px 24px 6px;
    background: var(--color-paper-raised);
    box-shadow: 0 18px 40px -26px color-mix(in oklab, var(--color-ink) 30%, transparent);
}

.ln-pd-ask__body { color: var(--color-ink); font-size: 14px; line-height: 1.5; }

.ln-pd-stars {
    display: flex;
    gap: 4px;
    margin-top: var(--space-sm);
    color: var(--color-warm);
}

/* The stars are drawn as outlines and filled in CSS, so the markup stays
   fill="none" like every other icon in this file and the "on" state is a
   single declaration rather than a second drawing. */
.ln-pd-stars svg { width: 22px; height: 22px; stroke-width: 1.4; }
.ln-pd-stars .is-on svg { fill: currentColor; }

.ln-pd-ask__note {
    margin-top: var(--space-xs);
    color: var(--color-muted);
    font-size: 11.5px;
}

/* ── Scene 6 · the readout, on ink ─────────────────────────────────────────
 *
 * The section's one dark object, and its close.
 *
 * Analytics is the right service to spend it on because it is the only one of
 * the twelve that is about the other eleven — so the band is a single ink panel
 * holding both the copy and the readout, rather than a scene beside an index.
 * Ink is already in this palette and already on this page; a dark panel is the
 * identity turned up, not a new colour introduced at the end.
 */
/*
 * The readout scene, on the page's own paper.
 *
 * It was an ink panel — the section's one dark object — and on a page that is
 * paper from the header down it read as a slide borrowed from a different
 * deck. Five scenes drawn in the product's light interface followed by one in
 * near-black is not a climax, it is an inconsistency, and it made the rotation
 * flash dark once every eighteen seconds.
 *
 * So it is the same materials as the other five: raised paper, one hairline,
 * ink type, and the accent doing the only colouring. The one thing it keeps is
 * the panel — analytics is a screen in the product, and a screen has an edge.
 */
.ln-pd-final {
    display: grid;
    grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
    align-items: center;
    gap: clamp(1.75rem, 4vw, 3.5rem);
    padding: clamp(1.5rem, 3.5vw, 3rem);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
    background: var(--color-paper-raised);
}

/* Nothing to override any more: the kicker, figures and title are the same
   tokens every other scene uses, so the inverted variants are gone rather than
   restated in reverse. */

.ln-pd-read {
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge);
    background: var(--color-paper);
}

.ln-pd-read__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: 10px 12px;
    border-bottom: 1px solid var(--color-rule-soft);
}

.ln-pd-read__name {
    color: var(--color-ink);
    font-size: 12.5px;
    font-weight: 600;
}

/*
 * The sample-data flag, in the warm family.
 *
 * It is the one label on the page that qualifies a number rather than stating
 * one, so it gets the colour the identity reserves for figures — and warm-ink
 * on warm-wash measures 4.72:1, which clears the floor for text this small.
 */
.ln-pd-read__flag {
    flex: none;
    padding: 2px 7px;
    border: 1px solid var(--color-warm-line);
    border-radius: var(--ln-edge-sm);
    background: var(--color-warm-wash);
    color: var(--color-warm-ink);
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

.ln-pd-funnel { margin-top: 0; padding: 12px; }

.ln-pd-funnel li {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-sm);
    padding: 5px 0;
}

.ln-pd-funnel__label {
    color: var(--color-muted);
    font-size: 12px;
}

.ln-pd-funnel__rail {
    display: block;
    height: 6px;
    border-radius: 2px;
    background: var(--color-paper-sunken);
}

/*
 * The fill, as a block with a width, not an inline span.
 *
 * It was an inline element inside another inline element, so `width: var(--w)`
 * did nothing and every bar drew full-length regardless of its number — a
 * funnel where 29% and 100% looked identical.
 */
.ln-pd-funnel__fill {
    display: block;
    width: var(--w, 100%);
    height: 100%;
    border-radius: 2px;
    background: var(--color-accent);
}

.ln-pd-funnel__fig {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
}

.ln-pd-spark {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 34px;
    padding: 10px 12px 12px;
    border-top: 1px solid var(--color-rule-soft);
}

.ln-pd-spark span {
    flex: 1;
    height: var(--h, 50%);
    border-radius: 2px;
    background: color-mix(in oklab, var(--color-accent) 34%, transparent);
}

/*
 * ── The services scenes ───────────────────────────────────────────────────
 *
 * Six scenes drawn from the real interface, one frame, three seconds each.
 *
 * Stacked down the page they ran to roughly six screens, which is a section
 * nobody scrolls to the end of. In one frame the section is as tall as its
 * tallest scene and the six play instead — the same content, a sixth of the
 * page, and the motion is what makes a reader wait rather than scroll past.
 *
 * All six live in a single grid cell. That is what fixes the height: the frame
 * measures itself against the tallest scene once and then never moves, so
 * swapping scenes cannot nudge the page under the pointer. Absolute
 * positioning would have needed a measured height and would drift the moment a
 * line of copy changed.
 */
.ln-pd-rota {
    position: relative;
    margin-top: clamp(1.75rem, 3vw, 2.5rem);
}

.ln-pd-rota__frame {
    display: grid;
    align-items: start;
}

/*
 * The scene, inside the frame.
 *
 * The band's own padding and rule are cancelled here. Both existed to separate
 * six scenes stacked down a page — up to 64px above and below each, plus a
 * hairline — and inside a frame that shows one at a time they are 128px of
 * nothing between the label and the scene it names. Worse, the padding was
 * suppressed on :first-of-type only, so scene one was a different height from
 * the other five and the frame jumped the first time it advanced.
 */
.ln-pd-rota__scene {
    grid-area: 1 / 1;
    padding-block: 0;
    border-top: 0;
    opacity: 0;
    visibility: hidden;
    transform: translateY(12px);
    transition: opacity var(--dur-mid) var(--ease-out),
                transform var(--dur-mid) var(--ease-out),
                visibility var(--dur-mid);
}

.ln-pd-rota__scene.is-on {
    opacity: 1;
    visibility: visible;
    transform: none;
}

/* ── The tabs ──────────────────────────────────────────────────────────── */

/*
 * Six controls, each carrying its own progress.
 *
 * A row of dots would say which scene is showing and nothing else. These say
 * how long is left, which is the difference between a carousel that feels like
 * it is running and one that feels like it is stuck: the fill is the timer
 * made visible.
 *
 * They are buttons, not decoration — pressing one takes the scene and stops
 * the rotation, because somebody who has chosen a scene is reading it.
 */
.ln-pd-rota__tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs);
    /* Above the frame, and close to it. They are the scene's own label — a row
       of controls floating away from what they control reads as a separate
       thing rather than as this thing's heading. */
    margin-bottom: var(--space-sm);
}

.ln-pd-rota__tab {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    padding: 9px 12px 11px;
    border: 0;
    border-top: 2px solid var(--color-rule);
    background: none;
    color: var(--color-muted);
    font-family: var(--font-sans);
    font-size: 12px;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: color var(--dur-short) var(--ease-out);
}

.ln-pd-rota__tab:hover { color: var(--color-ink-soft); }
.ln-pd-rota__tab.is-on { color: var(--color-ink); }

/*
 * The fill, as a scaled pseudo-element rather than an animated width.
 *
 * transform runs on the compositor; width is a layout property and would lay
 * the row out again sixty times a second for three seconds, six times a
 * minute, forever. On a marketing page that is the kind of cost nobody
 * attributes to anything.
 */
.ln-pd-rota__tab::after {
    content: '';
    position: absolute;
    top: -2px;
    left: 0;
    right: 0;
    height: 2px;
    background: var(--color-accent);
    transform: scaleX(0);
    transform-origin: left;
}

.ln-pd-rota__tab.is-on::after {
    animation: ln-pd-fill var(--ln-pd-dwell, 3s) linear forwards;
}

/* Held while the pointer is down here: three seconds is enough to read a
   scene and not enough to read it twice. */
.ln-pd-rota.is-held .ln-pd-rota__tab.is-on::after { animation-play-state: paused; }

/* Chosen by hand, the timer stops rather than restarting — the bar fills and
   stays, so the control still reads as current. */
.ln-pd-rota.is-stopped .ln-pd-rota__tab.is-on::after {
    animation: none;
    transform: scaleX(1);
}

@keyframes ln-pd-fill {
    from { transform: scaleX(0); }
    to { transform: scaleX(1); }
}

.ln-pd-rota__tab-num {
    display: block;
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.04em;
}

/*
 * Reduced motion: no rotation and no fill, and every scene is shown.
 *
 * A carousel that a reader cannot advance and cannot see past is a carousel
 * that has hidden five sixths of the section from them. Stacking is the honest
 * fallback, and it is what the section was before it was folded into a frame.
 */
@media (prefers-reduced-motion: reduce) {
    .ln-pd-rota__frame { display: block; }

    .ln-pd-rota__scene {
        opacity: 1;
        visibility: visible;
        transform: none;
        transition: none;
    }

    .ln-pd-rota__scene + .ln-pd-rota__scene { margin-top: clamp(2rem, 4vw, 3.5rem); }
    .ln-pd-rota__tabs { display: none; }
}

/*
 * ── The API section ───────────────────────────────────────────────────────
 *
 * Two columns: what it talks to, and the request that does it.
 *
 * The reference draws a fake terminal around its sample — three traffic-light
 * dots, a title bar, near-black chrome. None of that is here. Drawn browser
 * furniture is the most reliable tell of a page written by somebody with
 * nothing real to show, and the sample stands up better as what it is: a code
 * block, labelled with the verb and path it actually calls.
 *
 * The panel is the page's one sunken surface. Everything else on this page
 * lifts off the paper; a request body is something you read *into*, so it goes
 * the other way.
 */
.ln-api {
    display: grid;
    grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
    gap: clamp(1.25rem, 2.5vw, 2rem);
    margin-top: clamp(2rem, 4vw, 3rem);
    align-items: start;
}

/* The badge above the heading. Warm, because it is a label rather than a
   statement — the same step the wordmark's numeral occupies. */
.ln-api__badge {
    margin-bottom: var(--space-2xs);
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* ── The three jobs ────────────────────────────────────────────────────── */

/*
 * Numbered steps, not cards.
 *
 * These were three bordered panels — Automation, E-commerce, Payments — and
 * they lost to the code beside them, which is the correct outcome for the
 * wrong reason: the panels were competing at all. The code is the argument
 * here; the list is the table of contents for it.
 *
 * So they are steps. A figure, a name, a line, one rule between them, and the
 * chosen one takes an accent edge. Numbered because the order is the argument:
 * send it, learn what happened to it, then scale it.
 */
.ln-api__jobs {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--color-rule-soft);
}

.ln-api__job {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-sm);
    width: 100%;
    padding: clamp(0.85rem, 1.6vw, 1.1rem) var(--space-sm);
    border: 0;
    border-bottom: 1px solid var(--color-rule-soft);
    /* A border rather than a pseudo-element, so appearing costs no layout and
       cannot shift the name a pixel sideways. */
    border-left: 2px solid transparent;
    background: none;
    text-align: left;
    cursor: pointer;
    transition: border-left-color var(--dur-short) var(--ease-out),
                background var(--dur-short) var(--ease-out);
}

.ln-api__job.is-on {
    border-left-color: var(--color-accent);
    background: color-mix(in oklab, var(--color-accent-wash) 45%, transparent);
}

/* The step number, in the identity's figure colour — the same token as the
   wordmark's numeral. Tabular so three numbers stack in a straight column. */
.ln-api__job-step {
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 11.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.6;
}

.ln-api__job-body { min-width: 0; }

.ln-api__job-name {
    display: block;
    color: var(--color-ink-soft);
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.01em;
    transition: color var(--dur-short) var(--ease-out);
}

.ln-api__job.is-on .ln-api__job-name { color: var(--color-ink); }

.ln-api__job-note {
    display: block;
    margin-top: 2px;
    color: var(--color-muted);
    font-size: 13px;
    line-height: 1.5;
}

/* ── The platforms ─────────────────────────────────────────────────────── */

/*
 * One line, under everything.
 *
 * A platform name is a reassurance, not an argument, so it gets a line rather
 * than a third of the section. Names in the page's own type, not logo files:
 * naming a company is a statement about this software, while reproducing their
 * mark is a claim about them, and nobody here has agreed to appear as an
 * integration partner.
 */
.ln-api__wired {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
}

.ln-api__wired-lead {
    margin-right: var(--space-2xs);
    color: var(--color-muted);
    font-size: 12.5px;
}

.ln-api__tag {
    padding: 4px 9px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    font-size: 12px;
    font-weight: 500;
}

/* ── The request ───────────────────────────────────────────────────────── */

.ln-api__panel {
    overflow: hidden;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
    background: var(--color-paper-sunken);
}

/*
 * The three samples, in one cell.
 *
 * Stacked in a single grid area the panel measures itself against the longest
 * request once and then holds — switching groups cannot make the page jump
 * under the pointer that is doing the switching.
 */
.ln-api__stack { display: grid; }

.ln-api__bar {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 10px 14px;
    border-bottom: 1px solid var(--color-rule-soft);
    background: var(--color-paper-raised);
}

/* The verb, in the accent. It is the one word in the panel that says what the
   request does. */
.ln-api__verb {
    flex: none;
    padding: 2px 7px;
    border-radius: var(--ln-edge-sm);
    background: var(--color-accent-wash);
    color: var(--color-accent);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.06em;
}

.ln-api__path {
    min-width: 0;
    overflow: hidden;
    color: var(--color-ink-soft);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12.5px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
 * The body.
 *
 * Scrolls inside its own box rather than pushing the page wide — a curl with a
 * bearer token on one line is longer than a phone, and the alternative to a
 * scroller here is a horizontally scrolling page.
 */
.ln-api__code {
    margin: 0;
    padding: 14px;
    overflow-x: auto;
    color: var(--color-ink-soft);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    line-height: 1.65;
    tab-size: 2;
}

.ln-api__facts {
    display: flex;
    flex-direction: column;
    gap: 7px;
    margin: 0;
    padding: 12px 14px 14px;
    border-top: 1px solid var(--color-rule-soft);
    background: var(--color-paper-raised);
}

.ln-api__fact {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-xs);
}

.ln-api__fact-label {
    flex: none;
    min-width: 5.5rem;
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.ln-api__fact-value {
    margin: 0;
    min-width: 0;
    color: var(--color-ink-soft);
    font-size: 13px;
    line-height: 1.45;
}

/* ── The way out ───────────────────────────────────────────────────────── */

.ln-api__docs {
    margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
    text-align: center;
}

.ln-api__docs-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 11px 20px;
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge);
    color: var(--color-ink);
    font-size: 14px;
    font-weight: 600;
    transition: border-color var(--dur-short) var(--ease-out),
                background var(--dur-short) var(--ease-out);
}

.ln-api__docs-link:hover {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
}

.ln-api__docs-link svg { width: 15px; height: 15px; stroke-width: 2; }

/* The count is read from the view model, which counts the endpoint partials
   the published reference actually renders. */
.ln-api__docs-count {
    padding: 2px 7px;
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-sunken);
    color: var(--color-muted);
    font-size: 11.5px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}

/* The services head is centred; it is the only section head on the page that
   is, and that is the point — two sections with identical heading treatment
   read as one long block. */
.ln-section__head--mid {
    margin-inline: auto;
    text-align: center;
}

/*
 * ── The contact form ──────────────────────────────────────────────────────
 *
 * Two columns: what happens to the message on the left, the fields on the
 * right. The left is not padding — somebody about to type their phone number
 * into a marketing page is deciding whether it is worth it, and those three
 * lines are the answer.
 *
 * The form sits on raised paper with one hairline, the same material as
 * everything else on this page. No shadow: a form that lifts off the page
 * implies a dialogue, and this is part of the page rather than something that
 * has opened on top of it.
 *
 * Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5
 * boundary: neutral long-form contact → ink full-width decision band
 */
/*
 * Contact is a band, not a column.
 *
 * It used to be the measure itself — max-width 1240px, centred — which is
 * fine while a section is transparent and wrong the moment it takes a surface:
 * a background on a centred box paints a 1240px rectangle with page either
 * side of it, which is a card, not a section. The width constraint therefore
 * moves inward to .ln-contact__inner and the section goes full-bleed.
 *
 * Sunken, because the sections above and below it — pricing and the closing
 * block — are both plain paper, and the tail's rule is that no two adjacent
 * sections share a surface. border-block draws the single hairline at each of
 * those two junctions; neither neighbour draws one, so neither is doubled.
 */
.ln-contact {
    width: 100%;
    padding: clamp(3.5rem, 9vw, 7rem) clamp(1rem, 4vw, 2.5rem) clamp(3.5rem, 6vw, 5rem);
    background: var(--color-paper-sunken);
    border-block: 1px solid var(--color-rule);
}

.ln-contact__inner {
    /* The measure, moved here from the section so the section can be a band.
       Same 1240px it always was. */
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    display: grid;
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    gap: clamp(1.75rem, 4vw, 3.5rem);
    align-items: start;
}

.ln-contact__badge {
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.ln-contact__title {
    margin-top: var(--space-xs);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(1.65rem, 3.2vw, 2.4rem);
    font-weight: 600;
    line-height: 1.12;
    letter-spacing: -0.02em;
}

.ln-contact__lede {
    max-width: 34ch;
    margin-top: var(--space-sm);
    color: var(--color-muted);
    font-size: 15px;
    line-height: 1.6;
}

.ln-contact__note {
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
    color: var(--color-muted);
    font-size: 13.5px;
    line-height: 1.55;
}

/* ── The form ──────────────────────────────────────────────────────────── */

.ln-contact__form {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: clamp(1.25rem, 2.6vw, 1.9rem);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
    background: var(--color-paper-raised);
}

/* Two fields to a row, and each one keeps its own label — a placeholder is
   not a label, and it is gone the moment somebody starts typing. */
.ln-contact__row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
}

.ln-field {
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-width: 0;
}

.ln-field__label {
    color: var(--color-ink-soft);
    font-size: 12.5px;
    font-weight: 600;
}

.ln-field__hint {
    margin-left: 3px;
    color: var(--color-muted);
    font-weight: 500;
}

/*
 * A field.
 *
 * --color-rule-control, not the decorative hairline: this is the only thing
 * marking the edge of an interactive element, and the palette measures that
 * token at 3.15:1 against paper for exactly this reason. The softer rules used
 * between rows sit at 1.2:1 and would leave the inputs looking like unlined
 * gaps.
 */
.ln-field__input {
    width: 100%;
    min-width: 0;
    padding: 11px 13px;
    border: 1px solid var(--color-rule-control);
    border-radius: var(--ln-edge);
    background: var(--color-paper);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: 14.5px;
    line-height: 1.4;
    transition: border-color var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out);
}

.ln-field__input::placeholder { color: var(--color-muted); }

/* The focus ring is a shadow rather than an outline so it follows the field's
   own corner radius instead of drawing a rectangle around it. */
.ln-field__input:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 18%, transparent);
}

.ln-field__input.is-wrong {
    border-color: var(--color-halt, var(--color-warm-ink));
}

.ln-field__select {
    appearance: none;
    /* Room for the chevron, and the chevron itself drawn as a background so the
       select needs no wrapper element. */
    padding-right: 34px;
    background-image: linear-gradient(45deg, transparent 50%, var(--color-muted) 50%),
                      linear-gradient(135deg, var(--color-muted) 50%, transparent 50%);
    background-position: calc(100% - 18px) 52%, calc(100% - 13px) 52%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
    cursor: pointer;
}

.ln-field__area {
    resize: vertical;
    min-height: 7rem;
}

.ln-field__error {
    color: var(--color-warm-ink);
    font-size: 12px;
    line-height: 1.4;
}

.ln-contact__send {
    align-self: flex-start;
    margin-top: var(--space-2xs);
}

/*
 * The outcome.
 *
 * Both states use the palette's own families rather than a green and a red
 * invented here — accent for the one that worked, warm for the one that did
 * not. Warm rather than a true error red because a failed send is a "try
 * again", not a fault: the enquiry has been logged either way.
 */
.ln-contact__flash {
    padding: 11px 13px;
    border: 1px solid;
    border-radius: var(--ln-edge);
    font-size: 13.5px;
    line-height: 1.5;
}

.ln-contact__flash.is-good {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
    color: var(--color-accent);
}

.ln-contact__flash.is-bad {
    border-color: var(--color-warm-line);
    background: var(--color-warm-wash);
    color: var(--color-warm-ink);
}

/*
 * The honeypot.
 *
 * Off-screen rather than display:none — a bot that skips hidden inputs is the
 * one this has to catch, and `display: none` is the first thing it checks. It
 * is aria-hidden and tabindex -1 in the markup, so nothing announces it and
 * nothing tabs into it.
 */
.ln-contact__trap {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/*
 * ── Pricing ───────────────────────────────────────────────────────────────
 *
 * A calculator, the money facts, then the rate rows — the portal's pricing
 * panel rearranged for somebody who has no account yet.
 *
 * Sunken ground with raised panels on it, which is this page's own way of
 * saying "a section that is not the one before it" — the arrangement
 * .ln-tail-api and .ln-foot use. The three levels together are what make it
 * read as a section; the half-shade alone would not.
 */
/*
 * Pricing sits on plain paper, and that is the whole point of it.
 *
 * The tail alternates between exactly two surfaces — paper and paper-sunken —
 * and no two sections in a row may take the same one. Pricing used to be
 * sunken directly under the API section, which is also sunken, so the two ran
 * together into one long grey slab with a hairline stranded in the middle of
 * it: two sections, one surface, and no way to see where one ended.
 *
 * Its neighbours carry the lines. The API panel above and the contact section
 * below are both sunken with border-block, so each junction has exactly one
 * hairline and this section needs no border of its own — giving it one would
 * double every rule it shares.
 */
.ln-pricing {
    padding-block: clamp(3.75rem, 7vw, 5.75rem);
}

/*
 * A stacked head, not .ln-tail-head.
 *
 * That one is a two-column grid whose lede is pulled up beside a short title —
 * right for "Our Services", and it opened a screen-wide hole under a two-line
 * pricing headline with the lede stranded on the far side of it.
 */
.ln-pricing__head { max-width: 46rem; }

.ln-pricing__title {
    margin-top: var(--space-xs);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(2.25rem, 1.6rem + 2.4vw, 3rem);
    font-weight: 500;
    line-height: 1.1;
    letter-spacing: -0.03em;
}

.ln-pricing__lede {
    max-width: 54ch;
    margin-top: var(--space-md);
    color: var(--color-muted);
    font-size: 15px;
    line-height: 1.6;
}

/* ── The calculator ────────────────────────────────────────────────────── */

/*
 * Pick on the left, what it comes to on the right.
 *
 * One raised panel split by a rule rather than two cards: it is a single
 * question and its answer, and two separate cards would read as two choices.
 */
.ln-calc {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    margin-top: clamp(2rem, 4vw, 3rem);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
    overflow: hidden;
}

/*
 * The control side, which is the shorter of the two columns.
 *
 * The answer beside it carries a price, four lines of what that buys, a button
 * and a footnote, so the grid stretches this one to match and leaves a stretch
 * of nothing under the presets. That gap is where the artwork goes — hence the
 * positioning context, and the floor under the height so the coins still have
 * somewhere to be if the two columns ever come out level.
 *
 * The padding stays as it is. An absolutely positioned child is laid out
 * against its ancestor's padding box, which already runs to the border edge,
 * so the artwork reaches the bottom of the card without this column giving up
 * the clearance that keeps the presets off it.
 */
.ln-calc__pick {
    position: relative;
    padding: clamp(var(--space-lg), 3vw, var(--space-xl));
    min-block-size: 21rem;
}

/* Above the artwork — which is the one child that stays behind. */
.ln-calc__pick > *:not(.ln-calc__art) { position: relative; z-index: 1; }

/*
 * The credits, in the corner the controls leave empty.
 *
 * A backdrop rather than a column: this is a dark studio shot on a black
 * vignette, and dropped into a light card at any real strength it would be the
 * loudest thing in a block whose job is to answer a price. Held faint, it
 * reads as the card being about credits rather than as a picture of some.
 *
 * Whole, not cropped — the coins are a single object and a slice through them
 * reads as texture rather than as coins. The box therefore carries the photo's
 * own 3:2, which is also what lets the mask line up: a radial centred on the
 * box is centred on the picture, and dissolves the black surround that would
 * otherwise show as a grey rectangle ruled across the card.
 *
 * Bottom-inline-end, and larger than the run of nothing this column is left
 * with — so the box does pass behind the track and the presets. That is why
 * the mask is weighted low and towards the end rather than centred: the
 * strength sits in the corner, and what crosses the controls is the faint
 * tail of it. The preset pills carry their own opaque fill and are lifted a
 * layer above (see .ln-calc__pick > *), so what actually reads over the
 * photograph is their hairline outline and the gaps between them.
 *
 * Logical inset, so in Arabic it settles into the other corner.
 */
.ln-calc__art {
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    z-index: 0;
    inline-size: min(72%, 32rem);
    aspect-ratio: 3 / 2;
    pointer-events: none;
    opacity: 0.38;
}

.ln-calc__art img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
    -webkit-mask-image: radial-gradient(76% 66% at 62% 78%, #000 30%, rgba(0, 0, 0, 0.4) 62%, transparent 88%);
    mask-image: radial-gradient(76% 66% at 62% 78%, #000 30%, rgba(0, 0, 0, 0.4) 62%, transparent 88%);
}

/* The answer side is the sunken step, so the eye lands on it without a second
   colour being introduced. */
.ln-calc__out {
    display: flex;
    flex-direction: column;
    padding: clamp(var(--space-lg), 3vw, var(--space-xl));
    background: var(--color-paper-sunken);
    border-inline-start: 1px solid var(--color-rule-soft);
}

@media (max-width: 52rem) {
    .ln-calc { grid-template-columns: minmax(0, 1fr); }

    /*
     * Stacked, there is no leftover space to fill and no reason to reserve
     * any: the answer follows straight on underneath, and a screen's worth of
     * decoration between the slider somebody is dragging and the price that
     * answers them would break the one adjacency this block depends on.
     */
    .ln-calc__pick { min-block-size: 0; }

    .ln-calc__art { display: none; }

    /* Stacked, the divider is the top edge of the answer, not its side. */
    .ln-calc__out {
        border-inline-start: 0;
        border-block-start: 1px solid var(--color-rule-soft);
    }
}

.ln-calc__legend {
    display: block;
    color: var(--color-ink);
    font-size: 13px;
    font-weight: 600;
}

.ln-calc__count-wrap {
    display: flex;
    align-items: baseline;
    gap: var(--space-2xs);
    margin-top: var(--space-sm);
}

/*
 * The typed figure is the display figure — it is set at display size rather
 * than as a form field, because on this page it is the number being read far
 * more often than it is the number being edited.
 */
.ln-calc__count {
    width: 7ch;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 1.4rem + 1.4vw, 2.25rem);
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}

/* The spinners are noise at this size; the slider is the control. */
.ln-calc__count::-webkit-outer-spin-button,
.ln-calc__count::-webkit-inner-spin-button { appearance: none; margin: 0; }
.ln-calc__count { appearance: textfield; }

.ln-calc__count:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 4px;
    border-radius: 4px;
}

.ln-calc__count-unit { color: var(--color-muted); font-size: 14px; }

.ln-calc__slider {
    display: grid;
    gap: 2px;
    margin-top: var(--space-lg);
    /* Room above the track for the readout that rides the handle. */
    padding-top: 30px;
}

.ln-calc__track { position: relative; }

/* ── The readout's geometry stays physical ──────────────────────────────
   `bubbleStyle()` in landing.js returns a pixel `left`, measured from the
   left edge of the track. A logical `inset-inline-start` here would be
   measured from the other edge in Arabic while that inline style kept
   measuring from the left, and the bubble would sit at the wrong end —
   half-mirrored, which is worse than not mirrored at all. The track itself
   carries dir="ltr" for the same reason. Same pairing as the portal's
   buy-credits slider in public/portal-assets/pages/credits.css. */

/*
 * The handle's readout: how many credits, and what they cost.
 *
 * The figure is already stated twice on this card — typed above the slider,
 * and priced in the panel alongside — and this is neither of those. It is the
 * one that answers while the handle is still moving, at the point the eye is
 * actually looking, which is the difference between a control that responds
 * and one that seems to be catching up.
 */
.ln-calc__bubble {
    position: absolute;
    bottom: calc(100% - 2px);
    /* Centres the bubble on the `left` landing.js measured; a transform
       rather than an inline nudge, so it is the same in both directions. */
    transform: translateX(-50%);
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 3px 8px;
    border-radius: 6px;
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 11.5px;
    line-height: 1.4;
    white-space: nowrap;
    pointer-events: none;
}

/* The pointer down to the handle. `--pointer` is how far the handle is from
   the bubble's own centre — zero across the middle of the track, and growing
   at the ends where the bubble has been clamped but the handle keeps going. */
.ln-calc__bubble::after {
    content: "";
    position: absolute;
    top: 100%;
    left: calc(50% + var(--pointer, 0px));
    transform: translateX(-50%);
    border: 4px solid transparent;
    border-top-color: var(--color-accent);
}

.ln-calc__bubble-count { font-weight: 700; }

/* The price is the quieter half — it follows from the amount. */
.ln-calc__bubble-price {
    opacity: 0.82;
    border-inline-start: 1px solid currentColor;
    padding-inline-start: 6px;
}

/*
 * The track, drawn rather than left to the platform.
 *
 * A native range control is grey-blue on every platform and would be the one
 * object on this page that is not in the palette. Both the WebKit and the
 * Firefox pseudo-elements are needed; there is no shorthand for them.
 */
.ln-calc__range {
    width: 100%;
    height: 22px;
    /* The user agent gives a range `margin: 2px`, which would make the track
       wrapper 4px wider than the distance the handle actually travels — and
       that wrapper is what landing.js measures to place the readout. */
    margin: 0;
    appearance: none;
    background: none;
    cursor: pointer;
}

.ln-calc__range::-webkit-slider-runnable-track {
    height: 4px;
    border-radius: 999px;
    background: var(--color-rule-track);
}

.ln-calc__range::-moz-range-track {
    height: 4px;
    border-radius: 999px;
    background: var(--color-rule-track);
}

/* Firefox can fill the travelled part; WebKit cannot without a wrapper, and a
   wrapper is not worth a gradient nobody will miss. */
.ln-calc__range::-moz-range-progress {
    height: 4px;
    border-radius: 999px;
    background: var(--color-accent);
}

.ln-calc__range::-webkit-slider-thumb {
    appearance: none;
    width: 20px;
    height: 20px;
    margin-top: -8px;
    border: 3px solid var(--color-paper-raised);
    border-radius: 999px;
    background: var(--color-accent);
}

.ln-calc__range::-moz-range-thumb {
    width: 20px;
    height: 20px;
    border: 3px solid var(--color-paper-raised);
    border-radius: 999px;
    background: var(--color-accent);
}

.ln-calc__range:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; border-radius: 999px; }

.ln-calc__scale {
    display: flex;
    justify-content: space-between;
    color: var(--color-muted);
    font-size: 11.5px;
    font-variant-numeric: tabular-nums;
}

.ln-calc__presets {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs);
    margin-top: var(--space-lg);
}

.ln-calc__preset {
    padding: 7px 14px;
    border: 1px solid var(--color-rule);
    border-radius: 999px;
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    font-family: var(--font-display);
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: border-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.ln-calc__preset:hover { border-color: var(--color-rule-control); color: var(--color-ink); }

/* Marked by its edge, not a fill: the accent is spent on the button below. */
.ln-calc__preset.is-on {
    border-color: var(--color-accent);
    color: var(--color-accent);
}

.ln-calc__preset:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ── The answer ────────────────────────────────────────────────────────── */

.ln-calc__total-label {
    color: var(--color-muted);
    font-size: 12px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.ln-calc__total {
    margin-top: var(--space-2xs);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(2.25rem, 1.8rem + 1.8vw, 3rem);
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}

.ln-calc__buys {
    display: grid;
    gap: var(--space-xs);
    margin-top: var(--space-lg);
    padding-top: var(--space-lg);
    border-top: 1px solid var(--color-rule);
}

.ln-calc__buy {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    color: var(--color-muted);
    font-size: 13.5px;
}

.ln-calc__buy dd {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* The free line is the best sentence in the panel, so it is the one in colour. */
.ln-calc__buy--free dd { color: var(--color-accent); }

/* margin-top:auto so the button sits on the panel floor whichever rows the
   account's rates happen to produce. */
.ln-calc__cta { width: 100%; margin-top: auto; }

.ln-calc__free {
    margin-top: var(--space-sm);
    color: var(--color-muted);
    font-size: 12.5px;
    line-height: 1.5;
    text-align: center;
}

/* ── The money facts ───────────────────────────────────────────────────── */

.ln-pricing__facts {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-sm);
    margin-top: var(--space-lg);
}

@media (max-width: 40rem) {
    .ln-pricing__facts { grid-template-columns: minmax(0, 1fr); }
}

.ln-fact {
    padding: var(--space-md) var(--space-lg);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge);
}

.ln-fact__key {
    color: var(--color-muted);
    font-size: 12px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.ln-fact__val {
    margin-top: 2px;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 1.375rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.ln-fact__link {
    display: inline-block;
    margin-top: var(--space-2xs);
    color: var(--color-accent);
    font-size: 12.5px;
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* ── The rate rows ─────────────────────────────────────────────────────── */

.ln-pricing__rates-title {
    margin-top: clamp(2rem, 4vw, 2.75rem);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* The portal panel's own row shape: mark, label with its note, value. */
.ln-pricing__rates {
    margin-top: var(--space-md);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge);
    overflow: hidden;
}

.ln-rate {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--space-md);
    align-items: center;
    padding: var(--space-md) var(--space-lg);
}

.ln-rate + .ln-rate { border-top: 1px solid var(--color-rule-soft); }

.ln-rate__icon {
    width: 20px;
    height: 20px;
    color: var(--color-ink-soft);
    stroke-width: 1.6;
}

.ln-rate__label { color: var(--color-ink); font-size: 14px; }

.ln-rate__note {
    margin-top: 1px;
    color: var(--color-muted);
    font-size: 12.5px;
    line-height: 1.45;
}

.ln-rate__value {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/*
 * A rate of zero is the best fact on this page, so it is the one that gets the
 * colour: confirmations and replies really are free, and a reader skimming
 * four grey rows would not notice.
 */
.ln-rate--free .ln-rate__value,
.ln-rate--free .ln-rate__icon { color: var(--color-accent); }

@media (max-width: 34rem) {
    /* The value drops under the label rather than squeezing the note into two
       words a line. */
    .ln-rate {
        grid-template-columns: auto minmax(0, 1fr);
        align-items: start;
    }

    .ln-rate__value { grid-column: 2; }
}

.ln-pricing__note {
    max-width: 68ch;
    margin-top: var(--space-md);
    color: var(--color-muted);
    font-size: 12.5px;
    line-height: 1.55;
}

/*
 * Hallmark · component: pricing signal rail · genre: editorial-technical
 * theme: inherited 1confirmed identity · enrichment: none
 * palette: cool paper + forest signal · anchor hue: green
 * macrostructure: signal-rail · continuous instrument + open rate ledger
 * states: default · hover · focus · active · disabled · loading · error · success
 * Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5
 * contrast: pass (40–41) · motion: control feedback only · slop: pass (1–58)
 * honest: pass (46) · chrome: pass (47) · tokens: pass (48) · icons: pass (30)
 * mobile: pass (34, 49, 50–57) · nav/footer: inherited outside component scope
 *
 * The former split card presented input and output as two separate products,
 * then filled its spare space with coin artwork. This composition is one live
 * instrument: a volume rail followed by an aligned answer rail. The fixed
 * facts and detailed rates become an open ledger underneath, matching the
 * landing page's serif/mono editorial rhythm without imitating the portal.
 */

.ln-pricing__head {
    display: grid;
    gap: var(--space-md);
    max-width: none;
    padding-block-end: clamp(var(--space-lg), 4vw, var(--space-xl));
    border-block-end: 1px solid var(--color-rule);
}

.ln-pricing__title {
    min-width: 0;
    max-width: 12ch;
    margin-block-start: 0;
    color: var(--color-ink);
    font-family: var(--font-accent);
    font-size: clamp(2.5rem, 1.9rem + 2.3vw, 4rem);
    font-style: normal;
    font-weight: 400;
    line-height: 1.08;
    letter-spacing: -0.03em;
    overflow-wrap: anywhere;
}

.ln-pricing__lede {
    max-width: 58ch;
    margin-block-start: 0;
    color: var(--color-ink-soft);
    font-size: 1rem;
    line-height: 1.65;
}

.ln-calc {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin-block-start: clamp(var(--space-xl), 5vw, var(--space-2xl));
    overflow: clip;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge-sm);
}

.ln-calc__pick {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
    min-block-size: 0;
    padding: var(--space-lg);
    background: var(--color-paper-raised);
}

.ln-calc__field {
    min-width: 0;
}

.ln-calc__legend {
    display: block;
    color: var(--color-ink-soft);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.ln-calc__count-wrap {
    display: flex;
    align-items: baseline;
    gap: var(--space-xs);
    min-width: 0;
    margin-block-start: var(--space-xs);
}

.ln-calc__count {
    width: 6.5ch;
    min-width: 0;
    padding: 0 0 var(--space-2xs);
    border: 0;
    border-block-end: 1px solid var(--color-rule-control);
    border-radius: 0;
    outline: 2px solid transparent;
    outline-offset: 2px;
    background: transparent;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(2rem, 1.55rem + 2vw, 3rem);
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
}

.ln-calc__count:focus-visible {
    outline-color: var(--color-accent);
    border-radius: var(--ln-edge-sm);
}

@media (hover: hover) and (pointer: fine) {
    .ln-calc__count:hover {
        border-block-end-color: var(--color-ink-soft);
    }
}

.ln-calc__count:active {
    border-block-end-color: var(--color-accent);
}

.ln-calc__count:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.ln-calc__count[aria-invalid="true"],
.ln-calc__count[data-state="error"] {
    border-block-end-color: var(--color-warm-ink);
}

.ln-calc__count[aria-busy="true"] {
    cursor: progress;
}

.ln-calc__count[data-state="success"] {
    border-block-end-color: var(--color-live);
}

.ln-calc__count-unit {
    color: var(--color-muted);
    font-size: 0.875rem;
    white-space: nowrap;
}

.ln-calc__slider {
    display: grid;
    align-self: end;
    gap: var(--space-2xs);
    min-width: 0;
    margin-block-start: 0;
    padding-block-start: 0;
}

.ln-calc__track {
    position: relative;
    min-width: 0;
}

.ln-calc__range {
    width: 100%;
    min-height: 2.75rem;
    margin: 0;
    appearance: none;
    background: transparent;
    cursor: pointer;
}

.ln-calc__range::-webkit-slider-runnable-track {
    height: var(--space-2xs);
    border-radius: var(--radius-pill);
    background: linear-gradient(
        to right,
        var(--color-accent) 0 var(--ln-progress, 0%),
        var(--color-rule-track) var(--ln-progress, 0%) 100%
    );
}

.ln-calc__range::-moz-range-track {
    height: var(--space-2xs);
    border-radius: var(--radius-pill);
    background: var(--color-rule-track);
}

.ln-calc__range::-moz-range-progress {
    height: var(--space-2xs);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.ln-calc__range::-webkit-slider-thumb {
    width: 1.25rem;
    height: 1.25rem;
    margin-block-start: -0.5rem;
    appearance: none;
    border: 0.1875rem solid var(--color-paper-raised);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.ln-calc__range::-moz-range-thumb {
    width: 1.25rem;
    height: 1.25rem;
    border: 0.1875rem solid var(--color-paper-raised);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.ln-calc__range:focus-visible {
    outline: 0;
}

.ln-calc__range:focus-visible::-webkit-slider-thumb {
    outline: 2px solid var(--color-accent);
    outline-offset: var(--space-2xs);
}

.ln-calc__range:focus-visible::-moz-range-thumb {
    outline: 2px solid var(--color-accent);
    outline-offset: var(--space-2xs);
}

.ln-calc__range:active::-webkit-slider-thumb {
    transform: scale(0.94);
}

.ln-calc__range:active::-moz-range-thumb {
    transform: scale(0.94);
}

.ln-calc__range:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.ln-calc__range[aria-busy="true"] {
    cursor: progress;
}

.ln-calc__range[aria-invalid="true"]::-webkit-slider-thumb,
.ln-calc__range[data-state="error"]::-webkit-slider-thumb {
    background: var(--color-warm-ink);
}

.ln-calc__range[aria-invalid="true"]::-moz-range-thumb,
.ln-calc__range[data-state="error"]::-moz-range-thumb {
    background: var(--color-warm-ink);
}

.ln-calc__range[data-state="success"]::-webkit-slider-thumb {
    outline: 2px solid var(--color-live);
    outline-offset: var(--space-2xs);
}

.ln-calc__range[data-state="success"]::-moz-range-thumb {
    outline: 2px solid var(--color-live);
    outline-offset: var(--space-2xs);
}

.ln-calc__scale {
    display: flex;
    justify-content: space-between;
    gap: var(--space-sm);
    color: var(--color-muted);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

.ln-calc__presets {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-self: end;
    gap: var(--space-xs);
    min-width: 0;
    margin-block-start: 0;
}

.ln-calc__preset {
    min-width: 0;
    min-height: 2.75rem;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-block-end: 2px solid var(--color-rule);
    border-radius: 0;
    background: transparent;
    color: var(--color-ink-soft);
    font-family: var(--font-display);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                transform var(--dur-tick) var(--ease-out);
}

.ln-calc__preset:hover {
    border-color: var(--color-rule);
    color: var(--color-ink-soft);
}

@media (hover: hover) and (pointer: fine) {
    .ln-calc__preset:hover {
        border-color: var(--color-rule-control);
        color: var(--color-ink);
    }
}

.ln-calc__preset.is-on,
.ln-calc__preset.is-on:hover {
    border-color: var(--color-accent);
    color: var(--color-accent);
    font-weight: 600;
}

.ln-calc__preset:active {
    transform: translateY(1px);
}

.ln-calc__preset:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.ln-calc__preset:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.ln-calc__preset[aria-busy="true"],
.ln-calc__preset.is-loading {
    cursor: progress;
}

.ln-calc__preset[data-state="error"],
.ln-calc__preset.is-error {
    border-color: var(--color-warm-ink);
    color: var(--color-warm-ink);
}

.ln-calc__preset[data-state="success"],
.ln-calc__preset.is-success {
    border-color: var(--color-live);
    color: var(--color-live);
}

.ln-calc__out {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    padding: 0;
    background: var(--color-paper-sunken);
    border-block-start: 1px solid var(--color-rule);
    border-inline-start: 0;
}

.ln-calc__total-wrap,
.ln-calc__commit {
    padding: var(--space-lg);
}

/* What the `cqw` on .ln-calc__total is measured against. */
.ln-calc__total-wrap {
    container-type: inline-size;
    min-width: 0;
}

.ln-calc__total-label {
    color: var(--color-muted);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/*
 * The total is sized to fit its own cell, whatever the figure turns out to be.
 *
 * It was `clamp(2.75rem, 2.15rem + 2.5vw, 4rem)`, which asks the window how
 * big the number should be — and the window does not know how much room the
 * number has. The figure lives in a grid track whose width comes from the fr
 * distribution and does not grow for content, so at the top of the slider
 * "$1,900.00" rendered 317px wide inside a 267px cell and lay across the
 * message counts beside it.
 *
 * Two facts make this exactly solvable rather than a matter of tuning:
 *
 *   1. --font-display is IBM Plex Mono. Every glyph advances 0.6em — digits,
 *      the currency mark, the separators — so a figure's width is its length
 *      times a constant, not something only the browser can know. With the
 *      -0.05em tracking below the step is 0.55em; 0.57 leaves a hair of slack.
 *   2. The slider knows how long the figure is. `--ln-total-len` is bound to
 *      it in the template, server-rendered for the first paint and updated by
 *      Alpine on every drag.
 *
 * So: take the size the design asks for, and never more than the width can
 * hold. The `max()` is the floor — an account can be configured with a range
 * and a rate that put a fourteen-character figure here, and past some point
 * the number should stay readable and let the cell scroll rather than shrink
 * to nothing.
 *
 * This replaces the media-query ladder the old clamp needed. One rule, correct
 * at every viewport, for every value an account's own pricing can produce.
 */
.ln-calc__total {
    --ln-total-step: 0.57;

    margin-block-start: var(--space-xs);
    min-width: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: max(
        1.25rem,
        min(
            clamp(2.75rem, 2.15rem + 2.5vw, 4rem),
            calc(100cqw / (var(--ln-total-len, 9) * var(--ln-total-step)))
        )
    );
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.05em;
    font-variant-numeric: tabular-nums;
    /*
     * The last resort, below the floor above. Covering the column next door is
     * the one outcome that must not happen; a wrapped price is merely ugly.
     */
    overflow-wrap: anywhere;
}

.ln-calc__buys {
    display: grid;
    gap: 0;
    margin: 0;
    padding: 0;
    border-block: 1px solid var(--color-rule-soft);
}

.ln-calc__buy {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-md);
    min-width: 0;
    padding: var(--space-md) var(--space-lg);
    color: var(--color-muted);
    font-size: 0.875rem;
}

.ln-calc__buy + .ln-calc__buy {
    border-block-start: 1px solid var(--color-rule-soft);
}

.ln-calc__buy dt {
    min-width: 0;
}

.ln-calc__buy dd {
    flex: 0 0 auto;
    margin: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 1.125rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ln-calc__buy--free dd {
    color: var(--color-accent);
}

.ln-calc__commit {
    display: grid;
    align-content: center;
    gap: var(--space-sm);
}

.ln-calc__cta {
    width: 100%;
    min-height: 2.75rem;
    margin-block-start: 0;
    white-space: nowrap;
}

.ln-calc__free {
    margin-block-start: 0;
    color: var(--color-muted);
    font-size: 0.875rem;
    line-height: 1.55;
    text-align: start;
}

.ln-pricing__details {
    /*
     * The rate sheet's heading, as a measurement.
     *
     * The right column opens with "Per message" and the left with a rule, so
     * the two columns started on the same line and their rules did not — the
     * left rail sat a heading's worth above the row it belongs beside, and the
     * ledger read as floating rather than as the other half of the same table.
     *
     * These are the heading's own metrics, declared once here and consumed
     * both by the heading and by the offset below, so the two cannot drift
     * apart the way a hardcoded pixel value would the first time somebody
     * changes the type scale.
     */
    --ln-rates-title-size: clamp(2rem, 1.7rem + 1vw, 2.5rem);
    --ln-rates-title-leading: 1.15;
    --ln-rates-title-gap: var(--space-lg);
    --ln-rates-offset: calc(
        var(--ln-rates-title-size) * var(--ln-rates-title-leading) + var(--ln-rates-title-gap)
    );

    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(var(--space-xl), 5vw, var(--space-2xl));
    margin-block-start: clamp(var(--space-xl), 5vw, var(--space-2xl));
}

.ln-pricing__facts {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    margin: 0;
    border-block-start: 1px solid var(--color-rule);
}

.ln-fact {
    padding: var(--space-lg) 0;
    background: transparent;
    border: 0;
    border-block-end: 1px solid var(--color-rule);
    border-radius: 0;
}

.ln-fact__key {
    color: var(--color-muted);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.ln-fact__val {
    margin: var(--space-xs) 0 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 1.75rem;
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}

.ln-fact__action {
    margin: 0;
}

.ln-fact__link {
    display: inline-block;
    min-height: 2.75rem;
    margin-block-start: var(--space-xs);
    padding-block: var(--space-sm);
    color: var(--color-accent);
    font-size: 0.875rem;
    text-decoration: underline;
    text-underline-offset: var(--space-2xs);
    white-space: nowrap;
}

.ln-pricing__rate-sheet {
    min-width: 0;
}

.ln-pricing__rates-title {
    margin-block-start: 0;
    color: var(--color-ink);
    font-family: var(--font-accent);
    /* Declared on .ln-pricing__details — the left column's offset is measured
       from these, so they have one source. */
    font-size: var(--ln-rates-title-size);
    font-style: normal;
    font-weight: 400;
    line-height: var(--ln-rates-title-leading);
    letter-spacing: -0.025em;
    text-transform: none;
}

.ln-pricing__rates {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin-block-start: var(--ln-rates-title-gap);
    overflow: visible;
    background: transparent;
    border: 0;
    border-block-start: 1px solid var(--color-rule);
    border-radius: 0;
}

.ln-rate {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-md);
    min-width: 0;
    padding: var(--space-md) 0;
    border-block-end: 1px solid var(--color-rule);
}

.ln-rate + .ln-rate {
    border-block-start: 0;
}

.ln-rate__icon {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--color-ink-soft);
    stroke-width: 1.6;
}

.ln-rate__body {
    min-width: 0;
}

.ln-rate__label {
    color: var(--color-ink);
    font-size: 0.9375rem;
}

.ln-rate__note {
    margin-block-start: var(--space-2xs);
    color: var(--color-muted);
    font-size: 0.875rem;
    line-height: 1.55;
}

.ln-rate__value {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 0.875rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.ln-rate--free .ln-rate__value,
.ln-rate--free .ln-rate__icon {
    color: var(--color-accent);
}

.ln-pricing__note {
    max-width: 68ch;
    margin-block-start: var(--space-md);
    color: var(--color-muted);
    font-size: 0.875rem;
    line-height: 1.55;
}

@media (min-width: 40rem) {
    .ln-calc__pick {
        padding: var(--space-xl);
    }

    .ln-calc__presets {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    .ln-calc__out {
        grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
    }

    .ln-calc__total-wrap {
        display: grid;
        align-content: center;
        border-inline-end: 1px solid var(--color-rule-soft);
    }

    .ln-calc__buys {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        border-block: 0;
    }

    .ln-calc__buy {
        display: grid;
        align-content: center;
        gap: var(--space-xs);
        padding: var(--space-lg);
    }

    .ln-calc__buy + .ln-calc__buy {
        border-block-start: 0;
        border-inline-start: 1px solid var(--color-rule-soft);
    }

    .ln-calc__commit {
        grid-column: 1 / -1;
        grid-template-columns: minmax(12rem, 0.72fr) minmax(0, 1.28fr);
        align-items: center;
        border-block-start: 1px solid var(--color-rule-soft);
    }

    .ln-pricing__details {
        grid-template-columns: minmax(12rem, 0.58fr) minmax(0, 1.42fr);
        align-items: start;
    }

    /*
     * Only once the columns are side by side. Stacked, there is no heading
     * beside the ledger to line up with and the offset would just be a gap.
     */
    .ln-pricing__facts {
        margin-block-start: var(--ln-rates-offset);
    }
}

@media (min-width: 60rem) {
    .ln-calc__pick {
        grid-template-columns: minmax(12rem, 0.72fr) minmax(0, 1.5fr) minmax(18rem, 0.9fr);
    }

    /*
     * The output row does not share the row above it any more.
     *
     * The three tracks were 0.72 / 1.5 / 0.9, which gave the money the
     * narrowest cell on the row and the three message counts the widest —
     * backwards, since the counts are short and the price is the largest type
     * on the page. Moving 0.23fr from the counts to the money buys the figure
     * about 85px, which is the difference between the top of the slider
     * fitting at the full display size and being scaled down to reach it.
     */
    .ln-calc__out {
        grid-template-columns: minmax(12rem, 0.95fr) minmax(0, 1.27fr) minmax(17rem, 0.9fr);
    }

    .ln-calc__pick {
        align-items: end;
        gap: var(--space-xl);
    }

    .ln-calc__presets {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .ln-calc__total-wrap {
        border-inline-end: 1px solid var(--color-rule-soft);
    }

    .ln-calc__buys {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .ln-calc__commit {
        grid-column: 3;
        grid-template-columns: minmax(0, 1fr);
        align-items: center;
        border-block-start: 0;
        border-inline-start: 1px solid var(--color-rule-soft);
    }
}

@media (pointer: coarse) {
    .ln-calc__preset,
    .ln-calc__cta,
    .ln-fact__link {
        min-height: 3rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .ln-calc__preset {
        transition-duration: var(--dur-tick);
    }
}

/*
 * ── The demo page ─────────────────────────────────────────────────────────
 *
 * One screen: the reasons on the left, the form on the right, the customer
 * marks underneath.
 *
 * It does not reuse .ln-contact's grid, and the reason is the panel. On the
 * landing page the form is a bare column on the page's own paper, because it
 * is the last of eight sections and a raised card there would be a ninth
 * object competing with the eight above it. Here the form IS the page, so it
 * gets a surface of its own and the reasons sit on the paper beside it — the
 * emphasis is the other way round.
 *
 * The 0.9/1.1 split gives the form the wider half. It has six controls; the
 * reasons have three short cards and want the shorter measure anyway.
 */
/*
 * A ground that starts at the top of the page has nothing to fade from.
 *
 * .ln-ground dissolves into the dot grid over its first 150px, which is
 * correct where it begins two screens down. A page that opens with it gets
 * that gradient over its own headline instead, with the grid and the backdrop
 * canvas both showing through the content.
 */
.ln-ground--flat {
    background-image: none;
    background-color: var(--color-paper);
}

.ln-demo {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1rem, 4vw, 2.5rem) clamp(3rem, 6vw, 4.5rem);
}

/*
 * Both halves end on the same line.
 *
 * `start` let each column run to its own content, and the three cards
 * overshot the form by about 170px — the page finished twice, and the ragged
 * bottom was most of why the two halves read as unrelated.
 *
 * `stretch` makes the row as tall as the taller side and hands that height to
 * both. Each column then distributes it internally rather than being trimmed,
 * which is the part that survives translation: fr runs longer than en and ar
 * shorter, and nothing here depends on the two sides happening to measure the
 * same.
 */
.ln-demo__inner {
    display: grid;
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: clamp(2rem, 5vw, 4rem);
    align-items: stretch;
}

/*
 * One column under a tablet.
 *
 * The reasons stay above the form rather than below it: somebody who has
 * arrived on this page from a "Get a demo" link has already decided, but the
 * three cards are what makes the six fields worth filling in, and on a phone
 * the only way to put them first is to put them first.
 */
@media (max-width: 60rem) {
    .ln-demo__inner {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ── The reasons ───────────────────────────────────────────────────────── */

.ln-demo__title {
    margin-top: var(--space-xs);
    max-width: 14ch;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(2.25rem, 1.6rem + 2.4vw, 3rem);
    font-weight: 500;
    line-height: 1.1;
    letter-spacing: -0.03em;
    overflow-wrap: anywhere;
}

.ln-demo__lede {
    max-width: 40ch;
    margin-top: var(--space-md);
    color: var(--color-muted);
    font-size: 15px;
    line-height: 1.6;
}

/* The column is a stack whose last block absorbs the slack. */
.ln-demo__say {
    display: flex;
    flex-direction: column;
}

/*
 * The three cards take whatever the heading and lede leave, and split it
 * equally — grid-auto-rows: 1fr rather than three cards at their own heights,
 * so a two-line note and a three-line note still produce two cards the same
 * size instead of a staircase down the column.
 */
.ln-demo__points {
    flex: 1;
    display: grid;
    grid-auto-rows: 1fr;
    gap: var(--space-sm);
    margin-top: var(--space-xl);
}

/*
 * The same card as the panel opposite it.
 *
 * These were outlined and unfilled while the form's panel was filled white,
 * and the two halves read as two design languages sharing a page: an empty
 * bordered box on the left, a solid card on the right, both at 40px, neither
 * agreeing with the other.
 *
 * One surface for both. Raised paper with a hairline is what .ln-contact__form
 * already is everywhere else on this site, so this is the page joining the
 * system rather than a third treatment invented for it.
 */
.ln-demo__point {
    padding: var(--space-lg);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
    background: var(--color-paper-raised);
}

/*
 * Drawn in the wordmark's hand, and coloured by its own grammar.
 *
 * brand.css spends exactly three colours across the engine's twenty-second
 * loop: ink-soft for the shell and most scenes, accent for the message scene
 * and the confirm tick, warm for the review star. 1.6px is the base scene's
 * stroke; 1.75px is what the accented scenes carry.
 *
 * An earlier pass painted all three of these accent green, which put four
 * accents in one viewport beside a green submit. One pass after that made
 * them all ink, which said nothing. The tone comes from the idea now — see
 * LandingViewModel::demoPoints — so approval is the confirmed green, a reply
 * is the warm human note, and delivery is structure.
 */
.ln-demo__point-icon {
    width: 22px;
    height: 22px;
    margin-bottom: var(--space-sm);
    color: var(--color-ink-soft);
    stroke-width: 1.6;
}

.ln-demo__point-icon.is-accent { color: var(--color-accent); stroke-width: 1.75; }
.ln-demo__point-icon.is-warm { color: var(--color-warm-ink); stroke-width: 1.75; }

.ln-demo__point-name {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 1.0625rem;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.02em;
}

.ln-demo__point-note {
    margin-top: var(--space-2xs);
    color: var(--color-muted);
    font-size: 14px;
    line-height: 1.55;
}

/* ── The panel ─────────────────────────────────────────────────────────── */

/*
 * The one raised surface on the page.
 *
 * Sticky on a wide screen so the form stays with the reader while the three
 * cards scroll past it — the panel is taller than the cards, so without this
 * the right column would end first and leave the fields off-screen exactly
 * when somebody has finished reading the argument for filling them in.
 */
/*
 * Same fill, same hairline weight, same radius as the three cards beside it.
 *
 * A column rather than a block, so when the row is taller than the form the
 * slack collects in one place — under the last field, above the button —
 * instead of being spread through the gaps between fields.
 *
 * It used to be sticky above 60rem. That was for a panel taller than the
 * cards beside it; now that both sides are the same height there is nothing
 * for it to stick past, and a sticky item cannot stretch to its row.
 */
.ln-demo__panel {
    display: flex;
    flex-direction: column;
    padding: clamp(var(--space-lg), 3vw, var(--space-xl));
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--ln-edge-lg);
}

.ln-demo__panel-title {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(1.5rem, 2.4vw, 1.875rem);
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: -0.02em;
}

.ln-demo__panel-note {
    max-width: 44ch;
    margin-top: var(--space-2xs);
    color: var(--color-muted);
    font-size: 14px;
    line-height: 1.55;
}

/*
 * The form gives up its own card.
 *
 * .ln-contact__form is a surface in its own right — hairline, 40px radius,
 * raised fill — because on the landing page it sits bare on the section's
 * paper with the argument beside it. Dropped inside .ln-demo__panel it became
 * a second bordered box 40px inside the first, two rounded outlines tracing
 * each other with a strip of a third colour between them.
 *
 * The panel is the card here, so the form is just the stack of fields: no
 * edge, no fill, no padding of its own. Only the gap between fields and the
 * space under the title survive.
 */
.ln-demo__panel .ln-contact__form {
    flex: 1;
    margin-top: var(--space-lg);
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
}

/*
 * The message field takes the slack, not the button.
 *
 * Matching the two columns leaves the form shorter than its row. The first
 * attempt pushed the button to the floor with `margin-top: auto`, which put
 * every spare pixel into one dead gap between the last input and the action —
 * the form looked broken rather than tall.
 *
 * Giving it to the textarea instead is the same height in a place that wants
 * it: a "what are you trying to send" box is the one field on this form that
 * benefits from being bigger, and the button stays where it belongs, directly
 * under the last input.
 *
 * `label:last-of-type` rather than `:has(.ln-field__area)` — among the form's
 * label children the message is the last, and this needs no :has support.
 */
.ln-demo__panel .ln-contact__form > label:last-of-type {
    flex: 1;
}

.ln-demo__panel .ln-field__area {
    flex: 1;
}

/* Full width: it is the only action in here, and a button sized to its own
   label leaves a ragged edge under six full-width fields. */
.ln-demo__panel .ln-contact__send {
    width: 100%;
}

/*
 * ── The footer ────────────────────────────────────────────────────────────
 *
 * The page's last surface, and deliberately its quietest.
 *
 * Three columns beside the mark, then a base line. Nothing here animates: a
 * footer that fades in is a footer that is not there when somebody scrolls to
 * it looking for the terms.
 */

/* ── The written pages ────────────────────────────────────────────────────
 *
 * The privacy policy, the terms, and anything else stored in `pages`.
 *
 * Hallmark · component: document · genre: editorial · theme: 1confirmed
 * contrast: pass (ink on paper throughout; muted only above 12.5px)
 * mobile: 320 / 375 / 768 / 1440 — one column at every width, no scroll
 *
 * These are documents, not compositions. Everything below is about one thing:
 * making a wall of admin-authored HTML readable. No panel, no border, no card
 * — a frame around a policy makes it an object on a page instead of the page.
 *
 * The rules are typed against .ln-prose > element rather than against classes,
 * because the markup comes out of an editor and carries whatever that editor
 * emitted. Anything the styles do not name still inherits a sane paragraph.
 */

.ln-page {
    padding-block: clamp(3rem, 7vw, 5.5rem) clamp(4rem, 9vw, 7rem);
    padding-inline: clamp(1rem, 4vw, 2.5rem);
}

/*
 * 68 characters, not the page's 1240px.
 *
 * The rest of the site is laid out in columns and grids where 1240 is right.
 * A single run of continuous prose is the one thing on the site that is read
 * line after line, and a 1240px line is roughly 150 characters — about twice
 * the width at which the eye reliably finds the start of the next line.
 */
.ln-page__inner {
    width: 100%;
    max-width: 68ch;
    margin-inline: auto;
}

.ln-page__head {
    padding-bottom: var(--space-lg);
    margin-bottom: var(--space-xl);
    border-bottom: 1px solid var(--color-rule);
}

.ln-page__title {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(2rem, 1.6rem + 1.7vw, 3rem);
    font-weight: 500;
    letter-spacing: -0.035em;
    line-height: 1.08;
}

/* Small, quiet, and the first thing under the title — on a policy, when it
   last changed is the fact the reader came for. muted, not muted-soft: 12.5px
   needs 4.5:1 and the softer one measures 2.6:1 on paper. */
.ln-page__stamp {
    margin-top: var(--space-sm);
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 12.5px;
    letter-spacing: 0.04em;
}

.ln-page__empty {
    color: var(--color-muted);
    font-size: 15px;
}

/* ── The prose itself ─────────────────────────────────────────────────── */

.ln-prose {
    color: var(--color-ink-soft);
    font-size: 15.5px;
    line-height: 1.75;
}

/*
 * Headings own the space above them, never below.
 *
 * A heading belongs to what follows it, so the gap that separates it from the
 * previous section has to be larger than the gap to its own first paragraph.
 * Get that the wrong way round — equal margins top and bottom — and every
 * heading floats between two sections instead of starting one, which is the
 * single most common way a long document stops being scannable.
 */
.ln-prose :is(h1, h2, h3, h4, h5, h6) {
    margin-top: var(--space-xl);
    margin-bottom: var(--space-sm);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-weight: 500;
    letter-spacing: -0.02em;
    line-height: 1.2;
}

/* The first thing in the document does not need space above it — the header's
   rule already provides the separation. */
.ln-prose > :first-child { margin-top: 0; }

/* The stored <h1>, when a document carries its own. It takes .ln-page__title's
   size so a page that supplies its heading and one that does not look the
   same, and it is the only h1 on the page either way — see
   PageViewModel::bodyHasHeading. */
.ln-prose h1 {
    font-size: clamp(2rem, 1.6rem + 1.7vw, 3rem);
    letter-spacing: -0.035em;
    line-height: 1.08;
}

.ln-prose h2 { font-size: clamp(1.4rem, 1.25rem + 0.6vw, 1.75rem); }
.ln-prose h3 { font-size: 1.2rem; }
.ln-prose :is(h4, h5, h6) { font-size: 1.02rem; }

.ln-prose p { margin-block: var(--space-sm); }

.ln-prose :is(ul, ol) {
    margin-block: var(--space-sm);
    padding-inline-start: 1.4em;
}

.ln-prose li { margin-block: 6px; }
.ln-prose ul { list-style: disc; }
.ln-prose ol { list-style: decimal; }
.ln-prose li::marker { color: var(--color-muted); }

.ln-prose strong, .ln-prose b {
    color: var(--color-ink);
    font-weight: 600;
}

/* Underlined, not just coloured. These documents name external services and
   authorities inline; colour alone is not a link cue for a reader who cannot
   separate the two hues. */
.ln-prose a {
    color: var(--color-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-thickness: 1px;
}

.ln-prose a:hover { color: var(--color-accent-hover); }

.ln-prose a:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: 2px;
}

/* A section break in a document, drawn as one. */
.ln-prose hr {
    height: 0;
    margin-block: var(--space-xl);
    border: 0;
    border-top: 1px solid var(--color-rule);
}

.ln-prose blockquote {
    margin-block: var(--space-md);
    padding-inline-start: var(--space-md);
    border-inline-start: 2px solid var(--color-rule);
    color: var(--color-muted);
}

.ln-prose :is(code, pre) {
    font-family: var(--font-mono);
    font-size: 0.92em;
}

.ln-prose code {
    padding: 1px 5px;
    border-radius: var(--ln-edge-sm);
    background: var(--color-paper-sunken);
}

.ln-prose pre {
    /* The one thing in a document that legitimately runs past the measure.
       It scrolls inside itself rather than widening the page. */
    overflow-x: auto;
    margin-block: var(--space-md);
    padding: var(--space-md);
    border: 1px solid var(--color-rule);
    border-radius: var(--ln-edge);
    background: var(--color-paper-sunken);
}

.ln-prose pre code { padding: 0; background: none; }

.ln-prose img {
    max-width: 100%;
    height: auto;
    border-radius: var(--ln-edge);
}

/* Wide tables scroll in their own box; the page never does. */
.ln-prose table {
    display: block;
    overflow-x: auto;
    width: 100%;
    margin-block: var(--space-md);
    border-collapse: collapse;
    font-size: 14px;
}

.ln-prose :is(th, td) {
    padding: 8px 12px;
    border: 1px solid var(--color-rule);
    text-align: start;
}

.ln-prose th {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
    font-weight: 600;
}

/*
 * The editor's alignment classes, neutralised.
 *
 * The stored HTML carries Bootstrap `text-center` on its title block and on
 * some section headings — correct for the old template, which centred its
 * banner. In a left-aligned reading column a centred heading every few screens
 * reads as an error rather than as emphasis, and in Arabic a hardcoded
 * `text-left` would fight the document's own direction.
 *
 * `inherit`, so the document follows the page rather than either the class or
 * a second opinion expressed here.
 */
.ln-prose :is(.text-center, .text-left, .text-right) {
    text-align: inherit;
}
/*
 * Full width, and its own ground.
 *
 * The band spans the viewport while its contents stay on the page's 1240px
 * measure — the standard arrangement, and the reason for it here is the
 * backdrop: the dot grid and its charges are fixed to the viewport, so
 * anything below the ground that is not opaque has current running behind it.
 * A footer is the last thing on the page and should be still.
 */
/*
 * The footer is the dark one now — the ink the closing block used to wear.
 *
 * That colour was doing nothing useful as a band in the middle of the tail: it
 * interrupted the paper/sunken alternation and then handed off to a sunken
 * footer, so the page ended on a stripe. At the bottom it is not an
 * interruption, it is the floor — the last surface, under everything, which is
 * what a footer is. And it keeps the alternation intact above it, because the
 * closing block is paper and no two neighbours now share a surface.
 *
 * No border-top. Between paper and ink the edge is already the hardest
 * boundary on the page; a pale hairline across it is one stray light row of
 * pixels and nothing else.
 */
.ln-foot {
    position: relative;
    z-index: var(--z-base);
    width: 100%;
    background: var(--color-ink);
    color: var(--color-paper);
}

/*
 * Inverted by re-pointing tokens, not by rewriting rules.
 *
 * Everything in here — the note, the column names, the links, and the brand
 * logo with its twenty-second animation — is written against `--color-ink`,
 * `--color-ink-soft` and `--color-muted`. Restating each of those against a
 * dark ground would mean duplicating brand.css's logo rules into this file,
 * where they would rot the first time the logo changed. Redefining the three
 * tokens for the subtree inverts all of it at once, and the logo keeps its own
 * accent and warm strokes untouched — they were chosen to sit on ink already,
 * which is what the closing block proved.
 *
 * On the children rather than on .ln-foot itself: a custom property redeclared
 * on an element also applies to that element's own declarations, so putting
 * `--color-ink: <paper>` here would resolve the background above to paper and
 * the footer would come out white.
 *
 * muted-soft is the token the old ink band used for its secondary line, so
 * this is the pairing that already existed rather than a new one.
 */
.ln-foot > * {
    --color-ink: var(--color-paper);
    --color-ink-soft: var(--color-muted-soft);
    --color-muted: var(--color-muted-soft);
    --color-rule-soft: color-mix(in oklab, var(--color-paper) 16%, transparent);
}

/*
 * The air above the links lives here rather than as a margin on the footer.
 *
 * A margin would open a gap between the ground and this band, and the fixed
 * backdrop shows through any gap — so the space would have had charges running
 * across it. As padding it is inside the band, which means the separation is
 * made of the footer's own surface.
 */
.ln-foot__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
    gap: clamp(2rem, 5vw, 4rem);
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2.5rem);
    padding-top: clamp(3rem, 7vw, 5.5rem);
    padding-bottom: clamp(2rem, 4vw, 3rem);
}

/*
 * The mark, left-aligned with the line beneath it.
 *
 * .cl-rail__logo ships with `width: 100%` and `justify-content: center`,
 * because in the portal it sits alone at the top of a fixed-width rail. Left
 * as it came, it centred itself across the footer column — about 200px right
 * of the sentence under it, which is what made the block look ragged.
 *
 * Height alone was not enough either: the engine and the word are sized in
 * font-size and flex-basis, so a 48px logo at 34px type ignored a 30px box.
 * All four are set here, the same way .ln-nav__mark sets them.
 */
.ln-foot__mark {
    --cl-logo-size: 30px;
    width: auto;
    height: 30px;
    justify-content: flex-start;
    font-size: 22px;
}

.ln-foot__mark .cl-logo__engine { flex-basis: 30px; width: 30px; height: 30px; }
.ln-foot__mark .cl-logo__word { font-size: 17px; }

.ln-foot__note {
    max-width: 34ch;
    margin-top: var(--space-sm);
    color: var(--color-muted);
    font-size: 13px;
    line-height: 1.55;
}

.ln-foot__nav {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(1.25rem, 3vw, 2.5rem);
}

.ln-foot__col-name {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.ln-foot__list {
    display: flex;
    flex-direction: column;
    gap: 9px;
    margin-top: var(--space-sm);
}

/*
 * A link.
 *
 * --color-muted rather than ink-soft at an opacity: this is the token the
 * palette names for "present but not shouting", and it measures 4.82:1 on
 * paper. An opacity would have been a contrast cut no token can account for,
 * which is the mistake the marquee names made before they were fixed.
 */
.ln-foot__link {
    color: var(--color-muted);
    font-size: 13.5px;
    transition: color var(--dur-short) var(--ease-out);
}

.ln-foot__link:hover { color: var(--color-ink); }

.ln-foot__base {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2.5rem);
    padding-block: var(--space-md) clamp(1.5rem, 3vw, 2.5rem);
    border-top: 1px solid var(--color-rule-soft);
}

.ln-foot__copy {
    color: var(--color-muted);
    font-size: 12.5px;
}

.ln-foot__legal {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}

/*
 * Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V4
 * genre: modern-minimal · tone: restrained technical · theme: existing 1confirmed identity
 * macrostructure: Scan → Proof → Convert → Exit · palette anchor: brand green
 * enrichment: minimalist service linework · nav: preserved · footer: compact sitemap
 * motion: small route-following points over six still WebPs · reduced motion: stills
 * pre-emit critique: P5 H5 E5 S5 R5 V4
 * contrast: pass (40–41) · honest: pass (46) · chrome: pass (47)
 * tokens: pass (48) · responsive/mobile: pass (34, 49, 50–57) · icons: pass (30)
 */

.ln-tail-shell {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
    padding-inline: clamp(1rem, 4vw, 2.5rem);
}

.ln-tail-services {
    padding-top: clamp(3.75rem, 7vw, 5.75rem);
}

.ln-tail-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: end;
    gap: var(--space-lg);
    padding-bottom: clamp(1.5rem, 3vw, 2.25rem);
    border-bottom: 1px solid var(--color-rule);
}

.ln-tail-head__eyebrow {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-bottom: var(--space-sm);
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.ln-tail-head__eyebrow::before {
    content: '';
    width: 20px;
    height: 1px;
    background: currentColor;
}

.ln-tail-head__title,
.ln-tail-api__title,
.ln-tail-cta__title {
    min-width: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-style: normal;
    font-weight: 500;
    letter-spacing: -0.035em;
    overflow-wrap: anywhere;
}

.ln-tail-head__title {
    max-width: 10ch;
    font-size: clamp(2.5rem, 2.1rem + 1.7vw, 3.5rem);
    line-height: 0.98;
}

.ln-tail-head__lede {
    max-width: 38ch;
    margin-top: calc(-1 * var(--space-xl));
    margin-left: auto;
    padding-bottom: 0.35rem;
    color: var(--color-ink-soft);
    font-size: clamp(1rem, 0.92rem + 0.3vw, 1.18rem);
    line-height: 1.65;
}

/* Six connected boards, not twelve isolated cards. */
.ln-service-index {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin-top: clamp(1.5rem, 2.5vw, 2.25rem);
    border-top: 1px solid var(--color-rule);
    border-left: 1px solid var(--color-rule);
}

.ln-service-board {
    position: relative;
    isolation: isolate;
    min-height: clamp(20rem, 26vw, 22rem);
    overflow: hidden;
    padding: clamp(1.25rem, 2.6vw, 2rem);
    border-right: 1px solid var(--color-rule);
    border-bottom: 1px solid var(--color-rule);
    background: var(--color-paper-raised);
}

.ln-service-board__artwork,
.ln-service-board__veil {
    position: absolute;
    inset: 0;
    z-index: -2;
    width: 100%;
    height: 100%;
}

.ln-service-board__artwork {
    overflow: hidden;
    transform-origin: 72% 50%;
}

/*
 * The painting and its invisible point routes live in the same SVG viewBox.
 * This keeps the points registered to the artwork through every card crop.
 */
.ln-service-motion {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.ln-service-motion__signals circle {
    fill: var(--color-accent);
    stroke: color-mix(in oklab, var(--color-paper-raised) 82%, transparent);
    stroke-width: 3;
}

.ln-service-motion__signals .ln-service-motion__signal--warm {
    fill: var(--color-warm);
}

@media (prefers-reduced-motion: reduce) {
    .ln-service-motion__signals { display: none; }
}

.ln-service-board__veil {
    z-index: -1;
    background: linear-gradient(
        100deg,
        var(--color-paper-raised) 0%,
        var(--color-paper-raised) 44%,
        color-mix(in oklab, var(--color-paper-raised) 96%, transparent) 56%,
        color-mix(in oklab, var(--color-paper-raised) 36%, transparent) 78%,
        transparent 100%
    );
}

.ln-service-board__head {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    max-width: 25rem;
    padding-bottom: var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
}

.ln-service-board__range,
.ln-service-board__num {
    flex: none;
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.07em;
}

.ln-service-board__title {
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.ln-service-board__list {
    max-width: 25rem;
    margin-top: clamp(2.75rem, 5vw, 4.25rem);
}

.ln-service-board__item {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr);
    gap: var(--space-sm);
    padding-block: var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
}

.ln-service-board__item:first-child { border-top: 0; }

.ln-service-board__name {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: clamp(1rem, 0.94rem + 0.25vw, 1.12rem);
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.015em;
}

.ln-service-board__note {
    max-width: 40ch;
    margin-top: 5px;
    color: var(--color-muted);
    font-size: 13px;
    line-height: 1.5;
}

/* The API is the one proof surface in the tail. */
.ln-tail-api {
    margin-top: clamp(3.5rem, 7vw, 6.5rem);
    padding-block: clamp(3.25rem, 6vw, 5.5rem);
    border-block: 1px solid var(--color-rule);
    background: var(--color-paper-sunken);
}

.ln-tail-api__layout {
    display: grid;
    grid-template-columns: minmax(15rem, 0.72fr) minmax(0, 1.55fr);
    align-items: start;
    gap: clamp(2.5rem, 7vw, 7rem);
}

.ln-tail-api__intro {
    position: sticky;
    top: 7rem;
}

.ln-tail-api__title {
    max-width: 13ch;
    font-size: clamp(2rem, 1.7rem + 1.2vw, 2.75rem);
    line-height: 1.04;
}

.ln-tail-api__lede {
    max-width: 38ch;
    margin-top: var(--space-lg);
    color: var(--color-muted);
    font-size: 15px;
    line-height: 1.65;
}

.ln-tail-api__wired {
    margin-top: clamp(1.5rem, 3vw, 2.25rem);
    color: var(--color-ink-soft);
    font-size: 12.5px;
    line-height: 1.75;
}

.ln-tail-api__wired span {
    display: block;
    color: var(--color-muted);
}

.ln-tail-api__docs {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    margin-top: var(--space-lg);
    padding-bottom: 4px;
    border-bottom: 1px solid var(--color-accent);
    color: var(--color-ink);
    font-size: 13.5px;
    font-weight: 600;
}

.ln-tail-api__docs span {
    color: var(--color-muted);
    font-size: 11.5px;
    font-weight: 500;
}

.ln-tail-api__docs svg { width: 14px; height: 14px; }

.ln-tail-api__tabs {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    border: 1px solid var(--color-rule);
    border-bottom: 0;
    background: var(--color-paper);
}

.ln-tail-api__tab {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-height: 48px;
    padding: 10px 13px;
    border-right: 1px solid var(--color-rule);
    border-bottom: 2px solid transparent;
    color: var(--color-muted);
    font-size: 12.5px;
    font-weight: 600;
    text-align: left;
}

.ln-tail-api__tab:last-child { border-right: 0; }
.ln-tail-api__tab.is-on { border-bottom-color: var(--color-accent); color: var(--color-ink); }
.ln-tail-api__tab span { color: var(--color-warm-ink); font-family: var(--font-display); font-size: 10.5px; }

.ln-tail-api__tab:focus-visible,
.ln-tail-api__docs:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/* On the dark decision band, the primary uses a paper ring; the paper
   secondary uses the accent ring. Both stay distinct from control and band. */
.ln-tail-cta__secondary:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/* The accent button's focus ring was flipped to paper so it would show
   against the ink band. There is no ink band, so it goes back to the accent
   ring every other button on the page uses — paper on paper is no ring. */

.ln-tail-api__tab:active { background: var(--color-accent-wash); }
.ln-tail-api__docs:active { color: var(--color-accent); }
.ln-tail-api__tab:disabled,
.ln-tail-api__docs[aria-disabled='true'] { opacity: 0.55; cursor: not-allowed; }

.ln-tail-api__panel {
    overflow: hidden;
    border: 1px solid var(--color-rule);
    background: var(--color-paper-raised);
}

.ln-tail-api__stack { display: grid; }

/*
 * The switch.
 *
 * The samples swapped instantly — opacity 0 to 1 with nothing between them —
 * so pressing a tab replaced the panel's contents in a single frame and the
 * only way to tell it had worked was to read the new path.
 *
 * The incoming request rises into place instead, which is the gesture the rest
 * of this page already uses for something arriving.
 *
 * ## The opacities are linear, and the same length
 *
 * That is the part that makes it smooth, and it took modelling the curve to
 * see. Both samples share one grid cell, so what a reader perceives is the sum
 * of the two opacities — and any eased fade dips in the middle. On the first
 * pass the outgoing ran 130ms against the incoming's 300ms and the sum fell to
 * 0.55 a third of the way through: the panel visibly emptied before it
 * refilled, which is exactly what reads as a jump rather than a dissolve.
 * Matching the durations and making both linear holds the sum at 1 throughout,
 * so nothing ever thins.
 *
 * The rise is the one thing that stays eased and outlasts the fade — 460ms
 * against 280ms — so the new request is fully opaque well before it finishes
 * settling. The movement is the finish, not the entrance.
 *
 * The transform delay on the way out is deliberate. Without it the leaving
 * sample slides back down as it fades, which reads as the panel dropping its
 * contents; delayed past its own fade, it snaps back invisibly and only ever
 * travels on the way in.
 *
 * opacity and transform only — both compositor properties, so switching costs
 * no layout. That matters here because the panel is the tallest thing in the
 * section.
 */
.ln-tail-api__sample {
    grid-area: 1 / 1;
    min-width: 0;
    opacity: 0;
    visibility: hidden;
    /*
     * 6px, not 10.
     *
     * Ten read as the panel being lifted in from below — at that distance the
     * eye follows the travel and the movement becomes the subject. Six is
     * enough to give the fade a direction and not enough to watch.
     */
    transform: translateY(6px);
    transition: opacity 280ms linear,
                transform 0ms linear 280ms,
                visibility 280ms;
}

.ln-tail-api__sample.is-on {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 280ms linear,
                transform 460ms var(--ln-ease-soft),
                visibility 280ms;
}

/*
 * The press, acknowledged on the control itself.
 *
 * Without this the only feedback for a click is 300ms away on the other side
 * of the panel, which is long enough for somebody to press again.
 */
.ln-tail-api__tab.is-on span {
    animation: ln-tail-api-tick 380ms var(--ln-ease-soft);
}

@keyframes ln-tail-api-tick {
    from { opacity: 0.4; transform: translateY(2px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .ln-tail-api__sample,
    .ln-tail-api__sample.is-on {
        transform: none;
        transition: opacity 120ms linear, visibility 120ms;
    }

    .ln-tail-api__tab.is-on span { animation: none; }
}

.ln-tail-api__bar {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 12px 16px;
    border-bottom: 1px solid var(--color-rule-soft);
    background: var(--color-paper-raised);
}

.ln-tail-api__verb {
    padding: 2px 7px;
    background: var(--color-accent-wash);
    color: var(--color-accent);
    font-family: var(--font-display);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
}

.ln-tail-api__path,
.ln-tail-api__code {
    color: var(--color-ink-soft);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.ln-tail-api__path { font-size: 12.5px; overflow-wrap: anywhere; }

.ln-tail-api__job-note {
    padding: 12px 16px 0;
    color: var(--color-muted);
    font-size: 12.5px;
    line-height: 1.5;
}

.ln-tail-api__code {
    min-height: 17rem;
    margin: 0;
    padding: 14px 16px 20px;
    overflow-x: auto;
    background: var(--color-paper-raised);
    font-size: 11.5px;
    line-height: 1.65;
    tab-size: 2;
}

/* Syntax colour comes from the landing identity: keys spend the same warm
   orange as section figures, while values use the product's green signal. */
.ln-code-key { color: var(--color-warm-ink); font-weight: 600; }
.ln-code-string { color: var(--color-accent); }
.ln-code-value { color: var(--color-warm-ink); }
.ln-code-comment { color: var(--color-muted); }

.ln-tail-api__facts {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    margin: 0;
    border-top: 1px solid var(--color-rule);
    background: var(--color-paper);
}

.ln-tail-api__fact {
    padding: 14px;
    border-right: 1px solid var(--color-rule-soft);
}

.ln-tail-api__fact:last-child { border-right: 0; }

.ln-tail-api__fact dt {
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.ln-tail-api__fact dd {
    margin: var(--space-2xs) 0 0;
    color: var(--color-ink-soft);
    font-size: 12px;
    line-height: 1.45;
}

/*
 * A closing decision, not the bottom row of the contact form. The ink surface
 * spans the viewport while the copy remains on the shared 1240px measure.
 * The full-width silhouette is what makes this read as a new section; boxing
 * the inner content would turn it into another card.
 */
/*
 * The closing block is a section like the others now, not a dark band.
 *
 * It was --color-ink, which made it the only inverted surface on the page and
 * broke the tail's two-surface alternation twice over: a third colour where
 * there are meant to be two, and then a sunken footer directly under it, so
 * the page ended on a stripe rather than on a rhythm.
 *
 * On paper it takes the same surface as pricing, with contact's sunken band
 * between them and the sunken footer below — paper, sunken, paper, sunken,
 * straight down to the bottom of the page.
 */
.ln-tail-cta {
    padding-block: clamp(3rem, 5vw, 4.5rem);
}

.ln-tail-cta__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: clamp(2rem, 6vw, 5rem);
    padding-bottom: 0;
}

.ln-tail-cta__title {
    max-width: 18ch;
    /* Paper while the band was ink. The band is paper now, so the heading
       takes the same ink every other heading on the page takes — the rule
       above already sets it, and restating the inverse here would silently
       win. */
    color: var(--color-ink);
    font-size: clamp(1.85rem, 1.55rem + 1.2vw, 2.5rem);
    line-height: 1.08;
}

.ln-tail-cta .ln-tail-head__eyebrow { color: var(--color-warm); }

.ln-tail-cta__note {
    max-width: 48ch;
    margin-top: var(--space-sm);
    /* muted, not muted-soft — the same swap .ln-catch__note documents. The
       softer of the two was legible against ink; on paper it measures 2.6:1,
       and 14px body copy needs 4.5:1. */
    color: var(--color-muted);
    font-size: 14px;
    line-height: 1.6;
}

.ln-tail-cta__actions { display: flex; gap: var(--space-xs); }

.ln-tail-cta__secondary {
    border: 1px solid var(--color-rule-control);
    background: var(--color-paper-raised);
    color: var(--color-ink);
}

@media (hover: hover) and (pointer: fine) {
    .ln-tail-cta__secondary:hover {
        border-color: var(--color-accent);
        color: var(--color-accent);
        transform: translateY(-1px);
    }
}

.ln-tail-cta__secondary:active { transform: translateY(1px); }

@media (max-width: 64rem) {
    .ln-tail-api__layout { grid-template-columns: minmax(13rem, 0.7fr) minmax(0, 1.3fr); gap: 2.5rem; }
    .ln-tail-api__intro { position: static; }

    /* The form goes under the argument rather than beside it. */
    .ln-contact__inner { grid-template-columns: minmax(0, 1fr); }

    .ln-foot__inner { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 48rem) {
    .ln-tail-head,
    .ln-tail-api__layout,
    .ln-tail-cta__inner { grid-template-columns: minmax(0, 1fr); }

    .ln-tail-head { align-items: start; gap: var(--space-lg); }
    .ln-tail-head__title { font-size: clamp(2.5rem, 11vw, 3.5rem); }
    .ln-tail-head__lede { margin-top: 0; margin-left: 0; padding-bottom: 0; }

    .ln-service-index { grid-template-columns: minmax(0, 1fr); }
    .ln-service-board { min-height: 22rem; }
    .ln-service-board__veil {
        background: linear-gradient(
            100deg,
            var(--color-paper-raised) 0%,
            var(--color-paper-raised) 44%,
            color-mix(in oklab, var(--color-paper-raised) 96%, transparent) 56%,
            color-mix(in oklab, var(--color-paper-raised) 36%, transparent) 78%,
            transparent 100%
        );
    }

    .ln-tail-api__layout { gap: 2rem; }
    .ln-tail-api__tabs { display: flex; overflow-x: auto; }
    .ln-tail-api__tab { flex: 0 0 auto; min-width: 10rem; }
    .ln-tail-api__facts { grid-template-columns: minmax(0, 1fr); }
    .ln-tail-api__fact { border-right: 0; border-bottom: 1px solid var(--color-rule-soft); }
    .ln-tail-api__fact:last-child { border-bottom: 0; }
    .ln-tail-api__code { min-height: 15rem; }

    .ln-contact {
        padding-bottom: 3rem;
    }

    .ln-tail-cta {
        padding-block: 2.75rem 3rem;
    }

    .ln-tail-cta__inner {
        align-items: start;
        gap: var(--space-lg);
    }

    .ln-foot__nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/*
 * The request says that it scrolls, the way the journey strip does.
 *
 * On a phone each sample is wider than its panel and scrolls sideways inside
 * it with no sign that it can: the first line stopped at "/api/v1/mess"
 * against the panel's edge and read as cut off. The edge with more beyond it
 * fades instead, driven by the sample's own scroll position, on the strip's
 * keyframes. Where the code fits the timeline is inactive and nothing applies.
 *
 * Mirrored in Arabic, where the sample starts at the right.
 */
@supports (animation-timeline: scroll()) {
    @media (max-width: 48rem) {
        .ln-tail-api__code {
            -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            animation: ln-film-switcher-fade linear both;
            animation-timeline: scroll(self inline);
        }

        [dir="rtl"] .ln-tail-api__code {
            -webkit-mask-image: linear-gradient(to left, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            mask-image: linear-gradient(to left, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
        }
    }
}

@media (max-width: 30rem) {
    .ln-tail-services { padding-top: 3rem; }
    .ln-tail-head__title { font-size: clamp(2.35rem, 10.5vw, 3rem); }

    .ln-service-board {
        min-height: 21rem;
        padding: var(--space-sm) var(--space-md);
    }

    .ln-service-board__head { padding-bottom: var(--space-sm); }
    .ln-service-board__list { margin-top: 2.25rem; }
    .ln-service-board__item { padding-block: var(--space-sm); }
    .ln-service-board__name { font-size: 0.98rem; }
    .ln-service-board__note { font-size: 12px; line-height: 1.45; }

    .ln-tail-cta__actions { align-items: stretch; flex-direction: column; }
    .ln-tail-cta__actions .ln-btn { justify-content: center; width: 100%; }

    .ln-foot__inner { gap: var(--space-lg); padding-top: 2.75rem; padding-bottom: 1.75rem; }
    .ln-foot__nav { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-lg) var(--space-md); }
    .ln-foot__col:last-child { grid-column: 1 / -1; }
    .ln-foot__list { gap: 2px; }
    .ln-foot__link { display: inline-flex; align-items: center; min-height: 36px; }
    .ln-foot__base { align-items: flex-start; flex-direction: column; }
}

/*
 * ── The loading screen ────────────────────────────────────────────────────
 *
 * The C draws itself, the mark assembles around it, and the sheet lifts away.
 *
 * ## Why the dismissal is CSS and not JavaScript
 *
 * A loader taken down by a `load` listener is a white screen forever if the
 * bundle 404s; one taken down by Alpine is a white screen forever if Alpine
 * throws. This one leaves on a `forwards` animation, so it goes whether or not
 * a single line of script runs. The worst case is that it stays 1.5s.
 *
 * JavaScript can only shorten it — `is-ready` on <html> cuts to a 260ms fade.
 * It cannot extend it and cannot cancel it.
 *
 * The overlay paints above everything including the sticky header, so it takes
 * the top of the stacking order rather than a value it shares with anything.
 */
.ln-boot {
    position: fixed;
    inset: 0;
    z-index: var(--z-toast);
    display: grid;
    place-items: center;
    background: var(--color-paper);
    /*
     * The failsafe, not the plan.
     *
     * The plan is `is-ready` below: the overlay goes when the page has actually
     * loaded, which is what a loading screen is for. This delay exists only for
     * the case where that flag never arrives — a 404'd bundle, a thrown Alpine,
     * a browser that never fires `load`.
     *
     * Three seconds, which is a compromise with a cost either way: too short
     * and a genuinely slow page loses its loader early, too long and a broken
     * one shows a blank sheet. Three is past the point where a page that is
     * going to load has loaded.
     */
    animation: ln-boot-lift 420ms var(--ease-in) 3s forwards;
}

/*
 * ?boot=hold — development only.
 *
 * Cancels both dismissals so the loader can be looked at. See
 * LandingViewModel::bootHeld; delete both once it is settled.
 */
.ln-boot.is-held,
html.is-ready .ln-boot.is-held {
    animation: none;
}

/* Loaded before the hold is up: cut to a short fade rather than sitting on a
   finished page for the rest of the second. */
html.is-ready .ln-boot {
    animation: ln-boot-lift 260ms var(--ease-in) forwards;
}

@keyframes ln-boot-lift {
    to {
        opacity: 0;
        visibility: hidden;
        /* pointer-events go with it, so the finished page is never covered by
           an invisible sheet that still swallows the first click. */
        pointer-events: none;
    }
}

/*
 * The mark: the numeral and the engine, without the word.
 *
 * "1C" is the logo's own abbreviation — the part that reads as the identity at
 * a glance. The word was dropped because it is static type, and setting it
 * beside the one thing on screen that moves made the animation the small
 * detail in a wordmark.
 *
 * Baseline-aligned rather than centred: the numeral is a glyph with a baseline
 * and the engine is a box, so centring them puts the 1 visibly high against
 * the C's bowl. The nudge is on the numeral because the engine is the fixed
 * thing here.
 */
/*
 * The mark: the numeral and the engine, without the word.
 *
 * "1C" is the logo's own abbreviation — the part that reads as the identity at
 * a glance. The word was dropped because it is static type, and setting it
 * beside the one thing on screen that moves made the animation the small
 * detail in a wordmark.
 */
.ln-boot__mark {
    /* One size drives both, so the numeral, the engine and the pull that
       closes the space between them can never be adjusted out of step. */
    --ln-boot-engine: clamp(4.25rem, 11.5vw, 6.5rem);

    position: relative;
    display: flex;
    align-items: center;
    /* Zero. The space between the two is handled by the pull below, which is
       the only one of the two that knows how much dead space to remove. */
    gap: 0;
    animation: ln-boot-settle 4.5s var(--ease-out) infinite;
}

/*
 * The mark steps aside for the name.
 *
 * The word is out of flow, so without this the finished lockup would hang off
 * to the right of centre. Shifting the mark by half the word's overhang puts
 * the whole thing back on the centre line — measured as 231px of overhang
 * against a 104px engine, so 1.111 of the engine's width.
 *
 * This number moves whenever the word's font-size does, because the width it
 * is half of is the word's. Change one and re-measure the other; they are two
 * halves of one setting.
 *
 * A ratio and not a pixel value because the word's size derives from
 * --ln-boot-engine too: change the one variable and the mark, the numeral, the
 * word and this shift all move together. The one thing it cannot survive is
 * the display face failing to load, since a fallback's metrics would make the
 * word a different width — the lockup would sit a few pixels off centre, which
 * is the right failure for a loading screen to have.
 *
 * Timed to the word exactly, so nothing slides while nothing is arriving.
 */
@keyframes ln-boot-settle {
    0% { opacity: 0; transform: translateX(0); }
    4%, 70% { opacity: 1; transform: translateX(0); }
    78%, 92% { opacity: 1; transform: translateX(calc(var(--ln-boot-engine) * -1.111)); }
    97% { opacity: 0; transform: translateX(calc(var(--ln-boot-engine) * -1.111)); }
    100% { opacity: 0; transform: translateX(0); }
}

/*
 * The numeral, sized off its own bounds.
 *
 * The box is the stroke's bounding box now (see the viewBox in the component),
 * so height is the numeral's real height rather than the height of a square
 * grid it happened to sit inside. That is also what closed the gap: the old
 * 24-unit box carried about 11 units of empty space to the right of the stem,
 * which rendered as a hole between the 1 and the C.
 *
 * Matched to the engine's *drawn* height, not its box.
 *
 * Those are two different numbers and using the wrong one is what went wrong
 * twice here. The engine's box is 128px but its shell only draws 91 of them —
 * the rest is the inset inside its 24-unit grid. So a numeral box set to the
 * engine's box renders a 1 that is a third taller than the C beside it, which
 * is what 131% looked like: a numeral with a logo attached rather than the two
 * halves of one mark.
 *
 * The height here is solved backwards from the ink: the numeral's own box is
 * 19.6 units for 16.2 units of stroke, so the box has to be about 121% of the
 * height the stroke should end up. 91 x 1.21 is where 6.9rem comes from.
 */
.ln-boot__numeral {
    flex: none;
    /*
     * Derived from the engine rather than clamped on its own.
     *
     * Both were separate clamps and that is a drift waiting to happen: resize
     * one and the two stop matching at some widths and not others, which is
     * the hardest kind of layout bug to see. 0.8625 is the ratio that puts the
     * numeral's stroke at the shell's drawn height — solved once, and now the
     * only number anybody has to change is --ln-boot-engine.
     */
    height: calc(var(--ln-boot-engine) * 0.8625);
    aspect-ratio: 9 / 19.6;
    overflow: visible;
}

/*
 * The name, arriving last.
 *
 * Absolutely positioned, and that is the load-bearing part: in flow it would
 * widen the mark the moment it appeared, so either the 1C jumps left when the
 * word arrives or it sits off-centre for the three quarters of the cycle the
 * word is absent. Out of flow it costs no layout, and the whole lockup is
 * re-centred by shifting the mark instead — one transform, on the compositor,
 * against a width that cannot change because the word's size is derived from
 * the same variable everything else here is.
 */
.ln-boot__word {
    position: absolute;
    left: 100%;
    top: 50%;
    margin-left: calc(var(--ln-boot-engine) * -0.06);
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: calc(var(--ln-boot-engine) * 0.52);
    font-weight: 700;
    line-height: 1;
    white-space: nowrap;
    /*
     * Static, not animated.
     *
     * This used to ride on the wrapper's own keyframes, and when the letters
     * took over the animating the transform went with it — the word dropped to
     * `top: 50%` with nothing pulling it back up, so it sat with its top edge
     * on the mark's centre line, 200px below where it belonged. Centring is not
     * animation state and does not belong in a keyframe.
     */
    transform: translateY(-50%);
}

/*
 * A letter.
 *
 * Each one carries its own delay, derived from its index, so the word types
 * rather than fades. 42ms apart: fast enough to read as one gesture and slow
 * enough that the eye catches the order — under about 30ms the eight letters
 * arrive as a single flash, which is just a fade with extra steps.
 *
 * The delay is on an infinite animation, so every letter is phase-shifted by a
 * constant and the stagger repeats identically on every loop rather than
 * drifting.
 *
 * They do not animate out. The mark's own fade at 97% takes the whole lockup,
 * and eight letters leaving one at a time is a different, slower gesture than
 * the one that brought them in.
 */
.ln-boot__char {
    display: inline-block;
    opacity: 0;
    animation: ln-boot-char 4.5s var(--ease-out) infinite;
    animation-delay: calc(var(--c) * 42ms);
}

/*
 * In at seven tenths, held to the end.
 *
 * The engine's story runs 4.5s and the numeral is drawn in its first fifth, so
 * by 70% both have been watched and the name is what is left to say. Each
 * letter takes 4% of the cycle to arrive — 180ms — and the eight of them are
 * complete by about 86%.
 */
@keyframes ln-boot-char {
    0%, 70% { opacity: 0; transform: translateY(0.14em); }
    74%, 100% { opacity: 1; transform: none; }
}

/*
 * The pen.
 *
 * 23.2 is the path's real length, read with getTotalLength() rather than
 * estimated. The first pass left a --ln-1-len custom property with a 24
 * fallback and never set it, so every frame was computed against a length 3%
 * long — which does not look broken, and is exactly the kind of thing that
 * survives a review. Measured, then written down.
 *
 * The animation walks the dash from one end of the path to the other, and
 * because the path is authored in writing order that reads as a hand drawing
 * the numeral: up the flag, then down the stem.
 *
 * The weight is deliberately heavier than the engine's 1.7. The engine is a
 * fine outline of a shape; this is a mark being made, and a pen that matches
 * the outline weight reads as another outline rather than as a stroke.
 */
.ln-boot__stroke {
    fill: none;
    stroke: var(--color-warm-ink);
    stroke-width: 3.4;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 23.2;
    stroke-dashoffset: 23.2;
    animation: ln-boot-write 4.5s var(--ease-out) infinite;
}

/*
 * Drawn in the first fifth, then held to the end of the cycle.
 *
 * It used to un-draw over the last 6%, which put the pen running backwards up
 * the stem while the word beside it was still fading out — sampled at 95% of
 * the cycle the numeral was 31% erased. Now it simply holds, and the snap back
 * to a full dashoffset happens at the loop boundary, where .ln-boot-settle has
 * already taken the whole mark to zero opacity. The reset is real; it is just
 * never on screen.
 *
 * Tied to the engine's 4.5s so the two restart together — a numeral on its own
 * clock drifts against the story beside it and the pair stops reading as one
 * mark.
 */
@keyframes ln-boot-write {
    0% { stroke-dashoffset: 23.2; }
    18%, 100% { stroke-dashoffset: 0; }
}

/*
 * The engine, pulled left by exactly its own dead space.
 *
 * brand-engine is a shared component on a 24x24 grid, and its shell's bounding
 * box starts at x 3.4 — a 14.2% inset that renders as 18px of nothing on the
 * left at this size. That is what sat between the 1 and the C, and it is why
 * closing the flex `gap` did not close it: the gap was already small and the
 * hole was inside the engine's own box.
 *
 * The viewBox cannot be tightened the way the numeral's was, because the
 * component is shared with the header, the rail and the media slot. So the
 * inset is cancelled here instead, as a fraction of the engine's own width,
 * which holds at every size the clamp produces rather than at the one width a
 * pixel value would have been measured at.
 *
 * -0.085 rather than the full -0.142 the inset measures. Removing all of it
 * closed the ink gap to 12px and the numeral's stem crossed into the C's bowl:
 * the two shapes are a slab and a circle, so their bounding boxes touching is
 * already an optical overlap. What is wanted is the two sitting close enough
 * to read as one mark and far enough apart to read as two shapes.
 */
.ln-boot__engine {
    flex: none;
    width: var(--ln-boot-engine);
    height: var(--ln-boot-engine);
    margin-left: calc(var(--ln-boot-engine) * -0.085);
}

/*
 * The engine, as the header carries it — scenes and all.
 *
 * This is the same component the header, the portal rail and the template
 * media slot animate: the C shell that never moves, and ten inner scenes
 * telling the product story one at a time — messaging, bot, flows, templates,
 * forms, OTP, real estate, stores, reviews, confirmed.
 *
 * ## The one thing overridden: how fast the story runs
 *
 * brand.css runs it over twenty seconds, which is right for a mark somebody
 * glances at all day and wrong for a screen that exists for about a second: at
 * 20s a reader sees a twentieth of one scene — a shape that appears, does not
 * resolve, and is gone. Every keyframe in brand.css is a percentage, so a
 * single duration override compresses the whole story proportionally without
 * touching a step of it.
 *
 * 4.5s here. The loader holds for 1.08s without JavaScript and rather less
 * with it, so a reader sees roughly the first quarter — messaging into bot into
 * flows, which is three resolved scenes rather than a fifth of one.
 *
 * The header keeps its twenty seconds. This is scoped to .ln-boot precisely so
 * that speeding up a loading screen cannot speed up the identity.
 */
.ln-boot__engine .cl-logo__shell,
.ln-boot__engine .cl-logo__scene,
.ln-boot__engine .cl-logo__impact {
    animation-duration: 4.5s;
}

/*
 * Reduced motion: no loading screen at all.
 *
 * Not a static version of it — a loading screen is motion by definition, and
 * the content behind it is already in the DOM. Somebody who has asked for less
 * movement is better served by the page than by a still panel they have to
 * wait out.
 */
@media (prefers-reduced-motion: reduce) {
    .ln-boot { display: none; }
}

/* ── The entrance ─────────────────────────────────────────────────────── */

/*
 * A staggered rise, ordered by `--i` on each element.
 *
 * `backwards` fill so nothing is visible before its delay elapses — without it
 * every element paints in place first and then jumps back to start, which is
 * the flicker that makes a staggered entrance look broken.
 */
.ln-rise {
    animation: ln-rise 620ms var(--ease-out) backwards;
    animation-delay: calc(var(--i, 0) * 90ms);
}

@keyframes ln-rise {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: none; }
}

/* The delivery states arrive after the bubble they belong to, in their own
   order — the sequence is the argument, so it is paced slower than the rest
   and starts once the message is on screen. */
.ln-track__step {
    animation: ln-rise 520ms var(--ease-out) backwards;
    animation-delay: calc(700ms + var(--i) * 260ms);
}

@media (prefers-reduced-motion: reduce) {
    .ln-rise,
    .ln-track__step {
        animation: none;
    }

    /* The service names stop scrolling and wrap instead, so nothing is left
       frozen half-way off the edge. Three of the four passes go, because a
       wrapped list has no loop to be seamless for. */
    .ln-swarm { mask-image: none; -webkit-mask-image: none; }
    .ln-swarm__row { overflow: visible; }
    .ln-swarm__track { width: 100%; flex-wrap: wrap; justify-content: center; animation: none; }
    .ln-swarm__run:not(:first-child) { display: none; }
    .ln-swarm__run { flex-wrap: wrap; justify-content: center; row-gap: var(--space-xs); }

    /* One complete row remains available through native horizontal scrolling. */
    .ln-clients {
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scrollbar-width: thin;
        mask-image: none;
        -webkit-mask-image: none;
    }
    .ln-clients__track { animation: none; }
    .ln-clients__run[aria-hidden="true"],
    .ln-clients__item[aria-hidden="true"] { display: none; }
}

/*
 * Still, the row says that it scrolls.
 *
 * With the drift and its fades gone, a phone showed two marks and the third
 * cut down the middle at the screen edge, which reads as a broken row rather
 * than more to see. The journey strip's edge fade takes over: it follows the
 * reader's own scrolling and nothing else, so it adds no motion.
 */
@supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: reduce) {
        .ln-clients {
            -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            mask-image: linear-gradient(to right, transparent, #000 var(--ln-fade-start), #000 calc(100% - var(--ln-fade-end)), transparent);
            animation: ln-film-switcher-fade linear both;
            animation-timeline: scroll(self inline);
        }
    }
}

/* ── Narrower ─────────────────────────────────────────────────────────── */

@media (max-width: 64rem) {
    .ln-nav__menu { display: none; }
    .ln-nav__signin { display: none; }
    .ln-nav__cta { display: none; }
    .ln-nav__end { margin-inline-start: auto; }
    .ln-nav__burger { display: grid; place-items: center; }
    .ln-nav.is-open .ln-sheet { display: flex; }

    /*
     * Open, the menu is the whole screen.
     *
     * The sheet used to drop out of the bar and leave the page live around it.
     * On a phone that meant the page kept moving in the gap above the floating
     * bar, carried on below the last link, and scrolled under both. Open, the
     * header is fixed over the full viewport with its own ground, the page
     * behind it stops scrolling, and the bar runs flush along the top as the
     * sheet's heading rather than floating clear of it.
     */
    .ln-nav.is-open {
        position: fixed;
        inset: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
        /* The ground is the ::after below, so that it can grow; the header
           itself stays clear while it does. */
        background: transparent;
    }

    .ln-nav.is-open::before { display: none; }

    /*
     * The open menu's paper. A layer of its own, fixed over the viewport and
     * behind the bar and the links (the header is a stacking context, so -1
     * stays inside it), so it can spread out of the burger as a circle — see
     * the motion block at the end of this query.
     */
    .ln-nav.is-open::after {
        content: '';
        position: fixed;
        inset: 0;
        z-index: -1;
        background: var(--color-paper-raised);
    }

    .ln-nav.is-open .ln-nav__bar {
        width: 100%;
        margin-top: 0;
        padding-inline: clamp(1rem, 4vw, 2.5rem);
        border-color: transparent;
        border-bottom-color: var(--color-rule-soft);
        border-radius: 0;
        background: var(--color-paper-raised);
        box-shadow: none;
        backdrop-filter: none;
    }

    /* Nothing follows the sheet on the open screen, so its closing rule would
       only draw a line across the empty paper below it. Clear, too: the paper
       is the layer behind, and a ground of its own would arrive as a hard
       rectangle ahead of the circle. */
    .ln-nav.is-open .ln-sheet { border-bottom: 0; background: transparent; }

    html:has(.ln-nav.is-open) { overflow: hidden; }

    /* Open, the burger is the way out, so it shows a cross. The middle bar
       goes and the outer two meet at the centre. */
    .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars { background: transparent; }
    .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars::before { top: 0; transform: rotate(45deg); }
    .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars::after { top: 0; transform: rotate(-45deg); }

    /*
     * ── The menu, moving ────────────────────────────────────────────────
     *
     * Opening is one gesture out of the button that was pressed:
     *
     *   · the burger's bars slide together, then twist into the cross on
     *     the spring — the same three lines becoming the way out
     *   · a wave of the brand green spreads out of the burger, and the paper
     *     follows a beat behind it, so a green edge sweeps across the screen
     *     and is gone (both circles are centred on the button, measured by
     *     landing.js into --ln-menu-x / --ln-menu-y)
     *   · the links arrive one after another, each popping out of its own
     *     bottom corner like a message coming in
     *   · "Start free" lands last, on the spring
     *
     * Nothing in it looks like waiting. The menu is there at once, so the
     * button never shows a loading-like pulse, and no row carries a mark
     * that could be read as "selected" or "done" — both were tried (typing
     * dots, read ticks) and both read as state rather than motion.
     *
     * Closing sends it all away: the rows lift off upwards, the cross
     * untwists, the paper folds back into the button and the green wave
     * follows it in. The store holds the menu open for that long
     * (MENU_CLOSE_MS) before letting the page go.
     *
     * None of it runs for a reader who asked for less motion: the menu simply
     * opens and closes, on its paper, with no green.
     */
    @media (prefers-reduced-motion: no-preference) {
        .ln-nav__burger {
            transition: transform var(--dur-mid) var(--ease-spring),
                        border-color var(--dur-short) var(--ease-out);
        }

        .ln-nav__burger:active {
            transform: scale(0.9);
            transition-duration: var(--dur-tick);
        }

        /* Closed again: untwist first, then spread back into three bars. */
        .ln-nav__burger-bars {
            transition: background-color var(--dur-short) var(--ease-out) var(--dur-short);
        }

        .ln-nav__burger-bars::before,
        .ln-nav__burger-bars::after {
            transition: top var(--dur-short) var(--ease-out) var(--dur-short),
                        transform var(--dur-mid) var(--ease-out);
        }

        /* Opening: slide together first, then twist into the cross on the
           spring. */
        .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars {
            transition-delay: 0s;
        }

        .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars::before,
        .ln-nav__burger[aria-expanded="true"] .ln-nav__burger-bars::after {
            transition: top var(--dur-short) var(--ease-out),
                        transform calc(var(--dur-mid) * 1.4) var(--ease-spring) calc(var(--dur-short) * 0.8);
        }

        /* The green wave, out of the button, with the paper a beat behind. */
        .ln-nav.is-open .ln-sheet::before {
            content: '';
            position: fixed;
            inset: 0;
            z-index: -2;
            background: var(--color-accent);
            pointer-events: none;
            animation: ln-menu-grow calc(var(--dur-mid) * 2.1) var(--ease-out) both;
        }

        .ln-nav.is-open::after {
            animation: ln-menu-grow calc(var(--dur-mid) * 2.5) var(--ease-out) calc(var(--dur-tick) * 0.9) both;
        }

        /* Away: the paper folds first and the green wave follows it in. */
        .ln-nav.is-closing::after {
            animation: ln-menu-fold calc(var(--dur-mid) * 1.4) var(--ease-in-out) both;
        }

        .ln-nav.is-closing .ln-sheet::before {
            animation: ln-menu-fold calc(var(--dur-mid) * 1.4) var(--ease-in-out) calc(var(--dur-tick) * 0.6) both;
        }

        /* The rows, like messages arriving one after another. */
        .ln-sheet > :nth-child(2) { --ln-i: 1; }
        .ln-sheet > :nth-child(3) { --ln-i: 2; }
        .ln-sheet > :nth-child(4) { --ln-i: 3; }
        .ln-sheet > :nth-child(5) { --ln-i: 4; }
        .ln-sheet > :nth-child(6) { --ln-i: 5; }
        .ln-sheet > :nth-child(7) { --ln-i: 6; }
        .ln-sheet > :nth-child(8) { --ln-i: 7; }
        .ln-sheet > :nth-child(n + 9) { --ln-i: 8; }

        .ln-nav.is-open .ln-sheet > * {
            transform-origin: left bottom;
            animation: ln-sheet-arrive calc(var(--dur-mid) * 2.2) var(--ease-spring) backwards;
            animation-delay: calc(var(--dur-mid) * 0.8 + var(--ln-i, 0) * 55ms);
        }

        [dir="rtl"] .ln-nav.is-open .ln-sheet > * { transform-origin: right bottom; }

        .ln-nav.is-open .ln-sheet > .ln-sheet__cta {
            transform-origin: center;
            animation-name: ln-sheet-cta;
            animation-duration: calc(var(--dur-mid) * 2.6);
        }

        /* Sent: every row lifts off upwards, fast, bottom first, gone well
           before the folding paper can uncover them. */
        .ln-nav.is-closing .ln-sheet > *,
        .ln-nav.is-closing .ln-sheet > .ln-sheet__cta {
            animation: ln-sheet-send calc(var(--dur-tick) * 1.2) var(--ease-in) both;
            animation-delay: calc((8 - var(--ln-i, 0)) * 10ms);
        }

        /* A pressed link answers before the page moves under it. */
        .ln-sheet__link {
            transition: color var(--dur-short) var(--ease-out),
                        padding-inline-start var(--dur-mid) var(--ease-spring);
        }

        .ln-sheet__link:active {
            color: var(--color-accent);
            padding-inline-start: var(--space-sm);
            transition-duration: var(--dur-tick);
        }
    }

    /* The scene and its copy stack rather than sitting side by side: at this
       width a two-column band leaves the notes on a twenty-character measure. */
    .ln-pd-band { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 40rem) {
    .ln-nav__bar { min-height: 64px; }

    /*
     * Settled, the bar runs flush along the top of a phone.
     *
     * Floating 16px down with round corners works on a desktop, where the gap
     * is a sliver of a wide page. On a phone the page scrolled visibly in that
     * gap above the bar, and the hero's eyebrow slid under it with its first
     * letter left showing beneath the rounded corner — a lone "B" hanging
     * under the logo. Flush and edge to edge there is no gap and no corner for
     * anything to show through.
     */
    .ln-nav.is-settled .ln-nav__bar {
        width: 100%;
        margin-top: 0;
        padding-inline: clamp(1rem, 4vw, 2.5rem);
        border-color: transparent;
        border-bottom-color: var(--color-rule-soft);
        border-radius: 0;
        background: color-mix(in oklab, var(--color-paper-raised) 94%, transparent);
    }

    /* Paired contact fields still have comfortable measures on tablets; they
       become single-column only when two usable controls no longer fit. */
    .ln-contact__row { grid-template-columns: minmax(0, 1fr); }

    /* 16px, for the email pill's reason below: iOS zooms the page into any
       field set smaller, and the contact fields were 14.5px, so on the demo
       page a tap into the form zoomed the whole page in. */
    .ln-field__input { font-size: 16px; }

    /* The demo page's card titles wrap at this width, and a greedy wrap left
       "approved" alone under "Templates Meta has" on a 360px phone. Balanced,
       the two lines come out even. A title that fits on one line is unchanged. */
    .ln-demo__point-name { text-wrap: balance; }

    /* The tabs stack two-up rather than six-across, where each would be
       nineteen pixels wide. */
    .ln-pd-rota__tab { flex: 1 1 44%; }

    /*
     * The pill stacks: at this width a field and a button side by side leaves
     * the field too narrow to read your own address in.
     *
     * Stacked, the container goes and the field carries its own edge. A
     * container drawn round a field above a button is a capsule two controls
     * tall, which a round skin turns into an oval with a pill inside it — and
     * its focus ring became a second oval round the first. Two pills, one under
     * the other, are the same group without the frame.
     */
    .ln-catch {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-xs);
        padding: 0;
        border: 0;
        background: none;
        box-shadow: none;
    }

    .ln-catch__field {
        padding: 13px 16px;
        border: 1px solid var(--color-rule);
        /* A pill like the button under it. The studio skin rounds the field
           to --ln-edge, which inside the desktop capsule nobody sees; stacked,
           it was a soft rectangle above a pill. */
        border-radius: 100px;
        background: var(--color-paper-raised);
        text-align: center;
        /* 16px, not 15: iOS zooms the page into any field set smaller. */
        font-size: 16px;
    }

    .ln-catch:focus-within { outline: 0; }

    /*
     * One ring, on the field's own edge.
     *
     * Offset outside the border, the ring and the grey border were two
     * rounded boxes one inside the other, and the outer one ran into the
     * button. Pulled onto the border, the stroke turns accent and thickens
     * to 2px; it stays an outline so a forced-colours mode still draws it.
     * The pill qualifier is for the specificity tie the rule above explains.
     */
    .ln-catch .ln-catch__field:focus-visible {
        border-color: var(--color-accent);
        outline: 2px solid var(--color-accent);
        outline-offset: -1px;
    }

    .ln-catch .ln-btn { width: 100%; }

    /* The dot grid is fixed, which iOS Safari treats as a repaint on every
       scroll frame. Scrolled with the page on a phone instead: the texture is
       identical at rest and the scrolling is not janky. */
    body { background-attachment: scroll; }
}

/* ── The mobile menu's motion — used by the "Narrower" block above ─────── */

@keyframes ln-menu-grow {
    from { clip-path: circle(0 at var(--ln-menu-x, calc(100% - 2.6rem)) var(--ln-menu-y, 2.2rem)); }
    to { clip-path: circle(150% at var(--ln-menu-x, calc(100% - 2.6rem)) var(--ln-menu-y, 2.2rem)); }
}

@keyframes ln-menu-fold {
    from { clip-path: circle(150% at var(--ln-menu-x, calc(100% - 2.6rem)) var(--ln-menu-y, 2.2rem)); }
    to { clip-path: circle(0 at var(--ln-menu-x, calc(100% - 2.6rem)) var(--ln-menu-y, 2.2rem)); }
}

/* A row arriving like a message: out of its own corner, with a little give. */
@keyframes ln-sheet-arrive {
    from { opacity: 0; transform: translateX(-14px) translateY(10px) scale(0.9); }
    to { opacity: 1; transform: none; }
}

@keyframes ln-sheet-cta {
    from { opacity: 0; transform: translateY(26px) scale(0.92); }
    to { opacity: 1; transform: none; }
}

/* Sent: up and away. */
@keyframes ln-sheet-send {
    from { opacity: 1; transform: none; }
    to { opacity: 0; transform: translateY(-16px) scale(0.97); }
}
