/* ==========================================================================
   Daylight — the canvas.

   v2/DESIGN.md, written from reference/ss-home.png, and the screenshot is the
   target: a dotted field, cards that float with a soft shadow and no visible
   edge, a slight offset and rotation on each, a small dark dock of round icon
   wells as the whole navigation, one black pill and one grey, hairline chips,
   big quiet type in a compact card. The reference is a portfolio template;
   Daylight takes its form and not its content — every card here is a real
   thing.

   Measured off the reference rather than guessed: the intro card is ~400px
   wide with a ~36px heading and ~14px body; the small cards are ~240px with
   12–13px type, a bold title, a divider and a footer row; pills are ~36px
   tall; the dock is ~48px tall and icon-only. The scale below follows that.

   Self-contained: one file, no CDN, no build step. Light is the default; dark
   is the system preference or an explicit choice, and the choice always beats
   the preference — so every dark token appears twice, once per mechanism.
   Which one paints is decided by an inline script in views/layouts/base.php
   before first paint, or the page flashes the wrong theme for a frame.

   Two type decisions carry the look, both inherited from V1: figures are
   tabular, because this product is columns of counts and they have to sit
   still; log material — domains, site keys, tokens, reject reasons — is
   monospace, because those strings come out of a log and should read as what
   they are.

   NOT in this file: anything to do with how the unit looks. /f.css lives on
   somebody else's page under their variables, and Daylight's palette reaches
   it only the way any publisher's would — by setting those variables on the
   [data-daylight] element, near the bottom of this file.

   Every ratio in the comments is measured (WCAG 2 contrast), not estimated.

   Contents, by component rather than by page (re-sectioned 2026-09-04):
     tokens · base · canvas · sheet · deal · card (and its kinds) · figures ·
     pills · chips · forms · note · ask · creative row · tiles and sparklines ·
     unit · snippet · tables and facts · claim · homepage · kestrel badge · dashboard ·
     the doors · a site's own page · a board's page · dock · desk switch ·
     flash · reduced motion.

   Order matters in three places and each says so where it sits: a card kind
   comes after .card, the badge's card padding comes after every kind that
   wears one, and the phone override of the intro card's padding comes after
   the badge. Reduced motion is last so it can undo anything above it.
   ========================================================================== */

:root {
    --field:         #fafaf9;   /* the canvas */
    --dot:           #d8d5cf;   /*  1.4:1 on the field — a texture, not a mark */
    --surface:       #ffffff;   /* a card */
    --border:        #e7e4de;   /* hairlines: chips, dividers, wells */
    --card-edge:     transparent; /* the reference cards have no edge in the light; the shadow carries it */
    --text:          #151412;   /* 17.6:1 on the field, 18.4:1 on a card */
    --muted:         #6a665e;   /*  5.5:1 / 5.7:1 */
    --faint:         #a29d93;   /*  2.6:1 — decorative only, never body text */
    /* Sun & graphite — the owner's pick off v2/html/palette.html, 2026-09-04,
       and the answer to "the gold is too dark, keep the sun."

       Every earlier version of this file asked one colour to be both the
       daylight in the name and the ink in a sentence, and lost: the sun is
       1.6:1 on white, so anything readable is a darker and darker gold until
       it is not a yellow any more. Graphite stops asking. The accent goes
       near-black and does all the reading; the sun keeps every job where it
       is a shape rather than a word — the mark's tile, the strip's rule and
       its pulse, a link's underline, the ground under a flash — and can
       therefore be the colour the name says, at full strength, everywhere it
       appears. Nothing bright has to be read, so nothing bright is dimmed.

       Graphite and not black: #2f2a22 is warm, so it belongs to the same
       family as the field and the sun rather than cutting across them. */
    --accent:        #2f2a22;   /* 13.6:1 on the field, 14.2:1 on a card, 12.2:1 on a pill */
    --accent-soft:   rgba(247, 199, 68, 0.30);   /* the sun at 30% — a ground, never a mark */
    --on-accent:     #fffdf7;   /* 14.0:1 on the accent */
    /* The sun at full strength. Where it has to be *read* — a chip's glyph, a
       note's mark, the unit's placeholder letter — it goes through --lit-ink
       instead: the same hue walked to a lightness each ground can carry. */
    --lit:           #f7c744;
    /* The glyph on the tile follows the theme (owner's ask, 2026-09-04): white
       in the light, the dark brown in the dark. This is the one place the
       "a logo that changes colour is two logos" rule is deliberately spent —
       the tile does not change, only the sun drawn on it, and a logotype is
       exempt from contrast rules anyway. The white is soft on purpose: 1.6:1
       is a sun in daylight rather than a sun drawn on top of it. */
    --on-lit:        #ffffff;   /*  1.6:1 on --lit — a mark, not a word */
    --lit-ink:       #8a6100;   /*  5.5:1 on a card, 5.3:1 on the field, 4.7:1 on a pill */
    --ink:           #111111;   /* the primary pill */
    --on-ink:        #ffffff;   /* 18.9:1 */
    --pill:          #efedea;   /* the secondary pill, and the unit's well */
    /* Grey, not amber, and a product decision rather than a taste one: a
       flagged click is not an error and not a warning, it is a click we chose
       not to count. Grey says "excluded from the total" exactly. It used to sit
       a luminance step off the old gold accent (0.171 against 0.159), which was
       fine and is now moot — graphite is 0.024. The pair that matters is
       flagged against verified (0.079), and that one is far apart. Colour is
       never the only carrier anyway: every flagged figure says "not counted"
       in words. */
    --flagged:       #7a7264;   /*  4.6:1 on the field, 4.8:1 on a card — 4.1:1 on a pill, so a flagged chip sits on the surface */
    --up:            #0d5c2b;   /*  7.8:1 */
    --down:          #a8231f;   /*  6.9:1 */
    --impressions:   #4f46e5;   /*  6.0:1 */
    --clicks:        #0f766e;   /*  5.2:1 — teal on purpose, the one borrow from Kestrel, which vouches for verified clicks */
    --shadow:        0 1px 2px rgba(21, 20, 18, 0.04), 0 10px 24px rgba(21, 20, 18, 0.06), 0 24px 56px rgba(21, 20, 18, 0.06);
    --radius:        12px;
    --radius-sm:     8px;
    --dock:          #161616;
    --on-dock:       #f4f4f4;   /* 16.5:1 */
    /* The theme control sits in a well rather than on the bar, so it reads as
       a switch among the destinations instead of one more of them. Translucent
       --on-dock, the way --accent-soft is translucent --accent: it has to work
       over whichever ground --dock is, and that swaps with the theme. */
    --dock-well:     rgba(244, 244, 244, 0.13);
    --dock-well-on:  rgba(244, 244, 244, 0.24);
    /* Kestrel's badge paints its own ground and never inherits the page, so
       the mock-up of it carries the badge's literal colours (lib/Badge.php in
       kestrel.host), both plates. It was the amber preset until 2026-09-03;
       it is now Daylight's own colour, because the badge takes a bare hex as
       well as one of its seven preset names and none of the seven is a yellow.

       The two shades below are NOT hand-picked and must not be hand-edited:
       they are what `Kestrel\Shade::pair('#f7c744')` returns — the sun from
       --lit, put through the same OKLCh lightness walk the real badge will
       run when it is wired with `?color=f7c744`. Shade holds hue and chroma
       and moves lightness alone until it clears the plate, which is why the
       light plate gets a darker gold (4.6:1 on #f4f4f5) and the dark plate
       keeps the brand yellow untouched (10.8:1 on #1b1b1f). Re-derive rather
       than adjust: `php -r` with KESTREL defined, Shade.php required. */
    --kb-plate:      #f4f4f5;
    --kb-edge:       #e4e4e7;
    --kb-text:       #18181b;
    --kb-muted:      #71717a;
    --kb-sep:        #c2c2c9;
    --kb-accent:     #896a00;   /*  4.6:1 on the badge's light plate */
    --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --dock-h: 68px;             /* what the canvas leaves clear at the bottom */
    /* The desk itself. Below 768px it is the flowed one: a field a little
       wider than a phone, so it pans sideways as well as down, with the
       cards' own nudges scaled back on a very narrow screen where that
       width is all there is. From 768px it is the placed one, 2000×1400,
       whose centre is the intro's. */
    --desk-w: 560px;
    --desk-h: auto;
    --nudge: 1;
    color-scheme: light;
}

/* Dark as the system preference — held off when light was chosen by hand. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --field:         #0f0f0e;
        --dot:           #2a2926;   /*  1.3:1 */
        --surface:       #1a1917;
        --border:        #2b2a27;
        --card-edge:     #2b2a27;   /* no shadow in the dark, so a card needs an edge */
        --text:          #ecebe7;   /* 16.1:1 on the field, 14.7:1 on a card */
        --muted:         #a39f96;   /*  7.3:1 / 6.7:1 */
        --faint:         #6d6657;   /*  3.4:1 — decorative only */
        --accent:        #e9e6df;   /* 15.4:1 on the field, 14.1:1 on a card, 12.3:1 on a pill */
        --accent-soft:   rgba(247, 199, 68, 0.16);
        --on-accent:     #1a1917;   /* 14.1:1 on the accent */
        --on-lit:        #3d2c00;   /*  8.5:1 on --lit — the dark sun stays in the dark */
        --lit-ink:       #f7c744;   /* 11.1:1 on a card — here the sun reads as itself */
        --ink:           #f4f4f4;
        --on-ink:        #111111;   /* 17.2:1 */
        --pill:          #26251f;
        --flagged:       #8b8374;   /*  5.1:1 / 4.7:1 */
        --up:            #4ade80;   /* 11.0:1 */
        --down:          #ff8a80;   /*  8.4:1 */
        --impressions:   #a5b4fc;   /*  9.6:1 */
        --clicks:        #2dd4bf;   /* 10.3:1 */
        --shadow:        none;
        --dock:          #f4f4f4;
        --on-dock:       #161616;   /* 16.5:1 */
        --dock-well:     rgba(22, 22, 22, 0.10);
        --dock-well-on:  rgba(22, 22, 22, 0.19);
        --kb-plate:      #1b1b1f;
        --kb-edge:       #2c2c33;
        --kb-text:       #e8e8ea;
        --kb-muted:      #a1a1aa;
        --kb-sep:        #4b4b55;
        --kb-accent:     #f7c744;
        color-scheme: dark;
    }
}

/* Dark as an explicit choice. Same tokens, second mechanism — this is what
   makes the toggle win in both directions rather than only away from the
   system default. */
:root[data-theme="dark"] {
    --field:         #0f0f0e;
    --dot:           #2a2926;
    --surface:       #1a1917;
    --border:        #2b2a27;
    --card-edge:     #2b2a27;
    --text:          #ecebe7;
    --muted:         #a39f96;
    --faint:         #6d6657;
    --accent:        #e9e6df;
    --accent-soft:   rgba(247, 199, 68, 0.16);
    --on-accent:     #1a1917;
    --on-lit:        #3d2c00;
    --lit-ink:       #f7c744;
    --ink:           #f4f4f4;
    --on-ink:        #111111;
    --pill:          #26251f;
    --flagged:       #8b8374;
    --up:            #4ade80;
    --down:          #ff8a80;
    --impressions:   #a5b4fc;
    --clicks:        #2dd4bf;
    --shadow:        none;
    --dock:          #f4f4f4;
    --on-dock:       #161616;
    --dock-well:     rgba(22, 22, 22, 0.10);
    --dock-well-on:  rgba(22, 22, 22, 0.19);
    --kb-plate:      #1b1b1f;
    --kb-edge:       #2c2c33;
    --kb-text:       #e8e8ea;
    --kb-muted:      #a1a1aa;
    --kb-sep:        #4b4b55;
    --kb-accent:     #f7c744;
    color-scheme: dark;
}

/* ------------------------------------------------------------------ base */

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

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    min-height: 100vh;
    background-color: var(--field);
    color: var(--text);
    font: 15px/1.5 var(--sans);
    -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; }

h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -0.02em; }
h1 { font-size: 28px; line-height: 1.2; }
h2 { font-size: 20px; line-height: 1.3; }
h3 { font-size: 15px; line-height: 1.4; }
p  { margin: 0 0 0.75em; }
p:last-child { margin-bottom: 0; }
strong { font-weight: 650; }

/* Graphite's one rule for links: the colour is the text's, near enough (1.3:1
   apart), so the underline is the entire signal and it is always drawn rather
   than waiting for a hover. It is drawn in the sun — 2px, offset clear of the
   descenders — which is the one place the yellow reaches a run of text. Hover
   pulls it up to the ink instead of adding anything. A link shaped as a card,
   a pill, a tile, a stat or a dock item turns it off again where it is
   styled; every one of those already did, for the old rule. */
a {
    color: var(--accent);
    text-decoration: underline;
    text-decoration-color: var(--lit);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}
a:hover { text-decoration-color: var(--accent); }

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

.small { font-size: 12px; }
.muted { color: var(--muted); }

/* Counts and figures hold their columns. */
.num { font-variant-numeric: tabular-nums; }

/* Log material: domains, site keys, tokens, reject reasons, slugs. */
.mono, code, kbd { font-family: var(--mono); font-size: 13px; }

/* Colour is never the only carrier of meaning (DESIGN §1). These three are
   always beside a word or a sign that says the same thing. */
.verified { color: var(--up); }
.rejected { color: var(--down); }
.flagged  { color: var(--flagged); }

/* Off screen until it is not: the dock's labels, the skip link, a form's
   label that the placeholder already says. */
.vh {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}
.skip {
    position: absolute;
    top: -60px;
    left: 12px;
    z-index: 60;
    padding: 8px 14px;
    border-radius: 999px;
    background: var(--ink);
    color: var(--on-ink);
    text-decoration: none;
}
.skip:focus { top: 12px; }

/* ---------------------------------------------------------------- canvas

   Two layouts over one markup, and the DOM stays in reading order for both.

   **The desk** is what a reader gets by default at every width, a phone
   included (owner's call, 2026-09-02: a phone acts like a desktop, dragging
   and all). The board is the window onto it — fixed to the viewport,
   scrolling in both directions, so the document itself never scrolls and the
   dock never moves. The field pans: a mouse drags it (daylight.js), a finger
   throws it, the wheel and the arrow keys move it. A card is dragged with a
   pointer, or picked up with a long press by a finger.

   From 768px the desk is *placed*: a 2000×1400 board whose centre is the
   intro's, every other card at an offset from that centre at its own width
   and rotation — the reference's composition, and a tablet gets it whole.
   Below that no two cards fit side by side at a readable width, so the desk
   is *flowed* instead: the same cards down a field wider than the phone,
   each nudged off the centre line by its own --px and turned by its own
   --prot, on a field that still pans in both directions. Flowed rather than
   placed because the card set is not fixed — the note comes and goes, the
   ads are one to three, a line wraps at one font size and not another — and
   hand-placed cards on a 390px screen collide the first time any of that
   changes.

   **The stack** is one tidy column (two from 768px), no rotation and no
   offsets, the document scrolling as a document. It is the layout below
   1024px with no script at all, and it is what the homepage's desk switch
   returns to (layouts/base.php, `data-desk`). A desktop always gets the
   desk: the switch is the phone's and the tablet's, and is not offered
   above 1023px. */

/* the stack — the base layout, and the switch's other state */
.canvas {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
    min-height: 100vh;
    min-height: 100dvh;
    max-width: 1560px;
    margin: 0 auto;
    padding: 28px 16px calc(var(--dock-h) + 28px);
}

/* The field: a 22px dot pitch, 2px dots one tone off the field, drawn rather
   than fetched — painted on whichever element actually *is* the field.

   On a desk that is the canvas, so the dots pan with the cards as the
   reference's do: the field is larger than the window and moving it is the
   whole point. A sheet does not pan, and its canvas is only as large as the
   composition asks for — 1180px wide and centred on the placed half, with the
   dock's clearance under it — so dots painted there stop short of the window
   on any screen wider or taller than that, which is the seam. On a sheet they
   go on the body instead, which propagates to the viewport the way its
   --field colour already does and so covers the whole page at any size, and
   scrolls with it. */
:root:not([data-page="sheet"]) .canvas,
:root[data-page="sheet"] body {
    background-image: radial-gradient(circle, var(--dot) 1px, transparent 1.4px);
    background-size: 22px 22px;
    background-position: 11px 11px;
}

@media (min-width: 768px) {
    .canvas {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 24px;
        padding: 32px 28px calc(var(--dock-h) + 40px);
        align-items: start;
    }
    .canvas > .card--intro { grid-column: 1 / -1; }
}

/* the board — the window onto the desk, at every width */
.board {
    position: fixed;
    inset: 0;
    overflow: auto;
    overscroll-behavior: contain;
}
.board.has-pan { cursor: grab; }
.board.is-panning { cursor: grabbing; user-select: none; }
.board.is-panning .card { pointer-events: none; }
/* The board scrolls, so the document must not: two scrollers over one desk
   is the phone bug where the page slides out from under the field. */
body { overflow: hidden; }

/* the desk, flowed — below 768px */
.canvas {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 26px;
    /* Wider than the phone, so the field pans sideways as well as down; and
       never narrower than the board, so the dots reach its edges. */
    width: max(var(--desk-w), 100%);
    min-height: 100%;
    max-width: none;
    /* The top clears the desk switch, which floats at that corner on the
       pages that offer it: the first card starts below the button rather
       than under it. Everything after it passes beneath on the way up,
       the way it passes beneath the dock on the way down. The switch is
       itself pushed down by the notch, so the clearance follows it. */
    padding: calc(64px + env(safe-area-inset-top, 0px)) 0 calc(var(--dock-h) + 44px);
}
/* Centred, then nudged. The margin pair shifts a card off the centre line by
   --px while leaving its margin box the width of its border box, so no
   amount of nudging can widen the desk or start a sideways scroll of its
   own. --dx/--dy are the drag's, the same two properties the placed desk
   uses, so daylight.js has one mechanism for both and a reload puts
   everything back where the view put it. */
.canvas > .card {
    width: min(var(--pw, 300px), calc(100vw - 40px));
    margin-right: calc(var(--px, 0px) * var(--nudge) * -1);
    margin-left: calc(var(--px, 0px) * var(--nudge));
    transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(var(--prot, 0deg)) scale(var(--lift, 1));
}
/* A long press picks a card up (daylight.js), so it must not also select the
   line under the finger or raise the callout menu over the card being moved. */
@media (pointer: coarse) {
    .canvas > .card { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}
/* A 320px phone has no room for the full nudge: at the widest offset a
   card would sit half off the field it is centred on. The composition
   holds, tightened. */
@media (max-width: 359px) {
    :root { --nudge: 0.5; }
}

/* the desk, placed — 768px and up */
@media (min-width: 768px) {
    :root { --desk-w: 2000px; --desk-h: 1400px; }
    .canvas {
        display: block;
        position: relative;
        width: var(--desk-w);
        height: var(--desk-h);
        min-height: 0;
        padding: 0;
    }
    .canvas > .card {
        position: absolute;
        top: calc(50% + var(--y, 0px));
        left: calc(50% + var(--x, 0px));
        width: var(--w, 330px);
        margin: 0;
        transform: translate(-50%, -50%) translate(var(--dx, 0px), var(--dy, 0px)) rotate(var(--rot, 0deg)) scale(var(--lift, 1));
    }
    /* A card is dragged by anything on it, its picture included, so the
       browser's own image and link drags are off inside one. */
    .canvas > .card img, .canvas > .card a { -webkit-user-drag: none; user-drag: none; }
}

/* The 2px lift is the pointer's alone. A touch screen reports no hover and
   :hover sticks there to whatever was tapped last, which would leave one
   card raised for the rest of the visit — and, worse, would have to restate
   the whole desk transform to do it. The finger gets the pickup instead. */
@media (hover: hover) and (max-width: 767px) {
    .canvas > .card:hover {
        transform: translate(var(--dx, 0px), calc(var(--dy, 0px) - 2px)) rotate(var(--prot, 0deg)) scale(var(--lift, 1));
    }
}
@media (hover: hover) and (min-width: 768px) {
    .canvas > .card:hover {
        transform: translate(-50%, -50%) translate(var(--dx, 0px), calc(var(--dy, 0px) - 2px)) rotate(var(--rot, 0deg)) scale(var(--lift, 1));
    }
}

/* the stack, chosen — or arrived at with no script at all, which comes to
   the same thing as far as the layout is concerned. Below 1024px only; a
   desktop is never offered the switch and never loses its desk.

   Not on a sheet, and the selector has to say so: a sheet carries no
   data-desk, so "not the desk" matched it, and below 1024px these rules were
   quietly taking the width and the lean back off a composition whose whole
   point is that it keeps them. The switch is the reader's choice between two
   layouts of the same desk; a sheet is not one of them. */
@media (max-width: 1023.98px) {
    :root:not([data-desk="desk"]):not([data-page="sheet"]) body { overflow: visible; }
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .board { position: static; overflow: visible; }
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .canvas {
        display: grid;
        align-items: start;
        gap: 18px;
        width: auto;
        height: auto;
        min-height: 100dvh;
        max-width: 1560px;
        padding: 28px 16px calc(var(--dock-h) + 28px);
    }
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .canvas > .card,
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .canvas > .card:hover {
        /* relative, not static: the Kestrel badge is absolute inside the
           intro card and would leave the page for the viewport's top-left
           corner the moment its card stopped being a containing block. And
           relative honours the offsets absolute was placed by, so the desk's
           own top and left have to go with it — a card still carrying
           left: calc(50% + 475px) in a grid pushes the column off the right
           of the page, which is what it did on a tablet with no script. */
        position: relative;
        top: auto;
        left: auto;
        width: auto;
        margin: 0;
        transform: none;
        -webkit-touch-callout: default;
        -webkit-user-select: auto;
        user-select: auto;
    }
}
@media (min-width: 768px) and (max-width: 1023.98px) {
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .canvas {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 24px;
        padding: 32px 28px calc(var(--dock-h) + 40px);
    }
    :root:not([data-desk="desk"]):not([data-page="sheet"]) .canvas > .card--intro { grid-column: 1 / -1; }
}

/* ----------------------------------------------------------------- sheet

   The desk's composition, sitting still (layouts/base.php, `$sheet`).

   A page somebody is typing into is not a page to be dragged out from under
   them, and "open it on the middle and let the reader push it around" is an
   argument about a homepage rather than about a form. So a sheet keeps the
   cards, the dotted field, the offsets and the lean, and gives up the two
   things that made the desk a desk: the board stops being a window onto
   something larger, and the field stops being larger than the window. The
   document then scrolls as a document, and daylight.js reads the same
   attribute to turn the pan, the drag, the grab cursor and the opening scroll
   off together (`desking`).

   **A sheet is composed twice, and its breakpoint is its own.** The desk
   switches at 768px because that is where two of its cards stop fitting side
   by side on a field it can pan; a sheet cannot pan, so the question is not
   whether the cards fit each other but whether the composition fits the
   window. Below --sheet-w it is flowed — one leaning column, the same --px
   and --prot a phone gets on the desk, which cannot overflow because nothing
   in it is placed. At --sheet-w and up it is placed, and a page's --x and --y
   may reach anywhere inside that field. These rules outrank the desk's own
   without restating its media queries, which is why the flowed half needs no
   media query at all.

   The height has to be given for the placed half — absolutely positioned
   cards contribute none of their own — so the page passes --sheet-h. There is
   no --dx/--dy here and no --lift: nothing on a sheet moves. */

:root[data-page="sheet"] body   { overflow: visible; }
:root[data-page="sheet"] .board { position: static; overflow: visible; }

/* flowed — the sheet below its field's width, at any width the desk would
   have placed at. The 64px of top padding on the desk was clearance for the
   switch, which a sheet does not render. */
:root[data-page="sheet"] .canvas {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 26px;
    width: 100%;
    height: auto;
    /* At least the window, however little the page holds. The dots are no
       longer the reason — a sheet's are the body's now and reach every corner
       on their own — but a field shorter than the screen it is drawn on is
       still the wrong size for the thing it represents. */
    min-height: 100dvh;
    max-width: none;
    margin: 0;
    padding: calc(28px + env(safe-area-inset-top, 0px)) 0 calc(var(--dock-h) + 44px);
}
:root[data-page="sheet"] .canvas > .card {
    position: relative;
    top: auto;
    left: auto;
    width: min(var(--pw, 300px), calc(100vw - 40px));
    margin-right: calc(var(--px, 0px) * var(--nudge) * -1);
    margin-left: calc(var(--px, 0px) * var(--nudge));
    transform: rotate(var(--prot, 0deg));
}

/* placed — from the width the field itself asks for, so a card placed
   anywhere inside that field is whole. */
@media (min-width: 1180px) {
    :root[data-page="sheet"] .canvas {
        display: block;
        position: relative;
        width: 100%;
        max-width: var(--sheet-w, 1180px);
        height: var(--sheet-h, 820px);
        /* ...and on a tall window the field grows past the height the page
           asked for, so a short sheet — log in is one card — sits in the
           middle of the screen rather than clinging to its top. Cards are
           placed from the centre, so they all move together and nothing can
           collide by growing. */
        min-height: calc(100dvh - var(--dock-h) - 32px);
        margin: 0 auto calc(var(--dock-h) + 32px);
        padding: 0;
    }
    :root[data-page="sheet"] .canvas > .card {
        position: absolute;
        top: calc(50% + var(--y, 0px));
        left: calc(50% + var(--x, 0px));
        width: var(--w, 330px);
        margin: 0;
        transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
    }
}

/* The 2px lift, restated because these selectors outrank the desk's. Under
   reduced motion there is no lift and no rotation, which is why the whole
   pair sits behind no-preference rather than being undone further down. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    :root[data-page="sheet"] .canvas > .card:hover {
        transform: translateY(-2px) rotate(var(--prot, 0deg));
    }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) and (min-width: 1180px) {
    :root[data-page="sheet"] .canvas > .card:hover {
        transform: translate(-50%, -50%) translateY(-2px) rotate(var(--rot, 0deg));
    }
}

/* ------------------------------------------------------------------- deal

   A sheet whose cards are dealt rather than placed (`$deal` in
   layouts/base.php): the dashboard, and any page after it whose card set
   belongs to the reader rather than to the view.

   The desk and the sheet are both composed by hand — a card's --x, --y, --w
   and --rot are written into the view — which is only possible for a page
   that knows its own cards. A dashboard does not: it is one card per
   product, per site and per board, nought on the day somebody joins and
   thirty a year later, different for every reader. So the browser lays them
   out, and the view says only which row each one belongs in.

   **Straight, and aligned** (owner's call, 2026-09-03). The first deal kept
   the desk's lean and packed the cards into masonry columns. On a page
   somebody works at, a tilted card is a card that looks dropped, and a
   ragged column is one more thing to read past — so a dealt page is a stack
   of rows: a top row for the cards about the page (the account, and its
   receipts), then one section per kind of holding, each a heading over a
   grid of equal-width cards — three across on a desktop, two on a tablet,
   one on a phone. A row's cards are stretched to one height so their
   footers line up, which is what makes a row of actions read as a row.
   Nothing leans, and nothing has an --x or --y to lose: the sheet's
   direct-child rules never reach a card, because every card is inside a
   row.

   The field is the body's on a sheet, so it already reaches every corner of
   the window; the canvas is only as tall as its rows come to. */

:root[data-page="sheet"] .canvas--deal {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 28px;
    width: 100%;
    max-width: 1180px;
    height: auto;
    min-height: 100dvh;
    margin: 0 auto;
    padding: calc(24px + env(safe-area-inset-top, 0px)) 16px calc(var(--dock-h) + 44px);
}
/* One row of the deal: the top row, or a section's cards. Stretched, so a
   row's cards are one height and their footers meet. */
.deal__top,
.deck__grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
    align-items: stretch;
}
/* The top row's two cards are one height — the account card's. Stretching
   alone would make the row as tall as the longer of the two, and the
   receipts list is the longer one by as much as it has to say; so on the
   row they share, the receipts card is told to take no size from its
   contents (`contain: size`, in the dashboard section), stretches to the
   account card, and its list scrolls inside it (owner's ask, 2026-09-03). */
.deal__top { align-items: stretch; }
.deal__top > .card,
.deck__grid > .card { min-width: 0; }
/* An anchor from the header's tiles lands with a little air above the heading. */
.deck { scroll-margin-top: 16px; }
@media (min-width: 768px) {
    :root[data-page="sheet"] .canvas--deal {
        gap: 32px;
        padding: calc(30px + env(safe-area-inset-top, 0px)) 24px calc(var(--dock-h) + 44px);
    }
    .deal__top,
    .deck__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
    /* On two columns each card about the page takes the row. */
    .deal__top > .card { grid-column: 1 / -1; }
}
@media (min-width: 1024px) {
    .deal__top,
    .deck__grid { grid-template-columns: repeat(var(--deal-cols, 3), minmax(0, 1fr)); }
    /* The card that is the page takes two of the three and whatever stands
       beside it the third — the dashboard's receipts, a board's traffic; with
       nothing beside it, it takes the row. By position rather than by name,
       because the next page to be dealt will stand something else there. */
    .deal__top > .card--head { grid-column: span 2; }
    .deal__top > .card--head:only-child { grid-column: 1 / -1; }
    .deal__top > .card--head + .card { grid-column: auto; }
}

/* The rows of a dealt page (views/dashboard/*.php and every page after
   them): one section per kind of thing, a heading over a grid. */
/* deck — one section per kind of holding: a heading with the kind's glyph
   and its count, one line on what the kind is, then the grid. The heading
   is the page's structure and is set like one, not like a card's eyebrow. */
.deck { display: flex; flex-direction: column; gap: 12px; }
.deck__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; padding: 0 2px; }
.deck__title { display: inline-flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 650; letter-spacing: -0.015em; line-height: 1.3; }
.deck__title svg { flex: none; color: var(--muted); }
.deck__count { color: var(--muted); font-weight: 500; }
.deck__note { flex-basis: 100%; margin: 0; }
/* A card that takes the whole row rather than a column of it: the three tags
   on a site's page are equals and theming is the reference under them; a
   board's listings are one table and there is nothing to stand beside it. At
   one column it is already the row, so this holds at every width. */
.deck__grid > .card--wide { grid-column: 1 / -1; }

/* ------------------------------------------------------------------ card

   --surface, --radius, --shadow, 18–32px padding, and no visible edge in the
   light — the reference cards have none, the shadow carries it; in the dark
   there is no shadow, so --card-edge becomes a hairline. Three regions: a
   header row (a bold 14px title, an optional chip), a body, and a footer row
   under a divider. On hover it lifts 2px over 150ms. Kinds are a class on
   the same element (DESIGN §5). */

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: 0;
    padding: 18px 20px;
    border: 1px solid var(--card-edge);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
    font-size: 14px;
    line-height: 1.45;
    transition: transform 150ms ease, box-shadow 150ms ease;
}
@media (hover: hover) {
    .card:hover { transform: translateY(-2px); }
}
.card > p { margin: 0; }

.card.is-dragging {
    z-index: 5;
    /* The card grows a hair as it comes off the desk. On a pointer the
       cursor says it is held; a finger covers the cursor's job, so the size
       and the deeper shadow are all the confirmation a phone gets that the
       long press worked. */
    --lift: 1.03;
    cursor: grabbing;
    user-select: none;
    transition: none;
    box-shadow: 0 2px 4px rgba(21, 20, 18, 0.06), 0 28px 56px rgba(21, 20, 18, 0.16);
}

.card__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.card__title {
    flex: 1 1 auto;
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.3;
}
.card__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-top: auto;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}
/* Two or three small pills, grouped. The footer row puts one thing at each
   end, which is the reference's shape and the wrong one for a row that is
   nothing but actions. */
.card__acts { display: flex; flex-wrap: wrap; gap: 8px; }

/* The kinds every page shares. Each is a class on the same element and
   comes after .card, whose padding and gap it overrides at equal specificity. */
/* flat — a card that sits on the field rather than floating over it: a
   hairline edge instead of the shadow, so it reads as the page rather than
   as another loose card. The homepage's centrepiece is one, and so is the
   form on a sheet — the card that *is* the page, in both cases. */
.card--flat {
    border-color: var(--border);
    box-shadow: none;
}

/* head — the card that is the page: the account, what it holds as tiles,
   the ways to add, and the only sign-out on the site. Flat on the field
   like the homepage's centrepiece and the join form, for the same reason:
   the card that is the page does not float over it. */
.card--head { gap: 16px; padding: 24px 24px 20px; }
.card--head h1 { font-size: 26px; line-height: 1.15; letter-spacing: -0.025em; }
.head__top { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 8px 16px; }
.head__top > form { flex: none; }
.head__who { margin-top: 4px; }
.card--head .doors { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; }
.card--head p strong { color: var(--text); }
/* The way to a human, at the other end of the footer from the actions. */
.head__help { margin-left: auto; text-align: right; }

@media (max-width: 767px) {
    .card--head { padding: 20px 18px 18px; }
    .card--head h1 { font-size: 24px; }
    .head__help { margin-left: 0; text-align: left; }
}

/* form — the card that asks for more than one thing: join, log in, and the
   dashboard forms after them.

   The action card above is one form and one control, a well with a send
   button in its corner, and that is the right shape for a single address. It
   is the wrong shape for three fields, so this is the other one: a card whose
   header row is the page's own heading, then the fields, then a pill. What
   the two share is the field itself, which is partials/field.php and is
   written once.

   Everything below is spacing and the error line. The controls themselves are
   the bare form styles at the bottom of this file — an input is an input on
   every page — and the button is a plain .pill--primary, which already
   carries `border: 0` and `font: inherit` and so needs nothing to sit on a
   <button>. */

.card--form { gap: 14px; padding: 26px 24px 24px; }
/* The heading of the card that *is* the page, so it takes the page's size
   rather than a card title's 14px, and the flex-grow a .card__title has, so
   the chip beside it still goes to the right. */
.card--form .card__head > h1 { flex: 1 1 auto; font-size: 24px; line-height: 1.15; }
.fields { display: flex; flex-direction: column; gap: 12px; }

/* list and traffic — a table in a card, and a card of figures that stands
   beside the card that is the page: a board's, an ad's, and the public pages. */
.card--list { gap: 10px; }
/* The traffic card stands beside the card that is the page and stretches to
   its height (the deal's top row). The figures stay at the top and the line
   that qualifies them drops to the foot, so the slack is between them rather
   than in a hole underneath — the receipts card's arrangement, for the same
   reason. */
.card--traffic > .small:last-child { margin-top: auto; }
.card--list .card__foot p { margin: 0; }

/* ---------------------------------------------------------------- figures

   A number and what it counts, a few of them across a card
   (partials/figures.php). The homepage's two tiles are the same idea with a
   border and a sparkline round it; these are the bare pair, because inside a
   card that is already about one thing a border would be a box in a box.

   Tabular, like every figure on this site: a column of cards should have its
   numbers in the same places whatever digits they happen to be. */
.figures { display: flex; flex-wrap: wrap; gap: 12px 16px; }
.figure { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.figure__value { font-size: 17px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.15; }
.figure__label { color: var(--muted); font-size: 11px; line-height: 1.3; }
/* Colour is never the only carrier: both of these figures are labelled in
   words, and the label is what says which is which. */
.figure--verified .figure__value { color: var(--up); }
.figure--flagged .figure__value { color: var(--flagged); }
/* The row's history, at its end: one figure's fortnight, as the tiles on
   the homepage carry theirs. Bottom-aligned with the figures so the line
   sits on the same baseline the numbers do. */
.figures__spark { display: flex; align-self: flex-end; align-items: flex-end; margin-left: auto; padding-bottom: 2px; }

/* ----------------------------------------------------------------- pills

   Primary is --ink on --on-ink, fully rounded, 36px tall like the
   reference's. Secondary is --pill with --text. Small is the outlined one in
   a card's footer row. Links inside prose are --accent, underlined on hover. */

.pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 36px;
    padding: 0 16px;
    border: 0;
    border-radius: 999px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: transform 150ms ease, opacity 150ms ease;
}
.pill:hover { text-decoration: none; }
@media (hover: hover) {
    .pill:hover { transform: translateY(-1px); }
}
.pill--primary { background: var(--ink); color: var(--on-ink); }
.pill--primary:hover { opacity: 0.92; }
.pill--secondary { background: var(--pill); color: var(--text); }
.pill--small {
    min-height: 30px;
    padding: 0 12px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font-size: 12px;
    font-weight: 550;
}
.pill--small:hover { border-color: var(--text); }

/* ----------------------------------------------------------------- chips

   12px type, fully rounded, on the surface with a hairline — the reference's
   chips are white with a light edge. Categories, statuses, "verified", and
   "not counted" in --flagged. */

.chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 9px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    font-family: var(--sans);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.3;
    white-space: nowrap;
}
.chip--verified { color: var(--up); }
.chip--flagged { color: var(--flagged); }
/* A glyph before the label (View::icon): a category's on the ad cards, the
   price tag on "free" in --lit-ink — the one coloured thing in a chip,
   because free is the pitch, and the one shade of the sun that can be read at
   12px. Tucked 2px into the padding, which was set for
   text alone. */
.chip__icon { flex: none; margin-left: -2px; }
.chip--free .chip__icon { color: var(--lit-ink); }

/* A quieter chip, for a card header that is labelling rather than flagging. */
.chip--quiet { color: var(--muted); background: none; border: 1px solid var(--border); font-weight: 500; }

/* The claimed badge on /a/{address} wears `chip chip--verified tag verified`.
   The first two are this file's: the shape, and the one tone --up is for. The
   pair after them is V1's name for the same component, kept because three
   acceptance checks read those two words out of the rendered page — a badge
   that says what it is in its own markup is cheaper than three greps drifting
   out of step with it. `.tag` styles nothing. */

/* ------------------------------------------------------------------ forms

   The bare controls: a label is a label and an input is an input on every
   page, styled once. The field with its hint (partials/field.php), the
   checkbox with its sentence (partials/check.php), a form's submit and the
   error line follow; the cards they sit in are above. */

label { display: block; margin-top: 12px; color: var(--muted); font-size: 12px; }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
    width: 100%;
    margin-top: 4px;
    padding: 8px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--field);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}
input:hover, select:hover, textarea:hover { border-color: var(--faint); }
button { font: inherit; }

/* Under 16px iOS zooms the page when a field takes focus, which on a fixed
   board leaves the desk somewhere the reader did not put it. */
@media (max-width: 767px) {
    input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea { font-size: 16px; }
}

/* Inside a field the label is the first line, so the 12px that separates a
   bare label from what precedes it is the gap's job here, not the label's. */
.field label { margin-top: 0; }
.field__hint { margin: 5px 0 0; }
/* Content width, like the intro card's pills: a form's submit is still a
   pill, and stretching it edge to edge is the one place this page would stop
   looking like the rest of them. */
.card__go { align-self: flex-start; }

/* The one thing that went wrong, above the fields rather than under the
   button. The words say what is wrong, so --down is not carrying it alone;
   the mark is there to be seen before the sentence is read. */
.formerr {
    display: flex;
    align-items: flex-start;
    gap: 7px;
    margin: 0;
    font-size: 13px;
    line-height: 1.4;
}
.formerr__mark { flex: none; margin-top: 2px; }

/* A checkbox and the sentence it is agreeing to (partials/check.php). The box
   is the control's own, tinted to the accent rather than redrawn: a hand-built
   checkbox is a keyboard bug waiting to happen, and the native one already
   knows every state this needs. The label is body text, not the 12px muted
   name a bare <label> gets — here the label *is* the sentence — and the line
   under it is where the idea gets explained, so both sit in a column beside
   the box and the box holds the first line. */
.check { display: flex; align-items: flex-start; gap: 10px; }

/* The box, drawn rather than replaced. The input is still the control — focus,
   keyboard, label, value, and the browser's own restore-on-back all belong to
   it — and `appearance: none` takes only the picture, which is the whole reason
   this is safe to do at all. Everything below is paint over a real checkbox.

   Unchecked it is a hairline square on the card, and it takes its hover from
   the same --faint every other input on the page takes. Checked it fills with
   the accent and the tick draws itself on. The focus ring is its own state and
   never the same as hover: a soft accent halo, on :focus-visible so a mouse
   click does not leave the page wearing one. */
.check__control { position: relative; display: inline-flex; flex: none; margin-top: 1px; }
.check__box {
    appearance: none;
    -webkit-appearance: none;
    display: block;
    width: 18px;
    height: 18px;
    margin: 0;
    border: 1.5px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    cursor: pointer;
    transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 90ms ease;
}
.check__box:hover { border-color: var(--faint); }
.check__box:focus-visible {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
    outline: none;
}
/* The press: the only movement the box itself makes, and it is the one that
   makes a drawn control feel like a real one under a finger. */
.check__box:active { transform: scale(0.92); }
.check__box:checked { border-color: var(--accent); background: var(--accent); }

/* The tick. It sits over the box, inert to the pointer so every click still
   lands on the input, and it is drawn on rather than switched on: the
   polyline's two segments are 15.56 and 7.07 units, so 23 is the whole stroke
   and the dash walks it from hidden to complete. The fill lands first and the
   tick follows it by 40ms, which is the difference between a box that changes
   and a box that gets ticked. */
.check__mark {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--on-accent);
    pointer-events: none;
}
.check__svg polyline {
    stroke-dasharray: 23;
    stroke-dashoffset: 23;
    transition: stroke-dashoffset 200ms ease-out 40ms;
}
.check__box:checked + .check__mark .check__svg polyline { stroke-dashoffset: 0; }

/* Reduced motion: the tick is there or it is not, and the box still says so
   with its fill. Nothing is drawn, pressed or eased. */
@media (prefers-reduced-motion: reduce) {
    .check__box { transition: none; }
    .check__box:active { transform: none; }
    .check__svg polyline { transition: none; }
    .check__box:checked + .check__mark .check__svg polyline { stroke-dashoffset: 0; }
    .check__box:not(:checked) + .check__mark .check__svg polyline { stroke-dashoffset: 23; }
}

/* Forced colours: hand the control back. A drawn checkbox is a picture, and a
   picture is exactly what this mode is built to ignore — so the native box
   returns with the system's own colours, and the tick that would sit on top of
   it goes away. */
@media (forced-colors: active) {
    .check__box { appearance: auto; -webkit-appearance: auto; border: 0; }
    .check__mark { display: none; }
}
.check__body { min-width: 0; }
.check__label {
    display: block;
    margin: 0;
    color: var(--text);
    font-size: 14px;
    line-height: 1.35;
    cursor: pointer;
}
.check__hint { margin: 4px 0 0; }
/* The hint that teaches rather than names a format. A field's hint is 12px
   muted because "at least 8 characters" is read once and never again; this one
   is the only place a capability gets explained, and set the same way it reads
   as small print to skip. So it takes the strip card's device — an accent rule
   down its edge — a little more line to breathe, and the phrases carrying the
   idea in the text colour. Marked as worth reading, not made loud: the rule and
   the weight say it, so the colour is not saying it alone. */
.check__hint--offer {
    margin-top: 7px;
    padding-left: 11px;
    border-left: 2px solid var(--lit);
    line-height: 1.55;
}
.check__key { color: var(--text); font-weight: 600; }
/* A domain wraps like the rest of the sentence. Holding it together with
   nowrap was tried and measured: a pill of a domain — 39 characters — then ran
   120px outside the card and scrolled the whole page sideways at 320px, which
   is the badge's bug wearing a different hat. A line ending inside a domain is
   a cosmetic cost at some widths; a line ending outside the card is a broken
   page at one. `anywhere` is the floor under both: whatever a domain's length,
   it breaks rather than escapes. */
.check__hint { overflow-wrap: anywhere; }

/* ------------------------------------------------------------------- note */

/* The note belongs to the field above it, so it sits nearer the well than
   the card's own 12px gap would put it.

   It is the field's promise rather than body copy, and it read as another
   grey line beside the sentence under it: an accent mark for what happens
   when you press the button, and the fact itself at full contrast while
   the rest of the line stays muted. The sentence below keeps its plain
   treatment, which is now the difference between them. */

/* note — the small line under a card's body (partials/note.php): a return
   mark in --lit-ink, one muted sentence, and the pieces of it that carry the
   weight in the text colour. Named for what it is rather than for the action
   card it started in, now that the join page draws one under a paragraph. */
.card > .note { margin-top: -4px; }
.note { display: flex; align-items: flex-start; gap: 7px; }
.note__mark { flex: none; margin-top: 2px; color: var(--lit-ink); }
.note__key { color: var(--text); font-weight: 600; }

/* -------------------------------------------------------------------- ask */

/* action — one form, one control: the reference's input card, which here is
   the product door's real form. The input sits in a well with the send
   button beside it on the same row, and the field's note sits under the
   well rather than inside it (owner's call, 2026-09-02) — one line, one
   thing, and nothing competing with what you typed. */
.ask {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 8px 8px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
}
.ask:focus-within { border-color: var(--text); }
.ask .ask__input {
    flex: 1;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 15px;
}
.ask .ask__input:focus { outline: none; }
/* The category, in the well beside the address (partials/ask.php). The bare
   form select with its box taken off: the well is already the box, and a
   second border inside it would read as two fields where there is one thing
   being added. Sized by its content, so the address keeps the room. */
.ask__select {
    width: auto;
    max-width: 40%;
    margin: 0;
    padding: 4px 2px 4px 6px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--muted);
    font-size: 13px;
}
.ask__select:hover { border-color: transparent; color: var(--text); }
.ask__go {
    display: grid;
    flex: none;
    place-items: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--ink);
    color: var(--on-ink);
    cursor: pointer;
}
.ask__go:hover { opacity: 0.9; }

@media (max-width: 767px) {
    .ask .ask__input { font-size: 16px; }   /* the iOS zoom rule, for the well's own input */
}

/* ---------------------------------------------------------- creative row

   Shared row vocabulary. The creative row is an ad shown small — its mark,
   its headline, and on some pages a line under it — for the dashboard's
   product cards, a board's listings and the page found at a domain being
   added; it is not partials/product.php, which presents an ad to a stranger.
   The offer is a sentence and one button that does the thing it describes
   (a site becoming a board, from its own card), and the also list is the
   things a site can also be, under its figures. */

.creative { display: flex; align-items: flex-start; gap: 10px; }
.creative__mark {
    flex: none;
    width: 44px;
    height: 44px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--pill);
    object-fit: cover;
}
.creative__title { min-width: 0; font-size: 14px; font-weight: 600; line-height: 1.35; }
/* The row with a second line under the headline: the page we found at a
   domain being added (views/submit/site.php), where the description is part
   of the evidence that this is the site they meant. The dashboard's product
   cards show the headline alone and are unaffected. */
.creative__words { min-width: 0; }
.creative__desc {
    display: -webkit-box;
    margin: 3px 0 0;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.4;
}

.offer { margin: 0; }
.offer__go {
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.4;
    text-align: left;
    cursor: pointer;
    text-decoration: underline;
    text-decoration-color: var(--lit);
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}
.offer__go:hover { text-decoration-color: var(--accent); }

/* The things a site can also be, as a short list under its figures: list it
   in the library, make it a board, manage the board it is. Each is one line
   that does the thing and one muted line that says what the thing is —
   the two sentences a capability gets, and no more. */
.also { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; font-size: 13px; line-height: 1.4; }
.also > li { min-width: 0; }
.also a { font-weight: 600; }
.also__why { display: block; margin-top: 1px; }

/* ------------------------------------------------------ tiles and sparklines

   The number card at tile scale (DESIGN §5): the figure, its sparkline
   (partials/spark.php) and its label — the homepage's box of two, and the
   dashboard's one-per-kind. */

/* number — two small tiles in a small box: the figure, its sparkline, its
   label. DESIGN §5's number card at pill scale, close together. */
.card--stats { padding: 8px; }
.stats { display: flex; gap: 8px; }
.stat {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    padding: 6px 10px 5px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    color: var(--text);
    text-decoration: none;
}
.stat:hover { border-color: var(--muted); text-decoration: none; }
.stat__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.stat__figure { font-size: 17px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.1; }
.stat__label { color: var(--muted); font-size: 11px; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.spark { flex: none; display: block; overflow: visible; }
.spark__area { fill: currentColor; opacity: 0.14; }
.spark__line { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.spark__dot { fill: currentColor; }

/* The tiles: one per kind the account holds, and only those. The
   homepage's number box at card scale — the figure and its fortnight under
   the kind's own word, so the row reads products, sites, boards before it
   reads any number. Each is a link down to its section. */
.stats--kinds { flex-wrap: wrap; gap: 10px; }
.stats--kinds .stat { flex: 1 1 200px; max-width: 380px; gap: 3px; padding: 10px 12px 9px; }
.stat__kind {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--muted);
    font-size: 10.5px;
    font-weight: 650;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.stat__kind svg { flex: none; }
.stat__kind .num { margin-left: auto; color: var(--text); letter-spacing: 0; }
.stats--kinds .stat__figure { font-size: 22px; }
.stats--kinds .stat__label { white-space: normal; font-size: 11.5px; }

/* ------------------------------------------------------------------- unit */

/* unit — the live ad, in the widget's own markup, sitting in a well the way
   the reference insets a quotation. Daylight is the publisher here and sets
   the widget's variables exactly as any publisher would: the supported
   surface, and the only way this stylesheet reaches a unit. /f.css itself is
   untouched, and the disclosure mark is the real one.

   The card is DESIGN §5's *unit* card (partials/unit_card.php): the submit
   preview's ad, the slot on a site being added, and the unit editor's live
   one when it lands. The well is what makes the difference between "here is
   the page" and "here is a thing that will appear on somebody else's page" —
   the unit is quoted, not presented, so it sits on --field inside the card
   with a hairline round it, at the width a sidebar slot really is. Nothing
   in here reaches inside the unit's own markup. */
.card--unit .unit {
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--field);
}
.canvas [data-daylight] {
    --dl-bg: var(--pill);
    --dl-fg: var(--text);
    --dl-muted: var(--muted);
    --dl-accent: var(--lit-ink);
    --dl-border: transparent;
    --dl-shadow: none;
    --dl-radius: 10px;
}

/* ---------------------------------------------------------------- snippet

   A block of code to copy (partials/snippet.php), its copy button, and the
   token colours Daylight\Code emits. */

.snippet {
    margin: 12px 0 0;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--field);
    overflow-x: auto;
    position: relative;
}
/* The copy button, added by daylight.js only when there is a clipboard to
   copy to — so a page with no script has a block to select, not a dead
   control. It sits in the block's corner and steps out of the way of the
   first line. */
.snippet__copy {
    position: absolute;
    top: 6px;
    right: 6px;
    padding: 3px 9px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--muted);
    font-size: 11px;
    line-height: 1.6;
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms ease, color 120ms ease, border-color 120ms ease;
}
.snippet:hover .snippet__copy,
.snippet__copy:focus-visible { opacity: 1; }
.snippet__copy:hover { color: var(--text); border-color: var(--faint); }
.snippet__copy.is-done { opacity: 1; color: var(--text); border-color: var(--faint); }
@media (hover: none) { .snippet__copy { opacity: 1; } }

.snippet code {
    display: block;
    color: var(--text);
    font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: 11.5px;
    line-height: 1.6;
    /* Scrolls rather than wraps (owner's ask). A pasted tag is one long line
       by nature, and wrapping it turned a three-line snippet into eleven and
       broke URLs mid-path. Copying is unaffected: the button reads
       textContent, which a scrollbar does not touch. */
    white-space: pre;
}

/* Syntax colour, from the palette the rest of the page already uses rather
   than from an editor theme: this is a page about receipts, not a code
   editor, so the tokens are the site's own accents at reading weight. The
   tokeniser is Daylight\Code and every token is escaped. */
.tok--comment { color: var(--muted); font-style: italic; }
.tok--tag,
.tok--verb    { color: var(--accent); font-weight: 600; }
.tok--attr,
.tok--prop,
.tok--key     { color: var(--lit-ink, var(--text)); }
.tok--str,
.tok--num     { color: var(--impressions, var(--muted)); }
.tok--custom  { color: var(--clicks, var(--text)); font-weight: 500; }
.tok--sel     { color: var(--text); font-weight: 600; }
.tok--mustache {
    color: var(--flagged, var(--muted));
    background: var(--accent-soft);
    border-radius: 3px;
    padding: 0 2px;
}
.tok--punct   { color: var(--muted); }

/* ------------------------------------------------------- tables and facts */

table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: left; font-size: 13px; vertical-align: top; }
th { color: var(--muted); font-size: 12px; font-weight: 500; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A table wider than its card scrolls inside it. `position: relative` is
   load-bearing rather than decoration: a visually-hidden label inside the
   table is `position: absolute`, and with no positioned ancestor its
   containing block is the page — so its static position, out at the far end
   of a 630px table, made the whole document 84px wider than the phone it was
   being read on. It is contained here for the same reason `.snippet` is
   positioned: the scroller has to own everything inside it. */
.scroll { position: relative; overflow-x: auto; }

/* facts — the label/value pairs a card lists: an ad's title, description and
   link on /dash/ad/{slug}, and the same three on its public page when that
   lands. A <dl>, because that is what a list of terms and their values is; the
   term is set like every other small label on the site and the value is body
   text, so the pair reads as a caption over a line rather than as a table. */
.facts { display: flex; flex-direction: column; gap: 2px; margin: 0; }
.facts dt { color: var(--muted); font-size: 11px; line-height: 1.3; }
.facts dt + dd { margin: 0 0 10px; }
.facts dd:last-child { margin-bottom: 0; }
.facts dd { margin: 0; line-height: 1.45; }
/* A product URL is one unbreakable token and a card is 340px wide, so it wraps
   wherever it has to rather than pushing the card open. */
.facts__wrap { overflow-wrap: anywhere; }

/* ------------------------------------------------------------------ claim

   The way a product's owner takes their listing over, on /a/{address}: a
   <details> inside the claim card, because the instructions are three
   paragraphs that most readers of a public stats page will never need, and a
   card that opens is cheaper than a page they have to go to.

   It is a disclosure widget and not a control we drew: the marker stays, the
   summary is a pill-shaped row so it reads as pressable, and with no CSS at
   all it is still a working <details>. There is no claim *button* to style —
   claiming is a conversation with a human until the Phase 2 verification
   lands (SPEC §6), and a button that opened an email client would be
   promising a flow that does not exist. */

.claim { margin: 2px 0 0; }
/* The summary is a small pill — this site's one word for "pressable" — and
   nothing else: `display: inline-flex` from .pill drops Chrome's disclosure
   marker, which is the trade. A pill that opens a card is the same gesture as
   a pill that goes somewhere, and with no stylesheet at all it is still a
   <summary> with its own marker. */
.claim__go { width: fit-content; }
.claim[open] .claim__go { margin-bottom: 10px; }
.claim__body { display: flex; flex-direction: column; gap: 8px; }
.claim__body p { margin: 0; }

/* --------------------------------------------------------------- homepage

   The desk's own cards (views/public/home.php): the intro and the mark, the
   receipts strip, the numbers box, and the three ad cards (partials/product.php). */

/* intro — the reference's centrepiece: a mark where its avatar is, the
   badges, a ~36px heading, two short sentences, one black pill and one grey.
   Compact: 400px wide on the desk. */
.card--intro {
    gap: 14px;
    padding: 36px 32px 32px;
    font-size: 14px;
    line-height: 1.5;
}
.card--intro h1 { margin-top: 4px; font-size: 34px; line-height: 1.12; letter-spacing: -0.028em; }
.brand__name { font-size: 15px; font-weight: 650; letter-spacing: -0.015em; }
.brand__tld { color: var(--accent); }
.card--intro p { color: var(--muted); }
.card--intro p strong { color: var(--text); }
.card--intro .doors { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.card--intro .small { font-size: 12px; }

/* The mark: a tile the size of the reference's avatar, with a small sun —
   the one place the brand gets to be literal about its own name. The tile is
   --lit in both themes; the sun on it is --on-lit, which is the one token
   that swaps (owner's ask, 2026-09-04): white in the light, the dark brown in
   the dark, so the mark reads as belonging to the theme it is sitting in. */
.brand {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
}
.brand__tile {
    display: grid;
    flex: none;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: 9px;
    background: var(--lit);
    color: var(--on-lit);
}

/* strip — the receipts above the intro: a beacon and an eyebrow, then up
   to three rows, each the ad's thumbnail and one line that truncates, so
   the card keeps its width whatever the line says; the owner's note takes
   the first row. A thin bar of the sun down the edge — a rule is a shape, so
   the yellow does it — and a small rise when the page opens. */
.card--strip {
    gap: 8px;
    padding: 9px 12px 10px 14px;
    overflow: hidden;
    font-size: 13px;
    line-height: 1.3;
    animation: strip-in 480ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.card--strip::before {
    content: "";
    position: absolute;
    top: 10px;
    bottom: 10px;
    left: 0;
    width: 3px;
    border-radius: 0 3px 3px 0;
    background: var(--lit);
}
.strip__head { display: flex; align-items: center; gap: 8px; margin: 0; }
.strip__pulse { position: relative; display: block; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--lit); }
.strip__pulse::after {
    content: "";
    position: absolute;
    inset: -4px;
    border: 2px solid var(--lit);
    border-radius: 50%;
    opacity: 0;
    animation: strip-ring 2.4s ease-out infinite;
}
.strip__eyebrow { color: var(--muted); font-size: 10.5px; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; }
.strip__rows { display: flex; flex-direction: column; gap: 5px; margin: 0; padding: 0; list-style: none; }
.strip__row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.strip__row--note { color: var(--accent); }
.strip__row--note a { color: inherit; }
.strip__mark {
    display: grid;
    flex: none;
    place-items: center;
    width: 24px;
    height: 24px;
    border-radius: 6px;
    background: var(--accent-soft);
    color: var(--accent);
}
.strip__thumb {
    display: block;
    flex: none;
    width: 24px;
    height: 24px;
    border-radius: 6px;
    background: var(--pill);
    box-shadow: 0 1px 2px rgba(21, 20, 18, 0.12);
    object-fit: cover;
}
.strip__line { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.strip__domain { color: var(--accent); font-weight: 650; }
.strip__when { color: var(--muted); }
@keyframes strip-in { from { opacity: 0; translate: 0 10px; } to { opacity: 1; translate: 0 0; } }
@keyframes strip-ring { 0% { transform: scale(0.5); opacity: 0.9; } 100% { transform: scale(1.5); opacity: 0; } }


/* product — a recent ad led by its image, captioned with its domain and its
   category the way the reference captions its photographs. A product with no
   image of its own shows the generated card it carries everywhere else. */
.card--product { gap: 0; padding: 0; overflow: hidden; }
.product { display: block; color: var(--text); text-decoration: none; }
.product:hover { text-decoration: none; }
.product:hover .product__name { text-decoration: underline; text-decoration-color: var(--lit); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.product__img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    background: var(--pill);
    object-fit: cover;
}
/* No og:image: the generated card it carries everywhere else, at badge size in
   a well, rather than a letter blown up to the size of a photograph. */
.product__img--card { padding: 22px; object-fit: contain; }
.product__body { padding: 12px 16px 14px; }
.card--product-board .product__body { padding-bottom: 12px; }
.product__caption {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.product__name {
    min-width: 0;
    overflow: hidden;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.product__title {
    display: -webkit-box;
    margin-top: 4px;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    font-size: 13px;
    line-height: 1.35;
}
.product__nums { display: block; margin-top: 2px; }

/* wide — the same card as a row: the image at the left at a fixed width,
   filling the row's height, and the words at the right with the ad's own
   title between the domain and the numbers, since the picture no longer
   carries it. The shape of a horizontal ad — the widget's own card layout
   is this shape — on one slot of the desk for now (owner's ask, 2026-09-02,
   to see how it looks). */
.card--product-wide .product { display: flex; align-items: stretch; }
.card--product-wide .product__img { flex: none; width: 150px; height: auto; aspect-ratio: auto; }
.card--product-wide .product__img--card { padding: 14px; }
.card--product-wide .product__body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
    padding: 14px 16px;
}

/* An ad from a board ends the way the reference's small cards end: a footer
   row under a divider (DESIGN §5). The board's own mark, "via {board}" as a
   link to it in the words the unit's disclosure uses, and the clicks the
   network has sent the board — its receipt (SPEC §9.4), never the product's.
   The row wraps rather than truncates: a long board domain pushes the number
   to its own line, and both stay readable. */
.product__credit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 8px;
    padding: 10px 16px 12px;
    border-top: 1px solid var(--border);
}
.product__credit-mark {
    display: block;
    flex: none;
    width: 20px;
    height: 20px;
    border-radius: 5px;
    background: var(--pill);
    box-shadow: 0 1px 2px rgba(21, 20, 18, 0.12);
    object-fit: cover;
}
.product__board { color: var(--text); font-weight: 650; text-decoration: none; }
.product__board:hover { text-decoration: underline; text-decoration-color: var(--lit); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.product__sent { margin-left: auto; white-space: nowrap; }

/* On a phone the wide ad has no room beside its text: it becomes the card
   the other two are, image over words. */
@media (max-width: 767px) {
    .card--product-wide .product { display: block; }
    .card--product-wide .product__img { width: 100%; height: auto; aspect-ratio: 16 / 10; }
    .card--product-wide .product__img--card { padding: 22px; }
    .card--product-wide .product__body { display: block; padding: 12px 16px 14px; }
}

/* ---------------------------------------------------------- kestrel badge

   After every card kind that wears it (.card--intro, .card--form, .card--head)
   because .card--badged overrides their padding at equal specificity. */

/* The Kestrel badge, mocked up in HTML to the image's own measurements
   (kestrel.host lib/Badge.php, the pill): 34px tall, 13.5px system type, a
   pulsing dot at the live reading, bold figures with regular labels, middots
   between readings; the whole pill is the link, as the embed is. It paints
   its own ground in the badge's colours because the real one will. Replaced
   by an <img> when the badge is wired. */
.kbadge {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    max-width: 100%;
    min-height: 34px;
    margin: 0 !important;
    padding: 0 15px;
    border: 1px solid var(--kb-edge);
    border-radius: 17px;
    background: var(--kb-plate);
    color: var(--kb-text);
    font-size: 13.5px !important;
    line-height: 32px;
    white-space: nowrap;
}
.kbadge b { font-weight: 700; }
.kbadge__label { color: var(--kb-muted); }
.kbadge__live, .kbadge__live b { color: var(--kb-accent); }
.kbadge__dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 7px;
    border-radius: 50%;
    background: var(--kb-accent);
    vertical-align: -1px;
    animation: kb-pulse 2.6s ease-in-out infinite;
}
.kbadge__sep { padding: 0 7px; color: var(--kb-sep); }
a.kbadge { text-decoration: none; }
a.kbadge:hover { border-color: var(--kb-muted); text-decoration: none; }
@keyframes kb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.28; } }

/* Stuck to the card's top edge, half over it, so it goes where the card
   goes when the card is dragged. Inset 24px from the left, and never nearer
   than that to the right: a pill cannot shrink, so without the ceiling a
   reading too many runs it off the card, off the screen, and — the part that
   is not merely ugly — scrolls the whole page sideways to follow it. */
.kbadge--attached {
    position: absolute;
    top: 0;
    left: 24px;
    z-index: 1;
    max-width: calc(100% - 48px);
    transform: translateY(-50%);
}
/* ...and the card it is stuck to keeps clear of it: the badge's lower half
   is 17px inside the top edge, so the first line needs its padding back.
   Last in the file so it outranks the padding shorthand of whichever card
   kind is carrying the badge. */
.card--badged { padding-top: 36px; }

/* At the narrowest phone a card is 280px wide and an attached pill has 232 of
   that. Both badges want more at full size — the homepage's two readings by a
   little, the join card's four by a lot — and a pill neither shrinks nor, at
   this width, wraps: it keeps its width, lets the ceiling clip the plate, and
   prints the rest of the reading on the field outside it. So the badge
   tightens here: a point off the type, less air either side of it and of the
   middots, and half the inset from the card's edge. The composition holds,
   tightened — the same bargain the nudge makes at this width. */
@media (max-width: 359px) {
    .kbadge--attached {
        left: 12px;
        max-width: calc(100% - 24px);
        padding: 0 10px;
        font-size: 12px !important;
    }
    .kbadge--attached .kbadge__sep { padding: 0 4px; }
}

/* A sheet is composed twice and its cards are narrower on the flowed half —
   330px, or a 320px phone's 280 — where a pill of four readings has nowhere
   to sit on a top edge. Under the ceiling above it would wrap, and a pill
   that wraps across the corner of a card is a rounded plate with a separator
   dangling off the end of its first line.
 
   So below the field's own width the badge comes off the edge and becomes the
   card's first line instead: in flow, shrink-wrapped, inside the card's own
   padding. The card then grows to hold it rather than the page growing to
   hold the card, which is the difference between a long badge looking cramped
   and a long badge breaking the page. A point off the type and a little off
   the padding is what keeps it to one line at 330 — the flowed half is a
   drawn pill either way, not the badge's image, so its measurements are the
   card's to negotiate. The desk's own badge is untouched: this is the sheet's
   rule, and the homepage is not a sheet. */
:root[data-page="sheet"] .kbadge--attached {
    position: static;
    align-self: flex-start;
    /* It may wrap here, and that is the point of being in flow: a pill that
       cannot wrap does not shrink either — it keeps its own width, lets
       max-width clip the plate, and prints the last reading on the field
       outside it. Better a taller badge than a reading half in the open. */
    flex-wrap: wrap;
    max-width: 100%;
    padding: 0 12px;
    font-size: 12.5px !important;
    transform: none;
}
:root[data-page="sheet"] .kbadge--attached .kbadge__sep { padding: 0 5px; }
/* On the narrowest phone four readings do not fit one line at any size worth
   reading, so this is the one place the badge is two. A middot is a mark
   *between* readings and has nowhere to be at the end of a line, so at this
   width the separating is done by the gap instead, and the rows are set
   closer than the pill's own 32px line. Same four figures, same plate, one
   row shorter of an excuse. The composition holds, tightened — as the nudge
   does at this width. */
@media (max-width: 359px) {
    :root[data-page="sheet"] .kbadge--attached {
        padding: 5px 12px;
        column-gap: 11px;
        row-gap: 1px;
        line-height: 23px;
    }
    :root[data-page="sheet"] .kbadge--attached .kbadge__sep { display: none; }
}
/* In flow the card needs no clearance for it — the badge takes its own room
   — so the card goes back to the padding its own kind asks for. */
:root[data-page="sheet"] .card--badged { padding-top: 26px; }

/* ...and the placed sheet, from the width the field itself asks for, puts it
   back on the edge at its own size. */
@media (min-width: 1180px) {
    :root[data-page="sheet"] .kbadge--attached {
        position: absolute;
        align-self: auto;
        max-width: calc(100% - 48px);
        padding: 0 15px;
        font-size: 13.5px !important;
        transform: translateY(-50%);
    }
    :root[data-page="sheet"] .kbadge--attached .kbadge__sep { padding: 0 7px; }
    :root[data-page="sheet"] .card--badged { padding-top: 36px; }
}

/* The pill is 13.5px type and cannot wrap; a phone's intro card gives up some
   padding so it fits on one line. After .card--badged on purpose: on a phone
   this padding is the one in force. */
@media (max-width: 767px) {
    .card--intro { padding: 24px 22px 26px; }
    .kbadge { flex-wrap: nowrap; }
}

/* -------------------------------------------------------------- dashboard

   The dealt page's own furniture (views/dashboard/index.php): the receipts
   beside the account card, and the bar of tabs over the sections. The
   account card is .card--head above; its tiles are .stats--kinds above; the
   section rows are the deal's. */

/* receipts — the last clicks on the things this account holds, judged. Two
   lines a row: which two things met, and what was decided about it. The
   thumbnail is the ad's mark; the thing that is theirs is the link.

   The list is the card's scroll region. The card is a flex column already
   (.card), so the list takes whatever height the head and the foot leave and
   scrolls past it — `min-height: 0` is what lets a flex child shrink below
   its content at all. Beside the account card (from 1024px, where the two
   share a row) the card's height is the account card's: `contain: size`
   makes its contents count for nothing when the row is measured, so the
   list can never grow the row, only scroll. Alone in a row, below 1024px,
   the card is as tall as it needs to be up to a cap, then scrolls the same
   way. A thin scrollbar in the page's own hairline, and a rule above the
   foot line so the region has an edge to scroll under. */
.receipts {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 10px;
    min-height: 0;
    margin: 0;
    padding: 0 6px 6px 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
    list-style: none;
    /* The last few pixels fade, so a list cut mid-row reads as a list that
       goes on rather than one that ends. On a Mac the scrollbar only shows
       while scrolling, so this is most of the cue. */
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
}
.receipts::-webkit-scrollbar { width: 6px; }
.receipts::-webkit-scrollbar-thumb { border-radius: 3px; background: var(--border); }
.receipts::-webkit-scrollbar-track { background: transparent; }
.card--receipts > .receipts + .small { padding-top: 10px; border-top: 1px solid var(--border); }
@media (min-width: 1024px) {
    /* Beside the account card only. Alone in a row this would collapse the
       card to its padding, which is why the cap below is the phone's and
       the tablet's rule instead. */
    .deal__top > .card--head + .card--receipts { contain: size; }
}
@media (max-width: 1023.98px) {
    .receipts { max-height: 300px; }
}
.receipt { display: flex; flex: none; align-items: flex-start; gap: 10px; min-width: 0; }
.receipt__thumb {
    display: block;
    flex: none;
    width: 28px;
    height: 28px;
    margin-top: 1px;
    border-radius: 7px;
    background: var(--pill);
    box-shadow: 0 1px 2px rgba(21, 20, 18, 0.12);
    object-fit: cover;
}
.receipt__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.receipt__what { overflow: hidden; font-size: 13px; line-height: 1.35; white-space: nowrap; text-overflow: ellipsis; }
.receipt__what .mono { font-size: 12.5px; }
.receipt__mine { color: var(--accent); font-weight: 650; }
.receipt__meta { line-height: 1.3; }
.card--receipts > .small:last-child { margin-top: auto; }

/* decks — the bar over the sections: a tab per section, and the switch
   between reading them one at a time and all at once (owner's ask,
   2026-09-03; layouts/base.php, `$tabs`). The tabs are links to their
   sections and work as such without script; the mode and the open section
   are attributes on <html> written before first paint, so which section
   shows is settled before anything is drawn, and there is no switch at all
   without script to honour it — the theme button's rule. The selected tab
   is drawn the way the dock draws the page you are on. */
.decks { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: -8px; }
.decks__tabs {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 4px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
}
.decks__tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 32px;
    padding: 0 12px;
    border-radius: 999px;
    color: var(--muted);
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    transition: background 150ms ease, color 150ms ease;
}
.decks__tab:hover { color: var(--text); text-decoration: none; }
.decks__tab svg { flex: none; }
.decks__count { font-weight: 500; }
:root[data-decks="tabs"][data-deck="products"] .decks__tab[data-deck="products"],
:root[data-decks="tabs"][data-deck="sites"] .decks__tab[data-deck="sites"],
:root[data-decks="tabs"][data-deck="boards"] .decks__tab[data-deck="boards"] {
    background: var(--ink);
    color: var(--on-ink);
}
/* The switch names what it will do, in two elements so the name a reader
   hears changes with the state and nothing is relabelled by script. */
.decks__switch { margin-left: auto; }
.decks__state { display: inline-flex; align-items: center; gap: 6px; }
:root:not([data-decks]) .decks__switch { display: none; }
:root[data-decks="tabs"] .decks__state--tabs { display: none; }
:root[data-decks="all"] .decks__state--all { display: none; }
/* One at a time: every section is off but the open one, named by id so the
   first frame is right; and the tab is the heading, so the section's own
   goes and its one line on the kind stays. */
:root[data-decks="tabs"] .deck { display: none; }
:root[data-decks="tabs"][data-deck="products"] #products,
:root[data-decks="tabs"][data-deck="sites"] #sites,
:root[data-decks="tabs"][data-deck="boards"] #boards { display: flex; }
:root[data-decks="tabs"] .deck__title { display: none; }

/* -------------------------------------------------------------- the doors

   /submit (views/submit/index.php): a card that is a link. The two door
   pages themselves are .card--form, .card--unit and the note, all above. */

/* door — a card that is a link (views/submit/index.php): the two things an
   account can add, one card each, and the whole card is the target rather
   than a button in its corner. The pill in the footer is a <span> and says
   the card is pressable; it is not a second control to aim at.

   Everything here is about undoing what `a` does to a block of text — the
   accent colour, the underline — and putting the card's own hover in its
   place: the 2px lift every card has, and the pill lighting up the way it
   would if it were the thing under the pointer. */
.card--door { color: var(--text); text-decoration: none; }
.card--door:hover { text-decoration: none; }
.card--door p { color: var(--muted); }
.card--door p strong { color: var(--text); }
.card--door .card__title { display: inline-flex; align-items: center; gap: 7px; }
.card__glyph { flex: none; color: var(--muted); }
.card--door .card__foot { justify-content: flex-start; }
@media (hover: hover) {
    .card--door:hover .pill--primary { opacity: 0.92; }
    .card--door:hover .pill--secondary { background: var(--border); }
}
/* The card is the link, so the ring belongs to the card and not to a pill
   inside it that nothing focuses. */
.card--door:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ----------------------------------------------- a site's own settings

   /dash/site/{slug}: the slot on the left, the settings beside it, the tags
   under both.

   **The row starts its cards at the top rather than stretching them.** A slot
   is an ad card and an ad card is short; the settings are a form and a form is
   tall. Stretched to a common height the preview was four fifths empty space,
   which read as a broken page rather than as a small ad.

   **And the settings run across rather than down.** This is the one page where
   seeing a control and its effect at the same time is the whole point, so
   three stacked blocks that pushed Save past the fold were the wrong shape:
   layout, colours and category sit side by side, and the card ends up about
   the height of the preview beside it. */

/* Both cards one height, whichever is taller (owner's ask): the row reads as
   one workbench rather than as two cards that happen to be adjacent. The
   preview's well takes the slack and centres the ad in it, so the extra space
   is around the ad rather than dumped underneath it. */
.deck--work .deck__grid { align-items: stretch; }
.deck--work .card--unit .unit {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    justify-content: center;
}

@media (min-width: 1024px) {
    /* The preview takes one column and the settings the other two: the form
       has three groups to show and the ad has one card. */
    .deck--work .deck__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
}

/* The settings' three groups, across on a desktop and down on a phone. */
.sets { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; margin-top: 14px; }
@media (min-width: 1024px) {
    .sets { grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr) minmax(0, 1fr); gap: 24px; }
}

.set { margin: 0; padding: 0; border: 0; min-width: 0; }
.set__legend {
    padding: 0 0 8px;
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.set__hint { margin: 0 0 10px; }

/* A group whose controls are not in force: the values are still shown, because
   "what would apply" is the useful thing to see, but they read as reference
   rather than as settings — and they are `disabled`, so Save cannot quietly
   turn them back on. */
.set--off .swatches,
.set--off .slider { opacity: 0.55; }
.set--off .swatch input[type=color],
.set--off .slider input[type=range] { cursor: default; }
.set--off .swatch,
.set--off .slider { cursor: default; }
.set--off .swatch__value { font-style: italic; }

/* One layout, chosen. The control comes first and the option beside it — a
   radio's label is the option, not a name for a box. */
.choice {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    margin: 0 0 8px;
    color: var(--text);
    font-size: 13px;
    cursor: pointer;
}
.choice input { flex: none; margin-top: 2px; accent-color: var(--accent); }
.choice__text { display: flex; flex-direction: column; line-height: 1.35; }
.choice__name { font-weight: 500; }
.choice__what { color: var(--muted); font-size: 12px; }

/* Four colours in two columns: a name, the well, and the value it holds. */
.swatches { display: grid; grid-template-columns: minmax(0, 1fr); gap: 7px; }
/* Name, well, value — a grid rather than a flex row so the three line up down
   the column instead of each row placing them wherever its name ends. */
.swatch {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 10px;
    margin: 0;
    color: var(--muted);
    font-size: 12px;
    cursor: pointer;
}
.swatch__name { flex: 1 1 auto; min-width: 0; color: var(--text); }
/* nowrap so "not set" stays on one line beside the slider, where the column
   is narrower than the four colour rows above it. */
.swatch__value { color: var(--muted); font-size: 11px; white-space: nowrap; }
.swatch input[type=color] {
    flex: none;
    width: 26px;
    height: 20px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: none;
    cursor: pointer;
}
.swatch input[type=color]::-webkit-color-swatch-wrapper { padding: 2px; }
.swatch input[type=color]::-webkit-color-swatch { border: 0; border-radius: 3px; }
.swatch input[type=color]::-moz-color-swatch { border: 0; border-radius: 3px; }

.slider {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 12px 0 0;
    color: var(--muted);
    font-size: 12px;
    cursor: pointer;
}
.slider__name { flex: none; color: var(--text); }
.slider input[type=range] { flex: 1 1 auto; min-width: 0; accent-color: var(--accent); cursor: pointer; }

/* The category select sits in the group rather than under a label of its own. */
.set select { margin-top: 0; }

/* The settings card's foot: the button and the one line that qualifies it. */
.card--settings .card__foot { align-items: center; gap: 12px; }
.card--settings .card__foot p { margin: 0; flex: 1 1 200px; }

/* The site key, inline in the header line. */
.keyline {
    padding: 1px 6px;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: var(--field);
    color: var(--text);
    font-size: 11px;
    user-select: all;
}

/* The tags. A publisher copies these by hand, so they wrap rather than run off
   the edge — a line somebody can only see half of is a line they paste half
   of. The cards are one height across the row so the three read as equals. */
/* One height across the row (owner's ask). This was briefly the other way —
   the snippets wrapped, so a common height meant a hole under the shorter
   intros — but a snippet that scrolls instead of wrapping is a predictable
   height, and three cards of one height read as three equal choices. */
.deck--install .deck__grid { align-items: stretch; }
/* The snippet sits under its own description, and the slack from the common
   height falls to the bottom of the card. Pushing the snippets down to align
   them instead put the hole in the middle, between a card's sentence and the
   thing that sentence is about. */
.card--tag { display: flex; flex-direction: column; }

/* ------------------------------------------------------- a board's listings

   /dash/board/{slug}: the board and its traffic in the top row, then the
   listings and the two ways to add one (DESIGN §6, *Your board*).

   The *list* card is a table in a card (DESIGN §5) and this is the first one.
   A rank is a position in an order, and an order with its numbers beside it
   reads as columns; every other shape ends up drawing the same table with
   more markup. On a narrow screen it scrolls sideways inside `.scroll` rather
   than reflowing into a stack of blocks — the choice a site's snippets make,
   for the same reason: the row is the unit of meaning here, and breaking one
   across three lines loses the comparison down the column that the page is
   for. The table carries a minimum width so the columns are never crushed
   into that scroll instead of using it. */

.listings { min-width: 580px; }
.listings th { white-space: nowrap; }
.listings td { vertical-align: middle; }
/* The last row of the last group closes the card; the rule under it would
   otherwise sit a card-gap above the footer's own. */
.listings tbody:last-of-type tr:last-child td { border-bottom: 0; }
/* The rank is the column the eye runs down, so it holds its width and its
   figures are the largest thing in the row. */
.listings__rank { width: 1%; padding-right: 14px; font-size: 15px; font-weight: 650; }
/* Every column but the listing is as wide as its content, so the words take
   the slack and the figures group together where they can be compared rather
   than being spread across a metre of card. */
.listings th:not(.listings__who), .listings td:not(.listings__who) { width: 1%; white-space: nowrap; }
.listings__who { width: 99%; white-space: normal; }
/* The break between what is on the board and what is off it: a row of the
   same table rather than a second table, because it is one list read in one
   order and the numbers still have to line up down the columns. Set like the
   card eyebrow it is doing the job of. */
.listings__split td {
    padding-top: 18px;
    border-bottom: 0;
    color: var(--muted);
    font-size: 11px;
    font-weight: 650;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.listings__off td { color: var(--muted); }
.listings .creative__mark { width: 34px; height: 34px; border-radius: 8px; }
.listings .creative__desc { margin-top: 2px; }

/* The controls on a row: a rank to type with the button that applies it, and
   the one that takes the listing off. A wrapping row rather than a cell full
   of forms, so on a narrow table the two land under each other instead of on
   top of each other. */
/* One line, and it stays one line: the column is as wide as its content
   (above), and a wrapping row's content width is its widest single item — so
   wrapping here made every row two rows tall and put Retire under Move. The
   table scrolls sideways when there is no room, which is the page's answer to
   a narrow screen everywhere else too. */
.rowacts { display: flex; flex-wrap: nowrap; align-items: center; justify-content: flex-end; gap: 6px; }
.rowact { display: flex; align-items: center; gap: 6px; }
/* Two classes deep, because the width and the top margin it is overriding
   come from `input[type=number]` and a single class would not reach them.
   Wide enough for the word "bottom", which is what the empty one means. */
.rowacts .rowact__rank {
    width: 6em;
    margin-top: 0;
    padding: 5px 8px;
    font-size: 13px;
    text-align: right;
}

/* ------------------------------------------------------------------ dock

   Fixed, bottom centre, --dock on --on-dock: the whole navigation, a <nav>
   of real links, in the shape of samebooks.club's tab bar — a floating pill
   with 6px padding and 4px gaps, each item a rounded pill around an 18px
   icon, the one you are on a light pill that shows its label, the rest
   their icon alone at 65% until hovered, and a deep soft shadow. Every item
   carries a title and its label for a screen reader. */

.dock {
    position: fixed;
    bottom: max(16px, env(safe-area-inset-bottom));
    left: 50%;
    z-index: 50;
    display: flex;
    align-items: center;
    gap: 4px;
    max-width: calc(100vw - 32px);
    margin: 0;
    padding: 6px;
    border-radius: 999px;
    background: var(--dock);
    color: var(--on-dock);
    box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.6);
    transform: translateX(-50%);
}
.dock__item {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border: 0;
    border-radius: 999px;
    background: none;
    color: inherit;
    opacity: 0.65;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: opacity 150ms ease, background 150ms ease;
}
.dock__item:hover { opacity: 1; text-decoration: none; }
.dock__item:focus-visible { opacity: 1; outline: 2px solid var(--on-dock); outline-offset: 2px; }
.dock__item[aria-current="page"] { opacity: 1; background: var(--on-dock); color: var(--dock); }
.dock__svg { display: block; flex: none; }

/* The tooltip: the item's name, in a small pill above it, on hover and on
   keyboard focus. Drawn from data-tip so it needs no script and no native
   title with its second-long delay; not on the item you are on, which shows
   its name already. It carries the dock's own --dock on --on-dock, so it
   reads as a piece of the bar and turns over with the theme like the rest
   of it. */
.dock__item { position: relative; }
.dock__item::after {
    content: attr(data-tip);
    position: absolute;
    bottom: calc(100% + 10px);
    left: 50%;
    padding: 6px 9px;
    border-radius: 8px;
    background: var(--dock);
    color: var(--on-dock);
    box-shadow: 0 8px 20px -8px rgba(0, 0, 0, 0.5);
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    opacity: 0;
    transform: translate(-50%, 4px);
    transition: opacity 120ms ease 60ms, transform 120ms ease 60ms;
    pointer-events: none;
}
.dock__item::before {
    content: "";
    position: absolute;
    bottom: calc(100% + 5px);
    left: 50%;
    border: 5px solid transparent;
    border-top-color: var(--dock);
    opacity: 0;
    transform: translate(-50%, 4px);
    transition: opacity 120ms ease 60ms, transform 120ms ease 60ms;
    pointer-events: none;
}
.dock__item:hover::after, .dock__item:hover::before,
.dock__item:focus-visible::after, .dock__item:focus-visible::before { opacity: 1; transform: translate(-50%, 0); }
/* A touch screen reports no hover and keeps :hover on whatever was tapped
   last, so on one the tooltip would stay open over the dock. The label is
   still on the item for a reader either way. */
@media (hover: none) {
    .dock__item::after, .dock__item::before { content: none; }
}
.dock__item[aria-current="page"]::after, .dock__item[aria-current="page"]::before { display: none; }
/* The label shows on the item you are on; the others keep theirs for a
   screen reader and a title for the pointer. */
.dock__item:not([aria-current="page"]) .dock__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* The theme control shows one of three icons, by the state the pre-paint
   script recorded. No script means no data-choice, and no data-choice means
   no button: a control that cannot work is not rendered. */
.theme-ico { display: none; }
:root[data-choice="light"]  .theme-ico--light,
:root[data-choice="dark"]   .theme-ico--dark,
:root[data-choice="system"] .theme-ico--system { display: block; }
:root:not([data-choice]) .dock__theme { display: none; }

/* And it is not one of the destinations, which is the thing the shape has to
   say before it is pressed: everything to its left goes somewhere, it acts
   here. Two marks, no copy and no divider — a rule drawn across the bar was
   tried and read as more furniture than the difference needed. The button is
   a round well rather than a pill, so it is visibly a knob and not a tab. And
   it never dims — the other items sit at 65% until you are on them or over
   them, because a destination you are not at is quiet; a control is always
   live, and the one bright thing on a bar of quiet ones is the one you press.

   The well deepens on hover and on focus, and the press takes it down 4% —
   feedback for an action whose whole result is somewhere else on the page. */
.dock__theme {
    padding: 10px;
    background: var(--dock-well);
    opacity: 1;
    transition: background 150ms ease, transform 120ms ease;
}
.dock__theme:hover,
.dock__theme:focus-visible { background: var(--dock-well-on); }
.dock__theme:active { transform: scale(0.94); }

/* On a phone the dock tightens and the current item loses its label; the
   canvas keeps less clear for it. */
@media (max-width: 767px) {
    :root { --dock-h: 60px; }
    /* max(), not 12px flat: this rule was overriding the safe-area
       inset on exactly the devices that have one, which put the dock
       under the home indicator. */
    .dock { bottom: max(12px, env(safe-area-inset-bottom)); gap: 2px; padding: 5px; }
    .dock__item { padding: 10px 9px; }
    .dock__item[aria-current="page"] .dock__label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }
}

/* ----------------------------------------------------------- desk switch

   The homepage's own control, and only its (layouts/base.php passes
   $deskToggle): the desk, or one tidy column. It exists for a phone and a
   tablet — a desktop has the room for the desk and is not offered the
   choice — and it exists only where there is script to honour it, the same
   rule the theme button follows: no data-desk, no button.

   It carries the dock's colours, so the two read as one bar of controls
   even at opposite corners, and it names what it will do rather than where
   you are: on the desk it offers the list, on the list it offers the desk.
   Each state is its own element, so the name a screen reader reads changes
   with the state and no script has to relabel it. */

.deskswitch {
    position: fixed;
    top: max(14px, env(safe-area-inset-top));
    right: max(14px, env(safe-area-inset-right));
    z-index: 50;
    display: inline-flex;
    align-items: center;
    min-height: 38px;
    padding: 0 14px;
    border: 0;
    border-radius: 999px;
    background: var(--dock);
    color: var(--on-dock);
    box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.6);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
}
.deskswitch__state { display: inline-flex; align-items: center; gap: 7px; }
.deskswitch__svg { display: block; flex: none; }
:root:not([data-desk]) .deskswitch { display: none; }
:root[data-desk="desk"] .deskswitch__state--desk { display: none; }
:root[data-desk="stack"] .deskswitch__state--stack { display: none; }
@media (min-width: 1024px) {
    .deskswitch { display: none; }
}

/* ----------------------------------------------------------------- flash */

.flash {
    max-width: 1560px;
    margin: 20px auto 0;
    padding: 10px 16px;
    border: 1px solid var(--lit);
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    font-size: 14px;
}

/* ---------------------------------------------------------- reduced motion

   No drag, no rotation, no hover lift (DESIGN §3). The offsets stay: they are
   composition, not movement. */

@media (prefers-reduced-motion: reduce) {
    .kbadge__dot, .card--strip, .strip__pulse::after { animation: none; }
    .card, .pill, .dock__item, .dock__item::after, .dock__item::before, .decks__tab { transition: none; }
    .dock__theme:active { transform: none; }
    .card:hover, .pill:hover { transform: none; }
    .canvas > .card { --rot: 0deg !important; --prot: 0deg !important; }
}
@media (prefers-reduced-motion: reduce) and (max-width: 767px) {
    .canvas > .card, .canvas > .card:hover {
        transform: translate(var(--dx, 0px), var(--dy, 0px));
    }
}
@media (prefers-reduced-motion: reduce) and (min-width: 768px) {
    .canvas > .card, .canvas > .card:hover {
        transform: translate(-50%, -50%) translate(var(--dx, 0px), var(--dy, 0px));
    }
}
