/* Deal With It -- the whole stylesheet.

   Two pages share this file: the app (/) and the docs (/docs). Everything is
   driven by the custom properties in the theme blocks below, so the light and
   dark palettes are the only place a colour is written down. */

/* ---------------------------------------------------------------- themes */

/* Light is the default. Every token that dark redefines must exist here, or
   the dark blocks would be the only definition and a light page would fall
   back to the browser's own colours. */
body {
    color-scheme: light;

    --bg: #f9f8f6;
    --surface: #ffffff;
    --surface-2: #efeee9;
    --border: #e3e2dc;
    --border-strong: #cfd0c7;
    --text: #37392E;
    --muted: #6b6f62;

    --band: #24261F;
    --band-fg: #f9f8f6;
    --band-muted: rgba(249, 248, 246, .7);
    --band-line: rgba(249, 248, 246, .16);

    --accent: #b3591c;
    --accent-hover: #d47a2a;
    --accent-ink: #8e4512;
    --accent-soft: #f7e8da;
    --focus: rgba(179, 89, 28, .18);

    --primary: #24261F;
    --primary-hover: #3a3d33;
    --primary-fg: #f9f8f6;

    --ok: #3f8f4c;
    --error: #b3261e;
    --error-soft: #fbeeed;
    --track: #e3e2dc;
    --code-bg: #24261F;
    --code-fg: #e8e6df;

    /* The glasses PNG is dark line art, so it needs inverting anywhere the
       surface underneath it is dark. */
    --invert: none;

    /* The switch is styled from the theme rather than from :checked, so the
       knob always shows the theme actually in force -- including the first
       load on a dark desktop, where there is no cookie and no checked box. */
    --switch-bg: rgba(249, 248, 246, .22);
    --knob-left: 3px;
    --theme-label: "light";
}

/* Three rules decide the theme, and the order matters.

   #theme-state carries the cookie: absent attribute means "no preference
   recorded yet", in which case the OS wins. Toggling the switch htmx-swaps
   this one span, which is why the whole page can change colour with no
   JavaScript and no reload. */
@media (prefers-color-scheme: dark) {
    body:not(:has(#theme-state[data-theme="light"])) {
        color-scheme: dark;

        --bg: #171814;
        --surface: #1f2119;
        --surface-2: #2a2c24;
        --border: #33362e;
        --border-strong: #484b42;
        --text: #e8e6df;
        --muted: #9a9d92;

        --band: #0f100c;

        --accent: #e08a3a;
        --accent-hover: #f0a55c;
        --accent-ink: #f0a55c;
        --accent-soft: rgba(224, 138, 58, .16);
        --focus: rgba(224, 138, 58, .28);

        --primary: #f9f8f6;
        --primary-hover: #ffffff;
        --primary-fg: #24261F;

        --ok: #6cc47a;
        --error: #f08a80;
        --error-soft: rgba(179, 38, 30, .24);
        --track: #33362e;
        --code-bg: #0f100c;

        --invert: invert(1);

        --switch-bg: var(--accent);
        --knob-left: 21px;
        --theme-label: "dark";
    }
}

body:has(#theme-state[data-theme="dark"]) {
    color-scheme: dark;

    --bg: #171814;
    --surface: #1f2119;
    --surface-2: #2a2c24;
    --border: #33362e;
    --border-strong: #484b42;
    --text: #e8e6df;
    --muted: #9a9d92;

    --band: #0f100c;

    --accent: #e08a3a;
    --accent-hover: #f0a55c;
    --accent-ink: #f0a55c;
    --accent-soft: rgba(224, 138, 58, .16);
    --focus: rgba(224, 138, 58, .28);

    --primary: #f9f8f6;
    --primary-hover: #ffffff;
    --primary-fg: #24261F;

    --ok: #6cc47a;
    --error: #f08a80;
    --error-soft: rgba(179, 38, 30, .24);
    --track: #33362e;
    --code-bg: #0f100c;

    --invert: invert(1);

    --switch-bg: var(--accent);
    --knob-left: 21px;
    --theme-label: "dark";
}

/* ------------------------------------------------------------ foundation */

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    color: var(--text);
    font-family: 'Manrope', system-ui, -apple-system, 'Segoe UI', sans-serif;
    font-size: 15px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

a {
    color: var(--accent);
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
    text-underline-offset: 2px;
}

a:hover {
    color: var(--accent-hover);
    text-decoration-color: currentColor;
}

img {
    max-width: 100%;
}

button {
    font-family: inherit;
}

.container {
    width: 100%;
    max-width: 1120px;
    margin: 0 auto;
    padding: 0 24px;
}

/* Kept off-screen rather than display:none: a hidden input is not focusable
   and would drop the file picker and the theme switch out of the tab order. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* One page in two shapes. The copy and the controls that only make sense on
   a phone are in the markup either way and a media query picks between them:
   no user-agent sniffing, and both halves are there for htmx to find. */
.mobile-only {
    display: none;
}

/* ---------------------------------------------------------------- header */

.band {
    background: var(--band);
    color: var(--band-fg);
}

.band .container {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
}

.brand {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--band-fg);
    text-decoration: none;
    flex: none;
}

.brand img {
    /* The band is dark in both themes, so this inversion is not theme-aware. */
    width: 44px;
    height: auto;
    display: block;
    filter: invert(1);
}

.brand span {
    font-weight: 800;
    font-size: 16px;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.nav {
    display: flex;
    align-items: center;
    gap: 22px;
    font-size: 14px;
    font-weight: 600;
    min-width: 0;
}

/* One cell of the two-row grid the band becomes on a phone. Above 600px it
   is not a box at all, so the desktop band is exactly as it was. */
.nav-links {
    display: contents;
}

.nav a {
    color: var(--band-muted);
    text-decoration: none;
    padding: 4px 0;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
}

.nav a:hover {
    color: var(--band-fg);
}

.nav a.here {
    color: var(--band-fg);
    border-bottom-color: var(--accent);
}

/* The status pill, refreshed by htmx every 30 seconds. */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: none;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    font-weight: 400;
    color: var(--band-muted);
    padding: 5px 10px;
    border: 1px solid var(--band-line);
    border-radius: 999px;
    white-space: nowrap;
}

.pill::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    /* Green and red read the same against the band in both themes, so these
       two are deliberately not tokens. */
    background: #5db56a;
    box-shadow: 0 0 0 3px rgba(93, 181, 106, .22);
}

.pill.down::before {
    background: #e0574d;
    box-shadow: 0 0 0 3px rgba(224, 87, 77, .22);
}

/* The switch: a checkbox that posts to /theme, plus a label drawn as a track
   and a knob. Both follow the theme tokens, not :checked -- see --switch-bg. */
.switch {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: none;
    cursor: pointer;
    color: var(--band-muted);
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
}

.switch::before {
    content: "";
    position: relative;
    display: block;
    width: 40px;
    height: 22px;
    border-radius: 999px;
    background: var(--switch-bg);
    transition: background .2s;
}

.switch::after {
    content: var(--theme-label);
}

.switch .knob {
    position: absolute;
    top: 3px;
    left: var(--knob-left);
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #f9f8f6;
    transition: left .2s;
    pointer-events: none;
}

.switch-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
}

#theme:focus-visible + .switch::before {
    outline: 2px solid var(--band-fg);
    outline-offset: 2px;
}

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

.layout {
    flex: 1;
    display: flex;
    flex-wrap: wrap;
    gap: 40px;
    align-items: flex-start;
    padding-top: 36px;
    padding-bottom: 72px;
}

/* Not sticky. It was, until the sample list grew past the height of a
   window: a sticky element taller than the viewport pins its top and puts its
   own bottom permanently out of reach. */
.pane {
    flex: 1 1 320px;
    max-width: 400px;
    display: flex;
    flex-direction: column;
    gap: 18px;
}

.lede h1 {
    margin: 0 0 10px;
    font-size: 26px;
    line-height: 1.2;
    font-weight: 800;
    letter-spacing: -.02em;
    text-wrap: pretty;
}

.lede p {
    margin: 0;
    color: var(--muted);
    font-size: 14px;
}

#form {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* The card is a <label> for the file input, which is what makes dropping
   work without a line of JavaScript: a file input is a native drop target,
   and dropping on its label counts as choosing files. Browsers also fire
   :hover while a drag is over the element, so that doubles as the drag state.

   The label is `.dropzone-face` inside the card rather than the card itself,
   because on a phone the card also holds a "Take a photo" label and a label
   cannot nest in a label. `.dropzone-face::before` stretches the label's hit
   area back over the whole card, so tapping or dropping anywhere on it --
   the padding, the limits line -- still lands on the input. */
.dropzone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 176px;
    padding: 24px;
    border-radius: 14px;
    border: 1.5px dashed var(--border-strong);
    background: var(--surface);
    cursor: pointer;
    text-align: center;
    transition: background .15s, border-color .15s;
}

.dropzone-face {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

.dropzone-face::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 14px;
}

.dropzone:hover {
    border-color: var(--accent);
    background: var(--accent-soft);
}

#files:focus-visible + .dropzone {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--focus);
}

/* htmx puts .htmx-request on the element a request came from, and a batch of
   files is sent one at a time from this input -- so this stays on for the
   whole batch, and says that more cards are still on their way. */
#files.htmx-request + .dropzone {
    border-color: var(--accent);
    background: var(--accent-soft);
    cursor: progress;
}

.dropzone img {
    width: 72px;
    height: auto;
    display: block;
    margin-bottom: 6px;
    filter: var(--invert);
}

.dropzone .title {
    font-size: 16px;
    font-weight: 700;
}

.dropzone .sub {
    font-size: 13px;
    color: var(--muted);
}

.dropzone .sub em {
    color: var(--accent);
    font-style: normal;
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.dropzone .limits {
    font-size: 12px;
    color: var(--muted);
    margin-top: 2px;
}

.divider,
.samples-label {
    color: var(--muted);
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
}

.divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 8px 0 0;
}

.divider::before,
.divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--border);
}

.url-row {
    display: flex;
    gap: 8px;
}

input[type="text"] {
    flex: 1;
    min-width: 0;
    height: 44px;
    padding: 0 14px;
    border: 1px solid var(--border-strong);
    border-radius: 9px;
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: 14px;
    outline: none;
}

input[type="text"]:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--focus);
}

.btn {
    /* inline-flex, not the button element's own centring: an <a class="btn">
       is a button too, and without this its text sits at the top of the
       44px box. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 44px;
    padding: 0 18px;
    border: none;
    border-radius: 9px;
    background: var(--primary);
    color: var(--primary-fg);
    font-weight: 700;
    font-size: 14px;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
}

/* The second button in a pair, where one of them has to be the quieter one. */
.btn.ghost {
    background: var(--surface);
    border: 1px solid var(--border-strong);
    color: var(--text);
}

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

.btn:hover {
    background: var(--primary-hover);
}

/* htmx sets this while the submit is in flight (hx-disabled-elt). */
.btn:disabled {
    opacity: .6;
    cursor: progress;
}

#url-hint {
    margin: 0;
    font-size: 13px;
    font-weight: 600;
}

#url-hint.ok {
    color: var(--ok);
}

#url-hint.error {
    color: var(--error);
}

.samples-label {
    display: block;
    margin: 6px 0 8px;
}

.samples {
    display: grid;
    /* Three, at every width. It used to pack by size -- three across a narrow
       column, four across a wide one -- which made the grid a different shape
       on every screen and the tiles a different size from the pictures beside
       them. A fixed count is one shape to design for. */
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
}

.sample {
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    color: inherit;
    cursor: pointer;
    text-align: left;
    overflow: hidden;
}

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

.sample img {
    width: 100%;
    /* `height: auto` is load-bearing, not tidiness: the tag carries width and
       height attributes (so the grid does not reflow as a dozen tiles land),
       and those become a presentational `height: 200px`. aspect-ratio only
       governs when height is auto, so without this the tile renders one
       column wide and 200px tall. */
    height: auto;
    aspect-ratio: 1;
    object-fit: cover;
    display: block;
}

.sample .caption {
    display: block;
    padding: 6px 8px 7px;
    min-width: 0;
}

.sample .caption b {
    display: block;
    font-size: 12px;
    font-weight: 700;
}

.sample .caption span {
    display: block;
    font-size: 10px;
    color: var(--muted);
    font-family: 'Fira Code', ui-monospace, monospace;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ----------------------------------------------------------- the session */

.session {
    flex: 2 1 460px;
    min-width: 0;
}

.session-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.session-head h2 {
    margin: 0;
    font-size: 18px;
    font-weight: 800;
    letter-spacing: -.01em;
}

.clear {
    border: none;
    background: none;
    padding: 4px 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--muted);
    cursor: pointer;
    /* Nothing to clear until a card exists. */
    display: none;
}

.clear:hover {
    color: var(--error);
}

body:has(#queue article) .clear {
    display: inline-block;
}

.note {
    margin: 2px 0 16px;
    font-size: 13px;
    color: var(--muted);
}

#queue {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* The empty state sits after #queue in the DOM so the sibling combinator can
   reach it; an empty #queue takes no vertical space, so it still reads as
   being in the list's place.

   :not(:has(article)) rather than :empty, which counts text nodes: htmx
   leaves the whitespace around a swapped-in card behind when the card is
   removed, and the empty state would then never come back. */
.empty {
    display: none;
    border: 1.5px dashed var(--border);
    border-radius: 14px;
    padding: 48px 24px;
    text-align: center;
    color: var(--muted);
    font-size: 14px;
}

#queue:not(:has(article)) + .empty {
    display: block;
}

/* ----------------------------------------------------------------- cards */

/* On a desktop a card is one box: the body dissolves into it and the swipe
   panel is not rendered. Both become real at 600px, where the card turns
   into a horizontal scroll-snap strip of the two. */
.card-body {
    display: contents;
}

.card-remove {
    display: none;
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 14px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.card-head {
    display: flex;
    align-items: center;
    gap: 12px;
}

.thumb {
    width: 44px;
    height: 44px;
    flex: none;
    border-radius: 8px;
    object-fit: cover;
    background: var(--surface-2);
}

.card-id {
    flex: 1;
    min-width: 0;
}

.card-id b {
    display: block;
    font-size: 15px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.card-id span {
    display: block;
    margin-top: 2px;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chip {
    flex: none;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: .08em;
    text-transform: uppercase;
    padding: 4px 9px;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--muted);
}

.chip.working {
    background: var(--accent-soft);
    color: var(--accent-ink);
}

.chip.done {
    background: var(--primary);
    color: var(--primary-fg);
}

.chip.failed {
    background: var(--error-soft);
    color: var(--error);
}

.remove {
    flex: none;
    width: 28px;
    height: 28px;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--muted);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
}

.remove:hover {
    background: var(--surface-2);
    color: var(--text);
}

.card-error {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    background: var(--error-soft);
    border-radius: 9px;
    padding: 10px 12px;
}

.card-error p {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--error);
}

.retry {
    border: 1px solid var(--error);
    background: transparent;
    color: var(--error);
    border-radius: 7px;
    padding: 6px 12px;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}

.retry:hover {
    background: var(--error);
    color: #fff;
}

.frame {
    position: relative;
    /* A stacking context, so the skeleton below can sit at -1: under every
       picture and control in here, but still over the frame's own
       background. Without it the -1 would fall through to the nearest
       context outside and the background would hide the sheen. The
       full-screen state raises this to 101 and takes the context with it. */
    z-index: 0;
    background: var(--surface-2);
    border-radius: 10px;
    overflow: hidden;
    display: flex;
    justify-content: center;
}

/* Tapping the picture opens the full-size view. A phone only, and over the
   frame rather than around it: the frame is also the overlay, and a label
   wrapping it would swallow every touch once it was open. */
.frame-tap {
    display: none;
    position: absolute;
    inset: 0;
    cursor: zoom-in;
}

.frame img {
    display: block;
    max-height: 460px;
    max-width: 100%;
}

/* The seconds before the first picture lands. An `<img>` with nothing in it
   yet has no height, so a finished card used to arrive as a footer with a
   gap above it and then jump; this reserves the shape and puts a sheen
   across it while the fetch is in flight, the same placeholder a video site
   shows over a thumbnail.

   `sweep` is the indeterminate progress bar's animation, unchanged: same
   200% background walked from one side to the other. */
.frame::after {
    content: '';
    position: absolute;
    inset: 0;
    /* Under the pictures, so a loaded one hides the skeleton by covering it
       -- no script in the path and nothing to time. Under the tap target and
       the phone's pills too, which is what the images must never be. */
    z-index: -1;
    background-image: linear-gradient(100deg,
        transparent 20%, var(--surface) 50%, transparent 80%);
    background-size: 200% 100%;
    animation: sweep 1.4s infinite linear;
}

/* Only until something has painted. `views.js` sets the attribute when the
   tab the card opens on has loaded; with no script the sheen keeps running
   under a picture that covers it, which is invisible and harmless. */
.result:not([data-ready]) .frame {
    min-height: 260px;
}

.result[data-ready] .frame::after {
    display: none;
}

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

/* Before/After/Animated with no script: a radio each and :has(). The radios
   stay in the tab order, so the switch is keyboard-operable. */
.result {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.result input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

/* One rule per tab rather than one per pair of them: with Before, After and
   Animated the pairs are six selectors, and `:not()` stays one per tab
   whatever gets added next. */
.result:has([value="before"]:checked) .frame img:not(.before),
.result:has([value="after"]:checked) .frame img:not(.after),
.result:has([value="animated"]:checked) .frame img:not(.animated) {
    display: none;
}

.card-foot {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.segmented {
    /* The radios are absolutely positioned to get them out of sight; without
       this they would resolve against the page instead of the control. */
    position: relative;
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
    font-size: 13px;
    font-weight: 700;
}

.segmented label {
    padding: 7px 12px;
    cursor: pointer;
    background: var(--surface);
    color: var(--text);
}

/* The phone's copy of the switch, which lives on the picture. Not `.pill`:
   that is the band's worker-status pill, and its ::before would put a green
   dot inside this one. Off here by its own selector, because `.mobile-only`
   alone would lose to `.segmented` above -- same specificity, comes later. */
.segmented.view-pill,
.open-full-pill {
    display: none;
}

/* Matched through the container rather than as `input:checked + label`, so
   the same rule can style a control that is not a sibling of its radios --
   which is what lets the overlay reuse them. */
.result:has([value="before"]:checked) label[data-view="before"],
.result:has([value="after"]:checked) label[data-view="after"],
.result:has([value="animated"]:checked) label[data-view="animated"] {
    background: var(--primary);
    color: var(--primary-fg);
}

.result input:focus-visible + label {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

.spacer {
    flex: 1;
}

.open-full {
    font-size: 13px;
    font-weight: 600;
    color: var(--accent);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}

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

.full-toggle:focus-visible + .open-full {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 3px;
}

/* Both only ever appear inside the overlay, on a phone. */
.zoom,
.zoom-hint {
    display: none;
}

/* The label says what the tap will do, so it is the other word that shows. */
.zoom .out,
.result:has(.zoom-toggle:checked) .zoom .in {
    display: none;
}

.result:has(.zoom-toggle:checked) .zoom .out {
    display: inline;
}

.download {
    display: inline-flex;
    align-items: center;
    height: 34px;
    padding: 0 14px;
    border-radius: 8px;
    background: var(--primary);
    color: var(--primary-fg);
    text-decoration: none;
    font-size: 13px;
    font-weight: 700;
}

.download:hover {
    background: var(--primary-hover);
    color: var(--primary-fg);
}

/* ------------------------------------------------------ full-screen view */

/* The same frame and footer, re-laid-out as an overlay -- not a second copy
   of them, which would put the whole result data URI in the card twice. A
   checkbox and :has() rather than <dialog>, which cannot be opened without
   showModal() and therefore without script.

   The trade-off that buys: no focus trap and no Escape key. The close button,
   the backdrop and the checkbox itself (still in the tab order) are the ways
   out. */
.backdrop,
.lightbox-close {
    display: none;
}

body:has(.full-toggle:checked) {
    overflow: hidden;
}

.result:has(.full-toggle:checked) .backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 100;
    /* Dark in both themes: it is a viewer, and a light one fights the image. */
    background: rgba(15, 16, 12, .93);
    cursor: zoom-out;
}

.result:has(.full-toggle:checked) .frame {
    position: fixed;
    inset: 0;
    z-index: 101;
    align-items: center;
    background: none;
    border-radius: 0;
    padding: 72px 24px 96px;
    /* So a click beside the image falls through to the backdrop and closes. */
    pointer-events: none;
}

.result:has(.full-toggle:checked) .frame img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 8px;
    pointer-events: auto;
}

.result:has(.full-toggle:checked) .lightbox-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    top: 18px;
    right: 18px;
    z-index: 102;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid rgba(249, 248, 246, .24);
    background: rgba(249, 248, 246, .08);
    color: #f9f8f6;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}

.result:has(.full-toggle:checked) .lightbox-close:hover {
    background: rgba(249, 248, 246, .2);
}

.result:has(.full-toggle:checked) .card-foot {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 102;
    justify-content: center;
    padding: 18px 24px 26px;
}

.result:has(.full-toggle:checked) .spacer,
.result:has(.full-toggle:checked) .open-full {
    display: none;
}

/* The overlay is dark whatever the theme, so its controls cannot use the
   theme's surfaces. */
.result:has(.full-toggle:checked) .segmented {
    border-color: rgba(249, 248, 246, .28);
}

.result:has(.full-toggle:checked) .segmented label {
    background: rgba(249, 248, 246, .1);
    color: #f9f8f6;
}

.result:has(.full-toggle:checked):has([value="before"]:checked) label[data-view="before"],
.result:has(.full-toggle:checked):has([value="after"]:checked) label[data-view="after"],
.result:has(.full-toggle:checked):has([value="animated"]:checked) label[data-view="animated"] {
    background: #f9f8f6;
    color: #24261F;
}

.result:has(.full-toggle:checked) .download {
    background: #f9f8f6;
    color: #24261F;
}

/* -------------------------------------------------------------- progress */

progress {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 6px;
    border: none;
    border-radius: 99px;
    background: var(--track);
}

progress::-webkit-progress-bar {
    background: var(--track);
    border-radius: 99px;
}

progress::-webkit-progress-value {
    background: var(--accent);
    border-radius: 99px;
    transition: width .4s ease;
}

progress::-moz-progress-bar {
    background: var(--accent);
    border-radius: 99px;
}

/* A queued job has no percentage to show, so the bar sweeps instead of
   sitting at zero looking broken. Stripping the native appearance also
   strips the browser's own indeterminate animation. */
progress:indeterminate {
    background-image: linear-gradient(90deg, var(--track) 0%, var(--accent) 50%, var(--track) 100%);
    background-size: 200% 100%;
    animation: sweep 1.2s infinite linear;
}

progress:indeterminate::-webkit-progress-bar {
    background: transparent;
}

@keyframes sweep {
    from {
        background-position: 100% 0;
    }

    to {
        background-position: -100% 0;
    }
}

/* ------------------------------------------------------------------ docs */

.docs {
    flex: 1;
    display: flex;
    gap: 56px;
    align-items: flex-start;
    padding-top: 40px;
    padding-bottom: 80px;
    font-size: 16px;
    line-height: 1.6;
}

.toc {
    flex: none;
    width: 180px;
    position: sticky;
    top: 32px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 14px;
    font-weight: 600;
}

.toc .label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--muted);
}

.toc > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    cursor: pointer;
    /* Both halves of dropping the native triangle: the marker box in Blink
       and WebKit, the list-item display everywhere else. */
    list-style: none;
}

.toc > summary::-webkit-details-marker {
    display: none;
}

.toc > summary::after {
    content: "▾";
    color: var(--muted);
    font-size: 11px;
}

.toc[open] > summary::after {
    content: "▴";
}

/* Blockified by the flex column above 860px, but inside the disclosure the
   links are slotted into a block, where an <a> would run into the next. */
.toc a {
    display: block;
    color: var(--text);
    text-decoration: none;
    padding: 4px 0;
}

.toc a.sub {
    color: var(--muted);
    font-weight: 500;
    padding: 2px 0 2px 14px;
}

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

.toc .try-it {
    margin-top: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 38px;
    border-radius: 9px;
    background: var(--primary);
    color: var(--primary-fg);
    font-size: 13px;
    font-weight: 700;
}

.toc .try-it:hover {
    background: var(--primary-hover);
    color: var(--primary-fg);
}

.doc {
    flex: 1;
    min-width: 0;
    max-width: 760px;
}

.doc section {
    margin: 0 0 56px;
}

.doc section:last-child {
    margin-bottom: 0;
}

.doc h1 {
    margin: 0 0 12px;
    font-size: 36px;
    line-height: 1.15;
    font-weight: 800;
    letter-spacing: -.02em;
}

.doc h2 {
    margin: 0 0 10px;
    font-size: 28px;
    line-height: 1.2;
    font-weight: 800;
    letter-spacing: -.02em;
}

.doc h3 {
    margin: 0 0 8px;
    font-size: 19px;
    font-weight: 700;
}

.doc h3 .n {
    font-family: 'Fira Code', ui-monospace, monospace;
    color: var(--accent-ink);
    font-weight: 500;
    margin-right: 10px;
}

.doc p {
    margin: 0 0 14px;
    text-wrap: pretty;
}

.doc .lead {
    margin: 0 0 24px;
    font-size: 18px;
    color: var(--muted);
}

.figures {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 12px;
}

.figures figure {
    margin: 0;
}

.figures img {
    display: block;
    width: 100%;
    border-radius: 10px;
}

.figures figcaption,
.credit {
    margin-top: 8px;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    color: var(--muted);
}

.credit {
    margin: 0 0 28px;
    line-height: 1.5;
}

.tiles {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
}

.tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 14px 16px;
}

.tile b {
    display: block;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 13px;
    font-weight: 500;
    color: var(--accent-ink);
    margin-bottom: 4px;
}

.tile span {
    font-size: 14px;
}

code {
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 14px;
    background: var(--surface-2);
    padding: 2px 6px;
    border-radius: 5px;
}

.snippet {
    position: relative;
    margin: 0 0 20px;
}

.snippet pre {
    margin: 0;
    padding: 14px 16px;
    background: var(--code-bg);
    color: var(--code-fg);
    border-radius: 10px;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 13px;
    line-height: 1.55;
    /* Selecting the whole snippet with one click is the fallback for anyone
       whose browser refuses clipboard access. */
    white-space: pre-wrap;
    word-break: break-word;
    user-select: all;
}

.snippet pre code {
    background: none;
    padding: 0;
    font-size: inherit;
}

.pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin: 0 0 20px;
}

.pair .snippet {
    margin: 0;
}

.copy {
    position: absolute;
    top: 8px;
    right: 8px;
    height: 26px;
    padding: 0 9px;
    /* On the code background in both themes, so fixed rather than tokenised. */
    border: 1px solid rgba(249, 248, 246, .2);
    border-radius: 6px;
    background: rgba(249, 248, 246, .06);
    color: #f9f8f6;
    font-size: 11px;
    font-weight: 700;
    cursor: pointer;
}

.copy:hover {
    background: rgba(249, 248, 246, .16);
}

/* The confirmation, with no timer of our own: :active shows it instantly,
   and releasing the button starts a delayed fade. */
.copied {
    position: absolute;
    top: 8px;
    right: 66px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    font-size: 11px;
    font-weight: 700;
    color: var(--accent-hover);
    opacity: 0;
    transition: opacity .3s ease 1.2s;
}

.copy:active + .copied {
    opacity: 1;
    transition: none;
}

.stages {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
    margin: 0 0 36px;
}

.stage {
    border-top: 3px solid var(--accent);
    padding-top: 8px;
}

.stage b {
    display: block;
    font-family: 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    font-weight: 400;
    color: var(--accent-ink);
}

.stage span {
    display: block;
    font-size: 13px;
    font-weight: 600;
}

.stage.last {
    border-top-color: var(--primary);
}

.stage.last b {
    color: var(--text);
}

/* The bar a phone gets once the list has something in it. Nothing above
   600px, where the form is on the page and needs no shortcut to it. */
.addbar {
    display: none;
}

/* ---------------------------------------------------------------- footer */

.site-footer {
    border-top: 1px solid var(--border);
    color: var(--muted);
    font-size: 13px;
}

.site-footer .container {
    padding-top: 18px;
    padding-bottom: 18px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}

.site-footer a {
    color: inherit;
}

.site-footer .links {
    display: flex;
    gap: 18px;
    flex-wrap: wrap;
}

/* ------------------------------------------------------------ responsive */

/* Above 860px the contents list is not a disclosure at all: the content box
   is forced open and the summary is inert, so it reads as the label and the
   links it was before it became a <details>. */
@media (min-width: 861px) {
    .toc > summary {
        margin-bottom: 6px;
        pointer-events: none;
    }

    .toc > summary::after {
        content: none;
    }

    .toc::details-content {
        content-visibility: visible;
        display: flex;
        flex-direction: column;
        gap: 6px;
    }
}

@media (max-width: 860px) {
    /* No gap: below 860px the contents list is a card that carries its own
       margin, and 32px on top of it would read as a hole. */
    .docs {
        flex-direction: column;
        gap: 0;
    }

    /* A card that opens on tap. Native disclosure, so no script and no
       state of our own to keep. */
    .toc {
        position: static;
        width: 100%;
        gap: 0;
        margin-bottom: 24px;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: 12px;
    }

    .toc > summary {
        height: 44px;
        padding: 0 14px;
    }

    .toc a {
        padding: 6px 14px;
    }

    /* The rule under the summary. On the first link rather than on the
       content box, which only a browser with ::details-content can style. */
    .toc a:first-of-type {
        padding-top: 10px;
        border-top: 1px solid var(--border);
    }

    .toc a.sub {
        padding: 4px 14px 4px 28px;
    }

    .toc .try-it {
        height: 40px;
        margin: 12px 14px 14px;
    }
}

@media (max-width: 760px) {
    .pane {
        max-width: none;
    }

    .pair,
    .figures {
        grid-template-columns: 1fr;
    }

    .stages {
        grid-template-columns: repeat(2, 1fr);
    }

    .nav {
        gap: 14px;
    }

    .band .container {
        height: auto;
        padding-top: 10px;
        padding-bottom: 10px;
        flex-wrap: wrap;
    }

    .doc h1 {
        font-size: 28px;
    }
}

/* -------------------------------------------------------------- a phone */

/* Everything below here is the phone layout. Two states, and the CSS reads
   which one it is from the list itself: with nothing in #queue the pane is
   the page, and with a card in it the results are the page and the input
   moves to a bar along the bottom. No endpoint and no script knows about
   any of this. */
@media (max-width: 600px) {
    body {
        /* The bar: 6px above 52px of items above the chin. The chin is the
           safe-area inset itself, floored at 12px for a device that has
           none -- adding a constant to it counted an iPhone's 34px twice. */
        --addbar-h: calc(58px + max(12px, env(safe-area-inset-bottom)));
    }

    .container {
        padding: 0 20px;
    }

    .desktop-only {
        display: none;
    }

    .mobile-only {
        display: initial;
    }

    /* ----- the band ----- */

    /* Two rows: the brand and the switch, then the links and the pill. The
       safe-area padding goes here rather than on body because the band is
       dark in both themes and the status bar is translucent over it. */
    .band .container {
        display: grid;
        grid-template-columns: 1fr auto;
        column-gap: 16px;
        row-gap: 0;
        height: auto;
        padding: env(safe-area-inset-top) 20px 6px;
    }

    .nav {
        display: contents;
    }

    .brand {
        grid-area: 1 / 1;
        height: 48px;
        gap: 10px;
    }

    .brand img {
        width: 40px;
    }

    .brand span {
        font-size: 15px;
    }

    /* No height here: the knob is positioned against this box, so
       stretching it would leave the knob behind at the top of the row. The
       48px comes from .brand, and the grid centres this against it. */
    .switch-wrap {
        grid-area: 1 / 2;
        justify-self: end;
    }

    .nav-links {
        grid-area: 2 / 1;
        display: flex;
        align-items: center;
        gap: 20px;
        height: 38px;
    }

    .pill {
        grid-area: 2 / 2;
        justify-self: end;
        font-size: 11px;
        padding: 4px 9px;
    }

    .pill::before {
        width: 7px;
        height: 7px;
    }

    /* ----- input mode ----- */

    /* Both columns are full width here, so the layout is two flex lines --
       and a wrapped flex container stretches its lines over any spare
       height, which on a short page pushes the second one down the screen. */
    .layout {
        align-content: flex-start;
        gap: 18px;
        padding: 22px 20px 36px;
    }

    .lede h1 {
        font-size: 24px;
    }

    /* Nothing to show until there is something in it, and a phone has no
       room to say so twice. */
    body:not(:has(#queue article)) .session {
        display: none;
    }

    .dropzone {
        min-height: 0;
        padding: 22px 18px 18px;
    }

    .dropzone .limits {
        margin-top: 4px;
    }

    .url-row .btn {
        padding: 0 16px;
    }

    /* ----- results mode ----- */

    body:has(#queue article) .layout {
        gap: 0;
        padding: 18px 20px 28px;
    }

    /* The pane stops being a column of its own: what is left of it is the
       form, which keeps its boxes only to hold the two file inputs. */
    body:has(#queue article) .pane {
        display: contents;
    }

    body:has(#queue article) .lede,
    body:has(#queue article) .dropzone,
    body:has(#queue article) .divider,
    body:has(#queue article) .sample-strip,
    body:has(#queue article) #form > .url-row,
    body:has(#queue article) #url-hint {
        display: none;
    }

    /* #form itself is never display:none. The file input is in it, and a
       label cannot open a picker for an input with no box -- which is what
       the bar's "Add a picture" is. */
    body:has(#queue article) #form {
        gap: 0;
    }

    body:has(#queue article) .site-footer .container {
        padding-bottom: calc(var(--addbar-h) + 12px);
    }

    /* ----- the bottom bar ----- */

    body:has(#queue article) .addbar {
        display: flex;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 40;
        background: var(--band);
        color: var(--band-fg);
        padding: 6px 8px max(12px, env(safe-area-inset-bottom));
    }

    /* A tab bar: the glyph over the word, and the open one in --accent. */
    .addbar-item {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 4px;
        height: 52px;
        color: var(--band-muted);
        font-size: 11px;
        font-weight: 700;
        cursor: pointer;
    }

    .addbar-item:hover {
        color: var(--band-fg);
    }

    /* Each panel item carries the same glyph and word twice: one label opens
       it, the other checks #show-none, so a second tap closes what is open. */
    .addbar-item .open,
    .addbar-item .close {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 4px;
    }

    .addbar-item .close {
        display: none;
    }

    body:has(#show-url:checked) .addbar-item.show-url,
    body:has(#show-samples:checked) .addbar-item.show-samples {
        color: var(--accent);
    }

    body:has(#show-url:checked) .addbar-item.show-url .open,
    body:has(#show-samples:checked) .addbar-item.show-samples .open {
        display: none;
    }

    body:has(#show-url:checked) .addbar-item.show-url .close,
    body:has(#show-samples:checked) .addbar-item.show-samples .close {
        display: flex;
    }

    /* The URL panel is the page's own form, re-laid over the bar. A second
       copy would be torn out by the out-of-band swap /submit answers with. */
    body:has(#queue article):has(#show-url:checked) #form {
        position: fixed;
        left: 0;
        right: 0;
        bottom: var(--addbar-h);
        z-index: 30;
        gap: 8px;
        padding: 12px 20px 14px;
        background: var(--surface);
        border-top: 1px solid var(--border);
    }

    body:has(#queue article):has(#show-url:checked) #form > .url-row {
        display: flex;
    }

    body:has(#queue article):has(#show-url:checked) #url-hint {
        display: block;
    }

    /* The sample grid, re-laid as a strip that scrolls sideways. */
    body:has(#queue article):has(#show-samples:checked) .sample-strip {
        display: block;
        position: fixed;
        left: 0;
        right: 0;
        bottom: var(--addbar-h);
        z-index: 30;
        padding: 12px 0 14px;
        background: var(--surface);
        border-top: 1px solid var(--border);
    }

    .sample-strip .samples-label {
        margin: 0 0 8px;
        padding: 0 20px;
    }

    /* "Or try a sample" follows the URL row on the page; over the bar it is
       a panel title with nothing before it. */
    body:has(#show-samples:checked) .sample-strip .lead-in {
        display: none;
    }

    body:has(#show-samples:checked) .samples {
        display: flex;
        gap: 10px;
        overflow-x: auto;
        padding: 0 20px 4px;
        scroll-snap-type: x mandatory;
        scrollbar-width: none;
    }

    body:has(#show-samples:checked) .samples::-webkit-scrollbar {
        display: none;
    }

    body:has(#show-samples:checked) .sample {
        flex: none;
        width: 96px;
        scroll-snap-align: start;
        /* The panel is --surface, so the tiles cannot be. */
        background: var(--bg);
    }

    /* 96px is too narrow for "Twenty-nine faces" to wrap gracefully. */
    body:has(#show-samples:checked) .sample .caption b {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* ----- cards ----- */

    /* Swipe left to remove: the card is a horizontal scroll-snap strip of
       the body and a panel behind it, so the gesture is the browser's own
       overscroll and costs no script. The x in the head still works. */
    .card {
        flex-direction: row;
        gap: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        background: none;
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        scrollbar-width: none;
    }

    .card::-webkit-scrollbar {
        display: none;
    }

    .card-body {
        flex: none;
        width: 100%;
        scroll-snap-align: start;
        display: flex;
        flex-direction: column;
        gap: 12px;
        padding: 14px 16px 16px;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: 14px;
    }

    .card-remove {
        display: flex;
        flex: none;
        width: 96px;
        scroll-snap-align: end;
        margin-left: 10px;
        border-radius: 14px;
        background: var(--error-soft);
    }

    .swipe-remove {
        flex: 1;
        border: 0;
        border-radius: 14px;
        background: none;
        color: var(--error);
        font-size: 13px;
        font-weight: 700;
        cursor: pointer;
    }

    .frame img {
        max-height: 400px;
    }

    .frame-tap {
        display: block;
    }

    /* One row of equal buttons. `grid-auto-flow: column` is the load-bearing
       part: before share.js runs, or on a browser that cannot share a file,
       the row is two equal buttons rather than three with a hole in it. */
    .card-foot {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        gap: 8px;
    }

    /* Out of the grid, not just invisible: a display:none child takes no
       track, which is what keeps the columns equal. */
    .card-foot .spacer,
    .card-foot .open-full,
    .card-foot .segmented.desktop-only {
        display: none;
    }

    .download-menu {
        display: flex;
    }

    .download {
        flex: 1;
        justify-content: center;
        height: 44px;
        padding: 0 10px;
        border-radius: 9px;
    }

    .card-share .share.desktop-only,
    .card-share .copy-wrap.desktop-only {
        display: none;
    }

    /* The wrap is the grid cell; the button has to fill it. */
    .card-foot .copy-wrap {
        display: flex;
    }

    .card-foot .copy-wrap .copy-link {
        flex: 1;
    }

    /* ----- the controls on the picture ----- */

    .frame .segmented.view-pill {
        display: inline-flex;
        position: absolute;
        left: 10px;
        bottom: 10px;
        z-index: 2;
        padding: 3px;
        border: 0;
        border-radius: 999px;
        /* Dark glass in both themes, like the overlay: the thing behind it
           is a photograph, not a surface. */
        background: rgba(15, 16, 12, .72);
        backdrop-filter: blur(8px);
        font-size: 12px;
    }

    .frame .segmented.view-pill label {
        padding: 5px 12px;
        border-radius: 999px;
        background: transparent;
        color: #f9f8f6;
    }

    .result:has([value="before"]:checked) .view-pill label[data-view="before"],
    .result:has([value="after"]:checked) .view-pill label[data-view="after"],
    .result:has([value="animated"]:checked) .view-pill label[data-view="animated"] {
        background: #f9f8f6;
        color: #24261F;
    }

    /* Top right, not beside the view pill: with three tabs on it that pill
       is 164px, and 320px of phone does not hold both on one line. */
    .open-full-pill {
        display: inline-flex;
        align-items: center;
        position: absolute;
        right: 10px;
        top: 10px;
        z-index: 2;
        height: 32px;
        padding: 0 12px;
        border-radius: 999px;
        background: rgba(15, 16, 12, .72);
        backdrop-filter: blur(8px);
        color: #f9f8f6;
        font-family: 'Fira Code', ui-monospace, monospace;
        font-size: 11px;
        cursor: zoom-in;
    }

    /* ----- the full-size view ----- */

    /* The footer is two rows here: the view pill over the buttons. */
    .result:has(.full-toggle:checked) .frame {
        padding: calc(env(safe-area-inset-top) + 64px) 16px
                 calc(env(safe-area-inset-bottom) + 150px);
    }

    /* Open, it is the picture itself; a tap target over it would swallow the
       pinch and close the view on the first touch. */
    .result:has(.full-toggle:checked) .frame-tap {
        display: none;
    }

    .result:has(.full-toggle:checked) .lightbox-close {
        top: calc(env(safe-area-inset-top) + 12px);
        right: 16px;
    }

    .result:has(.full-toggle:checked) .card-foot {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        gap: 8px;
        padding: 14px 20px calc(env(safe-area-inset-bottom) + 20px);
    }

    /* A link is not one of the things to do to a picture you are looking at. */
    .result:has(.full-toggle:checked) .card-foot a.share,
    .result:has(.full-toggle:checked) .card-foot .copy-wrap {
        display: none;
    }

    .result:has(.full-toggle:checked) .zoom,
    .result:has(.full-toggle:checked) .share-system {
        justify-content: center;
        height: 44px;
        padding: 0 10px;
        border: 1px solid rgba(249, 248, 246, .28);
        border-radius: 9px;
        background: rgba(249, 248, 246, .08);
        color: #f9f8f6;
        font-weight: 700;
    }

    .result:has(.full-toggle:checked) .download {
        background: #f9f8f6;
        color: #24261F;
    }

    /* The picture's pill moves up over the footer. It has to be fixed rather
       than absolute: the frame it sits in is pointer-events:none up there, so
       the pill takes its own back. */
    .result:has(.full-toggle:checked) .frame .segmented.view-pill {
        position: fixed;
        left: 50%;
        /* 14 + 44 + 20 of footer, then a 10px gap. */
        bottom: calc(env(safe-area-inset-bottom) + 88px);
        transform: translateX(-50%);
        z-index: 103;
        pointer-events: auto;
        background: rgba(249, 248, 246, .1);
        backdrop-filter: none;
    }

    .result:has(.full-toggle:checked) .frame .segmented.view-pill label {
        padding: 6px 14px;
    }

    .result:has(.full-toggle:checked) .open-full-pill {
        display: none;
    }

    .result:has(.full-toggle:checked) .zoom-hint {
        display: block;
        position: fixed;
        left: 20px;
        top: calc(env(safe-area-inset-top) + 22px);
        z-index: 102;
        font-family: 'Fira Code', ui-monospace, monospace;
        font-size: 11px;
        color: rgba(249, 248, 246, .6);
    }

    .result:has(.full-toggle:checked) .zoom {
        display: inline-flex;
        align-items: center;
        color: #f9f8f6;
        font-size: 13px;
        cursor: pointer;
    }

    /* Zoomed, the frame scrolls its own overflow, so panning is the
       browser's job too. Nothing here sets touch-action, so a pinch still
       does what a pinch does. */
    .result:has(.full-toggle:checked):has(.zoom-toggle:checked) .frame {
        overflow: auto;
        align-items: flex-start;
        justify-content: flex-start;
        pointer-events: auto;
    }

    .result:has(.full-toggle:checked):has(.zoom-toggle:checked) .frame img {
        max-width: none;
        max-height: none;
        width: 200%;
    }

    /* ----- the docs ----- */

    .docs {
        padding: 18px 20px 60px;
        font-size: 15px;
    }

    .doc section {
        margin: 0 0 44px;
    }

    .doc h1 {
        margin: 0 0 10px;
    }

    .doc h2 {
        font-size: 24px;
        margin: 0 0 8px;
    }

    .doc h3 {
        font-size: 18px;
    }

    .doc .lead {
        font-size: 17px;
        margin: 0 0 20px;
    }

    /* Portraits stay side by side; the group photos would be a stripe. */
    .figures {
        gap: 12px;
    }

    .figures.portraits {
        grid-template-columns: 1fr 1fr;
    }

    .credit {
        margin: 0 0 24px;
    }

    code {
        font-size: 13px;
    }

    .snippet,
    .pair {
        margin: 0 0 18px;
    }

    .pair {
        gap: 10px;
    }

    .stages {
        gap: 10px 8px;
        margin: 0 0 32px;
    }

    .tiles {
        grid-template-columns: 1fr;
        gap: 10px;
    }

    .tile {
        display: flex;
        align-items: baseline;
        gap: 14px;
        padding: 12px 14px;
    }

    .tile b {
        flex: none;
        width: 56px;
        margin-bottom: 0;
    }

    .snippet pre {
        font-size: 12px;
    }

    /* ----- the footer ----- */

    .site-footer .container {
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: 6px;
    }

    /* ----- the shared page ----- */

    .shared-cta {
        margin-top: 20px;
    }

    .shared-cta .btn {
        width: 100%;
    }
}

/* ------------------------------------------------------------------ 404 --
   A wrong URL, and -- more often -- a shared link whose picture has been
   swept. Centred in the layout rather than dropped into the app's two-column
   grid, which has nothing to put in either column here. */
.lost {
    flex: 1 1 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    text-align: center;
    padding: 64px 0 80px;
}

.lost h1 {
    margin: 0;
}

.lost .lead {
    max-width: 46ch;
    margin: 0;
    color: var(--muted);
}

.lost-glasses {
    width: 132px;
    opacity: 0.5;
    filter: var(--invert);
    /* Sat on the nose of nothing at all. */
    transform: rotate(-8deg);
}

.lost-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    margin: 10px 0 0;
}

@media (max-width: 600px) {
    .lost {
        padding: 40px 0 56px;
    }

    .lost-glasses {
        width: 96px;
    }
}

/* ---------------------------------------------------------- shared result --
   /s/<id>: one result on its own page, for someone who was not here. */
.shared {
    flex: 1 1 100%;
    max-width: 720px;
    margin: 0 auto;
    padding: 32px 0 64px;
    text-align: center;
}

.shared h1 {
    margin: 0 0 4px;
}

.shared .lead {
    margin: 0 0 22px;
    color: var(--muted);
}

.shared .result {
    text-align: left;
}

/* The way back into the app for someone who arrived on a shared link and
   has never seen the front page. */
.shared-cta {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin-top: 28px;
    padding-top: 24px;
    border-top: 1px solid var(--border);
}

.shared-cta p {
    margin: 0;
    font-size: 13px;
    color: var(--muted);
}

/* "Copy link" rather than "Get a link": the link is the page you are on.
   No `display` here: this rule comes after `.mobile-only`, and one on the
   wrap would beat it and leave the phone's copy on the desktop row. Each
   place the wrap appears lays it out itself. */
.copy-wrap {
    position: relative;
}

.card-share .copy-wrap {
    display: inline-flex;
}

.card-share .copy-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 34px;
    padding: 0 14px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}

.card-share .copy-link:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.card-share .copy-link code {
    padding: 0;
    background: none;
    font-size: 11px;
    color: var(--muted);
}

/* Over the button, not beside it: the phone's row has no width to spare. */
.copy-wrap .copied {
    top: auto;
    right: auto;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    height: auto;
}

.copy-link:active + .copied {
    opacity: 1;
    transition: none;
}

/* The expiry line, on a card and on a shared page. countdown.js rewrites the
   text; the server-rendered absolute time is what it says without script. */
.card-share {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    /* No horizontal padding: .card-foot has none either, and the two rows sit
       under one another -- an inset here made the lower one look indented. */
    padding: 8px 0 10px;
    font-size: 0.82rem;
    color: var(--muted);
}

.expiry {
    font-variant-numeric: tabular-nums;
}

.expiry.gone {
    color: var(--danger, #c0392b);
}

.card-share .share {
    font-weight: 600;
}

/* --------------------------------------------------- download and share --
   One Download that opens the formats, rather than one link per format:
   which formats exist depends on what was submitted, so the row used to be
   two items wide sometimes and three others. */
.download-menu {
    position: relative;
}

.download-menu > summary {
    list-style: none;
    cursor: pointer;
}

.download-menu > summary::-webkit-details-marker {
    display: none;
}

/* The arrow, and which way it points. */
.download-menu > summary::after {
    content: '';
    display: inline-block;
    width: 0;
    height: 0;
    margin-left: 7px;
    vertical-align: middle;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid currentColor;
    transition: transform 0.12s ease;
}

.download-menu[open] > summary::after {
    transform: rotate(180deg);
}

.download-menu .formats {
    position: absolute;
    right: 0;
    bottom: calc(100% + 6px);
    z-index: 3;
    display: flex;
    flex-direction: column;
    min-width: 100%;
    padding: 4px;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    background: var(--surface);
    box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
}

.download-menu .formats a {
    padding: 7px 14px;
    border-radius: 6px;
    font-size: 0.82rem;
    font-weight: 600;
    text-decoration: none;
    color: var(--text);
    white-space: nowrap;
}

.download-menu .formats a:hover {
    background: var(--bg);
    color: var(--accent);
}

/* Given the footer's own size, so a desktop browser that can share a file
   does not draw the native button next to the styled Download. */
.card-foot .share-system {
    display: inline-flex;
    align-items: center;
    height: 34px;
    padding: 0 14px;
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}

.card-foot .share-system:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* On the dark overlay, where the theme's surfaces do not apply. */
.result:has(.full-toggle:checked) .share-system {
    border-color: rgba(249, 248, 246, .28);
    background: rgba(249, 248, 246, .08);
    color: #f9f8f6;
}

/* Hidden until share.js has asked the browser whether it can share a file.
   `[hidden]` alone loses to the flex display the footer gives its children. */
.card-foot .share-system[hidden] {
    display: none;
}

/* The phone's own sizes for the three sections above. They are down here
   rather than up in the phone layout because that block comes first in the
   file, and these override rules of the same specificity: source order is
   what decides, so the override has to be the later one. */
@media (max-width: 600px) {
    /* One row of equal buttons, all 44px: the size a thumb needs. */
    .card-foot .share-system,
    .card-foot a.share,
    .card-foot .copy-link {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        height: 44px;
        padding: 0 10px;
        border: 1px solid var(--border-strong);
        border-radius: 9px;
        background: var(--surface);
        color: var(--text);
        font-size: 13px;
        font-weight: 700;
        text-decoration: none;
        white-space: nowrap;
    }

    .card-foot .share-system:hover,
    .card-foot a.share:hover,
    .card-foot .copy-link:hover {
        border-color: var(--accent);
        color: var(--accent);
    }

    /* The expiry line, under the row rather than beside anything. */
    .card-share {
        padding: 0;
        margin-top: -4px;
        font-family: 'Fira Code', ui-monospace, monospace;
        font-size: 12px;
    }

    /* Over the Download cell rather than the right edge of the row. */
    .download-menu .formats {
        left: 0;
        right: 0;
        min-width: 0;
    }

    .download-menu .formats a {
        display: flex;
        align-items: center;
        height: 40px;
        padding: 0 14px;
        font-size: 14px;
    }
}
