/* Cropmerger - design system.
   Dark only, deliberately quiet so the images carry the page.

   Colour   One accent, spent on state and focus only, never on a button.
            Neutral depth moves from --bg to --surface to --surface-lift.
            --surface-hover is interaction feedback, never a resting level.
            A card groups things on the page and is --surface. An action card
            is a rectangle you press - a token pack, a guide - one step above
            what it sits on: --surface on the page, --surface-lift in a card.
            --line draws quiet structure; --line-strong draws controls and
            marks that need a firmer edge; --line-hover answers the pointer.
            Buttons come from the neutral ramp: one solid light button for the
            action that matters, an outline for everything else. The exception
            is a button inside a callout, which takes the callout's own colour
            rather than standing apart from the thing it belongs to.
            A link in text is the text colour, underlined, never the accent.
            The page you are on keeps its button in the bar raised - the hover
            look, held - rather than lit in the accent.
            Text someone reads - a lede, a paragraph, a popup's message - is
            --text. --muted is for the quieter line beside it: a note, a hint,
            a field's label, a date, the footer. --faint is for what is
            disabled, waiting or drawn - a mark, a placeholder, a stage not
            yet reached.
            A state colour is never written out twice: --tone names it once and
            the callout rule derives its surface, its edge and its text.
   Type     Shared text styles live together below: page title, section title,
            heading, lede, page subtitle, body copy and note. Reuse a role
            before adding one.
            HTML heading levels describe document structure; the classes
            describe appearance, so an h2 can be a section title or a heading.
            Interface text inherits the 14px body default. Controls, branding
            and image labels keep their scoped variants; matching sizes alone
            do not make them the same role.
            Components may own inherited typography for their text groups.
            Size, leading and weight come from the existing tokens. Weight
            600 is the shared bold weight. No letter-spacing.
   Width    Every page of the tool - the front page, the library and the
            account - is --page wide, so their edges never move between them.
            Within that, two rules and no third. A region of nothing but
            prose - the FAQ, and the terms, privacy and contact pages - is
            --article wide and centred. Everywhere else text fills the element
            it is in, so a paragraph ends where the card or the column it sits
            in ends, and a stack of them shares one edge.
            No paragraph or heading carries a width of its own.
            A form takes --article, the same measure a page of prose takes,
            so its fields end where a paragraph would. On a page already that
            wide the cap does not bind and they fill the content. No input
            carries a width of its own.
            A popup is --popup wide. Checkout and adding a card take
            --popup-wide, the room Stripe's card fields need. A popup hangs
            from --popup-top rather than sitting in the middle, so one that
            changes shape grows downwards and its title never moves.
   Space    One named scale, held in --space-* tokens. It allows 2px and 6px
            where fine spacing needs them, then follows the broader 4px grid.
            Every gap, and every padding that is space rather than the size of
            a control, is one of them, so an in-between number cannot creep
            back in - the same reason the type scale is tokens. --pad is the
            page gutter.
            Structural containers use --radius; controls and compact rows use
            --radius-sm. An action card is a container on the page, so it takes
            --radius there and --radius-sm inside a card, the same step down
            its fill takes. Nested controls may derive half of --radius-sm.
            Fully round tracks and pills use --radius-pill, circles use 50%,
            and explicit resets use 0. Literal radii are reserved for
            functional geometry such as an annotation drawn over an image.
            Stacks of text and fields take their gaps from the --gap-*
            tokens, each named for a relationship rather than a size: a label
            to its control, a note under what it is about, lines that belong
            together, one block to the next and a new section of prose.
            Changing one moves every page and popup that has that
            relationship. Fields are blocks like any other, so a form's
            fields follow one another at --gap-block, on a page or in a popup.
            Cards name their edges with --pad-card, one value everywhere.
            Inside a card, a popup or the workspace panel, blocks follow one
            another at --gap-card, which each sets as its own --gap-block, so
            a field or a paragraph inside keeps that spacing wherever the card
            sits. On the page itself blocks keep the standard --gap-block.
            Everything with a card's edge takes --pad-card, including action
            cards and library tiles, so a card pads the same as its neighbours
            wherever it sits.
            A component with a gutter of its own names it --gutter rather than
            typing the number, so anything measuring against it derives
            instead of repeating: a message under a field sits at --gap-note
            by taking back the difference from the gutter that would
            otherwise carry it there. Padding and gaps have
            independent ownership unless their relationship is intentional.
            Equal values alone do not justify a shared component variable.
            A size used more than once is a token too - an icon, the tick box,
            the pill, the hairline, the target round a small cross - and a
            number that follows from others is calculated from them. What
            stays literal is functional geometry rather than spacing.
   Controls Every button, select and input is --control tall and carries body
            text, so a row of them lines up whatever it is made of. Two
            sizes are named and scoped: the default and .primary.large, which
            is --control-lg and belongs to the landing page.
            A button anywhere else takes the
            default, including inside a callout - a smaller button next to
            body text is the mismatch this rule exists to stop.
   Motion   Direct manipulation should follow input immediately. Popup and
            upload-slot visibility changes are instant. Brief transitions can
            clarify feedback, reveals and reflow, including FAQ expansion and
            toast arrival or dismissal. CSS transitions use --motion-hover for
            local feedback and --motion-state for larger changes. Activity
            indicators and the comparison demonstration have their own timing.
            Reduced motion drops spatial movement - rotation, scale, position -
            everywhere it is decorative, and keeps colour and opacity feedback.
            Nothing bouncy.
   Icons    Lucide, kept once in web/icons.svg and drawn by name with <use> -
            never pasted, never hand-drawn, never a second pack. One 24px grid
            at stroke 2, sized with the --icon tokens and coloured with
            `color`. Arrows follow three rules, shared with audely.app.
            An arrow that is its own item beside a label sits in an
            inline-flex row, centred, with no offset. An arrow after text
            that can wrap stays inline with the last word in a nowrap span,
            at --icon size and a whole-pixel vertical-align, never em, calc
            or top. Every arrow slides 2px on hover with transform alone,
            from translate(0, 0) at rest, and keeps will-change: transform so
            Safari draws it the same way at rest and in motion. Without it,
            text between whole pixels made the arrow wobble as it slid. Two
            things are marks rather than icons. The coin stands for the
            currency, so it carries its own gold and never takes `color`. The
            brand mark is the logo, drawn on its own grid and sized to the name
            beside it.
   Copy     UK English, sentence case, no semicolons.
            A refusal is one sentence wherever it is caught. The page and the
            server both check the same rules, so they say the same words for
            them - a form must never say no in two voices depending on which
            side noticed.
   Checks   scripts/stylecheck.py holds this file, the pages and the icons to
            every rule above that a machine can check. A literal it allows is
            named there with its reason, as the exceptions here are.
*/

:root {
  color-scheme: dark;

  --bg: #0a0b0d;
  --surface: #101216;
  --surface-lift: #161920;
  --guide-dim: rgba(0, 0, 0, 0.5);
  --guide-margin: color-mix(in srgb, var(--accent) 38%, transparent);
  --line: #23272f;
  --line-strong: #333944;

  --text: #e9ecf1;
  --muted: #868e9c;
  --faint: #5b6270;

  --accent: #5b8cff;

  /* Raised by hover, never by a colour: the neutral ramp answers a pointer. */
  --surface-hover: #1d2129;
  --line-hover: #414855;

  /* Shared hover increments for coloured buttons, tuned for a subtle change.
     Perceived contrast still depends on the colour and background. */
  --tint-hover: 5%;
  --edge-hover: 10%;
  /* A state colour's own surface and edge, as shares of it. */
  --tint: 10%;
  --edge: 34%;

  /* The viewer's checkerboard reuses the page and raised-surface neutrals so
     transparency is clear without introducing a separate grey palette. */
  --check-a: var(--bg);
  --check-b: var(--surface-lift);

  /* Chrome drawn on top of someone's photograph. It cannot come from the ramp
     above, because it has to read against an image we have never seen. */
  --on-image: #ffffff;
  --on-image-edge: rgba(255, 255, 255, 0.85);
  --scrim: rgba(12, 14, 18, 0.66);
  --crop-handle: #3872c5;

  /* Depth and veils. Named here so the same job is never done by two slightly
     different blacks, which is what happens when a shadow is typed twice. */
  --shadow-frame: 0 12px 40px rgba(0, 0, 0, 0.5);
  --shadow-card: 0 20px 60px rgba(0, 0, 0, 0.6);
  --shadow-placement: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 18px rgba(91, 140, 255, 0.35);
  /* One veil for every popup. Chosen to hide the picture and still leave the
     layout underneath legible. --veil-soft is the inline busy state,
     which is not a popup and wants the image visible through it. */
  --veil: rgba(6, 7, 10, 0.72);
  --veil-soft: rgba(6, 7, 10, 0.62);

  /* The token is currency, so it is drawn rather than iconised: gold, with a
     rim and a struck letter, at any size. */
  --coin-face: #e9b949;
  --coin-rim: #a8761d;
  --coin-mark: #6d4a0f;

  --good: #46c88a;
  --warn: #e0a83a;
  --bad: #e5695b;

  /* One solid button colour for the whole product, taken from the neutral
     ramp so the accent stays a signal rather than decoration. */
  --solid: #eceff4;
  --solid-hover: #ffffff;
  --on-solid: #0a0b0d;

  /* One type scale. Even 2px steps through the body sizes, then wider steps
     up to the section and display roles. The guide variants are the same 16px
     and 14px roles drawn on the canvas inside a guide visual. */
  --fs-display: clamp(30px, 3.4vw, 40px);
  --fs-group: 24px;
  --fs-title: 20px;
  --fs-lede: 18px;
  --fs-lg: 16px;
  --fs-md: 14px;
  --fs-sm: 12px;
  --fs-guide-lg: calc(16 * var(--guide-unit));
  --fs-guide-md: calc(14 * var(--guide-unit));

  /* Leading, one per kind of text: a page's display title, body, a lede and
     prose. */
  --lh-display: 1.1;
  --lh-body: 1.5;
  --lh-lede: 1.55;
  --lh-prose: 1.6;
  /* The only bold there is. */
  --weight-bold: 600;

  /* Brief local feedback and larger state transitions. */
  --motion-hover: 0.14s;
  --motion-state: 0.25s;

  /* A region of nothing but prose is this wide - the FAQ and the legal
     pages, which are the only two. It is the only width any text is given:
     everywhere else text fills whatever it is inside. */
  --article: 720px;

  /* Every page of the tool is this wide - the front page, the library and
     the account - so moving between them never shifts the edges. */
  --page: 1120px;

  /* A popup, and the wider one Stripe's card fields need. */
  --popup: 400px;
  --popup-wide: 480px;
  /* A popup's top edge, fixed rather than centred, so a card that changes
     shape grows downwards. */
  --popup-top: max(var(--pad), 12vh);

  /* One named spacing scale, the same idea as the type scale above. Fine
     spacing may add 2px and 6px tokens when a real use calls for them; the
     broader steps stay on the 4px grid:
     every gap, and every padding that is space rather than the size of a
     control, is one of these, so an in-between number cannot creep back in.
     A number that centres or sizes something is not on this scale - see the
     exceptions named at the top of this file. */
  --space-4: 4px;
  --space-6: 6px;
  --space-8: 8px;
  --space-12: 12px;
  --space-16: 16px;
  --space-20: 20px;
  --space-24: 24px;
  --space-32: 32px;
  --space-48: 48px;
  --space-56: 56px;
  --space-64: 64px;

  /* One unit of the 600-unit canvas each guide visual is drawn on. Everything
     inside that frame is sized in these, so the whole composition scales with
     the frame rather than with the page around it. */
  --guide-unit: calc(100cqw / 600);

  --radius: 12px;
  --radius-sm: 8px;
  /* Fully round: a pill, a track, a disc on a picture. */
  --radius-pill: 999px;
  --pad: var(--space-20);
  /* The page gutter, plus the space outside the centred page on wide screens. */
  --page-edge: max(var(--pad), calc((100% - var(--page)) / 2 + var(--pad)));
  /* Header content, 16px above and below, plus its divider. */
  --header-height: calc(var(--control) + 2 * var(--space-16) + var(--hairline));
  /* The width of every rule and edge. */
  --hairline: 1px;
  /* The heavier line, for the few drawn marks that have to hold their own: a
     ring, a spinner, a stage dot, or the comparison bar over an unknown
     photograph. Half a screen pixel rather than a tenth of one, so it is drawn
     at the width it asks for. */
  --stroke: 1.5px;
  /* A dashed line. Phone Safari can drop the side dashes of a 1.5px dashed
     border, so a narrow screen draws whole pixels instead. */
  --dash: var(--stroke);

  /* Vertical rhythm, named for the relationship rather than the size. Every
     stack of text and fields takes its gaps from these, so changing one moves
     every page and popup that has that relationship. */
  --gap-control: var(--space-8);   /* a label to its control */
  --gap-note: var(--space-8);      /* a message under the thing it is about */
  --gap-line: var(--space-12);     /* lines that belong together */
  --gap-card: var(--space-12);     /* one block to the next in the workspace panel */
  --gap-block: var(--space-16);    /* one block to the next - a field, a paragraph, a card's parts */
  --gap-section: var(--space-32);  /* above a new section of prose */
  /* Between the front page's top-level sections, above and below the rule
     that divides them, stepping down with the screen. */
  --section-space: var(--space-48);
  --pad-card: var(--space-16);     /* a card's edges */

  /* One height for every control, so a button beside a select beside an input
     is one row rather than three. */
  --control: 34px;
  --control-lg: 48px;
  /* A tick box, a radio and a slider's thumb. */
  --box: 14px;

  /* Icons at the sizes they are drawn. A cross that is its own button's box
     has a size of its own, so the gutter spaces the drawn mark. */
  --icon: 16px;
  --icon-xs: 12px;
  --icon-sm: 14px;
  --icon-md: 22px;
  --icon-lg: 26px;
  --icon-cross: 15px;
  /* The least a finger is asked to hit. A cross smaller than this grows its
     target back out to it. */
  --hit: 30px;
  /* A fixed badge height keeps its placement independent of the text line box. */
  --badge-height: 32px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Reserve monospace for identifiers. Quantities use the UI face with
     tabular figures so they align without looking like code. */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

/* A border counts inside a box's size - for a drawn shape as much as for an
   element, since a ring that grows by its own edge is a ring nobody can
   centre or measure. */
*, *::before, *::after { box-sizing: border-box; }

html { height: 100%; scrollbar-gutter: stable; }

body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 400 var(--fs-md)/var(--lh-body) var(--font);
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 { font-weight: var(--weight-bold); margin: 0; }
p { margin: 0; }

/* ---------- text styles ---------- */

/* The seven shared roles. Legal pages apply section-title and body-copy styles
   through their document scope so each paragraph needs no extra class.
   These rules own typography, not spacing, widths or text trimming. */
.page-title { font-size: var(--fs-display); line-height: var(--lh-display); font-weight: var(--weight-bold); }
.group-title { font-size: var(--fs-group); line-height: var(--lh-display); font-weight: var(--weight-bold); }
.section-title, .legal > h2 { font-size: var(--fs-title); line-height: var(--lh-body); font-weight: var(--weight-bold); }
.heading, .popup-head h2 { font-size: var(--fs-lg); line-height: var(--lh-body); font-weight: var(--weight-bold); }
.lede { font-size: var(--fs-lede); line-height: var(--lh-lede); }
.page-subtitle { color: var(--muted); font-size: var(--fs-lg); line-height: var(--lh-body); }

/* Body copy has more open leading than interface text. The legal scope has
   low specificity so a note or lede inside it keeps its own role. */
.body-copy, :where(.legal) :is(p, ul) { font-size: var(--fs-lg); line-height: var(--lh-prose); }

/* A line of explanation under something: body size, in the quieter colour.
   Its three states answer an action in place - a form's own refusal belongs
   under the form, not in the corner - so they live here with it rather than
   beside whatever prompted them. */
.note, .legal .updated,
.field:not(:has(:disabled)) > span {
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
}
.note.bad { color: var(--bad); }

/* ---------- top bar ---------- */

.bar {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding-block: var(--space-16);
  padding-inline: var(--page-edge);
  height: var(--header-height);
  border-bottom: var(--hairline) solid var(--line);
  background: var(--surface);
  flex: none;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  line-height: 1;
  font-weight: var(--weight-bold);
  font-size: var(--fs-title);
}

.brand span { display: block; line-height: 1; }

.glyph { width: 27px; height: 27px; flex: none; display: block; color: var(--accent); }

/* ---------- icons ---------- */

/* Lucide, drawn from web/icons.svg. One rule so every icon carries the pack's
   own geometry - a 24px grid at stroke 2 - and the drawing inherits it. */
svg[viewBox="0 0 24 24"] {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------- buttons ---------- */

button, .primary, .secondary {
  font: inherit;
  border-radius: var(--radius-sm);
  border: var(--hairline) solid transparent;
  cursor: pointer;
  transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease, color var(--motion-hover) ease;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* The buttons differ in weight and in colour, never in size. Height comes
   from the control token and padding is horizontal only, so a button, a select
   and an input side by side agree without anyone measuring. */
.primary, .secondary, .danger {
  height: var(--control);
  padding: 0 var(--space-16);
  flex: none;
}

.primary {
  background: var(--solid);
  color: var(--on-solid);
  font-weight: var(--weight-bold);
}
.primary:hover { background: var(--solid-hover); }

.secondary {
  background: var(--surface-lift);
  border-color: var(--line-strong);
  color: var(--text);
}
.secondary:hover { background: var(--surface-hover); border-color: var(--line-hover); }

/* Where you are: the hover look, held, on a lifted ground. */
.bar .secondary[aria-current="page"] { border-color: var(--line-hover); background: var(--surface-hover); }

/* Danger buttons provide their own tone and tint. Banner buttons inherit
   the banner's tone and start transparent to avoid doubling its tint. */
.danger, .banner button {
  background: color-mix(in srgb, var(--tone) var(--tint), transparent);
  border-color: color-mix(in srgb, var(--tone) var(--edge), transparent);
  color: var(--tone);
}
.danger:hover, .banner button:hover {
  background: color-mix(in srgb, var(--tone) calc(var(--tint) + var(--tint-hover)), transparent);
  border-color: color-mix(in srgb, var(--tone) calc(var(--edge) + var(--edge-hover)), transparent);
}
.danger { --tone: var(--bad); }
.banner button { --tint: 0%; --edge: 40%; }

/* Sign-up and sign-in, matched in width so the pair does not shuffle when the
   labels change length. */
.account { min-width: 112px; }

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

/* The document scrolls, so the footer is the end of the page rather than a bar
   pinned over it. On a short page `flex: 1` still pushes it to the bottom. */
main {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* ---------- landing ---------- */

/* The pitch, with the FAQ below it to scroll to. It is as tall as what is in
   it and no taller: standing it in a box a screen high and centring it there
   made the headline sit further down the taller the screen was, so the same
   page began under the bar on a laptop and a third of the way down a tablet.
   `align-items` keeps the two columns aligned to each other's tops rather than
   to their own middles. */
.landing {
  margin: 0 auto;
  width: 100%;
  max-width: var(--page);
  padding: var(--section-space) var(--pad);
  display: grid;
  /* The picture takes a share of the row rather than a width picked per
     breakpoint, so it grows and shrinks with the page the way a guide visual
     does instead of resizing in a step on the way down. */
  grid-template-columns: minmax(0, 1fr) minmax(200px, 35%);
  gap: var(--space-56);
  align-items: start;
}

/* Two groups, so the vertical gaps say what belongs together: 24px between
   the pitch and the uploader, 16px inside the prose, and 12px down the
   uploader stack - the same gutter the rows use inside themselves. */
.pitch { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-24); }
.intro { display: flex; flex-direction: column; gap: var(--space-16); }
.uploader { display: flex; flex-direction: column; gap: var(--space-12); }
.merge-submit { display: flex; flex-direction: column; gap: var(--gap-note); }


.assurances {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

/* The text is wrapped in one span: emphasis inside a flex item would
   otherwise be split into separate items, opening the gap around it. */
.assurances li { display: flex; align-items: flex-start; gap: var(--space-8); }
.assurances strong { font-weight: var(--weight-bold); }
/* On the landing page the promise leads and its explanation recedes. Consent
   lists keep one colour because every word in them carries equal weight. */
.intro .assurances li > span { color: var(--muted); }
.intro .assurances strong { color: var(--text); }

/* One line tall, so the glyph centres on the text's line box rather than
   hanging off its top. The square viewBox letterboxes inside it. */
.tick { flex: none; width: var(--icon); height: 1lh; color: var(--good); }

/* The two uploads are stacked, so each one is a row rather than a card. */
.drops { display: grid; gap: var(--space-12); }

.drop {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  /* Padding and column spacing can change independently. */
  gap: var(--space-16);
  padding: var(--space-8) var(--space-16);
  background: var(--surface);
  border: var(--hairline) dashed var(--line-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--motion-hover) ease, background var(--motion-hover) ease;
}
.drop:hover { border-color: var(--line-hover); background: var(--surface-lift); }
.drop.over {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) var(--tint), var(--surface));
  border-style: solid;
}
/* A filled slot says so in the same accent its icon takes, so the edge and
   the mark inside it speak at one volume. */
.drop.filled { border-style: solid; border-color: var(--accent); }
.drop:has(input[aria-invalid="true"]) { border-color: var(--bad); }
.drop + .drop-error { margin-top: calc(var(--gap-note) - var(--space-12)); }

/* Direct child only: the remove button's cross is not the slot's icon. The
   mark changes colour on the row's own timing - without this it snapped to
   the accent while the edge around it was still travelling. */
.drop > svg {
  width: var(--icon-md);
  height: var(--icon-md);
  color: var(--faint);
  transition: color var(--motion-hover) ease;
}
.drop.filled > svg { color: var(--accent); }

/* The label inherits the group's typography; the hint uses .note.
   Normal line boxes stack without an extra gap, and wrapping grows the row. */
.drop-text { display: flex; flex-direction: column; min-width: 0; }
.drop strong { font-weight: var(--weight-bold); }
/* Fits within the text stack, so a row does not grow when an image lands.
   The placeholder is shown until the bitmap is ready to appear instantly. */
.thumb {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  border: var(--hairline) dashed var(--line-strong);
  overflow: hidden;
}

/* While it is still a placeholder the square answers a pointer with the row
   it sits in. Once the picture has landed there is no edge left to answer
   with, which is the point of it. */
.drop:hover .thumb:not(.ready) { border-color: var(--line-hover); }

/* The square's own edge eases, because it stays where it is and only changes
   colour. Coming and going is not eased: the square and the cross are there
   because a file is, and gone the moment the cross is pressed. */
.thumb { transition: border-color var(--motion-hover) ease; }

.thumb img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

/* The bitmap appears instantly once decoded. Only the placeholder's edge
   fades away, leaving the image without a visible border. */
.thumb.ready { border-color: transparent; }
.thumb.ready img { opacity: 1; }

/* The spinner fades in on a delay, so a file that decodes immediately - which
   is nearly all of them - never flashes one. */
.thumb:not(.ready)::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--icon-sm);
  height: var(--icon-sm);
  margin: calc(var(--icon-sm) / -2) 0 0 calc(var(--icon-sm) / -2);
  border-radius: 50%;
  border: var(--stroke) solid var(--line-strong);
  border-top-color: var(--muted);
  opacity: 0;
  animation: thumb-spin 0.6s linear infinite, thumb-wait 0.2s ease 0.15s forwards;
}

@keyframes thumb-spin { to { transform: rotate(360deg); } }
@keyframes thumb-wait { to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* A static ring, still delayed the same way, rather than one that spins. */
  .thumb:not(.ready)::after { animation: thumb-wait 0.2s ease 0.15s forwards; }
}

/* The button's box is the cross itself, so the row's gutter spaces the drawn
   mark rather than the padding around it - otherwise the cross sits visibly
   further from the edge than the slot icon on the left does. The hit target
   and hover disc are grown back out with a pseudo-element. */
.remove {
  position: relative;
  width: var(--icon-cross);
  height: var(--icon-cross);
  /* Reduce just the cross's gutters from the row's 16px to 12px. */
  margin-inline: calc(var(--space-12) - var(--space-16));
  padding: 0;
  background: none;
  border: none;
  color: var(--faint);
}

.remove::before {
  content: "";
  position: absolute;
  inset: calc((var(--icon-cross) - var(--hit)) / 2);
  border-radius: 50%;
  transition: background var(--motion-hover) ease;
}

.remove svg { position: relative; z-index: 1; width: var(--icon-cross); height: var(--icon-cross); }

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

/* ---------- landing sample ---------- */

.sample { margin: 0; display: flex; flex-direction: column; gap: var(--space-8); }

.sample-frame {
  aspect-ratio: 2 / 3;
  border-radius: var(--radius);
  background: var(--surface);
  border: var(--hairline) solid var(--line);
}

.sample-frame img,
.guide-compare img,
.guide-split img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}

/* ---------- compare slider ---------- */

/* One slider, worn by the landing sample and by the result viewer. Everything
   both frames need to be dragged over is here, and each keeps only its own
   size, border and background. Clipped rather than resized: both layers stay
   at the frame's full size, so the two halves cannot drift out of register as
   the split moves. The original is always revealed from the left edge, and the
   two labels say so. */
.compare {
  position: relative;
  overflow: hidden;
  touch-action: none;
  /* Safari drags a selection across the pictures unless the prefixed property
     is the one told not to. */
  user-select: none;
  -webkit-user-select: none;
}

/* Only while there is something to compare, so the cursor never promises a
   drag the view cannot do. */
.compare:has(.compare-clip:not([hidden])) { cursor: ew-resize; }

.compare-clip { position: absolute; inset: 0; clip-path: inset(0 50% 0 0); }
.compare-clip img { position: absolute; inset: 0; width: 100%; height: 100%; }

/* The bar takes no pointer events, so the drag is handled by the frame and a
   visitor can grab the image anywhere rather than aiming at two pixels. */
.compare-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: var(--stroke);
  margin-left: calc(var(--stroke) / -2);
  background: var(--on-image-edge);
  pointer-events: none;
}

.compare-handle span {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 32px;
  height: 32px;
  margin: -16px 0 0 -16px;
  border-radius: 50%;
  background: var(--scrim);
  border: var(--stroke) solid var(--on-image-edge);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--on-image);
}
.compare-handle svg { width: var(--icon-xs); height: var(--icon-xs); margin: 0 -2px; stroke-width: 2.4; }

.compare-tag {
  position: absolute;
  bottom: var(--space-12);
  font-size: var(--fs-sm);
  /* Its own, not the frame's. The result viewer sets a frame's line box to
     nothing so the images sit flush in it, and a label borrowing that would be
     a pill with no height. */
  line-height: var(--lh-body);
  color: var(--on-image);
  background: var(--scrim);
  backdrop-filter: blur(4px);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--radius-pill);
  pointer-events: none;
}
.compare-tag.left { left: var(--space-12); }
.compare-tag.right { right: var(--space-12); }

/* ---------- landing guide ---------- */

/* Four equal screenshot slots tell the workflow as one sequence. Each step
   keeps its explanation directly beneath the image it describes. */
.guide {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--section-space) var(--pad);
  display: flex;
  flex-direction: column;
  gap: var(--gap-section);
}

.guide-copy { display: flex; flex-direction: column; gap: var(--gap-note); }
/* 16px from the step label to its title: the column's own gap, plus this.
   Named in full so the pair keeps its spacing if that gap ever moves. */
.guide-copy > .section-title { margin-top: calc(var(--space-16) - var(--gap-note)); }
.guide-copy :is(.note, .section-title) { line-height: var(--lh-display); }
/* The arrow follows the last word, so a title that wraps ends with it rather
   than setting it beside the whole block. The last word and the arrow are kept
   together so the arrow never starts a line on its own. */
.guide-link svg { width: var(--icon); height: var(--icon); vertical-align: -3px; margin-left: var(--space-6); }
.guide-link svg, .open-app-arrow { transform: translate(0, 0); will-change: transform; transition: transform var(--motion-hover) ease; }
.guide-link-end { white-space: nowrap; }
.guide-link:hover svg, .action-card:hover .guide-link svg, .open-app:hover .open-app-arrow { transform: translate(2px, 0); }

.guide-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-56);
  row-gap: var(--gap-section);
}

.guide-step {
  display: flex;
  flex-direction: column;
  gap: var(--gap-note);
}

/* Each visual is a 600-by-400 design canvas. Its contents use container-width
   units so the whole composition scales as one image at every guide width. */
.guide-visual {
  aspect-ratio: 3 / 2;
  container-type: inline-size;
  margin: 0;
  border-radius: var(--radius);
  border: var(--hairline) solid var(--line);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.guide-crop-preview,
.guide-result-preview,
.guide-split { position: relative; }

.guide-crop-preview > img,
.guide-result-preview > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: center;
}

/* Each picture holds for 2s and each fade takes 1s, so the loop is 6s and
   both states get the same time. A third of the loop is 2s. */
.guide-result-before {
  opacity: 0;
  animation: guide-result-cycle 6s ease-in-out infinite;
  pointer-events: none;
}

@keyframes guide-result-cycle {
  0%, 33.333% { opacity: 0; }
  50%, 83.333% { opacity: 1; }
  100% { opacity: 0; }
}

.guide-crop-selection {
  --guide-stroke: var(--dash);
  --handle-edge: calc(var(--guide-stroke) / -2);
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(350 * var(--guide-unit));
  aspect-ratio: 1;
  border: var(--guide-stroke) dashed var(--on-image);
  box-shadow: 0 0 0 100vmax var(--guide-dim);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* The unchanged image to leave around a change, drawn as the band between
   the crop's edge and the edit itself. An outline pulled inside the box is the
   band, so the middle stays clear without a second element to cut it out. */
.guide-crop-margin {
  /* 10% of the crop's 350 units: the room to leave on every side. */
  --margin-band: calc(35 * var(--guide-unit));
  position: absolute;
  inset: 0;
  outline: var(--margin-band) solid var(--guide-margin);
  outline-offset: calc(-1 * var(--margin-band));
}

/* Where the room to leave ends and the change may begin. The line is centred
   on that edge, the way the crop's own handles sit on its corners. */
.guide-crop-margin::after {
  content: "";
  position: absolute;
  inset: calc(var(--margin-band) - var(--guide-stroke) / 2);
  border: var(--guide-stroke) dashed var(--accent);
}

.guide-crop-handle {
  position: absolute;
  width: calc(12 * var(--guide-unit));
  height: calc(12 * var(--guide-unit));
  border: var(--guide-stroke) solid var(--on-image);
  border-radius: 50%;
  background: var(--crop-handle);
  box-shadow: 0 var(--guide-unit) calc(2 * var(--guide-unit)) var(--scrim);
}

.guide-crop-handle.top-left { top: var(--handle-edge); left: var(--handle-edge); transform: translate(-50%, -50%); }
.guide-crop-handle.top { top: var(--handle-edge); left: 50%; transform: translate(-50%, -50%); }
.guide-crop-handle.top-right { top: var(--handle-edge); right: var(--handle-edge); transform: translate(50%, -50%); }
.guide-crop-handle.left { top: 50%; left: var(--handle-edge); transform: translate(-50%, -50%); }
.guide-crop-handle.right { top: 50%; right: var(--handle-edge); transform: translate(50%, -50%); }
.guide-crop-handle.bottom-left { bottom: var(--handle-edge); left: var(--handle-edge); transform: translate(-50%, 50%); }
.guide-crop-handle.bottom { bottom: var(--handle-edge); left: 50%; transform: translate(-50%, 50%); }
.guide-crop-handle.bottom-right { right: var(--handle-edge); bottom: var(--handle-edge); transform: translate(50%, 50%); }

.guide-ai-preview {
  position: relative;
  display: block;
  background: var(--bg);
}

.guide-ai-composer {
  position: absolute;
  z-index: 1;
  inset: calc(24 * var(--guide-unit));
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: calc(16 * var(--guide-unit));
  padding: calc(12 * var(--guide-unit));
  border: var(--hairline) solid var(--line-strong);
  border-radius: calc(24 * var(--guide-unit));
  background: var(--surface-lift);
  box-shadow: 0 calc(12 * var(--guide-unit)) calc(40 * var(--guide-unit)) var(--guide-dim);
  color: var(--text);
}

.guide-ai-upload {
  position: relative;
  height: 100%;
  aspect-ratio: 1;
  min-height: 0;
  justify-self: start;
}

.guide-ai-upload > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  border-radius: calc(16 * var(--guide-unit));
}

.guide-ai-remove {
  position: absolute;
  top: calc(8 * var(--guide-unit));
  right: calc(8 * var(--guide-unit));
  width: calc(24 * var(--guide-unit));
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--solid);
  color: var(--on-solid);
  display: flex;
  align-items: center;
  justify-content: center;
}
.guide-ai-remove svg { width: calc(14 * var(--guide-unit)); height: calc(14 * var(--guide-unit)); }

.guide-ai-prompt { font-size: var(--fs-guide-lg); line-height: var(--lh-prose); }

.guide-ai-message {
  display: grid;
  grid-template-rows: auto calc(34 * var(--guide-unit));
  gap: calc(8 * var(--guide-unit));
}

.guide-ai-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.guide-ai-actions > svg {
  width: calc(20 * var(--guide-unit));
  height: calc(20 * var(--guide-unit));
  transform: translateX(calc(7 * var(--guide-unit)));
}
.guide-ai-tools > svg {
  width: calc(18 * var(--guide-unit));
  height: calc(18 * var(--guide-unit));
}
.guide-ai-tools { display: flex; align-items: center; gap: calc(12 * var(--guide-unit)); }

.guide-ai-send {
  width: calc(34 * var(--guide-unit));
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--solid);
  color: var(--on-solid);
  display: flex;
  align-items: center;
  justify-content: center;
}
.guide-ai-send svg {
  width: calc(18 * var(--guide-unit));
  height: calc(18 * var(--guide-unit));
}

.guide-upload-preview {
  position: relative;
  display: block;
  background: var(--bg);
}

.guide-upload-scene {
  position: absolute;
  inset: calc(24 * var(--guide-unit));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(12 * var(--guide-unit));
}

.guide-upload-stack {
  position: relative;
  display: grid;
  gap: calc(12 * var(--guide-unit));
}

.guide-upload-row {
  position: relative;
  min-width: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: calc(16 * var(--guide-unit));
  padding: calc(8 * var(--guide-unit)) calc(16 * var(--guide-unit));
  border: var(--hairline) dashed var(--line-strong);
  border-radius: calc(8 * var(--guide-unit));
  background: var(--surface);
  font-size: var(--fs-guide-lg);
  line-height: var(--lh-body);
}

.guide-upload-row > svg:first-child {
  width: calc(22 * var(--guide-unit));
  height: calc(22 * var(--guide-unit));
  color: var(--faint);
}

.guide-upload-row.is-filled { border-style: solid; border-color: var(--accent); }
.guide-upload-row.is-filled > svg:first-child { color: var(--accent); }
/* The dragged crop is lifted out of the flow, so the row holds an empty
   track its width for it to sit over. Without one the caption runs on
   underneath the image at narrower widths. */
.guide-upload-row:not(.is-filled) { grid-template-columns: auto minmax(0, 1fr) calc(112 * var(--guide-unit)); }

.guide-upload-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.guide-upload-copy strong { font-weight: var(--weight-bold); }
.guide-upload-copy .note { font-size: var(--fs-guide-md); }

.guide-upload-thumb {
  width: calc(44 * var(--guide-unit));
  height: calc(44 * var(--guide-unit));
  border-radius: calc(8 * var(--guide-unit));
  overflow: hidden;
}

.guide-upload-thumb img,
.guide-upload-image {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.guide-upload-remove {
  width: calc(15 * var(--guide-unit));
  height: calc(15 * var(--guide-unit));
  margin-inline: calc(-4 * var(--guide-unit));
  color: var(--faint);
}

.guide-upload-drag {
  position: absolute;
  z-index: 3;
  top: calc(50% + calc(12 * var(--guide-unit)));
  right: calc(16 * var(--guide-unit));
  width: calc(96 * var(--guide-unit));
  aspect-ratio: 1;
  border-radius: calc(8 * var(--guide-unit));
  transform: translateY(-50%);
}

.guide-upload-image { border-radius: inherit; }

.guide-upload-hand {
  position: absolute;
  top: calc(-20 * var(--guide-unit));
  right: calc(-14 * var(--guide-unit));
  width: calc(52 * var(--guide-unit));
  height: calc(48 * var(--guide-unit));
  display: block;
}

.guide-upload-merge {
  height: calc(48 * var(--guide-unit));
  border: var(--hairline) solid var(--line);
  border-radius: calc(8 * var(--guide-unit));
  background: var(--surface-lift);
  color: var(--faint);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-guide-lg);
  font-weight: var(--weight-bold);
}

/* ---------- landing FAQ ---------- */

/* Native `details`, so a question is still open to find with the browser's own
   search and still opens without script. The height is animated by hand
   because a closed `details` hides its contents outright, which nothing can
   transition from. */
.faq {
  width: 100%;
  max-width: var(--article);
  margin: 0 auto;
  padding: var(--section-space) var(--pad) var(--space-64);
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}

.faq-list { display: flex; flex-direction: column; }

/* The rule between the front page's sections: the header's own line, across
   the whole page. */
.section-rule { margin: 0; border: 0; border-top: var(--hairline) solid var(--line); }

.faq-item + .faq-item { border-top: var(--hairline) solid var(--line); }

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  padding: var(--space-16) 0;
  cursor: pointer;
  /* The default marker is replaced by the chevron, in both engines. */
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }

/* The chevron is what answers the hover, as it does on a select. The question
   is already at full strength, so there is nothing to brighten there. */
.faq-item summary svg {
  width: var(--icon);
  height: var(--icon);
  flex: none;
  color: var(--faint);
  transition: transform var(--motion-state) ease, color var(--motion-hover) ease;
}
.faq-item[open] summary svg { transform: rotate(180deg); color: var(--muted); }
/* After the open rule, so a hover still brightens the chevron of an already
   open question rather than being overridden back to --muted by it. */
.faq-item summary:hover svg { color: var(--text); }

/* Closing holds `[open]` until the answer has finished shrinking, so without
   this the chevron would wait for the height and turn back at the end of the
   collapse rather than at the click that started it. */
.faq-item.closing summary svg { transform: none; }

.faq-answer { overflow: hidden; transition: height var(--motion-state) ease; }

.faq-answer p { padding: 0 0 var(--space-16); }

@media (prefers-reduced-motion: reduce) {
  .guide-result-before { animation: none; opacity: 0; }
  :is(.guide-link svg, .open-app-arrow) { transition: none; }
  .faq-answer { transition: none; }
  .faq-item summary svg { transition: color var(--motion-hover) ease; }
}

/* ---------- workspace ---------- */

.workspace {
  height: calc(100dvh - var(--header-height));
  display: grid;
  grid-template-columns: minmax(0, 1fr) 312px;
}

/* The panel is empty until a result has been painted into it, so until then its
   column must go too - otherwise the image is centred in the space left over
   rather than in the page. */
.workspace:has(.panel[hidden]) { grid-template-columns: minmax(0, 1fr); }

.viewer { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.tabs {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-12) var(--pad) 0;
  border-bottom: var(--hairline) solid var(--line);
}

.tabs button {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  color: var(--muted);
  height: 40px;
  padding: 0 var(--space-12);
  font-size: var(--fs-md);
}
.tabs button:hover { color: var(--text); }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }

.stage {
  flex: 1;
  min-height: 0;
  position: relative;
  display: grid;
  place-items: center;
  padding: var(--pad);
  /* A checkerboard from the two darkest neutrals, so a transparent or a very
     dark result still reads as an image sitting on a surface. */
  background:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%, transparent 75%, var(--check-b) 75%),
    linear-gradient(45deg, var(--check-b) 25%, var(--check-a) 25%, var(--check-a) 75%, var(--check-b) 75%);
  --check-size: 22px;
  background-size: var(--check-size) var(--check-size);
  background-position: 0 0, calc(var(--check-size) / 2) calc(var(--check-size) / 2);
  overflow: hidden;
}

/* Sized in script to the image's aspect, scaled to fill the stage. Keeping the
   frame exactly the image's shape is what lets the compare divider and the
   placement box be positioned in plain percentages. */
.frame {
  line-height: 0;
  box-shadow: var(--shadow-frame);
}

.layer { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }



.placement {
  position: absolute;
  border: var(--dash) dashed var(--accent);
  box-shadow: var(--shadow-placement);
  border-radius: 2px;
  pointer-events: none;
}

.busy {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--veil-soft);
  backdrop-filter: blur(2px);
}
.spinner {
  width: var(--icon-lg); height: var(--icon-lg);
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  /* Still visible as a ring - the two-tone border - just not turning. */
  .spinner { animation: none; }
}

/* ---------- mask brush ---------- */

/* The trace of a stroke still being placed, drawn on the frame at the mask
   image's own size and cleared once the server returns the real mask. It is
   inert until Paint is pressed, so plain inspection and the compare gesture are
   never caught by it. */
.mask-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  touch-action: none;
  opacity: 0;
  transition: opacity var(--motion-state) ease;
}
/* Drawing feedback is immediate. Removing the trace keeps the existing fade. */
.mask-overlay.trace { opacity: 1; transition: none; }
.mask-overlay.live { pointer-events: auto; cursor: none; }

/* Sized in script to the brush's real footprint on the image. Dashed and in the
   warning colour for exclude, so the two kinds of stroke never look alike. */
.mask-ring {
  position: absolute;
  top: 0;
  left: 0;
  border: var(--stroke) solid var(--accent);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  box-shadow: 0 0 0 1px var(--scrim);
}
.mask-ring.exclude { border-style: dashed; border-color: var(--bad); }

/* The editor is a panel card like the others. Its only additions are the
   pressed state on the toggles and the two icons that tell include from
   exclude at a glance. */
.mask-tools { display: flex; flex-direction: column; gap: var(--gutter); }
.mask-modes { display: flex; gap: var(--space-8); }
.mask-modes .secondary,
.mask-tools .row .secondary { flex: 1; }
/* An icon and a label side by side in half a narrow card, so these take the
   ghost's tighter padding rather than overflowing the card. */
.mask-modes .secondary { gap: var(--space-8); padding: 0 var(--space-12); }
.mask-modes svg { width: var(--icon); height: var(--icon); }
#maskCard .field:has(:disabled) em { color: var(--faint); }
#maskFeather:disabled { opacity: .35; cursor: not-allowed; }

#maskCard [aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--text);
  background: color-mix(in srgb, var(--accent) var(--tint), var(--surface-lift));
}
#maskCard .secondary:disabled {
  color: var(--faint);
  border-color: var(--line);
  cursor: not-allowed;
}
#maskCard .secondary:disabled:hover { background: var(--surface-lift); border-color: var(--line); }

@media (prefers-reduced-motion: reduce) {
  .mask-overlay { transition: none; }
}

/* One line saying what the view above it is, and nothing else, so there is no
   row to share out. The height is held steady rather than fitted, because the
   legend changes with the tab and the picture must not shift when it does. */
.stage-foot {
  display: flex;
  align-items: center;
  padding: var(--space-8) var(--pad);
  border-top: var(--hairline) solid var(--line);
  color: var(--muted);
  min-height: 40px;
}

/* ---------- panel ---------- */

.panel {
  border-left: var(--hairline) solid var(--line);
  background: var(--bg);
  overflow-y: auto;
  padding: var(--space-16);
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  /* The tool's column holds cards and fields alike, so it spaces its blocks
     the way a card does. */
  --gap-block: var(--gap-card);
}

/* Scrolled content met the top edge and was cut through the middle of a word.
   The fade is carried only while there is something above to fade. */
.panel.scrolled { mask-image: linear-gradient(to bottom, transparent 0, #000 var(--space-16)); }

.card {
  background: var(--surface);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  padding: var(--pad-card);
  display: flex;
  flex-direction: column;
  --gap-block: var(--gap-card);
  --gutter: var(--gap-block);
  gap: var(--gutter);
}

.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-8); }

/* The hour a merge lasts is said where it is acted on: inside the line under
   the download, which is the reason to press it rather than a statistic of its
   own. Tabular figures, because a number that changes every second must not
   shuffle the words beside it. */
.clock { color: var(--text); font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; }

.pill {
  font-size: var(--fs-sm);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--radius-pill);
  border: var(--hairline) solid var(--line-strong);
  color: var(--muted);
  white-space: nowrap;
}

/* ---------- callouts ---------- */

/* Anything that reports a state names its colour once, in --tone, and takes
   its surface, its edge and its text from it - so a new state is one line
   rather than four values copied from a neighbour. The banner sits in the page
   and wears the tint; the toast sits over it and has to be opaque, so it is
   named here and given its surface where it is drawn. */
.banner, .pill.card-expired {
  --tone: var(--warn);
  background: color-mix(in srgb, var(--tone) var(--tint), transparent);
  border: var(--hairline) solid color-mix(in srgb, var(--tone) var(--edge), transparent);
  color: var(--tone);
}
.pill.card-expired { --tone: var(--bad); }

.toast.success { --tone: var(--good); }
.toast.info { --tone: var(--accent); }
.toast.warning { --tone: var(--warn); }
.toast.error { --tone: var(--bad); }

/* A field is its label over its control, and fields follow one another at the
   gutter of whatever holds them, like any other block. */
.field { display: flex; flex-direction: column; gap: var(--gap-control); }

/* A note directly under a field or control group is about that thing, so it
   sits at --gap-note rather than at the container's general block gutter. The
   pull-up is the difference, so a denser container keeps the same relationship
   without anyone editing a second number. An empty saved-card list is not a
   group to annotate: its note is the section's content and keeps the block gap. */
:is(.field, .mask-tools, .check, .split, .packs) + .note,
.saved-cards:not(:empty) + .note {
  margin-top: calc(var(--gap-note) - var(--gutter));
}
.field > span { display: flex; justify-content: space-between; align-items: center; }
.field em { font-style: normal; font-variant-numeric: tabular-nums; color: var(--text); }

select,
textarea,
input[type="email"],
input[type="password"],
input[type="text"] {
  font: inherit;
  font-size: var(--fs-md);
  color: var(--text);
  background: var(--bg);
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius-sm);
  width: 100%;
  outline: 0 solid transparent;
  outline-offset: 1px;
  transition: outline-color var(--motion-hover) ease, outline-width var(--motion-hover) ease;
}

select,
input[type="email"],
input[type="password"],
input[type="text"] {
  height: var(--control);
  padding: 0 var(--space-12);
}

/* Not a single-line control, so it is padded rather than fixed. */
textarea { padding: var(--space-8) var(--space-12); }

select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m6 9 6 6 6-6' fill='none' stroke='%23868e9c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-8) center;
  background-size: var(--icon-xs);
}
select:focus-visible, input:focus-visible, button:focus-visible, a:focus-visible,
.drop:has(input:focus-visible), .compare-handle:focus-visible span {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.compare-handle:focus-visible { outline: none; }

@media (prefers-reduced-motion: reduce) {
  /* Keep the final focus ring, but do not grow it into place. */
  select, textarea, input[type="email"], input[type="password"], input[type="text"] { transition: none; }
}

/* A field the form has refused. The browser's own bubble is turned off and the
   reason is said in the note under the form, so all the field itself has to do
   is be the one being talked about - in the callout red, and in red again when
   it takes focus, because the accent there would read as approval.
   Focused, the ring says it and the border stops saying it too: two reds a
   pixel apart read as a doubled edge rather than one field. The border goes
   back to the neutral it has everywhere else, which leaves a focused field
   the same shape whether it was refused or not - ring outside, plain border
   under it - and only the colour of the ring tells them apart. */
input[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: var(--bad); }
input[aria-invalid="true"]:focus-visible,
textarea[aria-invalid="true"]:focus-visible {
  outline-color: var(--bad);
  border-color: var(--line-strong);
}

input[type="range"] {
  appearance: none;
  width: 100%;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--line-strong);
  margin: var(--space-8) 0;
}
input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: var(--box); height: var(--box);
  border-radius: 50%;
  background: var(--text);
  border: none;
  cursor: pointer;
}

/* A control with nothing left to work on. It stays where it is, because it
   still describes the merge that was made, and stops answering. */
select:disabled, input:disabled { color: var(--faint); cursor: not-allowed; }
.field:has(:disabled), .check:has(:disabled) { color: var(--faint); cursor: not-allowed; }
input[type="range"]:disabled { background: var(--line); }
input[type="range"]:disabled::-webkit-slider-thumb { background: var(--faint); cursor: not-allowed; }

/* The box starts one grid step below the label's line box, so it sits beside
   the first line when a label wraps. */
.check { display: flex; align-items: flex-start; gap: var(--space-8); cursor: pointer; }
/* The box is a field the size of a tick rather than of a line of typing, so it
   takes the field's own ground, edge and curve and only its height is its own.
   Ticked is a state, which is what the accent is for. The tick is carried as
   the box's own mark because a pseudo-element on an input is not dependable. */
.check input {
  appearance: none;
  flex: none;
  width: var(--box);
  height: var(--box);
  margin: 0;
  font: inherit;
  margin-top: var(--space-4);
  background: var(--bg);
  border: var(--hairline) solid var(--line-strong);
  border-radius: calc(var(--radius-sm) / 2);
  cursor: pointer;
  transition: background-color var(--motion-hover) ease, border-color var(--motion-hover) ease;
}
.check input:hover { border-color: var(--line-hover); }
.check input:checked {
  background: var(--accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 76% no-repeat;
  border-color: var(--accent);
}

/* Buttons side by side, each its own size. */
.row { display: flex; gap: var(--space-8); }

/* One action and a way to change it, rather than two actions competing for a
   row too narrow to hold both their labels. The chevron is an icon on a
   default-height button, so the pair is still one row. */
.split { position: relative; display: flex; gap: var(--space-8); }
.split > .primary { flex: 1; }
.split-more { padding: 0 var(--space-8); }
.split-more svg { width: var(--icon); height: var(--icon); transition: transform var(--motion-hover) ease; }
.split-more[aria-expanded="true"] svg { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  /* The orientation still says open or closed; it just gets there at once. */
  .split-more svg { transition: none; }
}

/* Hung under the control it belongs to. The outer gutter keeps each row's
   hover fill visibly inset rather than painting edge to edge. */
.menu {
  position: absolute;
  z-index: 5;
  top: calc(100% + var(--space-8));
  right: 0;
  min-width: 160px;
  background: var(--surface-lift);
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  padding: var(--space-4);
  overflow: hidden;
}
.menu :is(a, button) {
  display: flex;
  align-items: center;
  height: var(--control);
  padding: 0 var(--space-12);
  color: var(--text);
  text-decoration: none;
  font-size: var(--fs-md);
  border-radius: calc(var(--radius-sm) / 2);
  transition: background var(--motion-hover) ease;
}
.menu button { width: 100%; justify-content: flex-start; border: none; background: none; }
.menu :is(a, button):hover { background: var(--surface-hover); }
.menu button:disabled { color: var(--faint); cursor: not-allowed; }
.menu button:disabled:hover { background: none; }
.menu .menu-remove { color: var(--bad); }

/* Global answers hang below the bar where they meet the eye without competing
   with its account controls. Newest sits first and older answers move down.
   Popup answers stay in the popup because the browser's top layer is above
   every fixed element, regardless of z-index. */
.toasts {
  position: fixed;
  z-index: 30;
  top: calc(var(--header-height) + var(--space-16));
  left: 50%;
  transform: translateX(-50%);
  width: min(360px, calc(100% - 2 * var(--pad)));
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  /* The gaps between toasts are not part of them. */
  pointer-events: none;
}

/* Two layers, because two things move at once. The row carries a toast to its
   new place in the stack when a neighbour arrives or leaves; the card carries
   the toast's own arrival and departure. One element cannot hold both
   transforms, and clipping the card to a row that opens would wipe it into
   view rather than fading it in. */
.toast { transition: transform var(--motion-state) ease; }

.toast-card {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-12) var(--space-16);
  border-radius: var(--radius-sm);
  font-size: var(--fs-md);
  box-shadow: var(--shadow-card);
  /* Opaque, unlike the callouts that sit in the page: the same tint over
     whatever it happens to be covering would be unreadable. The colour is
     still named once, in --tone. */
  background: color-mix(in srgb, var(--tone) var(--tint), var(--surface));
  border: var(--hairline) solid color-mix(in srgb, var(--tone) var(--edge), transparent);
  color: var(--tone);
  transition: opacity var(--motion-state) ease, transform var(--motion-state) ease;
}

.toast p { margin: 0; margin-right: auto; }

/* The close belongs to the callout, so it takes the tone rather than the
   neutral ramp, and sits in the row rather than over it. */
.toast .close { flex: none; color: inherit; opacity: 0.7; transition: opacity var(--motion-hover) ease; }
.toast .close:hover { color: inherit; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .toast, .toast-card { transition: none; }
}



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

.bar .brand {
  background: none;
  border: none;
  padding: 0;
  color: var(--text);
  cursor: pointer;
}

.bar-actions { margin-left: auto; display: flex; gap: var(--space-8); align-items: center; }

/* Who the page is for is stamped into the body before the page is sent, so the
   bar is right on the first paint. Both halves are in the markup and the wrong
   one is hidden, rather than the bar being painted once the account answers. */
[data-account="out"] .signed-in,
[data-account="in"] .signed-out { display: none; }

.primary.large { height: var(--control-lg); padding: 0 var(--space-24); font-size: var(--fs-lg); }
.block { width: 100%; }
.primary:disabled,
.primary[aria-disabled="true"] { background: var(--surface-lift); border-color: var(--line); color: var(--faint); cursor: not-allowed; }
.primary:disabled:hover,
.primary[aria-disabled="true"]:hover { background: var(--surface-lift); }

/* The action and its price read as one centred group. */
.merge-action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
}
.merge-cost {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  height: var(--badge-height);
  padding: 0 var(--space-12);
  border-radius: var(--radius-sm);
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
  /* Bake the ink tint into an opaque fill. Animating transparency over the
     button's changing fill makes the badge appear to lag behind it. */
  color: var(--on-solid);
  background: color-mix(in srgb, var(--on-solid) 6%, var(--solid));
  /* The price is part of the button, so it answers a change of state on the
     button's own timing. Without this the badge snaps while the face eases,
     and one control looks like two. */
  transition: background var(--motion-hover) ease, color var(--motion-hover) ease;
}
.merge-cost .coin-outline { width: var(--icon-sm); height: var(--icon-sm); }

.primary:hover .merge-cost {
  background: color-mix(in srgb, var(--on-solid) 6%, var(--solid-hover));
}

/* On the disabled, dark button the badge follows the label's own muted grey. */
.primary:disabled .merge-cost {
  color: var(--faint);
  background: var(--line);
}

/* ---------- popups ---------- */

/* Every popup is the same object: one veil over the page and one card on top
   of it, at one width and one padding. Only the contents differ, so a card
   that asks you to agree and a card that asks you to pay are recognisably the
   same thing happening twice.

   A real <dialog>, opened as a modal, so the browser holds the focus inside it,
   gives it back on the way out, makes the page behind inert and answers Escape.
   That is work nobody here has to do, and doing it by hand is how a dialog ends
   up tabbable from behind. The element itself stays the full window and stays
   transparent: it is what a click outside the card lands on, and the darkening
   belongs to ::backdrop, which is what the pseudo-element is for.

   The card hangs from --popup-top rather than sitting in the middle. Centred,
   the log-in card moved its title every time it changed into sign-up or a
   password reset, because each is a different height. */
.popup {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: start center;
  /* The user agent's box, undone: it arrives sized to its contents and drawn. */
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  color: inherit;
  padding: var(--popup-top) var(--pad) var(--pad);
  background: none;
}

/* The browser hides a closed dialog through its own stylesheet, which an
   author's display loses to. */
.popup:not([open]) { display: none; }

/* A popup holds the focus when it opens so no control is ringed before anyone
   has used the keyboard. The popup itself is not a control and draws no ring. */
.popup:focus { outline: none; }

/* Nothing here moves. Everything a popup answers - a view changing behind it,
   a second popup taking its place - happens at once, and a veil or a card
   easing in half a beat later reads as lag rather than polish. */
.popup::backdrop {
  background: var(--veil);
  backdrop-filter: blur(5px);
}

.popup-card {
  position: relative;
  width: min(var(--popup), 100%);
  --gap-block: var(--gap-card);
  --gutter: var(--gap-block);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--gutter);
  padding: var(--pad-card);
  background: var(--surface);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.unlock-loading {
  min-height: var(--control);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  color: var(--muted);
}
.unlock-loading .spinner { width: var(--icon); height: var(--icon); }

/* One arrangement: every popup runs left, whether it asks a question, makes a
   statement or holds a form, so its title and its action are always in the
   same place. A mark that heads a popup sits above its title. */
.popup-head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-8); }
.popup-card .assurances { gap: var(--gap-line); }

/* The row under a popup's action: the ways out, and anything the action needs
   said about it. */
.popup-foot { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-12); }

/* The consent card makes two separate statements - what you confirm and what
   we promise - so each label stays with its own list rather than floating
   between them at the card's own gap. */
.consent-group { display: flex; flex-direction: column; gap: var(--gap-line); }

/* Each label heads its group rather than annotating it, so it is not a note
   wearing a note's small grey: body size, the interface bold and full
   strength, which is what a label over a description is set in everywhere
   else here. */
.consent-label { font-size: var(--fs-md); font-weight: var(--weight-bold); color: var(--text); }

/* Underlining identifies a link without lifting it out of its sentence. It
   inherits that sentence's hierarchy, then brightens on hover like the footer. */
.popup-card a, .legal a, .body-copy a, .link {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color var(--motion-hover) ease;
}
.popup-card a:hover, .legal a:hover, .body-copy a:hover, .link:hover { color: var(--text); }

/* A link in a popup names a page, so the name is kept on one line. Split
   across two, "Terms of service" read as two links. */
.popup-card a { white-space: nowrap; }

/* A way out that is not the action. Used in a foot, never on its own. */
.link {
  background: none;
  border: none;
  padding: 0;
}

/* The padding keeps a finger-sized target and the margin gives it back, so
   the foot sits at the gutter. */
.popup-foot .link {
  display: block;
  padding-block: var(--space-8);
  margin-block: calc(-1 * var(--space-8));
  color: var(--muted);
}
.popup-foot .link:hover { color: var(--text); }

/* A cross that closes the thing it sits on. */
.close {
  position: relative;
  width: var(--icon);
  height: var(--icon);
  padding: 0;
  background: none;
  border: none;
  color: var(--faint);
}
.close svg { width: var(--icon); height: var(--icon); }
.close:hover { color: var(--text); }
/* The box is the drawn cross, so the target is grown back out around it. */
.close::before { content: ""; position: absolute; inset: calc((var(--icon) - var(--hit)) / 2); }

/* In a popup it keeps to the card's padding and one grid step below the top
   content edge, aligned consistently whether the head starts with text or a
   mark. */
.popup-card > .close {
  position: absolute;
  right: var(--pad-card);
  top: calc(var(--pad-card) + var(--space-4));
}

/* ---------- the run ---------- */

.stages {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap-line);
  font-size: var(--fs-md);
  color: var(--faint);
  align-self: stretch;
}

.stages li {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  transition: color var(--motion-state) ease;
}

.stages li::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: var(--stroke) solid currentColor;
  flex: none;
  transition: background var(--motion-state) ease, transform var(--motion-state) ease;
}

.stages li[data-state="active"] { color: var(--text); }
.stages li[data-state="active"]::before { background: var(--accent); border-color: var(--accent); transform: scale(1.3); }
.stages li[data-state="done"] { color: var(--muted); }
.stages li[data-state="done"]::before { background: var(--good); border-color: var(--good); }

@media (prefers-reduced-motion: reduce) {
  /* The dot still fills in colour as a stage completes; it stops growing to say so. */
  .stages li::before { transition: background var(--motion-state) ease; }
  .stages li[data-state="active"]::before { transform: none; }
}

.meter {
  width: 100%;
  height: 8px;
  /* This popup has no terminal button, so the short bar needs its own breathing
     room. Derive the margins from the card's gap and padding to leave one equal
     space-24 interval above and below it. */
  margin-block:
    calc(var(--space-24) - var(--gutter))
    calc(var(--space-24) - var(--pad-card));
  border-radius: var(--radius-pill);
  background: var(--line-strong);
  overflow: hidden;
}

/* A sheen travelling along the filled part, the way an installer's bar does.
   The tile is a fixed width and the animation moves it exactly one tile, so it
   repeats without a seam however wide the fill happens to be. */
.meter span {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--radius-pill);
  background-color: var(--accent);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in srgb, var(--on-image) 32%, transparent) 50%,
    transparent 100%
  );
  --sheen: 96px;
  background-size: var(--sheen) 100%;
  background-repeat: repeat-x;
  animation: meter-wave 1.2s linear infinite;
}

@keyframes meter-wave {
  from { background-position: 0 0; }
  to { background-position: var(--sheen) 0; }
}

@media (prefers-reduced-motion: reduce) {
  .meter span { animation: none; background-image: none; }
}

/* The one moment in the app that is unambiguously good news, so it is the one
   icon that takes the good tone rather than the accent. */
.done { width: var(--icon-lg); height: var(--icon-lg); color: var(--good); }

/* ---------- results grid ---------- */

.results,
.account-page {
  padding: var(--space-48) var(--pad) var(--space-64);
  max-width: var(--page);
  width: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

/* A secondary page's name and subtitle keep their own relationship. */
.page-head { display: flex; flex-direction: column; gap: var(--space-16); }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--gap-block);
}

.tile {
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  display: flex;
  flex-direction: column;
}

.tile-image { position: relative; display: block; }

.tile img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  display: block;
  background: var(--bg);
}

/* The picture is the way back into a merge. It is bought once, so the library
   has to open it rather than only offer to throw it away. */
.tile-open {
  position: absolute;
  inset: 0;
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  cursor: pointer;
}

/* One gutter, as padding and as gap, like a row on the landing page. */
.tile-foot {
  display: flex;
  align-items: center;
  --gutter: var(--space-12);
  gap: var(--gutter);
  /* A compact row, padded like the verify-email banner on the account page. */
  padding: var(--space-12) var(--pad-card);
}

/* Set like the clock in the workspace: tabular figures in the body face, so a
   number that changes every minute does not shuffle the words beside it. */
.countdown { color: var(--muted); font-variant-numeric: tabular-nums; margin-right: auto; white-space: nowrap; }

.tile-actions { position: relative; }
.tile-more { width: var(--control); padding: 0; }
.tile-more svg { width: var(--icon); height: var(--icon); }
.tile-menu { top: auto; bottom: calc(100% + var(--space-8)); }

.empty { color: var(--muted); text-align: center; padding: var(--space-48) 0; }

[hidden] { display: none !important; }

/* Safari can style a page once with only its own defaults before this sheet
   applies, then fade every link and border from its default blue. Nothing
   fades until motion.js says the page has drawn once with this sheet. */
:root:not([data-loaded]) :is(*, *::before, *::after) { transition: none !important; }

/* Said, not drawn. For a heading a page needs to be navigable but has no room
   to show - the workspace, where the picture is the page. */
.offscreen {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}


/* ---------- account ---------- */

/* The balance is the one number worth carrying in the bar, so it is a button
   rather than a badge: the thing you want after reading it is the page it
   comes from. */
.balance { gap: var(--space-8); font-variant-numeric: tabular-nums; }
.balance span { color: var(--text); font-weight: var(--weight-bold); }

/* Its own colours, set on each shape, so the icon rule above cannot reach it
   and one gold coin looks the same wherever it is drawn. */
.coin { width: var(--icon); height: var(--icon); flex: none; display: block; }

/* The account balance uses the same compact filled shape as the price on the
   Merge button. Its coloured coin keeps the two roles distinct. */
.pill.tokens {
  --pill-height: var(--badge-height);
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  height: var(--pill-height);
  padding: 0 var(--space-12);
  font-size: var(--fs-md);
  color: var(--text);
  background: var(--line);
  border: none;
  border-radius: var(--radius-sm);
}
.pill.tokens .coin { width: var(--icon-sm); height: var(--icon-sm); }

/* A direct account visit reveals its cards together once the initial snapshot
   has filled the catalogue, rather than painting partially populated cards. */
.account-page[aria-busy="true"] > :not(.page-head) { visibility: hidden; }

.banner {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-12) var(--pad-card);
  border-radius: var(--radius);
  font-size: var(--fs-md);
}
.banner-copy { display: flex; flex-direction: column; gap: var(--space-4); margin-right: auto; }

/* An action card. Its line underneath sits at the note gap, and it is
   one step of depth above whatever holds it, as the design system says. */
.action-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-note);
  padding: var(--pad-card);
  background: var(--surface);
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius);
  text-align: left;
}
.card .action-card { background: var(--surface-lift); border-radius: var(--radius-sm); }
.action-card:hover { border-color: var(--line-hover); background: var(--surface-hover); }
/* As a button it fades like every button. As a link it is the whole card, so
   it drops the underline and colour-only fade a link in prose carries, and
   takes the buttons' fade instead. */
a.action-card {
  color: var(--text);
  text-decoration: none;
  transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease, color var(--motion-hover) ease;
}

/* The way from a guide into the app: one action card laid out as a row, the
   brand mark beside the name and the arrow at the far end. */
.open-app { flex-direction: row; align-items: center; gap: var(--space-8); }
.open-app .glyph { width: var(--icon-md); height: var(--icon-md); }
.open-app-arrow { width: var(--icon); height: var(--icon); flex: none; margin-left: auto; }

/* Spaced at the card's own gutter, across as well as down. */
.packs { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--gutter); }

.pack strong { font-size: var(--fs-lede); font-weight: var(--weight-bold); }
.pack span { color: var(--muted); }
.pack:disabled { color: var(--muted); background: var(--bg); border-color: var(--line); cursor: not-allowed; }
.pack:disabled span { color: var(--faint); }
.pack:disabled:hover { background: var(--bg); border-color: var(--line); }

/* Purchase history stays a popup even when the account has many receipts.
   The card fits the viewport while only the rows scroll, keeping its title
   and close control in place. */
#receipts .popup-card { max-height: calc(100dvh - var(--popup-top) - var(--pad)); }
.receipt-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }

.receipt-list li {
  display: flex;
  align-items: baseline;
  gap: var(--space-12);
  padding: var(--gap-line) 0;
  border-top: var(--hairline) solid var(--line);
}
.receipt-list li:first-child { border-top: none; padding-top: 0; }
.receipt-list li:last-child { padding-bottom: 0; }
.receipt-list .paid { margin-left: auto; }
.receipt-list .when { color: var(--muted); white-space: nowrap; min-width: 7ch; text-align: right; }

/* Equal-width digits keep billing values aligned without changing the font. */
.pill.tokens, .pack, .checkout-summary, .saved-card .note,
.receipt-list, #doneReceived { font-variant-numeric: tabular-nums; }
#doneReference { font-family: var(--mono); }


/* A form on a page: the password form, the contact form and the withdrawal
   form are one thing. Its fields follow one another at --gap-block, the same
   as in a popup. It takes the measure a page of prose takes, so its fields end
   where a paragraph would. On a page already that wide the cap does not bind
   and the fields fill the page's content. Elsewhere it stops a password box
   from running the full width of a card. */
.stack { --gutter: var(--gap-block); display: flex; flex-direction: column; gap: var(--gutter); max-width: var(--article); }
.stack button { align-self: flex-start; flex: none; }

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

/* The footer closes the page with the same explicit vertical padding as the
   header, and its contents keep the page's horizontal edges. */
.foot {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  padding-block: var(--space-16);
  padding-inline: var(--page-edge);
  border-top: var(--hairline) solid var(--line);
  color: var(--muted);
  flex: none;
}
.foot nav { margin-left: auto; display: flex; gap: var(--space-16); }
.foot a, a.guide-link { color: var(--muted); text-decoration: none; transition: color var(--motion-hover) ease; }
.foot a:hover, a.guide-link:hover { color: var(--text); }

/* ---------- legal pages ---------- */

.legal {
  width: 100%;
  max-width: var(--article);
  margin: 0 auto;
  padding: var(--space-48) var(--pad) var(--space-64);
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
}

/* A new section - a heading, a form or a card - opens --gap-section below what
   came before, more than the paragraphs keep between themselves. Its size is
   the section title's, from the text styles. A heading inside a card belongs
   to the card and keeps its own role. */
.legal > h2,
.legal > :is(.stack, .card) { margin-top: calc(var(--gap-section) - var(--gap-block)); }

/* Guidance towards a neighbouring form recedes as one sentence. Its links
   inherit that hierarchy and their underlines keep the destinations clear. */
.legal .route-note { color: var(--muted); }

/* The one list on these pages carries the rules that matter most, so it is
   given the room to be read rather than skimmed. */
.legal ul {
  margin: 0;
  padding: 0 0 0 20px;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}
.legal li::marker { color: var(--faint); }

/* The guide list is a column of tiles, spaced as the packs in a card are. */
.legal .guide-index {
  list-style: none;
  padding: 0;
  gap: var(--gap-block);
}

.prompt-example {
  margin: 0;
  padding: var(--pad-card);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  font-family: var(--font);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.prompt-example code { font: inherit; }

/* The prompt is there to be taken, so it carries its own copy control in the
   corner. The block holds its right padding clear of the button so no wrapped
   line runs underneath it. */
.prompt-block { position: relative; }
.prompt-block .prompt-example { padding-right: var(--space-56); }

.prompt-copy {
  position: absolute;
  top: var(--space-8);
  right: var(--space-8);
  width: var(--control);
  height: var(--control);
  padding: 0;
  background: none;
  color: var(--muted);
}
.prompt-copy svg { width: var(--icon); height: var(--icon); }
.prompt-copy:hover { background: var(--surface-lift); color: var(--text); }

.bar a.brand { text-decoration: none; }

/* ---------- withdrawal ---------- */

/* The withdrawal form replaces its fields with a receipt in place, so each
   state is a column at the form's own gutter. Receipt details wrap rather than
   widening the page on a narrow screen. Its opening line takes focus when the
   state changes and, being prose rather than a control, shows no focus ring. */
.withdraw-step { display: flex; flex-direction: column; gap: var(--gutter); min-width: 0; }
.withdraw-step li,
.withdraw-step .note { overflow-wrap: anywhere; }
.withdraw-step p[tabindex]:focus { outline: none; }


textarea { resize: vertical; min-height: 120px; line-height: var(--lh-body); }
input::placeholder, textarea::placeholder { color: var(--faint); }

/* ---------- checkout ---------- */

/* Stripe fields and saved cards share the account form's spacing. Their fills
   and edges follow their separate control and card roles. */
.checkout-popup .popup-card { width: min(var(--popup-wide), 100%); max-height: calc(100dvh - var(--popup-top) - var(--pad)); overflow-y: auto; }
.payment-options { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); border: 0; padding: 0; margin: 0; min-width: 0; }
.payment-option { display: flex; align-items: center; gap: var(--space-8); height: var(--control); padding: 0 var(--space-12); border: var(--hairline) solid var(--line-strong); border-radius: var(--radius-sm); cursor: pointer; transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease; }
.payment-option:not(:has(:checked, :disabled)):hover { border-color: var(--line-hover); background: var(--surface-lift); }
.payment-option:has(:checked) { border-color: var(--accent); background: var(--surface-lift); }
.payment-option:has(:disabled) { opacity: .5; cursor: not-allowed; }
.payment-option:has(:disabled):not(:has(:checked)) { border-color: var(--line); }
.payment-option input { width: var(--box); height: var(--box); margin: 0; padding: 0; accent-color: var(--accent); }
.payment-option-short { display: none; }
/* Rows sit at body line spacing. The rule splits the card's gutter evenly. */
.checkout-summary { display: flex; flex-direction: column; gap: var(--gap-line); margin: 0; border-top: var(--hairline) solid var(--line); padding-top: var(--gutter); }
.checkout-summary > div { display: flex; justify-content: space-between; gap: var(--space-12); }
.checkout-summary dt { color: var(--muted); }
.checkout-summary dd { margin: 0; text-align: right; }
.checkout-total { font-weight: var(--weight-bold); }
.saved-cards { display: flex; flex-direction: column; gap: var(--space-12); }
/* An empty list would still take a gap either side of it. */
.saved-cards:empty { display: none; }
.saved-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  padding: var(--space-8) var(--space-16);
  background: var(--surface-lift);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius-sm);
}
/* A card's name, with its expiry under it as a note about it. */
.saved-card > :first-child { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.card-heading { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-8); }
.card-number { font-family: var(--mono); white-space: nowrap; }
/* Keep Stripe's bootstrapping frame out of the loading message's layout. */
:is(#cardCheckout, #cardSetup):has(> .note:not([hidden])) > div { height: 0; overflow: hidden; }



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

/* Every breakpoint lives here rather than beside the thing it changes, so the
   set can be read as one decision. The landing sample is what sets them: it
   sits beside the uploader for as long as both can be read, gives up width
   before anyone gives up the layout, and only moves above the uploader once
   two columns genuinely no longer fit. */

/* Desktop, where the two-column layout holds and there is room to breathe:
   the landing introduction and pitch open by a step. Upload labels use 16px
   with body leading and normal text flow. */
@media (min-width: 821px) {
  :root { --section-space: var(--space-64); }
  .legal, .results, .account-page { padding-top: var(--space-64); }
  .pitch { gap: var(--space-32); }
  .intro { gap: var(--space-24); }
  .intro .assurances { font-size: var(--fs-lg); }
  .drop-text { font-size: var(--fs-lg); line-height: var(--lh-body); }
}

/* A narrower desk. The panel shrinks to keep the stage worth looking at;
   the landing and the guide are already sized as shares and need no step. */
@media (max-width: 1040px) {
  .workspace { grid-template-columns: minmax(0, 1fr) 280px; }
}

/* Tablet. The panel comes out from beside the viewer, so the page stops being
   one screen and scrolls - otherwise the stage would be squeezed to nothing to
   make room for the controls underneath it. */
@media (max-width: 880px) {
  .workspace {
    grid-template-columns: minmax(0, 1fr);
    height: auto;
  }
  .stage { min-height: 60svh; }
  .panel { border-left: none; border-top: var(--hairline) solid var(--line); overflow: visible; }
}

/* One column. The pitch splits in two around the sample, so the headline and
   what it promises are read first, the sample proves it, and the uploads
   follow - rather than a portrait picture owning the whole first screen and
   the pitch beginning below it. `display: contents` lifts the two halves out
   of .pitch so they can be ordered separately, and each keeps its own gap; the
   distance between them is the column's, which is the same 24px .pitch used. */
@media (max-width: 820px) {
  .landing { grid-template-columns: minmax(0, 1fr); gap: var(--space-24); }
  .pitch { display: contents; }
  .intro { order: 1; }
  .uploader { order: 3; }
  /* The width it had as a column, so the sample never grows on the way down.
     Below 420px the column is narrower than this and the cap stops applying,
     which is what keeps the picture on the same left edge as the text on a
     phone rather than inset a few pixels inside it. */
  .sample { order: 2; }
  :is(.sample, .guide-list) { width: 100%; max-width: 380px; margin: 0 auto; }
  .guide-list { grid-template-columns: minmax(0, 1fr); row-gap: var(--gap-section); }
}

/* Phone. */
@media (max-width: 700px) {
  /* Whatever page it is, its content starts the same distance under the bar,
     and on a phone that distance closes up. */
  :root { --section-space: var(--space-32); }
  .legal, .results, .account-page { padding-top: var(--space-32); }
  .brand { font-size: var(--fs-lede); }
  .glyph { width: 23px; height: 23px; }
  .payment-option-wide { display: none; }
  .payment-option-short { display: inline; }
}

/* Small phone. The gutter closes up and the bar's buttons take their own width
   rather than the matched one. */
@media (max-width: 560px) {
  :root { --pad: var(--space-16); --dash: 1px; }
  /* The links stack into a column beside the name, which keeps its place at
     the start of the first line. */
  .foot { align-items: flex-start; }
  .foot nav { flex-direction: column; align-items: flex-end; gap: var(--gap-line); }
  .bar { gap: var(--space-8); }
  .account { min-width: 0; }
  .landing { gap: var(--space-24); }
  /* Three packs read as one set: keep the row intact or stack the whole set. */
  .packs { grid-template-columns: minmax(0, 1fr); }
  .row { flex-wrap: wrap; }
}

/* Narrower than the bar's own contents. The workspace carries the most
   buttons, so it gives the name up first. */
@media (max-width: 420px) {
  body.compact .brand span { display: none; }
}

/* Narrow enough that even the landing's pair of account buttons no longer fits
   beside the name, so the mark stands in for it everywhere. */
@media (max-width: 360px) {
  .brand span { display: none; }
}
