:root {
    color-scheme: light dark;
    --bg: #fbfbfa;
    --fg: #16150f;
    --muted: #6b6a60;
    --rule: #dedcd3;
    --accent: #b4531f;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #12110e;
        --fg: #f2f0e8;
        --muted: #918d80;
        --rule: #2b2924;
        --accent: #e2854a;
    }
}

/* ---------- the screen face ----------------------------------------------
   Everything with a phosphor glow on it is set in Sihilisko CRT, the bitmap
   face built by tools/make-crt-font.py. Two restyles of the private areas kept
   coming back as "still too clear", and the reason was never the colour: an
   outline font antialiased for a modern panel cannot read as a character
   generator ROM, because what gives a ROM font away is that its curves are
   staircases. This one has no curves at all.

   Unlike the colour tokens further down, a font name is safe to put on :root -
   a family only takes effect where something asks for it, so there is nothing
   here to leak into the light public palette the way a stray --green would.

   The fallback stack matters more than usual: if the woff2 fails the pages
   must still be monospaced, because the login caret is a mirror element whose
   width has to track the input's text exactly.

   swap, not block. The face is 2.4 KB and will normally be there before first
   paint, but a private page that is readable in the wrong font beats a private
   page that is blank. */
@font-face {
    font-family: "Sihilisko CRT";
    src: url("/fonts/sihilisko-crt.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

:root {
    --crt-face: "Sihilisko CRT", ui-monospace, "DejaVu Sans Mono",
                "Liberation Mono", Menlo, Consolas, monospace;
}

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

html, body {
    height: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
}

a {
    color: var(--accent);
}

/* Off-screen but not out of the accessibility tree, which display:none and
   visibility:hidden both are. Used for text that the design carries as
   artwork — the landing page's <h1> — so screen readers and crawlers get it
   while sighted visitors see the artwork alone. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ---------- layout ---------- */

.page {
    min-height: 100%;
    display: flex;
    flex-direction: column;
}

main {
    flex: 1;
    display: grid;
    place-items: center;
    padding: 2rem 1.5rem;
}

/* Every page is a grid item of that centring grid, and a grid item's default
   min-width is auto — meaning it can never be laid out narrower than its own
   min-content width, whatever `width: 100%` says. One unbreakable thing
   anywhere inside therefore widens the entire page and pushes it off the
   right of a phone: the heading, the summary and every panel go with it,
   because they are all just filling a section that is now too wide.

   That is exactly what the coming-soon enquiry button did. `.btn` is
   `white-space: nowrap` and that button's label carries the product name, so
   "Enquire about MIB3 Discovery Pro" was 348px of text with no wrap
   opportunity in it — a floor no phone viewport could satisfy.

   min-width: 0 lets the item shrink to its track. What is inside then has to
   be able to reflow into that width, which is what the two rules below are
   for: anything whose text is user data gets overflow-wrap: anywhere, and
   `anywhere` rather than `break-word` because only `anywhere` also lowers the
   min-content width — `break-word` breaks the line but leaves the floor
   exactly where it was, which would fix nothing here. */
main > * { min-width: 0; }

.site-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.65rem 1rem;
    padding: 1.5rem;
    text-align: center;
    font-size: 0.8125rem;
    color: var(--muted);
    border-top: 1px solid var(--rule);
}

.site-footer a {
    color: inherit;
    text-decoration: none;
}

.site-footer a:hover {
    color: var(--accent);
    text-decoration: underline;
}

.footer-group,
.footer-links { display: inline-flex; align-items: center; gap: 0.65rem; }
.footer-links a + a::before { content: "·"; margin-right: 0.65rem; color: currentColor; }

/* ---------- hero ---------- */

.hero {
    width: 100%;
    max-width: 34rem;
    text-align: center;
}

.mark {
    font-size: clamp(2.75rem, 12vw, 4.5rem);
    font-weight: 700;
    letter-spacing: -0.045em;
    line-height: 1;
    margin: 0;
}

.mark span {
    color: var(--accent);
}

.tagline {
    margin: 1.25rem 0 0;
    font-size: clamp(1.05rem, 3.2vw, 1.3rem);
    line-height: 1.5;
    font-weight: 400;
}

.hero hr {
    border: 0;
    border-top: 1px solid var(--rule);
    margin: 2.25rem auto;
    width: 4rem;
}

.note {
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--muted);
}

.badge {
    display: inline-block;
    margin-top: 1.75rem;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
}

/* ---------- blazor error ui ---------- */

#blazor-error-ui {
    color-scheme: light only;
    background: #ffe9c9;
    color: #16150f;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* ---------- private area (/me) ---------- */

.private {
    width: 100%;
    max-width: 52rem;
    padding: 1rem 0 2rem;
}

.private-head {
    text-align: left;
    margin-bottom: 2.5rem;
}

.private-tag {
    display: inline-block;
    padding: 0.25rem 0.6rem;
    border: 1px solid var(--accent);
    border-radius: 999px;
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
}

.private-head h1 {
    margin: 1rem 0 0.5rem;
    font-size: clamp(1.75rem, 6vw, 2.5rem);
    letter-spacing: -0.03em;
    line-height: 1.1;
}

.private-sub {
    margin: 0;
    color: var(--muted);
    font-size: 0.9375rem;
    line-height: 1.6;
}

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
}

.card {
    border: 1px solid var(--rule);
    border-radius: 0.6rem;
    padding: 1.25rem;
    background: color-mix(in srgb, var(--fg) 3%, transparent);
}

.card h2 {
    margin: 0 0 0.5rem;
    font-size: 1rem;
    letter-spacing: -0.01em;
}

.card p {
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--muted);
}

.card code {
    font-size: 0.8125em;
    padding: 0.1em 0.35em;
    border-radius: 0.25rem;
    background: color-mix(in srgb, var(--fg) 8%, transparent);
}

.private-foot {
    margin-top: 2.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--rule);
    font-size: 0.875rem;
}

.private-foot a {
    color: var(--muted);
    text-decoration: none;
}

.private-foot a:hover {
    color: var(--accent);
}

/* ---------- admin + recipient UI ---------- */

.you {
    width: 100%;
    max-width: 46rem;
    padding: 1rem 0 2rem;
}

.crumb {
    font-size: 0.8125rem;
    color: var(--muted);
    text-decoration: none;
}

.crumb:hover { color: var(--accent); }

.panel {
    border: 1px solid var(--rule);
    border-radius: 0.6rem;
    padding: 1.25rem 1.35rem 1.4rem;
    margin-bottom: 1.25rem;
    background: color-mix(in srgb, var(--fg) 3%, transparent);
}

.panel h2 {
    margin: 0 0 0.85rem;
    font-size: 1rem;
    letter-spacing: -0.01em;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.count {
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--muted);
    border: 1px solid var(--rule);
    border-radius: 999px;
    padding: 0.05rem 0.45rem;
}

.hint {
    margin: 0 0 0.85rem;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--muted);
}

.empty {
    margin: 0;
    font-size: 0.875rem;
    color: var(--muted);
    font-style: italic;
}

.opt {
    font-weight: 400;
    font-size: 0.75rem;
    color: var(--muted);
}

/* forms */

.stack { display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }
.stack .row { display: flex; flex-wrap: wrap; gap: 0.75rem; width: 100%; }
.stack .row label { flex: 1 1 12rem; }

label {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    font-size: 0.8125rem;
    font-weight: 500;
    width: 100%;
}

input[type="text"], input[type="email"], input[type="password"], input:not([type]), textarea {
    font: inherit;
    font-size: 0.9375rem;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--rule);
    border-radius: 0.4rem;
    background: var(--bg);
    color: var(--fg);
    width: 100%;
}

input[type="file"] { font-size: 0.875rem; color: var(--muted); }

textarea { resize: vertical; line-height: 1.6; font-family: inherit; }

input:focus-visible, textarea:focus-visible, .btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

.upload { flex-direction: row; align-items: center; flex-wrap: wrap; }

.btn {
    display: inline-block;
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 500;
    padding: 0.45rem 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 0.4rem;
    background: transparent;
    color: var(--fg);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
}

/* A button label that carries user data cannot refuse to wrap. Every other
   button on the site is a fixed verb — Save, Manage, Resend, Details — and
   keeps the nowrap above, which stops "Mark paid" breaking across two lines.

   Must stay *below* .btn: both are one class, so source order is the only
   thing that decides, and the first version of this sat above and did
   nothing at all. */
.btn--wrap {
    white-space: normal;
    /* Hyphenate before breaking: this label carries a product name, and
       "Enquire about MIB3 Discovery Pr/o" reads as a fault. */
    hyphens: auto;
    -webkit-hyphens: auto;
    overflow-wrap: anywhere;
}

.btn:hover { border-color: var(--accent); color: var(--accent); }

.btn.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

.btn.primary:hover { filter: brightness(1.08); color: #fff; }

.btn.danger { color: #c0392b; border-color: color-mix(in srgb, #c0392b 35%, var(--rule)); }
.btn.danger:hover { background: #c0392b; border-color: #c0392b; color: #fff; }

@media (prefers-color-scheme: dark) {
    .btn.danger { color: #ff8a75; }
    .btn.danger:hover { background: #c0392b; color: #fff; }
}

/* lists */

.user-list, .file-list { list-style: none; margin: 0; padding: 0; }

.user-list li, .file-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.7rem 0;
    border-top: 1px solid var(--rule);
}

.user-list li:first-child, .file-list li:first-child { border-top: 0; }

.user-main, .file-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }

.user-name, .file-main a {
    font-weight: 600;
    font-size: 0.9375rem;
    color: var(--fg);
    text-decoration: none;
    word-break: break-word;
}

.user-name:hover, .file-main a:hover { color: var(--accent); }

.user-display { font-size: 0.8125rem; color: var(--fg); }
.user-meta, .file-meta { font-size: 0.75rem; color: var(--muted); }

.user-actions { display: flex; gap: 0.5rem; align-items: center; }

.danger-zone { border-color: color-mix(in srgb, #c0392b 25%, var(--rule)); }

/* flash messages */

.flash {
    margin: 0 0 1.25rem;
    padding: 0.6rem 0.85rem;
    border-radius: 0.4rem;
    font-size: 0.875rem;
    border: 1px solid var(--rule);
}

.flash.ok  { border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); }
.flash.err { border-color: color-mix(in srgb, #c0392b 45%, var(--rule)); color: #c0392b; }

@media (prefers-color-scheme: dark) { .flash.err { color: #ff8a75; } }

/* sign-in */

.signin { max-width: 21rem; margin: 0 auto; text-align: center; }
.signin .mark { font-size: clamp(2.25rem, 9vw, 3rem); }
.signin-sub { margin: 0.75rem 0 1.5rem; color: var(--muted); font-size: 0.9375rem; }
.signin .stack { text-align: left; }
.signin .btn { width: 100%; text-align: center; margin-top: 0.25rem; }
.signin-note { margin: 1.5rem 0 0; font-size: 0.75rem; color: var(--muted); line-height: 1.5; }

.you-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 2rem;
}

.you-head h1 {
    margin: 0.75rem 0 0;
    font-size: clamp(1.5rem, 5vw, 2rem);
    letter-spacing: -0.03em;
}

/* preserve the owner's line breaks without allowing markup */
.message {
    margin: 0;
    white-space: pre-wrap;
    line-height: 1.7;
    font-size: 0.9375rem;
}

/* ---------- landing page: logo only ---------- */

/* The artwork carries its own black background, so the whole viewport is
   forced black in both colour schemes. Scoped to the landing page so /you
   and /me keep the normal light/dark theme.

   .bare paints the background itself rather than relying only on :has(),
   so the page is still correct in browsers without :has() support. The
   :has() rule additionally covers overscroll and the browser UI colour. */
.bare {
    min-height: 100dvh;
    background: #000;
    color: #7ee787;
    color-scheme: dark;
    display: grid;
    place-items: center;
    padding: 1.5rem;
}

body:has(.bare) {
    background: #000;
    color-scheme: dark;
}

.logo-stage {
    display: block;
    /* The stage a glitch band hangs in — see crt-glitch.js. The gecko's eye
       and tongue are placed against the <picture> inside, not against this. */
    position: relative;
    width: 100%;
    /* Wider than the logo itself so the strapline is not forced to overflow
       this box — anything overflowing would fall outside the scanline
       overlay and render unstriped. */
    max-width: min(92vw, 700px);
}

.logo-stage picture {
    /* position:relative so the eyelid can be placed over the pupil in
       percentages of the image box, which keeps it aligned at any size. */
    position: relative;
    display: block;
    width: 100%;
    max-width: min(78vmin, 620px);
    margin-inline: auto;

    /* The gecko itself powers on with the same top-down curtain as the CRT
       menu panel further down this file — a picture-tube reveal rather than a
       plain fade, and the first thing the visitor sees. The strapline's
       CrtType is given a matching StartDelay (Home.razor) — this 400ms plus a
       200ms beat of silence — so it starts typing after a pause once this has
       finished, rather than racing it or following immediately. */
    animation-name: logo-reveal;
    animation-duration: 400ms;
    animation-timing-function: linear;
    animation-fill-mode: both;
}

@keyframes logo-reveal {
    0% {
        clip-path: inset(0% 0% 100% 0%);
        filter: brightness(1.7);
    }
    70% {
        filter: brightness(1.2);
    }
    100% {
        clip-path: inset(0% 0% 0% 0%);
        filter: brightness(1);
    }
}

/* The gecko blinks. The pupil is a dark disc at 61.9%/18.2% of the artwork,
   3.9% across; covering it with the surrounding lid reads as an eyelid
   closing. Measured from the source PNG rather than eyeballed, so it stays
   aligned however the logo is scaled. */
.gecko-eye {
    position: absolute;
    left: 61.5%;
    top: 17.8%;
    width: 4.5%;
    height: 4.5%;
    border-radius: 50%;
    pointer-events: none;
    opacity: 0;

    /* The lid is made of the same coarse scanlines as the artwork, not a
       solid fill. Measured from the source: the scan pitch is 9.3px on a
       1254px image, so across this 4.5% element one stripe pair is 16.5% of
       its height. The lit part is kept below the artwork's 56% duty so the
       lid stays sparse rather than reading as a green blob, and the gaps are
       transparent so the dark pupil shows through between the lines.
       Percentages rather than px, so it scales with the logo. */
    background: repeating-linear-gradient(
        to bottom,
        #1bcd06 0 6.2%,
        rgba(0, 0, 0, 0) 6.2% 16.5%
    );

    /* Choreographed load: the logo reveals for 400ms, then a 200ms beat, then
       the strapline types for 2s, the hamburger assembles from 2.75s to
       3.37s, then the eye blinks, then the tongue flicks.

       A negative delay is what places the first blink. The animation runs on a
       10s cycle with the blink at the very end, so starting it 6.4s in puts the
       first blink at 3.6s and every one after that 10s apart — no separate
       intro animation, and no chance of the two drifting out of step. */
    animation-name: gecko-blink;
    animation-duration: 10s;
    animation-timing-function: step-end;
    animation-iteration-count: infinite;
    animation-delay: -6.4s;
}

/* A blink is ~150ms out of every 10s — 1.5% of the cycle. step-end keeps it
   a hard closed/open with no fade. */
@keyframes gecko-blink {
    0%    { opacity: 0; }
    98.5% { opacity: 1; }
    100%  { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .gecko-eye { animation: none; opacity: 0; }
}

/* The gecko flicks a forked tongue. The snout tip is at 68.6%/16.3% of the
   artwork and the eye centre at 63.8%/19.9%, so the head points up and right
   at roughly 37 degrees — measured from the master image, not guessed, so it
   stays on the snout at any size.

   The box is deliberately NOT rotated. Rotating it would rotate the scanlines
   with it, and the artwork's scanlines are horizontal; the diagonal is in the
   clip-path instead, so the stripes stay in phase with everything around them.

   It grows from its base rather than sliding, so transform-origin is the
   bottom-left corner — the point where the tongue leaves the mouth. */
.gecko-tongue {
    position: absolute;
    left: 68.3%;
    top: 9.8%;
    width: 8.4%;
    height: 6.8%;
    pointer-events: none;
    transform-origin: 0% 100%;
    transform: scale(0);
    opacity: 0;

    /* Solid, unlike the eyelid. The eyelid is striped because it sits on the
       striped head and has to disappear into it; the tongue is against black,
       and at this size the artwork's stripe pitch chopped a thin diagonal into
       disconnected rectangles instead of reading as one shape. The flick is
       50ms out — texture would never register anyway. */
    background: #1bcd06;
    filter: drop-shadow(0 0 0.35em rgba(47, 220, 12, 0.7));

    /* A thin strip leaving the mouth, splitting into two prongs — a snake's
       tongue. Traced base edge, up to the first prong, back to the notch, out
       to the second prong, then back down the other edge. */
    clip-path: polygon(
        0% 90%,
        46% 46%,
        80% 2%,
        71% 26%,
        58% 45%,
        75% 40%,
        100% 34%,
        62% 58%,
        10% 100%
    );

    /* Same trick: a 15s cycle started 11s in, so the first flick lands at 4.0s
       — just after the blink — and every one after that is 15s apart. */
    animation-name: gecko-tongue;
    animation-duration: 15s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    animation-delay: -11s;
}

/* Out and back in 100ms of every 15s — 0.667% of the cycle, so 50ms out and
   50ms back. */
@keyframes gecko-tongue {
    0%       { transform: scale(0); opacity: 0; }
    99.333%  { transform: scale(0); opacity: 0; }
    99.667%  { transform: scale(1); opacity: 1; }
    100%     { transform: scale(0); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .gecko-tongue { animation: none; opacity: 0; }
}

.logo-stage img {
    display: block;
    width: 100%;
    height: auto;
}

/* ---------- CRT terminal strapline ---------- */

.strapline {
    margin: 1.5rem 0 0;
    text-align: center;
}

/* Set in capitals, and only here - .crt-line is also the /you heading, which
   stays as it is written.
   
   The text in Home.razor is lower case and stays that way: the visually-hidden
   h1 and the aria-label on the block are built from the same string, and
   shouting at a screen reader is not the effect wanted. This is presentation,
   so it belongs in CSS.
   
   Safe to do without touching the sizes: the face is monospaced, so a capital
   and a lower-case letter occupy exactly the same cell and the line cannot
   reflow. What it does buy is height - lower case is five rows of the grid
   against seven for capitals, which is a far bigger gap than the outline face
   this replaced, and at strapline size the difference is the whole effect. */
.strapline .crt-line {
    text-transform: uppercase;
}

.crt-block {
    --amber: #ffb000;
    display: block;
}

/* One row per line, shrink-to-fit so the scanline overlay tracks the text
   exactly rather than the container. */
.crt-line {
    position: relative;
    display: block;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;

    font-family: var(--crt-face);
    font-size: clamp(1rem, 5.9vw, 1.75rem);
    letter-spacing: 0.02em;
    /* 1.65 was set for an outline face that filled its em box. The bitmap face
       carries its own leading inside the cell - capitals occupy rows 2 to 8 of
       twelve, so there is already a sixth of an em above and a quarter below
       before any line-height is applied. Stacking 0.65em of extra leading on
       top of that pushed the two strapline rows more than a full em apart and
       they stopped reading as one sentence. 1.3 leaves about 0.72em between
       cap-bottom and cap-top, which still breathes.

       Safe to change: the block cursor hangs off .crt-char, which sets
       line-height 1 of its own precisely so it is measured against the em box
       and not against this. */
    line-height: 1.3;
    white-space: pre;
    color: var(--amber);
    text-shadow: 0 0 0.3em rgba(255, 176, 0, 0.5), 0 0 1em rgba(255, 140, 0, 0.32);
}

/* Scanlines over the whole row, so glyphs and cursor are striped alike. */
.crt-line::after {
    content: "";
    position: absolute;
    inset: -0.1em -0.1em;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 1px,
        rgba(0, 0, 0, 0.62) 1px 2px
    );
}

/* Each character carries its own reveal time, and optionally its own place
   on a curve (--dy/--rot from C#, both 0 by default so the line is flat).

   line-height: 1 makes the span exactly one em tall, so the cursor below can
   be positioned against the em box rather than against a line box whose
   height depends on the font's ascent and the row's leading. Getting that
   wrong is what left the cursor riding high. */
.crt-char {
    display: inline-block;
    position: relative;
    line-height: 1;
    transform: translateY(var(--dy)) rotate(var(--rot));
    opacity: 0;
    animation: crt-char-on 1ms linear var(--d) forwards;
}

/* The cursor belongs to whichever character was typed last, so it travels
   along the line as the text appears. step-end holds it lit for exactly its
   window, then drops it; before --d the animation has not begun, so the base
   opacity of 0 applies. */
.crt-char::after {
    content: "";
    position: absolute;
    left: 100%;
    top: 0;
    width: 0.58em;
    height: 1em;
    background: var(--amber);
    opacity: 0;
    /* linear, not step-end: with step-end + fill-forwards Safari retains the
       0% value after the animation ends, leaving every character's cursor lit
       and covering the next letter. The keyframes hold the lit value until the
       last instant instead, which behaves the same everywhere.

       The second animation is a backstop: once the whole line has finished
       writing it forces the cursor off, so even if the first animation is
       mishandled nothing can sit over a letter. Later animation wins on the
       same property, and .is-final overrides both. */
    animation:
        crt-cursor-window var(--win) linear var(--d) forwards,
        crt-cursor-clear 1ms linear var(--total) forwards;
}

/* The last character keeps its cursor and blinks for good. */
.crt-char.is-final::after {
    animation: crt-cursor-blink 1.05s step-end var(--d) infinite;
}

@keyframes crt-char-on { to { opacity: 1; } }

@keyframes crt-cursor-clear {
    to { opacity: 0; }
}

@keyframes crt-cursor-window {
    0%      { opacity: 1; }
    99.99%  { opacity: 1; }
    100%    { opacity: 0; }
}

@keyframes crt-cursor-blink {
    0%, 49%   { opacity: 1; }
    50%, 100% { opacity: 0; }
}

/* Respect a stated preference: show the finished lines, keep only the
   final cursor blinking. */
@media (prefers-reduced-motion: reduce) {
    .crt-char { opacity: 1; animation: none; }
    .crt-char::after { animation: none; opacity: 0; }
    .crt-char.is-final::after { opacity: 1; animation: crt-cursor-blink 1.05s step-end infinite; }
}


/* ---------- CRT login (/you, signed out) ---------- */

/* Black screen for the sign-in view only; the signed-in view keeps the
   normal theme. Painted on the panel itself as well as the body so it is
   still correct without :has() support. */
body:has(.crt-login) {
    background: #000;
    color-scheme: dark;
}

body:has(.crt-login) .site-footer {
    display: none;
}

.crt-login {
    /* Green terminal chrome; amber is reserved for the heading and for what
       the visitor actually types, so entry stands out against the frame.
       Greens sampled from the logo artwork. */
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;

    /* The input, the hidden mirror that measures it and the block caret all
       have to agree to the pixel or the caret drifts along the line. They used
       to carry the same three literals in three rules, which is a standing
       invitation to change one and miss the others - and switching to the
       bitmap face meant changing all three at once. Tokens instead. */
    --crt-input-size: 1.2rem;
    --crt-input-tracking: 0.03em;
    --crt-input-leading: 1.25;

    position: relative;
    max-width: 28rem;
    margin: 2rem auto;
    padding: 1.9rem 1.6rem 2rem;
    background: #000;
    border: 1px solid var(--green-dim);
    border-radius: 0.35rem;
    box-shadow: 0 0 2.5rem rgba(47, 220, 12, 0.12), inset 0 0 3rem rgba(47, 220, 12, 0.05);
    font-family: var(--crt-face);
    color: var(--green);
    text-align: left;
}

/* Scanlines across the whole panel, matching the rest of the site. */
.crt-login::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 1px,
        rgba(0, 0, 0, 0.5) 1px 2px
    );
}

.crt-heading {
    margin: 0 0 1.6rem;
    font-size: 1rem;      /* CrtType sizes itself; this only sets the box */
    font-weight: 400;
    text-align: center;
}

.crt-form {
    display: flex;
    flex-direction: column;
    gap: 1.05rem;
}

.crt-form label {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    font-size: 1rem;
    font-weight: 400;
    letter-spacing: 0.02em;
}

.crt-prompt {
    color: var(--green);
    text-shadow: 0 0 0.35em rgba(47, 220, 12, 0.45);
    text-transform: lowercase;
}

/* Inputs are a bare underline with an amber block caret, not a boxed field. */
.crt-login input {
    font: inherit;
    font-size: var(--crt-input-size);
    letter-spacing: var(--crt-input-tracking);
    line-height: var(--crt-input-leading);
    width: 100%;
    padding: 0.3rem 0 0.35rem;
    border: 0;
    border-bottom: 1px solid var(--green-dim);
    border-radius: 0;
    background: transparent;
    color: var(--green);
    caret-color: var(--green);
    /* Block caret where the browser supports it; ignored elsewhere, which is
       why the decorative block below covers the empty state. */
    caret-shape: block;
    text-shadow: 0 0 0.35em rgba(47, 220, 12, 0.5);
}

.crt-login input:focus {
    outline: none;
    border-bottom-color: var(--green);
    background: rgba(47, 220, 12, 0.06);
}

.crt-login input:-webkit-autofill {
    -webkit-text-fill-color: var(--green);
    -webkit-box-shadow: 0 0 0 40rem #000 inset;
}

/* Hide the placeholder itself — it exists only so :placeholder-shown can
   tell us the field is empty. */
.crt-login input::placeholder {
    color: transparent;
}

/* A solid block cursor instead of the browser's thin line.

   Without JavaScript this shows only while the field is focused and empty
   (caret-shape: block covers the rest where supported). With crt-caret.js
   the block is positioned at the end of the text and stays a block the whole
   time, and the native caret is hidden entirely. */
.crt-field {
    position: relative;
}

.crt-inputwrap {
    position: relative;
    display: block;
}

/* Invisible copy of the value, used only to measure its rendered width.
   Must match the input's text metrics exactly or the caret drifts. */
.crt-mirror {
    position: absolute;
    left: 0;
    top: 0.3rem;
    visibility: hidden;
    white-space: pre;
    pointer-events: none;
    font: inherit;
    font-size: var(--crt-input-size);
    letter-spacing: var(--crt-input-tracking);
    line-height: var(--crt-input-leading);
}

.crt-caret {
    position: absolute;
    left: 0;
    top: 0.3rem;
    width: calc(0.5em + var(--crt-input-tracking));   /* exactly one cell of the bitmap face */
    height: calc(var(--crt-input-leading) * 1em);     /* the full line box, so it matches the glyphs */
    background: var(--green);
    box-shadow: 0 0 0.5em rgba(47, 220, 12, 0.45);
    pointer-events: none;
    opacity: 0;
}

/* No JS: block only while focused and empty. */
.crt-login input:focus:placeholder-shown ~ .crt-caret {
    animation: crt-cursor-blink 1.05s step-end infinite;
}

/* First field is autofocused, so hold its cursor until the heading finishes.
   Same selector plus a class so the shorthand above cannot reset the delay. */
.crt-login .crt-field--first input:focus:placeholder-shown ~ .crt-caret {
    animation-delay: 1.2s;
}

/* With JS the block follows the text, so it blinks whenever the field has
   focus rather than only when empty. */
.crt-field--js input:focus ~ .crt-caret {
    animation: crt-cursor-blink 1.05s step-end infinite;
}

.crt-login .crt-field--js.crt-field--first input:focus:placeholder-shown ~ .crt-caret {
    animation-delay: 1.2s;
}

/* Hide the native caret: while empty always, and everywhere once JS has
   taken over and can draw the block at the right place. */
.crt-login input:placeholder-shown,
.crt-field--js input {
    caret-color: transparent;
}

@media (prefers-reduced-motion: reduce) {
    .crt-login input:focus:placeholder-shown ~ .crt-caret { animation: none; opacity: 1; }
}

.crt-btn {
    font: inherit;
    font-size: 0.9rem;
    letter-spacing: 0.14em;
    margin-top: 0.5rem;
    padding: 0.5rem 0.4rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    background: transparent;
    color: var(--green);
    cursor: pointer;
    text-shadow: 0 0 0.4em rgba(47, 220, 12, 0.5);
}

.crt-btn:hover,
.crt-btn:focus-visible {
    background: var(--green);
    border-color: var(--green);
    color: #000;
    text-shadow: none;
    outline: none;
}

/* Flash messages inside the terminal, in the same palette. */
.crt-login .flash {
    border-color: var(--green-dim);
    color: var(--green);
    background: rgba(47, 220, 12, 0.07);
    font-size: 0.8125rem;
    letter-spacing: 0.03em;
}

.crt-login .flash.err {
    border-color: #c0392b;
    color: #ff8a75;
    background: rgba(192, 57, 43, 0.12);
}

/* ---------- CRT hamburger menu (landing page) ---------- */

.crt-nav {
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;

    /* Fixed, so it stays in the corner while the logo stays centred, and so
       it does not move if the page ever scrolls on a short viewport. The
       safe-area inset keeps it clear of the notch on an iPhone. */
    position: fixed;
    top: max(0.85rem, env(safe-area-inset-top));
    left: max(0.85rem, env(safe-area-inset-left));
    z-index: 20;
    font-family: var(--crt-face);
}

/* On a wide window the viewport corner is a long way from the logo, and the
   icon reads as marooned. Past the point where the logo stops growing, anchor
   it to the centred content column instead: the logo's own half-width is
   min(78vmin, 620px) / 2, and the icon sits a comfortable margin outside that
   edge. max() keeps the viewport gutter as a floor, so it can never be pushed
   off-screen on an awkward window size, and the phone layout is untouched. */
@media (min-width: 60rem) {
    .crt-nav {
        --logo-half: calc(min(78vmin, 620px) / 2);
        left: max(1.5rem, calc(50% - var(--logo-half) - 3.5rem));
        top: max(2rem, env(safe-area-inset-top));
    }
}

/* ---- the hamburger itself ---- */

.crt-burger {
    /* 44px is the smallest comfortable touch target; the icon inside is
       smaller, so the button reads as light while staying easy to hit. */
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    cursor: pointer;
    border-radius: 0.3rem;
    /* Remove the disclosure triangle. Both are needed: list-style for
       standards-based engines, the pseudo-element for older WebKit. */
    list-style: none;
    -webkit-tap-highlight-color: transparent;
}

.crt-burger::-webkit-details-marker {
    display: none;
}

.crt-burger:focus-visible {
    outline: 1px solid var(--green);
    outline-offset: 2px;
}

/* Three lines: the element is the middle one, its pseudo-elements the outer
   two. Sharp, unrounded and evenly spaced — a terminal glyph, not an app
   icon. */
.crt-burger-lines,
.crt-burger-lines::before,
.crt-burger-lines::after {
    display: block;
    width: 1.375rem;
    height: 2px;
    background: var(--green);
    box-shadow: 0 0 0.4em rgba(47, 220, 12, 0.55);
}

.crt-burger-lines {
    position: relative;
}

.crt-burger-lines::before,
.crt-burger-lines::after {
    content: "";
    position: absolute;
    left: 0;
}

.crt-burger-lines::before { top: -7px; }
.crt-burger-lines::after  { top: 7px; }

/* ---- the icon assembling itself ---- */

/* The three bars fly in from off the icon and slam together, overshooting
   slightly past their resting positions before settling — so it lands rather
   than fades in.

   Timed to start once the strapline has finished typing: the logo reveals for
   400ms, a 200ms beat, then CrtType runs for 2s, and 2.75s leaves a further
   beat of silence between that and this so they read as separate events
   rather than one busy load. The middle bar is the parent element, so its
   scaleX carries the outer two with it and the whole glyph bumps as one.
   fill-mode: both holds the 0% state through the delay, keeping the icon
   invisible until its moment. */
.crt-burger-lines {
    animation-name: crt-burger-mid;
    animation-duration: 620ms;
    animation-timing-function: linear;
    animation-delay: 2.75s;
    animation-fill-mode: both;
}

.crt-burger-lines::before,
.crt-burger-lines::after {
    animation-duration: 620ms;
    animation-timing-function: linear;
    animation-delay: 2.75s;
    animation-fill-mode: both;
}

.crt-burger-lines::before { animation-name: crt-burger-top; }
.crt-burger-lines::after  { animation-name: crt-burger-bot; }

@keyframes crt-burger-mid {
    0%   { opacity: 0; transform: scaleX(0); filter: brightness(2.6); }
    22%  { opacity: 1; }
    /* Impact: the glyph flares and stretches, then rings down. */
    58%  { transform: scaleX(1.22); filter: brightness(2.6); }
    74%  { transform: scaleX(0.94); filter: brightness(1.3); }
    88%  { transform: scaleX(1.03); }
    100% { opacity: 1; transform: scaleX(1); filter: brightness(1); }
}

/* translateY is relative to the resting top offset, so 0 is the final place
   and a positive value on the top bar is an overshoot toward the middle. */
@keyframes crt-burger-top {
    0%   { opacity: 0; transform: translateY(-15px); }
    22%  { opacity: 1; }
    58%  { transform: translateY(3px); }
    74%  { transform: translateY(-1.5px); }
    88%  { transform: translateY(0.5px); }
    100% { opacity: 1; transform: translateY(0); }
}

@keyframes crt-burger-bot {
    0%   { opacity: 0; transform: translateY(15px); }
    22%  { opacity: 1; }
    58%  { transform: translateY(-3px); }
    74%  { transform: translateY(1.5px); }
    88%  { transform: translateY(-0.5px); }
    100% { opacity: 1; transform: translateY(0); }
}

/* Not tappable while it is still absent, so an early tap cannot open a menu
   from what looks like empty screen. pointer-events is not interpolated, it
   simply switches at the keyframe. */
.crt-burger {
    animation-name: crt-burger-hit;
    animation-duration: 2.75s;
    animation-fill-mode: both;
}

@keyframes crt-burger-hit {
    from { pointer-events: none; }
    to   { pointer-events: auto; }
}

/* Open state brightens rather than morphing into an X — the icon stays the
   three lines it started as. */
.crt-menu[open] .crt-burger-lines,
.crt-menu[open] .crt-burger-lines::before,
.crt-menu[open] .crt-burger-lines::after {
    background: #7cff5c;
    box-shadow: 0 0 0.7em rgba(47, 220, 12, 0.9);
}

/* ---- the dropdown panel ---- */

.crt-panel {
    position: absolute;
    top: calc(100% + 0.4rem);
    left: 0;
    /* Never wider than the viewport allows, so on a narrow phone the panel
       stays inside the screen instead of being clipped at the right edge.
       Sized by the about text, not by the rows: at the old 14rem the prose
       broke to about 26 characters a line, which is too narrow to read. */
    width: min(16rem, calc(100vw - 1.7rem));
    padding: 0.5rem 0;
    background: #000;
    border: 1px solid var(--green-dim);
    border-radius: 0.3rem;
    box-shadow:
        0 0 2rem rgba(47, 220, 12, 0.16),
        inset 0 0 2.5rem rgba(47, 220, 12, 0.05);
    /* The panel is its own stacking context so the scanline overlay below
       cannot be painted over by the items. */
    isolation: isolate;
    /* Shared by the curtain and by the beam that rides its edge, so the two
       cannot drift apart. */
    /* The single source of truth for the curtain's length: crt-menu.js reads
       this off the panel rather than carrying its own copy. */
    --curtain: 100ms;
    animation-name: crt-panel-on;
    animation-duration: var(--curtain);
    animation-timing-function: linear;
    animation-fill-mode: both;
}

/* A little more room where there is room to spare — a wider measure for the
   about text, and rows that do not look cramped beside a large logo.

   This has to sit *after* the base rule, not up with the other desktop
   overrides near .crt-nav. Both selectors are a single class, so specificity is
   equal and source order decides; while it lived above, the base width silently
   won at every viewport and the desktop value never once took effect. */
@media (min-width: 60rem) {
    .crt-panel {
        width: min(18rem, calc(100vw - 1.7rem));
    }
}

/* The hot leading edge. A curtain of clipped content is hard to read on its
   own — nothing marks where the edge actually is — so a bright line rides it
   down, which is what makes the movement legible. It sits inside the panel and
   is therefore clipped by the same path, so it can never appear below the
   edge it is drawing. */
/* A real element, not a pseudo-element: crt-menu.js drives both this and the
   curtain through the Web Animations API, and animating a pseudo-element from
   script is not portable. */
.crt-beam {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 2px;
    z-index: 2;
    pointer-events: none;
    background: #d6ffc9;
    box-shadow:
        0 0 0.75rem 0.15rem rgba(47, 220, 12, 0.85),
        0 0 2rem 0.4rem rgba(47, 220, 12, 0.45);
    opacity: 0;
    animation-name: crt-panel-beam;
    animation-duration: var(--curtain);
    animation-timing-function: linear;
    animation-fill-mode: both;
}

/* top rather than translateY: a percentage translate resolves against the
   beam's own 2px height, not the panel's. */
@keyframes crt-panel-beam {
    0%   { top: 0%; opacity: 1; }
    88%  { opacity: 1; }
    100% { top: 100%; opacity: 0; }
}

/* Drops downward like a curtain: the panel is revealed top to bottom over the
   full 400ms, its contents held still rather than squashed.

   clip-path rather than scaleY or max-height. scaleY would stretch the text as
   the panel grows, and max-height forces a layout on every frame; clipping
   reveals the finished panel through a moving edge, which is what a curtain
   actually does. The border and glow are clipped with it, so the frame draws
   itself downward too.

   Deliberately still animated under reduced motion — see the note there. */
@keyframes crt-panel-on {
    /* Every side is a percentage, including the zeroes. A bare 0 is a
       <length>, and WebKit will not interpolate a length against a percentage
       — it falls back to a discrete jump, so the curtain simply appeared open
       on iOS while animating correctly in Chromium. */
    0% {
        opacity: 1;
        clip-path: inset(0% 0% 100% 0%);
        filter: brightness(1.7);
    }
    /* Brightness falls off gradually rather than early, so the panel is still
       visibly warm while the edge is halfway down. */
    70% {
        filter: brightness(1.2);
    }
    100% {
        opacity: 1;
        clip-path: inset(0% 0% 0% 0%);
        filter: brightness(1);
    }
}

/* Scanlines over the panel, same pitch as the login terminal. */
.crt-panel::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    border-radius: inherit;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 1px,
        rgba(0, 0, 0, 0.5) 1px 2px
    );
}

/* The menu's one scroll container.

   The nav is position: fixed, so nothing below the fold can be reached by
   scrolling the page — with the automotive tree open the panel is taller than
   a phone held in landscape, and "contact" simply fell off the bottom. Capping
   here rather than on any individual section means the bound holds for
   whatever combination of sections happens to be open.

   6rem is the gap above the panel plus a gutter below it. The max() floor
   keeps the value positive on an absurdly short window, where a negative
   max-height would be dropped as invalid and the cap lost altogether. */
.crt-menu-list {
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: max(6rem, calc(100vh - 6rem));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--green-dim) transparent;
}

.crt-menu-item {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    /* Roomy enough to tap accurately without looking padded on a desktop. */
    padding: 0.6rem 0.9rem;
    color: var(--green);
    font-size: 0.8125rem;
    letter-spacing: 0.08em;
    text-decoration: none;
    text-transform: lowercase;
    text-shadow: 0 0 0.35em rgba(47, 220, 12, 0.45);
    cursor: pointer;
    list-style: none;
    -webkit-tap-highlight-color: transparent;
}

.crt-menu-item::-webkit-details-marker {
    display: none;
}

/* The caret is dim until the row is engaged, so the active row is obvious
   without moving anything. */
.crt-menu-caret {
    color: var(--green-dim);
}

.crt-menu-item:hover,
.crt-menu-item:focus-visible {
    background: rgba(47, 220, 12, 0.12);
    color: #8dff70;
    outline: none;
}

.crt-menu-item:hover .crt-menu-caret,
.crt-menu-item:focus-visible .crt-menu-caret,
.crt-menu details[open] > .crt-menu-item .crt-menu-caret {
    color: var(--amber);
}

/* ---- anything a nested disclosure reveals ---- */

/* Products, its two categories and about all drop in the same way the panel
   does, one level down and at a gentler pace. Keyed off a class rather than
   each section's own selector so a new section inherits the behaviour by
   being marked, and so crt-menu.js has one thing to look for. */
.crt-menu details[open] > .crt-reveal {
    animation-name: crt-reveal-on;
    animation-duration: 260ms;
    animation-timing-function: linear;
    animation-fill-mode: both;
}

/* Percentages on every side, including the zeroes — a bare 0 is a <length>,
   and WebKit will not interpolate a length against a percentage. Same trap
   that made the panel's own curtain jump straight to open on iOS. */
@keyframes crt-reveal-on {
    0%   { clip-path: inset(0% 0% 100% 0%); filter: brightness(1.6); }
    70%  { filter: brightness(1.15); }
    100% { clip-path: inset(0% 0% 0% 0%); filter: brightness(1); }
}

/* ---- products ---- */

.crt-submenu {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A category reads as subordinate to "products" without a second type size:
   indented to sit under the parent's label rather than its caret. */
.crt-menu-item--sub {
    padding-left: 1.55rem;
    font-size: 0.75rem;
}

.crt-tree-out {
    padding: 0.1rem 0.7rem 0.5rem 2.2rem;
}

.crt-tree {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Only nested levels indent. The outermost list is already placed by
   .crt-tree-out, and indenting it again would waste a column the labels need.
   0.7rem is about one character: enough to read as a level, cheap in width. */
.crt-tree .crt-tree {
    padding-left: 0.7rem;
}

/* pre-line, so a label can put a parenthetical on its own line: a newline in
   the catalogue breaks, and the runs of spaces around the indent still
   collapse the way they would in normal text. */
.crt-tree-label {
    display: block;
    white-space: pre-line;
    padding: 0.12rem 0;
    color: var(--amber);
    font-size: 0.6875rem;
    line-height: 1.45;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.3);
}

/* The dash is the indent guide. A hanging indent keeps a wrapped label lined
   up under its own first character instead of under the dash, so the level
   stays readable when a label runs to two lines — which several do in a 16rem
   column. */
.crt-tree-label::before {
    content: "- ";
    color: var(--green-dim);
}

.crt-tree-item {
    padding-left: 0.85rem;
    text-indent: -0.85rem;
}

/* A node with children is a heading for the ones below it, so it takes the
   chrome colour and its children keep the content colour. */
.crt-tree-branch > .crt-tree-label {
    color: var(--green);
    text-shadow: 0 0 0.35em rgba(47, 220, 12, 0.4);
}

.crt-tree-branch > .crt-tree-label::before {
    content: "+ ";
}

/* ---- about ---- */

/* The one place in the menu that is prose rather than a label, so it gets its
   own measure: a size down from the rows, and line-height opened up, because a
   narrow monospace column set solid is genuinely hard to read. */
.crt-about-out {
    padding: 0.1rem 0.9rem 0.55rem 1.55rem;
}

.crt-about-para {
    margin: 0 0 0.7rem;
    /* Amber for the payload, matching the address and the strapline: green is
       the chrome, amber is the content. */
    color: var(--amber);
    font-size: 0.75rem;
    line-height: 1.6;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.35);
}

.crt-about-para:last-child {
    margin-bottom: 0;
}

/* ---- contact ---- */

.crt-contact-out {
    padding: 0.1rem 0.9rem 0.7rem 1.55rem;
}

/* The company name, first in the communications section, above the address.
   Amber like the address — it is content, not chrome — but static rather than
   typed, so the mailto stays the thing that draws the eye and the tap. */
.crt-contact-company {
    margin: 0 0 0.3rem;
    color: var(--amber);
    font-size: 0.8125rem;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.5);
}

/* The business ID under the company name. Green rather than amber and a shade
   down in size: amber is the payload on this panel — the company name and the
   address — and a registration number is neither. */
.crt-contact-reg {
    margin: 0 0 0.45rem;
    color: var(--green-dim);
    font-size: 0.75rem;
    letter-spacing: 0.04em;
}

.crt-contact-link {
    display: inline-flex;
    align-items: center;
    /* Amber for the payload, matching the strapline: green is the chrome,
       amber is the content. */
    color: var(--amber);
    font-size: 0.8125rem;
    text-decoration: none;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.5);
}

.crt-contact-link:hover,
.crt-contact-link:focus-visible {
    text-decoration: underline;
    outline: none;
}

/* Typed out one character per step. --chars comes from the C# string length,
   so the count cannot drift from the address itself. No letter-spacing here:
   it would make ch units under-measure and clip the tail.

   content-box plus a sliver of right padding is deliberate: 1ch is the advance
   of "0", and rounding it against the real text width can land a fraction of a
   pixel short, which clips the last glyph. The padding is slack outside the
   animated width, so the final character can never be cut. */
.crt-contact-type {
    display: inline-block;
    box-sizing: content-box;
    padding-right: 0.14em;
    overflow: hidden;
    white-space: nowrap;
    vertical-align: bottom;
    width: 0;
}

/* Longhands rather than the animation shorthand: var() substitution inside
   the shorthand has been unreliable in WebKit. The step count cannot come
   from var() either — steps() needs a literal integer — so the component
   writes animation-timing-function inline from the same string it renders,
   which is why there is no character count written down in this file. */
.crt-contact[open] .crt-contact-type {
    animation-name: crt-contact-type;
    animation-duration: calc(var(--chars) * 45ms);
    animation-delay: 60ms;
    animation-fill-mode: both;
}

@keyframes crt-contact-type {
    from { width: 0; }
    to   { width: calc(var(--chars) * 1ch); }
}

/* The block travels with the text while it is being typed, then goes out and
   stays out — the address is the point, not a cursor parked next to it. */
.crt-contact-caret {
    width: 0.5em;
    height: 1em;
    margin-left: 1px;
    background: var(--amber);
    opacity: 0;
}

/* Lit for exactly the typing window, then cleared.

   linear with the value held until the last instant, rather than step-end with
   a forwards fill: WebKit has left step-end blocks stuck on at the end of a
   run before on this site — see the strapline cursor — and this avoids relying
   on that behaviour at all. */
.crt-contact[open] .crt-contact-caret {
    animation-name: crt-contact-caret;
    animation-duration: calc(var(--chars) * 45ms + 60ms);
    animation-timing-function: linear;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
}

@keyframes crt-contact-caret {
    0%     { opacity: 1; }
    99.9%  { opacity: 1; }
    100%   { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    /* The curtain deliberately stays. "Reduce motion" is about vestibular
       triggers — things that fly, scale, parallax or spin — and a clip edge
       travelling down a panel that is otherwise still is none of those. An
       earlier version disabled it outright, which on a phone with Reduce
       Motion switched on made the menu appear to pop open with no animation
       at all. What goes is the brightness flare, so it is a plain reveal. */
    @keyframes crt-panel-on {
        0%   { clip-path: inset(0% 0% 100% 0%); }
        100% { clip-path: inset(0% 0% 0% 0%); }
    }

    /* Same reasoning as the menu panel: the logo's clip-path wipe stays, the
       flare goes. */
    @keyframes logo-reveal {
        0%   { clip-path: inset(0% 0% 100% 0%); }
        100% { clip-path: inset(0% 0% 0% 0%); }
    }

    /* Same reasoning for every nested reveal: it stays, the flare goes. */
    @keyframes crt-reveal-on {
        0%   { clip-path: inset(0% 0% 100% 0%); }
        100% { clip-path: inset(0% 0% 0% 0%); }
    }

    /* The travelling beam is the one part of the curtain that is genuinely
       motion, so it goes; the edge still reads from the clip itself. */
    .crt-beam { display: none; }

    /* The icon lands without the flight or the bump: no travel, no overshoot,
       just brought up in place. */
    .crt-burger-lines,
    .crt-burger-lines::before,
    .crt-burger-lines::after {
        animation-name: crt-burger-plain;
        animation-duration: 260ms;
    }

    @keyframes crt-burger-plain {
        from { opacity: 0; }
        to   { opacity: 1; }
    }

    /* Show the address outright rather than typing it, and no cursor. */
    .crt-contact[open] .crt-contact-type {
        animation: none;
        width: calc(var(--chars) * 1ch);
    }

    .crt-contact[open] .crt-contact-caret {
        animation: none;
        opacity: 0;
    }
}

/* ---------- CRT product window ---------- */

/* Once crt-window.js has wired itself up, the nested list in the menu is dead
   weight — the window shows the same catalogue. Retiring it from the class the
   script sets, rather than from the script directly, keeps the fallback
   visible for the whole time the script has not run, including if it fails. */
.crt-win-ready .crt-products > .crt-reveal {
    display: none;
}

.crt-win-backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(0, 0, 0, 0.72);
}

.crt-win-backdrop[hidden],
.crt-win[hidden] {
    display: none;
}

/* Semi-big on purpose: an event, but still visibly a window over the page
   rather than a new page. The viewport subtractions keep a margin on a phone,
   where min() would otherwise resolve to the full screen. */
.crt-win {
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;

    position: fixed;
    z-index: 41;
    top: 50%;
    left: 50%;
    width: min(38rem, calc(100vw - 1.6rem));
    height: min(32rem, calc(100vh - 3rem));
    /* The switch-on animates transform, so the centring cannot live there —
       it would be overwritten the moment the animation starts. */
    margin: 0;
    translate: -50% -50%;
    transform-origin: 50% 50%;

    display: flex;
    flex-direction: column;
    background: #000;
    border: 1px solid var(--green-dim);
    border-radius: 0.35rem;
    box-shadow:
        0 0 3rem rgba(47, 220, 12, 0.22),
        inset 0 0 4rem rgba(47, 220, 12, 0.06);
    font-family: var(--crt-face);
    overflow: hidden;
}

/* ---- title bar ---- */

.crt-win-bar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.7rem;
    border-bottom: 1px solid var(--green-dim);
    background: rgba(47, 220, 12, 0.06);
}

/* The path is the title, and it retypes on every move — most of what tells
   you that you went somewhere. Clipped from the right by the script, so it
   needs no per-character markup. */
.crt-win-path {
    flex: 1;
    min-width: 0;
    color: var(--green);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    text-shadow: 0 0 0.4em rgba(47, 220, 12, 0.5);
    /* Long paths lose the front, not the end: the level you are on matters
       more than the ones above it. */
    direction: rtl;
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.crt-win-back,
.crt-win-close {
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    display: grid;
    place-items: center;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    background: transparent;
    color: var(--green);
    font: inherit;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.crt-win-back:hover,
.crt-win-back:focus-visible,
.crt-win-close:hover,
.crt-win-close:focus-visible {
    background: rgba(47, 220, 12, 0.15);
    color: #8dff70;
    outline: none;
}

.crt-win-back svg {
    display: block;
    width: 1.05rem;
    height: 1.05rem;
    fill: currentColor;
    shape-rendering: crispEdges;
}

/* Invisible at the top level rather than gone: the space is held, so the path
   beside it does not shift every time you move a level. */
.crt-win-back[disabled] {
    visibility: hidden;
}

/* ---- the picture ---- */

.crt-win-screen {
    position: relative;
    flex: 1;
    min-height: 0;
    isolation: isolate;
}

/* Scanlines over the whole picture. Deliberately weaker than the login
   terminal's and the menu panel's: the glyphs carry a coarser set of their
   own, and at full strength the two stacked turned every icon into a bar
   code — the shapes disappeared entirely. This one is the fine texture of the
   tube; the coarse one on the glyphs is what actually reads as a scanline. */
.crt-win-screen::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 1px,
        rgba(0, 0, 0, 0.22) 1px 2px
    );
}

/* The vertical-hold slip: between screens in the window, and as a fault on
   either picture. Created and removed by the script, so it costs nothing when
   nothing is moving. The two classes are one look on purpose — changing
   channel and losing the hold are the same event in the tube. */
.crt-win-roll,
.crt-glitch-roll {
    position: absolute;
    left: 0;
    right: 0;
    height: 14%;
    z-index: 2;
    pointer-events: none;
    background: linear-gradient(
        to bottom,
        rgba(47, 220, 12, 0) 0%,
        rgba(47, 220, 12, 0.16) 45%,
        rgba(214, 255, 201, 0.4) 55%,
        rgba(47, 220, 12, 0.1) 75%,
        rgba(47, 220, 12, 0) 100%
    );
}

/* A torn strip of the picture. The script clones the picture into .crt-glitch
   -slice-in and clips the wrapper to one horizontal band, so what shows is that
   band of picture and nothing else.

   The clip is here on the static wrapper and the movement is on the inner
   element, because transform carries an element's clip with it — one element
   cannot both hold the window open and slide through it.

   The inner is opaque black so the untorn picture underneath is covered rather
   than doubled: two copies of the same strip a few pixels apart is a ghost,
   which is a different fault from a tear. Black is right for both pictures on
   this site and would be wrong on any other background. */
.crt-glitch-slice {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    overflow: hidden;
}

.crt-glitch-slice-in {
    position: absolute;
    inset: 0;
    background: #000;
}

.crt-win-grid {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--green-dim) transparent;

    /* Flex rather than grid, so a row that does not fill is centred too. A
       grid can centre its tracks but not a short last row, and most levels
       here are short rows — two entries at the top, three under vw ag — which
       in a four-track grid all sat hard against the left edge.

       Tiles are a fixed width, so wrapping still gives two columns on a phone
       and four in the 38rem window, without a breakpoint. */
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    /* Centred vertically as well, because a level of two entries pinned to the
       top reads as a page that failed to load. safe, not plain centre: an
       overfull box centres its overflow and puts the first row out of reach
       above the scroll origin. */
    align-content: safe center;
    gap: 0.4rem;
    padding: 0.8rem;
}

/* ---- a tile ---- */

.crt-tile {
    /* Fixed width: the row is centred by the container, so a tile that grew to
       take up the slack would put the centring back where it started. */
    flex: 0 0 8rem;
    max-width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0.7rem 0.4rem 0.6rem;
    border: 1px solid transparent;
    border-radius: 0.3rem;
    background: transparent;
    color: var(--green);
    font: inherit;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    /* The power-on scales from the middle of the row of glyphs, not from the
       top of the box, so a tile opens the way a picture tube does. */
    transform-origin: 50% 50%;
}

.crt-tile:hover,
.crt-tile:focus-visible {
    border-color: var(--green-dim);
    background: rgba(47, 220, 12, 0.1);
    outline: none;
}

.crt-tile-label {
    /* See .crt-tree-label: a newline in a catalogue label breaks here too. */
    white-space: pre-line;
    color: var(--amber);
    font-size: 0.6875rem;
    line-height: 1.35;
    letter-spacing: 0.03em;
    text-transform: lowercase;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.4);
    /* Long names are common in the catalogue and a tile is narrow. Hyphenate
       where the language allows it, and only then break anywhere rather than
       push the column out. */
    hyphens: auto;
    -webkit-hyphens: auto;
    overflow-wrap: anywhere;
}

.crt-tile:hover .crt-tile-label,
.crt-tile:focus-visible .crt-tile-label {
    color: #ffc94d;
}

.crt-tile--wide {
    /* A row of its own, the flex equivalent of the old grid-column: 1 / -1. */
    flex: 0 0 100%;
    flex-direction: row;
    justify-content: center;
}

/* ---- the glyphs ---- */

/* The scanlines over an icon are a darkening overlay, not gaps cut out of the
   artwork. The logo's own stripes are gaps, which works at 620px and does not
   work at 56: it chopped the gecko's tongue into a dotted line. A 4px pitch
   with the dark band the thinner half reads as a picture tube and leaves every
   stroke continuous — more glyph than stripe, which is the way round it has to
   be at this size. */
.crt-tile-icon,
.crt-card-icon {
    position: relative;
    display: block;
    color: var(--green);
    filter: drop-shadow(0 0 0.3em rgba(47, 220, 12, 0.55));
}

.crt-tile-icon {
    width: 3.5rem;
    height: 3.5rem;
}

.crt-tile--wide .crt-tile-icon {
    width: 1.75rem;
    height: 1.75rem;
}

.crt-tile-icon svg,
.crt-card-icon svg {
    display: block;
    width: 100%;
    height: 100%;
    fill: currentColor;
    /* Hard edges: the glyphs are drawn on a coarse grid and smoothing them
       would undo the point of it. */
    shape-rendering: crispEdges;
}

.crt-tile-icon::after,
.crt-card-icon::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 2.6px,
        rgba(0, 0, 0, 0.62) 2.6px 4px
    );
}

.crt-tile:hover .crt-tile-icon,
.crt-tile:focus-visible .crt-tile-icon {
    color: #7cff5c;
    filter: drop-shadow(0 0 0.5em rgba(47, 220, 12, 0.85));
}

/* ---- a leaf ---- */

.crt-card {
    /* The leaf is the whole screen — one row to itself, as .crt-tile--wide. */
    flex: 0 0 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
    padding: 1rem 0.6rem 0.4rem;
    text-align: center;
    transform-origin: 50% 50%;
}

.crt-card-icon {
    width: 4.5rem;
    height: 4.5rem;
}

.crt-card-title {
    margin: 0;
    color: var(--amber);
    font-size: 0.9375rem;
    font-weight: 400;
    letter-spacing: 0.05em;
    text-transform: lowercase;
    text-shadow: 0 0 0.5em rgba(255, 176, 0, 0.45);
}

.crt-card-body {
    margin: 0;
    max-width: 26rem;
    color: var(--amber);
    font-size: 0.75rem;
    line-height: 1.65;
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.3);
}

/* Nothing is written behind most entries yet. Saying so in the chrome colour
   marks it as the machine talking rather than as copy. */
.crt-card-body--pending {
    color: var(--green-dim);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    text-shadow: none;
}

.crt-card .crt-tile--wide {
    margin-top: 0.4rem;
}

/* On a wide window a leaf has room to breathe. */
@media (min-width: 40rem) {
    .crt-card {
        padding: 2rem 1.5rem 1rem;
        gap: 1rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    /* The script already drops the flares, the staggers and the roll; the
       band is the one piece of genuine motion that could still be created if
       a preference changes mid-session. */
    .crt-win-roll,
    .crt-glitch-roll,
    .crt-glitch-slice { display: none; }
}

/* ============================================================
   The private areas, in the same phosphor as the front page.

   /me, /me/files, the per-person pages and the signed-in half of /you were
   plain light-mode forms while
   the public site is a green CRT, so signing in felt like leaving the site.
   This restyles what is already there rather than rewriting the markup: the
   pages keep their :is(.private, .you-page, .shop) / .panel / .btn structure and pick up the terminal
   look from here.

   .shop was added to the same list rather than given a palette of its own.
   The shop is public where these are private, but it is the same screen, and
   two copies of these tokens would drift apart the first time one was tuned.
   Shop-only rules — the catalogue grid, the price, the order facts — are at
   the end of this file and are additions, not overrides.

   Tokens are duplicated from .crt-login rather than lifted to :root on
   purpose - :root is the light public palette, and a stray --green there would
   leak into anything that later asked for it.
   ============================================================ */
body:has(:is(.private, .you, .shop)) {
    background: #000;
    color-scheme: dark;
}

/* ---------- customer portal and legal record ----------
   The portal is the same Sihilisko terminal, but its timeline is a single
   phosphor trace: every real interaction lands on that trace and absent
   product areas draw nothing. It reads as a customer record, not a generic
   dashboard full of empty statistic cards. */
body:has(:is(.customer-portal, .legal-page)) {
    background: #000;
    color-scheme: dark;
}

body:has(:is(.customer-portal, .legal-page)) .site-footer {
    color: #17800a;
    border-color: rgba(47, 220, 12, 0.18);
}

:is(.customer-portal, .legal-page) {
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;
    width: 100%;
    max-width: 54rem;
    margin: 1rem auto 2rem;
    color: var(--green);
    font-family: var(--crt-face);
}

:is(.customer-portal, .legal-page) h1 {
    margin: 0.35rem 0 0.75rem;
    color: var(--amber);
    font-size: clamp(1.55rem, 5vw, 2.35rem);
    font-weight: 400;
    line-height: 1.2;
    letter-spacing: 0.045em;
    text-shadow: 0 0 0.55em rgba(255, 176, 0, 0.34);
}

:is(.customer-portal, .legal-page) h2,
:is(.customer-portal, .legal-page) h3 {
    color: var(--green);
    font-weight: 400;
}

.portal-intro {
    max-width: 43rem;
    margin-bottom: 2rem;
}

.portal-intro p,
.portal-head p,
.portal-record p,
.portal-section > p,
.legal-copy p,
.legal-copy li {
    color: var(--green-dim);
    line-height: 1.65;
}

.portal-intro .portal-login-prompt { color: var(--amber); }

.portal-signin,
.portal-section,
.legal-page .panel {
    position: relative;
    margin-bottom: 1.25rem;
    padding: clamp(1rem, 4vw, 1.5rem);
    border: 1px solid var(--green-dim);
    border-radius: 0.35rem;
    background: #000;
    box-shadow: 0 0 2.5rem rgba(47, 220, 12, 0.09), inset 0 0 3rem rgba(47, 220, 12, 0.035);
}

.portal-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1.5rem;
}

.portal-head p { margin: 0; overflow-wrap: anywhere; }
.portal-section > header { margin-bottom: 1rem; border-bottom: 1px solid rgba(47, 220, 12, 0.2); }
.portal-section > header h2 { margin: 0 0 0.7rem; font-size: 0.9rem; letter-spacing: 0.18em; text-transform: uppercase; }

:is(.customer-portal, .legal-page) label {
    display: block;
    margin-bottom: 0.8rem;
    color: var(--green-dim);
    font-size: 0.8rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

:is(.customer-portal, .legal-page) :is(input[type="text"], input[type="email"], input:not([type]), textarea) {
    display: block;
    width: 100%;
    margin-top: 0.35rem;
    padding: 0.55rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    background: #000;
    color: var(--amber);
    font: inherit;
}

:is(.customer-portal, .legal-page) :is(input, textarea):focus {
    outline: none;
    border-color: var(--green);
    box-shadow: 0 0 0.6em rgba(47, 220, 12, 0.25);
}

:is(.customer-portal, .legal-page) .btn {
    display: inline-block;
    margin-top: 0.35rem;
    padding: 0.48rem 0.78rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    background: transparent;
    color: var(--green);
    font: inherit;
    font-size: 0.78rem;
    letter-spacing: 0.1em;
    text-decoration: none;
    text-transform: uppercase;
    cursor: pointer;
}

:is(.customer-portal, .legal-page) .btn:is(:hover, :focus-visible),
:is(.customer-portal, .legal-page) .btn.primary {
    border-color: var(--green);
}

:is(.customer-portal, .legal-page) .btn:is(:hover, :focus-visible) {
    background: var(--green);
    color: #000;
    outline: none;
}

:is(.customer-portal, .legal-page) .btn.danger { color: #ff6b3d; border-color: #7a2d12; }
.portal-legal-note { color: var(--green-dim); font-size: 0.76rem; line-height: 1.55; }
.portal-legal-note a, :is(.customer-portal, .legal-page) a { color: var(--green); }

.portal-records {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
    gap: 0.8rem;
}

.portal-record {
    padding: 1rem;
    border: 1px solid rgba(47, 220, 12, 0.22);
    border-left-color: var(--green);
}

.portal-record h3,
.portal-contract-list h3 { margin: 0.35rem 0 0.65rem; color: var(--amber); font-size: 1rem; }
.portal-record-top { display: flex; justify-content: space-between; gap: 0.75rem; }
.portal-record-top strong { color: var(--amber); white-space: nowrap; }
.portal-status { color: var(--green-dim); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; }

.portal-record dl { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 0.8rem; font-size: 0.78rem; }
.portal-record dt { color: var(--green-dim); }
.portal-record dd { margin: 0; color: var(--green); overflow-wrap: anywhere; }
.portal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.55rem; }
.portal-actions form { margin: 0; }

.portal-timeline ol { list-style: none; margin: 0; padding: 0; }
.portal-timeline li {
    position: relative;
    display: grid;
    grid-template-columns: 8.5rem 1fr;
    gap: 1.25rem;
    padding: 0 0 1.3rem 1rem;
    border-left: 1px solid var(--green-dim);
}
.portal-timeline li::before {
    content: "";
    position: absolute;
    left: -0.26rem;
    top: 0.15rem;
    width: 0.45rem;
    height: 0.45rem;
    background: var(--green);
    box-shadow: 0 0 0.65rem rgba(47, 220, 12, 0.8);
}
.portal-timeline li:last-child { padding-bottom: 0; }
.portal-timeline time { color: var(--green-dim); font-size: 0.72rem; }
.portal-timeline h3 { margin: 0; color: var(--amber); font-size: 0.9rem; }
.portal-timeline p { margin: 0.35rem 0 0; color: var(--green-dim); font-size: 0.78rem; }

.portal-contract-list { display: grid; gap: 0.75rem; }
.portal-contract-list article {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid rgba(47, 220, 12, 0.18);
}
.portal-contract-list article:last-child { padding-bottom: 0; border-bottom: 0; }
.portal-contract-list p { margin: 0; color: var(--green-dim); font-size: 0.75rem; }
.portal-data details { margin-top: 1rem; color: var(--green-dim); }
.portal-data summary { cursor: pointer; }
.portal-ready { text-align: center; }

.legal-copy { max-width: 48rem; }
.legal-copy h2 { margin: 1.6rem 0 0.5rem; font-size: 0.9rem; letter-spacing: 0.13em; text-transform: uppercase; }
.legal-copy h2:first-child { margin-top: 0; }
.legal-copy li + li { margin-top: 0.55rem; }
.contract-document .message { white-space: pre-wrap; }

.shop-customer-nav {
    display: flex;
    justify-content: flex-end;
    gap: 1rem;
    margin: -0.5rem 0 1.25rem;
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}
.shop-customer-nav a { color: var(--green-dim); text-decoration: none; }
.shop-customer-nav a:is(:hover, :focus-visible) { color: var(--green); }

.shop-check {
    display: flex !important;
    align-items: flex-start;
    gap: 0.65rem;
    text-transform: none !important;
    letter-spacing: 0.02em !important;
    line-height: 1.45;
}
.shop-check input[type="checkbox"] { flex: 0 0 auto; margin: 0.18rem 0 0; accent-color: var(--green); }
.shop-check a { color: var(--green); }

@media (max-width: 36rem) {
    .portal-head,
    .portal-contract-list article { flex-direction: column; align-items: flex-start; }
    .portal-timeline li { grid-template-columns: 1fr; gap: 0.25rem; }
    .portal-record-top { flex-direction: column; }
    .shop-customer-nav { justify-content: flex-end; }
}

@media print {
    .contract-document .private-foot,
    .contract-document .site-footer { display: none; }
    .contract-document { color: #000; }
}
body:has(:is(.private, .you, .shop)) .site-footer {
    color: var(--green-dim, #17800a);
    border-color: rgba(47, 220, 12, 0.18);
}
body:has(:is(.private, .you, .shop)) .site-footer a {
    color: inherit;
}

:is(.private, .you-page, .shop) {
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;

    position: relative;
    width: 100%;
    max-width: 46rem;   /* wider than the login box: these pages hold lists */
    margin: 1rem auto 2rem;
    font-family: var(--crt-face);
    color: var(--green);
    text-align: left;
}

.private-head { margin-bottom: 1.5rem; }
.private-tag {
    display: inline-block;
    font-size: 0.72rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--green-dim);
}
:is(.private, .you-page, .shop) h1 {
    margin: 0.35rem 0 0.6rem;
    font-size: 1.5rem;
    letter-spacing: 0.08em;
    color: var(--amber);
    text-shadow: 0 0 0.5em rgba(255, 176, 0, 0.35);
}
.private-sub, :is(.private, .you-page, .shop) .hint, :is(.private, .you-page, .shop) .empty, :is(.private, .you-page, .shop) .user-meta {
    color: var(--green-dim);
    font-size: 0.85rem;
    line-height: 1.55;
}

/* Each panel is its own little screen, matching the login box's chrome. */
:is(.private, .you-page, .shop) .panel {
    position: relative;
    margin: 0 0 1.25rem;
    padding: 1.15rem 1.1rem 1.3rem;
    background: #000;
    border: 1px solid var(--green-dim);
    border-radius: 0.35rem;
    box-shadow: 0 0 2.5rem rgba(47, 220, 12, 0.1), inset 0 0 3rem rgba(47, 220, 12, 0.04);
}
/* The comb is NOT on the panel. It used to be, and the panel then had two
   problems at once: the rule below lifts every child to z-index 1, so the
   overlay sat *under* the text it was supposed to stripe — only the empty
   background between the lines was combed — and everything outside a panel
   (the shop logo, the page heading, the back link) had no comb at all. Next
   to the burger menu, whose glyphs are visibly striped, the shop read as a
   different site. One raster for the whole page is both the fix and the
   truer model: a tube has one, and it does not stop at a box edge. See
   `:is(.private, .you-page, .shop)::after`.

   The z-index here stays: it is what keeps panel content above ::before, the
   curvature-and-falloff wash. */
:is(.private, .you-page, .shop) .panel > * { position: relative; z-index: 1; }

:is(.private, .you-page, .shop) h2,
:is(.private, .you-page, .shop) h3 {
    margin: 0 0 0.9rem;
    font-size: 0.95rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--green);
}
:is(.private, .you-page, .shop) .count {
    margin-left: 0.4rem;
    color: var(--green-dim);
    font-size: 0.8rem;
}
:is(.private, .you-page, .shop) .opt { color: var(--green-dim); font-size: 0.75rem; }

:is(.private, .you-page, .shop) label {
    display: block;
    margin-bottom: 0.8rem;
    font-size: 0.8rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--green-dim);
}
/* input:not([type]) is not padding. A bare <input> defaults to text but does
   not match input[type="text"], and half the fields on these pages are
   written without the attribute — the VAT box on a product page, the name,
   slug, price and currency boxes under /me/shop. They fell through to the
   browser's own control, which on a black page is a white slab with a blue
   focus ring. The public palette at the top of the file already carries the
   same :not([type]) for the same reason; this list had simply lost it.

   <select> is here for the country picker, the one control on a shop page
   that is not an input. It needs appearance: none as well (below) before any
   of this reaches it; the popup it opens stays OS-drawn, which is where the
   styling has to stop for a control that must remain operable. */
:is(.private, .you-page, .shop) input[type="text"],
:is(.private, .you-page, .shop) input[type="email"],
:is(.private, .you-page, .shop) input[type="password"],
:is(.private, .you-page, .shop) input[type="file"],
:is(.private, .you-page, .shop) input:not([type]),
:is(.private, .you-page, .shop) select,
:is(.private, .you-page, .shop) textarea {
    display: block;
    width: 100%;
    margin-top: 0.35rem;
    padding: 0.45rem 0.5rem;
    box-sizing: border-box;
    background: #000;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    font: inherit;
    font-size: 0.9rem;
    color: var(--amber);
    caret-color: var(--amber);
}
:is(.private, .you-page, .shop) input:focus,
:is(.private, .you-page, .shop) select:focus,
:is(.private, .you-page, .shop) textarea:focus {
    outline: none;
    border-color: var(--green);
    box-shadow: 0 0 0.6em rgba(47, 220, 12, 0.25);
}

/* The OS dropdown, replaced. appearance: none strips the native widget so the
   box above applies; the arrow is then ours to draw, and a chevron built from
   two gradients keeps it in the same square-edged idiom as the rest — no
   image, nothing to load. padding-right leaves it room. The popup list is
   drawn by the browser and cannot be styled; color-scheme: dark on the body
   is what keeps it from opening as a white sheet. */
:is(.private, .you-page, .shop) select {
    appearance: none;
    padding-right: 1.9rem;
    background-image:
        linear-gradient(45deg, transparent 50%, var(--green) 50%),
        linear-gradient(135deg, var(--green) 50%, transparent 50%);
    background-position:
        right 1.05rem center,
        right 0.62rem center;
    background-size: 0.44rem 0.44rem;
    background-repeat: no-repeat;
    cursor: pointer;
}

:is(.private, .you-page, .shop) .btn {
    display: inline-block;
    margin-top: 0.4rem;
    padding: 0.45rem 0.8rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.25rem;
    background: transparent;
    font: inherit;
    font-size: 0.82rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--green);
    cursor: pointer;
    text-shadow: 0 0 0.4em rgba(47, 220, 12, 0.5);
}
:is(.private, .you-page, .shop) .btn:hover, :is(.private, .you-page, .shop) .btn:focus-visible {
    background: var(--green);
    border-color: var(--green);
    color: #000;
    text-shadow: none;
    outline: none;
}
:is(.private, .you-page, .shop) .btn.primary { border-color: var(--green); }
:is(.private, .you-page, .shop) .btn.danger { color: #ff6b3d; border-color: #7a2d12; text-shadow: none; }
:is(.private, .you-page, .shop) .btn.danger:hover, :is(.private, .you-page, .shop) .btn.danger:focus-visible {
    background: #ff6b3d; border-color: #ff6b3d; color: #000;
}

:is(.private, .you-page, .shop) .user-list { list-style: none; margin: 0; padding: 0; }
:is(.private, .you-page, .shop) .user-list li {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    align-items: center;
    justify-content: space-between;
    padding: 0.7rem 0;
    border-top: 1px dashed rgba(47, 220, 12, 0.22);
}
:is(.private, .you-page, .shop) .user-list li:first-child { border-top: 0; }
:is(.private, .you-page, .shop) .user-main { min-width: 0; }
/* Long paths are the norm in the shared drawer; wrap rather than overflow. */
:is(.private, .you-page, .shop) .user-name {
    display: block;
    color: var(--amber);
    text-decoration: none;
    word-break: break-all;
}
:is(.private, .you-page, .shop) .user-name:hover { text-decoration: underline; }
/* wrap because three uppercase buttons at 1.15rem — Resend, Refund, Archive
   on an order — are wider than a narrow phone, and a row that cannot wrap is
   one more floor the page has to be as wide as. Same fault as the note under
   `main > *`, one level down. */
:is(.private, .you-page, .shop) .user-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
:is(.private, .you-page, .shop) .user-actions form { display: inline; }

.customer-search > label { flex: 1 1 20rem !important; }
.customer-search > .btn { flex: 0 0 auto !important; }
.customer-name-form { max-width: 30rem; margin-top: 1.2rem; }
.customer-account-actions { margin-top: 1rem; }
.customer-facts { margin-bottom: 0; }
.customer-state { color: var(--amber); }
.customer-interactions .user-meta { overflow-wrap: anywhere; }

:is(.private, .you-page, .shop) .stack > * + * { margin-top: 0.2rem; }
/* align-items: end so the boxes in a row line up along their bottom edge.
   The labels above them are not the same height — "Display name" carries an
   "optional" tag that wraps at narrow widths — and stretched labels put each
   field at a different offset. Now that every field in a row is a full-width
   box rather than a browser default, that stagger is the only thing left
   breaking the row. */
:is(.private, .you-page, .shop) .row { display: flex; flex-wrap: wrap; gap: 0.8rem; align-items: end; }
:is(.private, .you-page, .shop) .row > * { flex: 1 1 12rem; }

.private-foot {
    margin-top: 1.5rem;
    font-size: 0.82rem;
    letter-spacing: 0.1em;
}
.private-foot a { color: var(--green-dim); text-decoration: none; }
.private-foot a:hover { color: var(--green); }

/* The sihilisko badge as a small signature mark beside the footer copyright.
   No frame — the source art is already green-on-black, so its own black is the
   backing; it carries only the green phosphor glow and the same fine horizontal
   scanlines the landing-page CRT artwork uses. Sized by height to sit on the
   copyright's text line. */
.footer-sig {
    position: relative;
    display: inline-block;
    vertical-align: middle;
    height: 1.9em;
    margin-left: 0.55rem;
    /* Nudge up so the mark's centre lines up with the address text rather than
       sitting a touch low against it. */
    top: -0.18em;
    overflow: hidden;
    line-height: 0;
}

@media (max-width: 36rem) {
    .site-footer {
        flex-direction: column;
        gap: 0.55rem;
        padding: 1.1rem 0.75rem 1.3rem;
    }
    .footer-group,
    .footer-links { justify-content: center; }
    .footer-sig { margin: 0.2rem 0 0; top: 0; }
}
.footer-sig img {
    display: block;
    height: 100%;
    width: auto;
    /* Lift the green off the black so it reads as lit phosphor, not print. */
    filter: saturate(1.1) drop-shadow(0 0 0.25em rgba(47, 220, 12, 0.4));
}
/* Fine horizontal scanlines, plus a barely-there phosphor flicker. Decoration
   only, so the ::after is pointer-transparent. */
.footer-sig::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 1px,
        rgba(0, 0, 0, 0.28) 1px 2px
    );
    animation: footer-sig-flicker 5.5s steps(1) infinite;
}
@keyframes footer-sig-flicker {
    0%, 96% { opacity: 1; }
    97%     { opacity: 0.82; }
    98%     { opacity: 1; }
    99%     { opacity: 0.88; }
    100%    { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .footer-sig::after { animation: none; }
}

/* The flash message the endpoints redirect back with. */
:is(.private, .you-page, .shop) .flash, .flash {
    margin: 0 0 1rem;
    padding: 0.6rem 0.8rem;
    border-left: 2px solid var(--green);
    background: rgba(47, 220, 12, 0.06);
    font-size: 0.85rem;
    color: var(--green);
}
:is(.private, .you-page, .shop) .flash.error, .flash.error {
    border-left-color: #ff6b3d;
    background: rgba(255, 107, 61, 0.07);
    color: #ff6b3d;
}

/* No prefers-reduced-motion carve-out for the comb. It used to hide it, which
   left a reduced-motion reader on a shop page with no scanlines at all while
   the burger menu's .crt-panel::after kept its own — the comb is a static
   overlay, nothing about it moves. The hum animation below is the part that
   does, and that is the part the preference turns off. */

/* ---------- C0 dataset editor ------------------------------------------ */

body:has(.dataset-editor) {
    background: #000;
    color-scheme: dark;
}

body:has(.dataset-editor) main {
    display: block;
    padding: 1rem;
}

body:has(.dataset-editor) .site-footer {
    display: none;
}

.dataset-editor {
    --amber: #ffb000;
    --green: #2fdc0c;
    --green-dim: #17800a;
    width: min(100%, 72rem);
    margin: 0 auto;
    padding: 1rem 0 2rem;
    color: var(--green);
    font-family: var(--crt-face);
    font-synthesis: none;
    text-shadow: 0 0 0.06em rgba(47, 220, 12, 0.95), 0 0 0.45em rgba(47, 220, 12, 0.38);
}

.dataset-editor [hidden] { display: none !important; }

.dataset-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1.25rem;
    margin-bottom: 1rem;
}

.dataset-tag {
    color: var(--green-dim);
    font-size: 0.78rem;
    letter-spacing: 0.16em;
}

.dataset-editor h1 {
    margin: 0.25rem 0 0;
    color: var(--amber);
    font-size: clamp(1.35rem, 4vw, 1.8rem);
    font-weight: 400;
    letter-spacing: 0.06em;
    text-shadow: 0 0 0.06em rgba(255, 176, 0, 0.95), 0 0 0.5em rgba(255, 176, 0, 0.42);
}

.dataset-bases {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0;
    padding: 0;
    border: 0;
}

.dataset-bases label,
.dataset-formats label {
    display: inline-flex;
    width: auto;
    margin: 0;
    cursor: pointer;
}

.dataset-bases input,
.dataset-formats input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

.dataset-bases span,
.dataset-formats span {
    display: block;
    padding: 0.48rem 0.68rem;
    border: 1px solid var(--green-dim);
    color: var(--green-dim);
    font-size: 0.78rem;
    text-transform: uppercase;
}

.dataset-bases input:checked + span,
.dataset-formats input:checked + span {
    border-color: var(--green);
    color: #000;
    background: var(--green);
    text-shadow: none;
}

.dataset-bases input:focus-visible + span,
.dataset-formats input:focus-visible + span {
    outline: 1px solid var(--amber);
    outline-offset: 2px;
}

.dataset-tabs {
    display: flex;
    gap: 0;
    overflow-x: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--green-dim) transparent;
}

.dataset-tabs button {
    min-width: max-content;
    padding: 0.55rem 0.8rem;
    border: 1px solid var(--green-dim);
    border-bottom: 0;
    background: #000;
    color: var(--green-dim);
    font: inherit;
    font-size: 0.8rem;
    text-transform: uppercase;
    cursor: pointer;
}

.dataset-tabs button + button { border-left: 0; }
.dataset-tabs button[aria-selected="true"] { color: var(--amber); }
.dataset-tabs button:hover,
.dataset-tabs button:focus-visible { color: var(--green); outline: none; }

.dataset-screen {
    position: relative;
    min-height: 22rem;
    padding: 0.7rem;
    overflow: hidden;
    border: 1px solid var(--green-dim);
    background: #000;
    box-shadow: 0 0 2.5rem rgba(47, 220, 12, 0.1), inset 0 0 3rem rgba(47, 220, 12, 0.04);
}

.dataset-screen::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    background: repeating-linear-gradient(to bottom, transparent 0 1px, rgba(0, 0, 0, 0.38) 1px 2px);
}

.dataset-screen > section { position: relative; z-index: 2; }
.dataset-table-wrap { overflow-x: auto; }

.dataset-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.82rem;
}

.dataset-table th {
    padding: 0.5rem 0.45rem;
    color: var(--green-dim);
    font-weight: 400;
    text-align: left;
    text-transform: uppercase;
    border-bottom: 1px solid var(--green-dim);
}

.dataset-table td {
    padding: 0.48rem 0.45rem;
    border-bottom: 1px dashed rgba(47, 220, 12, 0.18);
    vertical-align: middle;
}

.dataset-table td:first-child { min-width: 13rem; }
.dataset-table td:nth-child(2) { min-width: 9rem; }
.dataset-table td:nth-child(3) { min-width: 7rem; color: var(--green-dim); }
.dataset-table td:last-child { min-width: 12rem; color: var(--green-dim); }

.dataset-field { display: block; color: var(--green); }
.dataset-table code { color: var(--green-dim); font-family: inherit; font-size: 0.72rem; }
.dataset-value { display: flex; align-items: center; gap: 0.4rem; color: var(--green-dim); }

.dataset-editor input[type="number"] {
    width: 7.5rem;
    padding: 0.38rem 0.45rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.15rem;
    background: #000;
    color: var(--amber);
    font: inherit;
    font-size: 0.88rem;
    caret-color: var(--amber);
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.38);
}

.dataset-editor input[type="number"]:focus {
    outline: none;
    border-color: var(--green);
    box-shadow: 0 0 0.55em rgba(47, 220, 12, 0.25);
}

.dataset-curve {
    display: block;
    width: 100%;
    height: auto;
    min-height: 13rem;
    border-bottom: 1px dashed rgba(47, 220, 12, 0.2);
}

.dataset-curve line { stroke: #0b3905; stroke-width: 1; }
.dataset-curve .curve-one { stroke: var(--green-dim); }
.dataset-curve text { fill: var(--green-dim); font: 9px var(--crt-face); }
.dataset-curve polyline { fill: none; stroke-width: 1; }
.dataset-curve [data-curve-ref="eu"] { stroke: #4f604c; }
.dataset-curve [data-curve-ref="nar"] { stroke: var(--amber); stroke-dasharray: 4 3; }
.dataset-curve [data-curve-current] { stroke: var(--green); stroke-width: 2; }

.curve-key {
    margin: 0.55rem 0;
    color: var(--green-dim);
    font-size: 0.75rem;
}

.curve-fields {
    display: grid;
    grid-template-columns: repeat(8, minmax(4.5rem, 1fr));
    gap: 0.45rem;
}

.curve-fields label { display: block; width: auto; margin: 0; color: var(--green-dim); font-size: 0.72rem; }
.curve-fields input { width: 100% !important; margin-top: 0.2rem; }

.dataset-export {
    display: grid;
    grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 0.8fr);
    align-items: center;
    gap: 0.8rem;
    margin-top: 0.9rem;
    padding: 0.8rem;
    border: 1px solid var(--green-dim);
}

.dataset-export code {
    min-width: 0;
    overflow-wrap: anywhere;
    color: var(--amber);
    font: 0.78rem var(--crt-face);
}

.dataset-formats { display: flex; gap: 0.35rem; }

.dataset-file {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.2rem 0.5rem;
    width: auto;
    margin: 0;
    color: var(--green-dim);
    font-size: 0.72rem;
    text-transform: uppercase;
}

.dataset-file input { grid-column: 2; color: var(--green); font: inherit; }
.dataset-file small { grid-column: 2; overflow-wrap: anywhere; color: var(--green-dim); text-transform: none; }

.dataset-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
    margin-top: 0.8rem;
}

.dataset-editor .btn {
    margin: 0;
    padding: 0.48rem 0.8rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.15rem;
    background: #000;
    color: var(--green);
    font: 0.8rem var(--crt-face);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
}

.dataset-editor .btn:hover,
.dataset-editor .btn:focus-visible,
.dataset-editor .btn.primary {
    border-color: var(--green);
    outline: none;
}

.dataset-editor .btn:hover,
.dataset-editor .btn:focus-visible {
    background: var(--green);
    color: #000;
    text-shadow: none;
}

.dataset-editor .btn:disabled { opacity: 0.45; cursor: wait; }
.dataset-import { position: relative; overflow: hidden; }
.dataset-import input { position: absolute; width: 1px; height: 1px; opacity: 0; }

.dataset-actions output { min-width: 12rem; margin-left: 0.25rem; color: var(--green-dim); font-size: 0.78rem; }
.dataset-actions output.error { color: #ff6b3d; }

.dataset-demo-note {
    margin: 0.6rem;
    padding: 0.55rem 0.7rem;
    border: 1px solid var(--amber);
    color: var(--amber);
    font-size: 0.78rem;
}

.dataset-demo-note a { color: inherit; }

.dataset-warnings {
    margin-top: 0.7rem;
    padding: 0.65rem 0.8rem;
    border-left: 2px solid var(--amber);
    color: var(--amber);
    font-size: 0.78rem;
}
.dataset-warnings ul { margin: 0; padding-left: 1.2rem; }

@media (max-width: 54rem) {
    .dataset-export { grid-template-columns: 1fr; }
    .curve-fields { grid-template-columns: repeat(4, minmax(4.5rem, 1fr)); }
}

@media (max-width: 36rem) {
    body:has(.dataset-editor) main { padding: 0.65rem; }
    .dataset-head { align-items: flex-start; flex-direction: column; }
    .dataset-screen { padding: 0.35rem; }
    .curve-fields { grid-template-columns: repeat(2, minmax(4.5rem, 1fr)); }
    .dataset-table { font-size: 0.76rem; }
}

/* ---- More phosphor -----------------------------------------------------
   The first pass got the colours right but the text stayed crisp, which reads
   as "green website" rather than "screen". Real phosphor blooms: a glyph is a
   dot of excited coating, brightest at the centre and falling off, so every
   character carries a halo and thin strokes smear into it.

   Done with layered text-shadow rather than a filter: blur() on the text
   itself, which turns small monospace into mush. Two shadows - a tight bright
   one for the bloom right at the glyph, a wide dim one for the glow thrown onto
   the surrounding glass.
   ------------------------------------------------------------------------ */

/* Python editor geometry, CRT palette */
.dataset-editor {
    width: min(100%, 67.5rem);
    padding-top: 0;
    border: 1px solid var(--green-dim);
    background: #000;
}

.dataset-titlebar {
    padding: 0.32rem 0.6rem;
    border-bottom: 1px solid var(--green);
    background: #061d04;
}

.dataset-titlebar h1 {
    margin: 0;
    color: var(--green);
    font-size: clamp(0.72rem, 2vw, 0.94rem);
    line-height: 1.4;
    letter-spacing: 0;
}

.dataset-header { padding: 0.45rem 0.65rem 0.35rem; }
.dataset-markings { margin-bottom: 0.35rem; color: var(--green-dim); font-size: 0.72rem; }
.dataset-selectors { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem 1.25rem; }
.dataset-selectors fieldset { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.7rem; margin: 0; padding: 0; border: 0; }
.dataset-selectors legend { float: left; width: auto; margin: 0 0.15rem 0 0; padding: 0; color: var(--green); font-size: 0.78rem; }
.dataset-selectors label { display: inline-flex; flex-direction: row; align-items: center; gap: 0.3rem; width: auto; margin: 0; color: var(--green-dim); font-size: 0.76rem; cursor: pointer; }
.dataset-selectors input { margin: 0; accent-color: var(--green); }

.dataset-tabs { padding: 0 0.6rem; }
.dataset-tabs button { padding: 0.42rem 0.68rem; font-size: 0.72rem; text-transform: none; }
.dataset-tabs button[aria-selected="true"] { color: #000; background: var(--green); text-shadow: none; }
.dataset-tabs button:disabled { color: #0b3905; cursor: not-allowed; }

.dataset-screen {
    min-height: 29rem;
    max-height: 36rem;
    margin: 0 0.6rem;
    padding: 0.45rem;
    overflow: auto;
}

.dataset-note { margin: 0.2rem 0.25rem 0.55rem; color: var(--green-dim); font-size: 0.76rem; }
.dataset-screen hr { margin: 0.3rem 0.25rem; border: 0; border-top: 1px solid var(--green-dim); }
.dataset-table { min-width: 45rem; font-size: 0.78rem; }
.dataset-table td { padding: 0.26rem 0.28rem; border: 0; }
.dataset-table td:first-child { width: 21%; min-width: 10rem; }
.dataset-table td:nth-child(2) { width: 26%; min-width: 13rem; color: var(--green-dim); white-space: nowrap; }
.dataset-table td:nth-child(3) { width: 7.5rem; min-width: 7.5rem; }
.dataset-table td:nth-child(4) { width: 3rem; min-width: 3rem; color: var(--green-dim); }
.dataset-table td:nth-child(5) { min-width: 12rem; color: var(--amber); }
.dataset-reference span { color: #0b3905; }
.dataset-field { color: var(--green); }
.dataset-editor input[type="number"] { width: 6.2rem; padding: 0.25rem 0.34rem; border-radius: 0; }
.dataset-editor input:disabled { border-color: #0b3905; color: #0b3905; opacity: 0.7; }

.dataset-curve { height: 12.5rem; min-height: 12.5rem; border: 1px solid var(--green-dim); }
.curve-markings { display: flex; justify-content: space-between; gap: 1rem; margin: 0.25rem 0.4rem; color: var(--green-dim); font-size: 0.68rem; }
.curve-fields { display: grid; grid-template-columns: repeat(16, minmax(2.4rem, 1fr)); min-width: 43rem; gap: 0.1rem; margin: 0.3rem 0.3rem 0; }
.curve-fields label { display: grid; justify-items: center; gap: 0.18rem; width: auto; color: var(--green-dim); font-size: 0.64rem; }
.curve-fields input[type="range"] { width: 1.1rem; height: 7.5rem; margin: 0; accent-color: var(--green); writing-mode: vertical-lr; direction: rtl; }
.curve-fields input[type="number"] { width: 2.35rem !important; padding: 0.16rem 0.1rem; font-size: 0.62rem; text-align: center; appearance: textfield; }
.curve-fields input[type="number"]::-webkit-inner-spin-button { appearance: none; }
.curve-actions { display: flex; gap: 0.4rem; margin: 0.45rem 0.3rem; }

.dataset-export { grid-template-columns: auto 1fr; gap: 0.35rem 0.7rem; margin: 0.45rem 0.6rem 0; padding: 0.45rem; font-size: 0.74rem; }
.dataset-export strong { font-weight: 400; }
.dataset-export code { font-size: 0.72rem; }
.dataset-formats { grid-column: 1 / -1; display: flex; gap: 1rem; }
.dataset-formats label { flex-direction: row; align-items: center; gap: 0.3rem; color: var(--green-dim); }
.dataset-formats input { position: static; opacity: 1; pointer-events: auto; margin: 0; accent-color: var(--green); }
.dataset-formats span,
.dataset-formats input:checked + span { padding: 0; border: 0; color: inherit; background: transparent; text-shadow: inherit; font-size: 0.72rem; text-transform: none; }
.dataset-file { grid-column: 1 / -1; grid-template-columns: auto auto minmax(10rem, 1fr); align-items: center; }
.dataset-xml { position: relative; display: inline-flex !important; width: auto; overflow: hidden; }
.dataset-xml input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dataset-file small { grid-column: 3; }
.dataset-actions { margin: 0.45rem 0.6rem 0.6rem; }
.dataset-editor .btn { padding: 0.35rem 0.58rem; border-radius: 0; font-size: 0.72rem; letter-spacing: 0; text-transform: none; }
.dataset-import { display: inline-flex !important; flex-direction: row !important; width: auto !important; }

@media (max-width: 36rem) {
    .dataset-editor { min-width: 0; }
    .dataset-screen { min-height: 25rem; }
    .dataset-selectors { display: block; }
    .dataset-selectors fieldset + fieldset { margin-top: 0.35rem; }
    .curve-markings { display: block; }
    .curve-markings span { display: block; }
    .dataset-file { grid-template-columns: auto 1fr; }
    .dataset-file small { grid-column: 2; }
}

:is(.private, .you-page, .shop) {
    text-shadow:
        0 0 0.06em rgba(47, 220, 12, 0.95),
        0 0 0.45em rgba(47, 220, 12, 0.45);
}
/* Amber runs hotter than green on the same tube. */
:is(.private, .you-page, .shop) h1,
:is(.private, .you-page, .shop) .you-head h1,
:is(.private, .you-page, .shop) input[type="text"],
:is(.private, .you-page, .shop) input[type="email"],
:is(.private, .you-page, .shop) input[type="password"],
:is(.private, .you-page, .shop) input:not([type]),
:is(.private, .you-page, .shop) select,
:is(.private, .you-page, .shop) textarea,
:is(.private, .you-page, .shop) .user-name {
    color: var(--amber);
    text-shadow:
        0 0 0.06em rgba(255, 176, 0, 0.95),
        0 0 0.5em rgba(255, 176, 0, 0.42);
}
/* /you's signed-in heading was sized for the light layout. */
:is(.private, .you-page, .shop) .you-head h1 {
    font-size: clamp(1.3rem, 4vw, 1.6rem);
    letter-spacing: 0.06em;
}
/* Dim text glows less - it is further from saturation, not just darker. */
:is(.private, .you-page, .shop) .private-sub,
:is(.private, .you-page, .shop) .hint,
:is(.private, .you-page, .shop) .empty,
:is(.private, .you-page, .shop) .user-meta,
:is(.private, .you-page, .shop) label {
    text-shadow: 0 0 0.35em rgba(47, 220, 12, 0.28);
}
/* A button that inverts to solid green is a lit block, not glowing text. */
:is(.private, .you-page, .shop) .btn:hover,
:is(.private, .you-page, .shop) .btn:focus-visible,
:is(.private, .you-page, .shop) .btn.danger:hover,
:is(.private, .you-page, .shop) .btn.danger:focus-visible {
    text-shadow: none;
}

/* Curvature and edge falloff. A tube is brightest in the middle and the corners
   never quite get there. Sits under the scanline layer, hence a second
   pseudo-element - ::after is already the comb. */
:is(.private, .you-page, .shop) .panel::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background:
        radial-gradient(120% 100% at 50% 45%,
            rgba(47, 220, 12, 0.05) 0%,
            rgba(0, 0, 0, 0) 55%),
        radial-gradient(140% 120% at 50% 50%,
            rgba(0, 0, 0, 0) 60%,
            rgba(0, 0, 0, 0.55) 100%);
}

/* Mains hum. Barely visible on purpose: a 2% swing at 11s reads as a live tube,
   anything stronger reads as a broken page and starts to hurt after a minute. */
@keyframes crt-hum {
    0%, 96%, 100% { opacity: 1; }
    97%           { opacity: 0.965; }
    98.5%         { opacity: 0.985; }
}
:is(.private, .you-page, .shop) .panel {
    animation: crt-hum 11s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    :is(.private, .you-page, .shop) .panel { animation: none; }
}

/* ---- Set for the bitmap face ---------------------------------------------
   The sizes above were chosen against DejaVu Sans Mono, and dropping a ROM
   font into them makes everything look shrunken. Two metrics changed:

     x-height   0.417em, against roughly 0.52em for a normal face, so the same
                font-size reads about a fifth smaller
     advance    0.5em against 0.602em, so a line of text is a sixth narrower

   Sizes therefore go up by about a quarter, and the panels get wider to hold
   the same filenames. Tracking goes the other way and mostly to zero: the face
   already carries a blank column inside every glyph cell, so the 0.12em-0.22em
   letter-spacing that gave the old face its terminal feel now pulls words into
   loose strings of unrelated characters.

   Kept as a trailing block rather than edited into the rules above so the
   original proportions stay legible - if the face is ever dropped, deleting
   this section restores them.
   ------------------------------------------------------------------------ */
:is(.private, .you-page, .shop) {
    /* 44rem at this size is about 65 characters a line. The face advances
       0.5em against DejaVu's 0.602em, so the 46rem that used to hold a
       comfortable measure now holds nearly ninety - long enough that the eye
       loses the line it is on. Narrower box, larger text. */
    max-width: 44rem;
    font-size: 1.35rem;
    line-height: 1.6;
    letter-spacing: 0;
    /* A bitmap face has no true italic or bold - synthesising either smears
       the grid. Anything that asks for emphasis gets it from colour instead. */
    font-synthesis: none;
    font-variant-ligatures: none;
}

:is(.private, .you-page, .shop) h1 {
    font-size: 2.4rem;
    letter-spacing: 0.02em;
}
:is(.private, .you-page, .shop) .you-head h1 {
    font-size: clamp(1.7rem, 5.5vw, 2.4rem);
    letter-spacing: 0.02em;
}
:is(.private, .you-page, .shop) h2,
:is(.private, .you-page, .shop) h3 {
    font-size: 1.5rem;
    letter-spacing: 0.04em;
}

.private-tag {
    font-size: 1.1rem;
    letter-spacing: 0.06em;
}
.private-sub,
:is(.private, .you-page, .shop) .hint,
:is(.private, .you-page, .shop) .empty,
:is(.private, .you-page, .shop) .user-meta,
:is(.private, .you-page, .shop) .flash,
.private-foot {
    font-size: 1.15rem;
    letter-spacing: 0;
}
:is(.private, .you-page, .shop) .count,
:is(.private, .you-page, .shop) .opt { font-size: 1.1rem; }

:is(.private, .you-page, .shop) label {
    font-size: 1.15rem;
    letter-spacing: 0.03em;
}
:is(.private, .you-page, .shop) input[type="text"],
:is(.private, .you-page, .shop) input[type="email"],
:is(.private, .you-page, .shop) input[type="password"],
:is(.private, .you-page, .shop) input[type="file"],
:is(.private, .you-page, .shop) input:not([type]),
:is(.private, .you-page, .shop) select,
:is(.private, .you-page, .shop) textarea {
    font-size: 1.3rem;
    letter-spacing: 0.02em;
    padding: 0.5rem 0.55rem;
}
:is(.private, .you-page, .shop) .btn {
    font-size: 1.15rem;
    letter-spacing: 0.04em;
    padding: 0.5rem 0.9rem;
}

/* The glow was tuned around antialiased strokes. Square dots take a tighter
   bloom before they start to fill in the gaps between the pixels and the
   staircase turns back into a blur - which would undo the whole point. */
:is(.private, .you-page, .shop) {
    text-shadow:
        0 0 0.04em rgba(47, 220, 12, 0.9),
        0 0 0.32em rgba(47, 220, 12, 0.42);
}
:is(.private, .you-page, .shop) h1,
:is(.private, .you-page, .shop) .you-head h1,
:is(.private, .you-page, .shop) input[type="text"],
:is(.private, .you-page, .shop) input[type="email"],
:is(.private, .you-page, .shop) input[type="password"],
:is(.private, .you-page, .shop) input:not([type]),
:is(.private, .you-page, .shop) select,
:is(.private, .you-page, .shop) textarea,
:is(.private, .you-page, .shop) .user-name {
    text-shadow:
        0 0 0.04em rgba(255, 176, 0, 0.9),
        0 0 0.34em rgba(255, 176, 0, 0.4);
}

/* Long filenames in the shared drawer wrap mid-word, and at this size a
   two-line filename next to a one-line row of buttons looked broken. */
:is(.private, .you-page, .shop) .user-list li { padding: 0.85rem 0; }

/* The page raster, drawn over everything in the section: headings, artwork,
   panels, buttons and the back link alike. This is what the burger menu gets
   from .crt-panel::after and what these pages did not have.

   Pitch is 2px on 4px, not the menu's 1px on 2px. The menu is set in
   12px-tall glyphs; the body here is 1.2rem, and a 1px comb against text that
   size reads as moiré rather than as scanlines. Same tube, coarser raster
   for coarser text — the site already varies pitch with glyph size (compare
   .crt-tile-icon at 2.6px on 4px against .crt-win-screen at 1px on 2px).

   z-index 5 clears the panels (positioned, z-index auto) and their children
   (z-index 1, set above), so the comb lands on the glyphs and not behind
   them. pointer-events:none so it never eats a click on the controls
   underneath. */
:is(.private, .you-page, .shop)::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 5;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0) 0 2px,
        rgba(0, 0, 0, 0.32) 2px 4px
    );
}

@media (max-width: 34rem) {
    /* Below this the 1.2rem body starts forcing horizontal scroll on the
       file list, and a private page that scrolls sideways is worse than one
       set a shade smaller. */
    :is(.private, .you-page, .shop) { font-size: 1.05rem; }
    :is(.private, .you-page, .shop) h1 { font-size: 1.7rem; }
    :is(.private, .you-page, .shop) :is(h2, h3) { font-size: 1.15rem; }
}

/* The file input's own button is drawn by the browser and ignores everything
   above, so it stayed a grey OS chrome button in the middle of a terminal.
   ::file-selector-button is the only handle on it. */
:is(.private, .you-page, .shop) input[type="file"] {
    padding: 0.4rem 0.5rem;
    color: var(--green-dim);
}
:is(.private, .you-page, .shop) input[type="file"]::file-selector-button {
    margin-right: 0.7rem;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--green-dim);
    border-radius: 0.2rem;
    background: transparent;
    font: inherit;
    font-size: 1.05rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--green);
    cursor: pointer;
}
:is(.private, .you-page, .shop) input[type="file"]::file-selector-button:hover {
    background: var(--green);
    border-color: var(--green);
    color: #000;
}

/* The footer is outside .private, so it kept the public sans face and sat
   under the terminal looking like a different website's furniture. */
body:has(:is(.private, .you, .shop)) .site-footer {
    font-family: var(--crt-face);
    font-size: 1rem;
    letter-spacing: 0;
}

@media (max-width: 34rem) {
    :is(.private, .you-page, .shop) { font-size: 1.15rem; }
    :is(.private, .you-page, .shop) h1 { font-size: 1.8rem; }
    :is(.private, .you-page, .shop) :is(h2, h3) { font-size: 1.25rem; }
}

/* ---------- shop --------------------------------------------------------

   Additions only. Everything structural — the panel chrome, the scanlines,
   the buttons, the inputs, the phosphor bloom — comes from the private-area
   rules above, which .shop was added to. What is here is the handful of
   things the shop has and those pages do not: a catalogue grid, a price, and
   the order page's fact list. */

/* The supplied shop artwork is square, but the mark itself is a wide band in
   its middle. Crop only the empty black canvas in layout so the original
   pixels remain untouched, then power the mark on with the landing logo's
   exact top-down CRT curtain. */
/* The h1 is a wrapper round the artwork and nothing else, so every rule
   .private-head h1 carries for a line of type — the clamp, the tracking, the
   leading, the margins — only adds space around a picture that already has
   its own. Reset to a plain box; .shop-logo below does the layout. */
.shop-title {
    margin: 0;
    font-size: 0;
    line-height: 0;
    letter-spacing: normal;
}

.shop-logo {
    display: block;
    position: relative;
    width: 100%;
    aspect-ratio: 2.5 / 1;
    overflow: hidden;
    margin: 0 0 1rem;
    animation: logo-reveal 400ms linear both;
}

.shop-logo img {
    display: block;
    width: 100%;
    height: auto;
    transform: translateY(-28%);
}

/* The empty catalogue speaks with the same amber type-on treatment as the
   landing-page strapline. The more general .shop .empty rule is deliberately
   dim green, so keep this one explicit for both the typed glyphs and cursor. */
.shop-empty {
    margin: 0;
    color: var(--amber);
}

.shop-empty .crt-line {
    color: var(--amber);
    text-transform: uppercase;
}

/* ---- categories ----

   A section per category, and one trailing section with no heading for
   everything that belongs to none. The heading takes the panel-heading look
   already set above — uppercase, tracked out — because that is exactly what
   it is: a label over a group, not a name. It sits outside the panels rather
   than inside one, so the cards below it stay the only lit boxes on the page
   and the heading reads as chrome. */
.shop-group + .shop-group {
    margin-top: 2rem;
}

/* The rule runs out from the heading to the edge, the way a section divider is
   drawn on a terminal. flex plus a growing pseudo-element rather than a border
   on the heading, so the line starts after the text however long the category
   name is. */
.shop-group-head {
    /* Tighter than the 0.9rem the shared heading rule gives it: the gap under
       a heading that labels a whole grid wants to be smaller than the gap
       under one that opens a paragraph, or the section falls away from it. */
    margin: 0 0 0.85rem;
    color: var(--green-dim);
    display: flex;
    align-items: center;
    gap: 0.7rem;
}
.shop-group-head::after {
    content: "";
    flex: 1;
    height: 1px;
    background: rgba(47, 220, 12, 0.22);
}

.shop-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 1.25rem;
}

/* One column until there is genuinely room for two. The cards hold a name, a
   line of summary and a price, which is narrow content — but at 46rem (the
   max-width these pages carry) two columns would put ~21rem of measure on
   each, and the summary is the one thing here that wraps. */
@media (min-width: 44rem) {
    .shop-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* The card is a .panel, so it already has the border, the glow and the
   scanline comb. This only makes it a column so the price can be pinned to
   the bottom and the cards line up regardless of summary length.

   min-width: 0 for the same reason main > * has it: a card is a grid item of
   .shop-grid, and in the single-column layout that track is auto-sized, so a
   card that cannot shrink takes the page sideways with it. */
.shop-card {
    display: flex;
    flex-direction: column;
    margin: 0;
    min-width: 0;
}

.shop-card h3 {
    margin-bottom: 0.5rem;
}

/* The panel heading style is uppercase with wide tracking, which is right for
   a section heading and wrong for a product name — a name is a proper noun,
   not a label, and MIB3 Region Tool set in caps at 0.16em reads as shouting. */
.shop-card h3 .user-name {
    text-transform: none;
    letter-spacing: 0.04em;
    font-size: 1.05rem;
}

/* Product names are the owner's text and can be anything. Set large in the
   product page's h1 and in a card title, one long name with no space in it
   has no wrap opportunity at all — see the note under `main > *`.
   
   Hyphenate first, break second. `anywhere` on its own chopped "Car workshop
   management syste/m" on a phone: a break with no hyphen in the middle of an
   ordinary word, which reads as a rendering fault rather than as typography.
   The page declares lang="en", so the browser knows where the syllables are;
   break-word stays underneath for a name that has no syllables to find — a
   part number, a slug — where a mid-word break really is the only option.
   
   break-word rather than anywhere is enough here because the grid items
   already carry min-width: 0, so the track can shrink without help from the
   min-content floor that `anywhere` lowers. */
.shop-card h3 .user-name,
.shop .private-head h1,
.shop .private-sub {
    hyphens: auto;
    -webkit-hyphens: auto;
    overflow-wrap: break-word;
    /* word-break: normal is the load-bearing line here. `.user-name` also
       matches `:is(.private, .you-page, .shop) .user-name`, which sets
       break-all for the shared drawer, where a name is a file path and
       breaking mid-string is right. A product name is not a path: break-all
       ignores both spaces and hyphenation and gave a phone "Car workshop
       management syste/m". This turns it off again for titles, and the
       hyphenation above then has something to do. */
    word-break: normal;
}

.shop-summary {
    hyphens: auto;
    -webkit-hyphens: auto;
    margin: 0 0 0.75rem;
    font-size: 0.88rem;
    line-height: 1.55;
}

.shop-meta {
    margin: 0 0 0.9rem;
    color: var(--green-dim);
    font-size: 0.78rem;
    letter-spacing: 0.06em;
}

/* The teaser's label, standing where the version and size would be. Amber
   rather than the dim green around it because it is the one thing on the card
   that changes what the reader can do with it — the same reason the price is
   amber — and boxed so it reads as a stamp on the listing rather than as one
   more line of metadata. */
.shop-soon {
    display: inline-block;
    padding: 0.1rem 0.45rem;
    border: 1px solid rgba(255, 176, 0, 0.45);
    color: var(--amber);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 0.72rem;
}

/* margin-top:auto is what pins this to the bottom of the flex column. */
.shop-buy {
    margin-top: auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.8rem;
    flex-wrap: wrap;
}

/* Amber, like every other number on this site that the reader is meant to
   act on — the headings, the filenames, the licence key. */
.shop-price {
    color: var(--amber);
    font-size: 1.1rem;
    letter-spacing: 0.06em;
    text-shadow: 0 0 0.5em rgba(255, 176, 0, 0.35);
}

.shop-price--large {
    display: block;
    margin: 0 0 0.6rem;
    font-size: 1.6rem;
}

/* The order page's facts. A two-column grid rather than a table: there are
   four rows of two short values, and a table would bring borders and cell
   padding that have to be undone before it looks like anything. */
.shop-facts {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.4rem 1rem;
    margin: 0;
    font-size: 0.85rem;
}

.shop-facts dt {
    color: var(--green-dim);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    font-size: 0.75rem;
    align-self: center;
}

.shop-facts dd {
    margin: 0;
    color: var(--green);
}

/* The order reference and the licence key: 43 and 29 characters of opaque
   string that a reader has to copy exactly. word-break so a long reference
   wraps inside its column instead of pushing the grid wider than the panel,
   and amber because these are the two things on the page worth copying.

   `.shop-facts dd.shop-mono` and not a bare `.shop-mono`: the dd rule above
   is a class plus an element and outweighs a lone class, so the plain
   selector lost and these rendered in the body green. */
.shop-facts dd.shop-mono,
.shop-mono {
    color: var(--amber);
    letter-spacing: 0.04em;
    word-break: break-all;
}

/* ---- the awaiting-payment screen --------------------------------------

   Three short lines and a QR code, and nothing else on the page: this is the
   one screen where the buyer has already decided and simply needs to be told
   where to send the money. Anything extra here is a chance to get it wrong. */

.shop-pay {
    margin: 0 0 0.9rem;
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--green);
}

/* Capped rather than full-width. The source is 426px square and a scan works
   from about 3cm across, so a phone camera has plenty at this size while a
   desktop window does not get a QR code the size of the panel. Sits on white
   because a QR reader wants the light modules light — the phosphor palette
   does not apply to something a camera has to decode. */
.shop-qr {
    display: block;
    width: 100%;
    max-width: 15rem;
    height: auto;
    margin: 0 0 1.1rem;
    padding: 0.6rem;
    background: #fff;
    border-radius: 0.35rem;
}

/* ---- the pending-purchase banner --------------------------------------

   Only drawn when something is actually waiting, so its presence is the
   signal. That is why it may be the loudest thing on the page without
   becoming wallpaper.

   Amber on a warm wash rather than a new colour: amber is already the "read
   this" tone across the private area, and the whole thing sitting inside a
   filled box is what separates it from an amber heading. */

.shop-alert {
    position: relative;
    margin: 0 0 1.5rem;
    padding: 1rem 1.1rem 1.2rem;
    background: rgba(255, 176, 0, 0.07);
    border: 1px solid var(--amber);
    border-left-width: 4px;
    border-radius: 0.35rem;
    box-shadow: 0 0 2.5rem rgba(255, 176, 0, 0.16);
    animation: shop-alert-breathe 3.4s ease-in-out infinite;
}

/* A slow swell on the glow, not a blink. A blink is an error state and reads
   as something being wrong; this is money waiting, which wants attention
   without alarm. The border and text never move, so nothing here makes the
   page harder to read while it animates. */
@keyframes shop-alert-breathe {
    0%, 100% { box-shadow: 0 0 2.5rem rgba(255, 176, 0, 0.16); }
    50%      { box-shadow: 0 0 3.2rem rgba(255, 176, 0, 0.30); }
}

@media (prefers-reduced-motion: reduce) {
    .shop-alert { animation: none; }
}

.shop-alert-head {
    margin: 0 0 0.7rem;
    font-size: 1rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--amber);
    text-shadow: 0 0 0.5em rgba(255, 176, 0, 0.45);
}

.shop-alert-list {
    list-style: none;
    margin: 0 0 0.8rem;
    padding: 0;
}

.shop-alert-list li {
    padding: 0.35rem 0;
    border-top: 1px dashed rgba(255, 176, 0, 0.25);
}

.shop-alert-list li:first-child { border-top: 0; }

.shop-alert-what {
    display: block;
    color: var(--amber);
    font-size: 0.9rem;
}

/* The buyer and the wait, one step down: which product and how much is what
   he checks against PayPal first. */
.shop-alert-who {
    display: block;
    color: var(--green-dim);
    font-size: 0.78rem;
    letter-spacing: 0.04em;
}

.shop-alert-do {
    margin: 0 0 0.2rem;
    color: var(--green);
    font-size: 0.85rem;
    line-height: 1.55;
}

/* The button is amber here rather than the usual green: it belongs to the
   banner, and a green button inside an amber box reads as unrelated to it. */
.shop-alert .btn.primary {
    border-color: var(--amber);
    color: var(--amber);
    text-shadow: 0 0 0.4em rgba(255, 176, 0, 0.5);
}

.shop-alert .btn.primary:hover,
.shop-alert .btn.primary:focus-visible {
    background: var(--amber);
    border-color: var(--amber);
    color: #000;
    text-shadow: none;
}

/* The demo counter on /me: four figures on one line, wrapping on a phone. */
.demo-figures { display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: baseline; }
.demo-figures .figure { font-size: .9rem; }
.demo-figures .figure b { font-size: 1.5rem; margin-right: .3rem; font-variant-numeric: tabular-nums; }
