/* ═══════════════════════════════════════════════════════════════════════════
   PAPERJET — visual system
   ═══════════════════════════════════════════════════════════════════════════
   Near-black, two neons, one accent per tool. The rule that keeps it from
   becoming noise: glow is reserved for things you can act on. A surface never
   glows, a button always can. Everything that moves, moves because you did
   something — there is no idle animation anywhere except the backdrop, which
   is slow enough to read as texture rather than motion.
   ═════════════════════════════════════════════════════════════════════════ */

:root {
    --bg:        #05060a;
    --bg-2:      #0a0c14;
    --surface:   #0e1119;
    --surface-2: #141824;
    --line:      #262e42;
    --line-2:    #333d56;

    --ink:   #eef2ff;
    --ink-2: #a6b1cd;   /* 8.80:1 on --surface */
    --ink-3: #828fb0;   /* 5.85:1 — was #5d6785 at 3.36:1, under AA */

    /* The same six hues and the same gradients, with the fluorescent edge taken
       off — roughly a quarter of the saturation, not two thirds. A full mute
       was tried and read as bland: the neon is the character of this page, it
       was only running slightly hot. Every one still clears 5.8:1 on
       --surface, so nothing lost contrast on the way down. */
    --cyan:    #3fc9e6;   /*  9.62:1 · was #00e5ff at 100% sat */
    --magenta: #f0559b;   /*  5.82:1 · was #ff2d95 */
    --lime:    #a8e05a;   /* 12.11:1 · was #b6ff3a */
    --amber:   #f0b04a;   /*  9.90:1 · was #ffb020 */
    --violet:  #a982f0;   /*  6.41:1 · was #a06bff */
    --red:     #f26b78;   /*  6.42:1 · was #ff4d5e */

    /* Every accent has two jobs, and they pull in opposite directions:
       small TEXT has to clear 4.5:1 against the page, while an icon, a chip or
       a glow only has to clear 3:1 and wants to be as colourful as it can get.
       On a near-black background one colour does both, so here the vivid twins
       ARE the neon. In the light theme they diverge — see the light block, and
       the note there about why everything looked washed out when they did not. */
    --cyan-v:    var(--cyan);
    --magenta-v: var(--magenta);
    --lime-v:    var(--lime);
    --amber-v:   var(--amber);
    --violet-v:  var(--violet);
    --red-v:     var(--red);

    /* Only the light theme fills a chip solid, but the variable has to resolve
       in both or --tool-on would be empty here. */
    --cyan-on: #04060d; --magenta-on: #04060d; --lime-on: #04060d;
    --amber-on: #04060d; --violet-on: #04060d; --red-on: #04060d;

    /* Per-tool accent, swapped on the workspace root. --tool for text and
       anything that has to be read, --tool-v for anything that is just colour. */
    --tool: var(--cyan);
    --tool-2: var(--magenta);
    --tool-v: var(--cyan-v);
    --tool-2v: var(--magenta-v);
    --tool-on: var(--cyan-on);

    /* ── Brand ──────────────────────────────────────────────────────────
       FIXED. These are not accents and must never be swapped by a theme or a
       palette pass. brand/paperjet-mark.svg, public/logo.svg, the favicon and
       apple-touch-icon.png are all built from these two values, so anything
       that recolours the header mark silently stops it matching the icon
       sitting beside it in the browser tab and on a phone home screen.

       That already happened once: the mark was left tracking --cyan-v, so
       softening the neon recoloured the brand and the copies drifted to three
       different gradients. A theme is a theme; a logo is a logo.

       ORDER IS THE POINT. --brand-1 is the top-left stop and --brand-2 the
       bottom-right one, and they are magenta→cyan rather than the cyan→magenta
       this started as. The old direction was the JioHotstar construction —
       cool on the left running to magenta on the right, on the same 45° axis —
       and at 16px in a tab strip the two read as the same mark. Running it the
       other way is what breaks the resemblance, so do not "tidy" these back
       into alphabetical or cool-to-warm order.

       Both stops are also brighter than the originals (#ff2d95 / #00e5ff),
       because the flip costs a little punch: magenta is the darker of the two
       and it now lands in the corner the eye reaches first.

       That brightness is CHROMA, not lightness. Raising L was tried and it
       just added white — the two ends went pastel, the middle of the ramp
       turned lavender, and the white dart started disappearing into its own
       tile. Both values are fully saturated instead, which reads brighter
       while keeping the dart's contrast. Do not lighten these to make them
       pop; push chroma or leave them alone. */
    --brand-1:   #ff0d9c;   /* top-left */
    --brand-2:   #00f5ff;   /* bottom-right */
    --brand-ink: #00f5ff;   /* the JET wordmark; light overrides for contrast */

    --font: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
    --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    --r:    16px;
    --r-sm: 10px;
    --ease: cubic-bezier(.22,.61,.36,1);
    --bounce: cubic-bezier(.34,1.56,.64,1);
}

* { box-sizing: border-box; }

html, body {
    margin: 0; padding: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--font);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Space Grotesk ships real 400-800 weights, so let the browser use them
       instead of synthesising anything, and keep numerals aligned in the
       size/percentage readouts. */
    font-synthesis: none;
    text-rendering: optimizeLegibility;
    overflow-x: hidden;
}

/* ── Backdrop ──────────────────────────────────────────────────────────────
   A grid that drifts, and a very slow pair of colour blooms. Fixed, behind
   everything, pointer-events off. `prefers-reduced-motion` stops all of it. */
.bg {
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    background:
        radial-gradient(900px 600px at 12% -10%, rgba(63,201,230,.085), transparent 60%),
        radial-gradient(800px 600px at 92% 8%, rgba(240,85,155,.075), transparent 60%),
        var(--bg);
}
.bg::before {
    content: ''; position: absolute; inset: -100px;
    background-image:
        linear-gradient(rgba(120,160,255,.055) 1px, transparent 1px),
        linear-gradient(90deg, rgba(120,160,255,.055) 1px, transparent 1px);
    background-size: 46px 46px;
    mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, #000 30%, transparent 78%);
    -webkit-mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, #000 30%, transparent 78%);
    animation: drift 24s linear infinite;
}
@keyframes drift { to { transform: translate(46px, 46px); } }

/* Scanlines — barely there, but they do the heavy lifting on "CRT". */
.bg::after {
    content: ''; position: absolute; inset: 0;
    background: repeating-linear-gradient(180deg,
        rgba(255,255,255,.016) 0 1px, transparent 1px 3px);
}

@media (prefers-reduced-motion: reduce) {
    .bg::before { animation: none; }
    * { animation-duration: .001ms !important; transition-duration: .06s !important; }
}

.wrap { position: relative; z-index: 1; max-width: 1320px; margin: 0 auto; padding: 0 22px; }

/* ── Header ───────────────────────────────────────────────────────────────── */
.hdr {
    position: sticky; top: 0; z-index: 40;
    display: flex; align-items: center; gap: 16px;
    padding: 14px 22px;
    background: rgba(5,6,10,.72);
    backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--line);
}
.logo {
    display: flex; align-items: center; gap: 10px;
    font-weight: 800; font-size: 19px; letter-spacing: -.4px;
    text-decoration: none; color: var(--ink); cursor: pointer;
    background: none; border: none; font-family: var(--font); padding: 0;
}
.logo-mark {
    width: 32px; height: 32px; border-radius: 9px; flex: 0 0 32px;
    display: grid; place-items: center;
    background: linear-gradient(135deg, var(--brand-1), var(--brand-2));
    /* Sharp, not glowing. The 16px halo that used to sit here bled the tile's
       edge into the header and softened the whole mark; a tight shadow plus a
       1px inner highlight reads as a solid object catching light and keeps the
       corner crisp. */
    box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.13);
}
/* Larger inside the tile: at 20px in a 30px tile the fold between the dart's
   two faces was closing up, which is most of what made the mark look soft. */
.logo-mark svg { width: 22px; height: 22px; display: block; }
/* The wordmark is ONE flex item. Left as a bare text node beside <b>JET</b> it
   was two, and the container's 10px gap landed between them — the brand read
   "Paper JET" everywhere it appeared, in the header and on the privacy page. */
.logo-word { display: inline-flex; }
/* The wordmark takes the brand ink, not an accent. It is the one part of the
   mark that has to move between themes: #00e5ff as text on white measures
   1.7:1 and fails even the 3:1 large-text rule, so the light theme drops it to
   the darkest cyan that still reads as the brand. "JET" is 19px/800, so 3:1 is
   the applicable bar and #0090b0 clears it at 3.74. */
.logo b { color: var(--brand-ink); }
.hdr-spacer { flex: 1; }
.hdr-safe {
    display: inline-flex; align-items: center; gap: 7px;
    font-family: var(--mono); font-size: 12px; color: var(--ink-3);
    letter-spacing: .3px;
}
.hdr-safe svg { width: 13px; height: 13px; color: var(--lime); }
.hdr-safe .dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--lime);
    box-shadow: 0 0 8px var(--lime);
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    font-family: var(--font); font-size: 14.5px; font-weight: 700;
    padding: 11px 18px; border-radius: var(--r-sm);
    border: 1px solid var(--line-2); background: var(--surface-2); color: var(--ink);
    cursor: pointer; text-decoration: none;
    transition: border-color .16s var(--ease), color .16s var(--ease),
                background .16s var(--ease), box-shadow .16s var(--ease),
                transform .12s var(--ease);
}
.btn:hover { border-color: var(--tool); color: #fff; box-shadow: 0 0 0 1px var(--tool), 0 0 22px -6px var(--tool); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; border-color: var(--line); }
.btn-primary {
    border: none; color: #04060d; font-weight: 800;
    /* The vivid twins, not the text-safe ones. A button is a fill, not small
       text, so it lives under the 3:1 rule — and filling it with the darkened
       accents turned amber into olive and made the main action of a tool look
       like it was disabled. */
    background: linear-gradient(135deg, var(--tool-v), var(--tool-2v));
    box-shadow: 0 0 26px -6px var(--tool-v);
}
.btn-primary:hover { color: #04060d; filter: brightness(1.1); box-shadow: 0 0 34px -4px var(--tool-v); }
.btn-lg { padding: 14px 26px; font-size: 16px; }
.btn-sm { padding: 7px 12px; font-size: 13px; }
.btn-ghost { background: transparent; }

/* ── Hero ─────────────────────────────────────────────────────────────────
   Left-aligned, and sized so the first row of tools is on screen when the page
   opens. It was centred at up to 78px over a left-aligned grid, which is two
   different pages stacked: the eye starts at the middle for the headline and
   then has to find the left edge again for everything below it. */
.hero { padding: 46px 0 30px; max-width: 780px; }
.kicker {
    display: inline-flex; align-items: center; gap: 8px;
    font-family: var(--mono); font-size: 12.5px; letter-spacing: 1.6px;
    text-transform: uppercase; color: var(--cyan);
    border: 1px solid rgba(0,229,255,.3); border-radius: 999px;
    padding: 6px 14px; background: rgba(0,229,255,.06);
}
.hero h1 {
    margin: 18px 0 0; font-size: clamp(32px, 4.6vw, 52px); line-height: 1.1;
    font-weight: 800; letter-spacing: -1.6px;
}
/* One phrase in colour, flat. The gradient-plus-drop-shadow this replaces was
   the loudest thing on the page and the most generic: every generated landing
   page has one. Colour still marks the phrase that matters; it no longer glows
   about it.

   --cyan, not --brand-ink. The brand value is the undimmed #00e5ff kept for the
   logo, and at 52px it is a lot of neon; the theme's own cyan is the same hue
   with the fluorescence taken off, and it is the value the rest of the page
   already speaks in. */
.hero h1 .hi { color: var(--cyan); }
.hero p {
    margin: 16px 0 0; max-width: 60ch; font-size: 17px; line-height: 1.6;
    color: var(--ink-2);
}

/* The label above a section heading. Mono and uppercase because that is
   already the voice of every other small label here — the filter counts, the
   page readout, PROCESSED LOCALLY. */
.eyebrow {
    margin: 0 0 10px; font-family: var(--mono); font-size: 11.5px;
    letter-spacing: 1.6px; text-transform: uppercase; color: var(--ink-3);
}

/* ── Tool grid ────────────────────────────────────────────────────────────── */
.grid-tools {
    display: grid; gap: 14px; padding: 22px 0 52px;
    grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
}
/* An <a>, not a <button>: each card is a real URL now, so it should behave like
   one — hoverable status bar, middle-click, "open in new tab", and something for
   a crawler to follow. display:block because an anchor is inline by default. */
.tool-card {
    display: block; text-decoration: none;
    position: relative; overflow: hidden; text-align: left;
    padding: 20px 18px 18px; border-radius: var(--r);
    background: linear-gradient(180deg, var(--surface), var(--bg-2));
    border: 1px solid var(--line); cursor: pointer;
    font-family: var(--font); color: var(--ink);
    transition: border-color .18s var(--ease), transform .18s var(--ease),
                box-shadow .18s var(--ease);
}
.tool-card::after {
    content: ''; position: absolute; inset: 0; opacity: 0; pointer-events: none;
    background: radial-gradient(420px 180px at var(--mx,50%) var(--my,0%),
                color-mix(in srgb, var(--tool-v) 18%, transparent), transparent 65%);
    transition: opacity .22s var(--ease);
}
.tool-card:hover { transform: translateY(-3px); border-color: var(--tool-v);
    box-shadow: 0 14px 40px -18px var(--tool-v); }
.tool-card:hover::after { opacity: 1; }
.tool-ico {
    width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
    background: color-mix(in srgb, var(--tool-v) 18%, transparent);
    border: 1px solid color-mix(in srgb, var(--tool-v) 42%, transparent);
    color: var(--tool-v); margin-bottom: 14px;
}
.tool-ico svg { width: 19px; height: 19px; }
/* A format mark brings its own colour, so the chip gets out of its way: a
   plain neutral tile, and the sheet drawn a size larger because it is a solid
   shape rather than a 2px outline and reads smaller at the same box. */
.tool-ico.brand {
    background: var(--surface-2);
    border-color: var(--line);
}
.tool-ico.brand svg { width: 23px; height: 23px; }
.tool-name { font-size: 16.5px; font-weight: 800; letter-spacing: -.3px; }
.tool-desc { margin-top: 6px; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }

/* Two to a row on a phone. At 232px minimum the grid fell to a single column
   and nineteen full-width cards became a twelve-screen scroll to reach the
   bottom of a list whose whole job is to be scanned. Paired, the same list is
   about four screens and the shape of it is visible. */
@media (max-width: 620px) {
    /* The header band shrinks with it, for the same reason: on a 390px screen
       the introduction was most of the first screenful. */
    .hero { padding: 26px 0 20px; }
    .hero h1 { font-size: 30px; letter-spacing: -1.1px; }
    .hero p { font-size: 15px; margin-top: 12px; }
    .kicker { font-size: 11px; letter-spacing: 1.2px; padding: 5px 11px; }
    .grid-tools { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .tool-card { padding: 15px 13px 14px; border-radius: var(--r-sm); }
    .tool-ico { width: 32px; height: 32px; border-radius: 9px; margin-bottom: 10px; }
    .tool-ico svg { width: 17px; height: 17px; }
    .tool-ico.brand svg { width: 20px; height: 20px; }
    .tool-name { font-size: 14.5px; }
    .tool-desc { margin-top: 4px; font-size: 12.5px; line-height: 1.5; }
    .tool-tag { top: 10px; right: 10px; }
}
.tool-tag {
    position: absolute; top: 14px; right: 14px;
    font-family: var(--mono); font-size: 10.5px; letter-spacing: .8px;
    text-transform: uppercase; padding: 3px 7px; border-radius: 5px;
    background: color-mix(in srgb, var(--lime) 15%, transparent);
    color: var(--lime); border: 1px solid color-mix(in srgb, var(--lime) 32%, transparent);
}

/* ── Workspace ────────────────────────────────────────────────────────────── */
.ws { display: none; padding: 22px 0 60px; }
.ws.on { display: block; }
.ws-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 18px; }
.ws-title { font-size: 26px; font-weight: 800; letter-spacing: -.7px; }
.ws-meta { font-family: var(--mono); font-size: 13px; color: var(--ink-3); }

.ws-body { display: grid; gap: 16px; grid-template-columns: minmax(0,1fr) 340px; align-items: start; }
@media (max-width: 940px) { .ws-body { grid-template-columns: minmax(0,1fr); } }

.panel {
    background: linear-gradient(180deg, var(--surface), var(--bg-2));
    border: 1px solid var(--line); border-radius: var(--r); padding: 18px;
}
.panel-head {
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
    margin-bottom: 14px;
}
.panel-head h3 { margin: 0; font-size: 15px; font-weight: 800; letter-spacing: -.2px; }

/* ── Drop zone ────────────────────────────────────────────────────────────── */
.drop {
    position: relative; display: grid; place-items: center; text-align: center;
    min-height: 240px; padding: 34px 24px; cursor: pointer; overflow: hidden;
    border-radius: var(--r); border: 2px dashed var(--line-2);
    background: rgba(10,12,20,.6); color: var(--ink-2);
    transition: border-color .2s var(--ease), background .2s var(--ease),
                box-shadow .2s var(--ease);
}
.drop:hover, .drop:focus-visible { border-color: var(--tool); background: color-mix(in srgb, var(--tool) 6%, transparent); }
.drop.over {
    border-color: var(--tool); border-style: solid;
    background: color-mix(in srgb, var(--tool) 11%, transparent);
    box-shadow: inset 0 0 60px -20px var(--tool), 0 0 0 4px color-mix(in srgb, var(--tool) 18%, transparent);
}
.drop.compact { min-height: 0; padding: 14px 18px; }
.drop.compact .drop-art { display: none; }
.drop-art { position: relative; width: 74px; height: 74px; margin: 0 auto 18px; }
.drop-hex {
    position: absolute; inset: 0; display: grid; place-items: center;
    color: var(--tool);
    filter: drop-shadow(0 0 14px color-mix(in srgb, var(--tool) 60%, transparent));
    transition: transform .3s var(--bounce);
}
.drop-hex svg { width: 100%; height: 100%; }
.drop:hover .drop-hex, .drop.over .drop-hex { transform: translateY(-5px) scale(1.05); }
.drop-main { font-size: 17px; font-weight: 800; color: var(--ink); letter-spacing: -.3px; }
.drop-sub { margin-top: 7px; font-size: 13.5px; color: var(--ink-3); }
.file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── File chips ───────────────────────────────────────────────────────────── */
.files { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.file {
    display: flex; align-items: center; gap: 10px; padding: 10px 12px;
    border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--surface-2); font-size: 14px;
}
.file-ico { color: var(--tool); display: grid; place-items: center; }
.file-ico svg { width: 16px; height: 16px; }
.file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.file-meta { font-family: var(--mono); font-size: 12px; color: var(--ink-3); white-space: nowrap; }
.file-x {
    border: none; background: none; color: var(--ink-3); cursor: pointer;
    font-size: 18px; line-height: 1; padding: 2px 5px; border-radius: 6px;
}
.file-x:hover { color: var(--red); background: rgba(255,77,94,.1); }

/* ── Page grid ────────────────────────────────────────────────────────────── */
.pages {
    display: grid; gap: 11px; margin-top: 16px;
    grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
    max-height: 460px; overflow-y: auto; padding-right: 4px;
}
.page {
    position: relative; border-radius: var(--r-sm); overflow: hidden;
    background: #fff; border: 2px solid var(--line);
    aspect-ratio: 1 / 1.35; transition: border-color .15s var(--ease), transform .15s var(--ease);
}
.page.pick { cursor: pointer; }
.page.pick:hover { transform: translateY(-2px); border-color: var(--tool); }
.page.sel { border-color: var(--tool); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tool) 25%, transparent); }
.page.drop-mode.sel { border-color: var(--red); box-shadow: 0 0 0 3px rgba(255,77,94,.25); }
.page canvas, .page img { width: 100%; height: 100%; object-fit: contain; display: block; }
.page-n {
    position: absolute; right: 5px; bottom: 5px;
    font-family: var(--mono); font-size: 11px; font-weight: 700;
    background: rgba(5,6,10,.82); color: var(--ink); padding: 2px 6px; border-radius: 5px;
}
.page-skel { background: linear-gradient(100deg, #12151f 30%, #1a1f2e 50%, #12151f 70%); }

/* ── Organize ─────────────────────────────────────────────────────────────── */
/* A tile here is something you pick up and put down, so it says so. The gap a
   page would drop into is marked on the tile itself rather than with an
   insertion line — a line between tiles in a wrapping grid is ambiguous at the
   end of every row. */
.page-org { cursor: grab; }
.page-org:active { cursor: grabbing; }
.page-org.dragging { opacity: .4; transform: scale(.97); }
.page-org.over { border-color: var(--tool);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--tool) 45%, transparent);
    transform: translateY(-3px); }
.page-org.turned { border-color: var(--tool); }

/* ── Placement surface (Sign, Redact) ─────────────────────────────────────── */
.place { margin-top: 16px; }
.place[hidden] { display: none; }
.place-bar {
    display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
}
.place-n { font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); }
.place-hint { font-size: 12.5px; color: var(--ink-3); }
.place-stage {
    position: relative; display: inline-block; max-width: 100%;
    border-radius: var(--r-sm); overflow: hidden;
    border: 1px solid var(--line); background: #fff;
}
.place-stage canvas { display: block; max-width: 100%; height: auto; }
.place-layer { position: absolute; inset: 0; cursor: crosshair; }

/* A box on the page. Redaction boxes are solid black because that is what they
   will BE — a preview that shows a translucent hint of what is underneath
   would misrepresent an operation whose whole point is that the thing
   underneath stops existing. */
.pbox {
    position: absolute; border: 2px solid var(--tool-v);
    background: rgba(0,0,0,.86); cursor: move;
}
.pbox.sign { background: transparent; border-style: dashed; }
.pbox.sign img { width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
.pbox-x {
    position: absolute; top: -10px; right: -10px; width: 20px; height: 20px;
    border-radius: 50%; border: none; cursor: pointer; padding: 0; line-height: 1;
    background: var(--red); color: #fff; font-size: 13px; font-weight: 700;
}
.pbox-h {
    position: absolute; right: -6px; bottom: -6px; width: 14px; height: 14px;
    border-radius: 3px; background: var(--tool-v); border: 2px solid #fff;
    cursor: nwse-resize;
}

/* ── Watermark preview ────────────────────────────────────────────────────── */
/* Fills the stage and scales with it. The SVG carries the page's own points in
   its viewBox, so the stamp keeps its true proportion at any column width — see
   paintBoxes() for why a pixel font size could not. */
.wm-mark {
    position: absolute; inset: 0; width: 100%; height: 100%;
    pointer-events: none; overflow: visible;
}

/* ── Colour swatches ──────────────────────────────────────────────────────── */
.sw-row { display: flex; flex-wrap: wrap; gap: 8px; }
.sw {
    width: 30px; height: 30px; padding: 0; cursor: pointer;
    border-radius: 9px; border: 1px solid var(--line-2);
    background: var(--sw);
    transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
.sw:hover { transform: translateY(-1px); }
/* The ring sits OUTSIDE the swatch, not on it — an inset mark would eat into
   the very thing being judged, which on a dark swatch is most of it. */
.sw.on { border-color: var(--bg-2); box-shadow: 0 0 0 2px var(--tool); }
/* The picker's own square: a spectrum until a colour is chosen through it, and
   that colour afterwards. The input itself is invisible and stretched over the
   whole square so the entire swatch is the target, not a 2px corner. */
.sw-any {
    position: relative; overflow: hidden; display: grid; place-items: center;
    background:
        linear-gradient(var(--sw), var(--sw)) padding-box,
        conic-gradient(#e34b3f, #e2b23a, #4bbd6b, #3aa8dd, #7a5cd8, #e34b3f) border-box;
    border: 3px solid transparent;
}
.sw-any input[type="color"] {
    position: absolute; inset: -25%; width: 150%; height: 150%;
    padding: 0; border: 0; background: none; opacity: 0; cursor: pointer;
}

/* ── Crop frame ───────────────────────────────────────────────────────────── */
/* The kept area is the lit area. A single huge box-shadow spread darkens
   everything outside the frame, so there is no second element to keep in step
   and no seams where four shading panels would meet. */
.cropbox {
    position: absolute; cursor: move;
    border: 1px solid rgba(255,255,255,.9);
    box-shadow: 0 0 0 9999px rgba(6,10,20,.55), inset 0 0 0 1px rgba(0,0,0,.35);
}
/* Thirds, because a crop is judged against them. Drawn on the frame itself so
   they move with it and cost nothing. */
.cropbox::before {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background:
        linear-gradient(to right, transparent 33.33%, rgba(255,255,255,.28) 33.33%,
            rgba(255,255,255,.28) calc(33.33% + 1px), transparent calc(33.33% + 1px),
            transparent 66.66%, rgba(255,255,255,.28) 66.66%,
            rgba(255,255,255,.28) calc(66.66% + 1px), transparent calc(66.66% + 1px)),
        linear-gradient(to bottom, transparent 33.33%, rgba(255,255,255,.28) 33.33%,
            rgba(255,255,255,.28) calc(33.33% + 1px), transparent calc(33.33% + 1px),
            transparent 66.66%, rgba(255,255,255,.28) 66.66%,
            rgba(255,255,255,.28) calc(66.66% + 1px), transparent calc(66.66% + 1px));
}
.crop-h {
    position: absolute; background: #fff; border-radius: 2px;
    box-shadow: 0 1px 4px rgba(0,0,0,.5);
}
/* Edges are long thin grips; corners are squares. Both are given a fat
   invisible hit area, because a 3px target is a 3px target on a trackpad. */
.crop-h.n, .crop-h.s { left: 50%; width: 34px; height: 4px; margin-left: -17px; cursor: ns-resize; }
.crop-h.e, .crop-h.w { top: 50%; width: 4px; height: 34px; margin-top: -17px; cursor: ew-resize; }
.crop-h.n { top: -2px; } .crop-h.s { bottom: -2px; }
.crop-h.w { left: -2px; } .crop-h.e { right: -2px; }
.crop-h.ne, .crop-h.nw, .crop-h.se, .crop-h.sw { width: 11px; height: 11px; }
.crop-h.nw { left: -5px; top: -5px; cursor: nwse-resize; }
.crop-h.ne { right: -5px; top: -5px; cursor: nesw-resize; }
.crop-h.sw { left: -5px; bottom: -5px; cursor: nesw-resize; }
.crop-h.se { right: -5px; bottom: -5px; cursor: nwse-resize; }
.crop-h::after { content: ''; position: absolute; inset: -10px; }
.crop-size {
    position: absolute; left: 50%; bottom: -26px; transform: translateX(-50%);
    font-family: var(--mono); font-size: 11px; color: #fff; white-space: nowrap;
    background: rgba(6,10,20,.72); padding: 2px 8px; border-radius: 5px;
    pointer-events: none;
}

/* ── Signature pad ────────────────────────────────────────────────────────── */
.sign-pad {
    border: 1px dashed var(--line-2); border-radius: var(--r-sm);
    background: #fff; touch-action: none; display: block; width: 100%; cursor: crosshair;
}
.sign-row { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }

/* ── Magnifier ────────────────────────────────────────────────────────────── */
/* On every tile in every tool, because opening a page and choosing a page are
   different questions — in Split a plain click already means "pick this one",
   so the magnifier is how you look without deciding. Always visible on touch,
   where there is no hover to reveal it. */
.mag {
    position: absolute; top: 6px; right: 6px; width: 26px; height: 26px;
    border: none; border-radius: 8px; cursor: pointer; padding: 0; z-index: 2;
    display: flex; align-items: center; justify-content: center;
    background: rgba(5,6,10,.72); color: #fff; backdrop-filter: blur(3px);
    opacity: 0; transform: scale(.85);
    transition: opacity .14s var(--ease), transform .16s var(--bounce), background .14s;
}
.mag svg { width: 14px; height: 14px; }
.page:hover .mag, .mag:focus-visible { opacity: 1; transform: scale(1); }
.mag:hover { background: var(--tool); color: #05060a; }
@media (hover: none) { .mag { opacity: 1; transform: none; } }
/* Organize already puts a control bar across the top of the tile; the
   magnifier sits under it rather than on top of it. */
.page-org .mag { top: 40px; }

/* ── Page viewer ──────────────────────────────────────────────────────────── */
.lb {
    position: fixed; inset: 0; z-index: 1200; display: flex;
    align-items: center; justify-content: center;
    background: rgba(5,6,10,.9); backdrop-filter: blur(6px);
    animation: lbFade .16s var(--ease);
}
.lb[hidden] { display: none; }
@keyframes lbFade { from { opacity: 0; } }
.lb-stage {
    display: flex; align-items: center; justify-content: center;
    max-width: min(92vw, 1100px); max-height: 84vh;
}
.lb-stage canvas, .lb-stage img {
    max-width: 100%; max-height: 84vh; width: auto; height: auto;
    display: block; border-radius: 6px; background: #fff;
    box-shadow: 0 30px 90px rgba(0,0,0,.7); animation: lbPop .2s var(--bounce);
}
@keyframes lbPop { from { opacity: 0; transform: scale(.97); } }
.lb-load {
    width: 58px; height: 58px; border-radius: 50%;
    border: 3px solid rgba(255,255,255,.16); border-top-color: var(--tool);
    animation: lbSpin .7s linear infinite;
}
@keyframes lbSpin { to { transform: rotate(360deg); } }
.lb-msg { color: rgba(255,255,255,.7); font-size: 13px; }
.lb-bar {
    position: absolute; top: 0; left: 0; right: 0; display: flex;
    align-items: center; gap: 14px; padding: 16px 20px; color: #fff;
}
.lb-title {
    font-size: 13px; font-weight: 700; letter-spacing: .2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lb-acts { margin-left: auto; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.lb-x {
    width: 34px; height: 34px; border: none; border-radius: 10px; cursor: pointer;
    background: rgba(255,255,255,.12); color: #fff; font-size: 21px; line-height: 1;
    padding: 0; transition: background .14s var(--ease);
}
.lb-x:hover { background: rgba(255,255,255,.24); }
/* Deciding while zoomed in is the point — if you opened a page to see whether
   it belongs, you should be able to say so without closing it first. */
.lb-pick {
    border: none; border-radius: 10px; cursor: pointer; padding: 9px 15px;
    font-family: var(--font); font-size: 12.5px; font-weight: 800; color: #fff;
    background: rgba(255,255,255,.14); transition: background .14s var(--ease);
}
.lb-pick:hover { background: rgba(255,255,255,.26); }
.lb-pick.on { background: var(--tool); color: #05060a; }
.lb-pick.on.cut { background: var(--red); color: #fff; }
.lb-nav {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 46px; height: 74px; border: none; border-radius: 12px; cursor: pointer;
    background: rgba(255,255,255,.1); color: #fff; font-size: 28px; line-height: 1;
    padding: 0; transition: background .14s var(--ease);
}
.lb-nav:hover:not(:disabled) { background: rgba(255,255,255,.24); }
.lb-nav:disabled { opacity: .22; cursor: default; }
.lb-nav.prev { left: 18px; }
.lb-nav.next { right: 18px; }
.lb-hint {
    position: absolute; bottom: 16px; left: 0; right: 0; text-align: center;
    font-size: 11px; color: rgba(255,255,255,.5); pointer-events: none;
}
@media (max-width: 700px) {
    .lb-nav { width: 38px; height: 58px; font-size: 22px; }
    .lb-nav.prev { left: 6px; }
    .lb-nav.next { right: 6px; }
    .lb-hint { display: none; }
}

/* Where the page came from, so a reordered grid can still be checked against
   the original. */
.page-src {
    position: absolute; left: 5px; bottom: 5px;
    font-family: var(--mono); font-size: 10px; font-weight: 700;
    background: rgba(5,6,10,.62); color: var(--ink); padding: 2px 5px; border-radius: 5px;
    opacity: 0; transition: opacity .14s var(--ease); pointer-events: none;
}
.page-org:hover .page-src { opacity: 1; }

.org-bar {
    position: absolute; left: 0; right: 0; bottom: 0; display: flex;
    justify-content: center; gap: 3px; padding: 5px 4px;
    background: linear-gradient(to top, rgba(5,6,10,.85), rgba(5,6,10,0));
    opacity: 0; transform: translateY(4px);
    transition: opacity .14s var(--ease), transform .16s var(--ease);
}
.org-bar-top { bottom: auto; top: 0; padding: 4px;
    background: linear-gradient(to bottom, rgba(5,6,10,.85), rgba(5,6,10,0)); }
.page-org:hover .org-bar, .page-org:focus-within .org-bar { opacity: 1; transform: none; }
@media (hover: none) { .org-bar { opacity: 1; transform: none; } }

.org-btn {
    width: 24px; height: 24px; padding: 0; border: none; border-radius: 6px;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    background: rgba(255,255,255,.92); color: #10131c;
    transition: background .13s var(--ease), color .13s var(--ease), transform .13s var(--ease);
}
.org-btn svg { width: 13px; height: 13px; }
.org-btn:hover { background: var(--tool); color: #05060a; transform: scale(1.1); }
.org-btn.danger:hover { background: var(--red); color: #fff; }

/* ── Options ──────────────────────────────────────────────────────────────── */
.opt-grid { display: grid; gap: 9px; }
.radio {
    display: flex; gap: 10px; align-items: flex-start; padding: 12px;
    border: 1.5px solid var(--line); border-radius: var(--r-sm); cursor: pointer;
    transition: border-color .15s var(--ease), background .15s var(--ease);
}
.radio:hover { border-color: var(--line-2); background: var(--surface-2); }
.radio.on { border-color: var(--tool); background: color-mix(in srgb, var(--tool) 9%, transparent); }
.radio-dot {
    width: 15px; height: 15px; border-radius: 50%; flex: 0 0 15px; margin-top: 1px;
    border: 2px solid var(--line-2); display: grid; place-items: center;
}
.radio.on .radio-dot { border-color: var(--tool); }
.radio.on .radio-dot::after {
    content: ''; width: 7px; height: 7px; border-radius: 50%;
    background: var(--tool); box-shadow: 0 0 8px var(--tool);
}
.radio-t { font-size: 14px; font-weight: 700; }
.radio-d { margin-top: 3px; font-size: 12.5px; line-height: 1.5; color: var(--ink-3); }

.field { margin-bottom: 13px; }
.field label {
    display: block; font-family: var(--mono); font-size: 11.5px; letter-spacing: .9px;
    text-transform: uppercase; color: var(--ink-3); margin-bottom: 6px;
}
.input, select.input {
    width: 100%; padding: 10px 12px; border-radius: var(--r-sm);
    border: 1px solid var(--line-2); background: var(--bg-2); color: var(--ink);
    font-family: var(--font); font-size: 14.5px; outline: none;
    transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.input:focus { border-color: var(--tool); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tool) 18%, transparent); }
.range { width: 100%; accent-color: var(--tool); }

/* ── Number with steps, and a slider under it ─────────────────────────────── */
/* Three controls, one value. The row is a grid so the slider can span the full
   width beneath the box without a second wrapper. */
.step { display: grid; grid-template-columns: 34px 1fr 34px; gap: 8px; align-items: center; }
.step .step-r { grid-column: 1 / -1; margin-top: 2px; }
.step-b {
    height: 34px; padding: 0; cursor: pointer;
    border-radius: var(--r-sm); border: 1px solid var(--line-2);
    background: var(--bg-2); color: var(--ink);
    font-size: 17px; font-weight: 700; line-height: 1;
    transition: border-color .15s var(--ease), color .15s var(--ease);
}
.step-b:hover:not(:disabled) { border-color: var(--tool); color: var(--tool); }
/* Keyboard focus wears the tool's colour like every other control here; the
   browser's default ring is blue, which on this panel reads as a fourth accent
   nobody chose. */
.step-b:focus-visible {
    outline: none; border-color: var(--tool);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--tool) 18%, transparent);
}
/* Dimmed rather than hidden at the ends of the range: a control that vanishes
   moves the two next to it, and the one you were aiming at is no longer there. */
.step-b:disabled { opacity: .38; cursor: default; }
/* The unit belongs to the number, so it sits inside the same box — "15 %" as
   one field reads as a quantity, where a % floating alongside reads as a third
   control. */
.step-f {
    display: flex; align-items: center; gap: 3px;
    padding: 0 10px; height: 34px;
    border-radius: var(--r-sm); border: 1px solid var(--line-2); background: var(--bg-2);
    transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.step-f:focus-within { border-color: var(--tool); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tool) 18%, transparent); }
.step-f i { font-style: normal; font-family: var(--mono); font-size: 12px; color: var(--ink-3); }
.step-in {
    flex: 1; min-width: 0; width: 100%;
    border: 0; background: none; outline: none; padding: 0;
    color: var(--ink); font-family: var(--mono); font-size: 14px;
    text-align: right; font-variant-numeric: tabular-nums;
}
/* The browser's own spinners are tiny, they only appear on hover, and there are
   already two buttons here doing that job properly. */
.step-in::-webkit-outer-spin-button, .step-in::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.step-in { -moz-appearance: textfield; appearance: textfield; }
.blurb { font-size: 13.5px; line-height: 1.65; color: var(--ink-2); margin-bottom: 16px; }

/* ── Progress ─────────────────────────────────────────────────────────────── */
.prog { margin-bottom: 14px; }
.prog-bar { height: 6px; border-radius: 4px; background: var(--line); overflow: hidden; }
.prog-fill {
    height: 100%; width: 0%; border-radius: 4px;
    background: linear-gradient(90deg, var(--tool), var(--tool-2));
    box-shadow: 0 0 14px var(--tool); transition: width .25s var(--ease);
}
/* The label and the way out sit on one line: a long run is exactly when
   someone wants to know both what it is doing and how to stop it. */
/* The label must never wrap. It changes on every step — "Starting", then
   "Reading page 3 of 4" — and in a narrow sidebar the longer strings wrapped
   to a second line, growing the row and pushing Stop down by a line, then back
   up on the next update. A control that jumps while you are reaching for it is
   a control you miss. Truncating keeps the row one line tall for every
   message, so the button holds its position for the whole run. */
.prog-foot { display: flex; align-items: center; gap: 12px; margin-top: 8px; min-height: 29px; }
.prog-label {
    font-family: var(--mono); font-size: 12.5px; color: var(--ink-3);
    flex: 1; min-width: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.prog-stop { flex: 0 0 auto; padding: 5px 12px; }
.prog-stop:hover { border-color: var(--red); color: var(--red); box-shadow: 0 0 0 1px var(--red); }

/* ── Result ───────────────────────────────────────────────────────────────── */
.result { border-radius: var(--r-sm); border: 1px solid var(--line); padding: 15px; margin-bottom: 13px; }
.result.good { border-color: color-mix(in srgb, var(--lime) 40%, transparent); background: color-mix(in srgb, var(--lime) 7%, transparent); }
.result.warn { border-color: color-mix(in srgb, var(--amber) 40%, transparent); background: color-mix(in srgb, var(--amber) 7%, transparent); }
.result.bad  { border-color: color-mix(in srgb, var(--red) 40%, transparent);  background: color-mix(in srgb, var(--red) 7%, transparent); }
.result-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.result-big { font-size: 20px; font-weight: 800; letter-spacing: -.5px; font-variant-numeric: tabular-nums; }
.result-pill {
    font-family: var(--mono); font-size: 12px; font-weight: 700; padding: 3px 9px;
    border-radius: 999px; background: var(--lime); color: #04060d;
}
.result.bad .result-pill { background: var(--red); color: #fff; }
.result-msg { margin-top: 9px; font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
/* One line, always. A download button is a fixed-size control, not a place to
   render a filename — a long one wrapped to three lines and was clipped by the
   result panel. The name is shortened in the middle by the script that builds
   this; the ellipsis here is the backstop. */
.result .btn {
    width: 100%; margin-top: 13px;
    display: block; text-align: center;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Carry the result on ──────────────────────────────────────────────────── */
/* Sits below the result rather than inside it: the download is the answer to
   what was asked, and this is a separate offer. Each button wears its own
   tool's accent, so the colour someone will meet on the next screen is the
   colour they pressed to get there. */
.chain { margin: -2px 0 13px; }
.chain-h {
    font-size: 12px; font-weight: 700; color: var(--ink-3);
    letter-spacing: .2px; margin-bottom: 8px;
}
.chain-row { display: flex; flex-wrap: wrap; gap: 7px; }
.chain-btn {
    display: inline-flex; align-items: center; gap: 7px;
    font-family: var(--font); font-size: 13px; font-weight: 700;
    padding: 7px 11px; border-radius: var(--r-sm);
    border: 1px solid var(--line-2); background: var(--surface-2); color: var(--ink-2);
    cursor: pointer;
    transition: border-color .16s var(--ease), color .16s var(--ease),
                box-shadow .16s var(--ease), transform .12s var(--ease);
}
.chain-btn:hover { color: var(--ink); border-color: var(--tool); box-shadow: 0 0 0 1px var(--tool), 0 0 20px -8px var(--tool); }
.chain-btn:active { transform: translateY(1px); }
.chain-ico { display: grid; place-items: center; width: 15px; height: 15px; color: var(--tool-v); }
.chain-ico svg { width: 15px; height: 15px; }

.diag { margin-top: 10px; }
.diag summary { font-size: 12px; font-weight: 700; color: var(--ink-3); cursor: pointer; list-style: none; }
.diag summary::-webkit-details-marker { display: none; }
.diag summary::before { content: '▸ '; }
.diag[open] summary::before { content: '▾ '; }
.diag pre {
    margin: 8px 0 0; padding: 9px 10px; border-radius: 8px;
    background: rgba(0,0,0,.35); border: 1px solid var(--line);
    font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--ink-3);
    white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow: auto;
}

/* ── Motion ───────────────────────────────────────────────────────────────
   Arrival, not idle. Everything here happens once when something appears and
   then stops; the backdrop above is the only thing that moves on its own.

   Everything is `backwards`, never `forwards`. A forwards fill leaves the last
   keyframe applied at a weight that outranks :hover, so a card animating in
   with `transform: none` could never be lifted by its hover rule again. */
@keyframes pj-rise {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}
@keyframes pj-fade { from { opacity: 0; } to { opacity: 1; } }

/* The home page's own entrance lives with the intro further down, gated on
   .pj-landed, because it must not play until the plane has landed. Everything
   below is motion that belongs to using the site rather than to arriving at
   it, so it stays ungated. */

/* The workspace fades rather than rises. Deliberately: the right-hand column
   holds every control for the tool — Run, Stop, the option radios — and a rise
   keeps them travelling for 420ms after arrival. Someone who knows where the
   button is and goes straight for it is reaching for a moving target. Measured
   Stop drifting a pixel a frame while a run was already under way, which is
   how it was found. Opacity moves nothing. */
.ws.on .ws-top          { animation: pj-fade .34s var(--ease) backwards; }
.ws.on .ws-body > div   { animation: pj-fade .4s var(--ease) 40ms backwards; }
.ws.on .ws-body > aside { animation: pj-fade .4s var(--ease) 90ms backwards; }

/* A result is the answer to the question that was asked; it should land. */
.result { animation: pj-rise .34s var(--ease) backwards; }
.chain  { animation: pj-fade .4s var(--ease) 120ms backwards; }
.page:not(.page-skel) canvas { animation: pj-fade .3s var(--ease) backwards; }

/* Short on purpose: while the sheet slides, every tap target inside it is
   moving, and a finger already on its way down does not wait. */
.mnav { animation: pj-fade .15s var(--ease) backwards; }
.mnav-sheet { animation: pj-slide .15s var(--ease) backwards; }
@keyframes pj-slide { from { transform: translateY(-5px); } to { transform: none; } }

/* ── Intro page ───────────────────────────────────────────────────────────
   A screen of its own that hands over to the site. The plane leaves the mark,
   flies ONE TRUE CIRCLE around everything, and re-enters the mark; the moment
   it lands the intro lifts and the home page arrives underneath.

   THE PATH IS A REAL CIRCLE. Two SVG arc commands, not four cubics pretending.
   It can be written here as a constant because the flight happens inside a
   fixed 560x560 stage rather than against the viewport — which is also why it
   needs no measuring in JavaScript and is identical on every screen. Read it
   as: leave the hub, curve out to the top of the orbit arriving level, go all
   the way round, then curve back down into the hub.

   Both cubics meet the circle heading due right, matching the tangent at the
   top of a clockwise orbit, so the plane never kinks at the joins — with
   offset-rotate following the tangent, a discontinuity there is a visible snap
   of the nose. */
/* Not a centring grid. The stage is a fixed 560px, which is wider than a phone,
   and a grid item that overflows its track is aligned to the START rather than
   centred — so on a 390px screen the whole stage sat flush left and the wordmark
   ran off the right edge. The stage centres itself with margins below instead,
   which overflows symmetrically and leaves transform free for the animation. */
.pj-intro {
    position: fixed; inset: 0; z-index: 300;
    overflow: hidden;
    background: var(--bg);
    opacity: 1;
    transition: opacity .5s var(--ease), transform .5s var(--ease);
}
/* Lifts rather than dissolves, so the home page reads as arriving from below
   rather than the intro simply vanishing. */
.pj-intro.pj-lift { opacity: 0; transform: translateY(-14px); }
.pj-intro.pj-cut  { transition-duration: .18s; }

/* Centred on margins, not on transform: the entrance animation owns transform,
   and an animation with `both` beats a normal declaration — a translate here
   would be thrown away the moment the animation filled. */
.pj-orbit {
    --pj-scale: 1;
    position: absolute; left: 50%; top: 50%;
    margin: -280px 0 0 -280px;
    width: 560px; height: 560px;
    animation: pj-stage-in .5s var(--ease) both;
}
/* The responsive scale rides INSIDE the keyframes for the same reason. It used
   to be a plain `transform: scale(.62)` in a media query, which the animation's
   own `transform: none` overrode — so phones silently got the full 560px stage. */
@keyframes pj-stage-in {
    from { opacity: 0; transform: scale(calc(var(--pj-scale) * .94)); }
    to   { opacity: 1; transform: scale(var(--pj-scale)); }
}

/* The page the plane draws. Nothing is here at the start; the outline appears
   stroke by stroke behind the plane, so the sheet is made by the flight rather
   than revealed by it. dasharray and the timing are set from JavaScript, which
   is the only place the path's true LENGTH is known. */
.pj-sheet {
    position: absolute; inset: 0; width: 100%; height: 100%;
    overflow: visible; pointer-events: none;
}
.pj-sheet path {
    fill: none;
    stroke: color-mix(in srgb, var(--ink) 26%, transparent);
    stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.pj-sheet-fold { opacity: 0; }
@keyframes pj-draw { to { stroke-dashoffset: 0; } }

/* ── The hub: mark, wordmark, and what this is ──────────────────────────── */
.pj-hub { position: absolute; inset: 0; }
/* Centred with margins rather than a translate, so the pop-in keyframes below
   are free to use transform without having to carry the centring too. */
.pj-hub-mark {
    position: absolute; left: 50%; top: 50%; margin: -37px 0 0 -37px;
    width: 74px; height: 74px; border-radius: 20px;
    display: grid; place-items: center;
    background: linear-gradient(135deg, var(--brand-1), var(--brand-2));
    box-shadow: 0 2px 6px rgba(0,0,0,.3), inset 0 0 0 1px rgba(255,255,255,.14);
    animation: pj-hub-pop .52s cubic-bezier(.34,1.56,.64,1) both;
}
.pj-hub-mark svg { width: 50px; height: 50px; display: block; }
@keyframes pj-hub-pop {
    from { opacity: 0; transform: scale(.5) rotate(-14deg); }
    to   { opacity: 1; transform: none; }
}
/* Empty while the plane is away — the whole point is that it is the same
   object, not a copy. Otherwise the tile keeps its dart and there are visibly
   two planes on screen at once.

   Timed off the flight rather than toggled from JavaScript, and `forwards`
   rather than `both`: a backwards fill would apply the 0% keyframe during the
   .45s delay and the mark would be empty before the plane had even left. */
/* Timed off the flight's own variables, set once in intro.js. The duration
   used to be written here AND there, which is a pair that drifts: retune one
   and the mark reappears while the plane is still out. */
.pj-hub-mark svg { animation: pj-mark-away var(--pj-dur, 4.2s) var(--pj-delay, .55s) forwards; }
@keyframes pj-mark-away {
    0%, 97% { opacity: 0; }
    100%    { opacity: 1; }
}

/* Out of flow, below the mark, so it cannot move the mark off centre — and
   narrow enough to sit INSIDE the sheet the plane draws. At full width the
   second line ran out past the page's left edge, which rather undermines a
   drawing of a page. */
.pj-hub-text {
    position: absolute; left: 50%; transform: translateX(-50%);
    top: calc(50% + 54px); width: 266px;
    text-align: center;
}
.pj-hub-word {
    font-size: 34px; font-weight: 800; letter-spacing: -1.1px;
    color: var(--ink);
    animation: pj-hub-rise .5s var(--ease) .18s both;
}
.pj-hub-word b { color: var(--brand-ink); font-weight: 800; }
.pj-hub-line {
    margin-top: 10px; font-family: var(--mono); font-size: 11px;
    letter-spacing: .3px; line-height: 1.5; color: var(--ink-2);
    animation: pj-hub-rise .5s var(--ease) .3s both;
}
.pj-hub-sub {
    margin-top: 6px; font-size: 12.5px; line-height: 1.5; color: var(--ink-3);
    animation: pj-hub-rise .5s var(--ease) .4s both;
}
@keyframes pj-hub-rise {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* ── The flight ─────────────────────────────────────────────────────────── */
/* The outer element does ONE thing: follow the path. `scale` on this element
   composes with the motion transform and puts the plane off its own path —
   measured, not guessed — so everything visual happens on .pj-plane-in. */
.pj-plane {
    position: absolute; top: 0; left: 0;
    width: 62px; height: 62px;
    offset-path: path("M 280 280 C 360 258, 196 80, 280 80 A 200 200 0 1 1 280 480 A 200 200 0 1 1 280 80 C 364 80, 300 214, 280 280");
    -webkit-offset-path: path("M 280 280 C 360 258, 196 80, 280 80 A 200 200 0 1 1 280 480 A 200 200 0 1 1 280 80 C 364 80, 300 214, 280 280");
    offset-rotate: auto;
    -webkit-offset-rotate: auto;
    offset-distance: 0%;
    /* Linear on purpose. With constant speed the time fraction equals the
       distance fraction, which is what lets the outline be drawn in step with
       the plane using nothing but a delay and a duration. An eased flight
       would need that curve inverted to stay in sync. */
    animation: pj-fly var(--pj-dur, 4.2s) linear var(--pj-delay, .55s) both;
    will-change: offset-distance;
}
@keyframes pj-fly {
    from { offset-distance: 0%; }
    to   { offset-distance: 100%; }
}

/* Scales up as it climbs away from the mark and back down as it comes home, so
   the orbit reads as distance rather than as a sprite sliding round a disc. */
.pj-plane-in {
    width: 100%; height: 100%;
    animation: pj-fly-scale var(--pj-dur, 4.2s) linear var(--pj-delay, .55s) both;
    transform: translateZ(0);
}
.pj-plane-in svg { width: 100%; height: 100%; display: block; }
/* One clean outline, from the stylesheet so it flips with the theme. An
   outline exists to separate the shape from what is behind it, so it is a
   solid ink and never a gradient. vector-effect holds it at 2.2px for the
   whole flight, so the edge never thins as the plane scales. */
/* White, exactly as it is inside the mark, with the fold shown by a shaded
   second face — the same relationship the logo uses, where that face is white
   at 58% over the tile. It was briefly cyan and magenta, which made it a
   different object the moment it left the logo. A dark outline carries it on
   both themes: white on white needs an edge, and white on near-black does not
   mind having one. */
.pj-face-a { fill: #ffffff; }   /* the lit face */
.pj-face-b { fill: #c9d2e2; }   /* the fold     */
.pj-plane-in svg g { stroke: rgba(14,22,38,.55); }
:root[data-theme="dark"] .pj-plane-in svg g { stroke: rgba(8,14,26,.62); }

@keyframes pj-fly-scale {
    0%   { transform: scale(.26); opacity: 0; }
    8%   { transform: scale(.9);  opacity: 1; }
    16%  { transform: scale(1);   opacity: 1; }
    86%  { transform: scale(1);   opacity: 1; }
    95%  { transform: scale(.62); opacity: 1; }
    100% { transform: scale(.26); opacity: 0; }
}

/* The trail: same path, a beat BEHIND — positive delay, since a negative one
   starts the animation partway in and puts the "trail" ahead of the plane. */
/* The ghost trail is gone — see intro.js for why. */

/* The stage is a fixed 560px so the path can be a constant; small screens
   scale the whole thing rather than recomputing the page.

   The scale is set as a variable, never as a transform — the keyframes above
   read it. The numbers are chosen against what is actually VISIBLE rather than
   against the 560px box: the drawn sheet plus the plane's own width span about
   362×442 in stage units, so a 390px phone needs only a nudge, not the .62 that
   was written here (and never applied), which would have taken the 12.5px line
   of type down to under 8px. */
@media (max-width: 430px) { .pj-orbit { --pj-scale: .9; } }
@media (max-width: 380px) { .pj-orbit { --pj-scale: .82; } }
@media (max-height: 700px) { .pj-orbit { --pj-scale: .82; } }
@media (max-height: 560px) { .pj-orbit { --pj-scale: .68; } }

/* ── The hand-off ─────────────────────────────────────────────────────────
   The home page's entrance is gated on .pj-landed, set when the plane touches
   down, so the hero arrives AFTER the flight instead of having played out
   underneath it. .pj-boot is set by an inline script in the head and hides the
   page for the few frames before the intro is built, so there is no flash of
   the hero first; it clears on landing, and on a 5s failsafe in that script so
   a broken build can never leave the page blank. */
.pj-boot .hdr, .pj-boot main { opacity: 0; }
.pj-landed .hero .kicker { animation: pj-fade .5s var(--ease) backwards; }
.pj-landed .hero h1      { animation: pj-rise .62s var(--ease) 60ms backwards; }
.pj-landed .hero p       { animation: pj-rise .62s var(--ease) 150ms backwards; }
.pj-landed .filters      { animation: pj-fade .5s var(--ease) 200ms backwards; }
.pj-landed .hdr          { animation: pj-fade .5s var(--ease) backwards; }
.pj-landed .tool-card {
    animation: pj-rise .46s var(--ease) backwards;
    animation-delay: calc(min(var(--i, 0), 12) * 26ms);
}

@media (prefers-reduced-motion: reduce) { .pj-intro { display: none; } }

/* ── Toast ────────────────────────────────────────────────────────────────── */
.toast {
    position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 90px);
    z-index: 90; padding: 12px 20px; border-radius: 999px;
    background: var(--surface-2); border: 1px solid var(--line-2); color: var(--ink);
    font-size: 14px; font-weight: 600; box-shadow: 0 14px 40px -12px #000;
    opacity: 0; transition: transform .3s var(--bounce), opacity .3s var(--ease);
    pointer-events: none; max-width: 90vw; text-align: center;
}
.toast.on { transform: translate(-50%, 0); opacity: 1; }
.toast.err { border-color: var(--red); color: #ffd6da; }
.toast.ok  { border-color: var(--lime); color: #e8ffc2; }

/* ── Modal (auth / email) ─────────────────────────────────────────────────── */
.modal {
    position: fixed; inset: 0; z-index: 100; display: none;
    place-items: center; padding: 20px;
    background: rgba(3,4,8,.78); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.modal.on { display: grid; }
.modal-card {
    width: 100%; max-width: 400px; border-radius: var(--r); padding: 26px;
    background: linear-gradient(180deg, var(--surface), var(--bg-2));
    border: 1px solid var(--line-2);
    box-shadow: 0 30px 90px -30px #000, 0 0 60px -30px var(--cyan);
}
.modal-card h2 { margin: 0 0 6px; font-size: 22px; letter-spacing: -.6px; }
.modal-card .sub { margin: 0 0 20px; font-size: 14px; line-height: 1.6; color: var(--ink-2); }
.modal-x { float: right; border: none; background: none; color: var(--ink-3); font-size: 23px; cursor: pointer; line-height: 1; }
.modal-x:hover { color: var(--ink); }
.consent { display: flex; gap: 9px; align-items: flex-start; margin: 14px 0 4px; font-size: 12.5px; line-height: 1.55; color: var(--ink-3); }
.consent input { margin-top: 2px; accent-color: var(--cyan); flex: 0 0 auto; }
.modal-alt { margin-top: 15px; font-size: 13.5px; color: var(--ink-3); text-align: center; }
.modal-alt button { background: none; border: none; color: var(--cyan); cursor: pointer; font-family: var(--font); font-size: 13.5px; font-weight: 700; padding: 0; }

/* ── Header nav ───────────────────────────────────────────────────────────── */
/* The four tools people arrive already looking for, plus the way back. Hidden
   on narrow screens, where the grid is one scroll away anyway. */
.hdr-nav { display: flex; align-items: center; gap: 4px; margin-left: 8px; }
.hdr-nav a {
    padding: 7px 11px; border-radius: 8px; text-decoration: none;
    font-size: 13.5px; font-weight: 700; color: var(--ink-2);
    transition: background .14s var(--ease), color .14s var(--ease);
}
.hdr-nav a:hover { background: var(--surface-2); color: var(--ink); }
.hdr-nav .hdr-all { color: var(--ink-3); }

/* ── Mobile navigation ────────────────────────────────────────────────────── */
/* The comment above used to say the grid is "one scroll away anyway". That is
   true on the home page and false everywhere else: from inside a tool the grid
   is a different view, so hiding the nav left four tools reachable out of
   nineteen. The menu button is what replaces it. */
.hdr-menu {
    display: none; flex: 0 0 auto;
    width: 38px; height: 38px; padding: 0; border-radius: var(--r-sm);
    align-items: center; justify-content: center;
    border: 1px solid var(--line-2); background: var(--surface-2); color: var(--ink);
    cursor: pointer;
}
.hdr-menu svg { width: 19px; height: 19px; }
.hdr-menu:hover { border-color: var(--tool); }

.mnav {
    position: fixed; inset: 0; z-index: 60;
    background: rgba(4,6,12,.55);
    -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
    overflow-y: auto; overscroll-behavior: contain;
}
.mnav-sheet { background: var(--surface); border-bottom: 1px solid var(--line); }
/* Sticky, because the list is longer than the screen and the way out must not
   scroll off the top of it. */
.mnav-bar {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: center; justify-content: space-between;
    padding: 13px 20px; border-bottom: 1px solid var(--line);
    background: var(--surface);
    font-family: var(--mono); font-size: 11px; font-weight: 700;
    letter-spacing: .8px; text-transform: uppercase; color: var(--ink-3);
}
.mnav-x {
    width: 34px; height: 34px; margin: -6px -8px -6px 0; padding: 0;
    display: grid; place-items: center;
    border: none; background: none; color: var(--ink-2);
    font-size: 26px; line-height: 1; cursor: pointer; border-radius: 8px;
}
.mnav-x:hover { background: var(--surface-2); color: var(--ink); }
.mnav-panel {
    padding: 4px 20px 22px;
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 18px;
}
.mnav-col { min-width: 0; }
.mnav-col h3 {
    font-family: var(--mono); font-size: 11px; font-weight: 700;
    letter-spacing: .8px; text-transform: uppercase; color: var(--ink-3);
    margin: 12px 0 4px;
}
.mnav-col a {
    display: flex; align-items: center; gap: 9px;
    padding: 9px 8px; margin: 0 -8px; border-radius: 9px;
    text-decoration: none; color: var(--ink-2);
    font-size: 14.5px; font-weight: 600;
}
.mnav-col a:hover, .mnav-col a:focus-visible { background: var(--surface-2); color: var(--ink); }
.mnav-ico { display: grid; place-items: center; width: 17px; height: 17px; flex: 0 0 17px; color: var(--ink-3); }
.mnav-ico svg { width: 17px; height: 17px; }

/* The page behind must not scroll while the sheet is open. */
body.menu-on { overflow: hidden; }

@media (max-width: 900px) {
    .hdr-nav { display: none; }
    .hdr-menu { display: inline-flex; }
    /* Four controls plus a 26-character badge do not fit a 360px header, and
       the same promise is made far more prominently in the hero. */
    .hdr-safe { display: none; }
    .hdr { gap: 10px; padding: 12px 14px; }
}
@media (max-width: 420px) {
    .mnav-panel { grid-template-columns: minmax(0, 1fr); }
}

/* ── Category chips ───────────────────────────────────────────────────────── */
.filters { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 0 2px; }
.chip {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 8px 15px; border-radius: 999px; cursor: pointer;
    font-family: var(--font); font-size: 13.5px; font-weight: 700;
    background: transparent; color: var(--ink-2);
    border: 1px solid var(--line);
    transition: border-color .15s var(--ease), color .15s var(--ease),
                background .15s var(--ease);
}
.chip span { font-size: 11.5px; font-weight: 700; color: var(--ink-3); }
.chip:hover { border-color: var(--line-2); color: var(--ink); }
.chip.on { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.chip.on span { color: var(--bg); opacity: .6; }

/* ── Trust ────────────────────────────────────────────────────────────────── */
/* Not a badge row. Every one of these sites shows badges; the difference here
   is a thing the reader can do, so the layout gives that the emphasis. */
/* A rule, because the page now has two halves: the tools you came for, and the
   case for them. The line is where one ends and the other starts. */
.trust { padding: 44px 0 10px; border-top: 1px solid var(--line); }
.trust h2, .faq h2 {
    margin: 0 0 10px; font-size: clamp(24px, 3.4vw, 32px);
    font-weight: 800; letter-spacing: -1px;
}
.trust-lead { margin: 0 0 20px; max-width: 68ch; font-size: 15px; line-height: 1.65; color: var(--ink-2); }
.trust-proof {
    display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
    padding: 16px 18px; border-radius: var(--r-sm);
    background: var(--surface-2); border: 1px solid var(--line);
    font-size: 14.5px; line-height: 1.6; color: var(--ink);
}
.trust-proof-k {
    font-family: var(--mono); font-size: 11px; letter-spacing: 1.4px;
    text-transform: uppercase; color: var(--lime); flex: none;
}
.trust-grid {
    display: grid; gap: 14px; margin-top: 18px;
    grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
}
.trust-item {
    padding: 16px 16px 18px; border-radius: var(--r-sm);
    border: 1px solid var(--line); background: var(--surface);
    font-size: 13.5px; line-height: 1.6; color: var(--ink-2);
}
.trust-item b { display: block; margin-bottom: 5px; font-size: 14.5px; color: var(--ink); }

/* ── FAQ ──────────────────────────────────────────────────────────────────── */
.faq { padding: 42px 0 10px; max-width: 80ch; }
.faq details {
    border-bottom: 1px solid var(--line); padding: 2px 0;
}
.faq summary {
    cursor: pointer; padding: 15px 0; font-size: 15.5px; font-weight: 700;
    list-style: none; display: flex; align-items: center; gap: 10px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::before {
    content: '+'; font-family: var(--mono); font-size: 17px; color: var(--ink-3);
    width: 14px; flex: none; transition: transform .18s var(--ease), color .18s var(--ease);
}
.faq details[open] summary::before { content: '−'; color: var(--tool); }
.faq summary:hover { color: var(--tool); }
.faq p { margin: 0 0 16px 24px; font-size: 14.5px; line-height: 1.65; color: var(--ink-2); }

/* ── Per-tool prose ───────────────────────────────────────────────────────────
   Sits under the workspace on a tool page: the visitor came to merge a file,
   so the tool is first and the reading matter is what they scroll past it to.
   Reuses the .faq details styling rather than restating it. */
.tseo { padding: 46px 0 6px; border-top: 1px solid var(--line); margin-top: 40px; }
.tseo-main { max-width: 80ch; }
.tseo h1 {
    margin: 0 0 12px; font-size: clamp(26px, 3.6vw, 34px);
    font-weight: 800; letter-spacing: -1px;
}
.tseo h2 {
    margin: 34px 0 12px; font-size: clamp(19px, 2.4vw, 23px);
    font-weight: 800; letter-spacing: -.5px;
}
.tseo-lead { margin: 0; font-size: 15.5px; line-height: 1.7; color: var(--ink-2); }
.tseo-steps { margin: 0; padding-left: 20px; display: grid; gap: 9px; }
.tseo-steps li { font-size: 14.5px; line-height: 1.65; color: var(--ink-2); padding-left: 4px; }
.tseo-steps li::marker { color: var(--ink-3); font-family: var(--mono); font-size: 13px; }
.tseo-notes {
    display: grid; gap: 14px; margin-top: 26px;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.tseo-note {
    padding: 16px 18px; border-radius: var(--r-sm);
    background: var(--surface-2); border: 1px solid var(--line);
}
.tseo-note b { display: block; margin-bottom: 6px; font-size: 14.5px; }
.tseo-note p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-2); }
.tseo-faq details { border-bottom: 1px solid var(--line); padding: 2px 0; }
.tseo-faq summary {
    cursor: pointer; padding: 15px 0; font-size: 15.5px; font-weight: 700;
    list-style: none; display: flex; align-items: center; gap: 10px;
}
.tseo-faq summary::-webkit-details-marker { display: none; }
.tseo-faq summary::before {
    content: '+'; font-family: var(--mono); font-size: 17px; color: var(--ink-3);
    width: 14px; flex: none; transition: transform .18s var(--ease), color .18s var(--ease);
}
.tseo-faq details[open] summary::before { content: '−'; color: var(--tool); }
.tseo-faq summary:hover { color: var(--tool); }
.tseo-faq p { margin: 0 0 16px 24px; font-size: 14.5px; line-height: 1.65; color: var(--ink-2); }

/* ── Footer ───────────────────────────────────────────────────────────────── */
.ftr { border-top: 1px solid var(--line); margin-top: 54px; padding: 34px 0 44px; }
.ftr-cols {
    display: grid; gap: 22px;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ftr-col { display: flex; flex-direction: column; gap: 9px; }
.ftr-col h3 {
    margin: 0 0 3px; font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase;
    font-family: var(--mono); color: var(--ink-3); font-weight: 700;
}
.ftr a { color: var(--ink-2); text-decoration: none; font-size: 13.5px; }
.ftr a:hover { color: var(--tool); }
.ftr-base {
    display: flex; gap: 16px; flex-wrap: wrap; justify-content: space-between;
    margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line);
    font-size: 12.5px; color: var(--ink-3);
}

.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ── Survey dialog ────────────────────────────────────────────────────────── */
.sv-row { display: flex; gap: 10px; margin-top: 18px; }
.sv-row .btn { flex: 1; }
.sv-two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.sv-dots { display: flex; gap: 6px; justify-content: center; margin-top: 16px; }
.sv-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--line-2); }
.sv-dots span.on { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }
.consent a { color: var(--cyan); }

/* ── Prose (privacy page) ─────────────────────────────────────────────────── */
.prose { max-width: 720px; margin: 0 auto; padding: 40px 0 70px; }
.prose h1 { font-size: clamp(30px, 5vw, 44px); letter-spacing: -1.2px; margin: 0 0 10px; }
.prose .lede { font-size: 17px; line-height: 1.7; color: var(--ink-2); margin: 0 0 8px; }
.prose .updated { font-family: var(--mono); font-size: 12.5px; color: var(--ink-3); margin-bottom: 30px; }
.prose h2 { font-size: 20px; letter-spacing: -.4px; margin: 34px 0 10px; }
.prose p, .prose li { font-size: 14.5px; line-height: 1.75; color: var(--ink-2); }
.prose ul { padding-left: 20px; }
.prose li { margin-bottom: 7px; }
.prose strong { color: var(--ink); }
.prose a { color: var(--cyan); }
.prose .callout {
    border: 1px solid color-mix(in srgb, var(--lime) 35%, transparent);
    background: color-mix(in srgb, var(--lime) 7%, transparent);
    border-radius: var(--r); padding: 18px 20px; margin: 26px 0;
}
.prose .callout p { color: var(--ink); margin: 0; }
.prose table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 14.5px; }
.prose th, .prose td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); color: var(--ink-2); }
.prose th { color: var(--ink); font-size: 13px; text-transform: uppercase; letter-spacing: .6px; font-family: var(--mono); }

/* ═══════════════════════════════════════════════════════════════════════════
   LIGHT THEME
   ═══════════════════════════════════════════════════════════════════════════
   Not the dark palette inverted. Two things have to change in kind, not just
   in value:

   1. NEON DOES NOT SURVIVE A WHITE BACKGROUND. #00e5ff on white is roughly
      1.7:1 — unreadable. Each accent gets a deeper twin here, chosen to stay
      recognisably the same hue while actually passing as text and borders.
   2. GLOW STOPS MEANING ANYTHING. A bloom against near-black reads as light;
      against white it reads as blur. So the glows are cut back hard and the
      work of showing "this is interactive" moves to borders and fills.

   Selection is explicit — data-theme is set on <html> from a stored choice,
   never from the operating system. A tool people open once a week should look
   the way they last left it, not the way their laptop happens to feel today.
   ═════════════════════════════════════════════════════════════════════════ */
:root[data-theme="light"] {
    --bg:        #f4f6fb;
    --bg-2:      #ffffff;
    --surface:   #ffffff;
    --surface-2: #eef1f8;
    --line:      #d7dde9;
    --line-2:    #c6cee1;

    --ink:   #0d1017;
    --ink-2: #454e63;
    --ink-3: #5a6379;   /* 5.31:1 on --surface-2 — was 4.14:1 */

    /* Measured against --bg (#f4f6fb), not eyeballed. Each of these is used as
       small text somewhere — the hero kicker, prose links, the modal's text
       button — so each clears 4.5:1 rather than the 3:1 that would do for a
       border or an icon. The first pass used lighter twins that looked right
       and measured 3.79:1 on cyan. */
    --cyan:    #00758d;   /* 4.94:1 */
    --magenta: #c30f61;   /* 5.45:1 */
    --lime:    #456f00;   /* 5.51:1 */
    --amber:   #8f5a00;   /* 5.35:1 */
    --violet:  #5f2cc4;   /* 7.35:1 */
    --red:     #c11f30;   /* 5.54:1 */

    /* The colours you actually SEE on this theme.
       Darkening every accent until it passed as small text is what made the
       light theme look dull: a 14% wash of a 5:1 teal is barely a colour at
       all, and the tool cards came out as grey chips with grey glyphs. These
       twins carry the colour instead, and are used only where the rule is 3:1
       rather than 4.5:1 — icon chips, hover glows, borders, selection rings.
       Each is the most saturated version that still clears 3:1 against its own
       18% chip: measured, not eyeballed. Text keeps the darker set above. */
    /* The tile keeps the brand gradient on both themes; only the wordmark
       ink moves, and only because neon cyan cannot be read on white. */
    --brand-ink: #0090b0;
    --cyan-v:    #0090b0;
    --magenta-v: #f5137a;
    --lime-v:    #6cae00;
    --amber-v:   #e89600;
    --violet-v:  #8b4dff;
    --red-v:     #f52d45;

    /* The ink that goes ON a solid chip of the colour above. Four of them take
       white; amber and lime do not. Forcing white onto those two meant
       darkening them until the orange was brown and the green was olive —
       which is what "dull" looked like on this page. Given a dark glyph they
       can be as bright as they want: amber reads 7.29:1, lime 5.98:1, both
       better than the white-on-dark versions they replace. */
    --cyan-on:    #fff;
    --magenta-on: #fff;
    --lime-on:    #14240a;
    --amber-on:   #241800;
    --violet-on:  #fff;
    --red-on:     #fff;
}

/* The backdrop: same construction, far gentler. The scanlines go entirely —
   on white they are dirt rather than texture. */
:root[data-theme="light"] .bg {
    background:
        radial-gradient(900px 600px at 12% -10%, rgba(0,137,166,.10), transparent 60%),
        radial-gradient(800px 600px at 92% 8%, rgba(212,18,106,.07), transparent 60%),
        var(--bg);
}
:root[data-theme="light"] .bg::before {
    background-image:
        linear-gradient(rgba(30,60,120,.07) 1px, transparent 1px),
        linear-gradient(90deg, rgba(30,60,120,.07) 1px, transparent 1px);
}
:root[data-theme="light"] .bg::after { display: none; }

/* On a near-black page a 18% tint of a neon IS a colour — it glows. On white
   it is a pastel, and eleven pastel chips on eleven white cards is the whole
   reason this theme read as dull. So here the chip carries the colour solid
   and the glyph goes white: same icon, roughly five times the coloured area,
   and white-on-vivid measures 3.7–4.6:1 across the palette, which is more
   contrast than the tinted version had. */
:root[data-theme="light"] .tool-ico {
    background: var(--tool-v);
    border-color: transparent;
    color: var(--tool-on);
    box-shadow: 0 4px 12px -4px color-mix(in srgb, var(--tool-v) 55%, transparent);
}
/* A format mark is already a solid coloured sheet; putting it on a solid chip
   would be a colour inside a colour. It keeps the neutral tile. */
:root[data-theme="light"] .tool-ico.brand {
    background: var(--surface-2);
    border-color: var(--line);
    color: inherit;
    box-shadow: none;
}

:root[data-theme="light"] .hdr { background: rgba(244,246,251,.8); }

/* Cards are flat white on a tinted page, so they need a real edge and a soft
   lift instead of the dark theme's gradient-into-black. */
:root[data-theme="light"] .tool-card,
:root[data-theme="light"] .panel,
:root[data-theme="light"] .modal-card {
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(16,24,48,.05), 0 6px 18px -12px rgba(16,24,48,.22);
}
:root[data-theme="light"] .tool-card:hover {
    box-shadow: 0 10px 30px -14px color-mix(in srgb, var(--tool) 60%, transparent),
                0 1px 2px rgba(16,24,48,.06);
}

/* Glow, dialled back to a hint. */
:root[data-theme="light"] .btn-primary { box-shadow: 0 4px 14px -6px var(--tool); }
:root[data-theme="light"] .btn-primary:hover { box-shadow: 0 6px 18px -6px var(--tool); }
:root[data-theme="light"] .btn:hover { box-shadow: 0 0 0 1px var(--tool); }
:root[data-theme="light"] .logo-mark {
    box-shadow: 0 1px 2px rgba(16,24,48,.22), inset 0 0 0 1px rgba(255,255,255,.28);
}
:root[data-theme="light"] .prog-fill { box-shadow: none; }
:root[data-theme="light"] .hdr-safe .dot { box-shadow: 0 0 5px var(--lime); }
:root[data-theme="light"] .radio.on .radio-dot::after { box-shadow: none; }
:root[data-theme="light"] .sv-dots span.on { box-shadow: none; }
:root[data-theme="light"] .drop.over {
    box-shadow: inset 0 0 30px -18px var(--tool), 0 0 0 4px color-mix(in srgb, var(--tool) 16%, transparent);
}
:root[data-theme="light"] .toast { box-shadow: 0 12px 34px -14px rgba(16,24,48,.4); }
:root[data-theme="light"] .modal { background: rgba(226,232,244,.72); }
:root[data-theme="light"] .drop { background: var(--surface-2); }
:root[data-theme="light"] .page-n { background: rgba(255,255,255,.9); color: #10131c; border: 1px solid var(--line); }
:root[data-theme="light"] .diag pre { background: var(--surface-2); }
:root[data-theme="light"] .page-skel { background: linear-gradient(100deg, #eef1f8 30%, #e3e8f2 50%, #eef1f8 70%); }
/* The organize bars are a scrim over a white page, so they stay dark in both
   themes — but the origin chip sits on the page itself and has to flip. */
:root[data-theme="light"] .page-src { background: rgba(255,255,255,.92); color: #10131c;
    border: 1px solid var(--line); }
:root[data-theme="light"] .org-btn { background: rgba(255,255,255,.96); }

/* ── Theme toggle ─────────────────────────────────────────────────────────── */
.theme-btn {
    display: inline-grid; place-items: center;
    width: 34px; height: 34px; flex: 0 0 34px; padding: 0;
    border-radius: 9px; border: 1px solid var(--line-2);
    background: var(--surface-2); color: var(--ink-2); cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.theme-btn:hover { color: var(--cyan); border-color: var(--cyan); }
.theme-btn svg { width: 16px; height: 16px; }
/* One icon per theme, and only the relevant one is in the box: in the dark
   theme you are offered the sun, in the light theme the moon. */
.theme-btn .i-sun { display: none; }
:root[data-theme="light"] .theme-btn .i-sun { display: block; }
:root[data-theme="light"] .theme-btn .i-moon { display: none; }

/* "by ACSPL" in the header — quiet, but it answers "who made this?", which is
   the first thing anyone asks before handing a document to a website. */
.hdr-by { font-family: var(--mono); font-size: 12px; letter-spacing: .3px;
    color: var(--ink-3); text-decoration: none; white-space: nowrap; }
.hdr-by b { color: var(--ink-2); font-weight: 700; }
.hdr-by:hover { color: var(--cyan); }
.hdr-by:hover b { color: var(--cyan); }
.ftr a { text-decoration: underline; text-underline-offset: 2px; }

/* Rotate preview — the page turns to show what you will get. */
.page canvas, .page img { transition: transform .3s var(--ease); }
