:root {
    /* Black album paper, faintly green — not a neutral charcoal */
    --ink: #12140f;
    --ink-raised: #1b1e17;
    --ink-line: rgba(247, 244, 236, 0.09);

    /* The white border of a print */
    --paper: #f7f4ec;
    --paper-dim: #a7ac9c;

    /* The date an old camera burned into the corner of the negative.
       This is the editorial voice: eyebrows, frame numbers, captions. */
    --stamp: #ff7a1a;

    /* Telegram's own blue, kept for one job only: the button that leaves for
       Telegram. Two accents are justified when they mean different things. */
    --tg: #2aabee;
    --tg-hover: #3cb8f7;

    --display: 800 clamp(2.4rem, 7.2vw, 4.15rem)/0.98 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    --ease: cubic-bezier(0.2, 0, 0, 1);

    /* Layered, so it sits on any surface without drawing a hard line */
    --lift:
        0 1px 1px rgba(0, 0, 0, 0.5),
        0 8px 24px rgba(0, 0, 0, 0.35),
        inset 0 1px 0 rgba(247, 244, 236, 0.06);
}

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

html {
    scroll-behavior: smooth;
    /* clip rather than hidden: hidden on body alone still lets the viewport
       itself scroll sideways on iOS */
    overflow-x: clip;
}

body {
    margin: 0;
    background: var(--ink);
    color: var(--paper);
    font: 400 17px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
    letter-spacing: -0.011em;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-wrap: pretty;
    overflow-x: hidden;
}

/* A single ambient warm pool behind the hero, like light through album paper.
   One atmospheric gesture, not several. */
body::before {
    content: "";
    position: fixed;
    inset: -30vh 0 auto;
    height: 90vh;
    pointer-events: none;
    background: radial-gradient(60% 50% at 62% 30%, rgba(255, 122, 26, 0.13), transparent 70%);
}

.wrap {
    width: min(68rem, 100% - 2.5rem);
    margin-inline: auto;
}

/* ── the date-stamp voice ─────────────────────────────────────────── */
.stamp {
    font-family: var(--mono);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--stamp);
    font-variant-numeric: tabular-nums;
}

/* ── header ───────────────────────────────────────────────────────── */
.top {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.5rem 0 0;
}

.mark {
    font-family: var(--mono);
    font-size: 0.92rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--paper);
    text-decoration: none;
}

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

/* ── buttons ──────────────────────────────────────────────────────── */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    min-height: 52px;
    padding: 0 1.6rem;
    border-radius: 0.8rem;
    font-size: 1.02rem;
    font-weight: 620;
    letter-spacing: -0.015em;
    text-decoration: none;
    white-space: nowrap;
    transition-property: transform, background-color, box-shadow, color;
    transition-duration: 0.18s;
    transition-timing-function: var(--ease);
}

/* White on #2aabee is 3.0:1, which only clears AA at large-text size — so the
   label is set at 18.9px/700 rather than quietly failing at 16px. */
.btn-primary {
    min-height: 56px;
    padding: 0 1.75rem;
    background: var(--tg);
    color: #fff;
    font-size: 1.18rem;
    font-weight: 700;
    box-shadow: var(--lift);
}

.btn-primary:hover {
    background: var(--tg-hover);
    transform: translateY(-1px);
}

.btn-quiet {
    min-height: 44px;
    padding: 0 1.05rem;
    font-size: 0.94rem;
    color: var(--paper);
    box-shadow: inset 0 0 0 1px var(--ink-line);
}

.btn-quiet:hover {
    background: var(--ink-raised);
}

.btn:active {
    transform: scale(0.96);
}

.btn:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 3px;
}

/* Telegram's plane carries its mass low-left; geometric centring reads high */
.btn svg {
    flex: none;
    translate: -1px 1px;
}

/* ── hero ─────────────────────────────────────────────────────────── */
.hero {
    position: relative;
    display: grid;
    gap: clamp(2.5rem, 6vw, 4rem);
    grid-template-columns: 1fr;
    align-items: center;
    padding: clamp(3rem, 8vw, 5.5rem) 0 clamp(3.5rem, 9vw, 6rem);
}

@media (min-width: 62rem) {
    .hero {
        grid-template-columns: 1.05fr 0.95fr;
    }

    /* A fixed height, filled from the bottom the way a real chat is. The examples
       are different lengths and the typing bubble collapses mid-sequence, so
       without this the panel resizes and drags the headline, the buttons and the
       whole page height around with it. The bar at the top is extra height, not
       a share of this — taking it out of the conversation cut the top off the
       first photograph. */
    .thread {
        height: 48rem;
        justify-content: flex-end;
        overflow: hidden;
    }
}

h1 {
    font: var(--display);
    letter-spacing: -0.042em;
    margin: 0.9rem 0 0;
    text-wrap: balance;
}

h1 em {
    font-style: normal;
    color: var(--stamp);
    /* Cross-fade rather than a hard swap: scale 0.98 and a touch of blur read
       as the words being replaced, not as the page repainting. */
    display: inline-block;
    transition-property: opacity, scale, filter;
    transition-duration: 0.28s;
    transition-timing-function: var(--ease);
}

h1 em[data-out] {
    opacity: 0;
    scale: 0.98;
    filter: blur(4px);
}

/* Reserve room for the tallest phrase so nothing below the headline moves when it
   changes. Three lines at desktop widths, four once it wraps harder; the script
   measures the real phrases and overrides this exactly, so these are only what a
   reader without JavaScript gets. Scoped with :has() because only the cycling
   headline has an <em> — the per-use-case pages must not inherit dead space. */
h1:has(em) {
    min-height: 3.92em;
}

@media (min-width: 62rem) {
    h1:has(em) {
        min-height: 2.94em;
    }
}

.lede {
    max-width: 33ch;
    margin: 1.25rem 0 0;
    font-size: clamp(1.05rem, 2.3vw, 1.19rem);
    color: var(--paper-dim);
}

.hero-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin-top: 2rem;
}

/* Side by side, the two have to agree on height. The quiet button stays small
   in the header, where it sits next to nothing. */
.hero-actions .btn-quiet {
    min-height: 56px;
    padding: 0 1.4rem;
    font-size: 1.02rem;
}

.fineprint {
    margin: 1rem 0 0;
    font-size: 0.9rem;
    color: var(--paper-dim);
}

/* ── the thread: the signature element ────────────────────────────── */

/* The shell around it. The bar at the top does no work beyond saying what you
   are looking at is a chat on a phone, so it is quiet enough to skip over. */
.phone {
    display: flex;
    flex-direction: column;
    width: min(24rem, 100%);
    margin-inline: auto;
    border-radius: 1.35rem;
    background: linear-gradient(180deg, #1d211a, #171a14);
    box-shadow: var(--lift);
    overflow: hidden;
}

.phone-bar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.7rem 0.9rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
    background: rgba(255, 255, 255, 0.025);
}

.phone-back {
    width: 1.1rem;
    height: 1.1rem;
    flex: 0 0 auto;
    color: var(--paper-dim);
    opacity: 0.7;
}

.phone-avatar {
    display: grid;
    place-items: center;
    width: 1.9rem;
    height: 1.9rem;
    flex: 0 0 auto;
    border-radius: 50%;
    background: #2f5d47;
    color: #eaf3ea;
}

.phone-avatar svg {
    width: 0.95rem;
    height: 0.95rem;
}

.phone-who {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
    min-width: 0;
}

.phone-who b {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--paper);
}

/* Not italic — it is a status line, and the tag is only there to name it */
.phone-who i {
    font-style: normal;
    font-size: 0.72rem;
    color: var(--paper-dim);
}

.thread {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
    padding: 1rem;
}

/* 1.15rem of padding inside a 1.35rem shell wants ~0.2rem left over, but a
   chat bubble has its own conventional shape — so the bubbles set the radius
   and the shell is derived from them instead of the other way round. */
.bubble {
    max-width: 82%;
    padding: 0.62rem 0.85rem;
    border-radius: 1.05rem;
    font-size: 0.95rem;
    line-height: 1.45;
}

.bubble.from-user {
    align-self: flex-end;
    border-bottom-right-radius: 0.35rem;
    background: #2f5d47;
    color: #f2f7f2;
}

.bubble.from-bot {
    align-self: flex-start;
    border-bottom-left-radius: 0.35rem;
    background: #262a21;
    color: var(--paper);
}

.bubble time {
    display: block;
    margin-top: 0.15rem;
    font-family: var(--mono);
    font-size: 0.63rem;
    letter-spacing: 0.06em;
    opacity: 0.5;
    font-variant-numeric: tabular-nums;
}

/* A print: white border, near-square corners, the paper it is actually on */
.print {
    align-self: flex-end;
    width: min(7.2rem, 48%);
    padding: 0.4rem 0.4rem 1.35rem;
    border-radius: 0.2rem;
    background: var(--paper);
    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4), 0 14px 30px rgba(0, 0, 0, 0.4);
    rotate: 0.7deg;
}

.print.from-bot {
    align-self: flex-start;
    rotate: -0.9deg;
}

.print svg,
.print img {
    display: block;
    width: 100%;
    height: auto;
    /* A real photo is not always 4:5, but the frame has to be, or the thread
       jumps every time the conversation cycles */
    aspect-ratio: 120 / 148;
    object-fit: cover;
    /* Pure white at 0.1 — a tinted neutral would read as dirt on the edge */
    outline: 1px solid rgba(255, 255, 255, 0.1);
    outline-offset: -1px;
}

.print figcaption {
    padding-top: 0.5rem;
    font-family: var(--mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #b4360f;
    font-variant-numeric: tabular-nums;
}

/* The faded original, and the same frame restored */
.tone-faded {
    --p-sky: #b9b4a8;
    --p-body: #6f6d67;
    --p-skin: #9a958c;
    --p-hair: #706c66;
    --p-collar: #838079;
    --p-damage: 1;
}

.tone-restored {
    --p-sky: #9db8c4;
    --p-body: #7a4e39;
    --p-skin: #e6b894;
    --p-hair: #4a352a;
    --p-collar: #f0e6d6;
    --p-damage: 0;
}

/* Same shapes, wildly different instructions — which is the point. These are
   stand-ins until real before/after photos land; see site/img/README.md. */
.tone-hero {
    --p-sky: #2b3f8f;
    --p-body: #c62b2b;
    --p-skin: #e6b894;
    --p-hair: #4a352a;
    --p-collar: #f2c94c;
    --p-damage: 0;
}

.tone-moon {
    --p-sky: #0b0e1c;
    --p-body: #e8ecf2;
    --p-skin: #e6b894;
    --p-hair: #4a352a;
    --p-collar: #b9c2d4;
    --p-damage: 0;
}

.tone-oil {
    --p-sky: #6b5a3e;
    --p-body: #3a2f24;
    --p-skin: #d9a97f;
    --p-hair: #2b2118;
    --p-collar: #d9c9a8;
    --p-damage: 0;
}

.tone-anime {
    --p-sky: #ffd2e4;
    --p-body: #5b8dd9;
    --p-skin: #ffe0c8;
    --p-hair: #7b3fb0;
    --p-collar: #ffffff;
    --p-damage: 0;
}

.tone-brick {
    --p-sky: #f2d024;
    --p-body: #c8102e;
    --p-skin: #ffd400;
    --p-hair: #2b2118;
    --p-collar: #0057a8;
    --p-damage: 0;
}

.tone-snapshot {
    --p-sky: #cdd6c8;
    --p-body: #4a5b6e;
    --p-skin: #e6b894;
    --p-hair: #4a352a;
    --p-collar: #eef1ea;
    --p-damage: 0;
}

.typing {
    display: inline-flex;
    gap: 0.22rem;
    align-items: center;
    height: 0.75rem;
}

.typing i {
    width: 0.32rem;
    height: 0.32rem;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.45;
    animation: blink 1.25s var(--ease) infinite;
}

.typing i:nth-child(2) { animation-delay: 0.16s; }
.typing i:nth-child(3) { animation-delay: 0.32s; }

@keyframes blink {
    0%, 60%, 100% { opacity: 0.25; }
    30% { opacity: 0.9; }
}

/* The load sequence: each message arrives on its own beat, the way it would
   in the real chat. One orchestrated moment; everything else on the page
   stays still. */
.thread.play > * {
    animation: send 0.5s var(--ease) backwards;
}

/* Delays are set inline per message by the script, because the conversation is
   rebuilt each time it cycles and nth-child would go stale. */

/* The typing indicator has to leave again — a bot that is still typing after
   it has answered is the one detail that would give the whole thing away.
   It collapses its own height and eats the flex gap on the way out, so the
   picture below lands where the dots were. Its duration is TYPING_MS in the
   script; the result is scheduled to land after it, never during. */
.thread.play > .is-typing {
    overflow: hidden;
    animation-name: type-and-go;
    animation-duration: 1.2s;
    animation-fill-mode: both;
}

/* Leaving is quieter than arriving, and it overrides the arrival animation
   rather than fighting it */
.thread.leaving > *,
.thread.leaving > .is-typing {
    animation: none;
    opacity: 0;
    transform: translateY(6px);
    transition-property: opacity, transform;
    transition-duration: 0.34s;
    transition-timing-function: var(--ease);
}

/* Every collapsing property is pinned at 72% as well as at the end. Leaving
   one out means it interpolates across the whole animation instead, and the
   bubble starts shrinking the moment it appears. */
@keyframes type-and-go {
    0% {
        opacity: 0;
        transform: translateY(10px) scale(0.97);
        max-height: 3rem;
        padding-block: 0.62rem;
        margin-bottom: 0;
    }

    35%,
    72% {
        opacity: 1;
        transform: none;
        max-height: 3rem;
        padding-block: 0.62rem;
        margin-bottom: 0;
    }

    100% {
        opacity: 0;
        transform: none;
        max-height: 0;
        padding-block: 0;
        margin-bottom: -0.5rem;
    }
}

@keyframes send {
    from { opacity: 0; transform: translateY(10px) scale(0.97); }
    to { opacity: 1; transform: none; }
}

/* ── sections ─────────────────────────────────────────────────────── */
.band {
    padding: clamp(3.5rem, 8vw, 6rem) 0;
    box-shadow: inset 0 1px 0 var(--ink-line);
}

.band-head {
    max-width: 40rem;
    margin-bottom: 2.25rem;
}

h2 {
    margin: 0.7rem 0 0;
    font-size: clamp(1.6rem, 3.6vw, 2.15rem);
    font-weight: 750;
    letter-spacing: -0.032em;
    line-height: 1.1;
    text-wrap: balance;
}

.band-head p {
    margin: 0.75rem 0 0;
    color: var(--paper-dim);
    max-width: 46ch;
}

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

.card {
    padding: 1.35rem 1.4rem 1.5rem;
    border-radius: 1.15rem;
    background: var(--ink-raised);
    box-shadow: var(--lift);
}

/* Line icons in the date-stamp colour, so they read as part of the same
   editorial voice as the eyebrows rather than as a separate icon set. */
.card .icon {
    display: block;
    width: 26px;
    height: 26px;
    color: var(--stamp);
}

/* Headings inherit the body's 1.6 otherwise, which is set for paragraphs and
   falls apart the moment a two-word heading wraps */
.card h3 {
    margin: 0.85rem 0 0.4rem;
    font-size: 1.06rem;
    font-weight: 640;
    line-height: 1.25;
    letter-spacing: -0.02em;
}

.card p {
    margin: 0;
    color: var(--paper-dim);
    font-size: 0.96rem;
}

/* ── gallery ──────────────────────────────────────────────────────────────
   Every result on the site at once, each one a door into the page it came from.
   Only the finished pictures: the before-and-after is the argument the pages
   make, and this grid is there to be looked at rather than read.

   One shape for all of them, cropped rather than letterboxed — a grid of mixed
   aspect ratios reads as a pile of files, not a body of work. The full picture
   in its own shape is one click away. */
.gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
    gap: 0.9rem;
}

.tile {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: 0.7rem;
    background: #14170f;
    outline: 1px solid rgba(255, 255, 255, 0.07);
    outline-offset: -1px;
    text-decoration: none;
    transition-property: outline-color, box-shadow, scale;
    transition-duration: 0.25s;
    transition-timing-function: var(--ease);
}

.tile img {
    display: block;
    width: 100%;
    height: 100%;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    transition: scale 0.4s var(--ease);
}

/* The label sits on the picture, so it needs its own ground to stand on */
.tile span {
    position: absolute;
    inset: auto 0 0;
    padding: 2.2rem 0.7rem 0.6rem;
    background: linear-gradient(180deg, transparent, rgba(10, 12, 8, 0.82) 62%);
    color: var(--paper);
    font-size: 0.86rem;
    font-weight: 560;
    letter-spacing: -0.01em;
}

.tile:hover {
    outline-color: rgba(255, 255, 255, 0.22);
    box-shadow: 0 14px 30px rgba(0, 0, 0, 0.45);
}

/* The picture moves inside the frame rather than the frame moving on the page —
   twelve of these lifting at once would make the section restless. */
.tile:hover img {
    scale: 1.04;
}

.tile:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .tile:hover img {
        scale: 1;
    }
}

/* These are a real sequence, so they are numbered — in the headings, where a
   reader actually reads them, rather than as a decorative marker. */
.steps {
    display: grid;
    gap: 1.6rem 2rem;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}

.step {
    padding-top: 1.15rem;
    box-shadow: inset 0 1px 0 var(--ink-line);
}

.step h3 {
    margin: 0 0 0.35rem;
    font-size: 1.04rem;
    font-weight: 640;
    line-height: 1.25;
    letter-spacing: -0.02em;
}

.step p {
    margin: 0;
    color: var(--paper-dim);
    font-size: 0.96rem;
}

/* ── price ────────────────────────────────────────────────────────── */
.price {
    display: grid;
    gap: 1.4rem;
    align-items: center;
    padding: clamp(1.5rem, 4vw, 2.25rem);
    border-radius: 1.35rem;
    background: var(--ink-raised);
    box-shadow: var(--lift);
}

@media (min-width: 48rem) {
    .price {
        grid-template-columns: 1fr auto;
    }
}

.price h2 {
    margin: 0.55rem 0 0.5rem;
}

.price p {
    margin: 0;
    color: var(--paper-dim);
    max-width: 48ch;
}

/* ── faq ──────────────────────────────────────────────────────────── */
.faq {
    max-width: 46rem;
}

details {
    padding: 1.05rem 0;
    box-shadow: inset 0 1px 0 var(--ink-line);
}

summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: 40px;
    font-size: 1.03rem;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -0.018em;
    cursor: pointer;
    list-style: none;
}

summary::-webkit-details-marker {
    display: none;
}

summary::after {
    content: "";
    flex: none;
    width: 0.55rem;
    height: 0.55rem;
    margin-right: 0.35rem;
    border-right: 1.5px solid var(--stamp);
    border-bottom: 1.5px solid var(--stamp);
    rotate: 45deg;
    transition: rotate 0.2s var(--ease);
}

details[open] summary::after {
    rotate: 225deg;
}

summary:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 4px;
    border-radius: 0.35rem;
}

details p {
    margin: 0.6rem 0 0;
    color: var(--paper-dim);
    font-size: 0.97rem;
    max-width: 62ch;
}

/* ── close ────────────────────────────────────────────────────────── */
.close {
    text-align: center;
    padding: clamp(4rem, 10vw, 7rem) 0;
    box-shadow: inset 0 1px 0 var(--ink-line);
}

.close h2 {
    max-width: 18ch;
    margin-inline: auto;
}

.close .btn {
    margin-top: 1.9rem;
}

footer {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.5rem;
    justify-content: space-between;
    padding: 1.5rem 0 2.5rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--paper-dim);
}

footer a {
    color: inherit;
    text-decoration-color: var(--ink-line);
}

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

@media (prefers-reduced-motion: reduce) {

    html {
        scroll-behavior: auto;
    }

    .thread.play > *,
    .typing i {
        animation: none;
    }

    /* Without the sequence there is nothing for the dots to be waiting on */
    .thread.play > .is-typing {
        display: none;
    }

    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
    }
}

/* ── pieces used by the per-use-case pages ────────────────────────────── */

/* A pair of prints, side by side, captioned. This is the slot real before/after
   photographs drop into — see site/img/README.md. */
.pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.9rem;
    align-items: start;
    width: min(26rem, 100%);
    margin-inline: auto;
}

/* Grid and flex items default to min-width:auto, and a replaced element with a
   percentage width reports its intrinsic size as the minimum — so a 620px-wide
   photograph refuses to let its column shrink below that and the last one hangs
   off the side of a phone. This is the fix for that, and it is needed on both
   the item and the track. */
.pair,
.stack,
.pair .print,
.stack .print {
    min-width: 0;
}

.pair .print {
    align-self: start;
    width: 100%;
}

.pair .print:first-child { rotate: -1.2deg; }
.pair .print:last-child { rotate: 1.2deg; }

.crumbs {
    margin: 1.75rem 0 0;
}

.crumbs a {
    color: var(--paper-dim);
    text-decoration: none;
    font-family: var(--mono);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.crumbs a:hover { color: var(--paper); }

/* Cross-links between the pages. A card that is a link has to say so on hover,
   and has to keep the whole card as the target rather than just the words. */
a.card {
    display: block;
    text-decoration: none;
    color: inherit;
    transition-property: transform, background-color;
    transition-duration: 0.18s;
    transition-timing-function: var(--ease);
}

a.card:hover {
    transform: translateY(-2px);
    background: #212519;
}

a.card:active { transform: scale(0.99); }

a.card:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 3px;
}

.tips {
    max-width: 46rem;
    padding-left: 1.15rem;
    color: var(--paper-dim);
}

.tips li {
    margin-bottom: 0.6rem;
}

.tips strong {
    color: var(--paper);
    font-weight: 600;
}

/* Long-form pages: privacy, terms. Narrow measure, nothing else. */
.prose {
    max-width: 42rem;
    padding: clamp(2rem, 6vw, 3.5rem) 0 4rem;
}

.prose h2 {
    font-size: 1.25rem;
    margin: 2.5rem 0 0.6rem;
}

.prose h1 {
    font-size: clamp(2rem, 5vw, 2.8rem);
    margin-bottom: 0.75rem;
}

.prose p,
.prose li {
    color: var(--paper-dim);
}

.prose li { margin-bottom: 0.5rem; }

.prose a { color: var(--stamp); }

.prose .note {
    margin-top: 2.5rem;
    padding: 1.1rem 1.3rem;
    border-radius: 1.15rem;
    background: var(--ink-raised);
    box-shadow: var(--lift);
    font-size: 0.95rem;
}

/* Every page reachable from every page. This is what actually gets the
   per-use-case pages crawled — a sitemap tells a search engine a URL exists, an
   internal link tells it the page matters. */
.sitenav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.4rem;
    margin-top: 4rem;
    padding-top: 1.4rem;
    box-shadow: inset 0 1px 0 var(--ink-line);
    font-size: 0.9rem;
}

.sitenav a {
    color: var(--paper-dim);
    text-decoration: none;
    padding-block: 0.35rem;
}

.sitenav a:hover {
    color: var(--paper);
    text-decoration: underline;
}

.sitenav a:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 3px;
    border-radius: 0.3rem;
}

/* The nav already draws the rule above it */
.sitenav + footer {
    margin-top: 1.75rem;
    padding-top: 0;
    box-shadow: none;
}

/* A real photograph keeps the shape it was taken in. The drawn stand-in is 4:5;
   cropping a landscape photo into that would hide the half of the picture the
   page exists to show. */
.print.photo img {
    aspect-ratio: auto;
    object-fit: fill;
}

/* Not in the conversation. The panel there is a fixed height filled from the
   bottom, so a photograph that turns out taller than expected pushes the start
   of the conversation out of the top of it — and until a lazy image has loaded
   its frame has no height at all, so the whole thread jumped downwards as each
   one arrived. Every photo in the thread is therefore cropped to the frame's
   own shape, which is what a chat does with a thumbnail anyway. */
.thread .print.photo img {
    aspect-ratio: 120 / 148;
    object-fit: cover;
}

/* Landscape pairs need the room. Two 4:3 prints at 12rem each are too small to
   see the difference, which is the only reason they are there. */
.pair:has(.photo) {
    width: min(38rem, 100%);
}

@media (min-width: 62rem) {

    /* A page whose proof is a photograph gives the photograph the wider column */
    .hero:has(.pair .photo) {
        grid-template-columns: 0.9fr 1.1fr;
    }
}

@media (max-width: 34rem) {
    .pair {
        grid-template-columns: 1fr;
    }
}


/* Picking a print up off the table: it straightens, comes forward a little, and
   its shadow deepens. Motion is small on purpose — the page is a photo album, not
   a carousel, and the only job here is to say the thing is an object. */
.print {
    position: relative;
    transition-property: rotate, scale, box-shadow;
    transition-duration: 0.3s;
    transition-timing-function: var(--ease);
}

.pair .print:hover,
.pair .print:focus-within {
    rotate: 0deg;
    scale: 1.045;
    z-index: 1;
    box-shadow:
        0 3px 5px rgba(0, 0, 0, 0.45),
        0 24px 48px rgba(0, 0, 0, 0.5);
}

/* In the conversation the prints are small and closely stacked, so they straighten
   without growing — scaling one would push into the bubble above it. */
.thread .print:hover {
    rotate: 0deg;
    z-index: 1;
    box-shadow:
        0 3px 5px rgba(0, 0, 0, 0.45),
        0 20px 40px rgba(0, 0, 0, 0.45);
}

/* ── lightbox ─────────────────────────────────────────────────────────────
   Any container with [data-lightbox] becomes a group: clicking a print inside
   it opens every print in that group, big. One <dialog> is shared by all the
   groups on a page, so this costs the same whether there is one or six. */

[data-lightbox] .print {
    cursor: zoom-in;
}

[data-lightbox] .print:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 4px;
}

/* No panel. The prints already carry a white border and a shadow, so a box behind
   them is a second frame around a frame — the darkened backdrop is the container. */
.lightbox {
    width: min(94vw, 68rem);
    max-width: none;
    /* dvh, not vh: on a phone vh is the height with the browser chrome hidden, so
       a panel sized to it is taller than the screen actually is */
    max-height: 92dvh;
    margin: auto;
    padding: 2dvh 0;
    border: 0;
    background: transparent;
    color: var(--paper);
    /* Three portrait prints do not fit a phone at any size worth looking at, so
       the dialog scrolls rather than clipping them */
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
}

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

.lightbox::backdrop {
    background: rgba(6, 7, 5, 0.86);
    backdrop-filter: blur(5px);
}

/* The dialog itself only fades. A transform on it would make it the containing
   block for the fixed-position close button, which then sits against the dialog
   until the animation ends and snaps to the corner — so the movement lives on the
   stage inside instead. */
.lightbox[open] {
    animation: lightbox-fade 0.2s var(--ease);
}

.lightbox[open] .lightbox-stage {
    animation: lightbox-in 0.28s var(--ease);
}

@keyframes lightbox-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes lightbox-in {
    from { opacity: 0; transform: translateY(10px) scale(0.985); }
    to { opacity: 1; transform: none; }
}

/* Prints laid on a table rather than in a grid — they overlap slightly, and
   whichever one you point at comes to the top. */
.lightbox-stage {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 0;
}

/* fit-content, not a flex basis. The image is capped by height, so a frame that
   stretches to fill its share of the row ends up wider than the picture inside it
   and shows a band of blank paper down one side. */
.lightbox-stage .print {
    flex: 0 1 auto;
    width: fit-content;
    max-width: min(30rem, 46%);
    padding: 0.6rem 0.6rem 1.9rem;
    cursor: default;
}

/* Three prints at the two-print size wrap onto a second row, which is what was
   making the dialog scroll. Smaller cap when there are three. */
.lightbox-stage:has(.print:nth-child(3)) .print {
    max-width: 32%;
}

.lightbox-stage:has(.print:nth-child(3)) .print img,
.lightbox-stage:has(.print:nth-child(3)) .print svg {
    max-height: 56dvh;
}

/* Sized so the whole stack fits without the dialog needing to scroll. A scrolled
   print is cut off flat at the top edge, which on a white frame reads as damage
   rather than as scrolling. */
.lightbox-stage .print img,
.lightbox-stage .print svg {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 64dvh;
}

/* Enough overlap to read as a stack, not so much that it covers the caption of
   the print underneath. A proportion rather than a fixed distance, so it stays
   the same 7% whether there are two prints or three. */
.lightbox-stage .print + .print {
    margin-left: -7%;
}

.lightbox-stage .print figcaption {
    padding-top: 0.7rem;
    font-size: 0.72rem;
}

.lightbox-stage .print:hover,
.lightbox-stage .print[data-front] {
    rotate: 0deg;
    scale: 1.02;
    z-index: 2;
    box-shadow:
        0 4px 6px rgba(0, 0, 0, 0.45),
        0 30px 60px rgba(0, 0, 0, 0.55);
}

/* Pointing at one has to beat having clicked another, or the stack stops
   responding after the first hover */
.lightbox-stage:hover .print[data-front]:not(:hover) {
    z-index: 1;
    scale: 1;
}

@media (max-width: 44rem) {
    .lightbox-stage {
        flex-direction: column;
        align-items: center;
    }

    .lightbox-stage .print {
        flex: 0 0 auto;
        width: min(19rem, 86vw);
        max-width: none;
    }

    .lightbox-stage .print + .print {
        margin-left: 0;
        margin-top: -7%;
    }

    /* Stacked vertically there is room for each one to be worth looking at, and
       the dialog scrolls to reach them */
    .lightbox-stage .print img,
    .lightbox-stage .print svg {
        max-height: none;
        width: 100%;
    }
}

/* Anchored to the viewport rather than to a panel that no longer exists. The
   dialog is in the top layer, so fixed positioning is relative to the screen. */
.lightbox-close {
    position: fixed;
    top: 1.25rem;
    right: 1.25rem;
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    background: var(--paper);
    color: var(--ink);
    font-size: 1.35rem;
    line-height: 1;
    cursor: pointer;
    box-shadow: var(--lift);
    transition-property: transform, background-color;
    transition-duration: 0.18s;
    transition-timing-function: var(--ease);
}

.lightbox-close:hover { transform: scale(1.06); }
.lightbox-close:active { transform: scale(0.96); }

.lightbox-close:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 3px;
}

/* showModal() blocks interaction but not scrolling */
html:has(.lightbox[open]) {
    overflow: hidden;
}

/* Attribution for the source photographs. Small, under the pair, and a link
   rather than a name — the point is that someone can follow it. */
.credit {
    margin: 0.9rem 0 0;
    text-align: center;
    font-family: var(--mono);
    font-size: 0.66rem;
    letter-spacing: 0.06em;
    color: var(--paper-dim);
}

.credit a {
    color: inherit;
    text-decoration-color: var(--ink-line);
}

.credit a:hover {
    color: var(--paper);
    text-decoration-color: currentColor;
}

/* Three states of one photograph rather than two */
.pair.trio {
    grid-template-columns: repeat(3, 1fr);
    width: min(40rem, 100%);
}

@media (max-width: 56rem) {
    .pair.trio {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* Three portraits two-up on a phone are too small to read as photographs, and
   the odd one out looks like a mistake */
@media (max-width: 34rem) {
    .pair.trio {
        grid-template-columns: 1fr;
        width: min(20rem, 100%);
    }
}

/* Two photos in, one out — the layout says it before the copy does: a small
   column of inputs, then the single larger result. The inputs stacked are twice
   as tall as they are wide, so the column has to be narrow or they dominate the
   thing they are inputs to. */
.pair.combine {
    grid-template-columns: 0.33fr 0.67fr;
    align-items: center;
    width: min(34rem, 100%);
}

.stack {
    display: grid;
    gap: 0.7rem;
}

.stack .print {
    width: 100%;
}

.stack .print:first-child { rotate: -1.6deg; }
.stack .print:last-child { rotate: 1.4deg; }

@media (max-width: 34rem) {
    .pair.combine {
        grid-template-columns: 1fr;
    }

    .stack {
        grid-template-columns: 1fr 1fr;
    }
}


/* Several photos sent at once, fanned rather than listed. Two full prints plus a
   result is taller than the panel, and the top of the conversation was the part
   that got cut off — this makes one object out of them, and every photo stays
   visible instead of the last one hiding the rest. */
.thread .print-stack {
    align-self: flex-end;
    display: flex;
    align-items: flex-start;
    animation: send 0.5s var(--ease) backwards;
}

/* An explicit width, because the fan's own width is shrink-to-fit — a percentage
   here would resolve against the container it is helping to size, which is
   circular and gives different answers in different engines. */
.thread .print-stack .print {
    align-self: auto;
    flex: none;
    width: 7.2rem;
    animation: none;
}

.thread .print-stack .print + .print {
    /* Deep overlap on purpose: the fan is one object, not a row. Roughly 55% of a
       print, so the one underneath keeps its caption. */
    margin-left: -4rem;
}

.thread .print-stack .print:hover {
    z-index: 5 !important;
    rotate: 0deg !important;
}

/* ── language switcher ────────────────────────────────────────────────────
   Three links, not a dropdown. A dropdown hides the fact that the other
   languages exist, and a crawler follows a link. */
.langs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.2rem;
    margin-top: 1.5rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
}

.langs a {
    color: var(--paper-dim);
    text-decoration: none;
    padding-block: 0.35rem;
}

.langs a:hover { color: var(--paper); }

.langs a[aria-current] {
    color: var(--stamp);
}

.langs a:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 3px;
    border-radius: 0.3rem;
}

/* ── showcase ─────────────────────────────────────────────────────────────
   The photographs, big and side by side: what went in, what came back, and
   underneath them what was asked for, in the words somebody actually typed.

   Every slide sits in the same grid cell so the section never changes height
   as it cycles, and so one can slide in over the one it replaces. The overflow
   is clipped because they arrive and leave past the edges of the section. */
.showcase {
    display: grid;
    /* An auto track would be sized by the photographs' intrinsic widths — six
       6000px originals wide — and overflow the page. */
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    overflow: hidden;
    /* Pictures and a quoted line, not a document. Dragging across it only ever
       produces a stray half of somebody else's sentence, and the caret cursor
       invites a click that does nothing. */
    user-select: none;
    -webkit-user-select: none;
}

.slide {
    grid-area: 1 / 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 1.15rem;
    margin: 0;
    width: 100%;
    /* So the row below can work out how tall the photographs may be from how
       much width it actually has */
    container-type: inline-size;
    opacity: 0;
    visibility: hidden;
    /* Waiting off to the right, where it will come in from */
    translate: 4% 0;
    transition-property: opacity, translate;
    transition-duration: 0.5s;
    transition-timing-function: var(--ease);
}

.slide.is-current {
    opacity: 1;
    visibility: visible;
    translate: 0 0;
}

/* Right to left: the one being replaced keeps moving the way the new one came,
   so the pair reads as one movement rather than a crossfade. app.js hangs this
   on the outgoing slide for the length of the transition. */
.slide.is-leaving:not(.is-current) {
    visibility: visible;
    translate: -4% 0;
}

/* Every step at once, at the size the difference is actually visible. Sized by
   height rather than width: a portrait pair sized to fill the row would be
   twice as tall as a landscape one, and every slide shares a grid cell with the
   tallest of them. Equal heights also crop nothing, which is the point of a
   before and after.

   --ratios and --shots come from the generator, which reads them out of the
   files: --ratios is how wide the row is per unit of height, so dividing the
   width left over by it gives the tallest the photographs may be and still fit.
   Without that the wide pairs would be squashed to fit, or run off the page. */
.shots {
    --gutter: 1.25rem;
    --arrow: 2.6rem;
    --frame: 1.1rem;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: var(--gutter);
    width: 100%;
}

.showcase .print {
    display: block;
    width: max-content;
    min-width: 0;
    max-width: 100%;
    padding: 0.55rem 0.55rem 1.6rem;
}

.showcase .print img {
    display: block;
    height: min(
        clamp(11rem, 30vw, 30rem),
        calc((100cqw - (var(--shots) - 1) * (2 * var(--gutter) + var(--arrow)) - var(--shots) * var(--frame)) / var(--ratios))
    );
    width: auto;
    max-width: 100%;
    aspect-ratio: auto;
}

/* Left to right: the photograph on the left became the one on the right */
.shot-arrow {
    flex: 0 0 auto;
    align-self: center;
    display: grid;
    place-items: center;
    color: var(--stamp);
    opacity: 0;
}

.shot-arrow svg {
    display: block;
    width: clamp(1.4rem, 3vw, 2.6rem);
    height: auto;
}

/* Alternating tilt down the row, so a strip of three does not read as a chart */
.shots > .print:nth-child(4n + 1) { rotate: -1.4deg; }
.shots > .print:nth-child(4n + 3) { rotate: 1.4deg; }

/* Straighten on hover without growing — a print that scaled here would reach
   into the one beside it. */
.showcase .print:hover {
    rotate: 0deg;
    z-index: 1;
    box-shadow:
        0 3px 5px rgba(0, 0, 0, 0.45),
        0 24px 48px rgba(0, 0, 0, 0.5);
}

/* What was asked, underneath. A list of them is a list of messages: asking for
   the colour after seeing the repair is two things somebody typed, not one. */
.says {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
}

.slide .bubble {
    align-self: center;
    max-width: min(34rem, 92%);
    font-size: 1.02rem;
    text-align: center;
    text-wrap: balance;
}

/* A slide that was asked twice plays its messages in the order they happened:
   the request, then the arrow, then what came back, then the next request. One
   message on its own has nothing to be in order with, so it simply sits there. */
.slide .says:has(.bubble + .bubble) .bubble {
    opacity: 0;
}

.slide.is-current .says:has(.bubble + .bubble) .bubble {
    animation: says-in 0.4s var(--ease) calc((var(--step) - 1) * 0.85s) forwards;
}

@keyframes says-in {
    from {
        opacity: 0;
        translate: 0 45%;
        scale: 0.96;
    }

    to {
        opacity: 1;
        translate: 0 0;
        scale: 1;
    }
}

/* Each step lands a beat after the one it came from, so the row plays out in
   the order it happened. They hold their places while they wait — the row is
   laid out from the start and nothing moves. */
.slide .shot-after,
.slide .shot-arrow {
    opacity: 0;
}

.slide.is-current .shot-arrow {
    animation: arrow-in 0.35s var(--ease) calc(0.35s + (var(--step) - 1) * 0.85s) forwards;
}

.slide.is-current .shot-after {
    animation: hand-over 0.5s var(--ease) calc(0.6s + (var(--step) - 1) * 0.85s) forwards;
}

@keyframes arrow-in {
    from {
        opacity: 0;
        translate: -35% 0;
    }

    to {
        opacity: 0.85;
        translate: 0 0;
    }
}

@keyframes hand-over {
    from {
        opacity: 0;
        translate: -6% 4%;
        scale: 0.97;
    }

    to {
        opacity: 1;
        translate: 0 0;
        scale: 1;
    }
}

.showcase-dots {
    grid-area: 2 / 1;
    position: relative;
    z-index: 2;
    display: flex;
    gap: 0.5rem;
    margin-top: 1.1rem;
}

.showcase-dots button {
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    display: grid;
    place-items: center;
}

.showcase-dots button::before {
    content: "";
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--paper-dim);
    opacity: 0.4;
    transition-property: opacity, background-color, scale;
    transition-duration: 0.2s;
    transition-timing-function: var(--ease);
}

.showcase-dots button[aria-selected="true"]::before {
    background: var(--stamp);
    opacity: 1;
    scale: 1.25;
}

.showcase-dots button:hover::before { opacity: 0.8; }

.showcase-dots button:focus-visible {
    outline: 2px solid var(--stamp);
    outline-offset: 2px;
    border-radius: 50%;
}

/* On a phone the row gives up its gutters before it gives up the photographs —
   they have to stay side by side, because the comparison is the whole point. */
@media (max-width: 40rem) {
    .shots {
        --gutter: 0.45rem;
        --arrow: 1.5rem;
        --frame: 0.8rem;
    }

    .showcase .print {
        padding: 0.4rem 0.4rem 1.1rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .slide {
        translate: 0 0;
    }

    .slide.is-leaving:not(.is-current) {
        translate: 0 0;
    }

    .slide.is-current .shot-arrow,
    .slide.is-current .shot-after,
    .slide .says .bubble {
        animation: none;
        opacity: 1;
    }
}
