/* Public design system: handwritten CSS outside Tailwind, with pub- classes only.
   Define both themes together. Self-host Inter for the wordmark; use system text fonts.
   Keep tokens independent from the app. See docs/public-site.md. */

/* --- The name ------------------------------------------------------------- */
/* "RinVis", wherever it stands as the name rather than as a word in a sentence, is
   Inter Bold at -0.02em — the same face and tracking as the lockup in the logo
   files. A private family name, so a visitor's own installed Inter is never mixed
   in, and `swap` so the name is never invisible while the 7 KB arrives. The same
   file serves the app's `.rv-wordmark` (input.css); the two rules are kept apart
   like every other token the systems happen to share. */
@font-face {
    font-family: "RinVis Wordmark";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("../fonts/inter-bold-latin.8a8f252e2442.woff2") format("woff2");
}

/* --- Tokens, both themes -------------------------------------------------- */
:root {
    color-scheme: light;

    /* The two ends of the logo (static/img/rinvis-lg.svg): its left bracket is
       #1b2a6b and its right one #35beec, the darkest and lightest points in the
       mark. Everything below is one of these two, or a neutral tinted toward
       them. They are brand constants and are never redefined per theme. */
    --pub-brand-dark: #1b2a6b;
    --pub-brand-light: #35beec;

    /* Neutrals: a near-white with a little warmth and an almost-black ink, so the
       one color on the page is the mark's own navy. */
    --pub-bg: #f1f2ef;
    --pub-bg-2: #fafaf8;
    --pub-ink: #101317;
    --pub-ink-dim: #48505a;
    --pub-ink-dimmer: #5d6570;
    --pub-line: #d2d5da;
    --pub-line-strong: #98a0aa;

    /* The accent is whichever end can carry text against this theme's card. On
       the light card that is the dark edge; the light edge manages under 2:1 there
       and is therefore only ever a graphic. */
    --pub-accent: var(--pub-brand-dark);
    --pub-accent-soft: rgb(27 42 107 / 0.08);

    /* The illustration is graded left-to-right between the same two edges, in
       the same order the logo puts them in. */
    --pub-edge-l: var(--pub-brand-dark);
    --pub-edge-r: var(--pub-brand-light);

    /* The three brands on the front page's shelf: the two ends of the mark and the
       mid blue they fade through — three colors the logo already contains. The cross
       beside a board that holds the wrong one is the one hue on this site that is not
       a blue, because "wrong" in any blue reads as "a fourth brand". */
    --pub-brand-a: var(--pub-brand-dark);
    --pub-brand-b: #2b83b9;
    --pub-brand-c: var(--pub-brand-light);
    --pub-bad: #b42318;
    /* The amber a fill figure turns on its way to red. Dark enough to carry an
       11px figure on white; the dark theme lifts it. */
    --pub-warn: #a15c07;
    --pub-tray: #cfd5e3;

    /* Square corners everywhere a box is drawn, and no drop shadows: a frame is
       a frame, not a card floating over the page. */
    --pub-radius: 0;
    --pub-radius-sm: 6px;
    --pub-shadow: none;

    --pub-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
        Arial, sans-serif;
    /* The readout face: eyebrows, notes, buttons, the share chips. System stack,
       for the same no-webfont reason as the sans. */
    --pub-wordmark: "RinVis Wordmark", var(--pub-sans);
    --pub-lens-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    /* The viewfinder's dot field on the front page, drawn by lens.js. */
    --pub-lens-dot: #101317;
}

/* Dark is a choice, never a default. The public site opens light whatever the
   operating system says, and the switch in the header (js/public/theme.js) sets
   data-theme="dark" on <html> and remembers it in this browser. So this block keys
   on the attribute alone, and there is no prefers-color-scheme query: the system
   setting no longer decides anything here. */
:root[data-theme="dark"] {
    color-scheme: dark;

    --pub-bg: #07090c;
    --pub-bg-2: #0d1116;
    --pub-ink: #e6edf3;
    --pub-ink-dim: #93a0ad;
    --pub-ink-dimmer: #7d8a97;
    --pub-line: #1e2830;
    --pub-line-strong: #3b4b58;

    /* Roles swap: the light edge carries text here and the dark edge cannot — it
       is too close to the background. So the illustration gradient's left stop
       becomes #2b83b9, which is not an invention: it is the logo's own mid blue,
       the color its top and bottom brackets fade through. */
    --pub-edge-l: #2b83b9;
    /* The dark edge is unreadable here, so brand A lifts to a navy that still
       reads as the dark end against the card. */
    --pub-brand-a: #5b6fd6;
    --pub-bad: #f97066;
    --pub-warn: #fbbf24;
    --pub-tray: #2a3457;
    --pub-accent: var(--pub-brand-light);
    --pub-accent-soft: rgb(53 190 236 / 0.12);
    --pub-lens-dot: #9fdcf2;
}

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

body.pub-body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: var(--pub-sans);
    font-size: 16px;
    line-height: 1.55;
    color: var(--pub-ink);
    background: var(--pub-bg);
    -webkit-font-smoothing: antialiased;
}

img,
svg {
    max-width: 100%;
}

/* --- Skeleton ------------------------------------------------------------- */
.pub-main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem 1.5rem 4rem;
}

/* Positioned so lens.js can lay the scan beam across it. */
.pub-container {
    position: relative;
    width: 100%;
    max-width: 1240px;
    margin: 0 auto;
}

.pub-skip-link {
    position: absolute;
    left: -9999px;
}
.pub-skip-link:focus {
    left: 1rem;
    top: 1rem;
    z-index: 10;
    padding: 0.5rem 0.85rem;
    background: var(--pub-bg-2);
    color: var(--pub-ink);
    border: 1px solid var(--pub-line-strong);
    border-radius: var(--pub-radius-sm);
}

:focus-visible {
    outline: 2px solid var(--pub-accent);
    outline-offset: 2px;
}

/* Responsive native details menu; the theme switch stays outside at every width. */
.pub-header {
    background: var(--pub-bg-2);
    border-bottom: 1px solid var(--pub-line);
    position: relative;
    z-index: 10;
}
.pub-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 1px;
    background: var(--pub-accent);
}

.pub-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    flex-wrap: wrap;
    max-width: 1304px;
    margin: 0 auto;
    padding: 0.7rem 1.25rem;
}

/* The menu and the theme switch sit together on the right. */
.pub-nav__menu {
    margin-left: auto;
}

.pub-nav__brand {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    color: var(--pub-ink);
    text-decoration: none;
    font-weight: 700;
    letter-spacing: -0.02em;
    font-size: 1.1rem;
}
.pub-wordmark {
    font-family: var(--pub-wordmark);
    font-weight: 700;
    letter-spacing: -0.02em;
}
.pub-nav__brand img {
    width: 28px;
    height: 28px;
}

.pub-nav__links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.15rem 0.25rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.pub-nav__links a {
    display: inline-block;
    padding: 0.4rem 0.7rem;
    border-radius: var(--pub-radius-sm);
    color: var(--pub-ink-dim);
    font-size: 0.92rem;
    font-weight: 500;
    text-decoration: none;
}
.pub-nav__links a:hover {
    color: var(--pub-ink);
    background: var(--pub-accent-soft);
}
/* The current page: ink rather than accent, with a rule under the word. A filled pill
   would make it read as the call to action beside it. */
.pub-nav__links a[aria-current="page"] {
    color: var(--pub-ink);
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--pub-accent);
    border-radius: var(--pub-radius-sm) var(--pub-radius-sm) 0 0;
}

/* Contact is the one thing the site asks a visitor to do, so it is the one filled
   control in the row. Same face as .pub-btn, smaller. */
.pub-nav__links a.pub-nav__cta {
    margin-left: 0.35rem;
    color: var(--pub-bg-2);
    background: var(--pub-accent);
    font-weight: 600;
}
.pub-nav__links a.pub-nav__cta:hover {
    filter: brightness(1.1);
}

/* The menu button: shown only below the breakpoint. A 40px square — the bars alone,
   with "Menu" kept for screen readers — so the header stays one short row. The bars are
   drawn by CSS so there is no icon request. */
.pub-nav__toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 1px solid var(--pub-line);
    border-radius: var(--pub-radius-sm);
    color: var(--pub-ink);
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.pub-nav__toggle-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.pub-nav__toggle::-webkit-details-marker {
    display: none;
}
.pub-nav__toggle:hover {
    background: var(--pub-accent-soft);
}
.pub-nav__burger,
.pub-nav__burger::before,
.pub-nav__burger::after {
    display: block;
    width: 18px;
    height: 2px;
    border-radius: 1px;
    background: currentColor;
}
.pub-nav__burger {
    position: relative;
}
.pub-nav__burger::before,
.pub-nav__burger::after {
    content: "";
    position: absolute;
    left: 0;
}
.pub-nav__burger::before {
    top: -6px;
}
.pub-nav__burger::after {
    top: 6px;
}
/* Open, the bars become a cross: the same control closes the panel. */
.pub-nav__menu[open] .pub-nav__burger {
    background: transparent;
}
.pub-nav__menu[open] .pub-nav__burger::before {
    top: 0;
    transform: rotate(45deg);
}
.pub-nav__menu[open] .pub-nav__burger::after {
    top: 0;
    transform: rotate(-45deg);
}

/* Wide: the summary is gone and the list shows whether or not the <details> is open.
   `::details-content` is what a closed <details> hides its body with; a browser that
   predates it shows the menu button instead, which still works. */
@media (min-width: 760px) {
    .pub-nav__menu::details-content {
        content-visibility: visible;
    }
}

/* Narrow: Menu on the right of the mark, the links a full-width panel under the
   header. The <details> stays unpositioned so the panel anchors to .pub-header. */
@media (max-width: 759.98px) {
    .pub-nav {
        flex-wrap: nowrap;
        padding-top: 0.3rem;
        padding-bottom: 0.3rem;
    }
    .pub-nav__toggle {
        display: inline-flex;
    }
    .pub-nav__links {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        padding: 0.5rem 0.75rem 0.85rem;
        margin-top: 2px;
        background: var(--pub-bg-2);
        border-bottom: 1px solid var(--pub-line);
        box-shadow: 0 12px 24px -12px rgb(0 0 0 / 0.25);
    }
    .pub-nav__links a {
        display: flex;
        align-items: center;
        min-height: 48px;
        padding: 0 0.85rem;
        font-size: 1rem;
    }
    /* A rule beside the word rather than under it: in a column, an underline reads as
       a divider between two rows. */
    .pub-nav__links a[aria-current="page"] {
        box-shadow: inset 3px 0 0 var(--pub-accent);
        border-radius: 0 var(--pub-radius-sm) var(--pub-radius-sm) 0;
    }
    .pub-nav__links a.pub-nav__cta {
        justify-content: center;
        margin: 0.5rem 0 0;
    }
}

/* --- Theme switch ---------------------------------------------------------
   One icon button: a moon on the light page, a sun on the dark one. Rendered
   hidden and shown by js/public/theme.js, so with JS off there is no control
   that does nothing. Which icon shows is read off <html>'s data-theme here, not
   toggled on the <svg> by script: an <svg> has no .hidden property. */
.pub-theme-switch {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--pub-line);
    border-radius: var(--pub-radius-sm);
    background: none;
    color: var(--pub-ink);
    cursor: pointer;
}
.pub-theme-switch[hidden] {
    display: none;
}
.pub-theme-switch:hover {
    background: var(--pub-accent-soft);
}
.pub-theme-switch svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.pub-theme-switch__sun,
:root[data-theme="dark"] .pub-theme-switch__moon {
    display: none;
}
:root[data-theme="dark"] .pub-theme-switch__sun {
    display: block;
}

/* Pressing the switch opens the new theme as a circle from the button, where the
   browser has view transitions; elsewhere it simply changes. theme.js sets the
   three custom properties from the button's position. */
::view-transition-old(root),
::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
}
::view-transition-new(root) {
    animation: pub-theme-reveal 0.6s cubic-bezier(0.6, 0, 0.3, 1);
}
@keyframes pub-theme-reveal {
    from {
        clip-path: circle(0 at var(--pub-vt-x, 100%) var(--pub-vt-y, 0%));
    }
    to {
        clip-path: circle(var(--pub-vt-r, 150vmax) at var(--pub-vt-x, 100%) var(--pub-vt-y, 0%));
    }
}

/* --- Hero ---------------------------------------------------------------------
   The front page opens on a viewfinder: a framed panel, the name and the one
   sentence on the left, the shelf on the right, and a live dot under the name.
   lens.js adds the dot field, the corner brackets, the reticle and the load scan
   on top; without it this is the still frame. Below 800px it folds into one
   column. */
.pub-hero {
    position: relative;
    display: grid;
    grid-template-columns: 1.05fr 1fr;
    grid-template-rows: auto auto auto auto;
    column-gap: 4rem;
    row-gap: 1rem;
    flex-direction: column;
    align-items: center;
    align-content: center;
    width: 100%;
    min-height: min(80svh, 820px);
    margin: 0 auto;
    padding: 4rem 4.5rem;
    text-align: left;
    background: var(--pub-bg-2);
    border: 1px solid var(--pub-line);
    border-radius: 0;
    isolation: isolate;
    overflow: clip;
}
.pub-hero > * {
    position: relative;
}

.pub-hero__mark {
    grid-column: 1;
    align-self: end;
    width: 52px;
    height: 52px;
}

.pub-hero__title {
    grid-column: 1;
    margin: 0;
    font-size: clamp(4rem, 9vw, 8.5rem);
    line-height: 0.9;
    /* Face, weight and tracking come from .pub-wordmark: it is the name. */
}

/* One sentence under the name saying what the product is. It took the place of the
   "Coming soon" badge and the development caveat under it; a hero that opens with a
   qualifier is a hero apologizing for itself. */
.pub-hero__tagline {
    grid-column: 1;
    margin: 0.1rem 0 0;
    max-width: 24ch;
    font-size: clamp(1.6rem, 2.7vw, 2.6rem);
    line-height: 1.12;
    font-weight: 400;
    letter-spacing: -0.02em;
    color: var(--pub-ink);
    text-wrap: balance;
}

/* The status line under the name, with a dot that pulses like a recording light. */
.pub-hero__note {
    grid-column: 1;
    grid-row: 4;
    align-self: start;
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0;
    max-width: 34ch;
    font-family: var(--pub-lens-mono);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--pub-ink-dim);
}
.pub-hero__note::before {
    content: "";
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: var(--pub-accent);
    box-shadow: 0 0 0 0 var(--pub-accent);
    animation: pub-lens-live 2.4s ease-out infinite;
}
@keyframes pub-lens-live {
    0% {
        box-shadow: 0 0 0 0 color-mix(in srgb, var(--pub-accent) 55%, transparent);
    }
    70%,
    100% {
        box-shadow: 0 0 0 0.7rem transparent;
    }
}

/* The shelf and its share line take the right-hand column. */
.pub-hero > .pub-shelf {
    grid-column: 2;
    grid-row: 1 / 4;
    justify-self: center;
    max-width: 540px;
    margin: 2rem 0 0;
}
.pub-hero > .pub-shelf__share {
    grid-column: 2;
    grid-row: 4;
    align-self: start;
}

@media (max-width: 800px) {
    .pub-hero {
        display: flex;
        min-height: 0;
        padding: 3rem 1.5rem 2.5rem;
    }
}
@media (max-width: 560px) {
    .pub-hero {
        align-items: flex-start;
    }
    .pub-hero__mark {
        align-self: flex-start;
    }
    .pub-hero__title {
        font-size: 4.25rem;
    }
    .pub-hero__tagline {
        font-size: 1.65rem;
    }
    .pub-hero > .pub-shelf {
        margin-top: 1rem;
    }
    .pub-hero > .pub-shelf__share {
        gap: 0.4rem;
    }
}
@media (prefers-reduced-motion: reduce) {
    .pub-hero__note::before {
        animation: none;
    }
}

/* --- The shelf illustration -----------------------------------------------
   A shelf as a camera sees it: three boards and a floor, blocks in three brand
   colors, each board tinted with the brand it is meant to hold. Inline SVG
   rather than an image file, so it is sharp at any size and follows the theme
   without a second asset. Wider than the frame on both sides, for the mark on
   the left of each board and the fill figure on its right. */
.pub-shelf {
    width: 100%;
    max-width: 440px;
    height: auto;
    margin-top: 0.35rem;
}
.pub-shelf__frame {
    fill: none;
    stroke: var(--pub-line-strong);
    stroke-width: 1.5;
    stroke-dasharray: 5 6;
}
.pub-shelf__board {
    fill: var(--pub-line-strong);
}
.pub-shelf__board--1 {
    fill: var(--pub-brand-a);
}
.pub-shelf__board--2 {
    fill: var(--pub-brand-b);
}
.pub-shelf__board--3 {
    fill: var(--pub-brand-c);
}
.pub-shelf__floor {
    fill: var(--pub-line-strong);
}
/* One flat color per brand — a block has to say what it is from across the room,
   which a gradient graded by position cannot. */
.pub-shelf__block--1 {
    fill: var(--pub-brand-a);
}
.pub-shelf__block--2 {
    fill: var(--pub-brand-b);
}
.pub-shelf__block--3 {
    fill: var(--pub-brand-c);
}
/* The marks: a check in the accent, a cross in the one non-blue. Both are strokes
   on a transparent group, and only ever one of the two is visible. */
.pub-shelf__mark path {
    fill: none;
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.pub-shelf__mark-ok {
    stroke: var(--pub-accent);
}
.pub-shelf__mark-bad {
    stroke: var(--pub-bad);
}
.pub-shelf__fill {
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    fill: var(--pub-ink-dim);
}
/* A board running low, then nearly out. The script sets these from the figure, and
   the figure's own label says the same thing in words. */
.pub-shelf__fill--low {
    fill: var(--pub-warn);
}
.pub-shelf__fill--out {
    fill: var(--pub-bad);
}

/* One slow pass down the frame — a capture, not a loading spinner. */
.pub-shelf__scan {
    fill: var(--pub-accent);
    animation: pub-scan 5s ease-in-out infinite;
}
@keyframes pub-scan {
    0% {
        transform: translateY(0);
        opacity: 0;
    }
    10% {
        opacity: 0.5;
    }
    60% {
        transform: translateY(var(--pub-scan-travel, 149px));
        opacity: 0.5;
    }
    75%,
    100% {
        transform: translateY(var(--pub-scan-travel, 149px));
        opacity: 0;
    }
}
@media (prefers-reduced-motion: reduce) {
    .pub-shelf__scan {
        animation: none;
        opacity: 0.35;
    }
}

/* --- The shelf as a toy ----------------------------------------------------
   js/public/shelf-toy.js makes the blocks draggable and sets data-interactive on
   the drawing once it has. Everything below is conditional on that attribute or
   on a class the script adds, so with no script the picture is the one above.
   The scan line paints last and would take every pointerdown meant for a block
   under it, which is why it and the other static shapes let pointers through. */
.pub-shelf {
    user-select: none;
    -webkit-user-select: none;
}
.pub-shelf__scan,
.pub-shelf__frame,
.pub-shelf__board,
.pub-shelf__floor,
.pub-shelf__mark,
.pub-shelf__fill {
    pointer-events: none;
}
/* touch-action on the blocks and not the drawing: a finger on a block drags, a
   finger beside it still scrolls the page. On the whole svg this 380px card would
   be a scroll trap on a phone. */
.pub-shelf[data-interactive] .pub-shelf__block {
    cursor: grab;
    touch-action: none;
}
.pub-shelf[data-dragging] .pub-shelf__block {
    cursor: grabbing;
}
/* Lifted: outlined in the accent, with a shadow so it reads as above the shelf
   rather than on it. */
.pub-shelf__block--lifted {
    stroke: var(--pub-accent);
    stroke-width: 1.5;
    filter: drop-shadow(0 3px 4px var(--pub-ink-dimmer));
}
/* A stroke rather than the page's outline ring: an outline on an SVG rect is drawn
   on the element's bounding box and clipped at the drawing's edge, so the left and
   right blocks would lose half of theirs. */
.pub-shelf__block:focus-visible {
    outline: none;
    stroke: var(--pub-accent);
    stroke-width: 2;
}
/* No CSS transition on x or y: the script moves a block frame by frame — a fall
   under gravity, a slide beside it — and a transition underneath that would lag
   every frame by its own duration. prefers-reduced-motion is honored in the
   script, which then places everything at once. */

/* The chips the drawing measures itself into. Same quiet ink as the disclaimer. */
.pub-shelf__share {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.35rem 1.1rem;
    margin: 0;
    font-family: var(--pub-lens-mono);
    font-size: 0.78rem;
    color: var(--pub-ink-dim);
}
.pub-shelf__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    white-space: nowrap;
}
.pub-shelf__chip [data-share] {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--pub-ink);
}
/* The swatch is the brand's own color, so the chip and the shelf agree. The
   empty chip is bare board at chip size. */
.pub-shelf__swatch {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--pub-brand-a);
}
.pub-shelf__swatch--2 {
    background: var(--pub-brand-b);
}
.pub-shelf__swatch--3 {
    background: var(--pub-brand-c);
}
.pub-shelf__swatch--gap {
    background: none;
    border: 1.5px dashed var(--pub-ink-dimmer);
}

/* --- The two product-page toys --------------------------------------------
   js/public/placement-toy.js and js/public/bin-toy.js. Both follow the front
   page's rule: the template is the state, the script adds behavior, and with no
   script the picture and the figures under it are the starting layout's own.
   The shelf one reuses the front page's shelf vocabulary and adds a sign and a
   sold state; the bin one has its own. Everything a script toggles is a class or
   a data-state attribute, never an inline style. */
.pub-toy .pub-shelf,
.pub-toy .pub-bin {
    display: block;
    width: 100%;
    max-width: 520px;
    height: auto;
    margin: 1.75rem auto 0;
    user-select: none;
    -webkit-user-select: none;
}

/* A tap and not a drag, so the cursor says so. */
.pub-shelf--placement[data-interactive] .pub-shelf__block,
.pub-bin[data-interactive] .pub-bin__piece {
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}
/* A click focuses a piece, and the browser's own ring is drawn on the rotated
   shape's axis-aligned bounding box — a rectangle around nothing. Keyboard focus
   still gets the stroke below, on the shape itself. */
.pub-shelf--placement[data-interactive] .pub-shelf__block:focus,
.pub-bin[data-interactive] .pub-bin__piece:focus {
    outline: none;
}

/* A sold-out block is a gap: bare shelf where a product was, drawn as the dashed
   outline the empty chip already uses. The brand fill is kept as a faint wash so a
   visitor can find it again to restock. */
.pub-shelf__block--sold {
    fill-opacity: 0.08;
    stroke: var(--pub-ink-dimmer);
    stroke-width: 1.2;
    stroke-dasharray: 3 3;
}

/* A sign: paper on the shelf. Not a brand and not a gap. */
.pub-shelf__sign {
    fill: var(--pub-bg-2);
    stroke: var(--pub-ink-dimmer);
    stroke-width: 1;
    pointer-events: none;
}
.pub-shelf__sign-line {
    stroke: var(--pub-ink-dimmer);
    stroke-width: 2;
    stroke-linecap: round;
    pointer-events: none;
}

.pub-bin__tray {
    fill: var(--pub-tray);
}
/* The front page's three brands, so the tray reads as a jumble of different pieces
   in the same colors as every other toy on the site. A sold piece loses its color
   whichever it had: the --sold rule is written after the three and wins on order. */
.pub-bin__piece,
.pub-bin__piece--a {
    fill: var(--pub-brand-a);
}
.pub-bin__piece--b {
    fill: var(--pub-brand-b);
}
.pub-bin__piece--c {
    fill: var(--pub-brand-c);
}
.pub-bin__piece--sold {
    fill: var(--pub-tray);
    stroke: var(--pub-ink-dimmer);
    stroke-width: 1;
    stroke-dasharray: 3 3;
}
.pub-bin__piece:focus-visible {
    outline: none;
    stroke: var(--pub-accent);
    stroke-width: 2;
}
.pub-bin__gauge {
    fill: var(--pub-tray);
}
.pub-bin__gauge-fill {
    fill: var(--pub-accent);
}
.pub-bin__gauge-fill[data-state="low"] {
    fill: var(--pub-warn);
}
.pub-bin__gauge-fill[data-state="out"] {
    fill: var(--pub-bad);
}
.pub-bin__level {
    stroke: var(--pub-ink);
    stroke-width: 1.5;
    stroke-dasharray: 2 2;
    pointer-events: none;
}
.pub-bin__tag {
    font-size: 10px;
    font-weight: 600;
    fill: var(--pub-ink-dim);
    pointer-events: none;
}
/* The scan line travels the height of the frame it is in, less its own 3 units. The
   front page's frame is 152 tall and the keyframes default to that; these two frames
   are 142 and 170, so each sets the travel for itself. */
.pub-shelf--placement .pub-shelf__scan {
    --pub-scan-travel: 139px;
}
.pub-bin .pub-shelf__scan {
    --pub-scan-travel: 167px;
    pointer-events: none;
}

/* What the drawing reads itself into. Left-aligned inside a centered column: a
   reading is a list, and a centered list of chips of different widths is a ragged
   cloud. */
.pub-toy__readout {
    max-width: 520px;
    margin: 1.25rem auto 0;
    text-align: left;
}

.pub-toy__legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.35rem 1.1rem;
    margin: 0 0 1.1rem;
    font-size: 0.8rem;
    color: var(--pub-ink-dim);
}

.pub-reading {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem 0.75rem;
    align-items: center;
    margin: 0 0 1.1rem;
}
.pub-reading dt {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--pub-ink-dim);
    white-space: nowrap;
}
.pub-reading dd {
    margin: 0;
}
.pub-reading__row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* One chip per block, in shelf order. The brand chips wear the brand's own color as
   a left edge, so chip and block agree; a gap is dashed like the empty swatch; a
   sign is paper. */
.pub-chip {
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--pub-line-strong);
    border-radius: 999px;
    font-size: 0.78rem;
    line-height: 1.3;
    color: var(--pub-ink);
    background: var(--pub-bg-2);
}
.pub-chip--brand-1 {
    border-left: 4px solid var(--pub-brand-a);
}
.pub-chip--brand-2 {
    border-left: 4px solid var(--pub-brand-b);
}
.pub-chip--brand-3 {
    border-left: 4px solid var(--pub-brand-c);
}
.pub-chip--gap,
.pub-chip--empty {
    border-style: dashed;
    color: var(--pub-ink-dim);
    background: transparent;
}
.pub-chip--sign {
    color: var(--pub-ink-dim);
}

/* The rules: a sentence, what was measured, and a verdict that is a word and a
   color. Red is the one non-blue, and it is never the only signal. */
.pub-rules {
    margin: 0;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--pub-line);
}
.pub-rules__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.15rem 0.75rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--pub-line);
    font-size: 0.9rem;
}
.pub-rules__text {
    flex: 1 1 16rem;
    color: var(--pub-ink);
}
.pub-rules__detail {
    font-size: 0.82rem;
    color: var(--pub-ink-dim);
}
.pub-rules__verdict {
    margin-left: auto;
    padding: 0.1rem 0.55rem;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--pub-accent);
    background: var(--pub-accent-soft);
}
.pub-rules__row[data-state="bad"] .pub-rules__verdict {
    color: var(--pub-bg-2);
    background: var(--pub-bad);
}

/* The bin's figure: the number large, its state in words beside it. */
.pub-figure {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.75rem;
    margin: 0 0 0.9rem;
}
.pub-figure__value {
    font-size: 2.4rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    color: var(--pub-accent);
}
.pub-figure__value[data-state="low"] {
    color: var(--pub-warn);
}
.pub-figure__value[data-state="out"] {
    color: var(--pub-bad);
}
.pub-figure__state {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--pub-ink-dim);
}

.pub-toy__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
    margin: 0 0 0.5rem;
}
.pub-toy__actions .pub-btn--quiet {
    margin-left: 0;
}
.pub-btn--small {
    padding: 0.4rem 0.9rem;
    font-size: 0.85rem;
}
.pub-toy__refs {
    margin: 0 0 1rem;
    text-align: center;
    font-size: 0.82rem;
    color: var(--pub-ink-dim);
}

/* The three level sliders. Plain range inputs tinted with the accent; the value
   beside each is what the gauge's line is drawn at. */
.pub-levels {
    display: grid;
    gap: 0.4rem;
    margin: 0 0 1.1rem;
}
.pub-levels__row {
    display: grid;
    grid-template-columns: 3.5rem 1fr 3rem;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.85rem;
}
.pub-levels__name {
    font-weight: 600;
    color: var(--pub-ink-dim);
}
.pub-levels__value {
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--pub-ink);
}
.pub-range {
    width: 100%;
    margin: 0;
    accent-color: var(--pub-accent);
}

/* The controls that do nothing without the script ship `hidden` and the script takes
   it off. Both carry an author `display`, which beats the UA stylesheet's `[hidden]`
   whatever the specificity — the same trap CLAUDE.md names for .rv-field. */
.pub-toy__actions[hidden],
.pub-levels[hidden] {
    display: none;
}

/* --- FAQ -------------------------------------------------------------------
   A list of questions, each a <details>. Left-aligned in a reading column under a
   centered section head, for the reason the toy readouts are: a centered list of
   questions of different lengths is a ragged cloud. No script, no icon font — the
   marker is the browser's own, turned so it says open or closed. */
.pub-faq__item {
    max-width: 44rem;
    margin: 0 auto;
    border-bottom: 1px solid var(--pub-line);
    text-align: left;
}
.pub-faq .pub-faq__item:first-of-type {
    margin-top: 1.5rem;
    border-top: 1px solid var(--pub-line);
}
.pub-faq__item summary {
    padding: 0.85rem 0.25rem;
    font-size: 1rem;
    font-weight: 600;
    color: var(--pub-ink);
    cursor: pointer;
    list-style-position: inside;
}
.pub-faq__item summary::marker {
    color: var(--pub-accent);
}
.pub-faq__item summary:hover {
    color: var(--pub-accent);
}
.pub-faq__item p {
    max-width: 60ch;
    margin: 0 0 1rem 1.3rem;
    font-size: 0.95rem;
    color: var(--pub-ink-dim);
}
.pub-faq__item a {
    color: var(--pub-accent);
}

/* Full-width framed product sections; document pages retain their reading layout. */
.pub-main--page {
    align-items: flex-start;
}

/* The head of a product page: eyebrow, title, one lede, framed like a section so
   the eyebrow has an edge to sit on. The title is a step above a section title and
   a step below the front page's name. */
.pub-page-head {
    position: relative;
    margin: 0;
    padding: 3.5rem 3rem 3rem;
    text-align: left;
    background: var(--pub-bg-2);
    border: 1px solid var(--pub-line);
}

.pub-page-head__title {
    max-width: 22ch;
    margin: 0 0 1.25rem;
    font-size: clamp(2.4rem, 6vw, 4.75rem);
    line-height: 0.98;
    font-weight: 700;
    letter-spacing: -0.05em;
    text-wrap: balance;
}

.pub-section {
    position: relative;
    margin: 4.5rem 0 0;
    padding: 3.5rem 3rem 3rem;
    text-align: left;
    border: 1px solid var(--pub-line);
}

/* The label tab on a frame's top edge. It needs a positioned frame to sit on,
   which .pub-section and .pub-page-head both are. */
.pub-eyebrow {
    position: absolute;
    top: 0;
    left: 2rem;
    z-index: 1;
    transform: translateY(-50%);
    margin: 0;
    padding: 0.3rem 0.65rem;
    background: var(--pub-accent);
    font-family: var(--pub-lens-mono);
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--pub-bg-2);
}

/* `balance` keeps a wrapped title from leaving one word alone on its second line.
   Unsupported browsers wrap normally. */
.pub-section__title {
    max-width: 26ch;
    margin: 0 0 1.5rem;
    font-size: clamp(2rem, 4vw, 3.4rem);
    line-height: 1.02;
    font-weight: 700;
    letter-spacing: -0.045em;
    text-wrap: balance;
}

/* Two weights of body copy and no third. The lede carries the claim; the note
   carries the caveat under it, and the caveats on this site are load-bearing —
   both are --pub-ink-dim rather than the dimmer neutral, which is sized for a
   footer and cannot carry a paragraph. The note is set in the readout face. */
.pub-lede {
    max-width: 60ch;
    margin: 0 auto 0.9rem 0;
    font-size: 1.2rem;
    color: var(--pub-ink-dim);
}

.pub-note {
    max-width: 60ch;
    margin: 0.9rem auto 0 0;
    font-family: var(--pub-lens-mono);
    font-size: 0.82rem;
    color: var(--pub-ink-dim);
}

.pub-note a {
    color: var(--pub-accent);
}

/* --- Grid and cards ------------------------------------------------------
   auto-fit with a rem minimum, so a column drops out on its own and neither
   grid needs a breakpoint of its own. */
.pub-grid {
    display: grid;
    gap: 1.5rem;
    justify-content: center;
    margin-top: 1.75rem;
}

.pub-grid--2 {
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

/* Three narrower cards, for a triplet — the three kinds of block, the three fill
   levels. The minimum is set so three fit at the container width and two on a
   tablet; the fallback to one column is auto-fit's. */
.pub-grid--3 {
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}


/* Positioned because lens.js draws a detection box around each card. */
.pub-card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 2rem;
    text-align: left;
    background: var(--pub-bg-2);
    border: 1px solid var(--pub-line);
    border-radius: var(--pub-radius);
}

.pub-card__title {
    margin: 0 0 0.5rem;
    font-size: 1.02rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.pub-card__body {
    max-width: 42ch;
    margin: 0 auto 0 0;
    font-size: 0.94rem;
    color: var(--pub-ink-dim);
}

/* The link at the foot of a card that is the short version of a page. `margin-top:
   auto` pins it to the bottom so two cards in a row put their links on one line
   whatever their bodies ran to. A link and not the whole card as an anchor: a card
   that is a link reads its whole body out as its name. */
.pub-card__more {
    margin: auto 0 0;
    padding-top: 1rem;
    font-size: 0.9rem;
    font-weight: 600;
}
.pub-card__more a {
    color: var(--pub-accent);
    text-decoration: none;
}
.pub-card__more a:hover {
    text-decoration: underline;
}
.pub-card__more a::after {
    content: " \2192";
}


/* --- Steps ---------------------------------------------------------------
   Four across, each in its own frame with a zero-padded number in the readout
   face at the top — 01, 02 — the way a camera numbers its channels. Two across
   below 1000px and one on a phone. Numerals come from a counter rather than
   from `list-style`, so each can sit above its text instead of in the margin. */
.pub-steps {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1.5rem;
    margin: 2rem 0 0;
    padding: 0;
    list-style: none;
    counter-reset: pub-step;
}

.pub-steps li {
    position: relative;
    margin: 0;
    padding: 1.5rem;
    font-size: 0.93rem;
    color: var(--pub-ink-dim);
    background: var(--pub-bg-2);
    border: 1px solid var(--pub-line);
    counter-increment: pub-step;
}

.pub-steps li::before {
    content: counter(pub-step, decimal-leading-zero);
    display: flex;
    margin: 0 0 1.5rem;
    font: 500 0.8rem var(--pub-lens-mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.1em;
    color: var(--pub-accent);
}

/* The lead-in becomes the step's title on its own line. Inline, it ran straight
   into the sentence after it and the column read as one gray block. */
.pub-steps strong {
    display: block;
    margin-bottom: 0.3rem;
    color: var(--pub-ink);
}

/* --- Lists ---------------------------------------------------------------
   Three framed cells across rather than a column of paragraphs; the bold
   lead-in on its own line is the marker, so there is no bullet. One column
   below 800px. */
.pub-list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.5rem;
    margin: 1.75rem 0 0;
    padding: 0;
    list-style: none;
}

.pub-list li {
    position: relative;
    max-width: 52ch;
    margin: 0;
    padding: 1.5rem;
    font-size: 0.96rem;
    color: var(--pub-ink-dim);
    background: var(--pub-bg-2);
    border: 1px solid var(--pub-line);
}

.pub-list strong {
    display: block;
    margin-bottom: 0.25rem;
    color: var(--pub-ink);
}

/* --- The closing band ----------------------------------------------------
   No box: four large corner brackets, as if the camera had framed the question. */
.pub-cta {
    position: relative;
    margin: 4.5rem 0 0;
    padding: 6rem 2rem;
    text-align: center;
}
/* The brackets themselves are drawn by the shared rule in the Lens section. */
.pub-cta::before {
    content: "";
    position: absolute;
    inset: 0;
}

.pub-cta__title {
    margin: 0 0 0.6rem;
    font-size: clamp(2.4rem, 5.5vw, 5rem);
    line-height: 1;
    font-weight: 700;
    letter-spacing: -0.05em;
}

.pub-cta__body {
    max-width: 52ch;
    margin: 0 auto 1.4rem;
    font-size: 0.96rem;
    color: var(--pub-ink-dim);
}

@media (max-width: 1000px) {
    .pub-steps {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}
@media (max-width: 800px) {
    .pub-main {
        padding: 1rem 1rem 3rem;
    }
    .pub-page-head,
    .pub-section {
        padding: 3rem 1.5rem 2rem;
    }
    .pub-section {
        margin-top: 3.5rem;
    }
    .pub-eyebrow {
        left: 1.25rem;
    }
    .pub-list {
        grid-template-columns: 1fr;
    }
}
@media (max-width: 560px) {
    .pub-page-head__title {
        font-size: 2.4rem;
    }
    .pub-steps,
    .pub-grid {
        grid-template-columns: 1fr;
    }
    .pub-cta {
        padding: 4rem 1.25rem;
    }
}

/* Document styles target bare Markdown elements within .pub-doc only. */
.pub-main--doc {
    align-items: flex-start;
}

.pub-doc {
    width: 100%;
    max-width: 44rem;
    margin: 0 auto;
}

.pub-doc h1 {
    margin: 0 0 0.35rem;
    font-size: 1.7rem;
    line-height: 1.2;
    letter-spacing: -0.01em;
}

.pub-doc h2 {
    margin: 2rem 0 0.6rem;
    padding-bottom: 0.35rem;
    border-bottom: 1px solid var(--pub-line);
    font-size: 1.05rem;
    letter-spacing: 0.01em;
}

.pub-doc h3 {
    margin: 1.4rem 0 0.4rem;
    font-size: 0.98rem;
}

.pub-doc p,
.pub-doc li {
    color: var(--pub-ink-dim);
}

.pub-doc p {
    margin: 0 0 0.9rem;
}

.pub-doc ul,
.pub-doc ol {
    margin: 0 0 0.9rem;
    padding-left: 1.25rem;
}

.pub-doc li {
    margin-bottom: 0.35rem;
}

.pub-doc strong {
    color: var(--pub-ink);
}

.pub-doc a {
    color: var(--pub-accent);
}

/* The agreement opens with a blockquote saying it is a draft. Unstyled that reads
   as an indented paragraph, which is exactly what such a notice must not read as. */
.pub-doc blockquote {
    margin: 0 0 1.1rem;
    padding: 0.75rem 1rem;
    border-left: 3px solid var(--pub-accent);
    border-radius: 0 var(--pub-radius-sm) var(--pub-radius-sm) 0;
    background: var(--pub-bg-2);
}

.pub-doc blockquote p:last-child {
    margin-bottom: 0;
}

.pub-doc hr {
    margin: 2rem 0;
    border: 0;
    border-top: 1px solid var(--pub-line);
}

.pub-doc code {
    padding: 0.1em 0.35em;
    border-radius: var(--pub-radius-sm);
    background: var(--pub-accent-soft);
    font-size: 0.9em;
}

/* The agreement is largely tables, and a phone is narrower than any of them.
   Scrolling one is the only honest option; squeezing it is not. */
.pub-doc table {
    display: block;
    width: 100%;
    overflow-x: auto;
    margin: 0 0 1.1rem;
    border-collapse: collapse;
    font-size: 0.92rem;
}

.pub-doc th,
.pub-doc td {
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--pub-line);
    color: var(--pub-ink-dim);
    text-align: left;
    vertical-align: top;
}

.pub-doc th {
    color: var(--pub-ink);
    background: var(--pub-accent-soft);
}

.pub-doc__meta {
    margin: 0 0 1.75rem;
    font-size: 0.85rem;
    color: var(--pub-ink-dimmer);
}

/* --- Forms ----------------------------------------------------------------
   One form on the whole site. Widget classes are set in public/forms.py, which
   is the same rule the app side follows. */
.pub-form {
    margin-top: 1.5rem;
}

.pub-field {
    margin-bottom: 1.1rem;
    flex: 1 1 14rem;
}

/* Two fields on one line where they are halves of one answer — a name, a way to
   reach somebody. Wraps to one column on a phone with no media query, because
   the basis above is what decides it. */
.pub-form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1rem;
}

.pub-label {
    display: block;
    margin-bottom: 0.3rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--pub-ink);
}

.pub-required {
    margin-left: 0.15rem;
    color: var(--pub-ink-dimmer);
}

.pub-optional {
    font-weight: 400;
    color: var(--pub-ink-dimmer);
}

.pub-input,
.pub-textarea,
.pub-select {
    display: block;
    width: 100%;
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--pub-line-strong);
    border-radius: var(--pub-radius-sm);
    background: var(--pub-bg-2);
    color: var(--pub-ink);
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.45;
}

.pub-textarea {
    resize: vertical;
    min-height: 8rem;
}

/* The arrow is drawn rather than fetched: img-src allows data: and nothing on
   this site may cost a second request. It is the ink color at the theme's own
   opacity, so it swaps with everything else. */
.pub-select {
    appearance: none;
    padding-right: 2rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238b93ac' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 0.65rem;
}

/* The sum and its box read as one control, so they sit on one line. */
.pub-challenge {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.pub-challenge__q {
    font-variant-numeric: tabular-nums;
    font-size: 1.05rem;
    color: var(--pub-ink);
    white-space: nowrap;
}

.pub-challenge .pub-input {
    width: 5rem;
}

/* Offscreen rather than `display: none` or `hidden`: a field a bot can see is
   the entire point, and a field a person can reach is the entire failure. The
   wrapper carries aria-hidden and the input carries tabindex="-1", so neither
   a screen reader nor the tab key ever arrives here.

   The name is deliberately anodyne. It renders into the page, and a class that
   announced what this field is for would defeat it as surely as the HTML
   comment that once sat above one — public.tests asserts the word does not
   appear in the response. */
.pub-field--alt {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.pub-errorlist {
    margin: 0.35rem 0 0;
    padding-left: 1.1rem;
    font-size: 0.85rem;
    color: var(--pub-ink);
}

/* Set in the readout face, uppercase and tracked, with square corners. */
.pub-btn {
    padding: 1rem 1.6rem;
    border: 1px solid transparent;
    border-radius: var(--pub-radius);
    background: var(--pub-accent);
    color: var(--pub-bg-2);
    font-family: var(--pub-lens-mono);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    cursor: pointer;
}

.pub-btn:hover {
    filter: brightness(1.1);
}

/* A second button beside the first, for the one band that offers two destinations.
   Outlined so the pair has an order; `a.pub-btn` carries no underline either way. */
a.pub-btn {
    display: inline-block;
    text-decoration: none;
}
.pub-btn--quiet {
    margin-left: 0.5rem;
    color: var(--pub-accent);
    background: transparent;
    border-color: var(--pub-accent);
}
.pub-btn--quiet:hover {
    filter: none;
    background: var(--pub-accent-soft);
}

/* Status is never color alone here either: the left edge and the leading
   glyph in the copy both carry it. */
.pub-alert {
    margin: 1.25rem 0;
    padding: 0.85rem 1rem;
    border: 1px solid var(--pub-line);
    border-left: 3px solid var(--pub-line-strong);
    border-radius: var(--pub-radius-sm);
    background: var(--pub-bg-2);
}

.pub-alert p {
    margin: 0;
}

.pub-alert--ok {
    border-left-color: var(--pub-accent);
}

.pub-alert--bad {
    border-left-color: var(--pub-ink);
}

/* --- Footer --------------------------------------------------------------- */
.pub-footer {
    border-top: 1px solid var(--pub-line);
    padding: 1.5rem 1.25rem 2rem;
    text-align: center;
    font-size: 0.8rem;
    color: var(--pub-ink-dimmer);
}

/* `auto` rather than `0`, and it is load-bearing for the note below. Every other
   paragraph here is full width, so the side margins resolve to nothing and this reads
   as the same rule it always was — but `.pub-footer p` is a class *and* an element and
   therefore outranks a bare class, so a `margin: 0 auto` set on .pub-footer__note alone
   loses to this one and the note sits against the left edge with its text centered
   inside a 46ch box. It looked like a centering bug and was a specificity one. */
.pub-footer p {
    margin: 0 auto 0.4rem;
}

/* A statement rather than a decoration, so it is --pub-ink-dim and not the dimmer
   neutral the rest of the footer uses: the copyright line is furniture nobody has to
   read, and this is a caveat somebody might be held to. The measure keeps it to about
   two lines on a laptop instead of one long one edge to edge. */
.pub-footer__note {
    max-width: 46ch;
    color: var(--pub-ink-dim);
}

/* Nine links, so the row wraps on a phone; inline-block keeps each one whole
   across the wrap. */
.pub-footer__links {
    max-width: 40rem;
}

/* Padded out to a 40px-tall target with a gap between neighbors: as bare inline
   text at this size they failed PageSpeed's touch-target check. */
.pub-footer__links a {
    display: inline-block;
    margin: 0 0.15rem;
    padding: 0.7rem 0.45rem;
    line-height: 1.2;
    color: var(--pub-ink-dim);
    text-decoration: none;
}

.pub-footer__links a:hover {
    text-decoration: underline;
}

/* The disclaimer: the one long passage on the site, kept small and quiet but still
   --pub-ink-dim rather than the dimmer neutral, because it is a statement somebody may be
   held to and not furniture. Left-aligned in a reading column: two centered paragraphs of
   this length are unreadable. */
.pub-footer__disclaimer {
    max-width: 48rem;
    margin: 1.25rem auto 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--pub-line);
    text-align: left;
    font-size: 0.72rem;
    line-height: 1.5;
    color: var(--pub-ink-dim);
}
.pub-footer__disclaimer p {
    margin: 0 0 0.5rem;
}

/* --- Analytics consent ------------------------------------------------------
   Rendered hidden and shown by analytics.js, so with JS off none of this appears.
   Built from the theme tokens only, so both themes come for free. Accept all and Reject all
   share one class: declining must look exactly as easy as accepting. An author
   `display` beats the UA's [hidden], hence the explicit rules. */
.pub-footer__consent {
    padding: 0.7rem 0.45rem;
    border: 0;
    background: none;
    font: inherit;
    color: var(--pub-ink-dim);
    cursor: pointer;
}
.pub-footer__consent:hover {
    text-decoration: underline;
}
.pub-footer__consent[hidden] {
    display: none;
}

.pub-consent {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    left: 1rem;
    z-index: 50;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.25rem;
    align-items: center;
    justify-content: space-between;
    max-width: 44rem;
    margin: 0 auto;
    padding: 1rem 1.25rem;
    border: 1px solid var(--pub-line-strong);
    border-radius: var(--pub-radius);
    background: var(--pub-bg-2);
    box-shadow: var(--pub-shadow);
    color: var(--pub-ink);
    font-size: 0.9rem;
    line-height: 1.45;
}
.pub-consent[hidden] {
    display: none;
}
.pub-consent__text {
    flex: 1 1 18rem;
    margin: 0;
}
.pub-consent__text a {
    color: var(--pub-accent);
}
.pub-consent__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.pub-consent__btn {
    min-width: 6.5rem;
    min-height: 2.5rem;
    padding: 0.5rem 1rem;
    border: 1px solid var(--pub-line-strong);
    border-radius: var(--pub-radius-sm);
    background: var(--pub-bg);
    font: inherit;
    font-weight: 600;
    color: var(--pub-ink);
    cursor: pointer;
}
.pub-consent__btn:hover {
    border-color: var(--pub-accent);
}
.pub-consent__btn:focus-visible {
    outline: 2px solid var(--pub-accent);
    outline-offset: 2px;
}
/* Preferences is a way in, not an answer, so it is quieter than the two answers. */
.pub-consent__btn--quiet {
    border-color: transparent;
    background: none;
    font-weight: 500;
    text-decoration: underline;
}
/* Save is the dialog's one primary action. Text takes --pub-bg, which is the contrasting
   end in both themes: light ground on navy, dark ground on cyan. */
.pub-consent__btn--primary {
    border-color: var(--pub-accent);
    background: var(--pub-accent);
    color: var(--pub-bg);
}

/* The preferences dialog. A native <dialog> opened with showModal(), so focus is held
   inside it and Escape closes it without saving. */
.pub-prefs {
    width: min(32rem, calc(100vw - 2rem));
    max-height: calc(100vh - 2rem);
    padding: 0;
    border: 1px solid var(--pub-line-strong);
    border-radius: var(--pub-radius);
    background: var(--pub-bg-2);
    box-shadow: var(--pub-shadow);
    color: var(--pub-ink);
}
.pub-prefs::backdrop {
    background: rgb(8 13 31 / 0.55);
}
.pub-prefs__form {
    padding: 1.25rem 1.25rem 1rem;
}
.pub-prefs__title {
    margin: 0 0 0.75rem;
    font-size: 1.15rem;
}
.pub-prefs__row {
    display: flex;
    gap: 1rem;
    align-items: flex-start;
    justify-content: space-between;
    padding: 0.9rem 0;
    border-top: 1px solid var(--pub-line);
}
.pub-prefs__row:last-of-type {
    border-bottom: 1px solid var(--pub-line);
}
.pub-prefs__name {
    font-weight: 600;
}
.pub-prefs__desc {
    margin: 0.25rem 0 0;
    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--pub-ink-dim);
}
.pub-prefs__desc a {
    color: var(--pub-accent);
}
.pub-prefs > form > .pub-prefs__desc {
    margin: 0.9rem 0;
}
.pub-prefs__actions {
    justify-content: flex-end;
}

/* A checkbox drawn as a slider. Still a real checkbox with role="switch", so a screen
   reader announces on/off and Space toggles it. */
.pub-switch {
    appearance: none;
    position: relative;
    flex: none;
    width: 2.75rem;
    height: 1.5rem;
    margin: 0.1rem 0 0;
    border: 1px solid var(--pub-line-strong);
    border-radius: 999px;
    background: var(--pub-line);
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.pub-switch::before {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: calc(1.5rem - 6px);
    height: calc(1.5rem - 6px);
    border-radius: 50%;
    background: var(--pub-bg-2);
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
    transition: transform 0.15s ease;
}
.pub-switch:checked {
    border-color: var(--pub-accent);
    background: var(--pub-accent);
}
.pub-switch:checked::before {
    transform: translateX(1.25rem);
}
.pub-switch:disabled {
    cursor: not-allowed;
    opacity: 0.6;
}
.pub-switch:focus-visible {
    outline: 2px solid var(--pub-accent);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    .pub-switch,
    .pub-switch::before {
        transition: none;
    }
}

@media (max-width: 480px) {
    .pub-consent__actions {
        flex: 1 1 100%;
    }
    .pub-consent__btn {
        flex: 1;
    }
}

@media (max-width: 480px) {
    .pub-nav {
        padding: 0.3rem 0.75rem;
    }

    .pub-doc h1 {
        font-size: 1.45rem;
    }
}

/* Scan, pan, and table assembly share one 12-second loop.
   nth-child offsets move cells from the camera to their resting coordinates.
   Reduced motion shows the completed, panned view. */
.pub-page-head--figure {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(280px, 440px);
    gap: 2.5rem;
    align-items: center;
}
.pub-hiw {
    display: block;
    width: 100%;
    height: auto;
}
.pub-hiw__scene {
    animation: pub-hiw-pan 12s cubic-bezier(0.5, 0, 0.2, 1) infinite;
}
@keyframes pub-hiw-pan {
    0%,
    36% {
        transform: translateX(0);
    }
    42%,
    96% {
        transform: translateX(-150px);
    }
    100% {
        transform: translateX(0);
    }
}
.pub-hiw__region {
    fill: none;
    stroke: var(--pub-line-strong);
    stroke-width: 1.5;
    stroke-dasharray: 5 6;
}
/* The shelf is filled slabs, not outlines: a faint back panel, uprights and boards
   in the strong line color, no stroke anywhere. The blocks rest one unit above
   each board. */
.pub-hiw__panel {
    fill: var(--pub-tray);
    opacity: 0.35;
}
.pub-hiw__wood {
    fill: var(--pub-line-strong);
}
.pub-hiw__block--a {
    fill: var(--pub-brand-a);
}
.pub-hiw__block--b {
    fill: var(--pub-brand-b);
}
.pub-hiw__block--c {
    fill: var(--pub-brand-c);
}
/* The dome: a flat half-ball under a plate, one thin edge, a lens. */
.pub-hiw__base {
    fill: var(--pub-line-strong);
}
.pub-hiw__dome {
    fill: var(--pub-bg-2);
    stroke: var(--pub-line-strong);
    stroke-width: 1.5;
    stroke-linejoin: round;
}
.pub-hiw__lens {
    fill: var(--pub-accent);
}

/* The beam and the pass down the shelf. */
.pub-hiw__beam {
    fill: url(#pub-hiw-beam-fade);
    filter: url(#pub-hiw-beam-soft);
    opacity: 0;
    animation: pub-hiw-beam 12s linear infinite;
}
.pub-hiw__beam-near {
    stop-color: var(--pub-accent);
    stop-opacity: 0.9;
}
.pub-hiw__beam-far {
    stop-color: var(--pub-accent);
    stop-opacity: 0.25;
}
@keyframes pub-hiw-beam {
    0% {
        opacity: 0;
    }
    5%,
    30% {
        opacity: 0.16;
    }
    35%,
    100% {
        opacity: 0;
    }
}
.pub-hiw__scan {
    fill: var(--pub-accent);
    opacity: 0;
    animation: pub-hiw-scan 12s ease-in-out infinite;
}
@keyframes pub-hiw-scan {
    0%,
    5% {
        transform: translateY(0);
        opacity: 0;
    }
    7% {
        opacity: 0.8;
    }
    30% {
        transform: translateY(118px);
        opacity: 0.8;
    }
    33%,
    100% {
        transform: translateY(118px);
        opacity: 0;
    }
}

/* The table. Its frame appears as the first cell leaves the camera. */
.pub-hiw__sheet {
    fill: none;
    stroke: var(--pub-line-strong);
    stroke-width: 1.5;
    opacity: 0;
    animation: pub-hiw-sheet 12s linear infinite;
}
@keyframes pub-hiw-sheet {
    0%,
    40% {
        opacity: 0;
    }
    43.5%,
    90% {
        opacity: 1;
    }
    94%,
    100% {
        opacity: 0;
    }
}
.pub-hiw__box {
    fill: var(--pub-bg-2);
    stroke: var(--pub-line-strong);
    stroke-width: 1.2;
}
.pub-hiw__bar {
    fill: var(--pub-line-strong);
}
.pub-hiw__bar--flag {
    fill: var(--pub-bad);
}
/* The header row is solid. */
.pub-hiw__row:nth-child(1) .pub-hiw__box {
    fill: var(--pub-accent);
    stroke: var(--pub-accent);
}
.pub-hiw__row:nth-child(1) .pub-hiw__bar {
    fill: var(--pub-bg-2);
}
/* Where each cell starts from: the dome's right side at (262, 56), less the cell's
   own center. Rows set the vertical offset and their index; columns the
   horizontal and theirs. The cells are the row group's children, in column
   order, so the two :nth-child ladders are the whole addressing scheme. */
.pub-hiw__row:nth-child(1) { --pub-hiw-r: 0; --pub-hiw-dy: 2px; }
.pub-hiw__row:nth-child(2) { --pub-hiw-r: 1; --pub-hiw-dy: -22px; }
.pub-hiw__row:nth-child(3) { --pub-hiw-r: 2; --pub-hiw-dy: -46px; }
.pub-hiw__row:nth-child(4) { --pub-hiw-r: 3; --pub-hiw-dy: -70px; }
.pub-hiw__row:nth-child(5) { --pub-hiw-r: 4; --pub-hiw-dy: -94px; }
.pub-hiw__cell:nth-child(1) { --pub-hiw-c: 0; --pub-hiw-dx: -45px; }
.pub-hiw__cell:nth-child(2) { --pub-hiw-c: 1; --pub-hiw-dx: -75px; }
.pub-hiw__cell:nth-child(3) { --pub-hiw-c: 2; --pub-hiw-dx: -105px; }
.pub-hiw__cell:nth-child(4) { --pub-hiw-c: 3; --pub-hiw-dx: -135px; }
.pub-hiw__cell {
    transform-box: fill-box;
    transform-origin: center;
    opacity: 0;
    animation: pub-hiw-cell 12s cubic-bezier(0.3, 0.7, 0.3, 1) infinite both;
    animation-delay: calc(4.9s + (var(--pub-hiw-r, 0) * 4 + var(--pub-hiw-c, 0)) * 90ms);
}
@keyframes pub-hiw-cell {
    0% {
        transform: translate(var(--pub-hiw-dx, 0px), var(--pub-hiw-dy, 0px)) scale(0.25);
        opacity: 0;
    }
    1% {
        opacity: 1;
    }
    5.8% {
        transform: none;
        opacity: 1;
    }
    33% {
        opacity: 1;
    }
    37% {
        opacity: 0;
    }
    100% {
        transform: none;
        opacity: 0;
    }
}
@media (prefers-reduced-motion: reduce) {
    .pub-hiw__scene,
    .pub-hiw__beam,
    .pub-hiw__scan,
    .pub-hiw__sheet,
    .pub-hiw__cell {
        animation: none;
    }
    .pub-hiw__scene {
        transform: translateX(-150px);
    }
    .pub-hiw__sheet,
    .pub-hiw__cell {
        opacity: 1;
    }
}
@media (max-width: 800px) {
    .pub-page-head--figure {
        grid-template-columns: 1fr;
        gap: 1.5rem;
    }
    .pub-hiw {
        max-width: 440px;
    }
}

/* Lens decoration is aria-hidden and pointer-transparent, with reduced-motion opt-out.
   Never rotate toy ancestors: their pointer mapping uses getBoundingClientRect. */

/* Corner brackets: one element, eight gradients. Reused by the hero frame, the
   reticle, the closing band and every card's detection box. */
.pub-lens-frame,
.pub-cta::before {
    --pub-lens-arm: 22px;
    --pub-lens-weight: 2px;
    --pub-lens-c: var(--pub-accent);
    background:
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) top left / var(--pub-lens-arm) var(--pub-lens-weight),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) top left / var(--pub-lens-weight) var(--pub-lens-arm),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) top right / var(--pub-lens-arm) var(--pub-lens-weight),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) top right / var(--pub-lens-weight) var(--pub-lens-arm),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) bottom left / var(--pub-lens-arm) var(--pub-lens-weight),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) bottom left / var(--pub-lens-weight) var(--pub-lens-arm),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) bottom right / var(--pub-lens-arm) var(--pub-lens-weight),
        linear-gradient(var(--pub-lens-c), var(--pub-lens-c)) bottom right / var(--pub-lens-weight) var(--pub-lens-arm);
    background-repeat: no-repeat;
    pointer-events: none;
}
/* The closing band's brackets are larger; this has to follow the shared rule. */
.pub-cta::before {
    --pub-lens-arm: 44px;
}

.pub-hero > .pub-lens-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.pub-hero > .pub-lens-dots {
    z-index: -1;
    width: 100%;
    height: 100%;
}
.pub-hero > .pub-lens-frame {
    inset: 14px;
}

.pub-lens-reticle {
    z-index: 2;
    opacity: 0;
    transition: opacity 0.3s;
}
.pub-lens-aiming .pub-lens-reticle {
    opacity: 1;
}
.pub-lens-reticle__h,
.pub-lens-reticle__v {
    position: absolute;
    background: color-mix(in srgb, var(--pub-accent) 45%, transparent);
}
.pub-lens-reticle__h {
    top: 0;
    right: 0;
    left: 0;
    height: 1px;
    transform: translateY(var(--pub-lens-y, 50%));
}
.pub-lens-reticle__v {
    top: 0;
    bottom: 0;
    left: 0;
    width: 1px;
    transform: translateX(var(--pub-lens-x, 50%));
}
.pub-lens-reticle__box {
    position: absolute;
    top: 0;
    left: 0;
    width: 64px;
    height: 64px;
    margin: -32px 0 0 -32px;
    transform: translate(var(--pub-lens-x, 50%), var(--pub-lens-y, 50%));
    --pub-lens-arm: 12px;
    --pub-lens-weight: 2px;
}
.pub-lens-reticle__read {
    position: absolute;
    top: 0;
    left: 0;
    padding: 0.2rem 0.45rem;
    white-space: nowrap;
    transform: translate(calc(var(--pub-lens-x, 50%) + 40px), calc(var(--pub-lens-y, 50%) + 26px));
    background: var(--pub-accent);
    font: 500 0.68rem var(--pub-lens-mono);
    letter-spacing: 0.06em;
    color: var(--pub-bg-2);
}

/* The load scan: one number from 0 to 1, read by the hero's mask and by the beam. */
@property --pub-lens-scan {
    syntax: "<number>";
    inherits: true;
    initial-value: 1;
}
.pub-lens-scanning {
    animation: pub-lens-scan 1.5s cubic-bezier(0.45, 0.05, 0.25, 1) both;
}
@keyframes pub-lens-scan {
    from {
        --pub-lens-scan: 0;
    }
    to {
        --pub-lens-scan: 1;
    }
}
.pub-lens-scanning .pub-hero {
    -webkit-mask-image: linear-gradient(#000 calc(var(--pub-lens-scan) * 100%), transparent calc(var(--pub-lens-scan) * 100% + 60px));
    mask-image: linear-gradient(#000 calc(var(--pub-lens-scan) * 100%), transparent calc(var(--pub-lens-scan) * 100% + 60px));
}
.pub-lens-beam {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    z-index: 3;
    height: 2px;
    pointer-events: none;
    transform: translateY(calc(var(--pub-lens-scan) * var(--pub-lens-hero-h, 0px)));
    background: var(--pub-accent);
    box-shadow:
        0 0 18px 2px color-mix(in srgb, var(--pub-accent) 60%, transparent),
        0 -40px 60px -20px color-mix(in srgb, var(--pub-accent) 25%, transparent);
    opacity: 0;
}
.pub-lens-scanning .pub-lens-beam {
    opacity: 1;
}

/* Focus pull: --pub-lens-focus runs 0 → 1 as a section rises into view, and a
   section that has reached 1 keeps no filter while it is being read. */
.pub-lens-ready .pub-section:not(.pub-lens-sharp),
.pub-lens-ready .pub-cta:not(.pub-lens-sharp) {
    filter: blur(calc((1 - var(--pub-lens-focus, 1)) * 5px));
    opacity: calc(0.35 + var(--pub-lens-focus, 1) * 0.65);
    transform: scale(calc(0.97 + var(--pub-lens-focus, 1) * 0.03));
}

/* Detection boxes: a rect that draws itself, then brackets that settle on the
   corners. --pub-lens-d is each box's own draw time, set by lens.js between 0.5s and
   1.35s; the brackets and the fade key off it so they still land as that box finishes. An <svg> is a replaced element, so insets alone leave it at 300x150:
   size it outright. */
.pub-lens-box {
    position: absolute;
    top: -1px;
    left: -1px;
    width: calc(100% + 2px);
    height: calc(100% + 2px);
    overflow: visible;
    pointer-events: none;
}
.pub-lens-box rect {
    fill: none;
    stroke: var(--pub-accent);
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    transition:
        stroke-dashoffset var(--pub-lens-d, 0.9s) cubic-bezier(0.6, 0, 0.2, 1) calc(var(--pub-lens-i, 0) * 110ms),
        opacity 0.6s ease calc(var(--pub-lens-i, 0) * 110ms + var(--pub-lens-d, 0.9s) + 0.2s);
}
.pub-lens-seen > .pub-lens-box rect {
    stroke-dashoffset: 0;
    opacity: 0.35;
}
.pub-lens-corners {
    position: absolute;
    inset: -6px;
    opacity: 0;
    transform: scale(1.04);
    --pub-lens-arm: 14px;
    --pub-lens-weight: 2px;
    transition:
        opacity 0.4s ease calc(var(--pub-lens-i, 0) * 110ms + var(--pub-lens-d, 0.9s) * 0.78),
        transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) calc(var(--pub-lens-i, 0) * 110ms + var(--pub-lens-d, 0.9s) * 0.78);
}
.pub-lens-seen > .pub-lens-corners {
    opacity: 1;
    transform: none;
}
/* The label tab types itself out once its frame has been seen. */
.pub-lens-ready .pub-eyebrow {
    clip-path: inset(0 100% 0 0);
    transition: clip-path 0.6s steps(14, end) 0.2s;
}
.pub-lens-ready .pub-lens-seen > .pub-eyebrow {
    clip-path: inset(0);
}

@media (prefers-reduced-motion: reduce) {
    .pub-lens-box rect,
    .pub-lens-corners,
    .pub-lens-ready .pub-eyebrow {
        transition: none;
    }
}
