/*
 * The landing page's own treatment. Deliberately NOT calculator.css: that sheet is an app-continuity
 * sheet whose own header says it carries no font-family or colour, and whose first rule sledgehammers
 * every text-bearing descendant of .calc-page to one size with two opt-outs. A marketing front door
 * cannot live inside that, and widening those opt-outs would turn a small well-behaved sheet into a
 * general one. See DF.Claude/Features/LandingAndFirstLoad.md, open question 4.
 *
 * It declares NO button colour, and no font for anything but the hero's question wall, and both are
 * deliberate. The page inherits the app's face from app.css and its CTA is bootstrap's own
 * .btn.btn-primary, so the front door reads as the same product as the simulator rather than as a
 * separate brochure. The question wall is the one exception, and its many faces are all SYSTEM font
 * stacks: variety is the point of the wall, and not one of them downloads a byte. Colour comes from the
 * PALETTE: every value below is a ChartPalette entry that _Layout.cshtml already emits as a CSS custom
 * property, so a theme change there propagates here. There is no webfont, which keeps the cold load cheap
 * on the one page whose job is ranking.
 *
 * STRUCTURE (settled 2026-09-12): a hero, then ONE uniform stack of cards, and nothing else. Every block
 * below the hero wears the same .lp-card shell whatever it holds; the six product cards alternate which
 * SIDE the proof sits on and are identical in every other respect. The hero is the only thing on the page
 * allowed to look different, which is why the question wall (2026-09-18) lives INSIDE it.
 *
 * Everything is scoped under .lp so it cannot collide with bootstrap, app.css or icons.css, all three of
 * which _Layout loads before this file.
 *
 * ITERATING: this file and the images beside it need NO BUILD. The running server reads static files from
 * the plan's own wwwroot rather than from bin, and asp-append-version recomputes the cache-busting
 * hash per request, so an edit here is live on the next refresh. Only .cshtml and .cs changes need a
 * rebuild, because those are compiled into the assembly.
 */

.lp {
    /* Local tokens, each named for its ROLE, resolved from the palette _Layout emits. The fallbacks
       matter: a server-rendered page is served to crawlers and to the WASM client alike, and only the
       latter ever runs DF.charts.setPalette. */
    /* .lp itself paints NOTHING; the page GROUND is painted on body and the site header, in the page-ground
       section below (#FAFAFA since 2026-09-18). Two of the three grounds inside it, the card and the proof
       box, come from ChartPalette through the custom properties _Layout emits, so a theme change
       propagates here instead of stranding literals. The fallbacks exist because
       a crawler never runs DF.charts.setPalette.

       THE THIRD IS --lp-hover-bg AND IT IS A LITERAL, which is the thing this comment warns about, so it
       is stated rather than left to be discovered. It is a setup-dialog interaction state rather than a
       surface the page sits on, and nothing in ChartPalette carries "the neutral ground under a hovered
       tile": the two nearest entries are SurfaceLight (#FAFAFA, a RESTING surface and two steps lighter)
       and PillActionLight (#F0F8FF, the alice blue this state exists to avoid). The honest fix is a new
       palette entry, which is a deliberate change to ChartPalette rather than a drive-by here; until
       then it does not follow a theme, exactly as --lp-rule and --lp-card-border do not. (--lp-proof-border
       was on that list until 2026-09-18, when it became a palette entry: black then, GrayLight since
       2026-09-24.)

       One thing to know about the card's: --color-pill-action-light is a VALUE match, not a role match.
       Its palette entry is named for an action pill and a module-edit hover, and it happens to be the
       alice blue this page wants. If the palette ever repurposes that entry the card ground follows it,
       which is the price of propagation; the alternative was a literal that goes stale silently. */
    --lp-card-bg: var(--color-pill-action-light, #F0F8FF);
    /* The card's EDGE, all four sides since the spine went (2026-09-20). It has been through three
       states and the current one is the third: 1px #DDDDDD before 2026-09-19; 1px -> 2px #888888 on
       2026-09-19, when the card gained a 15px black spine and needed an edge that could hold its own
       beside it, then back to 1px when the spine went on 2026-09-20 and the black title bar became the
       card's weight; and 2px #444444 since 2026-09-23, so it holds its own against the graph paper
       behind it. */
    --lp-card-border: #444444;
    /* 2px and #444, up from 1px and #888 (user, 2026-09-23), so the card holds its own edge against the
       graph paper behind it rather than dissolving into it. It went 2px, back to 1px, and to 2px again
       with both seen on the page; the note below is why the two spellings differ.

       THE WIDTH IS SCALED AND THE 1px VERSION WAS NOT, which is not an inconsistency.
       TestLandingCardScale exempts exactly one unscaled length, a 1px hairline, on the grounds that it is
       the thinnest mark a display can make rather than a proportional quantity. 2px is a real weight, so
       it comes down with the card like every other measurement, and the guard's own token sweep catches
       `2px solid` behind a token if it does not. It renders 1.4px at the shipped 0.7. */
    --lp-card-edge: calc(2px * var(--lp-card-scale)) solid var(--lp-card-border);
    /* The edge around anything INSIDE a card: the proof box and the screenshot frame, drawn 1px since
       2026-09-20. It MATCHED the card's own edge then and deliberately does not now: the card went to
       2px #444 on 2026-09-23 to hold its own against the graph paper, which is a job an inner edge does
       not have, since it sits on the card's own fill and never on the grid. A proof framed as heavily as
       the card would read as a second card inside the first. Light grey since 2026-09-24 (user): it was
       #AAAAAA, then black from 2026-09-18 so the proof read in the title bar's ink, and the user took it
       back to a light hairline once the card itself carried the 2px #444 edge. */
    --lp-proof-border: var(--color-gray-light, #DDDDDD);
    --lp-proof-bg: var(--color-surface-light, #FAFAFA);
    --lp-ink: #0A1017;
    /* Body copy is dark on purpose. These were #34424F and ChartPalette's SlateBadge #5B6B73, which read
       washed out at this measure: SlateBadge is a BADGE colour chosen to sit quietly behind a pill, and
       borrowing it for paragraphs was the mistake. Secondary copy stays lighter than primary, but only
       just. */
    --lp-ink-soft: #1C2833;
    --lp-muted: #3E4A55;
    --lp-rule: #C9D3E0;
    --lp-rule-faint: #DFE6EF;
    /* MILLIMETRE PAPER, NOT SCHOOL SQUARED PAPER (user, 2026-09-23), and the difference is the TIER COUNT
       far more than the pitch. Squared paper is one weight at one spacing; engineering paper is three,
       a fine 1mm rule, a stronger one every 5mm and a stronger one again at the centimetre. Two tiers at
       24px read as an exercise book however faint they were made, which is what the first cut was.
       5px stands in for the millimetre (about 3.8px at 96dpi, rounded up because a 1px line on a 4px
       pitch is a quarter ink and moires on a fractionally-scaled display), so the 5mm and 1cm rules fall
       at 25px and 50px. All three are the ink colour at a low alpha rather than grey literals, so they
       sit on the ground rather than beside it.

       THESE ARE NEAR THE FLOOR, so read the next request to lighten carefully. Over the #FAFAFA ground
       the three rules land at about 245, 240 and 236 out of 255, which makes the MINOR rule a five-level
       difference: roughly the least an 8-bit sRGB display shows consistently, and less than a dim or
       glossy screen reliably will. Below about 0.02 it stops being faint and starts being ABSENT on some
       machines while still showing on the one it was tuned on. If it has to go lighter again, DROP THE
       MINOR TIER and keep the 5mm and 1cm rules: two tiers at 25px and 50px is still engineering paper,
       where an invisible tier is a value nobody can check. */
    --lp-grid-line: rgb(10 16 23 / calc(0.022 * var(--lp-paper-ink)));
    --lp-grid-medium: rgb(10 16 23 / calc(0.04 * var(--lp-paper-ink)));
    --lp-grid-major: rgb(10 16 23 / calc(0.06 * var(--lp-paper-ink)));

    /* THE PAPER IS MEASURED IN DEVICE PIXELS, AND THE ONLY WAY TO SAY THAT IN CSS IS TO DRAW IT ON A
       LAYER SCALED BY 1 / RATIO. That layer is `.lp::before` below; these four tokens are its units and
       are meaningless anywhere else, which is why they are named for it.

       WHY A CSS LENGTH CANNOT CARRY THE PITCH, which is the second thing tried here and the reason for
       the third. A 5px pitch is five DEVICE pixels only at a ratio of 1; on Windows at 150% scaling it
       is 7.5, and a tile half a device pixel long cannot repeat evenly, so the renderer puts two rules
       in one period and none in the next. Rounding the CSS length to land on whole device pixels is the
       obvious repair and it does not work, because Blink stores every length as a LayoutUnit of 1/64 CSS
       pixel: measured on this page, a snapped pitch resolves to 7.9922, 7.9893, 8.9941, 10.9688 and
       12.9609 device pixels at the five zoom levels of a 150% display, never the 8, 8, 9, 11, 13 asked
       for. The leftover thousandths accumulate, so the fast two-in-one-period beat becomes a slow band
       down the page and which zoom looks clean is a lottery. The user's report, in order: irregular at
       100%, then after the rounding "only perfect at 125%".

       WHAT THE SCALED LAYER BUYS. Inside a layer scaled by 1 / ratio the rasteriser works at exactly one
       CSS pixel per device pixel, so an INTEGER length is an exact number of device pixels and there is
       no fraction left to quantise. Measured by Scripts/measure-landing-grid.mjs, folding the rendered
       image and comparing period against period, on BOTH axes from a fractional origin: the raw 5px
       pitch reads x1.500 to x3.000 at every fractional ratio, so a period carries up to THREE times its
       neighbour's ink, and the scaled layer x1.000 to x1.054, against a control of x1.000 at a ratio of
       1. The residual is the paper's own origin, which is not ours to choose: at an origin that lands
       on a whole device pixel the shipped layer reads x1.000 exactly, and at one that lands between two
       it is a sub-level dither in how each straddling rule splits.

       THE HAIRLINE HAS TO ROUND AND THE ALPHA HAS TO PAY FOR IT. A rule can only be a whole number of
       device pixels in here, so at a 1.5 ratio it is 1 or 2, never the 1.5 that would keep the authored
       one-in-five ink. Measured as ink per DEVICE pixel of pitch, which is the density a viewer reads:

         hairline held at 1 device pixel   1.13 at a ratio of 1, 0.75 at 1.5, 0.44 at 2.625 (fades in)
         hairline rounded, alpha untouched 1.13,                 1.44,        1.33            (thickens)
         rounded and compensated, shipped  1.00,                 0.99,        1.07

       So the width rounds and --lp-paper-ink scales the alpha back by the same factor, which holds the
       density the design was tuned at while every rule stays crisp.

       THAT PUTS THE MINOR TIER'S PER-PIXEL ALPHA AT 0.0165 AT A 1.5 RATIO, under the 0.02 the comment
       above calls the floor, and it is a different shape rather than a breach of it: that floor was
       measured for a ONE device pixel rule, and this one is two device pixels wide with the same total
       ink. Crisper and wider at a lower alpha, not fainter.

       THE FOUR VALUES ARE DECLARED TWICE, AND THE FIRST SET IS NOT A DEFAULT, IT IS THE FALLBACK FOR AN
       ENGINE WITHOUT round(). A custom property is not validated when it is parsed, only when it is
       USED, so an unsupported round() does not fall back to an earlier declaration of the same token the
       way an ordinary property would: it makes `background-image` invalid at computed-value time and the
       element paints NO background at all. Measured by simulating it on the live page, ink range in a
       clean patch of paper: 0. That takes the grid AND the edge wash, on a page that rendered fine
       everywhere before this change, so an @supports gate is the price of the round() this depends on
       (Chrome 125, Firefox 118, Safari 15.4). Inside the fallback a ratio of 1 makes the scale the
       identity, the pitch 5px and the hairline 1px, which is exactly what this page rendered before any
       of this.

       --lp-device-pixel-ratio IS PUBLISHED ON <html> BY THE HEAD SCRIPT IN Landing.cshtml, and it is
       read through a var() FALLBACK rather than declared here with a default, which is the difference
       between working and not: these tokens sit on `.lp`, so a `--lp-device-pixel-ratio: 1` written in
       this block would be the element's OWN declaration and would beat the inherited one the script
       sets, pinning the page at 1 forever. Note the fallback block below DOES declare --lp-dpr as a flat
       1, deliberately: an engine that cannot round has no use for the real ratio, and reading it there
       would scale the layer while leaving the pitch in CSS pixels, which is worse than not trying. */
    --lp-grid-pitch: 5;
    --lp-dpr: 1;
    --lp-paper-pitch: calc(var(--lp-grid-pitch) * 1px);
    --lp-paper-hair: 1px;
    --lp-paper-ink: 1;
    /* The page ground the edge wash fades into, read from the same source `body` uses so the two
       cannot drift to different whites. */
    --lp-ground: var(--color-surface-light, #FAFAFA);
    /* The hover state of a tile, and NEUTRAL grey rather than the blue-tinted rule pair above, which is the
       point of it: hover has to read as "not the blue state" at a glance. The pair is a LIGHT grey ground
       under a DARK grey edge (user, 2026-09-16), so the edge does the announcing and the ground only
       separates the tile from the panel. The ground is one step DARKER than the panel it sits on
       (--lp-proof-bg is #FAFAFA), which is what makes the square visible at all: a tile rests on
       `transparent`, so without a ground of its own there is nothing between the glyph and the panel.

       Role-named rather than borrowing --lp-proof-*, and only ONE of that pair would even have matched:
       --lp-proof-border was also #AAAAAA until 2026-09-18, while --lp-proof-bg resolves to #FAFAFA, two
       steps lighter than the ground this state wants. So borrowing would have been a VALUE match on the
       edge (the trap the --lp-card-bg comment above spells out) and a wrong value on the ground, and the
       edge proved the point by turning black the day the proof frames did. */
    --lp-hover-bg: #F2F2F2;
    --lp-hover-border: #AAAAAA;

    /* The UNSELECTED tile name. It was --lp-muted (#3E4A55), which is a dark slate: against the selected
       name's --lp-ink (#0A1017) the difference was nearly invisible at 0.98rem, so a tile looked much the
       same on and off (user, 2026-09-16). This is a genuine gray, so on and off are unmistakable.

       #667079 rather than the #8A949E first shipped, for CONTRAST: this name is 0.98rem BOLD, about
       15.7px, which is under WCAG's 18.66px bold large-text threshold, so the 4.5:1 minimum applies and
       #8A949E gave 2.95:1 on the #FAFAFA panel. #667079 is 4.84:1, and nine of the fifteen choosable
       tiles render in this colour on first paint. Still unmistakably grey against the selected --lp-ink. */
    --lp-tile-name-idle: #667079;

    /* One step of the hero H1's staircase; the third line takes two. In em, so it scales with a headline
       that is itself clamp()ed across the viewport and the rhythm holds at every size. Dropped to zero on
       a phone, where the indent would push the longest line into a wrap and a wrapped staircase is worse
       than none. */
    --lp-hero-indent: 0.9em;
    /* The headline's size, lifted into a token on 2026-09-23 so the hero MARK can be sized from it. Read
       by .lp-hero h1's font-size and by .lp .lp-hero-logo's box, and by nothing else. */
    --lp-hero-size: clamp(2.6rem, 9vw, 6.5rem);

    /* The LIFT every question-wall strip casts on the page. A literal, like --lp-card-border: nothing in
       ChartPalette is a shadow. The light strips' hairline EDGE token went on 2026-09-24, when every strip
       gained a solid black border (user), which does that job on every ground at once. */
    --lp-ask-lift: 0 10px 22px -14px rgba(10, 16, 23, 0.55);

    /* Pure black, twice, for two roles (user, 2026-09-18): the SPINE, the 15px edge a card carries on the
       side its words and button sit, and the ink of a card's big FIGURE (the price, the rate, the time).
       Role-named so either can move without dragging the other. */
    /* (--lp-card-spine, the 15px edge, is DELETED as of 2026-09-20 with the spine itself.) */
    /* The card title's bar (user, 2026-09-20). Named rather than inlined as `black` / `white`, so it sits
       with every other colour in this block and a theme change has one place to look. */
    --lp-title-bar-bg: var(--color-black, #000000);
    --lp-title-bar-ink: var(--color-white, #FFFFFF);
    --lp-figure-ink: var(--color-black, #000000);
    /* (--lp-title-rule, the 2px rules above and below every card title, lived here from 2026-09-18 and
       is DELETED as of 2026-09-20: both rules are gone, so the token had no reader. Recorded rather than
       silently dropped, because a colour token with no consumer is the kind of thing that gets re-added
       from memory.) */
    /* The 6px rule over the hero subhead (user, 2026-09-19). */
    --lp-hero-rule: var(--color-black, #000000);

    /* (--lp-card-shift, how far a card stepped out toward the side its words sat on, is DELETED as of
       2026-09-20 with the step itself. Every card now aligns to the column.) */

    --lp-brand: var(--color-dark-blue, #1B3A63);
    --lp-accent: var(--color-blue-accent, #12509E);

    /* THE CARD STACK'S SCALE (user, 2026-09-21: "the cards are too big ... make everything smaller
       keeping the proportions exactly as they are now"). ONE multiplier, applied to EVERY absolute
       length inside .lp-stack, so the whole stack is a uniform reduction of itself rather than a set of
       numbers each taken down by hand that will drift apart the next time one of them moves.

       WHAT IT MULTIPLIES, and the rule is "absolute, therefore scaled": every px and every rem, the
       stack's own measure, and all three terms of every clamp() at once (clamp is monotonic
       and the factor is positive, so k * clamp(a, b, c) IS clamp(ka, kb, kc), including the vw slope).
       Scaling the vw term is not optional: leave it and the type sits at a different point on its ramp
       at every width between the floor and the ceiling, which is the proportions drifting in the one
       band nobody looks at.

       WHAT IT DOES NOT MULTIPLY, and each is a rule rather than an oversight:
         - a PERCENTAGE, a `ch`, an `em`, a `1fr`. Each is already relative to something that scaled, so
           multiplying it again would scale it twice. `min(420px, 100%)` therefore becomes
           `min(calc(420px * k), 100%)` and never `calc(k * min(420px, 100%))`, which would cap the
           image at 85% of a box that had already come down.
         - a 1px BORDER, of which the card, the figure frame and the shot mat have one each. A hairline
           is the thinnest mark the display can make, not a proportional quantity: below 1px a browser
           either rounds it away or antialiases it to a lighter grey, so scaling it changes the COLOUR
           of the edge rather than its weight. The three stay 1px at every scale.
         - the GUTTER, the air between a card and the window edge, which belongs to the page rather
           than to the card. Scaling it would only widen the card on any window narrower than the
           measure; see the .lp-stack rule, which has the measurement.
         - the HERO, which the user likes as it is. Nothing under .lp-hero reads this token, and
           TestLandingCardScale holds that: the two tokens above are pre-multiplied, so a hero rule
           reading one would silently start resizing with a knob documented as leaving it alone.

       IT IS ALSO THE ONLY NUMBER TO CHANGE, and it has already been turned once: it shipped at 0.85 and
       the user said "they are still too big" on seeing it, so it is 0.7. That round cost one character.
       Every "a bit smaller" on this stack before the token was a hand sweep of twenty-odd values (the
       2026-09-20 pass moved eight of them in one day), and a hand sweep is exactly where a proportion
       goes quietly wrong. 1 is the pre-2026-09-21 page, to the pixel.

       WHERE THE NEXT TURN RUNS OUT, measured rather than guessed, because 0.7 is already past one of
       these on one surface. On a DESKTOP at 1280 the smallest copy is the coda and the caption at
       16.6px, so 0.7 is about the last value that keeps every body string at or above 16px. On a PHONE
       at 390 the clamps are at their floors and 0.7 puts the turn at 14.4px and the coda at 13.2px,
       which is already under it: see "THE HONEST LIMIT" on .lp-stack for why a window-bound card cannot
       scale and what the one-line carve-out would be. The wide SCREENSHOTS go first of all: at 0.7 a
       16px in-app label renders about 7.7px, against 11.0px at scale 1. */
    --lp-card-scale: 0.7;

    /* Card-only, all three uses (the card, the figure frame, the shot mat), so the SCALE is applied
       here rather than at each of them. */
    --lp-radius: calc(14px * var(--lp-card-scale));
    --lp-title-gap: 28px;
    /* The CARD title's own gap, split from the hero's on 2026-09-20 when the card title lost its rule.
       They were one token because the two happened to want the same number, not because anything binds
       them: the hero's H1 is followed by a subhead across an empty band, while a card title now sits
       directly on its turn with nothing between them, so it wants to sit closer. Shrinking the shared
       token would have moved the hero, which nobody asked for. */
    --lp-card-title-gap: calc(16px * var(--lp-card-scale));
    --lp-measure: 1140px;
    --lp-gutter: 22px;
    /* THE CARD'S OWN WIDTH at the desktop measure: the content width the wrap would have had, scaled.
       Named because two things must agree on it: the card stack (which adds its unscaled gutters back
       around it) and the returning-visitor box in the hero, which is drawn as wide as the cards below it
       (user, Sep 2026). Below the measure neither binds and both are window-bound, so they agree there
       too. See .lp-stack for why it is the measure that is scaled rather than the margin. */
    --lp-card-measure: calc((var(--lp-measure) - 2 * var(--lp-gutter)) * var(--lp-card-scale));

    color: var(--lp-ink);
    /* No font-family here, deliberately: the page inherits the app's face from app.css so the front door
       and the simulator read as one product.

       ONE REM IS 10.8px ON THIS PAGE, NOT 16px. app.css anchors html at 10.8px as the whole site's single
       type knob, and a rem resolves against html, not against this 17px. Several comments in this file
       were written assuming 16px and quoted pixel sizes half as large again as the page renders (measured
       2026-09-18 in Edge: the H1's 4.4rem ceiling rendered at 47.5px, a 1.6rem caption at 17.3px). The
       hero and card rules were re-set on the true base that day; the setup dialog's rems were NOT and still
       carry the old arithmetic in their comments. Size in rem against 10.8px, and measure rather than
       multiply by 16. */
    font-size: 17px;
    line-height: 1.6;

    /* THE GRAPH PAPER: millimetre paper behind the page, dissolving at the left and right EDGES (user,
   2026-09-23), for the reason the hero mark is the graduated variant: this page's whole claim is
   measurement and precision, and it can say so before a word is read.

   MILLIMETRE PAPER, NOT SCHOOL SQUARED PAPER, and the difference is the TIER COUNT far more than the
   pitch. Squared paper is one weight at one spacing; engineering paper is three, a fine 1mm rule, a
   stronger one every 5mm and a stronger one again at the centimetre. Two tiers at 24px read as an
   exercise book however faint they were made, which is what the first cut was. 5px stands in for the
   millimetre (about 3.8px at 96dpi, rounded up because a 1px line on a 4px pitch is a quarter ink and
   moires on a fractionally-scaled display), so the 5mm and 1cm rules fall at 25px and 50px.

   THE FADE IS A WASH IN THE SAME BACKGROUND LIST, NOT A MASK, and that is the whole reason this rule can
   live on `.lp` at all. `mask-image` masks an element AND ITS CHILDREN, so a mask here would fade the
   cards and the copy with the grid; the first cut paid for that with a pseudo-element whose only job was
   to own the mask. The FIRST layer below is the page ground itself, opaque at the viewport edge and
   transparent by the time it reaches the content column, painted OVER the grid. Same picture, one
   element fewer, and no way for a later reader to move it onto `.lp` and quietly fade the page.

   It reads the ground through the same `--color-surface-light` the body does, so the wash cannot drift
   to a different white than the thing it is washing into.

   IT STOPS FADING WHEN THERE IS NO MARGIN TO FADE IN. Below --lp-measure the column fills the width, the
   two middle stops cross, and the wash collapses to nothing: a phone gets the paper at full strength edge
   to edge, which is what a sheet that size should look like. No media query.

   THE RAMP IS THE MARGIN, SO IT IS SHORT JUST ABOVE --lp-measure. At a 1200px viewport each side has
   30px to fade across, which reads closer to a band than to a dissolve; by 1600px it is 230px and the
   word fits. That narrow strip is where the column first stops filling the window, and it is left alone
   on purpose: a fixed minimum ramp would have to come out of the column's own width at exactly the
   widths where the column is tightest.

   THE PAINT COST IS UNMEASURED. Seven background layers, four of them repeating gradients at a 5px and
   25px pitch, over an element several thousand pixels tall, on the page whose boot budget is tracked
   (LandingAndFirstLoad.SetupAndBoot.md). Nothing here has profiled raster or scroll on a low-end device,
   and the rest of this file quotes measurements, so the absence is said out loud rather than left to be
   inferred (rule 1b).

   THE CARDS ARE NOT ON IT. Each card paints --lp-card-bg, so the grid shows in the gaps between them and
   behind the hero, and stops at a card's edge. That is what keeps it a ground rather than a texture
   running under the copy.

   IT IS ON `.lp::before`, NOT ON `.lp`, because the paper has to be drawn at device resolution and the
   only way to ask CSS for that is a layer scaled by 1 / ratio; the --lp-paper-* comment above is the
   argument. `position: relative` is what that layer is anchored to, and it is the only reason this rule
   is positioned.

   `isolation: isolate` IS THE OTHER HALF AND IS NOT DECORATION. `position: relative` with a z-index of
   `auto` does NOT create a stacking context, so a `z-index: -1` pseudo element joins the nearest
   ancestor that does, which here is the ROOT: it then paints below the in-flow background of everything
   between, `.lp` included. Without this the paper is visible only by the accident that `html` has no
   background, so `body`'s propagates to the canvas and is painted before anything else. Measured on the
   live page at a 1.5 ratio, ink range in a clean patch of paper: 42 as shipped, 0 the moment a
   `background-color` is added to `.lp`, 0 the moment ANY background is given to `html`, and 42 again
   with this line present in either case. It is a stacking context and nothing else, so it forces no
   compositing layer and changes no paint. */
    position: relative;
    isolation: isolate;
}

/* THE PAPER. Every length in here is a DEVICE pixel, because the transform below cancels the device
   scale exactly: the layer is laid out at ratio times the page's size and then scaled by its reciprocal,
   so it rasterises at one CSS pixel per device pixel and an integer length cannot land between two of
   them. Read the --lp-paper-* comment above before changing anything here; a length written in ordinary
   CSS pixels will be silently 1.5x too small on the machine this was reported from.

   z-index: -1 IS LOAD-BEARING and is what keeps this a ground rather than a veil. A positioned pseudo
   element at z-index 0 or auto paints ABOVE its parent's in-flow text; at -1, INSIDE THE STACKING
   CONTEXT `.lp` now makes with `isolation: isolate`, it paints above `.lp`'s own background and below
   everything else, which is exactly the slot the seven background layers occupied when they were on
   `.lp` itself. The two lines are one mechanism: read the `isolation` note above before touching either,
   because -1 without the isolation escapes to the root and the paper goes under the page.

   THE WASH MOVED IN HERE WITH THE GRID and had to: a parent's background paints BELOW its pseudo
   elements, so leaving it on `.lp` would have put the fade UNDER the paper it is supposed to fade out.
   It still masks nothing, which is the property the old comment cared about; it is a layer over the grid
   in the same list, and this layer has no children to fade. Its measure is scaled with everything else,
   since the percentages resolve against a box that is now ratio times as wide. */
.lp::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: calc(100% * var(--lp-dpr));
    height: calc(100% * var(--lp-dpr));
    transform: scale(calc(1 / var(--lp-dpr)));
    transform-origin: 0 0;
    z-index: -1;
    pointer-events: none;
    background-image:
        linear-gradient(to right,
            var(--lp-ground) 0,
            transparent calc(50% - var(--lp-measure) * var(--lp-dpr) / 2),
            transparent calc(50% + var(--lp-measure) * var(--lp-dpr) / 2),
            var(--lp-ground) 100%),
        repeating-linear-gradient(to right, var(--lp-grid-major) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) calc(var(--lp-paper-pitch) * 10)),
        repeating-linear-gradient(to bottom, var(--lp-grid-major) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) calc(var(--lp-paper-pitch) * 10)),
        repeating-linear-gradient(to right, var(--lp-grid-medium) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) calc(var(--lp-paper-pitch) * 5)),
        repeating-linear-gradient(to bottom, var(--lp-grid-medium) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) calc(var(--lp-paper-pitch) * 5)),
        repeating-linear-gradient(to right, var(--lp-grid-line) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) var(--lp-paper-pitch)),
        repeating-linear-gradient(to bottom, var(--lp-grid-line) 0 var(--lp-paper-hair), transparent var(--lp-paper-hair) var(--lp-paper-pitch));
}

/* THE DEVICE-PIXEL ARM, gated so that an engine without round() keeps the fallback above rather than
   losing the whole background. The condition tests round() in a place a browser DOES validate at parse
   time, which a custom property is not, so the gate is real; the `--lp-paper-*` comment on `.lp` has the
   measurement of what happens without it.

   It restates the four tokens and nothing else. The layer's geometry needs no gate of its own: at the
   fallback's ratio of 1 the width is 100% and the transform is scale(1), so the same `.lp::before` rule
   draws the old paper on an old engine and the device-pixel paper on a current one. */
@supports (width: round(1px, 1px)) {
    .lp {
        --lp-dpr: var(--lp-device-pixel-ratio, 1);
        --lp-paper-pitch: calc(max(1, round(var(--lp-grid-pitch) * var(--lp-dpr), 1)) * 1px);
        --lp-paper-hair: calc(max(1, round(var(--lp-dpr), 1)) * 1px);
        --lp-paper-ink: calc(var(--lp-dpr) / max(1, round(var(--lp-dpr), 1)));
    }
}

.lp *,
.lp *::before,
.lp *::after {
    box-sizing: border-box;
}

/* text-align is set EXPLICITLY because _Layout wraps every page in <main class="explore-main">, and
   app.css carries `.explore-main h2 { text-align: center }`. That rule has the same specificity as this
   one, so only source order (landing.css loads from @section Head, after app.css) decides it. Without
   this every card title on the page renders centred. */
.lp h1,
.lp h2,
.lp h3 {
    /* margin-BLOCK, for the reason the `.lp p` rule below spells out at length, and this was the
       shorthand until PR #3457. Zeroing the INLINE margins here is class-plus-element (0,1,1), so any
       heading wanting to centre its own box had to out-specify that or come later in source, and
       `.lp-hero h1 { width: fit-content; margin-inline: auto }` was winning only on source order.
       A heading has no default inline margin to kill, so the shorthand bought nothing and cost that. */
    margin-block: 0;
    padding: 0;
    font-weight: 600;
    text-align: left;
    text-indent: 0;
    text-wrap: balance;
}

/* margin-BLOCK, not the shorthand, and the difference is not pedantry: this rule wants to kill the
   browser's default paragraph spacing, which is a vertical concern, and the shorthand was also zeroing the
   INLINE margins. Since `.lp p` is a class-plus-element selector (0,1,1) it outranks any plain
   `.lp-whatever` class (0,1,0) whatever the order, so a paragraph that set `margin-inline: auto` to centre
   its own measure silently did not, and inherited text-align made it look like the text had been centred
   inside a left-hugging box. That is the THIRD instance of this specificity trap in this file, after the
   two `:not(.btn)` notes below; a paragraph has no default inline margin anyway, so zeroing them bought
   nothing. */
.lp p {
    margin-block: 0;
}

/* :not(.btn) matters: a class-plus-element selector OUTRANKS bootstrap's single-class .btn-primary, so
   without the exclusion this would repaint the CTA's own text colour. */
/* The site's link convention, copied from calculator.css rather than invented: no underline at rest,
   underlined on hover, and the underline sits 0.25em clear of the baseline so it does not crowd the
   descenders. The :not(.btn) exclusion is load-bearing, since a class-plus-element selector outranks
   bootstrap's single-class .btn-primary and would repaint the CTA's own text. */
.lp a:not(.btn) {
    color: var(--lp-accent);
    text-decoration: none;
    text-underline-offset: 0.25em;
}

.lp a:not(.btn):hover,
.lp a:not(.btn):focus-visible {
    text-decoration: underline;
}

.lp img {
    max-width: 100%;
    height: auto;
}

/* THE PAPER STARTS WHERE THE BRAND STRIP ENDS (user, 2026-09-23), so the grid's top edge is the header's
   bottom edge with nothing between them.

   IT WAS 35px, NOT 10px, AND FROM TWO RULES. `.explore-main`'s `padding: 10px 0 30px 0` is the one that
   looks responsible, and on its own it leaves 25 of the 35: `.explore-header` also carries
   `margin-bottom: 25px`. Removing either alone moves the paper and does not land it.

   BOTH OVERRIDES LIVE HERE RATHER THAN IN app.css, AND THAT IS THE POINT OF WRITING THEM OUT. Both
   originals are global: `.explore-main` wraps every server-rendered page, and `body.plain-page` is NOT a
   landing-only hook either, since Privacy, Terms, every calculator and every showcase set it too. This
   stylesheet is loaded by exactly one page (`Landing.cshtml`, in @section Head), so the sheet IS the
   scope, and the same (0,1,0) specificity resolves on source order because landing.css loads after
   app.css. That is the mechanism `.lp h1, .lp h2`'s text-align note above already depends on.

   THE STRIP KEEPS ITS BOUNDARY. `.explore-header` is sticky and carries a box-shadow for exactly this
   reason (app.css says so on the rule), so dropping its margin removes space, not separation: content
   scrolling under the strip still has an edge to pass beneath. */
.explore-header {
    margin-bottom: 0;
}

.explore-main {
    padding-top: 0;
}

.lp-wrap {
    width: 100%;
    max-width: var(--lp-measure);
    margin-inline: auto;
    padding-inline: var(--lp-gutter);
}

/* -------------------------------------------------------------------- the page ground */

/* The whole page sits on an off-white, #FAFAFA (user, 2026-09-18), ChartPalette's SurfaceLight. Landing-
   only for the same reason as the header rule below: this stylesheet is linked from Landing.cshtml alone.
   The HEADER is painted too, because app.css gives .explore-header an explicit white, which would
   otherwise leave a white band across the top of an off-white page. Both win on source order, since
   landing.css loads after bootstrap and app.css.

   Two consequences, checked: a figure proof's box is the same #FAFAFA, and still reads as a box because
   it sits on the alice-blue card rather than on the page; and the question wall's one #FAFAFA strip
   keeps its hairline edge, so it does not dissolve into the ground. */
body {
    background: var(--color-surface-light, #FAFAFA);
}

.explore-header {
    background: var(--color-surface-light, #FAFAFA);
}

/* ------------------------------------------------------------------- the site header */

/* THE CENTRING IS BACK, and it no longer lives here.

   It used to read `justify-content: center` on this rule, which worked only because the header's
   container was a COLUMN: as the sole item on its line the brand stretched full width, so centring its own
   contents centred them on the page. Putting the door on the same strip made the container a flex ROW, at
   which point the brand was sized to its contents and that line became inert; it was removed rather than
   left, since a rule that does nothing is worse than no rule.

   The strip is now a three-column grid in app.css (1fr | auto | 1fr) with the brand in the middle track,
   so the wordmark sits on the page's true axis with the door at the right edge, on EVERY page rather than
   only this one. Nothing landing-specific is needed for it, which is why only the size override remains
   below. Do not re-add a justify-content here: it would do nothing, and the next reader would have to work
   out why. */
.explore-header-brand {
    /* Bigger than the app's 2.2em. Set through app.css's token rather than as a font-size, so the phone
       rule there, which scales the wordmark with the screen, reaches this page too: a plain font-size
       here won on source order and kept the landing header at full size on a phone. The logo is sized in
       em of the wordmark over there, so it follows without a rule of its own (74px at this size). */
    --explore-header-size: 2.9em;
}

/* ------------------------------------------------------------------------ hero */

/* The one block on the page that is not a card. It carries the promise, the category, the question wall
   and the single action, in that order, and nothing else: no eyebrow, no qualifier line, and no rule
   under it (removed, user, 2026-09-18), so the first card follows the button directly. */

/* THE HERO IS CENTRED, and the H1 is the exception that makes it possible. Everything here centres, so
   the subhead and the CTA sit on the page's axis; the H1 keeps text-align: left and is centred as a BLOCK
   instead. That is not a fussy distinction: the staircase is a rising diagonal, and a diagonal needs a
   fixed left edge to rise from. Centre the LINES and each one floats on its own axis, the edge dissolves
   and the escalation with it, leaving three unrelated phrases. Centre the BOX and the diagonal survives
   inside a centred composition. */
.lp-hero-in {
    padding-block: 24px 64px;
    text-align: center;
}

.lp-hero h1 {
    /* 28px floor, 70px ceiling (user, 2026-09-18: "bigger"; it was capped at 4.4rem, 47.5px). The 9vw
       slope is set by the STAIRCASE, not by taste: "In your hands." plus its two indents measures 9.11
       times the font size in Tahoma, and 10vw wrapped it at 521px once a desktop scrollbar took its 17px.
       9vw keeps about 9% spare from 521px, where the staircase comes on, to the cap. On a 390px phone it
       gives 35px, up from 28px, with the staircase flattened. */
    font-size: var(--lp-hero-size);
    line-height: 1.03;
    letter-spacing: -0.02em;
    /* NO max-width any more. It was 24ch, and existed only to make the one-sentence H1 break at its commas
       (at 16ch it broke mid-clause, "Your money, your / future, in your / hands."). The three lines are
       markup now, so nothing is left for a measure to get wrong. */

    /* Shrink to the longest line, then centre THAT box, while the lines inside stay left-aligned. See the
       .lp-hero-in comment for why the staircase cannot survive centred lines.

       max-content AND flex: 0 0 auto, BOTH of which exist to stop the lockup compressing this box rather
       than folding it. As a flex item beside the mark, `fit-content` resolves against the space LEFT OVER
       and `flex-shrink: 1` gives the rest away, so a lockup wider than the column lands its whole deficit
       here and the staircase wraps: the one thing the 9vw slope above is calculated to prevent. Refusing
       to shrink is what makes the lockup's `flex-wrap` fire instead. */
    width: max-content;
    flex: 0 0 auto;
    text-align: left;
}

/* THE STAIRCASE. Block spans so each line can be indented, which a <br> cannot be. ONE number controls the
   step, so the rhythm is tuned in a single place rather than by editing two margins that can drift. */
/* THE HERO LOCKUP: the mark, then the three-line promise, centred together (user, 2026-09-23).

   THE MARK IS SIZED FROM THE HEADLINE, through --lp-hero-size, which is the h1's own clamp lifted into a
   token so one declaration drives both. It cannot be an `em` here: the mark is a SIBLING of the <h1>, so
   an em resolves against .lp's 17px rather than against a headline that ranges from 28px to 70px, and the
   two would drift apart at every width between the clamp's ends. x3.1 is the three-line block's height
   (three lines at 1.03 leading), so the mark's BOX matches the words' box.

   THE INK IS SHORTER THAN THE BOX, by the artwork rather than by this rule: the mark's polygons span
   y=5 to y=140 of a 145 viewBox, so the drawn glyph fills 93% of its box and is about 2.89x the font
   size against a 3.09x text block. Read "as tall as the headline" as the boxes matching, not the ink.
   x3.35 would make it literal and was not taken, 3.1 being the value that was looked at and approved on
   the page.

   THE H1'S `margin-inline: auto` IS DROPPED HERE, and that is not a tidy-up: inside a flex container an
   auto margin absorbs the free space BEFORE justify-content is consulted, so leaving it would pin the
   mark to the far left of the hero and centre the headline in what was left. The card title bar learned
   this the expensive way earlier the same day; see .lp-say h2.

   IT FOLDS BY MEASURING ITSELF, NOT AT A BREAKPOINT I PICKED. `flex-wrap: wrap` puts the headline on its
   own line the moment the pair stops fitting, at whatever width that is, for whatever the font and the
   language make it. The first cut stacked at a hand-chosen 700px and was WRONG from 700px to about 930px:
   the h1 measures 9.11x its font size (its own rule records the measurement) and the mark 3.1x, so the
   pair needs 12.21 x fs + 28px against a column of min(1140, V) - 44. Above 780px the font sits at its
   70.2px ceiling while the column keeps growing, so the pair needs a flat 885px and does not fit until
   about 930 -- every iPad portrait width and every half-screen desktop window. The mark is `flex: none`
   and the h1 refuses to shrink, so the deficit had nowhere to go but the staircase, which wrapped.

   THE LESSON IS THE DERIVATION, NOT THE NUMBER. 700px came from checking the 390px phone, which proves
   it must fold there and says nothing about where it starts fitting again; a second arithmetic check
   would have found the band, and a self-measuring layout removes the need for either. */
.lp-hero-lockup {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 28px;
    /* The headline's gap to the subhead, moved off the <h1> when it became a flex item: cross-alignment
       uses an item's MARGIN box, so a bottom margin there would have centred the mark against a box 28px
       taller than the type and dropped it about 14px. Same trap as the `margin-inline: auto` above, one
       property over. */
    margin-bottom: var(--lp-title-gap);
}

/* TWO CLASSES, because `.lp img { height: auto }` (see it above) is (0,1,1) and would otherwise beat a
   bare `.lp-hero-logo`. That override cost two rounds of iteration on the card marks; it is written out
   here so the next reader does not pay it again. */
.lp .lp-hero-logo {
    flex: none;
    width: calc(var(--lp-hero-size) * 3.1);
    height: calc(var(--lp-hero-size) * 3.1);
    max-width: none;
}

/* ON A PHONE THE MARK COMES DOWN TO 2x, because a folded hero spends its height on the mark before a
   word is read. The FOLD itself is not here any more: `flex-wrap` on the rule above does it at whatever
   width the pair stops fitting, so this block sets size and spacing only. `flex-direction: column` went
   with it, having become a second way of saying what the wrap already says. */
@media (max-width: 700px) {
    .lp-hero-lockup {
        gap: 16px;
    }

    .lp .lp-hero-logo {
        width: calc(var(--lp-hero-size) * 2);
        height: calc(var(--lp-hero-size) * 2);
    }
}

.lp-hero-line {
    display: block;
}

.lp-hero-line-2 {
    margin-left: var(--lp-hero-indent);
}

.lp-hero-line-3 {
    margin-left: calc(var(--lp-hero-indent) * 2);
}

/* TWO CLASSES, and the second is load-bearing: app.css styles `.explore-main p` (1.4em, grey #555) at
   (0,1,1), which outranks a lone class, so a single-class rule here lost its size AND its colour to it.
   That is how the subhead and the card turn rendered grey for weeks while --lp-ink-soft said dark. */
.lp-hero .lp-hero-sub {
    /* The BOX centres and the TEXT inside stays left-aligned. The auto margins put the box on the page's
       axis, and the explicit left undoes the centring inherited from .lp-hero-in, which is meant for the
       CTA rather than for prose: three lines that each start at a different x are harder to scan.

       SHRINK-TO-FIT, NOT A MEASURE (user, 2026-09-24). It was max-width: 60ch while this was a paragraph;
       as three one-sentence lines the user wants each on ONE line wherever the screen allows, and 60ch
       wrapped the two longer ones at every width. fit-content is the longest line's own width, capped at
       the column, so a line wraps only when the column is genuinely too narrow for it. The rule above
       spans this box, so it is as wide as the longest line. */
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
    /* 23.8px, which is exactly what app.css's 1.4em was rendering, so taking ownership of the colour
       moves no size. */
    font-size: 2.2rem;
    line-height: 1.5;
    color: var(--lp-ink-soft);
    /* A heavy rule between the promise and the explanation (user, 2026-09-19). It spans the subhead's own
       fit-content box (as wide as the longest line), not the page, so it sits centred under the H1 as
       the list does. */
    border-top: 6px solid var(--lp-hero-rule);
    /* 30px under the rule, up from 20px (user, 2026-09-24). */
    padding: 30px 0 0;
    list-style: none;
}

/* THREE ONE-SENTENCE LINES, each hung off a bullet in the rule's ink (user, 2026-09-24). The hanging
   indent keeps a wrapped line under its own text rather than under the bullet. */
.lp-hero .lp-hero-sub > li {
    position: relative;
    padding-left: 1.1em;
    /* No one-word last line: at 1280px "ones." and at 800px "taxed." each wrapped alone. */
    text-wrap: pretty;
}

.lp-hero .lp-hero-sub > li + li {
    margin-top: 0.35em;
}

.lp-hero .lp-hero-sub > li::before {
    /* A solid circle (user, 2026-09-24), DRAWN rather than typed: a U+25CF glyph depends on the face
       carrying it, and headless Edge rendered it as a missing-glyph box on top of the first letter. A box
       with a radius is the same dot in every font, sized in em so it tracks the text. */
    content: "";
    position: absolute;
    left: 0.1em;
    top: 0.5em;
    width: 0.45em;
    height: 0.45em;
    border-radius: 50%;
    background: var(--lp-hero-rule);
}

/* The VERBS are bold (user, 2026-09-24): explore, visualise, understand, learn, so a reader skimming the
   three lines picks up what they will DO before what the product is. */
.lp-hero .lp-hero-sub strong {
    font-weight: 700;
    color: var(--lp-ink);
}

/* ------------------------------------------------------------ the question wall */

/* Overlapping, tilted strips of questions, between the subhead and the button (user, 2026-09-18). No
   border, no ground, no card: the strips ARE the section.

   ONE POSTER THAT SCALES. Every measure is a container unit or a percentage of the stage, so the
   composition is identical at every desktop width and simply grows or shrinks; nothing reflows, which is
   what lets strips overlap by design rather than by accident. Each strip carries its own centre (--x,
   --y), tilt (--r), size (--s, in cqw), stacking (--z) and entrance order (--i), plus a portrait twin of
   the first four for the narrow layout, so there is one mechanism and every strip is a row of numbers.

   THE OVERLAPS WERE TUNED AGAINST WINDOWS FACES. The stacks fall back to macOS equivalents of similar
   width, so the composition holds there, but a platform whose fallbacks are much wider or narrower
   shifts where two strips meet. The composition is tuned so a strip covers another's PADDING rather
   than its words. Measured, not pinned (2026-09-18): each strip's resting box was read back from Edge on
   Windows, with its animation switched off, in frames 1280px, 800px and 390px wide, and every higher
   strip's rotated rectangle was intersected with every lower strip's content box. Result: 10
   overlapping pairs and no covered words on the desktop wall; 6 and none on the portrait wall at both
   narrow widths. Re-measure that way after moving a strip, and never off a screenshot taken during the
   entrance, which shows the strips at 2.6 times their resting tilt and far more crowded than they are.

   THAT MEASUREMENT PREDATES 2026-09-24, when every strip gained a 2px border and the tax strip joined
   the wall one row lower. Since then the wall has only been CHECKED BY EYE, off reduced-motion
   screenshots from headless Edge at 1280px, 800px and 520px: no strip covers another's words. The
   intersection method above has not been re-run.

   DELIBERATELY INERT: no hover, no link, no focus. The wall is read, not operated. */
.lp-asks {
    container-type: inline-size;
    margin-top: 48px;
    overflow-x: clip;
}

.lp-asks-stage {
    position: relative;
    margin: 0;
    padding: 0;
    list-style: none;
    aspect-ratio: 1096 / 362;
}

.lp-ask {
    --rot: var(--r);

    position: absolute;
    left: var(--x);
    top: var(--y);
    z-index: var(--z);
    transform: translate(-50%, -50%) rotate(var(--rot));
    font-size: calc(var(--s) * 1cqw);
    line-height: 1.08;
    white-space: nowrap;
    padding: 0.26em 0.58em 0.32em;
    /* 2px black on EVERY strip (user, 2026-09-24), so each reads as a label stuck to the wall whatever its
       ground; a light strip on the off-white page no longer needs a hairline of its own. In px, not cqw:
       an edge is a stroke weight, and a stroke that thinned with the portrait wall would vanish on a phone.
       It adds 4px to every box, which the overlap measurement at the top of this section predates. */
    border: 2px solid var(--color-black, #000000);
    box-shadow: 0 1px 0 rgba(10, 16, 23, 0.06), var(--lp-ask-lift);
    /* BACKWARDS, never forwards and never from opacity 0: during its delay each strip already sits
       visibly in its entrance pose, so a crawler, a thumbnail or a reader who never waits sees the wall. */
    animation: lp-ask-settle 0.7s cubic-bezier(0.2, 0.9, 0.25, 1.15) backwards;
    animation-delay: calc(var(--i) * 70ms + 120ms);
}

@keyframes lp-ask-settle {
    from {
        transform: translate(-50%, calc(-50% - 0.6em)) rotate(calc(var(--rot) * 2.6)) scale(1.07);
    }
}

/* ---- the three the user named: biggest, central, each overlapping the next ---- */

.lp-ask-retire {
    --x: 36%; --y: 27.1%; --r: -4deg; --s: 5.6; --z: 5; --i: 0;
    --mx: 50%; --my: 21.1%; --mr: -4deg; --ms: 9.2;
    font-family: Didot, "Bodoni 72", "Bodoni MT", "Sitka Banner", Georgia, serif;
    font-style: italic;
    font-weight: 400;
    letter-spacing: -0.01em;
    padding-inline: 0.5em 0.62em;
    /* STEEL BLUE (user, 2026-09-24), and that knowingly reverses the rule the maroon before it was
       chosen under. The strip went dark blue, then maroon in Sep 2026 because the wall was meant to hold
       no colour near the button blue (#12509E); the user then chose steel blue for it, and the maroon
       moved to the new tax strip. #4682B4 is a mid blue, lighter and greyer than the button, but it IS
       the same hue family, so the wall now has one blue strip.

       Contrast: white on #4682B4 is 4.11:1, under AA's 4.5 for body text and over its 3.0 for LARGE
       text, which this strip always is. calc(5.6 * 1cqw) is 48px at the 860px breakpoint and 61px at
       the 1096px cap; the portrait twin's calc(9.2 * 1cqw) stays past the 24px (18pt) large-text line
       for any wall wider than 261px. */
    background: var(--color-steel-blue, #4682B4);
    color: #FFFFFF;
}

.lp-ask-rent {
    --x: 61%; --y: 49.7%; --r: 3deg; --s: 3.9; --z: 6; --i: 2;
    --mx: 50%; --my: 49.6%; --mr: 3deg; --ms: 6.4;
    font-family: Bahnschrift, "Avenir Next Condensed", "Arial Narrow", sans-serif;
    font-stretch: condensed;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.01em;
    background: var(--color-caution-strong, #FFD400);
    color: var(--lp-ink);
}

.lp-ask-child {
    --x: 38%; --y: 65.8%; --r: -1.5deg; --s: 2.9; --z: 7; --i: 4;
    --mx: 51%; --my: 70.7%; --mr: -2deg; --ms: 4.6;
    font-family: "American Typewriter", "Courier New", Courier, monospace;
    font-weight: 700;
    background: #FFFFFF;
    color: var(--lp-ink);
}

/* ---- the supporting voices ---- */

.lp-ask-salary {
    --x: 24%; --y: 14.3%; --r: -2deg; --s: 2; --z: 8; --i: 1;
    --mx: 46%; --my: 8.2%; --mr: -2deg; --ms: 4.4;
    font-family: Georgia, "Times New Roman", serif;
    background: var(--color-pill-insight-light, #EAF5F5);
    color: var(--color-pill-insight-dark, #1F6B6B);
}

/* GREEN, not the accent, and the reason is the wall rather than this strip. It carried --lp-accent while
   that was Bootstrap's bright #0D6EFD, which stood clearly apart from the wall's two other blues. Darkening
   the accent to #12509E (Sep 2026) collapsed all three into one hue: retire is #1B3A63 at hue 214, fix is
   #3063A5 at hue 214, and the accent is hue 213, so a wall whose whole point is "many voices" grew three
   strips saying the same thing. GreenDark sits at hue 120, the furthest any palette colour gets from that
   cluster, and white on it is 7.44:1 (AAA).

   It does NOT track --lp-accent any more, deliberately: this strip's job is to be UNLIKE its neighbours, so
   following a variable that two neighbours also follow is what caused the collision. Purple and teal were
   the other candidates and are already spoken for, by overpay and salary. */
.lp-ask-isa {
    --x: 71%; --y: 17.6%; --r: 6deg; --s: 2.6; --z: 8; --i: 3;
    --mx: 66%; --my: 38.3%; --mr: 6deg; --ms: 5.4;
    font-weight: 700;
    background: var(--color-green-dark, #006400);
    color: #FFFFFF;
}

.lp-ask-overpay {
    --x: 80%; --y: 35.6%; --r: -2deg; --s: 1.9; --z: 9; --i: 5;
    --mx: 58%; --my: 81.9%; --mr: 2deg; --ms: 4.2;
    font-family: "Gill Sans", "Gill Sans MT", Candara, "Trebuchet MS", sans-serif;
    font-weight: 600;
    background: var(--color-pill-curiosity-light, #F5ECF6);
    color: var(--color-pill-curiosity-dark, #83358F);
}

.lp-ask-school {
    --x: 19%; --y: 50.7%; --r: 2deg; --s: 2; --z: 8; --i: 6;
    --mx: 42%; --my: 59.6%; --mr: 2deg; --ms: 4.3;
    font-family: "Palatino Linotype", Palatino, "Book Antiqua", serif;
    font-style: italic;
    background: var(--lp-card-bg);
    color: var(--lp-brand);
}

/* MAROON, the colour the retire strip carried until 2026-09-24 and gave up for steel blue. Tax is money
   OUT, and ChartPalette's Red is MoneyOut, so the colour says what the question is about. The whole
   wall was stretched one row taller for it (the stage's aspect ratio, and every --y / --my rescaled by
   the same factor so nothing above moved relative to anything else). A major strip, kept on phones. */
.lp-ask-tax {
    --x: 50%; --y: 90.8%; --r: 2.5deg; --s: 2.4; --z: 10; --i: 12;
    --mx: 42%; --my: 92.3%; --mr: -2.5deg; --ms: 5;
    font-family: Cambria, "Hoefler Text", Constantia, Georgia, serif;
    font-weight: 700;
    background: var(--color-red, #8B0000);
    color: #FFFFFF;
}

.lp-ask-redundant {
    --x: 83%; --y: 7.2%; --r: -1.5deg; --s: 1.8; --z: 7; --i: 7;
    font-family: Rockwell, "Rockwell Nova", "Roboto Slab", Georgia, serif;
    background: var(--lp-proof-bg);
    color: var(--lp-ink);
}

.lp-ask-company {
    --x: 85%; --y: 64.3%; --r: -4deg; --s: 1.3; --z: 9; --i: 8;
    font-family: "Franklin Gothic Medium", "Franklin Gothic", "Arial Narrow", Arial, sans-serif;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    background: var(--color-pill-event-light, #FFF4E9);
    /* The palette's dark amber rather than the event pill's #CC5500, which is 3.98:1 on this ground, under
       WCAG AA's 4.5:1 for text this small (14px at 1280, about 10px at 861). This is about 5.1:1. */
    color: var(--color-banner-warning-dark, #856404);
}

.lp-ask-trap {
    --x: 20%; --y: 77.0%; --r: -3deg; --s: 1.6; --z: 9; --i: 9;
    font-family: Consolas, Menlo, "Lucida Console", monospace;
    font-weight: 700;
    background: var(--color-black, #000000);
    color: var(--color-caution-strong, #FFD400);
}

.lp-ask-care {
    --x: 60%; --y: 76.2%; --r: 2deg; --s: 1.9; --z: 8; --i: 10;
    font-family: "Segoe Print", "Bradley Hand", Noteworthy, "Ink Free", cursive;
    background: var(--color-pill-note-light, #FFFFFB);
    color: var(--lp-brand);
}

.lp-ask-fix {
    --x: 89%; --y: 75.3%; --r: 5deg; --s: 1.6; --z: 10; --i: 11;
    font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif;
    font-weight: 900;
    letter-spacing: -0.02em;
    /* DEEP TEAL. This was the WORST of the three blues: #3063A5 is dE 11.7 from the new button blue,
       near enough to read as the same colour. PillInsightDark is dE 53.7 from it and 88.4 from the old
       one, and among the wall's SOLID strips its nearest neighbour is trap's black at dE 47.2.

       IT WENT TO BURNT ORANGE FIRST AND THAT WAS WRONG ON CONTRAST, which is worth recording because the
       error was in the SIZE, not the colour. #CC5500 gives white 4.31:1, and it was justified as large
       bold text on a reading of `--s: 1.9`. This rule sets 1.6; 1.9 belongs to .lp-ask-overpay. Measured
       in the browser rather than derived: `.lp-asks` is the container-query root and CAPS at 1096px, so
       calc(1.6 * 1cqw) runs 12.96px at the 860px breakpoint to 17.54px at the cap and NEVER reaches the
       18.66px where bold text becomes WCAG large. The 4.5 bar therefore applies at every width this
       strip is visible at, and 4.31 fails it. White on #1F6B6B is 6.22:1 and passes throughout.

       Salary carries this hue as its TEXT on a pale teal ground, which is deliberate rather than an
       oversight: as blocks the two read as one voice at two volumes, and the alternative was a brand-new
       palette constant for one strip. Darkening the orange instead was rejected on measurement, since it
       walks straight into retire's maroon (dE 30.9 falling to 23 by a 16% shade). */
    background: var(--color-pill-insight-dark, #1F6B6B);
    color: #FFFFFF;
}

/* ---- three more minor voices (user, 2026-09-24): landscape only, the portrait wall is full ---- */

.lp-ask-sacrifice {
    --x: 47%; --y: 5.2%; --r: 2deg; --s: 1.8; --z: 6; --i: 13;
    font-family: Garamond, "EB Garamond", "Times New Roman", serif;
    font-style: italic;
    font-weight: 700;
    background: var(--color-pill-error-light, #F9ECEC);
    color: var(--color-red, #8B0000);
}

.lp-ask-last {
    --x: 21%; --y: 94.5%; --r: -2deg; --s: 1.6; --z: 9; --i: 14;
    font-family: Tahoma, Verdana, Geneva, sans-serif;
    font-weight: 700;
    background: var(--color-slate-badge, #5B6B73);
    color: #FFFFFF;
}

.lp-ask-kids {
    --x: 80%; --y: 94.0%; --r: -2deg; --s: 1.7; --z: 9; --i: 15;
    font-family: "Century Gothic", Futura, "Avenir Next", sans-serif;
    font-weight: 700;
    background: var(--color-green-pale, #ECFCEC);
    color: var(--color-green-dark, #006400);
}

/* PORTRAIT, at the page's own card breakpoint rather than a new one. Below it the landscape poster would
   shrink its smallest strips under a readable size, so the wall turns portrait, keeps the eight major
   strips and drops the eight lp-ask-minor ones. Capped at 520px so a tablet gets a poster rather than a
   wall of 70px type. */
@media (max-width: 860px) {
    .lp-asks {
        max-width: 520px;
        margin: 32px auto 0;
    }

    .lp-asks-stage {
        aspect-ratio: 323 / 240;
    }

    .lp-ask {
        --rot: var(--mr);

        left: var(--mx);
        top: var(--my);
        font-size: calc(var(--ms) * 1cqw);
    }

    .lp-ask-minor {
        display: none;
    }
}

.lp-act {
    /* 60px, up from 36px (user, 2026-09-24), so the button stands clear of the wall's bottom row. */
    margin-top: 60px;
}

/* ------------------------------------------------------------------------ CTAs */

/* The CTA is the SYSTEM button. It carries bootstrap's .btn.btn-primary, so the blue, the hover and the
   focus ring are the ones every other button in the product uses; this class only sizes it, because a
   marketing CTA wants more presence than an in-app control. Setting a colour here would fork the brand. */
.lp-cta {
    font-size: 1.2em;
    font-weight: 600;
    padding: 10px 20px;
}

/* The hero button keeps more padding and no separate font size: one button size across the page, so the
   hero reads as primary through position and weight of surroundings rather than by being a different
   control. */
.lp-cta-hero {
    padding: 14px 28px;
}

/* THE PLAY MARK (user, 2026-09-24): a solid triangle after the label on every Plan & Play and on the
   setup panel's Start free, the one action they all lead to, so the mark says "this starts it" wherever
   it appears. ONE rule for both, so the two cannot drift. The sign-in panel's two blue actions carried
   it for a day (2026-09-25) and the user took it off them again, so it stays a landing-only mark.

   DRAWN, not typed, for two reasons. A U+25B6 glyph depends on the face carrying it (the hero bullets
   met exactly that, a missing-glyph box in headless Edge), and Start free's label is swapped through
   textContent while it waits (landing-setup.js), which would wipe a glyph written inside the button. A
   pseudo-element is outside the text node and survives the swap. currentColor, so it follows the
   button's own text through hover and focus; em-sized, so it follows each button's own type. */
.lp-cta::after,
.lp-setup-start::after {
    content: "";
    display: inline-block;
    width: 0.62em;
    height: 0.72em;
    margin-left: 0.55em;
    vertical-align: -0.04em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* CENTRED, inheriting text-align from .lp-hero-in, and that is a decision rather than a side effect.
   This pill is what stands in the CTA's place while the flag is off, so it sits where the CTA sits;
   leaving it flush left would put the one element in the hero that is not on the page's axis in the
   variant that is currently shipping. It is an inline-block, so the inherited centring moves the whole
   pill rather than only the words inside it. */
.lp-gated-line {
    font-size: 0.95rem;
    color: var(--lp-brand);
    border: 1px solid var(--lp-rule);
    display: inline-block;
    padding: 13px 18px;
}

/* ------------------------------------------------- the returning-visitor box */

/* Shown only to a browser positively identified as holding an identity with at least one project, and
   revealed by landing-setup.js when the shared probe settles. The served HTML carries `hidden`, so a
   stranger, a crawler and a visitor with JavaScript off never see it and never pay any space for it.

   MARGIN ONLY ABOVE, no reserved height anywhere: this element does not exist until it is shown, which is
   what keeps the primary CTA and everything above it from moving when it appears. See the markup comment
   in Landing.cshtml for why that placement is the answer to D3.

   Quieter than the CTA on purpose. It is the SECONDARY way out for someone who already has somewhere to
   be, and competing with the primary button would undo the hero's one-obvious-action shape.

   A BOX AS WIDE AS THE CARDS BELOW IT (user, Sep 2026), so it reads as the first of the page's objects
   rather than as loose text under the button. It borrows the card's EDGE and RADIUS tokens, so a change to
   the cards' frame reaches it, but paints white rather than the cards' alice-blue: it is a note to one
   visitor, not one of the page's claims, and the two should not be mistaken for each other. The width is
   the same --lp-card-measure the stack uses, and inside the hero's .lp-wrap the gutters already match the
   stack's, so the two edges line up at every width. */
.lp-welcome {
    width: 100%;
    max-width: var(--lp-card-measure);
    margin: 50px auto 0;
    padding: 15px;
    border: var(--lp-card-edge);
    border-radius: var(--lp-radius);
    background: var(--color-white, #FFFFFF);
}

/* TWO CLASSES, and it is the same trap the subhead's own comment records above: app.css styles
   `.explore-main p` at (0,1,1), which outranks a lone class, so a single-class rule here would silently
   lose BOTH its size and its colour to a 1.4em grey. This is a <p> inside the hero, so it is squarely in
   that selector's reach. */
.lp-welcome .lp-welcome-text {
    /* 2rem, up from 1rem (user, Sep 2026), and the unit is worth a word because rem is NOT 16px here:
       app.css sets `html, body { font-size: 10.8px }` as the root anchor for the whole app, so 1rem was
       rendering this line at 10.8px (measured in the browser, not inferred) and 2rem puts it at 21.6px.
       Anything sized in rem on this page is on that 10.8px base. 2.5rem (27px) since the box went to three
       lines at card width (user, Sep 2026): at 21.6px the text read small inside a box that wide. */
    font-size: 2.5rem;
    color: var(--lp-ink, #0A1017);
    margin: 0 0 6px;
}

/* ONE COLOUR ACROSS THE WHOLE SENTENCE (user, Sep 2026): the greeting no longer takes --lp-brand and the
   date no longer takes --lp-ink-soft, so "Welcome back." and "You were last here on ..." read as one line
   rather than as a coloured label with grey detail after it. Weight alone now carries the emphasis, which
   is what the original comment here said it wanted and then spent a colour on as well.

   THE COLOUR ITSELF IS DECLARED ON THE SIBLING RULE, `.lp-welcome .lp-welcome-text` just above, not
   here: this rule carries the weight and nothing else, and the sentence inherits its ink from the
   paragraph. --lp-ink rather than a raw #000000 there: it is the page's own ink token at #0A1017,
   indistinguishable from black on screen, and introducing a second black no other text on the page uses
   is how a palette grows colours nobody chose.

   IT DID NOT TAKE THE LAST NEAR-BUTTON BLUE OUT OF THIS BOX, which this comment claimed when written
   (Sep 2026) and which was false the moment it was written. The action link below is painted --lp-accent
   by the hero's blanket rule, which is the button colour ITSELF rather than something near it, and it
   stays that way on purpose: a link a returning visitor must notice should look like every other link on
   the site. The claim came from reading the link's own rule, whose colour declaration never rendered. */
.lp-welcome-title {
    font-weight: 600;
}

/* THREE LINES (user, Sep 2026): the greeting, the date, the way back. The two spans stay one <p> with a
   space between them, so a screen reader still hears one passage; they are broken onto their own lines in
   CSS only. The greeting's terminator is still what keeps that passage two sentences
   (TestLandingWelcomeBox.TheGreeting_IsAWholeSentence). */
.lp-welcome-title,
.lp-welcome-when {
    display: block;
}

/* A LINK, not a button. The button above it is the primary action and this must not read as a second one
   of equal weight, so it takes the ordinary link treatment: the site's blue, underlined on hover. It is
   the normal link colour deliberately, because that is what makes a returning visitor SEE it as the way
   back in (user, Sep 2026).

   SIZE AND WEIGHT ONLY. This rule used to declare `color: var(--lp-brand)` and `text-decoration: none`,
   and carried a `:hover` rule beside it, and NONE of the three ever rendered. `.lp a:not(.btn)` above
   paints every link in the hero at (0,2,1); a lone class here is (0,1,0), and the local hover at (0,2,0)
   loses to the blanket `:hover` at (0,3,1). So the colour and the decoration are already correct, and
   were always coming from there.

   THEY WERE DELETED BECAUSE THEY LIE, not to save three lines. A declaration that cannot win is
   indistinguishable in the source from one that does, so reading this rule gave --lp-brand, #1B3A63,
   while the browser had been painting --lp-accent the whole time: Bootstrap's #0D6EFD before the palette
   change and #12509E after. That misreading was reported as fact twice and very nearly cost a change
   nobody wanted. If this control ever does need its own colour, it needs a selector that can win, and
   the id is already on the element (see Landing.cshtml's header on ids outranking classes here).
   Guarded by TestLandingWelcomeAction. */
.lp-welcome-action {
    /* Matched to .lp-welcome-text above, so the greeting and the way back out of it stay one block
       rather than the link shrinking away from the sentence that introduces it. Same 10.8px rem base. */
    font-size: 2.5rem;
    font-weight: 600;
}

/* --------------------------------------------------------------- the card stack */

.lp-stack {
    /* THE STACK'S MEASURE, SCALED, so a card comes down in WIDTH with everything else and its aspect
       ratio is preserved rather than only its contents shrinking inside a box that stayed 1096px wide.
       The width is load-bearing beyond looks: the points grid breaks at a 280px track floor, so a card
       narrowed without that floor narrowing with it would silently drop a three-column list to two, and
       the stack would relay out rather than shrink.

       IT IS THE CARD'S MEASURE THAT IS SCALED, NOT THE PAGE'S MARGIN, which is what the shape of this
       calc says: take the content width the wrap would have had (--lp-measure less its two gutters),
       scale THAT, and add the two unscaled gutters back. The gutter is the air between the card and the
       window edge; it belongs to the page, not to the card, so it is not part of what "the card, 30%
       smaller" means.

       WRITING IT THE OBVIOUS WAY INSTEAD MAKES THE CARD WIDER ON A PHONE, which is measured rather than
       feared. `max-width: measure * k` with `padding-inline: gutter * k` is exact on a desktop and wrong
       everywhere the max-width does not bind: below it the stack is sized by the WINDOW, so the only
       thing the scaled gutter still does is give the card back whatever it took off the margin. Measured
       at a 900px viewport, with the token at 0.85 as it then was: the card went 839.8px -> 846.4px, WIDER
       than before, while its type came down 15%, so the line length rose about 19%. The error grows as
       the token falls, since the margin it eats is a fixed 44px.

       WHAT THIS FORM BUYS IS NARROWER THAN "IDENTICAL BELOW THE MEASURE", AND THE DIFFERENCE MOVES WITH
       THE TOKEN, which is why this paragraph now names a crossover instead of a rule. The cap is
       (1140 - 44) * k + 44, so it is 975.6px at 0.85 and 811.2px at 0.7: it stops binding at a viewport
       of about 1020px at 0.85 and about 826px at 0.7. Below THAT the card is window-bound and identical
       to the unscaled page; between it and 1140 the card is at its cap while the unscaled page's card is
       still growing with the window, so the two differ and the band is not a uniform scale. Measured at
       0.7: 768 / 520 / 430 / 390 / 360 are byte-identical, and 900 (839.8 -> 767.2) and 860 (807.8 ->
       772.8) are not. See "THE BAND" below.

       WHAT NO FACTOR CAN DO, stated because it is the honest limit of this token: a uniform scale is
       exact only where the card's width is the PAGE's to choose, which is a viewport at or above
       --lp-measure. Below that the window sizes the card and the window does not scale, so the type comes
       down inside a column that cannot follow it and the lines get proportionally longer. Above 1280 the
       page is a photograph of itself at k; at 390 it is smaller type in a column of the old width.

       THE BAND BETWEEN THE CAP AND --lp-measure IS A THIRD REGIME, and it was missed until the review of
       PR 3573 because it exists only once the cap is scaled. There the card sits at its cap while the
       unscaled page's card is still fluid, so the card's ratio to the unscaled page SLIDES (about 0.93 at
       1140 down to 0.70 at the crossover) against a type scale that is flat at k. That is a real
       departure from the uniform scale, and it RELAYS OUT rather than merely resizing: measured at 0.7,
       a list proof goes from TWO columns to THREE at 1024, 900 and 860, because the scaled 196px track
       floor fits three tracks in the capped figure box where the unscaled 280px floor fitted two in the
       narrower fluid one. The column count is unchanged at and above 1280 and at and below 768, which is
       what the scaled floor was for; the band is where the two effects cross.

       IT IS NOT FIXED BY UNSCALING THE FLOOR, which is the obvious move and is worse: at 0.7 the figure
       box is the same 690px at 1280 as at 900, so an unscaled 280px floor would give TWO columns at both,
       breaking the one region (>= 1280) that is exact today in order to tidy the one that cannot be. The
       band is accepted and documented rather than traded for that.

       These declarations out-specify .lp-wrap's on SOURCE ORDER, not specificity: both selectors are
       (0,1,0) and land on the same element (`class="lp-wrap lp-stack"`), so this rule must stay BELOW
       .lp-wrap, and padding-inline is deliberately NOT restated here so the 860px block's 18px gutter
       still reaches the stack. The same ordering constraint .lp-say h2 carries against .lp h2. */
    max-width: calc(var(--lp-card-measure) + 2 * var(--lp-gutter));
    padding-block: calc(34px * var(--lp-card-scale)) calc(56px * var(--lp-card-scale));
    display: flex;
    flex-direction: column;
    /* 60px between cards (user, 2026-09-18; it was 26px). ITS ORIGINAL REASONING IS GONE: it read "with
       the outward step, the spines and the ruled titles, each card now reads as its own object", and all
       three were deleted on 2026-09-20. The number survives on a different argument, which is why it was
       not tightened with every gap INSIDE the card that day: those separate parts of one object, this
       separates the objects, and with the cards now aligned and identical it is the only thing left
       marking where one ends and the next begins. Deliberately not re-tightened; revisit on sight. */
    gap: calc(60px * var(--lp-card-scale));
}

/* ONE shell for every block below the hero, whatever it holds. Same ground, same edge, same padding, so
   the page reads as a single sequence rather than as five different treatments. */
.lp-card {
    background: var(--lp-card-bg);
    /* One edge on all four sides. The 15px black SPINE (2026-09-18) and the outward STEP are both gone as
       of 2026-09-20: both existed to make the zig-zag readable from the card's edges, and the zig-zag
       itself went with the single column. An edge that alternates with nothing else alternating is not
       rhythm, it is an unexplained asymmetry, and a stack of cards that each sit 32px off the column in
       opposite directions reads as misalignment once their contents are centred. Every card now aligns. */
    border: var(--lp-card-edge);
    border-radius: var(--lp-radius);
    /* 30px (user, 2026-09-20; it was 44px): the cards read too big on a desktop, and padding is what a
       card spends its height on before any of its content. The 860px block below keeps its own inline
       24px, which is now the only thing that rule narrows. */
    padding: calc(30px * var(--lp-card-scale));

    /* A HALO OF THE GROUND, so the graph paper does not run up against the card's edge (user,
       2026-09-23). It is a box-shadow in --lp-ground rather than in white: the page is #FAFAFA, so a
       white glow would read as a second, brighter surface around every card instead of as the paper
       being wiped clean near it. Spread first and then blur, which is what makes it a band that clears
       the grid rather than a rim that draws one.

       SPREAD IS THE "STRONGER" KNOB AND BLUR IS THE "FURTHER" ONE, which is worth saying because the
       colour cannot carry either: --lp-ground is opaque, so there is no alpha to raise. Spread is the
       band the paper is erased from OUTRIGHT, blur is how far the erasure then fades. Taken from 12/34
       to 22/64 authored, so 15px of solid clearance and a fade out to about 60px rendered.

       AT THIS SIZE THE GRID BETWEEN TWO CARDS IS GONE, and that is a consequence to know rather than a
       bug: the stack's gap is 60px x 0.7 = 42px, and two facing haloes each clear about 38px of it. The
       paper now reads as two bands flanking the stack plus the ground behind the hero, not as a sheet
       showing through every gap. Cut the BLUR first if that is not wanted; cutting the spread instead
       just makes the edge of the clearance visible.

       SCALED, like every other length in the stack, and this one would have slipped through review on a
       reading rather than on a rule: it is a shadow rather than a box, so it is easy to think of as an
       effect instead of a measurement. TestLandingCardScale does not care, and neither should the next
       reader: at 0.6 an unscaled 64px halo is 67% too wide for the card it surrounds. */
    box-shadow: 0 0 calc(64px * var(--lp-card-scale)) calc(22px * var(--lp-card-scale)) var(--lp-ground);

    /* The card's own font-size, scaled, and this ONE declaration is what carries every `em` inside the
       card down with it. `1em` is the inherited 17px from .lp, so it says "the page's size, scaled"
       without restating the 17. Its only consumer today is .lp-cta's 1.2em; anything em-sized added to
       a card later is correct for free, which a per-site multiplication would not be. It cannot double
       up: the em values further down (.lp-unit's 0.34em, a list item's 1em indent, .lp-big's tracking)
       all resolve against a font-size this file scales explicitly, never against the card's. */
    font-size: calc(1em * var(--lp-card-scale));
}

/* THE MARK: one glyph per card, inside the black title bar and inverted (user, 2026-09-23).

   IT MOVED TWICE IN ONE DAY, both times on the rendered page rather than on an argument. It opened the
   CARD, above the coda and hard left; then sat beside the bar, which needed a flex row around the pair;
   then went INSIDE the bar in white, which left that row seating nothing. The row survived one more
   round because an <h2> inside it was a flex ITEM and so took content width, which is what makes the bar
   hug its title; `width: fit-content` on the bar says that directly, in the rule that wants it, so the
   wrapper is gone from the markup and from TestLandingCardScale's selector list.

   One family, one per card, none optional, which is the condition the card schema set when it ruled a
   glyph out of v1: "if it returns post-v1 it returns as ONE family chosen once". */

/* THE MARK IS INSIDE THE BLACK BAR, inverted (user, 2026-09-23). Tried as an experiment and kept.

   SIZED IN em, NOT in scaled px, and that is the interesting half. Inside the bar the mark's job is to
   match the TITLE, whose font-size is a clamp that moves with the viewport, so a px box would drift
   against the letters at every width between the clamp's floor and its ceiling. `1.2em` resolves against
   the h2's own font-size, which already carries --lp-card-scale, so the mark tracks the title exactly and
   the scale knob still reaches it. (em is deliberately outside TestLandingCardScale's detector for this
   reason: it is already relative to something that scales.)

   INVERTED WITH A FILTER rather than by re-authoring the artwork or by a CSS mask. filter: invert(1) maps
   black to white and leaves alpha alone, so a transparent background stays transparent, and it is one
   declaration to delete if this is dropped. If it is KEPT, the right form is the repo's existing idiom
   for a tintable glyph: a CSS mask with background-color, which is how images/fa/ is drawn (css/icons.css)
   and which takes any colour rather than only the inverse. That costs the per-card url() moving from the
   img's src into a custom property, and with it the file-existence guard that reads that src. */
.lp .lp-card-icon {
    /* THE HEIGHT DRIVES AND THE WIDTH FOLLOWS (user, 2026-09-23: fill the height available). This is the
       deliberate INVERSE of how the mark was sized when it sat outside the bar, and the flip is the whole
       point rather than a tidy-up. Out there both axes were fixed so the ten occupied one square box and
       no card's first line stood taller than its neighbour's; the cost was that a TRIMMED glyph, whose
       drawing is wider than it is tall, letterboxed inside that square and rendered barely half the
       height of the others. Anonymous is 297x159 after trimming, so in a square box it drew at 54% of the
       box's height and read as a smaller mark beside a full-height one.

       Inside the bar the mark's neighbour is the TEXT, so matching its height is what matters and equal
       footprints are not: `width: auto` lets a replaced element take its used width from the specified
       height and its own intrinsic ratio, which an SVG carries through its viewBox. Every mark is now
       exactly as tall as every other, and the wide ones are simply wider.

       1.2em RATHER THAN THE 1.1em LINE BOX, so the glyph reads against the cap height of the title rather
       than against its line box, at the cost of the bar standing about 3px taller than the text alone
       would make it. That is the "compatible with the padding" half: the bar's 15px block padding is
       untouched and the mark sits inside it, not over it.

       TWO CLASSES IN THE SELECTOR, AND THAT IS THE WHOLE OF WHY THIS WORKS. `.lp img` above sets
       `height: auto` at specificity (0,1,1), and a bare `.lp-card-icon` is (0,1,0), so for two rounds
       this rule's HEIGHT lost silently. Nothing looked wrong with the rule: `filter: invert(1)` comes
       from this same block and applied, which reads as proof that the block is live.

       ONE OVERRIDE EXPLAINS BOTH SYMPTOMS THE USER REPORTED, which is what identified it. With a fixed
       WIDTH and the height forced to auto, the height came out as width / ratio, so the TRIMMED wide
       marks rendered SHORT: reported as the trimmed ones not filling the height, and never the
       letterboxing-inside-a-square this comment first blamed. Then dropping the fixed width so the
       height could drive left the image with NO specified dimension at all, so it took the 300px default
       object size: reported as huge. Diagnosed from a screenshot, where the mark was 300x300 with the
       title wrapped underneath it, a size this file never asks for at any breakpoint.

       A FLEX ITEM since 2026-09-24, when the bar became a flex row so that a wrapped title keeps the mark
       centred on both lines (see `.lp-say h2`). The inline-block and vertical-align below are what it was
       before and are inert inside the row, kept so the mark still sits right if the row is ever undone.
       The gap to the title is this element's own end margin. 40px authored, 28px rendered (user). */
    display: inline-block;
    vertical-align: middle;
    height: 1.2em;
    width: auto;
    /* The OTHER half of `.lp img`, stated rather than inherited. `max-width: 100%` cannot bind at 1.2em
       inside a bar this wide, so this changes no pixel; it is here because two revisions were lost to the
       half of that reset which DID bind, and leaving the other half live invites the same hunt again. */
    max-width: none;
    margin-inline-end: calc(40px * var(--lp-card-scale));
    filter: invert(1);
}

/* SINGLE COLUMN, CENTRED (user, 2026-09-20). Every part of a card runs the card's own width, in reading
   order: title, turn, button, proof, coda.

   THE ZIG-ZAG IS GONE, and it went because its premise did rather than because the user changed their
   mind. It alternated the PROOF, a figure or a screenshot swapping sides, which is what gave a long
   scroll its rhythm; when the proof went full width earlier the same day, the only thing left
   alternating was the TEXT, and a title hopping left, right, left with no structure behind it reads as
   noise. Removing it is the consistent end of that change, not a reversal of the 2026-09-12 decision to
   restore it. The spine and the outward step went with it a few hours later, for the same reason: they
   were the zig-zag drawn at the card's edge, and once nothing else alternated they read as an
   unexplained asymmetry rather than as rhythm. `lp-flip` no longer exists in the markup or here.

   "Figures always right and Shots always left" therefore no longer exists as a reading grammar. It was a
   real aid and it is a real loss; what replaces it is that a proof is now always in the same place, which
   is the same promise made a different way. */

/* CENTRED, as are the turn and the button below it, so a card's words read as one centred column. It was
   "centred in its half" from 2026-09-18 until the card became a single column on 2026-09-20; the
   declaration is unchanged, only what it is centred within. This outranks `.lp h2`'s explicit left only
   on source order, both being (0,1,1), so it must stay below that rule.

   THE SLOPE IS STALE AS OF 2026-09-20 AND HAS NOT BEEN RE-MEASURED. It was set against a HALF-card
   column, so that the longest title, "Everything explained" (10.61 times its font size in Tahoma bold),
   kept to ONE line side by side, with about 5% to spare: 29px at 861px up to a 38.9px ceiling from about
   1100px. Two things invalidated that on 2026-09-20 and neither is in the number: the card became a
   single column, which gives the title roughly twice the width and makes the slope far too conservative;
   and the title became a bar with `padding: 15px` and `letter-spacing: 2px`, which takes 30px off the
   inline axis and adds about 2px per character back on. Computed independently by the author and by the
   PR #3555 reviewer, those two put the longest title over the available width at a 390px viewport, where
   the doc's recorded sweep says nothing wraps; neither computation was confirmed against a rendered
   page, so treat the wrap width as UNKNOWN rather than as either number.

   MEASURED 2026-09-24, headless Edge with phone emulation: the two longest titles ("Sophisticated
   engine", "Everything explained") needed 296px and wrapped below about a 395px viewport. The phone
   block at 520px now tightens the bar, after which they need 245px and keep to one line from about
   335px; at 320px they wrap, with the mark centred on both lines by the bar's flex row.

   RE-MEASURE ON A RETITLE, OR ON ANY CHANGE TO THE TITLE'S BOX: padding, tracking and column count all
   move it, and only the first of those is what the original note warned about. */
.lp-say h2 {
    font-size: calc(var(--lp-card-scale) * clamp(2.6rem, 4.3vw - 8px, 3.6rem));
    line-height: 1.1;
    /* A BLACK BAR (user, 2026-09-20, who supplied these five declarations). It replaces the 2px rules the
       title carried from 2026-09-18 until earlier today: across a full-width single column an underline
       had become a divider spanning the card, where a filled bar reads as the title's own object. The
       letter-spacing is what stops white caps-height type on black looking cramped. */
    background: var(--lp-title-bar-bg);
    color: var(--lp-title-bar-ink);
    /* TWO AXES, not the one value it was (user, 2026-09-23): 15px block as before, 30px inline, so the
       rendered side padding doubles from 10.5px to 21px and the height is untouched. It reads as more
       than it is because the bar HUGS its title rather than running to the cap: `width: fit-content`
       makes its width its own content plus exactly this padding, and every extra inline pixel is black
       the words did not have around them before. */
    padding: calc(15px * var(--lp-card-scale)) calc(30px * var(--lp-card-scale));
    border-radius: calc(10px * var(--lp-card-scale));
    letter-spacing: calc(2px * var(--lp-card-scale));
    /* CAPPED AND CENTRED AT 500 RENDERED PIXELS (user, 2026-09-23). The bar ran the card's full width from
       the day it became a bar, which is about 769px on a desktop: a black strip that wide reads as a
       divider between the coda above it and the turn below, where a capped one reads as the title's own
       object, which is what the bar was for. It is also what makes room for the MARK on the same row.

       AUTHORED AT 714px, NOT 500, and the odd number is the point rather than a typo. Every absolute
       length in the stack is multiplied by --lp-card-scale (TestLandingCardScale holds that), so the
       authored value is a PRE-scale length and 714 * 0.7 is the 500 the user asked for on the page in
       front of them. Writing 500px here would both fail that guard and, at any other scale, leave the one
       part of the card that did not come down with the rest. */
    max-width: calc(714px * var(--lp-card-scale));
    /* HUGS ITS OWN TITLE AND CENTRES ITSELF. `width: fit-content` is what makes the bar end where the
       words do rather than run to the cap, and the auto inline margin centres that narrower box. Both
       jobs were done by a wrapper div for two revisions on 2026-09-23, which made the h2 a flex item and
       got content width as a side effect; this says it in the rule that wants it, one concept fewer for
       an identical render.

       READ THE CAP AS A BACKSTOP, NOT AS THE WIDTH. Hugging means it binds only on a title long enough
       to reach it. Computed rather than measured, so treat it as a calculation and not a sighting: the
       font clamp tops out at 3.6rem, and 1rem is 10.8px on this page, so the title is at most
       38.88 x 0.7 = 27.2px at EVERY width, the 860px breakpoint included (its 6.2vw is 53.3px there and
       the clamp caps it). The longest title plus the mark stays well inside 500px at that size.

       The gap below is unchanged, 16px rather than the hero's 28px and no rule (user, 2026-09-20): the
       title was ruled above and below from 2026-09-18, both rules went with the single column, and with
       nothing between the title and its turn the gap that separated them from a 2px line is more air
       than they need. */
    width: fit-content;
    margin-inline: auto;
    margin-bottom: var(--lp-card-title-gap);
    text-align: center;
    /* A FLEX ROW, so a title that WRAPS keeps its mark beside the whole block (user, 2026-09-24, from a
       phone). As inline content the mark sat on the FIRST line's middle while the second line hung under
       it, which read as a mark shrunk and misplaced. As a flex item it centres on the title's full height;
       the text is one anonymous flex item, centred inside itself by the text-align above. On one line
       nothing moves. */
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Two classes to beat app.css's `.explore-main p`; see .lp-hero .lp-hero-sub. Dark, 25.9px on a wide
   desktop, easing to 20.5px on a phone. Its slope is set by the TITLE: it stays under the title's size at
   every width, including the narrow two-column band just above 860px, where the title is at its smallest. */
.lp-say .lp-turn {
    margin-top: 0;
    /* CENTRED, box and lines alike (user, 2026-09-19): the text centres within its 46ch measure, and the
       measure centres within the half, so a column wider than 46ch cannot leave centred lines sitting in a
       left-hugging box. */
    margin-inline: auto;
    padding-inline: 0;
    text-indent: 0;
    text-align: center;
    font-size: calc(var(--lp-card-scale) * clamp(1.9rem, 0.6rem + 1.8vw, 2.4rem));
    line-height: 1.5;
    color: var(--lp-ink-soft);
    /* 80ch, up from 46ch (user, 2026-09-20): the turn now runs the card's width rather than half of it.
       A cap remains, and it is not a hedge against the user's "the whole width" - at 1280 the card's
       1080px inner width IS about 80ch at this size, so the two coincide on the width the page is
       designed at. What the cap buys is a very wide monitor, where an uncapped measure would keep
       growing past anything readable. */
    max-width: 80ch;
}

.lp-cta-block {
    /* 10px, down from 30px (user, 2026-09-20). The button follows the turn directly now, where it used
       to close a half-card column and wanted air under it. */
    margin-top: calc(10px * var(--lp-card-scale));
    text-align: center;
}

/* THE CARD'S BUTTON SCALES WITH THE CARD, and that is a deliberate departure from .lp-cta-hero's "one
   button size across the page" (which meant one FONT size; the hero already carried more padding).
   A card is a uniformly scaled object, so a button inside it that kept full size would be the one thing
   in the stack out of proportion with its neighbours, sitting 43% larger than the copy around it at
   0.7. The consequence, stated rather than left to be discovered: the hero's CTA is now the largest
   button on the page. That reads as the primary call it is, but it is a change, and it reverses on this
   one rule if the page ever wants the old parity back.

   The font-size is in em, so it scales through .lp-card's own font-size with no multiplier; only the
   PADDING, which is px, has to be multiplied here. */
.lp-card .lp-cta {
    /* LARGER THAN .lp-cta's 1.2em (user, 2026-09-24), with the padding grown in proportion. Still in the
       card's own em, so it keeps scaling with the card. */
    font-size: 1.5em;
    padding: calc(14px * var(--lp-card-scale)) calc(30px * var(--lp-card-scale));
}

/* A PARAGRAPH'S 5px OF PADDING, SCALED, and it comes from app.css's bare `p { padding: 5px }`
   rather than from anything on this page. It is the one absolute length inside a card that this file
   does not author, and without this rule it is the one thing --lp-card-scale cannot reach.

   MEASURED rather than reasoned, because it is invisible in the stylesheet: at scale 1 every <p> in a
   card (the coda, the turn, each figure caption) is exactly 10px taller than its own line boxes.
   Measured while the token was at 0.85, the .lp-card height therefore came out at x0.861 instead of
   x0.850, and the paragraph rhythm inside the card ran about 4% looser than the type it sits in.

   THE INLINE AXIS IS NOT UNIFORM, which is worth one line because the obvious summary is wrong: the
   global sets all four sides, so the coda and the captions are also 10px narrower than their box, but
   the TURN is not, because `.lp-say .lp-turn`'s own `padding-inline: 0` is (0,2,0) against this rule's
   (0,1,1) and wins. Scaling all four here is still right: it reproduces whatever each paragraph had.

   IT MATTERS MOST AT THE VALUES SOMEONE WILL ACTUALLY TRY, which is not hypothetical: the token shipped
   at 0.85 and was turned to 0.7 the same day. A constant 10px against scaled type is 18% larger relative
   to its surroundings at 0.85, 43% at 0.7 and 67% at 0.6, so the error grows exactly as the knob is
   turned further, which is the one promise --lp-card-scale makes. Leaving it would mean the token holds
   at the value it shipped at and drifts at every other.

   THE 5 IS RESTATED, which this file elsewhere calls a trap (see .lp-card's padding-inline in the 860px
   block), and the restatement is deliberate because there is nothing to derive from: padding is not
   inherited, so CSS offers no way to say "the value app.css computed, times k". If that global ever
   moves, this is where the card stops being a true scale of itself, and the measurement above is the
   check: every <p> box in a card should be its line boxes plus 10px * --lp-card-scale, nothing else.

   SCALING IT RATHER THAN ZEROING IT is the point of the exercise. `padding: 0` here would be tidier and
   would take about 40px off a card, but the ask was a card that looks exactly like today's photographed
   smaller, and removing air today's card has is a different change wearing the same clothes. */
.lp-card p {
    padding: calc(5px * var(--lp-card-scale));
}

/* THE CODA: every card's closing line, across the whole card under both columns (user, 2026-09-19). Below
   the proof rather than above the title, so it reads as a verdict the card has earned, not a jab thrown
   before the evidence, and never competes with the ruled title as a second headline. No rule above it
   (tried and removed, user, 2026-09-19: it reads better unruled, so the title stays the only ruled line in
   a card). Centred, bold, between the turn and the title in size (28px at 1280, 21.6px on a phone). Two
   classes, for the `.explore-main p` reason .lp-hero .lp-hero-sub gives. */
.lp-card .lp-coda {
    /* THE CARD OPENS ON IT: a tracked uppercase line above the title (user, 2026-09-20).

       It has held three positions in one day and each move had a reason, so the history is worth one
       block rather than three. It was the card's LAST child from 2026-09-19, on the reasoning that after
       the proof it reads as a verdict the card has earned. That was written for a card about 420px tall;
       the single-column, full-width-proof card is about 1000px, so "after the proof" became a screen and
       a half later with the title long off the top, and the verdict arrived detached from the claim it
       is a verdict about. It moved under the title, then above it.

       ABOVE IT IS THE POSITION THIS FILE USED TO FORBID, and the objection was real: at the top it
       "would compete with the ruled title as a second headline, which is also the eyebrow this page
       bans". That was true of a title carrying a 2px rule. It stopped being true when the title became a
       solid black bar with white type, which a small tracked caps line cannot out-shout, so
       there is nothing left to arbitrate and no ambiguity about which is the heading. The ban was on a
       line that FIGHTS the title, never on the position. If the bar ever goes back to a rule, this
       moves back down with it. */
    /* 20px under it, up from 12px (user, 2026-09-24). */
    margin: 0 0 calc(20px * var(--lp-card-scale));
    text-align: center;
    text-wrap: balance;
    /* 23.8px at the ceiling and 18.9px on a phone, taken down twice on 2026-09-20 (28.1 -> 25.9 -> 23.8).
       It is now BELOW the turn's 25.9px rather than level with it, which is right for the position it
       ended up in: above the title it is a kicker introducing the card, not a second body paragraph, and
       a kicker that outweighs the prose under the heading reads as a mis-set standfirst. What keeps it
       from disappearing at that size is the caps, the tracking and the weight, not the scale. The ladder
       that keeps the TITLE first is the black bar, not type size at all. */
    font-size: calc(var(--lp-card-scale) * clamp(1.75rem, 0.6rem + 1.4vw, 2.2rem));
    /* UPPERCASE, tracked out (user, 2026-09-20, who supplied both spacing values). The spacing is not
       decoration: set solid, a caps line loses the ascender and descender shapes a reader scans by, and
       these codas are whole sentences rather than labels, the longest being "Which option wins is
       something you see, not something you take on trust." Letter-spacing opens the glyphs, word-spacing
       at three times the letter value keeps the word boundaries visible, and together they are what stop
       a long caps line reading as one undifferentiated block. */
    text-transform: uppercase;
    letter-spacing: calc(1px * var(--lp-card-scale));
    word-spacing: calc(3px * var(--lp-card-scale));
    font-weight: 600;
    line-height: 1.3;
    /* BLACK (user, 2026-09-24). It was --lp-ink, then #555 from 2026-09-20 so it would not compete
       with the black title bar; the user has taken it back to full black. The title still leads: it is
       white type on a solid bar, which a small tracked caps line cannot out-shout at any ink. */
    color: var(--color-black, #000000);
}

/* ----------------------------------------------------------------- proof: the row */

/* THE PROOF SPANS THE WHOLE CARD (user, 2026-09-20), below the grid rather than beside the words, and the
   reason is legibility rather than layout. A screenshot in a half-card column rendered at 472px from a
   1440px capture, a 33% scale at which a 16px label in the app is 5.2px on screen and unreadable. Across
   the card it renders at 1036px, 72%, and the same label is 11.5px. Nothing about the card's own sizing
   could have bought that: the whole range between a 20px and a 44px card padding moves the image by 18px.

   The cost is height, and it is real: the comparison shot goes from 305px tall to 669px, so a screenshot
   card roughly doubles. Crop a capture to a letterbox rather than shrinking the frame back down, since
   only the SOURCE width sets the scale and trimming dead height costs nothing.

   Applied to all ten cards, not only the three with screenshots. "The same" is this page's design
   constraint, and buying legibility on three cards by making them the one shape that differs is the trade
   the constraint exists to refuse. */
.lp-proof {
    /* 20px, down from 40px (user, 2026-09-20). It was the largest gap left inside a card once the title,
       coda, button and figure spacings had all come down. */
    margin-top: calc(20px * var(--lp-card-scale));
}

/* ---------------------------------------------------------------- proof: a figure */

/* Left-aligned, full width. It used to be centred with the caption capped at 34ch, which is right under
   a huge number and wrong for three paragraphs of prose: it made the text cards read as a different
   species and squeezed them into a column half the box wide.

   THERE ARE NOW TWO PROSE ALIGNMENTS, and it is the ONE exception to "every card is identical" (see
   LandingAndFirstLoad.md, "The single-column card"). A caption that is a direct child of the figure
   CENTRES, because it sits under a centred number and belongs to it; a list item stays LEFT, because it
   sits in its own column beside two others and a centred ragged column is unreadable. The difference is
   the SHAPE of the proof rather than the card, which is the same seam rule 10 of "The card treatment"
   already draws, so it does not reopen the per-card variation the constraint forbids. The big figure
   itself has centred since 2026-09-18. */
.lp-figure {
    background: var(--lp-proof-bg);
    border: 1px solid var(--lp-proof-border);
    border-radius: calc(var(--lp-radius) - 5px * var(--lp-card-scale));
    /* 25px, down from 34px (user, 2026-09-20). It nests inside the card's own 30px, so the figure's
       contents used to sit 64px in from the card edge; 55px now. The 520px block below still narrows the
       inline axis on a small phone. */
    padding: calc(25px * var(--lp-card-scale));
    text-align: left;
}

/* The one thing in a figure proof that is CENTRED (user, 2026-09-18), and black: it is the card's
   headline, while the captions under it are prose and stay left. 54px floor, 91.8px ceiling (it was 75.6px,
   then briefly 108px; the user took it down a notch the same day). */
.lp-figure .lp-big {
    font-size: calc(var(--lp-card-scale) * clamp(5rem, 11vw, 8.5rem));
    line-height: 0.9;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--lp-figure-ink);
    text-align: center;
    display: block;
}

/* The small raised tail of a figure: the % of a rate, the s of a time, the .00 of a price. Same ink as
   the figure, so the three read as one number rather than a number and a footnote. */
.lp-figure .lp-unit {
    font-size: 0.34em;
    vertical-align: 0.55em;
    margin-left: calc(4px * var(--lp-card-scale));
    color: var(--lp-figure-ink);
    font-weight: 400;
}

/* The gap lives BETWEEN children and never before the first one, so the box's 34px padding is the whole
   distance at the top AND at the bottom. Written as a sibling rule rather than as a margin on the caption
   with a :first-child exception, because that exception has the SAME specificity as `.lp-figure
   .lp-caption` and lost to it on source order, which is how this shipped broken once already. With no
   competing declaration there is nothing left to order. */
.lp-figure > * + * {
    margin-top: calc(22px * var(--lp-card-scale));
}

/* 10px, down from 36px (user, 2026-09-20). The 36px was set when the figure sat in a half-card column
   and needed to stand apart from its own explanation; across the full card the number already dominates
   by size alone, and the air under it had become the largest gap inside the box.

   NOTE it is now SMALLER than the 22px between two captions, which inverts the original intent stated
   here and is deliberate: the caption belongs to the number, and the space between two captions is what
   separates two separate things. Written as a sibling rule rather than as margin-bottom on .lp-big, so
   it cannot depend on margin collapsing; the rule above and this one then differ only in which siblings
   they match, and this one wins on specificity rather than on which number happens to be bigger, which
   is what makes a SMALLER value here work at all. */
.lp-figure .lp-big + * {
    margin-top: calc(10px * var(--lp-card-scale));
}

.lp-figure .lp-caption {
    /* 23.8px on a desktop (it was 17.3px, smaller than the turn beside it), easing to 20.5px on a phone,
       where the proof box is the full card width rather than half of it. */
    font-size: calc(var(--lp-card-scale) * clamp(1.9rem, 2.6vw, 2.2rem));
    line-height: 1.45;
    color: var(--lp-ink-soft);
}

/* A caption that is a DIRECT child of the figure sits under the big number, and is CENTRED so that it and
   the number read as one object: left-aligned prose running the card's whole width under a centred number
   reads as a caption come loose from it. It was also capped at 62ch for one hour on 2026-09-20 and the
   user removed the cap, so the centring is all that is left. The LIST items are unaffected: each already
   sits in its own ~330px column. */
.lp-figure > .lp-caption {
    /* UNCAPPED (user, 2026-09-20). It carried max-width: 62ch when the proof first went full width, on
       the reasoning that prose running the card's whole 1080px under a centred number reads as a caption
       come loose from it. `margin-inline: auto` went with the cap, since centring a block that already
       fills its parent does nothing. Centring the TEXT is what is left, and it is what the rule is for.
       The list items are unaffected: they are not direct children, and each sits in its own column. */
    text-align: center;
}

/* A figure proof whose captions are a LIST (each led by a bold phrase: "We compute, not sell.", "The
   source.", "PAYE take-home.") is marked up as one, and each item carries a bullet (user, 2026-09-18). A
   real <ul> rather than a typed bullet, so a screen reader announces a list and its length, and the bullet
   stays out of the copy. The bullet HANGS: the item's text keeps one left edge on every line, the bullet sits in the
   gutter before it. The item keeps .lp-caption, so it is sized exactly like any other caption. The gap
   between items was the gap between captions until 2026-09-20; the grid's own `gap: 24px 40px` replaced
   the `li + li` margin when the list went multi-column, so the two numbers are now independent. */
/* ACROSS, not down (2026-09-20). A stacked list was right in a half-card column and is wrong across the
   whole card: at 1080px these captions set about 91 characters to the line, where the turn beside them is
   deliberately capped at 46. auto-fit rather than a fixed three, so the same rule gives three columns on a
   desktop, two in the middle band and one on a phone without a breakpoint of its own; the 280px floor is
   what decides where each drop happens. It also puts the four list cards into the same three-across shape
   the calculators and showcases cards were already describing in their content. */
.lp-figure .lp-points {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    /* minmax(MIN(280px, 100%), 1fr), never a bare minmax(280px, 1fr). A bare 280px floor is a HARD
       minimum: where the container is narrower the track keeps its 280px and the content overflows it.
       Measured at HEAD before the fix, the figure's content box is 266px at a 390px viewport and 236px
       at 360px, so all four list-proof cards overflowed by 14px and 44px respectively, break-even about
       403px. min(280px, 100%) lets the floor collapse to the container on anything narrower, which is
       what makes auto-fit safe at phone widths. */
    /* The 280px floor is SCALED and the 100% is not, which is the general rule stated on
       --lp-card-scale: the track floor is an absolute width and comes down with the card, while the
       percentage is already relative to a container that came down. Scaling the floor is what keeps
       the column COUNT where it was, so the list shrinks rather than relaying out. */
    grid-template-columns: repeat(auto-fit, minmax(min(calc(280px * var(--lp-card-scale)), 100%), 1fr));
    gap: calc(24px * var(--lp-card-scale)) calc(40px * var(--lp-card-scale));
    align-items: start;
}

.lp-figure .lp-points > li {
    position: relative;
    padding-left: 1em;
}

.lp-figure .lp-points > li::before {
    content: "\2022";
    position: absolute;
    left: 0;
    color: var(--lp-figure-ink);
}

/* ------------------------------------------------------------------ proof: a shot */

/* The frame, not the image, carries the border, so a 20px white mat (10px until 2026-09-18) separates the
   screenshot from its edge. White rather than the proof box's #FAFAFA on purpose: this is a mat around a
   picture, and a screenshot's own chrome is usually near-white, so anything tinted reads as a second
   frame. The image keeps no border of its own, or the mat would sit between two lines. */
.lp-shot {
    background: #FFFFFF;
    border: 1px solid var(--lp-proof-border);
    border-radius: calc(var(--lp-radius) - 5px * var(--lp-card-scale));
    padding: calc(20px * var(--lp-card-scale));
}

/* NEVER UPSCALE (2026-09-20). It was `width: 100%`, which was right while every capture was 1440px wide
   and the box 1036px: the image only ever came DOWN. The re-taken sign-in shot is 558px, its own natural
   size, because the dialog it shows is only that wide; stretched to 1036px it would be blown up 1.9x and
   soft, which is the opposite of the legibility this full-width proof exists for. max-width caps the two
   wide captures at the box exactly as before and leaves the small one at 1:1; margin-inline centres it,
   since it no longer fills the row. */
.lp-shot img {
    display: block;
    max-width: 100%;
    height: auto;
    margin-inline: auto;
}

/* A shot whose SUBJECT is small. The sign-in capture is a 558px dialog with large, sparse type, so it
   reads clearly well below its natural size, where the two full-app captures need the card's whole width.
   420px is 75% of natural, and the honest version of what that buys is narrower than "sharper": at
   DPR 1 the untouched shot was already 1:1 and could not be improved on, so the cap only makes it
   smaller; at DPR 2 the browser wants 840 device pixels and has 558, which is a 1.5x upscale where the
   untouched shot needed 2x, so it is less soft rather than sharp. Neither reading is a reason to grow
   the source: the subject is large sparse dialog type, which is why it survives the downscale at all. */
.lp-shot-narrow img {
    /* min(420px, 100%), never a bare 420px. This rule and `.lp-shot img` are both (0,1,1) and this one is
       written later, so a bare value REPLACES the max-width: 100% guard above rather than narrowing it,
       and an <img width="558"> then has nothing left to shrink it. Measured before the fix: at a 390px
       viewport the box is 266px and the shot stuck out 154px, putting a horizontal scrollbar on the whole
       page below about 544px.

       This is the SAME trap, in the same file, that `.lp-figure .lp-points` documents 48 lines above,
       introduced here within hours of that one being fixed. A cap that is not composed with 100% is the
       shape to distrust, whichever property it is written on. */
    max-width: min(calc(420px * var(--lp-card-scale)), 100%);
}

/* --------------------------------------------------------------------- responsive */

@media (max-width: 860px) {
    .lp {
        --lp-gutter: 18px;
    }

    /* The INLINE axis only. It was the `padding` shorthand until 2026-09-20, which restated the base
       block value and so silently pinned mobile to whatever that value happened to be when it was
       written; a later change to the base would have left this stuck. Narrowing one axis is what the
       rule is for, and it makes the base rule's comment structurally true rather than true by luck. */
    .lp-card {
        padding-inline: calc(24px * var(--lp-card-scale));
    }

    /* One column: the title has the card's full width, so it grows with the viewport again, reaching the
       ceiling by about 630px. Here HIERARCHY wins over one line: the title stays well above the turn (23.8px
       against 20.5px on a phone), and on the narrowest phones (about 320px, below the 335px the 520px
       block's tighter bar reaches) the longest title takes a second line rather than shrinking to the
       turn's size. Stacked cards are read one after another, so a
       two-line title costs nothing there that it would cost side by side. */
    .lp-say h2 {
        font-size: calc(var(--lp-card-scale) * clamp(2.2rem, 6.2vw, 3.6rem));
    }

    .lp-hero-in {
        padding-block: 48px;
    }
}

@media (max-width: 520px) {
    .lp-figure {
        padding: calc(32px * var(--lp-card-scale)) calc(18px * var(--lp-card-scale));
    }

    /* THE BAR TIGHTENS ON A PHONE so the two longest titles keep to ONE line (user, 2026-09-24).
       Measured in headless Edge with phone emulation: "Sophisticated engine" and "Everything explained"
       need 296px and wrapped below about a 395px viewport (272px available at 360). Half the inline
       padding, half the tracking and half the gap to the mark give back about 50px (both now 245px), so
       they fit from about a 335px viewport: one line at 360, a wrap at 320, where the bar's flex row keeps
       the mark centred on both lines. */
    .lp-say h2 {
        padding-inline: calc(15px * var(--lp-card-scale));
        letter-spacing: calc(1px * var(--lp-card-scale));
    }

    .lp .lp-card-icon {
        margin-inline-end: calc(20px * var(--lp-card-scale));
    }

    /* The staircase FLATTENS on a phone and the three lines stay. Here the H1 is 9vw, so the two steps of
       indent would leave "In your hands." with almost no room to spare, and a wrapped staircase reads as a
       mistake rather than as a device.

       ON .lp, NOT :root, which is the whole of why an earlier version of this did nothing. Every --lp-*
       token is declared on .lp, and a declaration on a descendant beats a value inherited from an
       ancestor whatever the specificity, so overriding at :root left .lp's unconditional 0.9em winning at
       every width. The 860px block above overrides --lp-gutter the correct way; this override has been
       folded into the existing 520px block rather than kept in one of its own, so the next reader sees it
       beside a correctly scoped neighbour. */
    .lp {
        --lp-hero-indent: 0;
    }
}

/* -------------------------------------------------------------------------------------------------
   THE SETUP DIALOG

   A CTA click opens this instead of navigating. Four of its values are a SHARED CONTRACT with the
   shell's own loading screen at /Design, and they are the reason "seamless" is a specification here
   rather than an aspiration: clicking Start free is a navigation to another DOCUMENT, so the browser
   discards one page and paints another and nothing can animate across it. The seam disappears only if
   both documents paint the same object in the same place, so the swap reads as the panel changing its
   contents rather than as a page load. The four, which the shell's #loading must match when it is
   restyled:

     1. Panel width      560px max, centred on both axes.
     2. Panel edge       1px --rule, 4px radius, surface fill.
     3. The ground       WAS opaque, is now the page behind it blurred (user, 2026-09-15). See the
                         ::backdrop rule below for what that trade costs at the swap.
     4. The thread       REMOVED (user, 2026-09-15). There is no download indicator in the panel, so the
                         swap has no continuous element left and the continuity is the panel itself.

   TYPE SIZE. Everything here is at least 1rem, and that is a correction rather than a preference: the
   panel first shipped with labels at 0.95rem and tile names at 0.86rem, which put its copy visibly below
   the page it opens on top of. The trap is that .lp sets font-size: 17px while this block sizes in REM,
   which resolves against the ROOT and not against .lp, so "0.95" read as 0.95 of 16px rather than of the
   page's own 17px and the gap was larger than it looked in the stylesheet.

   Everything is scoped under .lp-setup and the panel inherits the --lp-* tokens from .lp, which is why
   the dialog element sits inside it in the markup: a modal renders in the top layer, but inherited
   properties still come from its DOM ancestors.
   ------------------------------------------------------------------------------------------------- */

.lp-setup {
    /* Contract value 1. The panel is the same box in both documents.

       690px, and it is the TILE GRID that sets it (user, 2026-09-17, adding Settings and asking for rows
       of five). It was 560px while the grid was four wide. A column span of N gives (width - 56 padding -
       12(N-1) gaps)/N per tile, so five columns at 560px is 91px, under the ~100px floor the narrow
       breakpoint below already records as where a two-word name stops fitting on two lines. 690px puts a
       five-column tile back at 117px, which is exactly what four columns gave at 560px, so no tile got
       smaller and the longest single-word name ("Investments") still sets on one line. Deriving the panel
       from the tile rather than the tile from the panel is the right way round: the tile size was measured
       against real labels and the panel is free. */
    width: min(690px, calc(100vw - 32px));
    max-width: 690px;
    padding: 0;

    /* Contract value 2. */
    border: 1px solid var(--lp-rule);
    border-radius: 4px;
    background: var(--lp-proof-bg);

    color: var(--lp-ink);
    font-size: 17px;
    line-height: 1.5;

    /* The panel scrolls rather than the page: a tall grid on a short viewport has to stay reachable, and
       the thread below is a child of THIS element so it stays welded to the bottom edge while it does. */
    max-height: calc(100vh - 32px);
    /* dvh where supported: on a phone 100vh is the viewport with the address bar HIDDEN, so a vh cap
       still runs past the visible screen while the bar shows. The vh line above is the fallback. */
    max-height: calc(100dvh - 32px);
    overflow: hidden;
    box-shadow: 0 18px 50px rgba(10, 16, 23, 0.18);
}

/* Contract value 3, REVERSED (user, 2026-09-15): the page behind stays, blurred, rather than being
   painted out in the shell's opaque white.

   What that costs, stated rather than discovered later: the opaque ground existed so that the DOCUMENT
   SWAP did not flash. Clicking Start free replaces this page with the Blazor shell, which paints its own
   plain background, so a blurred landing behind the panel becomes flat white at that instant. The panel
   itself still does not move, which is the larger half of the continuity, but the ground behind it now
   changes. That is ten seconds of a better-looking dialog against one frame of discontinuity, and it is
   the trade that was chosen deliberately.

   The translucent white is not decoration: on a page this colourful the panel needs something between it
   and the blur to stay legible, and it is also the whole fallback where backdrop-filter is unsupported. */
.lp-setup::backdrop {
    background: rgba(255, 255, 255, 0.55);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}

.lp-setup-panel {
    position: relative;
    /* The top padding CLEARS THE CLOSE BUTTON, which is absolutely positioned at top:8 and is 34px tall,
       so it occupies down to 42px. This was 26px and overlapped: the cut title used to fill that space, so
       removing the title is what exposed it. Any change to the button's top or height has to move with this.

       The reason used to be "anything less and the first row's right-aligned readout renders under it", and
       that stopped being true when sex became the first row: sex has no readout at all and its control
       stops well short of the button. The INSTRUCTION is unchanged and still load-bearing, the button and
       this padding move together; what is gone is the specific collision that motivated it, so do not
       shrink this off the old sentence. */
    padding: 50px 28px 30px;
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
}

/* The way out. Escape covers the keyboard; this covers touch, which has no Escape key. Small and quiet:
   it is an escape hatch rather than an offered path, so it must not compete with Start free. */
.lp-setup-close {
    position: absolute;
    top: 8px;
    right: 10px;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--lp-muted);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
}

.lp-setup-close:hover {
    background: var(--lp-rule-faint);
    color: var(--lp-ink);
}

.lp-setup-group {
    margin-top: 22px;
}

/* The panel's own top padding clears the close button now, so the first group adds nothing. */
.lp-setup-group:first-of-type {
    margin-top: 0;
}

/* ONE grid for all three question rows, so labels, controls and typed readouts each line up in a column.
   Three separate row grids could not do that: each sized its own label column to its own label, so the
   controls started at a different x on every row. */
.lp-setup-fields {
    display: grid;
    /* max-content, not a fixed width: the column is exactly as wide as the LONGEST label and no wider,
       so the controls start right after the words rather than after a column sized for the worst case.
       A fixed 200px (sized for "Your gross work income") left every shorter label trailing dead space,
       and would have left more still when that label became "Your salary". */
    grid-template-columns: max-content minmax(0, 1fr) max-content;
    align-items: center;
    gap: 16px;
}

/* The row wrappers lay nothing out; they exist to group a question in the markup and, for income, to carry
   the class the script dims. display:contents drops their boxes so their children are items of the grid
   above. A class on a display:contents element still matches its descendants, which is what keeps the
   dimming rule working.

   BOTH SELECTORS ARE CLASSES. The income wrapper carries `lp-setup-passthrough` alongside its id rather
   than being selected by `#lp-setup-income-group`: an id is specificity 100 against a class's 10, so a
   later override of `.lp-setup-row` would have reached the two plain rows and silently not the income one. */
.lp-setup-row,
.lp-setup-passthrough {
    display: contents;
}

/* EVERY item is placed in its column EXPLICITLY, and that is what lets the sex row carry no third cell
   without a placeholder: auto-placement would otherwise pull the next label up into the empty slot.
   With column 1 pinned, a label can only ever start a new row. */
.lp-setup-label {
    grid-column: 1;
    font-size: 1.15rem;
    color: var(--lp-ink-soft);
}

.lp-setup-control {
    grid-column: 2;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

/* RIGHT-ALIGNED inside the cell, because the two boxes are not the same width: the salary one is wider to
   hold six figures. The column is max-content, so it is as wide as the wider box and left-aligning would
   leave the age box floating in the middle of it with neither edge lining up. Ending them together is what
   makes the third column read as a column. */
.lp-setup-readout-cell {
    grid-column: 3;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

.lp-setup-range {
    flex: 1 1 140px;
    min-width: 0;
    accent-color: var(--lp-accent);
    /* A hand over the WHOLE control, track included, because the track is clickable: a click anywhere on it
       moves the thumb.

       ON THE INPUT, not on the thumb pseudo-elements, and the app's own sliders are not a counter-example.
       `.range-slider-input` puts `cursor` on `::-webkit-slider-thumb` / `::-moz-range-thumb` because it is
       a TWO-KNOB overlay: the input itself takes `pointer-events: none` so the rail below shows through,
       and only the thumbs take `auto`, so the input can carry no usable cursor. This is an ordinary single
       slider that receives its own pointer events, so one declaration covers track and thumb alike.
       Styling the thumb pseudo-element here would also risk WebKit dropping the native `accent-color`
       rendering for an unstyled box, which is a real cost for no gain. (`.amount-slider-input` sets no
       cursor at all, so the app's three sliders do not agree today.) */
    cursor: pointer;
}

/* The income slider is DISABLED when the job tile is unticked (landing-setup.js), and a hand over a control
   that cannot move is the same false affordance this change removes everywhere else. The row is already
   dimmed; the cursor has to agree with it. */
.lp-setup-range:disabled {
    cursor: default;
}

/* The readout is an input, not a label: the whole reason age is a slider PLUS a typed field is that a
   slider alone cannot land on 43 with a thumb, and the readers most likely to convert are the ones who
   care about the exact number. */
.lp-setup-readout {
    flex: 0 0 auto;
    width: 4.8rem;
    padding: 6px 9px;
    border: 1px solid var(--lp-rule);
    border-radius: 4px;
    background: #FFFFFF;
    color: var(--lp-ink);
    font-size: 1.2rem;
    text-align: right;
}

.lp-setup-readout-wide {
    width: 7rem;
}

/* It sits in the readout cell beside the box now, rather than being pushed across the control row by an
   auto margin, so it stays glued to the number it belongs to. */
.lp-setup-ccy {
    font-size: 1.1rem;
    color: var(--lp-muted);
}

/* A SEGMENTED CONTROL, never a toggle: neither option is the absence of the other. Two radios with
   their labels styled as segments, so the semantics are a radio group and need no script at all. */
.lp-setup-segments {
    display: flex;
    width: 100%;
    border: 1px solid var(--lp-rule);
    border-radius: 4px;
    overflow: hidden;
}

.lp-setup-segments label {
    flex: 1 1 0;
    padding: 9px 10px;
    background: #FFFFFF;
    color: var(--lp-ink-soft);
    /* Matches the typed readouts beside the sliders, which is the panel's size for text INSIDE a control.
       It was 1.05rem, the smallest type in the row, on a control carrying one of only three answers. */
    font-size: 1.2rem;
    text-align: center;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}

.lp-setup-segments label + input + label,
.lp-setup-segments input + label:not(:first-of-type) {
    border-left: 1px solid var(--lp-rule);
}

/* THE ACCENT, not the brand navy. Every other live control in this panel is accent blue (the sliders'
   accent-color, the focus rings, the Start free button), so the one control that answered in a different
   blue read as a different kind of thing. */
.lp-setup-segments input:checked + label {
    background: var(--lp-accent);
    color: #FFFFFF;
}

/* Keyboard users get the same ring every other control on the page shows, which is the browser's own
   on a native radio and would otherwise be invisible on a visually-hidden input. */
.lp-setup-segments input:focus-visible + label {
    outline: 2px solid var(--lp-accent);
    outline-offset: -2px;
}

/* THE TILE GRID IS THE APP'S OWN ADD-MODULE PICKER, deliberately and in detail: the same glyph, the
   same name, the same 14-degree tilt, and the same dim-at-rest treatment. The picker brightens its
   icon on HOVER; here that same brightening carries STATE, because these are toggles rather than
   buttons, so dim reads as not included and bright as included. A visitor who learns these nine squares
   here meets exactly the same nine the first time they open the picker inside the simulator.

   Three by three is fixed rather than wrapping: the resolved square is the shape the count was chosen
   for. */
/* EQUIDISTANT BY CONSTRUCTION, which the first version was not: it had three 1fr columns with the tile
   capped at 122px and centred in its cell, so the horizontal spacing was the 6px gap PLUS whatever the
   cap left over (about 43px) while the vertical spacing was the 6px alone. The fix is not a tuned number
   but letting the tile fill its cell, so the only spacing anywhere is the gap. */
.lp-setup-tiles {
    display: grid;
    /* FIVE, because there are exactly five mandatory modules and this makes row one BE that set rather
       than four fifths of it (user, 2026-09-17). Twenty tiles then read as five by four. The panel was
       widened to 690px to pay for it; see .lp-setup.

       THAT PROPERTY DOES NOT SURVIVE THE 520px BREAKPOINT. At three columns the compulsory set stops
       being a row exactly where the layout is tightest (the max-width: 520px block below states the
       shape). It is not a regression to fix here, and the arithmetic cuts both ways, so state both
       sides: the panel is min(690px, 100vw - 32px) with 56px of side padding and 12px gaps, so a
       five-column tile is (panel - 104)/5 and needs a 636px viewport to clear ~100px. At 592px it is
       91px, the very figure .lp-setup cites as under the floor, and at 521px it is 77px, worse than the
       ~104px for which four columns is rejected below; four columns at 592px would give 117px. So the
       floor is already breached over roughly 115px of viewport, and the column count is defended on the
       ROW meaning rather than on tile size in that band.

       What carries the grouping on a phone is .lp-setup-tile-fixed's border-color: var(--lp-rule), and
       that one declaration specifically. Row two's neighbour at three columns is Employment, which is
       TICKED, and the rules further down already give any checked tile a full-opacity glyph, an --lp-ink
       name and the same check mark, so none of those separate the two kinds. The grey edge against the
       ticked tile's --lp-brand is the whole signal; a tidier who keeps the border and drops the rest
       loses nothing, and one who drops the border loses the distinction entirely. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 12px;
}

.lp-setup-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    /* SQUARE, as the picker's tiles are, and it FILLS its cell rather than being capped inside one: the
       cap is what made the spacing uneven. A cell is about 117px, which is close to the picker's own tiles
       anyway, so the cap was buying nothing. That 117px held at four columns in a 560px panel and holds at
       five in a 690px one, which is why the panel widened rather than the tile shrinking. */
    width: 100%;
    aspect-ratio: 1;

    padding: 6px;
    /* Transparent rather than absent, and TWO pixels rather than one, so ticking a tile only ever changes
       the edge's COLOUR and the grid cannot move. The width went to 2px with the heavier selected edge
       (user, 2026-09-16); leaving the resting state at 1px would have reintroduced exactly the shift this
       line has always existed to prevent. */
    border: 2px solid transparent;
    border-radius: 8px;
    background: transparent;
    cursor: pointer;

    /* The origin for the corner check mark below. */
    position: relative;
    transition: background 0.12s ease, border-color 0.12s ease;
}

.lp-setup-tile-glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 44px;
}

/* The 14-degree tilt and the 0.4 resting opacity are the picker's, not a new idea: app.css sets both on
   .module-image / .add-module-button .module-image. Matching them is the whole point. */
.lp-setup-tile img {
    max-width: 40px;
    max-height: 40px;
    opacity: 0.4;
    transform: rotate(14deg);
    transition: opacity 0.12s ease;
}

.lp-setup-tile-name {
    margin-top: 6px;
    font-size: 0.98rem;
    font-weight: 700;
    line-height: 1.2;
    text-align: center;
    color: var(--lp-tile-name-idle);
    overflow-wrap: break-word;
    transition: color 0.12s ease;
}

/* IN YOUR PLAN, on both kinds of tile, and it is the mark rather than the colour that says so. That split
   is the whole design: the CHECK means "this is in your plan", and the BLUE means "you chose it", so a
   compulsory tile can state its inclusion without borrowing a colour that implies an agency it does not
   have (user, 2026-09-16).

   Drawn from two borders on a rotated box rather than a "tick" CHARACTER, deliberately: the character's
   weight and shape vary by platform font and on some stacks it falls back to a missing-glyph box, which is
   unacceptable for a mark that carries meaning rather than decoration. Two numbers size it. */
.lp-setup-tile-check {
    /* INSET rather than hugging the corner (user, 2026-09-17). The glyph's own box carries a lot of
       leading above it, so 1px/6px LOOKED further in than it measured and still read as clipped to the
       edge; these numbers place the visible tick about 6px clear of the 2px border on both sides. */
    position: absolute;
    top: 5px;
    right: 10px;
    font-size: 1.7rem;
    line-height: 1;
    color: var(--lp-ink);

    /* BOLDER THAN font-weight CAN MAKE IT, which is why the stroke is here rather than a bigger number.
       The panel inherits the system UI stack, whose heaviest face is Bold (700), so `font-weight: 900`
       clamps to 700 and asking for more weight changes nothing at all: the mark looked thin however high
       that number went (user, 2026-09-17, asking twice for bolder). A text stroke thickens the GLYPH
       itself and is the only thing that does. In em, so it tracks the font-size above rather than needing
       a second edit; the unprefixed property does not exist, and the -webkit- one is honoured by Chrome,
       Safari, Edge AND Firefox, so this is not a one-engine flourish. */
    font-weight: 700;
    -webkit-text-stroke: 0.045em var(--lp-ink);

    /* Hidden by default and revealed by the two selected states below, so the DEFAULT is "not in the
       plan" and a tile has to earn its mark. */
    display: none;
}

/* The character rather than a CSS-drawn box (user, 2026-09-17). U+2713 is present in the system font on
   every platform this page is served to, and at weight 900 it reads as a deliberate mark rather than a
   hairline. In CSS rather than the markup so the span stays empty and aria-hidden carries cleanly. */
.lp-setup-tile-check::before {
    content: "\2713";
}

.lp-setup-tile-fixed .lp-setup-tile-check,
.lp-setup-tile .lp-setup-tile-input:checked ~ .lp-setup-tile-check,
.lp-setup-tile:has(.lp-setup-tile-input:checked) .lp-setup-tile-check {
    display: block;
}

/* COMPULSORY: every plan has this module whatever the visitor does, so the tile STATES it rather than
   offering it. White ground rather than the ticked state's alice blue, because blue is reserved for "you
   chose this" -- though the white is one step off the panel's own #FAFAFA and so nearly invisible, which
   means the NEUTRAL EDGE and the check mark carry this state's whole signal and the ground only keeps it
   from reading as the transparent unticked one; a neutral edge for that reason; and no pointer, because there is no checkbox behind it
   and a tile that repaints under the cursor promises a click that does nothing. The glyph and name are at
   full strength: it IS in the plan.

   NO HOVER REACHES THIS TILE, and it is the hover block that enforces that, by excluding this class from
   every one of its arms. This comment said "no :hover rule anywhere" and was WRONG for a day: writing no
   NEW hover rule here is not the same as escaping the one that already matched .lp-setup-tile, which is
   (0,2,0) against this rule's (0,1,0) and so won. Pinned by
   TestGatedPublicSurface.NoHoverRule_ReachesACompulsoryTile, because no markup test can see it. */
.lp-setup-tile-fixed {
    border-color: var(--lp-rule);
    background: #FFFFFF;
    cursor: default;
}

.lp-setup-tile-fixed img {
    opacity: 1;
}

.lp-setup-tile-fixed .lp-setup-tile-name {
    color: var(--lp-ink);
}

/* ONE off-screen recipe for all THREE things this panel hides from sight but not from a reader: the
   compulsory tile's "Always included" line, the tile checkboxes, and the segmented control's radios. The
   first needs it because the check mark is the only other thing saying a compulsory tile is always in the
   plan, and a mark is no use to somebody who cannot see it. Two copies of this clip 120 lines apart is the duplication CodingConventions section 6
   forbids, and they had already drifted by a `border: 0`. The review that caught this found TWO copies;
   there were three, the segmented control's radios being the one nobody had looked at. */
.lp-visually-hidden,
.lp-setup-tile-input,
.lp-setup-segments input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* THE LIT STATE. Brand blue on the card ground, with the glyph and the name at full strength. A ticked
   tile stays blue while the cursor is on it rather than greying.

   THAT IS HELD BY THE HOVER BLOCK EXCLUDING THIS STATE, NOT BY SPECIFICITY, and the difference is a
   regression this file shipped. The comment here read "declared FIRST and is (0,3,0) against hover's
   (0,2,0)", which was true while the selector was `.lp-setup-tile-input:checked + .lp-setup-tile`
   against a bare `.lp-setup-tile:hover`. The scroll fix rewrote both sides and silently levelled them:
   `:has()` and `:not()` each take their ARGUMENT's specificity, so
   `.lp-setup-tile:has(.lp-setup-tile-input:checked)` is (0,3,0) and
   `.lp-setup-tile:not(.lp-setup-tile-fixed):hover` is ALSO (0,3,0). A tie goes to the later rule, and
   hover is declared below, so every ticked tile greyed out under the cursor: the exact thing the hover
   block's own comment argues must never happen, since the edge and the ground are the only signals
   separating hovered from ticked.

   So the hover arms now carry `:not(:has(.lp-setup-tile-input:checked))` and simply do not match a
   ticked tile. That is order-independent and semantic rather than arithmetic, which matters because an
   arithmetic argument is what broke: a selector rewrite can move specificity without touching either
   rule's declarations. (pinned: TestGatedPublicSurface.NoHoverRule_ReachesATickedTile) */
/* THE SIBLING SPELLING IS A FALLBACK, NOT A LEFTOVER. `:has()` is Firefox 121+ and Safari 15.4+, and
   before it the tile would have shown NO selected state at all on an older engine, which is worse than
   the state being slightly plainer. The input is the label's FIRST child, so `~` reaches every later
   sibling and carries the glyph, the name and the check mark everywhere; only the tile's own edge and
   ground genuinely need `:has()`, because no selector can style an ancestor without it. So an old engine
   loses the blue edge and keeps the mark, the full-strength glyph and the ink name, which still says
   "on". Both spellings set the same declarations, so a browser that understands both simply applies the
   same thing twice. */
.lp-setup-tile .lp-setup-tile-input:checked ~ img,
.lp-setup-tile:has(.lp-setup-tile-input:checked) img {
    opacity: 1;
}

.lp-setup-tile .lp-setup-tile-input:checked ~ .lp-setup-tile-name,
.lp-setup-tile:has(.lp-setup-tile-input:checked) .lp-setup-tile-name {
    color: var(--lp-ink);
}

.lp-setup-tile:has(.lp-setup-tile-input:checked) {
    border-color: var(--lp-brand);
    background: var(--lp-card-bg);
}

/* HOVER TAKES THE SHAPE OF THE LIT STATE AND NOT ITS COLOUR, which is the distinction that matters.
   The glyph and the name go to full strength for both, so hovering shows the visitor exactly how much
   this tile will stand out once it is on; only the EDGE and the GROUND tell them whether it already is:
   a LIGHT ground under a DARK edge, both neutral greys rather than the blue-tinted --lp-rule pair, so a
   hovered tile cannot be mistaken at a glance for a ticked one.

   Making hover identical to checked was tried first and is wrong for one specific gesture: UNTICKING.
   The cursor is still on the tile at the moment of the click, so an identical hover state means the tile
   does not visibly change and the visitor cannot tell the click registered. The blue has to mean "on"
   and nothing else.

   GATED ON A REAL POINTER, because that whole argument is about a cursor that stays put. A touch
   browser leaves :hover latched on the last element tapped, so without this the tile a visitor just
   UNTICKED would keep the full-strength glyph and full-ink name and lose only the blue, which is a
   weaker signal than the desktop one for precisely the gesture the greys were chosen for. The dialog
   has 520px and 380px breakpoints, so phones are squarely in scope. The selectors are spelled out
   again rather than shared with the checked rules above, because a selector list cannot straddle an
   at-rule. */
@media (hover: hover) {
    /* TWO EXCLUSIONS ON EVERY ARM, for the same reason in two directions: hover must not repaint a tile
       whose state is already settled. `:not(.lp-setup-tile-fixed)` keeps it off a COMPULSORY tile, which
       is a statement rather than a control; `:not(:has(.lp-setup-tile-input:checked))` keeps it off a
       TICKED one, whose blue has to mean "on" and nothing else. Both are stated here rather than left to
       the cascade, because the cascade is what failed: see the lit state's comment above.

       Excluding the compulsory tile HERE rather than raising the specificity of
       .lp-setup-tile-fixed is deliberate: this is where the intent belongs, and the alternative loses the
       cascade anyway. `.lp-setup-tile:hover` is (0,2,0) and `.lp-setup-tile-fixed` is (0,1,0), so the
       compulsory tile's own white ground LOST to the hover grey and the four stated tiles greyed on
       hover like buttons (user, 2026-09-17). The .lp-setup-tile-fixed rule above claimed "no :hover rule
       anywhere" while this block was quietly matching it, because omitting a new rule is not the same as
       escaping an existing one. */
    .lp-setup-tile:not(.lp-setup-tile-fixed):not(:has(.lp-setup-tile-input:checked)):hover img {
        opacity: 1;
    }

    .lp-setup-tile:not(.lp-setup-tile-fixed):not(:has(.lp-setup-tile-input:checked)):hover .lp-setup-tile-name {
        color: var(--lp-ink);
    }

    .lp-setup-tile:not(.lp-setup-tile-fixed):not(:has(.lp-setup-tile-input:checked)):hover {
        border-color: var(--lp-hover-border);
        background: var(--lp-hover-bg);
    }
}

.lp-setup-tile:has(.lp-setup-tile-input:focus-visible) {
    outline: 2px solid var(--lp-accent);
    outline-offset: 2px;
}

/* Dimmed rather than hidden, so unticking the job explains itself instead of making a group silently
   inert. The values are left alone, so re-ticking restores what was typed.

   EVERY CELL OF THE ROW, and the LABEL was missing until 2026-09-17 (user: "when the employment is
   disabled we should also gray the label"). A row whose control and readout dim while its label stays at
   full strength reads as half-disabled: the eye takes the brightest thing in the row as the live one, so
   "Your salary" went on asserting itself over the greyed-out number beside it. This is the same mistake the
   hover block's own remarks warn about one screen up, where the glyph, the name and the ground each needed
   their own arm and fixing one would have left a tile that half-responds.

   The narrow breakpoint below ENUMERATES the row's cells, so that list is the population this rule owes
   coverage of, and a cell added there without one here is the same defect again. No count is stated on
   purpose: the guard derives it, and a number written here would be the very claim the parse exists to
   avoid making. (pinned: TestGatedPublicSurface.EveryCellOfAnInertRow_IsDimmed) */
.lp-setup-inert .lp-setup-label,
.lp-setup-inert .lp-setup-control,
.lp-setup-inert .lp-setup-readout-cell {
    opacity: 0.45;
}

/* 26px from the tiles to the button, as before the bar existed: the bar's row sits in that gap (6px above
   it, its own ~15px line, 8px below), so the resting panel keeps its spacing to within a few pixels. */
.lp-setup-act {
    margin-top: 6px;
}

/* TWO CLASSES, and the second is load-bearing, the same trap as .lp-hero .lp-hero-sub: app.css sets
   `button.btn { font-weight: 500 }` at (0,1,1), which outranks a lone class, so a single-class rule here
   lost its 600 to it while every other declaration applied. The Plan & Play buttons never met it because
   they are <a> elements. */
.lp-setup-act .lp-setup-start {
    width: 100%;
    padding-block: 13px;
    /* 1.3em, 600, tracked 2px (user, 2026-09-24), so the panel's one action carries the weight of the
       page's Plan & Play buttons rather than reading as a form's submit. */
    font-size: 1.3em;
    font-weight: 600;
    letter-spacing: 2px;
    /* A button element defaults to the arrow cursor in every browser, and this one is the panel's whole
       purpose, so it says so on hover like any other primary action. */
    cursor: pointer;
}

/* While a click waits for the download the button is BUSY: aria-disabled rather than disabled, so it keeps
   keyboard focus (landing-setup.js, showWaiting), and it keeps its full colour, because Bootstrap's faded
   disabled look reads as "unavailable". The progress cursor says the rest. */
.lp-setup-start[aria-disabled="true"] {
    cursor: progress;
}

/* The download bar ABOVE Start free, with its percentage, shown only while a click waits for the runtime
   (see the markup for why above). The row is always in the layout and only made visible, so nothing moves
   when it appears. */
.lp-setup-progress-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
    visibility: hidden;
}

.lp-setup-progress-active {
    visibility: visible;
}

/* The same two tokens the cut edge thread used: the faint rule as the track and the accent as the fill. A
   native <progress>, so the track and the fill are styled through each engine's own pseudo-elements. */
.lp-setup-progress {
    flex: 1;
    min-width: 0;
    height: 6px;
    border: 0;
    border-radius: 3px;
    background: var(--lp-rule-faint);
    color: var(--lp-accent);
    -webkit-appearance: none;
    appearance: none;
}

/* Small and quiet, and a fixed width in tabular figures, so the bar does not twitch as 9% becomes 10%. */
.lp-setup-progress-pct {
    min-width: 4ch;
    font-size: 0.9rem;
    line-height: 1;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--lp-muted);
}

/* The answers while a Start free click waits: inert (set by the script) and dimmed, so they read as
   locked rather than merely failing to respond. The same 45% the unticked-job income row uses.

   Dimmed at the CELLS, not on the .lp-setup-locked section, because opacity multiplies down the tree: a
   section at 0.45 over the income row's own 0.45 would take that row to about 20% while everything else
   sat at 45%. Set on the same elements the inert rule sets, the two simply agree. It also keeps the inert
   rule the ONLY one that sets an inert cell's opacity, which TestGatedPublicSurface
   .EveryCellOfAnInertRow_IsDimmed requires. */
.lp-setup-locked .lp-setup-label,
.lp-setup-locked .lp-setup-control,
.lp-setup-locked .lp-setup-readout-cell,
.lp-setup-locked .lp-setup-tiles {
    opacity: 0.45;
    transition: opacity 0.2s ease;
}

.lp-setup-progress::-webkit-progress-bar {
    border-radius: 3px;
    background: var(--lp-rule-faint);
}

.lp-setup-progress::-webkit-progress-value {
    border-radius: 3px;
    background: var(--lp-accent);
    transition: width 0.25s linear;
}

.lp-setup-progress::-moz-progress-bar {
    border-radius: 3px;
    background: var(--lp-accent);
}

@media (max-width: 520px) {
    .lp-setup-panel {
        padding: 46px 18px 24px;
    }

    /* The three columns collapse to one: at this width even a max-content label column leaves nothing for
       the control, and a readout beside it less than nothing. Each item spans the single column, so a
       question reads as label, then control, then readout, down the panel. */
    .lp-setup-fields {
        grid-template-columns: minmax(0, 1fr);
        gap: 6px;
    }

    .lp-setup-label,
    .lp-setup-control,
    .lp-setup-readout-cell {
        grid-column: 1;
    }

    /* ON THE LABEL, which every question has, never on the readout cell, which SEX does not. Hanging the
       question separator on the readout gave the sex-to-age boundary the bare 6px stacking gap while
       age-to-salary got 16px, so the panel read as "sex and age glued, salary apart" on a phone. Master was
       a uniform 16px because the margin sat on the row, and the row has no box of its own any more. */
    .lp-setup-label {
        margin-top: 10px;
    }

    .lp-setup-row:first-child .lp-setup-label {
        margin-top: 0;
    }

    /* LEFT here, and the desktop rule's reason is exactly why. Ending the boxes together makes a
       max-content third COLUMN read as a column; at this width there is no third column, the single one is
       1fr, so right-alignment would push a lone number box to the far edge of the panel, away from both the
       slider it mirrors and its own left-aligned label. */
    .lp-setup-readout-cell {
        justify-content: flex-start;
    }
}

/* The tile grid is the one thing that must not reflow into a ragged shape: five by four is the desktop
   shape, and two columns of ten and a half is not it. Below this the tiles simply get tighter.

   Three columns rather than four, because four would squeeze a tile under about 100px, where
   "Current Account" stops fitting on two lines. THAT ~100px IS THE FLOOR THIS WHOLE GRID IS SIZED
   AGAINST, and it is the number any change to the column count has to clear. A column span of N gives
   (panel - 56 padding - 12(N-1) gaps)/N per tile, which is what set the desktop panel at 690px when the
   grid went to five columns: five at 560px would have been 91px, under the floor.

   The two sentences here read "three by three is the shape" and "Twelve tiles then read as 3 by 4
   instead of 4 by 3" until 2026-09-17, both left behind by the 9 -> 12 -> 20 growth. Twenty over three
   columns is six rows and a last row of TWO, which is the short row the desktop shape is chosen to
   avoid; it is tolerated here rather than endorsed, because twenty has no three-column rectangle at all
   and a phone scrolls the panel anyway. Four columns WOULD be rectangular and is deliberately not taken:
   at this breakpoint the panel is 100vw - 32px with 18px padding, so a four-column tile is about 104px at
   520px wide and shrinks with the viewport, crossing the floor immediately below it. */
@media (max-width: 520px) {
    .lp-setup-tiles {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (max-width: 380px) {
    .lp-setup-tile-glyph {
        height: 38px;
    }

    .lp-setup-tile img {
        max-width: 34px;
        max-height: 34px;
    }

    .lp-setup-tile-name {
        font-size: 0.88rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .lp * {
        transition: none !important;
    }

    .lp-ask {
        animation: none;
    }
}
