/* ==========================================================================
   Upscela
   Palette lifted verbatim from the Adobe Express reference. The only change
   is the type stack: the original called for acumin-pro / proxima-nova, which
   are Adobe Fonts and will not load outside Adobe's CDN. Manrope and Inter are
   the closest freely-licensed match for that geometric-humanist pairing.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Metric-matched fallbacks

   The two web fonts come from Google over the network, so between the first
   paint and their arrival the page is drawn in the local UI font -- and that
   font is a different width and a different height, so everything sized by its
   own text changed size the moment the woff2 landed. Measured: the credit pill
   went 123.3px to 129.3px and the wordmark 80.3 to 83.5, which is the nav
   corner visibly growing on load. It only shows on a cold font cache, which is
   exactly what a hard refresh gives you, so it was happening on every single
   Ctrl+Shift+R.

   These two faces are the local font with Manrope's and Inter's own metrics
   written over it, so the fallback occupies the same space as the real thing
   and the swap moves nothing. The numbers are measured, not guessed: canvas
   TextMetrics over a sample of what this site actually sets in each face --
   advance width for size-adjust, and the font's ascent and descent as a share
   of the em for the two overrides.

   Re-measure them if either family is ever changed; scripts/calibrate_fonts.py
   prints all three for any pair. A browser too old for these descriptors
   (Safari before 17) simply ignores the face and behaves as it did before.
   -------------------------------------------------------------------------- */
/* ONE FACE PER WEIGHT BUCKET, and the buckets are not decoration.

   A @font-face with no font-weight descriptor declares weight 400 and nothing
   else, so at 700 and 800 the browser stopped matching it and did its own
   thing. And the bucket's local() source has to BE the weight it claims: the
   bold bucket first pointed at the regular local face, which told the browser
   the face was already bold so it did not synthesise, and the wordmark came out
   14% NARROWER than the plain system font it was replacing - worse than no
   fallback at all.

   The ratios are not one number per family either. Calibrated against the real
   elements rather than a canvas string, because it is the elements that move:
   scripts/calibrate_fonts.py renders the nav with the web font and with the
   fallback and prints the ratio for each. Re-run it if either family changes.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Manrope Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 100 500;
  size-adjust: 104.1%;
  ascent-override: 107%;
  descent-override: 30%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Manrope Fallback';
  src: local('Segoe UI Bold'), local('Helvetica Neue Bold'), local('Arial Bold');
  font-weight: 501 750;
  size-adjust: 98.8%;
  ascent-override: 107%;
  descent-override: 30%;
  line-gap-override: 0%;
}
/* The wordmark and the page titles. A separate bucket because the local bold
   face does not keep widening past 700 the way Manrope does, so one number
   cannot serve both: at 800 the 700 bucket was still 8% narrow. */
@font-face {
  font-family: 'Manrope Fallback';
  src: local('Segoe UI Bold'), local('Helvetica Neue Bold'), local('Arial Bold');
  font-weight: 751 1000;
  size-adjust: 108.9%;
  ascent-override: 107%;
  descent-override: 30%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 100 500;
  size-adjust: 108.4%;
  ascent-override: 97%;
  descent-override: 24%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI Bold'), local('Helvetica Neue Bold'), local('Arial Bold');
  font-weight: 501 1000;
  size-adjust: 103.3%;
  ascent-override: 97%;
  descent-override: 24%;
  line-gap-override: 0%;
}

:root {
  --bg:                     #080b14;
  --surface:                #0d1120;
  --surface-2:              #111827;
  --surface-3:              #161d2e;
  --text:                   #f0f4ff;
  --fg:                     #8b96b8;
  --accent:                 #7c3aed;
  --accent-2:               #a855f7;
  --accent-pink:            #ec4899;
  --muted:                  #3d4a6b;
  --link:                   #a78bfa;
  --link-hover:             #c4b5fd;
  --text-on-surface-strong: #e8eeff;
  --primary:                linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #ec4899 100%);
  --primary-bg:             #7c3aed;
  --border:                 #1e2a45;
  --border-accent:          #2d1f5e;
  --text-on-accent:         #ffffff;
  --text-display-accent:    #c084fc;
  --surface-strong:         #05080f;
  /* The edit page's accent, and the warm pills that point at it. Amber
     rather than the orange-500 this started as: a full-saturation orange fill
     on a near-black page is hard to look at for as long as an editing session
     lasts. Everything warm derives from these two so they cannot drift apart. */
  --warm:                   #c97b3c;
  --warm-light:             #f0a35f;
  --gold:                   #f59e0b;
  --gold-light:             #fcd34d;
  /* The help centre's green. Muted emerald rather than a true green: a
     saturated green on a near-black page reads as a status light, and this has
     to sit under a page of body copy for as long as it takes to read it. The
     same reasoning that made the edit page amber instead of orange-500.
     Green because the page is about answers, and because it is the one hue on
     the site not already spoken for. */
  --mint:                   #2f9e75;
  --mint-light:             #6ee7b7;
  --danger:                 #f87171;
  --ok:                     #34d399;

  --font-display: 'Manrope', 'Manrope Fallback', system-ui, sans-serif;
  --font-body:    'Inter', 'Inter Fallback', system-ui, sans-serif;

  --radius:    14px;
  --radius-lg: 20px;
  --gap:       24px;
  --ease:      cubic-bezier(.4, 0, .2, 1);

  /* Each page reads best at a different size, so each carries its own scale.
     Applied to .page only — the nav, the footer and the logo sit outside it and
     stay at their natural size, so the chrome never changes between pages. */
  --ui-scale:          1.2;   /* upscaler   */
  --ui-scale-compare:  1.07;  /* comparisons — matches pricing */
  /* The page and the reading column. See the note on .shell. */
  --page-w:              1800px;
  --read-w:              860px;

  /* One type scale across Portfolio, Pricing, About and the help centre. Every
     h2 that titles a block is --t-section, every paragraph is --t-body, and the
     line under a heading is --t-sub. They were 30/27/24/23, 18/17/15.5/14.5/14
     and 13.5/12.5 before, which is not a scale, it is a list of decisions taken
     one page at a time. */
  --t-section:           24px;
  --t-body:              15px;
  --t-sub:               13.5px;

  --ui-scale-pricing:  1.07;  /* pricing     */
  /* The two side panels - Recent Tasks on the upscaler, the tools on Edit - are
     the same object to anyone switching tabs, so they get the same RENDERED
     width. Their pages carry different zooms, so each divides this by its own
     rather than both hard-coding a CSS width that only agrees at one zoom.
     Written here in rendered px, which is what the eye actually compares. */
  --side-panel:        400px;

  /* The canvas is one constant box in every state — empty, preview, working,
     result — so nothing on the page moves as the flow progresses. The image is
     centred inside it rather than resizing it.

     Deliberately px, and deliberately with no viewport-height override.
     Browser zoom shrinks vh and stops min-height media queries matching, so
     either would make the canvas smaller exactly when the customer zooms in to
     inspect it. In plain px it scales up with everything else. */
  /* A BACKSTOP, not the answer: measureStage overwrites this with a measured
     px value, and does it before the first paint now that the font stylesheet
     no longer blocks the module. It used to be a flat 272px, which is right at
     exactly one window height - at 950px tall the canvas painted 326px and
     became 253 on the next frame, which is the jump on reload. The expression
     is measureStage's own arithmetic: the window less the chrome, divided by
     the page's zoom, held between the same floor and ceiling. */
  --stage-h:     clamp(120px, calc((100vh - 433px - var(--controls-h) * 1.2) / 1.2), 900px);
  /* 433px is everything above and below the canvas EXCEPT the controls row:
     the nav, the footer, the workspace's own head and its assurance line. The
     controls row has to be in the expression rather than baked into that
     number, because --controls-h steps with the window WIDTH - 108px wide,
     220px below 1560, 298px below 1210 - so a single constant was right at one
     width and 134px out at every other. Measured against what the JavaScript
     then writes: 211/372/377/614 at 1500x950, 1920x1010, 2044x1016 and
     2560x1300, and the expression gives 210.8/372.8/377.8/614.5.
     x 1.2 because --controls-h is inside the page's zoom and 100vh is not. */

  /* The editor's two, for the same reason and by the same arithmetic. They had
     no default at all: --edit-h fell back to the 520px written inline at each
     use site, so on a 950px window the editor painted a 520px stage and grew to
     647 one frame later, taking the tools panel from 646 to 782 with it. That
     is the "the edit screen gets very small and then comes back". --ed-lift is
     the pill row's own height, which does not move with the window, so a
     constant is the honest default; the stacked layout below 950px still
     clears it to 0 and CSS sizes the stage there. */
  --edit-h:      clamp(260px, calc((100vh - 258px) / 1.07), 1400px);
  --ed-lift:     84px;

  /* How much height the controls row holds open, whatever is in it.
     border-box, so it includes the 18px padding-top above the rule.

     129px is the tallest single-line arrangement: the size picker, which stacks
     a credit cost under each button. The result state's plain buttons come to
     88px and would otherwise let the row shrink by 41px the moment a result
     opened — and measureStage would hand that 41px to the canvas, then take it
     straight back when the result closed. That round trip is the jerk.

     Below 1560px the result's five controls no longer share a line with the
     size picker's four, so the reserve has to hold two rows instead. */
  --controls-h:  108px;

  /* The two slots in that row hold their width as well as their height, and for
     the same reason: whether the row wraps depends on the total width of what is
     in it, and the result state carries five controls where the upload state
     carries two. Left to themselves the two states wrapped at different window
     widths, so the row was one line in one state and two in the other -- the
     height changed, and the canvas resized to follow.

     Sized to the wider occupant of each slot: the size picker (485) against
     Regenerate+Edit (323), and the result's three buttons (644) against the
     single Upscale button (250). Both states then measure the same total, wrap
     at the same window width, and come out the same height at every width.

     min() with 100% so a narrow window shrinks them instead of pushing the page
     sideways -- below that the reserve stops applying, which is correct: by then
     the tasks column has stacked and the page scrolls normally anyway.

     THESE ARE CSS PIXELS, NOT MEASURED ONES. .page carries zoom: 1.2, so
     getBoundingClientRect on this row reports about 20% more than the number to
     write here -- 485 measured is 404 to declare. Sized off the wider occupant
     of each slot with a little headroom: the size picker (404) against
     Regenerate+Edit (269), and the result's three buttons (537) against the lone
     Upscale button (208). --slot-h does the same for height, because the picker
     stacks a cost under each button and the result's buttons do not. */
  --slot-w:      410px;
  --action-w:    545px;
  --slot-h:       90px;
}

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%; scroll-behavior: smooth;
  /* Always a scrollbar, whether or not there is anything to scroll.
     Upscale and Edit fit the window; Portfolio, Pricing, About and the help
     centre do not, so switching tabs added a scrollbar, which narrowed the
     viewport, which slid the nav, the logo and every tab label a few pixels
     left. The chrome is supposed to be identical between pages -- that is the
     whole reason per-page scale lives on .page and not on body -- and a
     shifting nav gives the lie to it on every tab change.

     **`overflow-y: scroll`, NOT `scrollbar-gutter: stable`**, and the
     difference is the last of the reload jitter. The gutter reserves the
     seventeen pixels for LAYOUT while leaving the viewport its full width, so
     on a page with nothing to scroll the two disagree -- and a position:fixed
     element is resolved against the reserved box on the first frame and
     against the real viewport a moment later. Every fixed thing on the site
     therefore slid 17px on Upscale and on Edit; the language pill is simply
     the one that is always visible. `overflow-y: scroll` puts a real scrollbar
     there instead, so there is only ever one answer and nothing to correct.

     Measured in a HEADED browser, because this is invisible in a headless one:
     headless Chromium uses overlay scrollbars, which take no space, so every
     variant looks identical and the bug cannot be reproduced. With real
     Windows scrollbars at 1500x950: gutter gives 1483/1483/1483/1483 for the
     nav and slides the pill on / and /edit; overflow-y: scroll gives the same
     four nav widths and slides nothing anywhere; removing both gives 1483 on
     the long pages and 1500 on the short ones, which is the tab-change shift
     this rule exists to prevent.

     The cost is that a short page shows an inert scrollbar track rather than
     an empty reserved strip. That is the smaller of the two, and it is the
     more honest of the two: the chrome looks the same on every page.

     The tools panel keeps `scrollbar-gutter: stable`, correctly. It is an
     inner scroller with nothing positioned against it. */
  overflow-y: scroll;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex; flex-direction: column;
}

svg { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

button, input, a { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }

:focus-visible { outline: 2px solid var(--link); outline-offset: 3px; border-radius: 6px; }

/* ------------------------------- nav ----------------------------------- */
.nav {
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(12px);
  position: sticky; top: 0; z-index: 20;
}
/* Full width with the same side inset the pages use, so the brand lines up
   with the left edge of the content below and the account with its right. */
.nav-inner {
  position: relative;
  width: 100%; margin: 0 auto;
  display: grid;
  /* The flanking tracks are sized by their contents, not by an equal fraction.
     With `1fr` sides a track can be told to be narrower than what is in it, and
     the contents then spill sideways over the links -- which is exactly what a
     five-figure credit balance did. `auto` tracks cannot be squeezed, so the
     links give way instead of being overlapped. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 16px;
  padding: 12px 30px;
}
.nav-left { display: flex; align-items: center; gap: 16px; min-width: 0; }
.nav-brand, .footer-brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; }
/* The mark is one <symbol>; every instance is a <use> of it. */
.brand-mark { width: 32px; height: 32px; stroke: none; flex: none; }
.nav-brand-text { font-family: var(--font-display); font-weight: 800; font-size: 22px; letter-spacing: -.02em; }
.nav-sep { width: 1px; height: 22px; background: var(--border); flex: none; }
.nav-subtitle { color: var(--fg); font-size: 13.5px; white-space: nowrap; }
.nav-right { display: flex; align-items: center; justify-content: flex-end; gap: 14px; }

/* Centred in the room left between the two groups, and allowed to wrap inside
   it, so a narrow window costs a second line rather than a collision. */
.nav-links {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 6px 6px; min-width: 0;
}
/* Tabular figures so the balance does not jitter the row as it counts down. */
.nav-credits strong { font-variant-numeric: tabular-nums; }
/* Equal padding on every tab, so the gaps between them read as even. */
.nav-links a {
  text-decoration: none; color: var(--fg); font-size: 14px;
  padding: 9px 24px; border-radius: 999px;
  border: 1px solid transparent;
  transition: color .18s var(--ease), background .18s var(--ease), border-color .18s var(--ease);
}
.nav-links a:hover { color: var(--text); background: var(--surface-2); }
.nav-links a.is-active {
  color: var(--text-display-accent);
  background: color-mix(in srgb, var(--primary-bg) 22%, transparent);
  /* Same mix as the Edit and Pricing tabs below, for the same reason the
     eyebrow uses it: the flat --border-accent left the violet tabs with an
     edge you had to look for, beside two that had one. */
  border-color: color-mix(in srgb, var(--accent) 46%, var(--border));
}
/* Pricing is the one tab that is warm before you get to it. It is the page the
   business needs found, and at the default --fg it sat in the row as one link
   among six. Half-way to the gold and italic: the colour says which page, the
   italic says it without the colour having to shout, and the two together are
   still quieter than the filled pill an active tab gets. Both states are
   italic, so the tab does not change shape as you arrive on it.

   Written after the :hover rule above, which is the same specificity, so its
   own hover has to be written out or hovering would drop it back to grey. */
.nav-links a[data-route="pricing"] {
  color: color-mix(in srgb, var(--gold-light) 50%, var(--fg));
  /* A point over the 14px the rest of the row runs at. Italic already sets it
     apart; the extra point is what stops the italic reading as an accident.

     One point is also all it may take. The link row is the number the nav's
     two breakpoints are measured against, and a seventh link has already moved
     them twice. This widens it by 3px, from 577 to 580, and A/B'd against the
     14px roman it replaced at 1100-1400 in ten-pixel steps NOTHING moves: the
     links still wrap at 1120 and still fit on one line from 1130 up, and the
     strapline still appears at 1301. The nav row is 1px taller. Go to 16px and
     that has to be measured again rather than assumed. */
  font-size: 15px;
  font-style: italic;
}
.nav-links a[data-route="pricing"]:hover {
  color: var(--gold-light); background: var(--surface-2);
}

/* The one piece of chrome that follows the page it points at. Edit is orange
   from end to end, and a violet tab above an orange page reads as a leftover.
   These have to be written out: the nav lives outside .page, so the token
   overrides on the pages themselves cannot reach it. Comparisons is not here —
   it is a violet page and the default rule above already says so. */
/* Keyed on data-route, NOT on href. These were written as [href="#/edit"] and
   stopped matching the day the router moved from hash URLs to real paths, which
   is how Edit and Pricing quietly went violet above their own orange and gold
   pages. href is the wrong hook for a second reason too: it carries the language
   prefix, so /es/edit would never have matched either. data-route is the page's
   identity and it does not change. */
.nav-links a[data-route="edit"].is-active {
  color: var(--warm-light);
  background: color-mix(in srgb, var(--warm) 20%, transparent);
  border-color: color-mix(in srgb, var(--warm) 46%, var(--border));
}
.nav-links a[data-route="pricing"].is-active {
  color: var(--gold-light);
  background: color-mix(in srgb, var(--gold) 20%, transparent);
  border-color: color-mix(in srgb, var(--gold) 44%, var(--border));
}
.nav-links a[data-route="faq"].is-active {
  color: var(--mint-light);
  background: color-mix(in srgb, var(--mint) 22%, transparent);
  border-color: color-mix(in srgb, var(--mint) 46%, var(--border));
}

.nav-credits {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 17px; border-radius: 999px;
  color: var(--fg); font-size: 14px;
  border: 1px solid color-mix(in srgb, var(--gold) 38%, var(--border));
  background: color-mix(in srgb, var(--gold) 8%, var(--surface-2));
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.nav-credits:hover { border-color: var(--gold); color: var(--text); }
.nav-credits strong { color: var(--gold-light); font-weight: 700; }
.coin { width: 17px; height: 17px; color: var(--gold-light); flex: none; }
.coin-sm { width: 15px; height: 15px; }

/* The signed-out corner. Violet rather than the gold beside it, because
   violet is what "the primary action" means everywhere else on this site -- it
   is the Upscale button's own colour -- and gold here would read as a second
   credits pill. Author display rule, so [hidden] has to be restated. */
.nav-signin {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 20px; border-radius: 999px;
  background: var(--primary); color: var(--text-on-accent);
  font-family: var(--font-display); font-weight: 700; font-size: 14px;
  white-space: nowrap;
  transition: filter .18s var(--ease), transform .18s var(--ease);
}
.nav-signin:hover { filter: brightness(1.09); transform: translateY(-1px); }
.nav-signin:active { transform: none; }
.nav-signin[hidden] { display: none; }
.credits-wrap[hidden], .account-wrap[hidden], .nav-sep[hidden] { display: none; }

.nav-account { display: inline-flex; align-items: center; gap: 9px; color: var(--fg); font-size: 14px; transition: color .18s var(--ease); }
.nav-account:hover { color: var(--text); }
.avatar { width: 26px; height: 26px; }
.chev { width: 16px; height: 16px; transition: transform .18s var(--ease); }
.nav-account[aria-expanded="true"] .chev { transform: rotate(180deg); }

.account-wrap { position: relative; }
.account-menu {
  position: absolute; right: 0; top: calc(100% + 10px);
  min-width: 232px; z-index: 40;
  background: var(--surface-3); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 6px;
  box-shadow: 0 18px 46px rgba(0, 0, 0, .6);
}
.account-head {
  display: flex; flex-direction: column; gap: 2px;
  padding: 11px 12px 12px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.account-plan { font-family: var(--font-display); font-weight: 700; font-size: 14px; }
.account-credits { color: var(--fg); font-size: 12.5px; }
.account-credits b { color: var(--gold-light); }
.account-menu a, .account-menu button[role="menuitem"] {
  display: block; width: 100%; text-align: left;
  padding: 9px 12px; border-radius: 9px;
  color: var(--fg); font-size: 13.5px; text-decoration: none;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.account-menu a:hover, .account-menu button[role="menuitem"]:hover {
  background: var(--surface-2); color: var(--text);
}
#signin-button { color: var(--text-display-accent); font-weight: 600; }
#signout-button { color: var(--muted); }
/* An author display rule on the menu's own buttons outranks the UA's
   [hidden] { display: none }, so both of these have to say it again. Sixth and
   seventh instances of that trap; sweep for it, do not remember it. */
.account-menu button[role="menuitem"][hidden] { display: none; }
.account-email[hidden], .account-verify[hidden] { display: none; }
.account-email {
  padding: 8px 12px 10px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  color: var(--muted); font-size: 12px;
  overflow-wrap: anywhere;
}
/* The one thing between a new customer and their free credits, so it is the
   loudest thing in this menu and it says what to do rather than what is wrong. */
.account-verify {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  margin: 2px 0 6px; padding: 10px 12px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--gold-light) 11%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold-light) 32%, var(--border));
  color: var(--fg); font-size: 12px; line-height: 1.5;
}
.account-verify button {
  color: var(--gold-light); font-weight: 600; font-size: 12px;
  text-decoration: underline; text-underline-offset: 2px;
}
.account-note {
  padding: 9px 12px 6px; margin-top: 4px;
  border-top: 1px solid var(--border);
  color: var(--muted); font-size: 11.5px;
}

/* ------------------------------ layout --------------------------------- */
.page { flex: 1 0 auto; }
/* The closing line sits above the footer, not wherever the content happens to
   end.
 *
 * .page is `flex: 1` so the footer stays at the bottom of a short window, which
 * means a page shorter than the viewport has spare height BELOW its shell --
 * and "Secure processing", being the last thing in the shell, ended up floating
 * a hundred pixels above the footer instead of the nineteen it sits at on a
 * page that fills the screen. Worse, it then moved every time a question was
 * opened or closed, because it was following the content rather than the
 * bottom of the page.
 *
 * Handing the spare height to the shell and letting `margin-top: auto` on the
 * assurance absorb it puts the line in the same place on every one of these
 * pages, and stops it moving when an accordion does. On a page that already
 * overflows there is no spare height, so nothing changes at all.
 *
 * Scoped to the four content pages. The upscaler and the editor measure their
 * own shells in JS and must not become flex containers underneath that. */
/* :not([hidden]) is NOT decoration. `[hidden] { display: none }` is a UA rule
   at specificity 0,1,0 and `.page[data-page="faq"]` is 0,2,0, so writing a
   display here un-hides every page in the document: measured, the pricing page
   came out 12271px tall with Portfolio, About and the help centre stacked
   invisibly beneath it. This is the same trap as .tasks-group[hidden] and
   .cdlg-back[hidden]; it is written differently here only because these four
   are toggled on every navigation. */
.page[data-page="comparisons"]:not([hidden]),
.page[data-page="pricing"]:not([hidden]),
.page[data-page="about"]:not([hidden]),
.page[data-page="faq"]:not([hidden]) { display: flex; flex-direction: column; }
/* width: 100% on the shell AND on everything in it, and neither is a formality.
 *
 * A flex item with an auto margin on the cross axis sizes to its CONTENT rather
 * than stretching, and `margin: 0 auto` is exactly how every centred block on
 * this site is centred. So the moment the page became a flex column the shell
 * stopped filling it -- measured 1271px inside an 1852px page, with the gallery
 * shrinking to match. `align-self: stretch` does not help: an auto margin beats
 * it. A definite `width: 100%` does, and the auto margins then do what they
 * always did with whatever the max-width leaves over.
 *
 * flex: none for the other half of the same problem: a flex item shrinks by
 * default, so the gallery was also free to be squeezed below its own content
 * (5535px of samples down to 4273). Together these two put back exactly what
 * block layout was already doing, which is the whole intent -- the only thing
 * meant to change here is where the closing line ends up. */
.page[data-page="comparisons"] > .shell,
.page[data-page="pricing"] > .shell,
.page[data-page="about"] > .shell,
.page[data-page="faq"] > .shell,
.page[data-page="terms"] > .shell,
.page[data-page="privacy"] > .shell,
.page[data-page="refunds"] > .shell,
.page[data-page="contact"] > .shell,
.page[data-page="abuse"] > .shell {
  flex: 1; display: flex; flex-direction: column; width: 100%;
}
.page[data-page="comparisons"] > .shell > *,
.page[data-page="pricing"] > .shell > *,
.page[data-page="about"] > .shell > *,
.page[data-page="faq"] > .shell > *,
.page[data-page="terms"] > .shell > *,
.page[data-page="privacy"] > .shell > *,
.page[data-page="refunds"] > .shell > *,
.page[data-page="contact"] > .shell > *,
.page[data-page="abuse"] > .shell > * { flex: none; width: 100%; }
/* padding rather than margin, because `margin-top: auto` is doing the pushing
   and would have nothing left to say if the gap were a margin too. */
.page[data-page="comparisons"] > .shell > .assurance,
.page[data-page="pricing"] > .shell > .assurance,
.page[data-page="about"] > .shell > .assurance,
.page[data-page="faq"] > .shell > .assurance,
.page[data-page="terms"] > .shell > .assurance,
.page[data-page="privacy"] > .shell > .assurance,
.page[data-page="refunds"] > .shell > .assurance,
.page[data-page="contact"] > .shell > .assurance,
.page[data-page="abuse"] > .shell > .assurance { margin-top: auto; padding-top: 54px; }
.page[data-page="upscaler"]    { zoom: var(--ui-scale); }
.page[data-page="comparisons"] { zoom: var(--ui-scale-compare); }
/* Pricing's zoom, so the eyebrow pill lands on the same line here as on
   Comparisons, Pricing and Edit and a tab switch does not move it. */
/* Green from end to end, by token override rather than by restyling anything:
   every shared component on the page follows, exactly as Edit follows amber and
   Pricing follows gold. The active FAQ tab in the nav is written out separately
   up in the nav block, because the nav sits outside .page and a token block
   here cannot reach it. */
.page[data-page="faq"] {
  zoom: var(--ui-scale-pricing);
  --accent:              var(--mint);
  --accent-2:            #43c294;
  --accent-pink:         #8fe8c4;
  --primary:             linear-gradient(135deg, #23855f 0%, #2f9e75 55%, #43c294 100%);
  --primary-bg:          var(--mint);
  --link:                var(--mint-light);
  --link-hover:          #b8f5db;
  --border-accent:       #1d4c3b;
  --text-display-accent: var(--mint-light);
  /* Dark on green. White here is under 2.5:1 and unreadable on a filled pill. */
  --text-on-accent:      #04241a;
}
.page[data-page="about"]       { zoom: var(--ui-scale-pricing); }
/* Gold, because this page is about credits and credits are gold everywhere
   else on the site — the coin in the nav, the balance, the eyebrow. Same
   mechanism as the edit page: override the tokens, and every shared component
   follows. Muted rather than the full --gold, for the same reason the edit page
   is amber rather than orange. */
.page[data-page="pricing"] {
  zoom: var(--ui-scale-pricing);

  /* A darker emerald than the global --ok (#34d399), and only on this page.
     Beside a card of gold the brighter green was the loudest thing in the row,
     which put the eye on a badge rather than on the price. Overriding the token
     rather than restyling the badge takes the tier border and the feature ticks
     with it, so the whole card cools together instead of one part of it. The
     badge keeps its dark text, which at this value is still well over 4.5:1.

     #1fb27c is the page's own green, not the help centre's mint: mint was
     tried here and read as flat beside a card of gold, which is the trade a
     desaturated green makes. This is the same hue and lightness as the earlier
     #2aa97a with the saturation taken from 60% to 70%, so it keeps its distance
     from the global #34d399 without going grey. Dark badge text clears 5.8:1. */
  --ok:                  #1fb27c;

  /* Bright enough to look like a coin rather than like brass. The muted set
     this replaces was chosen against a glow that no longer exists; without it
     the buttons read as dark and slightly dirty. Still short of the raw
     --gold, which on a whole button is closer to a warning than to money. */
  --accent:              #d9ad22;
  --accent-2:            #ecc94b;
  --accent-pink:         #f2dc8f;
  --primary:             linear-gradient(135deg, #cf9f16 0%, #ebba28 55%, #fcd34d 100%);
  --primary-bg:          #d9ad22;
  --link:                #f0d98a;
  --link-hover:          #f8edc4;
  --border-accent:       #55461a;
  /* The same gold as the credit balance and the development banner, not the
     paler --link. At #f0d98a the tier blurbs and the one-off labels read as
     grey text that happened to be slightly warm. */
  --text-display-accent: var(--gold-light);
  /* Dark on gold. White on this would be under 2:1 and unreadable. */
  --text-on-accent:      #241c05;
}
/* ONE page width for every page that is not the tool itself.
 *
 * Portfolio, Pricing, About and the help centre were 1420, 1280, 1280 and 1020,
 * which is four different pages as far as anybody switching tabs is concerned:
 * the eyebrow, the H1 and both content edges landed somewhere new each time.
 * They are all --page-w now, and --page-w is the editor's own width, so five of
 * the six pages line up exactly. Only the upscaler is different, and it is
 * different on purpose: it is one screen that does not scroll, so it is fitted
 * rather than laid out.
 *
 * --read-w is the second half of the same idea. A page 1750px wide cannot put
 * prose across it -- 15px text at that measure is about 230 characters a line,
 * which nobody reads -- so anything that is a paragraph rather than a grid of
 * cards is held to a column and centred in the page. The page is the same
 * width everywhere; what fills it depends on what it is. */
.shell { max-width: var(--page-w); margin: 0 auto; padding: 42px 24px 80px; }
/* Pricing keeps the width it always had, and it is the only page that does.
   Four price cards, four one-off cards and four cost rows sit side by side
   here, and every pixel past this is spread between things that do not grow
   with it: at --page-w the cards were 420px of mostly air, and at the
   upscaler's 1800 they were still wide enough to look stretched. The plan
   cards are what this page is, and 1280 is what they were designed around.

   The contact band below them is NOT sized from this. It has its own
   max-width, so it comes out identical here and on the three wider pages,
   which is the whole reason it has one. */
.page[data-page="pricing"] .shell { max-width: 1280px; }
/* Wider than the other pages so the samples keep the size they had at the old
   larger scale, without the type growing with them. */
.shell-wide { max-width: 1500px; padding: 20px 24px 16px; }

/* Canvas on the left, tasks on the right. The upscaler is a tool, not an
   article, so it should sit inside the viewport rather than be scrolled. */
.work-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) calc(var(--side-panel) / var(--ui-scale));
  gap: 20px;
  align-items: start;
}

.page-head { text-align: center; margin-bottom: 40px; }
.page-head-tight { margin-bottom: 14px; }
.page-head-tight .eyebrow { margin-bottom: 7px; padding: 4px 13px; font-size: 10.5px; }
.page-head-tight .page-title { font-size: clamp(20px, 2vw, 26px); }
.page-head-tight .page-sub { font-size: 12.5px; margin-top: 4px; }
.shell-edit { max-width: 1500px; padding: 16px 24px 14px; }
.eyebrow {
  /* One pill, three pages. They were the same height and padding already and
     still did not look it, because each one is a different length of text and
     the shortest read as a smaller button. Sized to the longest of them, they
     are the same object wherever you meet it. */
  display: inline-block; margin-bottom: 18px;
  min-width: 196px; text-align: center;
  padding: 6px 16px; border-radius: 999px;
  background: color-mix(in srgb, var(--primary-bg) 18%, transparent);
  /* Built the same way the warm and gold variants below are, rather than from
     the flat --border-accent token. The token is a dark violet (#2d1f5e) that
     all but vanished against the page, so the one pill that fell through to it
     -- Comparisons -- looked unfinished next to Edit's orange and Pricing's
     gold, which mix their accent into the border and come out visibly drawn.
     Same recipe, same 46%, so the three now read as one object in three
     colours instead of two pills and a ghost. */
  border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--border));
  color: var(--text-display-accent);
  font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
}
.eyebrow-warm {
  background: color-mix(in srgb, var(--warm) 15%, transparent);
  border-color: color-mix(in srgb, var(--warm) 46%, var(--border));
  color: var(--warm-light);
}
.eyebrow-gold {
  background: color-mix(in srgb, var(--gold) 12%, transparent);
  border-color: color-mix(in srgb, var(--gold) 40%, var(--border));
  color: var(--gold-light);
}
.page-title { font-family: var(--font-display); font-weight: 800; font-size: clamp(28px, 3.6vw, 42px); letter-spacing: -.03em; margin: 0; }
.page-sub { color: var(--fg); margin: 12px 0 0; }
/* Portfolio only, and it earns its place by being the one sentence on the site
   that a visitor is entitled to be sceptical about. The samples used to be
   invented and watermarked SAMPLE; they are real jobs now, so the page says so
   plainly rather than hoping the pictures are believed. Held to --read-w for
   the same reason every other paragraph here is: the page is 1800px wide. */
.page-lede {
  color: var(--fg); font-size: 13.5px; line-height: 1.55;
  max-width: var(--read-w); margin: 10px auto 0;
}
/* Three pages open with a short line under the H1 and all three now say it the
   same way: .page-sub, 15px Inter at 400. It was display type at 18/500 on
   Portfolio and Pricing for a while and that was one voice too many - a page
   whose H1 is already display type does not need the sentence under it to be
   display type as well. .page-motto is gone; nothing references it.

   Only the colour differs, and only where the page needs it to. Portfolio is
   violet, so its line is gold: the one thing in that head that is not the page's
   own accent is where the eye goes. The help centre and Pricing leave theirs in
   --fg, because both of those pages already have a coloured element in the head
   (the eyebrow) doing that job. */
.page[data-page="comparisons"] .page-sub { color: var(--gold-light); }
.sub-title { font-family: var(--font-display); font-weight: 800; font-size: var(--t-section); text-align: center; margin: 0 0 24px; }

/* ---------------------------- workspace -------------------------------- */
.workspace {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 26px;
  padding: 22px 26px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .45);
  min-width: 0;
  transition: border-color .2s var(--ease);
}
/* A soft brand glow behind the card. Sits underneath and ignores pointers, so
   it is decoration only. */
.workspace::before {
  content: "";
  position: absolute; inset: -1px;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(135deg,
              color-mix(in srgb, var(--accent) 55%, transparent),
              transparent 42%, transparent 58%,
              color-mix(in srgb, var(--accent-pink) 45%, transparent));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: .5; pointer-events: none;
  transition: opacity .25s var(--ease);
}
/* Dropping anywhere on the card works, so the whole card acknowledges it. */
.workspace.is-dropping::before { opacity: 1; }
.workspace.is-dropping { border-color: var(--accent-2); }
.headline {
  font-family: var(--font-display);
  font-weight: 800; font-size: clamp(22px, 2.6vw, 32px);
  letter-spacing: -.03em; text-align: center; margin: 0 0 20px;
}
.headline-ai { background: var(--primary); -webkit-background-clip: text; background-clip: text; color: transparent; }
.spark { color: var(--accent-2); font-size: .52em; vertical-align: .5em; }

/* -------------------------------- stage -------------------------------- */
/* Fixed height, width derived from the image's own ratio. No letterboxing:
   a square photo gets a square canvas, not bars. */
.stage {
  position: relative;
  width: 100%;
  height: var(--stage-h);
  margin: 0 auto;
}
.canvas {
  display: none;
  position: absolute; inset: 0;
  align-items: center; justify-content: center;
}
.workspace[data-state="upload"]     [data-panel="upload"],
.workspace[data-state="preview"]    [data-panel="preview"],
.workspace[data-state="processing"] [data-panel="processing"],
.workspace[data-state="result"]     [data-panel="result"],
.workspace[data-state="error"]      [data-panel="error"] { display: flex; }
/* The rise is feedback for a STATE CHANGE, and only for one.
   It was on the base rule, so the panel that happens to be showing when the
   page loads played it too: the workspace glided 6px up over 280ms on every
   single reload of the home page. `layout-shift` scores that at ZERO, because
   a transform is not a layout change and nothing around it moved, which is
   exactly why it survived every pass of the shift measurements and had to be
   found by watching the boxes themselves (scripts/measure_settle.py).

   `is-live` is added one frame after the first paint, so the upload panel is
   simply there when you arrive and every state change after it still rises. */
.workspace.is-live [data-panel] { animation: rise .28s var(--ease); }
@keyframes rise { from { opacity: 0; transform: translateY(6px); } }
/* The empty and error states have no image, so they use the full width. */

.canvas img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain;
  display: block;
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 34px rgba(0, 0, 0, .45);
}
/* The comparison layers are the exception: the frame is already their exact
   ratio, so they must FILL it — every property of it, not just object-fit.
   The rule above wins on specificity, and for a long time this override only
   took back object-fit and left `width: auto; max-width: 100%` standing. That
   is invisible while the frame is smaller than the source, which it is on an
   ordinary window: max-width shrinks the picture to exactly the frame and all
   looks well. Give the frame more room than the picture has pixels — a large
   monitor, a zoomed-out browser, full screen on a 2556px screen — and the
   picture stops growing at its natural size and sits in the frame's top-left
   corner with black down two sides. That was the full-screen offset. */
.canvas .compare-layer {
  width: 100%; height: 100%;
  max-width: none; max-height: none;
  object-fit: fill; border-radius: 0;
}
.canvas-clear {
  position: absolute; top: 12px; right: 12px;
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(5, 8, 15, .8); border: 1px solid rgba(255, 255, 255, .14);
  color: #fff; backdrop-filter: blur(6px);
}
.canvas-clear svg { width: 16px; height: 16px; }

.stage-hint {
  display: flex; align-items: center; justify-content: center;
  height: 20px; margin: 12px 0 0;
  color: var(--fg); font-size: 13px;
}
.stage-name {
  display: flex; align-items: center; justify-content: center;
  height: 22px; margin: 2px 0 0; padding: 0 12px;
  color: var(--muted); font-size: 12.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stage-meta {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px; margin: 6px 0 0; height: 0;
  color: var(--fg); font-size: 12.5px;
}
.meta-pill {
  padding: 3px 10px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.meta-pill-strong { color: var(--text-display-accent); border-color: var(--border-accent); }

/* ----------------------------- dropzone -------------------------------- */
.dropzone {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 1.6px dashed color-mix(in srgb, var(--accent-2) 52%, var(--border));
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--accent) 4%, transparent);
  text-align: center; padding: 24px;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.dropzone:hover, .dropzone.is-dragging {
  border-color: var(--accent-2);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.dropzone-icon {
  width: 56px; height: 56px; color: var(--accent-2); stroke-width: 1.5;
  transition: transform .25s var(--ease);
}
/* The editor's cloud lifts on hover exactly as the upscaler's does. Two drop
   areas that do the same job should not behave differently. */
.ed-drop svg { transition: transform .25s var(--ease); }
.ed-drop:hover svg,
.ed-drop.is-dragging svg { transform: translateY(-3px) scale(1.05); }
.dropzone:hover .dropzone-icon,
.dropzone.is-dragging .dropzone-icon { transform: translateY(-3px) scale(1.05); }
.dropzone-title { font-family: var(--font-display); font-weight: 700; font-size: 20px; margin: 18px 0 0; }
.dropzone-link { color: var(--link); }
.dropzone-meta { color: var(--fg); font-size: 13.5px; margin: 12px 0 0; }
.dot { margin: 0 6px; color: var(--muted); }

/* ----------------------------- processing ------------------------------ */
[data-panel="processing"] img { filter: blur(9px) brightness(.5); transform: scale(1.03); }
.processing-overlay {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 24px;
}
.spinner {
  width: 46px; height: 46px; margin-bottom: 16px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--accent-2) 26%, transparent);
  border-top-color: var(--accent-2);
  animation: spin .9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.processing-title { font-family: var(--font-display); font-weight: 800; font-size: 17px; letter-spacing: .1em; text-transform: uppercase; margin: 0; }
.processing-note { color: var(--text-on-surface-strong); font-size: 14.5px; margin: 6px 0 0; min-height: 1.5em; }
/* No bottom margin: the 16px used to separate the Cancel button that stood
   under it. The way out is the X in the corner now, so this is the last line. */
.processing-elapsed { font-variant-numeric: tabular-nums; color: var(--fg); font-size: 13px; margin: 4px 0 0; }

/* ------------------------------ compare -------------------------------- */
.compare-frame {
  position: relative;
  height: 100%; width: auto;
  aspect-ratio: var(--frame-aspect, 1.6);
  max-width: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-strong);
  touch-action: pan-y; user-select: none; cursor: ew-resize;
}
.compare-layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: fill;          /* both sources are already the frame's exact ratio */
  display: block; pointer-events: none;
  border-radius: 0;
}
.compare-clip { position: absolute; inset: 0; clip-path: inset(0 0 0 var(--split, 50%)); will-change: clip-path; }
.compare-divider {
  position: absolute; top: 0; bottom: 0; left: var(--split, 50%);
  width: 2px; margin-left: -1px;
  background: rgba(255, 255, 255, .92);
  box-shadow: 0 0 14px rgba(0, 0, 0, .6);
  pointer-events: none;
}
.compare-handle {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center;
  background: #fff; color: var(--accent);
  box-shadow: 0 4px 18px rgba(0, 0, 0, .5);
}
.compare-handle svg { width: 22px; height: 22px; stroke-width: 2.3; }
/* .compare-label and its two sides are gone with the chips they styled. The
   upscaler's own frame had always hidden them; the Comparisons samples were the
   only thing that ever drew them, and they are unlabelled now. */

/* The same box as the frame, and the box the zoom is measured against. The
   controls are not in here: the transform is on the frame, and anything inside
   it is scaled and slid along with the picture. */
.compare-shell { position: relative; height: 100%; display: flex; max-width: 100%; }

/* Full screen sits beside the X, which owns the corner. Both are positioned
   against the canvas, so they land in the same place over a staged image and
   over a finished one — the X used to follow the picture's own edge and moved
   when the picture did. */
.stage-full {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: rgba(5, 8, 15, .8); border: 1px solid rgba(255, 255, 255, .14);
  color: var(--text-on-surface-strong); backdrop-filter: blur(6px);
  transition: background .15s var(--ease), border-color .15s var(--ease);
}
.stage-full:hover { background: rgba(5, 8, 15, .95); border-color: var(--accent-2); }
.stage-full svg { width: 17px; height: 17px; }
#fullscreen-button { right: 54px; }
/* In full screen the frame is the only thing on screen, so it fills it. */
#compare-frame:fullscreen { width: 100vw; height: 100vh; border-radius: 0; background: #000; }
#compare-frame:fullscreen .compare-layer { object-fit: contain; }
/* The range input is the accessible control; it sits invisibly over the frame
   so keyboard and screen-reader users get the same slider. */
.compare-range {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: ew-resize;
  -webkit-appearance: none; appearance: none; background: transparent;
}
.compare-range::-webkit-slider-thumb { -webkit-appearance: none; width: 44px; height: 100%; }
.compare-range::-moz-range-thumb { width: 44px; height: 100%; border: 0; background: transparent; }

.compare-frame:fullscreen {
  width: 100vw; height: 100vh; max-width: none; max-height: none;
  aspect-ratio: auto; border-radius: 0; background: #000;
}
.compare-frame:fullscreen .compare-layer { object-fit: contain; }

.compare-hint { display: flex; align-items: center; justify-content: center; gap: 7px; color: var(--fg); font-size: 13px; margin: 14px 0 0; }

/* ------------------------------- errors -------------------------------- */
.error-box {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 1px solid color-mix(in srgb, var(--danger) 40%, var(--border));
  background: color-mix(in srgb, var(--danger) 7%, var(--surface-2));
  border-radius: var(--radius-lg); padding: 26px; text-align: center;
}
.error-icon { width: 42px; height: 42px; color: var(--danger); }
.error-title { font-family: var(--font-display); font-weight: 700; font-size: 18px; margin: 16px 0 6px; }
.error-detail { color: var(--fg); margin: 0; font-size: 14px; }

.notice { border-radius: 10px; padding: 11px 14px; font-size: 13px; margin: 14px 0 0; }
.notice-warn {
  background: color-mix(in srgb, var(--gold) 10%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--gold) 34%, var(--border));
  color: var(--gold-light);
}

/* ------------------------------ controls ------------------------------- */
/* One row, identical in every state: sizes on the left, the action on the
   right. Only the button changes as the flow moves along. */
.controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; margin-top: 18px;
  padding-top: 18px; border-top: 1px solid var(--border);
  /* Four size buttons and an action do not always fit one line: just above
     1100px the workspace is about 550 CSS px wide once the page's 1.2 zoom is
     taken off, against roughly 650 of picker and button. Wrapping puts the
     action on its own line there; the alternative is the row pushing the whole
     page sideways, which is the failure the layout rules exist to stop. */
  flex-wrap: wrap;

  /* AND THE ROW RESERVES ITS HEIGHT, which is what stops the canvas resizing.
     measureStage sizes the canvas from whatever room is left over in the shell,
     so anything that changes height around it changes the canvas — and this row
     was three different heights: the size picker stacks a cost under each button
     (111px), the result's buttons are plain (66px), and either can wrap. Closing
     a result therefore re-fitted the whole page: the footer dropped off the
     bottom for a frame and came back. Reserved, the row is one box in every
     state, the canvas is measured against a constant, and nothing moves.
     align-items: center then holds whatever is in it on the same line. */
  min-height: var(--controls-h);
}
/* How much the reserve has to hold depends on the width, because the states do
   not all wrap at the same one: the result row carries five controls where the
   upload row carries two, so as the window narrows the result wraps first and
   the row would be two lines in one state and one in the other. Each step is the
   tallest any state reaches in that band, measured rather than guessed. Between
   them the row is a single constant box and the canvas never resizes.

   Plain viewport widths: media queries are evaluated before .page's zoom: 1.2,
   so these are the numbers a window actually reports. */
@media (max-width: 1560px) {
  :root { --controls-h: 220px; }
  /* Below this the result's five controls cannot share a line with the picker,
     so the action wraps -- but the upload state's two still fit, and the button
     then sat on line one in one state and line two in the other. Giving it the
     whole line in every state settles it on one place: the row keeps its height
     AND its arrangement, so Upscale Image and Download Image are the same pixel
     here too, not merely the same distance from the top of the page. */
  .action { flex-basis: 100%; }
}
/* 1210, not the 1199 it was: widening the tasks column to match the editor's
   panel took 13 CSS px off the workspace, which moved the result row's wrap
   point from just under 1200 to 1206. Measured - it fits at 1208 and wraps at
   1206 - and set a few px clear of that, because a 1px-exact breakpoint is at
   the mercy of font rendering on another machine. Over-reserving in the 1207
   to 1210 band costs a slightly shorter canvas there and nothing else: every
   state gets the same reserve, so none of them moves against the others. */
@media (max-width: 1210px) { :root { --controls-h: 298px; } }
/* Under 1100 the tasks column stacks below the canvas and hands the workspace
   its full width back, so the rows fit again and the reserve can come down. */
@media (max-width: 1100px) { :root { --controls-h: 220px; } }
/* Four tiers now, not two. The row has about 880 CSS px to share with the
   action button, so the buttons give up the width two of them could afford:
   116px each would be 512px of picker before the gaps, which crowds "Upscale
   Image" against it at the shell's narrower end. 92px still clears the widest
   label with room, and the four read as one set rather than four objects. */
.sizes { display: flex; gap: 12px; }
/* The author display above outranks the UA's [hidden] { display: none }, so the
   opt-out has to be written even though nothing sets `hidden` here today. This
   trap has cost this codebase four separate bugs -- .tasks-group, .ed-drop,
   .faq-search-clear, .cdlg-back -- every one of them a control that would not
   go away. Cheaper to close it than to remember it. */
.sizes[hidden] { display: none; }
.size-col { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.size {
  min-width: 92px; padding: 14px 18px;
  border: 1px solid var(--border); background: var(--surface-2);
  border-radius: var(--radius);
  font-family: var(--font-display); font-weight: 800; font-size: 19px; color: var(--text);
  transition: border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease);
}
.size:hover { border-color: var(--border-accent); transform: translateY(-1px); }
/* Filled, not haloed. The fill alone already separates the chosen size from the
   three beside it, and the glow was blurring the one-or-two characters it sat
   directly under. */
.size[aria-checked="true"] {
  background: var(--primary); border-color: transparent; color: var(--text-on-accent);
}
.size-cost { display: inline-flex; align-items: center; gap: 6px; color: var(--fg); font-size: 14px; }
.size-cost b { color: var(--text); font-weight: 600; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  border-radius: var(--radius); padding: 15px 34px;
  font-family: var(--font-display); font-weight: 700; font-size: 16px;
  text-decoration: none; min-width: 232px;
  transition: filter .18s var(--ease), transform .18s var(--ease), opacity .18s var(--ease);
}
.btn:hover:not(:disabled) { filter: brightness(1.09); transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn-icon { width: 19px; height: 19px; }
/* No glow. A coloured shadow under a coloured button on a near-black page is a
   smudge around the thing it is meant to be pointing at, and it takes the edge
   off the label sitting on top of it: the type reads soft when the shape behind
   it has no hard boundary. Pricing had already dropped its own for exactly this
   reason -- now nothing on the site glows, and every button is a crisp block of
   colour with sharp text on it. Size, colour and position say which button
   matters; a halo was saying it a fourth time. */
.btn-primary { background: var(--primary); color: var(--text-on-accent); }
.btn-ghost { background: var(--surface-2); border: 1px solid var(--border); color: var(--fg); font-size: 15px; padding: 13px 26px; }
.btn-ghost:hover { color: var(--text); border-color: var(--border-accent); }
.btn-quiet { background: rgba(255, 255, 255, .08); border: 1px solid rgba(255, 255, 255, .14); color: var(--text); padding: 9px 22px; font-size: 14px; min-width: 0; }

/* The wash: a translucent fill of the page's accent instead of a solid
   gradient. The editor wears it because that button sits inches from the photo
   being judged, and a saturated fill of that size pulls the eye off it. Pricing
   deliberately does NOT: Choose Plan is the one thing on that page that has to
   look pressable from across the room, and it is the twin of the solid Monthly
   pill above it. */
.ed-use.btn-primary {
  background: color-mix(in srgb, var(--accent) 32%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
  color: var(--link);
  box-shadow: none;
}
.ed-use.btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 44%, transparent);
  border-color: color-mix(in srgb, var(--accent) 78%, transparent);
  color: var(--link-hover);
  filter: none;
}

/* Only one primary action is ever on screen, and both are the same size, so
   Upscale becomes Download in place rather than the row reflowing.

   The gap and the ghost buttons' padding are tighter than elsewhere for one
   reason: the result state carries five controls (Regenerate, Edit, the step-up,
   New image, Download) against the upload state's two, and every pixel saved
   here is another 100px of window over which the two states still share a
   single line -- which is the width band where the row keeps its shape. */
.action {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  /* Right-aligned inside a slot wide enough for the result's three buttons, so
     the primary sits on the same pixel in every state: Upscale Image becomes
     Download Image without moving, and the step-up button keeps its place when
     New image appears beside it. */
  justify-content: flex-end;
}
/* The picker stacks a credit cost under each button and so stands taller than a
   row of plain buttons. Holding the shorter occupants open to the same height is
   what keeps the row one shape across upload, preview and result -- the preview
   state shows the picker AND Edit together, so these coexist rather than swap. */
.sizes, .regen { min-height: var(--slot-h); }

/* The size picker and the run-again controls occupy the same slot: you pick a
   size before there is an image, and act on the image once there is one. */
/* Holds Regenerate and Edit. Edit is offered as soon as there is an image to
   edit, which is one state earlier than Regenerate has anything to repeat. */
.regen { display: none; align-items: center; gap: 12px; }
.workspace[data-state="preview"] .regen,
.workspace[data-state="result"] .regen { display: flex; }
.workspace[data-state="preview"] #regen-button { display: none; }
/* Preview holds Edit alone, and space-between was stranding it in the middle of
   an empty row, miles from the button it belongs next to. Pushed right, it sits
   one gap from Upscale Image -- the two things you can do with a staged image,
   together. The result state keeps its own arrangement: Regenerate and Edit are
   a pair there, and five buttons want the width. */
.workspace[data-state="preview"] .regen { margin-left: auto; }
#edit-button { min-width: 0; padding: 15px 20px; }
.workspace[data-state="result"] .sizes { display: none; }
#regen-button { min-width: 0; padding: 15px 22px; }
#upscale-more-button { min-width: 0; padding: 15px 18px; }
/* An author display rule outranks the UA's [hidden] { display: none }, so the
   opt-out has to be written out or an 8K result would still show a step-up
   button with nowhere to step to. */
#upscale-more-button[hidden] { display: none; }
/* It steps a FINISHED result up a tier, so it exists only where there is one.
   showResult sets the hidden attribute and resetToUpload clears it, but state
   is what actually decides: without this rule the button survived closing the
   result and sat on the empty drop area offering to upscale nothing, which is
   also what made the controls row a third height and set the canvas resizing. */
.workspace:not([data-state="result"]) #upscale-more-button { display: none; }
#again-button, #download-button,
.workspace[data-state="result"] #upscale-button,
.workspace[data-state="error"]  #upscale-button { display: none; }
.workspace[data-state="result"] #download-button,
.workspace[data-state="result"] #again-button,
.workspace[data-state="error"]  #again-button { display: inline-flex; }
#again-button { min-width: 0; padding: 15px 18px; }
/* The two primaries stay the same width as each other so Upscale becomes
   Download without the row shifting, but 232px was sized for a row of two
   buttons and the result row has five. */
#upscale-button, #download-button { min-width: 208px; padding: 15px 26px; }

/* Centred under the controls, and in the page's own accent rather than the
   body grey it had: it is the one line on the page that answers "where does my
   photo go?", and grey-on-grey made it read as boilerplate nobody wrote. */
.assurance {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 6px 22px;
  color: var(--link); font-size: 13.5px; font-weight: 600;
  margin: 16px 0 0; text-align: center;
}
/* The line that closes both Pricing and Comparisons, in the same place on each.
   Both pages carry the same 1.07 zoom and the same .shell padding, so one rule
   lands it at the same height on both and a tab switch does not move it.

   White, and lighter than the 600 the assurance carries beside the photograph
   on Upscale. Gold was tried on both pages and read as part of the price list;
   white is the plain statement of fact this is. At 600 the extra weight was
   doing the same job the gold had been -- pulling the eye to the last line on
   the page, which is the one line that does not need it. 500 leaves it legible
   and lets it sit quietly where it belongs.

   The negative bottom margin cancels most of .shell's 80px bottom padding,
   which was otherwise leaving a band of empty page above the footer. */
.page[data-page="pricing"] .assurance,
.page[data-page="comparisons"] .assurance,
.page[data-page="faq"] .assurance,
.page[data-page="about"] .assurance,
.page[data-page="terms"] .assurance,
.page[data-page="privacy"] .assurance,
.page[data-page="refunds"] .assurance,
.page[data-page="contact"] .assurance,
.page[data-page="abuse"] .assurance {
  color: #fff;
  font-weight: 500;
  margin: 54px 0 -62px;
}
/* The help centre is green at the top and gold through the answers, and the
   split is by role rather than by accident: the chrome that says which page you
   are on (eyebrow, search box, topic chips, the nav tab) stays mint, and
   everything you actually operate takes Pricing's gold. Two accents on one page
   work when they divide like that. Scattered at random they read as undecided. */
/* Pricing joins the list. Its own --accent-2 and --link are a paler, softer
   gold than --gold-light - right for body links on a page of prices, wrong for
   the block of questions at the bottom, which is the same component the help
   centre carries and was reading as a washed-out copy of it. One gold for
   questions wherever questions appear. */
.page[data-page="faq"] .faq-mark::before,
.page[data-page="faq"] .faq-mark::after,
.page[data-page="about"] .faq-mark::before,
.page[data-page="about"] .faq-mark::after,
.page[data-page="pricing"] .faq-mark::before,
.page[data-page="pricing"] .faq-mark::after { background: var(--gold-light); }
.page[data-page="faq"] .faq-item[open],
.page[data-page="about"] .faq-item[open] {
  border-color: color-mix(in srgb, var(--gold) 38%, var(--border));
}
.page[data-page="faq"] .faq-item summary:hover,
.page[data-page="about"] .faq-item summary:hover,
.page[data-page="pricing"] .faq-item summary:hover { color: var(--gold-light); }
/* And the line under them, which is the way out of this block and into the one
   the block is a sample of. */
.page[data-page="pricing"] .faq-more a { color: var(--gold-light); }
.page[data-page="pricing"] .faq-more a:hover { color: #fdf0c4; }
.page[data-page="faq"] .faq-body a,
.page[data-page="about"] .faq-body a { color: var(--gold-light); }
.page[data-page="faq"] .faq-body a:hover,
.page[data-page="about"] .faq-body a:hover { color: #fdf0c4; }
.page[data-page="faq"] .contact-inner,
.page[data-page="about"] .contact-inner {
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--gold) 14%, var(--surface)) 0%, var(--surface) 70%);
  border-color: color-mix(in srgb, var(--gold) 34%, var(--border));
}
/* Pricing's own button, written out rather than inherited: the page tokens are
   mint here, so --primary would be the green gradient. No glow, for the reason
   Pricing has none -- a coloured shadow under a coloured button on a near-black
   page is a smudge around the thing it is meant to be pointing at. */
.page[data-page="faq"] .contact .btn-primary,
.page[data-page="about"] .contact .btn-primary {
  background: linear-gradient(135deg, #cf9f16 0%, #ebba28 55%, #fcd34d 100%);
  color: #241c05;
  box-shadow: none;
}
/* The mark on the primary button IS the wordmark, and it is sized and weighted
   to render as the wordmark does rather than to some number that looked right.
   Both were measured.

   In the header the mark is a 32px tile whose drawn ink is 19px tall beside a
   22px word, and its strokes land on screen at 2.0-2.2px. On the button the
   glyph had no tile to give it mass, sat in a 19px box (22.8px rendered under
   the page's 1.2 zoom) and drew at 1.52px - the same drawing, a fifth lighter
   and a fifth smaller, which is exactly how it read.

   22px and 1.85 put both back: a 26.4px rendered box, ink 15.7px against a
   16px label, strokes at 2.03px. Same proportion to its text as the mark has
   to the word, and the same weight of line. Change one of these two and the
   other has to move with it, or the button and the header stop agreeing about
   what the mark looks like. */
.btn-glyph { width: 22px; height: 22px; stroke-width: 1.82; }
/* The logo does not draw its two halves at one weight: the square is stroked at
   2 and the arrow at 2.2, so the arrow leads and the frame it grew from sits
   back. Same 10% here, and the same .85 on the square, or the button carries a
   flatter version of the same drawing. 1.82 and 2 land on 2.0px and 2.2px
   rendered, which is exactly what the header draws. */
.btn-glyph path { stroke-width: 2; }
.btn-glyph rect { opacity: .85; }
.assurance-item { display: inline-flex; align-items: center; gap: 8px; }
.assurance svg { width: 16px; height: 16px; stroke-width: 2; flex: none; }

/* ------------------------------- tasks --------------------------------- */
.tasks {
  position: sticky; top: 78px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 18px 18px 14px;
  display: flex; flex-direction: column;
  /* Two caps. The first keeps the column no taller than the workspace beside
     it, so a long task list cannot stretch the grid row and push the page into
     scrolling. The second is the viewport itself — vh is measured before this
     page's own zoom, so it has to be converted into the zoomed space. */
  max-height: min(calc(var(--stage-h) + 330px),
                  calc((100vh / var(--ui-scale)) - 92px));
}
.tasks-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.tasks-head h2 {
  font-family: var(--font-display); font-weight: 700; font-size: 12.5px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--fg); margin: 0;
}
.tasks-count { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.tasks-list {
  list-style: none; margin: 0; padding: 0 2px 0 0;
  display: flex; flex-direction: column; gap: 8px;
  overflow-y: auto; flex: 1; min-height: 0;
}
.tasks-list::-webkit-scrollbar { width: 8px; }
.tasks-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.tasks-empty { color: var(--muted); font-size: 12.5px; text-align: center; margin: 18px 0; }
.tasks-group + .tasks-group { margin-top: 14px; }
.tasks-group-title {
  font-size: 10.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 8px;
}
/* Only the finished list scrolls; anything still running stays in view. */
#group-active .tasks-list { overflow: visible; flex: none; }
.tasks-group { display: flex; flex-direction: column; min-height: 0; }
/* flex: none, and this is the whole of a real bug. .tasks has a max-height, so
   when the list is long the flex algorithm looks for a child to shrink - and
   .tasks-group carries min-height: 0, which says this one may shrink to
   nothing. The group duly collapsed to the height of its own title while its
   list, being overflow: visible, went on painting at full height straight over
   the Finished heading and the first finished row. Measured before the fix: a
   24px group whose list ran 60px past where Finished began. Refusing to shrink
   is correct as well as convenient - a job you are waiting on is the one thing
   in this column that must never be the part that gives way. */
#group-active { flex: none; }
/* And a rule between them, so the two sections read as two. Only when both are
   on screen: a border above a group that is alone under the heading is a line
   under the heading, which is not what it means. */
#group-active:not([hidden]) + #group-done {
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border);
}
/* An author display rule outranks the UA's [hidden] { display: none }, so the
   group has to opt back out explicitly or it never hides. */
.tasks-group[hidden] { display: none; }
#group-done { flex: 1; min-height: 0; }
.task-button {
  width: 100%; display: flex; align-items: center; gap: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 9px 11px; text-align: left;
  transition: border-color .16s var(--ease), background .16s var(--ease);
}
.task-thumb {
  width: 40px; height: 40px; flex: none;
  border-radius: 9px; object-fit: cover; display: block;
  background: var(--surface-3); border: 1px solid var(--border);
}
.task-thumb-blank { display: block; }
.task-button:hover { border-color: var(--border-accent); }
.task[data-current="true"] .task-button { border-color: var(--accent-2); background: color-mix(in srgb, var(--accent) 10%, var(--surface-2)); }
.task-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
.task[data-state="active"] .task-dot { background: var(--accent-2); animation: blink 1.4s ease-in-out infinite; }
.task[data-state="done"]   .task-dot { background: var(--ok); }
.task[data-state="failed"] .task-dot { background: var(--danger); }
@keyframes blink { 50% { opacity: .3; } }
.task-text { min-width: 0; flex: 1; }
.task-name { display: block; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.task-status { display: block; font-size: 12px; color: var(--fg); }
.task-target {
  flex: none; margin-right: 4px; font-size: 11px; font-weight: 700; letter-spacing: .06em;
  color: var(--text-display-accent);
  background: color-mix(in srgb, var(--primary-bg) 20%, transparent);
  border: 1px solid var(--border-accent); border-radius: 999px; padding: 3px 10px;
}
.task { position: relative; display: flex; align-items: center; }
.task .task-button { flex: 1; padding-right: 30px; }
/* Sits over the row's right edge, beside the 2K/4K badge. */
.task-delete {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 8px;
  color: var(--muted); opacity: 0;
  transition: opacity .15s var(--ease), color .15s var(--ease), background .15s var(--ease);
}
.task-delete svg { width: 15px; height: 15px; }
.task:hover .task-delete, .task-delete:focus-visible { opacity: 1; }
.task-delete:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }
.task-delete:disabled { opacity: .4; cursor: not-allowed; }
/* Touch devices have no hover, so the control is always visible there. */
@media (hover: none) { .task-delete { opacity: 1; } }

.tasks-foot { color: var(--muted); font-size: 12px; margin: 14px 0 0; }

/* --------------------------- comparisons page --------------------------- */
/* Justified rows.

   Every row fills the width of the page exactly. Its columns are `fr` units in
   proportion to the frames' own aspects, so frames of equal height have widths
   in that ratio -- each one fills its column, and the whole row therefore comes
   out one height, by construction. No measurement, no shared height constant to
   keep in step by hand, and every caption exactly as wide as its picture.

   What this replaces: a paired portrait row was capped to a maximum height and
   centred, which left a third of the row empty down both sides, while a wide
   sample took a whole row with its copy beside it -- so the page alternated
   between too full and too empty. Which samples share a row is now chosen in
   sections.js against this same width, so the rows come out close to a common
   height as well. */
#demo-stack { display: flex; flex-direction: column; gap: 96px; }

.show-row {
  display: grid;
  grid-template-columns: var(--cols, minmax(0, 1fr));
  /* ROW_GAP in sections.js has to match this, or the heights it packs rows to
     stop being the heights they come out at. */
  gap: 40px;
  align-items: start;
  width: 100%;
  /* A safety net rather than a layout: it only bites on a row the packer could
     not fill, which two or three mixed shapes never are. */
  max-width: var(--row-max, none);
  margin: 0 auto;
}

.show { display: flex; flex-direction: column; min-width: 0; }
.show-media, .show-copy { min-width: 0; }
/* Fills its column; the aspect ratio then decides the height, which is what
   makes every frame in a row the same height without being told. */
.show .compare-frame {
  width: 100%; max-width: 100%; height: auto;
  aspect-ratio: var(--frame-aspect, 1.4);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .45);
}
.show .compare-frame:fullscreen {
  width: 100vw; height: 100vh; max-width: none; max-height: none;
  aspect-ratio: auto; border-radius: 0; background: #000;
}
.compare-frame:fullscreen .compare-layer { object-fit: contain; }

.show-copy { margin-top: 26px; }
/* A short accent rule ties the copy to the image above it and gives the block a
   left edge to sit against, so it no longer floats in the column. */
.show-rule {
  display: block; width: 38px; height: 3px; border-radius: 2px;
  background: var(--primary); margin-bottom: 13px;
}
/* Sized against the page title rather than against the frame: at the old size
   the caption read as a footnote to the image instead of the heading of a
   section. It has to hold up in the narrowest column a row can produce, which
   is the column belonging to the tallest sample in the set. */
.show-title {
  font-family: var(--font-display); font-weight: 800;
  /* Same cap as every other block title. It keeps a lower floor for the same
     reason the feature headline does: the narrowest column a packed row can
     produce belongs to the tallest sample in the set. */
  font-size: clamp(20px, 1.4vw, var(--t-section)); line-height: 1.18;
  letter-spacing: -.025em; margin: 0 0 11px;
}
/* The two dimensions ARE the claim, and printing them is what separates a
   portfolio from a mood board: anybody can put a sharper picture beside a
   softer one, and the pair of numbers is the part that can be checked against
   the file. They are the real ones off the real files - src and out in
   sections.js - never the 2048px copies the page happens to serve.

   Tabular figures, because the cards sit side by side and ragged digits across
   a row read as a mistake. It wraps rather than overflowing: the narrowest
   column a packed row can produce belongs to the tallest sample in the set,
   and "11312 x 6016" is a wide thing to put in it. */
.show-facts {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 9px;
  margin: 0 0 12px; font-size: 12.5px; letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
}
.show-from { color: var(--fg); }
.show-to { color: var(--accent); font-weight: 600; }
.show-facts-arrow {
  width: 15px; height: 15px; flex: none; opacity: .55;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; color: var(--fg);
}

.show-text {
  color: var(--fg); font-size: var(--t-body); line-height: 1.6;
  margin: 0 0 15px; max-width: 46ch;
}
.show-cta { min-width: 0; align-self: flex-start; }
.show-arrow { width: 17px; height: 17px; }

.compare-hint { justify-content: flex-start; margin: 0 0 15px; font-size: 13px; }
.demo-missing { opacity: .45; }

/* ----------------------------- pricing page ----------------------------- */
.billing-toggle {
  display: inline-flex; gap: 4px; margin-top: 26px; padding: 5px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  /* It cannot be wider than the page, however narrow the page gets. */
  max-width: 100%; flex-wrap: wrap; justify-content: center;
}
.billing-toggle button { min-width: 0; }
.billing-toggle button {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 22px; border-radius: 999px; font-size: 14px; color: var(--fg);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.billing-toggle button[aria-checked="true"] { background: var(--primary); color: var(--text-on-accent); font-weight: 600; }
.save-pill {
  font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--gold) 20%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  color: var(--gold-light);
}
/* On the selected pill the chip has to go dark. White on gold is barely 2:1 —
   "Save 20%" was the one thing on the page nobody could read, and only once
   Annually was chosen, which is exactly when it matters. Dark chip, light text:
   the same trade the button makes with its own label, in reverse.

   Softened from .82 to .66 so the gold behind it shows through and the chip
   reads as part of the pill rather than a hole punched in it. That is as far
   as it goes: the text is the point, and every further step of transparency is
   paid for in contrast against the gold underneath. At .66 it still measures
   around 7:1, comfortably past AA. */
.billing-toggle button[aria-checked="true"] .save-pill {
  background: rgba(18, 13, 2, .66);
  border-color: rgba(18, 13, 2, .72);
  color: #ffeeb8;
}

/* Why somebody was sent to this page. Directly above the cards rather than up
   in the head, so the sentence and the answer to it are one glance apart. Gold
   because it is about credits, and boxed so it does not read as another line of
   page copy. Held to --read-w like every other sentence on a 1280px page. */
.pricing-reason {
  max-width: var(--read-w); margin: 0 auto 22px; padding: 13px 18px;
  border: 1px solid color-mix(in srgb, var(--gold-light) 38%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-light) 9%, transparent);
  color: var(--text-display-accent);
  font-size: 14.5px; line-height: 1.5; text-align: center;
}
.pricing-reason[hidden] { display: none; }

/* Four blocks on this page are built by JavaScript from /api/session, so until
   it answers they are empty and the page is 2068px tall instead of 3020 - the
   headings sit on top of each other and everything below jumps 952px into place
   when the cards arrive. Reserving what they will occupy costs nothing and
   holds the whole page still: the cards appear in the space already set aside
   for them rather than pushing the page open.

   Measured at the rendered desktop heights. They are MINIMA, and every narrower
   layout is taller than this - the rows reflow to fewer columns and grow - so
   one number is right everywhere rather than needing a breakpoint of its own. */
/* FOUR cards, so the row says four, two or one and never three.
   It was auto-fit, which counts how many 230px tracks fit and does not care
   whether the answer divides four: at about 1100px it produced three columns
   and left the fourth plan alone on a row of its own, which is the exact
   failure .about-grid was made explicit to avoid.

   It also made the height unknowable, and that is the worse half. These cards
   are built from /api/session, so the row has to hold their space open until
   they arrive; with auto-fit the row was 537px tall at 1400 and 2058 at 860,
   against a single reserved 537. On a phone the pricing page jumped fifteen
   hundred pixels on every load, which no measurement at desktop widths could
   ever have seen.

   The heights below are MEASURED at each column count, not derived: the cards
   stretch to the tallest of the four and that depends on the plan blurbs.
   Re-measure them if a plan's feature list changes. */
.tiers {
  display: grid; gap: 20px; align-items: stretch;
  grid-template-columns: minmax(0, 1fr);
  /* One column, and two heights within it: the blurbs wrap differently once
     the card drops below about 560px, which is a real phone. Measured at both.
     Below 300px it grows again and is left alone; the site's stated floor is
     320 CSS px, which is a 390px phone at 125% zoom. */
  min-height: 2058px;
}
@media (max-width: 559px) { .tiers { min-height: 2018px; } }
/* Every number here is the TALLEST the block gets anywhere in its band, taken
   with the reservations switched off. That matters both ways: reserve too
   little and the page grows when the cards land, reserve too much and the
   grid stretches its own row to fill - which is what 537/161/243 were doing,
   quietly adding 93px of nothing to a page that has to be read in one go. */
@media (min-width: 700px) {
  .tiers { grid-template-columns: repeat(2, minmax(0, 1fr)); min-height: 1021px; }
}
@media (min-width: 1200px) {
  /* 528, not the 502 this row comes to at 1400: between 1200 and about 1300
     the columns are narrow enough that two plan blurbs take a second line. */
  .tiers { grid-template-columns: repeat(4, minmax(0, 1fr)); min-height: 528px; }
}
.tier {
  position: relative; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 28px 24px 24px;
  transition: border-color .2s var(--ease), transform .2s var(--ease);
}
.tier:hover { border-color: var(--border-accent); transform: translateY(-3px); }
.tier-free { background: color-mix(in srgb, var(--surface-2) 60%, var(--surface)); }
.tier-popular {
  /* Flat, like the three beside it. The gold gradient down the head of the
     card was a second thing saying "this one" on top of the border and the
     badge, and the only one of the three that changed how the card itself
     looked — which made the row read as three cards and one advertisement. */
  border-color: var(--accent-2);
}
.tier-badge {
  position: absolute; top: -12px; left: 50%; transform: translateX(-50%); white-space: nowrap;
  background: var(--primary); color: var(--text-on-accent);
  font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  border-radius: 999px; padding: 5px 15px;
}
/* The name of the thing being bought, so it should not be the smallest type
   on the card. Uppercase and letterspaced at 13px it read as a label above the
   real heading; at 15px it reads as the heading. */
.tier-name { font-family: var(--font-display); font-weight: 700; font-size: 15px; letter-spacing: .1em; text-transform: uppercase; color: var(--text); margin: 0; }
.tier-blurb { color: var(--text-display-accent); font-size: var(--t-sub); margin: 6px 0 0; }
.tier-price { font-family: var(--font-display); font-weight: 800; font-size: 38px; letter-spacing: -.03em; margin: 12px 0 0; }
.tier-period { font-family: var(--font-body); font-weight: 500; font-size: 14px; letter-spacing: 0; color: var(--muted); margin-left: 6px; }
.tier-billed { color: var(--muted); font-size: 12px; margin: 2px 0 0; min-height: 1.3em; }
.tier-credit-pill {
  display: flex; align-items: center; gap: 9px; margin: 18px 0 18px; padding: 13px 16px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold) 9%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--gold) 32%, var(--border));
}
.tier-credit-pill b { font-family: var(--font-display); font-size: 19px; font-weight: 800; color: var(--gold-light); }
.tier-credit-pill span { color: var(--fg); font-size: 12.5px; }
.tier-facts { list-style: none; margin: 0 0 22px; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.tier-facts li { color: var(--fg); font-size: 13px; padding-left: 24px; position: relative; }
/* A tick for what the plan includes, a cross for what it does not, so the
   tiers can be compared down a column instead of read one at a time. */
.tier-facts li.has::before {
  content: ""; position: absolute; left: 3px; top: .42em;
  width: 9px; height: 5px;
  border-left: 1.9px solid var(--ok); border-bottom: 1.9px solid var(--ok);
  transform: rotate(-45deg);
}
.tier-facts li.hasnt { color: var(--muted); }
.tier-facts li.hasnt::before,
.tier-facts li.hasnt::after {
  content: ""; position: absolute; left: 3px; top: .68em;
  width: 11px; height: 1.9px; background: var(--muted); border-radius: 2px;
}
.tier-facts li.hasnt::before { transform: rotate(45deg); }
.tier-facts li.hasnt::after  { transform: rotate(-45deg); }

.tier-current { border-color: var(--ok); }
.tier-badge-current { background: var(--ok); color: #06281c; }
/* Pins the button to the bottom so tiers with different fact counts still line
   their buttons up. */
.tier .btn { margin-top: auto; width: 100%; min-width: 0; }
/* The page-specific no-glow rule that used to live here is gone because it is
   no longer specific: .btn-primary carries no shadow anywhere on the site now,
   so Pricing needs nothing of its own to stay flat. */

.oneoff { margin-top: 44px; text-align: center; }
.oneoff-title { font-family: var(--font-display); font-weight: 800; font-size: var(--t-section); margin: 0; }
.oneoff-sub { color: var(--muted); font-size: var(--t-sub); margin: 5px 0 22px; }
/* Four small price cards, centred rather than stretched: at the page's full
   width a 1fr track would give each of them 420px to hold "2K" and a number.
   The 210px cap was set against a 1230px page and left them huddled in the
   middle of an 1750px one, so the cap moved with the page. */
/* Four cards, and the same three column counts as the tiers above, at the
   same two widths. They were both auto-fit and they disagreed about where to
   wrap - the one-off row went to one column at 690 and the credit row at 770,
   so for eighty pixels of window the two halves of the same story were laid
   out differently. Written out, they cannot.

   Heights measured at each count. They are the same card, so one set does. */
.oneoff-rows, .cost-cards {
  display: grid; gap: 16px; justify-content: center;
  grid-template-columns: minmax(0, 300px);
  min-height: 481px;
}
@media (min-width: 700px) {
  .oneoff-rows, .cost-cards {
    grid-template-columns: repeat(2, minmax(0, 300px)); min-height: 232px;
  }
}
/* Compact, and the compactness is the point. Eight of these stack on this
   page and every pixel of card height is paid for twice; at 22px of padding
   around a 28px number the section stopped fitting on one screen, which is
   what it is for. 16/14 and a 25px number gets it back without the card
   reading as cramped: the kicker and the note keep their size, so what shrank
   is air and one point of the price. */
.oneoff-card {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 16px 18px 14px;
  transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.oneoff-card:hover { border-color: var(--accent-2); transform: translateY(-2px); }
/* Payments not open yet. The card still shows the price - the customer should
   be able to read what this will cost - but it must not lift under the pointer
   or look pressable, because a card that moves when you point at it has
   promised a click. `:disabled` rather than a class, so the same attribute that
   stops the click is what changes the look and the two cannot disagree. */
.oneoff-card:disabled { opacity: .55; cursor: default; }
.oneoff-card:disabled:hover { border-color: var(--border); transform: none; }
/* 11px was caption-sized under a 28px price, so the thing being priced read as
   a footnote to the number. A fifth bigger is enough to stop that; half again
   was tried and overshot -- it started competing with the price instead. */
.oneoff-kind { font-size: 13.2px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--text-display-accent); }
.oneoff-price { font-family: var(--font-display); font-weight: 800; font-size: 25px; line-height: 1.25; }
.oneoff-note { color: var(--muted); font-size: 12px; }

/* Mirrors .oneoff above, so the two halves of the pricing story - buy one
   image, or run on a plan - are laid out identically and read as a pair. The
   heading is centred; the cards themselves stay left-aligned beside their coin. */
.credits-block { margin-top: 44px; text-align: center; }
/* The same rule as the one-off row above it, and that is the point: both hold
   exactly one card per size, so they should break to two rows and back at the
   same width. They did not - a 380px cap against a 300px one put one of them on
   three columns and the other on four at 1440, which reads as two rows that
   disagree about how many sizes there are. Four columns above the page's one
   breakpoint, capped and centred below it. */

/* 1200 rather than the page's usual 1100. It was measured against the old
   one-line card, whose "4K Upscale - 40 credits" wrapped in a four-column
   track below 1200; the card is two short lines now and no longer cares. It
   stays because the two rows have to break to three columns TOGETHER - they
   are the same card at the same width, and one row of four above a row of
   three is the thing this whole section is arranged to avoid. */
@media (min-width: 1200px) {
  .oneoff-rows, .cost-cards {
    grid-template-columns: repeat(4, minmax(0, 340px)); min-height: 108px;
  }
}
/* The same card as .oneoff-card above, and that is the whole point: the two
   rows are the two halves of one story - buy a single image, or run on a plan -
   and they were being drawn as a row of cards and a row of list items. This one
   was a coin and a run-on sentence on a single line, 65px tall against the
   one-off row's 150, and it read as a footnote to the prices rather than as the
   other way of paying them.

   Kicker, one big number, note. Nothing else changed: still flat, still no
   glow, still no hover, because unlike the one-off cards these are not
   buttons and a card that lifts under the pointer promises a click. */
.cost-card {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-align: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 16px 18px 14px;
}
/* The coin sits ON the number's baseline rather than beside the text, so the
   gold reads as part of the amount and the row of four has one vertical rhythm
   with the prices above it. */
.cost-amount {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-display); font-weight: 800; font-size: 25px;
  line-height: 1.25;
}
.cost-card .coin { width: 20px; height: 20px; flex: none; }


/* ------------------------------- top-up --------------------------------- */
/* Buy credits outright. The slider indexes the ladder the server published, so
   every stop on the track is an amount the purchase endpoint already accepts. */
.topup { margin-top: 44px; text-align: center; }
.topup-card {
  /* Two cost cards wide, and the gap between them.
   *
   * The row above is four equal tracks with a 16px gap, so two of them plus one
   * gap is exactly (row - gap) / 2 -- written as that arithmetic rather than as
   * 608px, so it stays true if the gap, the column count or the page's width
   * ever move. At 760px this card was wider than the four-card rows it sits
   * under and read as a different page's furniture; aligned to two of them it
   * is plainly the same object at a smaller size.
   *
   * Only at four columns, though. Below 1200 the row is auto-fit and its column
   * count follows the window, so there is no fixed thing to be two of; the flat
   * cap below is the same fifth off the 760 this used to be, so the card gets
   * narrower there too without pretending to line up with anything. */
  width: 100%; max-width: 608px;
  /* The rhythm comes down with the width, so the card is smaller rather than
     just narrower: 26/28/24 -> 21/22/19, and the four internal margins below.
     Everything still clears the button, which is now the full Choose Plan and
     is the tallest thing in here. */
  margin: 21px auto 0; padding: 21px 22px 19px;
  /* The wash is what separates this card from the plain surface behind it.
     Flat was tried and the card stopped reading as a thing you act on and
     started reading as a paragraph with a slider in it. */
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--accent) 13%, var(--surface)) 0%, var(--surface) 72%);
  border: 1px solid var(--border-accent); border-radius: var(--radius-lg);
  text-align: left;
}
.topup-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.topup-total {
  font-family: var(--font-display); font-weight: 800; font-size: 15px; color: var(--fg);
}
.topup-total b {
  font-size: 28px; color: var(--text-display-accent); line-height: 1;
  font-variant-numeric: tabular-nums; margin-right: 6px;
}
.topup-breakdown { color: var(--fg); font-size: 13px; margin: 6px 0 0; }
.topup-bonus {
  flex: none; padding: 5px 12px; border-radius: 999px;
  background: color-mix(in srgb, var(--ok) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 45%, var(--border));
  color: var(--ok); font-size: 12px; font-weight: 700;
}
/* Author display rule above, so the UA's [hidden] has to be re-stated or the
   first rung shows an empty pill. */
.topup-bonus[hidden] { display: none; }

.topup-range {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 6px; margin: 18px 0 0;
  border-radius: 999px; background: var(--surface-2); cursor: pointer;
}
.topup-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--primary-bg); border: 2px solid var(--text);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .45);
}
.topup-range::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--primary-bg); border: 2px solid var(--text);
}
.topup-range:focus-visible { outline: 2px solid var(--link); outline-offset: 4px; }
.topup-ends {
  display: flex; justify-content: space-between;
  margin-top: 7px; color: var(--muted); font-size: 11.5px;
}
.topup-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-top: 18px;
}
.topup-rate { color: var(--fg); font-size: 13px; }
/* Same button as Choose Plan, which means the same WIDTH as well: everything
   else about the two already matched (btn-primary, 15/34 padding, 16px, the
   same radius, and no glow anywhere on this page), and 248 against 260 was the
   only difference left. `.tier .btn` is `width: 100%` of a tier card, so the
   number has to be written here -- 243 CSS px is what that comes to at the
   page's own capped width, measured, and it is checked at 1440 and up.

   It fixes a second thing worth having: at `min-width: 232px` the button was
   sized by its own label, so it grew and shrank as the slider moved through
   "Buy for 400" to "Buy for 8,000". A control that changes size while you drag
   a slider reads as part of the readout rather than as the thing you press. */
.topup-foot .btn { box-shadow: none; width: 243px; flex: none; }

/* The same 1200 the cost cards go to four columns at, and it has to be written
   after the rule above or the base max-width, being later in the file at equal
   specificity, would win. Two of four equal tracks plus the gap between them is
   exactly (row - gap) / 2, so this is the arithmetic and not a number: change
   the row's gap or its column count and the card follows. */
@media (min-width: 1200px) {
  .topup-card { max-width: calc((100% - 16px) / 2); }
}

/* The top-up card is built from the ladder the server publishes, so its space
   has to be held open too, and its text wraps three more times on the way down
   to a phone. Measured at each: 227 above 520, then 255, 275, 298. */
.topup-card { min-height: 227px; }
@media (max-width: 520px) { .topup-card { min-height: 255px; } }
@media (max-width: 370px) { .topup-card { min-height: 275px; } }
@media (max-width: 335px) { .topup-card { min-height: 298px; } }

@media (max-width: 520px) {
  .topup-card { padding: 18px 16px 17px; }
  .topup-total b { font-size: 25px; }
  .topup-foot { flex-direction: column; align-items: stretch; }
  /* The fixed Choose Plan width has to be given back here, or an explicit width
     beats the column's `align-items: stretch` and the button sits as a 243px
     block against the left edge of a phone-width card. */
  .topup-foot .btn { justify-content: center; width: 100%; }
}

/* -------------------------------- legal --------------------------------- */
/* Four documents that have to be READ, so they get the reading measure and
   nothing else: no cards, no columns, no decoration. Pricing's zoom and shell
   padding, like every other content page, so the eyebrow lands on the same
   line and a tab switch does not move it. */
.page[data-page="terms"], .page[data-page="privacy"],
.page[data-page="refunds"], .page[data-page="abuse"],
.page[data-page="contact"] { zoom: var(--ui-scale-pricing); }
.page[data-page="terms"]:not([hidden]), .page[data-page="privacy"]:not([hidden]),
.page[data-page="refunds"]:not([hidden]), .page[data-page="abuse"]:not([hidden]),
.page[data-page="contact"]:not([hidden]) {
  /* :not([hidden]) on every one of these, for the sixth time. [hidden] is a UA
     rule at 0,1,0 and .page[data-page=...] is 0,2,0, so any display written
     here un-hides all four pages underneath whichever one is showing. */
  display: flex; flex-direction: column;
}
.legal { max-width: var(--read-w); margin: 0 auto; width: 100%; text-align: left; }
.legal-block { margin-bottom: 34px; }
.legal-block h2 {
  font-family: var(--font-display); font-weight: 800; font-size: var(--t-section);
  letter-spacing: -.02em; margin: 0 0 12px; color: var(--text);
}
.legal-block p {
  color: var(--fg); font-size: var(--t-body); line-height: 1.72; margin: 0 0 13px;
}
.legal-block p:last-child { margin-bottom: 0; }
.legal-block b { color: var(--text); font-weight: 700; }
.legal-block a { color: var(--gold-light); }
.legal-block a:hover { text-decoration: underline; }

/* --------------------------------- FAQ ---------------------------------- */
.faq { margin-top: 74px; }
/* ONE column, at the reading measure, on all three pages that carry questions:
 * the help centre, the foot of Pricing and About's depth layer.
 *
 * Two columns above 1100px was the rule for a while, and it was asked back to
 * one on each of the three in turn. The argument for two was that a page 1750px
 * wide should not run an 860px ribbon down the middle of it -- but a question
 * list is read down, not surveyed, and side by side the eye has to choose a
 * column at every row. On the help centre the single column also lands at
 * exactly the width of the search box above it, so the search, the chips, the
 * group headings and the questions are one column down the page.
 *
 * A grid rather than CSS columns, deliberately. Multi-column would reflow items
 * between the columns every time a <details> opened, so the question you just
 * clicked could jump to the other side of the page. In a grid each item stays
 * where it is and only its own row grows.
 *
 * align-items: start so an open answer does not stretch the card beside it --
 * which costs nothing now and is what makes going back to two a one-line
 * change rather than a rewrite. */
.faq-list {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 10px; align-items: start;
  max-width: var(--read-w); margin: 0 auto;
}
.faq-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.faq-item[open] { border-color: var(--border-accent); }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 16px 20px; cursor: pointer; list-style: none;
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { color: var(--link-hover); }
/* A CSS-only plus/minus, so the accordion needs no JavaScript at all. */
.faq-mark { position: relative; width: 14px; height: 14px; flex: none; }
.faq-mark::before, .faq-mark::after {
  content: ""; position: absolute; background: var(--accent-2);
  transition: transform .18s var(--ease), opacity .18s var(--ease);
}
.faq-mark::before { top: 6px; left: 0; width: 14px; height: 2px; }
.faq-mark::after  { left: 6px; top: 0; width: 2px; height: 14px; }
.faq-item[open] .faq-mark::after { transform: rotate(90deg); opacity: 0; }
.faq-body { padding: 0 20px 18px; color: var(--fg); font-size: var(--t-body); line-height: 1.65; }
.faq-body a { color: var(--link); }
.faq-body b { color: var(--gold-light); }


/* ----------------------------- help centre ------------------------------ */
/* Fifty-two questions is a library, and a library needs a way in that is not
   scrolling. The search filters what is already on the page (faq.js) -- it
   never fetches and never builds a question, so it works with JavaScript off
   in the only way that matters: everything is visible. */
.faq-tools { max-width: var(--read-w); margin: 0 auto 18px; }
.faq-search { position: relative; display: flex; align-items: center; }
.faq-search-icon {
  position: absolute; left: 15px; width: 17px; height: 17px; pointer-events: none;
  fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round;
}
#faq-search {
  width: 100%; padding: 13px 44px 13px 42px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 999px;
  font-family: inherit; font-size: 14px;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}
#faq-search::placeholder { color: var(--muted); }
#faq-search:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
/* Safari draws its own X on type="search" and it sits on top of ours. */
#faq-search::-webkit-search-decoration,
#faq-search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.faq-search-clear {
  position: absolute; right: 8px; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  background: none; border: 0; border-radius: 50%; cursor: pointer;
}
.faq-search-clear svg {
  width: 15px; height: 15px;
  fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round;
}
.faq-search-clear:hover svg { stroke: var(--text); }
/* The author display rule above outranks the UA's [hidden] { display: none },
   so the opt-out has to be written. Without it the clear button is on screen
   with nothing to clear. */
.faq-search-clear[hidden] { display: none; }

.faq-count { margin: 9px 0 0; text-align: center; font-size: 12px; color: var(--muted); }

.faq-chips {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  max-width: var(--read-w); margin: 0 auto 30px;
}
.faq-chips button {
  padding: 6px 14px; border-radius: 999px; cursor: pointer;
  background: var(--surface); color: var(--fg);
  border: 1px solid var(--border);
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  transition: color .16s var(--ease), border-color .16s var(--ease),
              background .16s var(--ease);
}
.faq-chips button:hover { color: var(--text); border-color: var(--border-accent); }
.faq-chips button.is-active {
  color: var(--text); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, var(--surface));
}

/* The gap belongs between the groups, not after each one. As a margin it also
   sat after the LAST group, which is space nobody asked for and which used to
   escape the container by collapsing through it. A gap cannot do either, and a
   group the filter has hidden does not leave one behind. */
.faq-groups { display: flex; flex-direction: column; gap: 34px; }
.faq-group { margin-bottom: 0; }
.faq-group-title {
  max-width: var(--read-w); margin: 0 auto 12px;
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
}
.faq-empty {
  max-width: var(--read-w); margin: 6px auto 0; text-align: center;
  font-size: var(--t-body); color: var(--fg);
}
.faq-empty a { color: var(--link); }

/* Pricing's link into the help centre. */
.faq-more { max-width: var(--read-w); margin: 18px auto 0; text-align: center; font-size: var(--t-sub); }
.faq-more a { color: var(--link); font-weight: 600; text-decoration: none; }
.faq-more a:hover { color: var(--link-hover); text-decoration: underline; }

@media (max-width: 560px) {
  .faq-chips { gap: 6px; }
  .faq-chips button { padding: 5px 11px; font-size: 11.5px; }
  .faq-item summary { padding: 14px 16px; font-size: 14px; }
  .faq-body { padding: 0 16px 16px; }
}


/* --------------------------- about: feature rows ------------------------ */
/* Picture on one side, the argument on the other, alternating down the page.
   The picture is a real before/after slider rather than a still: the page then
   makes its case with the product instead of with adjectives, and a visitor can
   check the claim in the paragraph beside it without going anywhere.

   minmax(0, 1fr) on both tracks, not a bare 1fr: a 1fr track refuses to shrink
   below its content's minimum, and the frame inside would push the page
   sideways on a narrow window. */
.feature {
  display: grid; gap: 40px; align-items: center;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  margin-bottom: 64px;
}
/* Source order stays picture-then-copy so a screen reader and a narrow screen
   both get the image first; only the painted order flips. */
.feature-flip .feature-media { order: 2; }
.feature-media { min-width: 0; }
.feature-copy { min-width: 0; }
.feature-kicker {
  display: block; margin-bottom: 11px;
  font-family: var(--font-display); font-weight: 700; font-size: 11.5px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent-2);
}
.feature-copy h2 {
  font-family: var(--font-display); font-weight: 800;
  /* Capped at --t-section like every other block title, and allowed to fall
     below it only because this one sits in a column beside a picture and has
     to survive that column getting narrow. */
  font-size: clamp(20px, 1.8vw, var(--t-section)); line-height: 1.25;
  color: var(--text); margin-bottom: 13px;
}
.feature-copy p { color: var(--fg); font-size: var(--t-body); line-height: 1.75; }
/* Both feature rows end on the same button going to the same place. They were
   text links -- one to the portfolio, one to a single FAQ answer -- which read
   as two different offers of two different weights on rows that are otherwise a
   matched pair. A ghost button rather than a filled one: this page has one
   primary action and it is Contact support at the bottom. */
.feature-cta {
  margin-top: 20px; padding: 10px 20px;
  font-size: var(--t-sub); letter-spacing: .01em;
}
.feature-cta .btn-icon { width: 16px; height: 16px; transition: transform .16s var(--ease); }
.feature-cta:hover .btn-icon { transform: translateX(3px); }

.ab-frame {
  /* Written down, because createCompare reshapes --frame-aspect to the
     picture's own ratio once both layers have loaded - and until then the
     frame was whatever its content made it, which was 30px taller.

     It was a flat 16 / 9, on the grounds that both samples on this page are
     16:9. Neither of them is: the wildlife pair is 1.88 and the landscape 1.77,
     and since the layers are object-fit: fill, the frame was holding the page
     still by squashing the wildlife photograph 5.7% narrower than it was shot.
     Each frame carries its own --frame-aspect in the markup now, which holds
     the page just as still and does it without distorting anything. */
  aspect-ratio: var(--frame-aspect, 16 / 9);
  border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .38);
}

@media (max-width: 900px) {
  .feature { grid-template-columns: minmax(0, 1fr); gap: 22px; margin-bottom: 48px; }
  /* Order resets, so the picture leads on every row rather than alternating
     into a rhythm that no longer exists once the columns have stacked. */
  .feature-flip .feature-media { order: 0; }
}

/* ------------------------------- about ---------------------------------- */
/* Prose, so it is held to the reading column rather than run across the page.
   Centred under a centred H1, which is what it looked like at 760 as well. */
.about-lead { max-width: var(--read-w); margin: 0 auto 46px; }
.about-lead p { color: var(--fg); font-size: var(--t-body); line-height: 1.8; margin-bottom: 16px; }
/* The one paragraph on these four pages allowed above --t-body. It is the
   opening sentence of the page and it is meant to be read first; a point and a
   half over the body is what says so without another heading. */
.about-lead p:first-child {
  margin-top: 0;
  color: var(--text); font-size: 16.5px; font-weight: 500; line-height: 1.65;
}
.about-lead p:last-child { margin-bottom: 0; }

.about-block { margin-bottom: 64px; }

/* Six cards, so the only arrangements worth having are 3x2, 2x3 and a column.
   Written out rather than left to auto-fit, which counts how many tracks fit
   and does not care whether the answer divides six: at 1750px a 300px floor
   gave FIVE columns and left the sixth card alone on a row of its own, and
   nudging the floor only moved which width it happened at. Two floors also had
   the two grids on this page stepping down at different widths, which read as
   an accident because it was one.

   1100px is the same breakpoint the questions use, so the page reorganises at
   one width rather than at three. */
.about-grid {
  display: grid; gap: 18px;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 700px)  { .about-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .about-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.about-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 22px 22px 24px;
  transition: border-color .18s var(--ease), background .18s var(--ease),
              transform .18s var(--ease);
}
.about-card:hover {
  border-color: var(--border-accent);
  background: var(--surface-2);
  transform: translateY(-2px);
}
.about-card:hover .about-icon { stroke: var(--accent-pink); }
.about-card h3 {
  font-family: var(--font-display); font-weight: 700; font-size: 16px;
  color: var(--text); margin-bottom: 9px;
}
.about-card p { color: var(--fg); font-size: 13.5px; line-height: 1.7; }
.about-card-sm h3 { font-size: 15px; }
.about-icon {
  width: 26px; height: 26px; margin-bottom: 13px;
  transition: stroke .18s var(--ease);
  fill: none; stroke: var(--accent-2); stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

@media (max-width: 640px) {
  .about-block { margin-bottom: 44px; }
}



/* ----------------------------- credits panel ---------------------------- */
.credits-wrap { position: relative; }
.credits-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 40;
  width: 290px; padding: 18px;
  background: var(--surface); border: 1px solid var(--border-accent);
  border-radius: var(--radius); box-shadow: 0 18px 46px rgba(0, 0, 0, .5);
}
/* Author display rules above, so the UA's [hidden] has to be re-stated. */
.credits-panel[hidden] { display: none; }
.cp-top { display: flex; align-items: baseline; gap: 8px; }
.cp-balance {
  font-family: var(--font-display); font-weight: 800; font-size: 27px;
  color: var(--gold-light); line-height: 1; font-variant-numeric: tabular-nums;
}
.cp-unit { color: var(--fg); font-size: 12.5px; }
.cp-plan {
  margin-left: auto; padding: 3px 10px; border-radius: 999px;
  background: color-mix(in srgb, var(--gold) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 38%, var(--border));
  color: var(--gold-light); font-size: 11px; font-weight: 700;
}
.cp-bar {
  height: 5px; margin: 13px 0 7px; border-radius: 999px;
  background: var(--surface-2); overflow: hidden;
}
.cp-bar i { display: block; height: 100%; border-radius: 999px; background: var(--primary); }
.cp-note { color: var(--muted); font-size: 11.5px; line-height: 1.5; }
.cp-rows { margin: 14px 0; border-top: 1px solid var(--border); }
.cp-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 0; border-bottom: 1px solid var(--border);
  color: var(--fg); font-size: 12.5px;
}
.cp-row b { color: var(--text); font-weight: 600; }
.cp-actions { display: grid; gap: 8px; }
.cp-actions .btn { width: 100%; justify-content: center; padding: 10px 14px; font-size: 13px; }

@media (max-width: 560px) {
  /* The nav wraps down here and an anchored panel would hang off the edge, so
     it becomes a centred sheet instead. */
  .credits-panel { position: fixed; top: auto; right: 12px; left: 12px; width: auto; }
}

/* ----------------------------- contact dialog --------------------------- */
/* Every "Contact us" on the site opens this instead of firing a mailto at an
   email client that may not be configured. The triggers stay real
   <a href="mailto:"> links, so with JavaScript off they still work; main.js
   intercepts the click. */
.cdlg-back {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center; padding: 24px;
  background: color-mix(in srgb, #000 62%, transparent);
  backdrop-filter: blur(6px);
  opacity: 0; transition: opacity .18s var(--ease);
}
.cdlg-back.is-open { opacity: 1; }
/* The author display rule above outranks the UA's [hidden] { display: none },
   so the opt-out has to be spelled out or the dialog never closes. */
.cdlg-back[hidden] { display: none; }
.cdlg {
  position: relative; width: min(460px, 100%);
  background: var(--surface); border: 1px solid var(--border-accent);
  border-radius: var(--radius-lg); padding: 30px 30px 26px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .55);
  transform: translateY(8px) scale(.985);
  transition: transform .2s var(--ease);
}
.cdlg-back.is-open .cdlg { transform: none; }
.cdlg-close {
  position: absolute; top: 12px; right: 12px;
  display: grid; place-items: center; width: 32px; height: 32px;
  border-radius: 50%; color: var(--muted);
}
.cdlg-close:hover { color: var(--text); background: var(--surface-2); }
.cdlg-close svg { width: 16px; height: 16px; stroke-width: 2; }
.cdlg-mark {
  display: grid; place-items: center; width: 46px; height: 46px; margin-bottom: 16px;
  border-radius: 14px; background: color-mix(in srgb, var(--accent) 20%, var(--surface-2));
  border: 1px solid var(--border-accent);
}
.cdlg-mark svg { width: 22px; height: 22px; color: var(--accent-2); stroke-width: 1.8; }
.cdlg h2 {
  font-family: var(--font-display); font-weight: 800; font-size: 21px;
  color: var(--text); margin-bottom: 9px;
}
.cdlg p { color: var(--fg); font-size: 14px; line-height: 1.65; margin-bottom: 16px; }
.cdlg-mail {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 15px; margin-bottom: 14px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
}
.cdlg-mail a {
  font-family: var(--font-display); font-weight: 700; font-size: 15px;
  color: var(--link); text-decoration: none; word-break: break-all;
}
.cdlg-mail a:hover { color: var(--link-hover); }
.cdlg-copy {
  flex: none; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--fg); font-size: 12px; font-weight: 600;
}
.cdlg-copy:hover { color: var(--text); border-color: var(--border-accent); }
.cdlg-copy.is-done { color: var(--ok); border-color: var(--ok); }
.cdlg-when {
  display: flex; align-items: flex-start; gap: 9px;
  color: var(--muted); font-size: 12.5px; line-height: 1.55; margin-bottom: 20px;
}
.cdlg-when svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }
.cdlg .btn { width: 100%; justify-content: center; }

/* ---------------------------- the sign-in dialog ------------------------ */
/* Borrows .cdlg wholesale so the two overlays on this site are one object at
   two contents. Only what a form needs is added. */
.adlg p { margin-bottom: 14px; }
.adlg-form { display: flex; flex-direction: column; gap: 12px; text-align: left; }
.adlg-field { position: relative; display: flex; flex-direction: column; gap: 6px; }
.adlg-field > span {
  font-family: var(--font-display); font-weight: 600; font-size: 12.5px;
  color: var(--fg);
}
.adlg-field input {
  width: 100%; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text);
  font-family: inherit; font-size: 14.5px;
  transition: border-color .15s var(--ease);
}
.adlg-field input:focus {
  outline: none; border-color: var(--border-accent);
}
.adlg-field input::placeholder { color: var(--muted); }
/* Reveal the password rather than force a second "confirm" box. One field that
   can be checked beats two that have to agree. */
.adlg-peek {
  position: absolute; right: 10px; bottom: 11px;
  padding: 4px 8px; border-radius: 7px;
  color: var(--muted); font-size: 11.5px; font-weight: 600;
}
.adlg-peek:hover { color: var(--text); background: var(--surface-3); }
.adlg-hint { color: var(--muted); font-size: 12px; margin: -2px 0 0; }
.adlg-error {
  margin: 0; padding: 10px 12px; border-radius: 9px;
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 38%, var(--border));
  color: var(--text); font-size: 13px; line-height: 1.5; text-align: left;
}
/* Every one of these carries an author `display`, which outranks the UA's
   [hidden] { display: none } at 0,1,0. `.adlg-field` (flex) and the resend
   button (.btn, inline-flex) were both found by sweeping rather than by
   reading: without them the email box stayed on screen in reset mode, where
   there is nothing to type into it, and "Send it again" sat under the
   forgotten-password message offering to resend a thing that was never sent. */
/* Continue with Google, and whatever else is configured. A full-width row
   each, on the pale surface every provider's brand guidelines expect, because
   a coloured logo on a coloured button reads as neither. */
.adlg-oauth { display: flex; flex-direction: column; gap: 9px; margin-bottom: 4px; }
/* Google publishes a spec for this button and it is worth following to the
   letter, because the whole value of it is that somebody recognises it before
   they read it. A near-miss reads as a copy of a Google button, which is
   exactly the impression a site asking for your Google account cannot afford.
   Theirs: WHITE ground (not grey), Roboto MEDIUM (not a display face at 700),
   14px, and the mark at 18px. Every one of those was off by a little here,
   and a little in four places is what made it look wrong without looking
   nameable. It is the one control on the site that is deliberately not in the
   house style. */
.oauth-btn {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  width: 100%; padding: 12px 16px; border-radius: var(--radius);
  background: #fff; border: 1px solid #dadce0; color: #1f1f1f;
  font-family: Roboto, "Helvetica Neue", Arial, system-ui, sans-serif;
  font-weight: 500; font-size: 14px; letter-spacing: .15px;
  text-decoration: none;
  transition: background .15s var(--ease), border-color .15s var(--ease),
              box-shadow .15s var(--ease);
}
/* Google's own hover is a faint grey wash and a shadow, not a border change. */
.oauth-btn:hover {
  background: #f7f8f8; border-color: #d2d5d9;
  box-shadow: 0 1px 3px rgba(60, 64, 67, .18);
}
.oauth-btn svg { width: 18px; height: 18px; flex: none; }
/* A rule with the word on it, so the two halves read as alternatives rather
   than as a sequence. */
.adlg-or {
  display: flex; align-items: center; gap: 12px;
  margin: 14px 0 12px; color: var(--muted); font-size: 12px;
}
.adlg-or::before, .adlg-or::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}
.adlg-oauth[hidden], .adlg-or[hidden] { display: none; }

.adlg-hint[hidden], .adlg-error[hidden], .adlg-sent[hidden],
.adlg-devlink[hidden], .adlg-links[hidden], .adlg-small[hidden],
.adlg-form[hidden], .adlg-field[hidden], .adlg-sent .btn[hidden] { display: none; }
.adlg-sent { display: flex; flex-direction: column; gap: 12px; text-align: left; }
.adlg-sent p { margin: 0; }
/* Development only: the link the email would have carried, on a machine that
   cannot send email. It is a full-width block because it is long and gets
   clicked, not read. */
.adlg-devlink {
  display: block; padding: 10px 12px; border-radius: 9px;
  background: var(--surface-2); border: 1px dashed var(--border-accent);
  color: var(--link); font-size: 11.5px; line-height: 1.5;
  overflow-wrap: anywhere; text-decoration: none;
}
.adlg-devlink:hover { color: var(--link-hover); }
.adlg-links {
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px;
  margin-top: 14px;
}
.adlg-links button {
  color: var(--fg); font-size: 12.5px;
  text-decoration: underline; text-underline-offset: 2px;
}
.adlg-links button:hover { color: var(--link-hover); }
.adlg-small {
  margin: 14px 0 0 !important; padding-top: 13px;
  border-top: 1px solid var(--border);
  color: var(--muted) !important; font-size: 11.5px !important; line-height: 1.55;
}

/* ------------------------------ the code box ---------------------------- */
/* In the credits panel, closed until asked for: most people never have a code,
   and a box asking for one they do not have reads as something they are
   missing out on. */
.cp-code { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.cp-code-open {
  color: var(--fg); font-size: 12px;
  text-decoration: underline; text-underline-offset: 2px;
}
.cp-code-open:hover { color: var(--gold-light); }
.cp-code-form { display: flex; gap: 8px; margin-top: 9px; }
.cp-code-form[hidden] { display: none; }
.cp-code-form input {
  flex: 1; min-width: 0; padding: 9px 11px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 9px; color: var(--text);
  font-family: inherit; font-size: 13px; letter-spacing: .06em;
  text-transform: uppercase;
}
.cp-code-form input:focus { outline: none; border-color: var(--border-accent); }
.cp-code-form button {
  flex: none; padding: 9px 14px; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); font-family: var(--font-display);
  font-weight: 700; font-size: 12.5px;
}
.cp-code-form button:hover { border-color: var(--border-accent); }
.cp-code-note { margin: 8px 0 0; font-size: 11.5px; line-height: 1.5; }
.cp-code-note[hidden] { display: none; }
.cp-code-note.is-bad { color: #fca5a5; }
.cp-code-note.is-good { color: var(--ok); }

@media (max-width: 480px) {
  .cdlg { padding: 26px 20px 22px; }
  .cdlg h2 { font-size: 19px; }
}

/* ------------------------------ 404 page -------------------------------- */
/* Served standalone by pages.render_not_found, so it borrows the site's own
   classes and adds only what it needs: the page centred in the window, since
   there is no content below it to balance against. */
body.is-error { display: flex; flex-direction: column; min-height: 100vh; }
.error-page { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-page .shell { padding-bottom: 42px; text-align: center; }
.error-page .page-sub { max-width: var(--read-w); margin-left: auto; margin-right: auto; }
.error-actions {
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: 12px; margin-top: 30px;
}

/* ------------------------------- contact -------------------------------- */
.contact { margin-top: 56px; }
/* Adjacent margins collapse in block layout and do NOT between flex items, and
   the shells on these pages are flex columns now. Everywhere else on the site
   only one of any two neighbours carries a margin, so nothing was collapsing to
   begin with; About is the one place two did, and 64 + 56 became 120 where it
   used to be the larger of the two. Dropping the second restores it. */
.page[data-page="about"] .about-block + .contact { margin-top: 0; }
/* One width on all three pages that carry it. It used to be the width of
   whatever shell it happened to be in -- 1232 on Pricing and About, 972 in the
   help centre -- which is how the same card came to be two sizes. Capped and
   centred now, so it is one object wherever you meet it, and narrower than the
   page on purpose: it is a single sentence and a button, and a band 1750px
   wide would put those two things at opposite ends of the screen. */
.contact-inner {
  max-width: 1100px; margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 22px; flex-wrap: wrap;
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, var(--surface)) 0%, var(--surface) 70%);
  border: 1px solid var(--border-accent); border-radius: var(--radius-lg); padding: 28px 30px;
}
.contact-title { font-family: var(--font-display); font-weight: 800; font-size: 21px; margin: 0; }
.contact-sub { color: var(--fg); font-size: var(--t-sub); margin: 5px 0 0; }

/* The development-mode banner and the "illustrative samples" footnote both
   lived here and are gone: this is being dressed as a real business now, and a
   page that keeps announcing it is a rehearsal cannot also be the shop. The
   rules are removed rather than left unused so nothing re-attaches to them by
   accident. What the banner disclosed has NOT gone away -- see CLAUDE.md:
   purchases are still simulated while DEV_PURCHASES_ENABLED is true, and that
   has to be closed before this faces a paying stranger. */

/* -------------------------------- footer -------------------------------- */
/* One slim line. The page links lived here as well and simply duplicated the
   header, which cost a scroll on the upscaler for no benefit. */
.footer { border-top: 1px solid var(--border); margin-top: auto; }
/* display: contents rather than a block, so on the help centre the links join
   .footer-legal's own row exactly as they did when they were its children -
   there is no second flex context and the spacing is unchanged. */
.footer-links { display: none; }
body[data-page="faq"] .footer-links,
body[data-page="terms"] .footer-links,
body[data-page="privacy"] .footer-links,
body[data-page="refunds"] .footer-links,
body[data-page="contact"] .footer-links,
body[data-page="abuse"] .footer-links { display: contents; }

.footer-legal {
  display: flex; justify-content: center; align-items: center;
  gap: 20px; flex-wrap: wrap; padding: 13px 24px;
}
.footer-legal a, .footer-copy { color: var(--muted); font-size: 12.5px; text-decoration: none; margin: 0; }
.footer-legal a:hover { color: var(--fg); text-decoration: underline; }

/* -------------------------------- editor -------------------------------- */
/* The edit page carries Comparisons' and Pricing's zoom and the same shell
   inset, so the pill at the top of all three lands on exactly the same line.
   Switching tabs must not make the chrome jump. */
.page[data-page="edit"] {
  zoom: var(--ui-scale-pricing);

  /* Orange, end to end. The upscaler is the purple product surface; editing is
     a free local tool and reads as its own thing. Overriding the tokens rather
     than restyling each control means every shared component — buttons, links,
     chips, sliders, focus rings — recolours with the page. */
  --accent:              var(--warm);
  --accent-2:            #d99a6b;
  --accent-pink:         #d0a865;
  --primary:             linear-gradient(135deg, #a8602c 0%, #c97b3c 55%, #d3925a 100%);
  --primary-bg:          var(--warm);
  --link:                var(--warm-light);
  --link-hover:          #f2d3b6;
  --border-accent:       #56341a;
  --text-display-accent: var(--warm-light);
}
/* Wider than the reading pages. This one is a workspace: every pixel of width
   is another pixel of image. */
/* Written as the token so the editor and the four content pages cannot
   drift apart: this IS the width they were all set to match. */
.shell-edit { max-width: var(--page-w); padding: 42px 24px 20px; }

/* Height comes from JS as px, for exactly the reason the upscaler canvas does:
   vh shrinks under browser zoom, so a vh-sized editor would get smaller at the
   moment someone zooms in to work on detail. See measureEditor in main.js. */
.editor {
  --panel-w: calc(var(--side-panel) / var(--ui-scale-pricing));
  --panel-gap: 22px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--panel-w);
  /* Two rows: the pill, then the stage. The panel spans both, so it starts at
     the very top of the page and gets the pill's height back as working room.
     --edit-h sizes the stage row alone, not the grid, so it stays the one
     number the image's max-height can be measured against. */
  grid-template-rows: auto var(--edit-h, 520px);
  column-gap: var(--panel-gap);
  align-items: stretch;
  position: relative;
}
.ed-head {
  grid-column: 1; grid-row: 1;
  text-align: center; margin-bottom: 18px;
}
/* The pill has to stay centred on the *page*, not on its column, or it would
   jump on the way in from Comparisons and Pricing. Half the panel's width puts
   it back over the page centre, derived from the same two numbers the grid
   itself uses, so the two cannot drift apart. */
.ed-head .eyebrow {
  transform: translateX(calc((var(--panel-w) + var(--panel-gap)) / 2));
}
/* The panel stays in the layout with no image open, so the page reads as a
   whole editor from the first look rather than an empty box that sprouts a
   column later, and nothing shifts sideways when a photo is dropped. */
.ed-panel.is-idle .ed-panel-scroll,
.ed-panel.is-idle .ed-panel-foot { opacity: .38; }
.ed-panel-note {
  display: none;
  margin: 8px 0 4px; padding: 9px 11px;
  border: 1px dashed color-mix(in srgb, var(--accent) 34%, var(--border));
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 5%, transparent);
  color: var(--link); font-size: 12px; text-align: center;
}
.ed-panel.is-idle .ed-panel-note { display: block; }

/* Deliberately bare. Once an image is open it is the only thing on the stage —
   no card, no border, no padding around it to make it look smaller than it is. */
/* Centred on the TOOLS PANEL, not on the stage, and that is the whole of the
   height rule below.

   The stage begins under the pill row; the panel begins above it and is lifted
   another 16px besides. So a picture centred in the stage sat exactly that far
   below the panel's own centre line — measured 89 rendered px, identical at
   every viewport and every image shape, which is what made a landscape photo
   read as hanging off the top of the page.

   Centres can only meet if the picture gives that distance back, because it may
   not rise above the pill. Handing it back at the BOTTOM is what does it: the
   stage keeps its top where the grid put it and ends --ed-lift short, so
   `align-items: center` inside it now lands the picture's middle exactly on the
   panel's. Equal gap above and below, whatever shape is opened.

   The cost is --ed-lift of picture height on anything height-bound, and it is
   paid knowingly: a square that was 824px tall is now 735. The alternative was
   to shorten the panel, which cannot spare it — it already scrolls at 1080p —
   or to let the pill sit over the top of a portrait. */
.ed-stage {
  grid-column: 1; grid-row: 2;
  position: relative; min-width: 0; min-height: 0;
  height: calc(100% - var(--ed-lift, 0px));
  display: flex; align-items: center; justify-content: center;
}

.ed-drop {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 1.6px dashed color-mix(in srgb, var(--accent) 46%, var(--border));
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  text-align: center; padding: 24px;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
/* An author display rule outranks the UA's [hidden] { display: none }, so the
   drop area has to opt back out explicitly or it never hides. */
.ed-drop[hidden] { display: none; }
.ed-drop:hover, .ed-drop.is-dragging {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 11%, var(--surface));
}
.ed-drop svg { width: 64px; height: 64px; color: var(--link); stroke-width: 1.5; }
.ed-drop-title { font-family: var(--font-display); font-weight: 700; font-size: 21px; margin: 18px 0 0; }
.ed-drop .dropzone-meta { font-size: 14px; }

/* The viewport FILLS the stage instead of sitting at the bitmap's natural size.
   `max-width: 100%` + `height: auto` on the canvas only ever shrinks, so a
   953x507 photo opened in a 1170px stage drew at 953px and left a third of the
   room empty — the smaller the picture, the more was wasted, which is backwards.
   Both limits are written out as one min(), the same spelling the upscaler's
   immersive frame uses and for the same reason: with the height definite, a
   max-width clamp wins and `aspect-ratio` loses, which stretches anything far
   from 3:2. min() of the two fits any shape with nothing left to resolve, so
   the height can never exceed --edit-h and the page never scrolls.

   --ed-aspect is written by paintPreview from the bitmap it just drew, so it
   follows the crop when "Apply crop" reshapes the preview. The crop overlay is
   positioned in percentages on top, so the two stay locked at any size. */
.ed-viewport {
  position: relative; line-height: 0;
  /* The height limit is the SHORTENED stage, or a tall picture would overflow
     the bottom of it and the page would scroll. --ed-lift is the same number
     .ed-stage gives back, so the two cannot drift apart. */
  width: min(100%, calc((var(--edit-h, 520px) - var(--ed-lift, 0px)) * var(--ed-aspect, 1.5)));
}
.ed-viewport[hidden] { display: none; }
#ed-preview {
  display: block;
  width: 100%; height: auto;
  border-radius: 8px;
}

/* Clipped to the picture, which is the whole point of it being its own element:
   the hole carries the 100vmax spread, and `overflow: hidden` here is what stops
   that spread reaching the rest of the page. */
.ed-scrim {
  position: absolute; inset: 0; z-index: 2;
  display: none; overflow: hidden; pointer-events: none;
  border-radius: 8px;
}
.ed-scrim > i { position: absolute; box-shadow: 0 0 0 100vmax rgba(5, 8, 15, .55); }
/* Dimmed whenever the crop is really discarding something, box or no box: the
   preview must never show pixels the export will not contain. */
.ed-viewport.is-cropping .ed-scrim,
.ed-viewport.has-crop .ed-scrim { display: block; }
/* Except under "Apply crop", where the preview IS the crop and there is nothing
   left outside it to dim. */
.ed-viewport.is-applied .ed-scrim { display: none; }

/* Over the image, top right — the same shape and place as the upscaler's
   full-screen control, so the two pages behave alike. */
.ed-overlay {
  position: absolute; top: 12px; right: 12px; z-index: 4;
  display: flex; gap: 8px; line-height: 1;
}
.ed-obtn {
  display: grid; place-items: center;
  width: 36px; height: 36px; border-radius: 10px;
  background: rgba(5, 8, 15, .82); border: 1px solid rgba(255, 255, 255, .16);
  color: var(--text-on-surface-strong); backdrop-filter: blur(6px);
  transition: background .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
}
.ed-obtn:hover { background: rgba(5, 8, 15, .96); border-color: var(--accent); color: var(--link); }
.ed-obtn svg { width: 17px; height: 17px; }
#ed-close:hover { border-color: var(--danger); color: var(--danger); }

/* Only ever the box itself now — border, guides and handles. The dimming moved
   to .ed-scrim, so this no longer has to be displayed-but-invisible to carry a
   shadow, and the four rules that used to strip it back down are gone with it. */
.ed-crop {
  position: absolute; display: none; z-index: 3;
  border: 2px solid #fff;
  cursor: move; touch-action: none;
}
.ed-viewport.is-cropping .ed-crop { display: block; }

/* Rule-of-thirds guides, drawn with borders so they cost nothing. */
.ed-crop::before, .ed-crop::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-style: solid; border-color: rgba(255, 255, 255, .32);
}
.ed-crop::before { border-width: 0 1px; left: 33.33%; right: 33.33%; }
.ed-crop::after  { border-width: 1px 0; top: 33.33%; bottom: 33.33%; }
.ed-handle {
  position: absolute; width: 18px; height: 18px;
  background: #fff; border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .5);
}
.ed-handle[data-handle="nw"] { top: -9px; left: -9px; cursor: nwse-resize; }
.ed-handle[data-handle="ne"] { top: -9px; right: -9px; cursor: nesw-resize; }
.ed-handle[data-handle="sw"] { bottom: -9px; left: -9px; cursor: nesw-resize; }
.ed-handle[data-handle="se"] { bottom: -9px; right: -9px; cursor: nwse-resize; }
/* Bars rather than squares, lying along the edge they move, which is how every
   editor draws the difference between "this corner" and "this side". Centred,
   so a corner and an edge only meet on a crop small enough that the corner is
   the more useful of the two - and there the corner is later in the DOM and
   takes the pointer. */
.ed-handle-edge { width: 26px; height: 12px; border-radius: 6px; }
.ed-handle[data-handle="n"] { top: -6px;    left: 50%; margin-left: -13px; cursor: ns-resize; }
.ed-handle[data-handle="s"] { bottom: -6px; left: 50%; margin-left: -13px; cursor: ns-resize; }
.ed-handle[data-handle="w"] { left: -6px;  top: 50%; margin-top: -13px; cursor: ew-resize;
                              width: 12px; height: 26px; }
.ed-handle[data-handle="e"] { right: -6px; top: 50%; margin-top: -13px; cursor: ew-resize;
                              width: 12px; height: 26px; }

/* Exactly as tall as the stage beside it, and the page itself never scrolls.
   Only the list of tools gives way on a short screen — the actions are pinned
   below it, so Use in Upscaler is on screen whatever the window height. */
/* Positioned rather than placed in the grid. Spanning both rows made the
   panel's own content the thing that sized the pill's row, which pushed the
   stage down, which shrank the measured height, which shrank the stage again.
   Out of flow it fills the column top to bottom and sizes nothing but itself. */
.ed-panel {
  /* Lifted so it stands exactly as tall as Recent Tasks on the upscaler, which
     is the same object one tab over. The Edit page has a pill row the upscaler
     does not, so .editor starts lower than the workspace does and the panel
     inherited that. Pulling it up closes the gap without moving the pill, which
     has to stay on the same line as the eyebrows on Comparisons and Pricing.
     In CSS px against this page's 1.07 zoom: 16 x 1.07 is the 17 rendered px
     measured between the two panels' top edges. */
  position: absolute; top: -16px; right: 0; bottom: 0;
  width: var(--panel-w);
  min-height: 0;
  display: flex; flex-direction: column;
  font-size: 12.5px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 4px 16px 12px;
}
.ed-panel[hidden] { display: none; }
.ed-panel-scroll {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  overflow-y: auto; overscroll-behavior: contain;
  /* Room for the scrollbar, always reserved, so the controls do not shuffle
     sideways the moment the list gets long enough to scroll. */
  padding-right: 10px; margin-right: -6px;
  /* A classic scrollbar, not an overlay one: on a short window the tool list is
     the only thing that scrolls, so it has to look like it does. The standard
     properties rather than ::-webkit-scrollbar, which Chromium ignores once a
     gutter is reserved. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent) 55%, var(--border)) var(--surface-2);
}
.ed-panel-foot { flex: 0 0 auto; border-top: 1px solid var(--border); }

/* Spare room is shared out inside the blocks rather than dumped between them.
   Handing the slack to `justify-content: space-between` opened a visible hole
   under each divider -- five ruled lines with nothing beneath them read as
   something missing. Growing the blocks and centring their contents puts the
   same pixels above and below each group, which reads as breathing room.
   `flex-shrink: 0`, so a full panel scrolls rather than squashing. */
.tool-block {
  flex: 1 0 auto;
  display: flex; flex-direction: column; justify-content: center;
  padding: 5px 0; border-bottom: 1px solid var(--border);
}
.tool-block:last-child { border-bottom: 0; }
.tool-head { display: flex; align-items: center; justify-content: space-between; }
.tool-h {
  font-family: var(--font-display); font-weight: 700; font-size: 12px;
  letter-spacing: .11em; text-transform: uppercase; color: var(--fg); margin: 0 0 6px;
}
.tool-head .tool-h { margin-bottom: 6px; }
/* The heading carries a bottom margin, so a centred flex line puts the control
   beside it half a margin low. Giving it the same margin lines the two up. */
.tool-head .tool-check { margin-bottom: 6px; }
.tool-inline-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px 12px; flex-wrap: wrap; margin-bottom: 8px;
}
/* The heading breaks the row before it breaks itself — "Rotate &" over "flip"
   reads as a mistake, a heading with the buttons under it does not. */
.tool-inline-row .tool-h { margin: 0; white-space: nowrap; }
.tool-mini {
  font-size: 11.5px; color: var(--muted); padding: 2px 6px; border-radius: 6px;
  transition: color .15s var(--ease);
}
.tool-mini:hover { color: var(--link-hover); }
#tool-crop-toggle[aria-pressed="true"] { color: var(--link); }
/* Two rows of five chips, built by paintChips after the module loads. The
   block was 69px tall until they arrived and 147px after, so everything below
   it in the tools column jumped 78px on every load of the editor. */
.tool-presets { min-height: 78px; }
/* The rotate and flip buttons are built by tools.js, so the block was 58px
   until they arrived and 79px after - and because .tool-block is flex: 1 0 auto
   with its content centred, that 21px pushed every block below it down and
   re-centred the ones above.

   **The reserve is the button's own height, written once**, and that is the
   whole point of the token. It was a hand-measured 34px against a button that
   is 38, so four pixels of the jerk survived the fix that was meant to remove
   it: the row still grew when the buttons landed, every block below Rotate &
   flip still moved, and it was small enough to read as the panel shivering
   rather than as anything identifiable. Reserving a number somebody typed in
   next to an element sized by a different number somebody typed in is not a
   reservation, it is a coincidence waiting to lapse. */
:root { --tool-icon: 38px; }
.tool-icons { min-height: var(--tool-icon); }
.tool-presets { display: flex; flex-wrap: wrap; gap: 6px; }
.tool-chip {
  padding: 6px 11px; border-radius: 999px; font-size: 12.5px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--fg);
  transition: border-color .15s var(--ease), color .15s var(--ease), background .15s var(--ease);
}
.tool-chip:hover { color: var(--text); border-color: var(--border-accent); }
.tool-chip[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  color: var(--link);
}
.tool-icons { display: flex; gap: 8px; }
.tool-icon {
  display: grid; place-items: center;
  width: var(--tool-icon); height: var(--tool-icon); border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--fg);
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.tool-icon:hover { color: var(--link); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
.tool-icon svg { width: 19px; height: 19px; }

.tool-row { display: flex; gap: 10px; margin-bottom: 7px; }
.tool-row label, .tool-slider {
  display: flex; flex-direction: column; gap: 5px; flex: 1;
  font-size: 11.5px; color: var(--muted);
}
.tool-row input[type="number"], .tool-row select {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 9px; padding: 7px 10px; color: var(--text); font-size: 13.5px;
  min-width: 0; width: 100%;
}
.tool-slider { margin-bottom: 2px; position: relative; gap: 3px; }
.tool-slider b { position: absolute; right: 0; top: 0; color: var(--fg); font-weight: 600; }
.tool-slider input[type="range"] { width: 100%; accent-color: var(--accent); }

/* One line per adjustment: name, track, value. */
.tool-ranges { display: flex; flex-direction: column; gap: 6px; }
.tool-range {
  display: grid; grid-template-columns: 62px minmax(0, 1fr) 34px;
  align-items: center; gap: 9px;
  font-size: 11px; color: var(--muted);
}
.tool-range input[type="range"] { width: 100%; margin: 0; accent-color: var(--accent); }
.tool-range b {
  color: var(--fg); font-weight: 600; font-size: 11px;
  text-align: right; font-variant-numeric: tabular-nums;
}
/* The tone curve of what is actually on screen, so Highlights and Shadows are
   aimed at something rather than guessed at. Drawn from a 160px thumbnail of
   the preview, which is cheap enough to redraw on every frame of a drag. */
.tool-histogram {
  display: block; width: 100%; height: 34px; margin: 0 0 7px;
  border-radius: 7px;
  background: var(--surface-2); border: 1px solid var(--border);
  /* The bars are drawn in the canvas's own `color`, so the page's accent
     override reaches them without JavaScript being told which page it is on. */
  color: color-mix(in srgb, var(--accent) 78%, transparent);
}
.tool-minis { display: flex; align-items: center; gap: 4px; }
.tool-check { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--fg); }
.tool-check input { accent-color: var(--accent); }
/* The crop block's two switches, on their own line under the heading. They are
   a pair and read as one decision, so they sit together rather than one of them
   being tucked up beside the word "Crop" as it used to be. */
/* The switches sit on their own line between the heading and the ratios, with
   room on both sides — they are a decision to read, not a strip to skim past.
   The room is affordable because the crop hint that used to sit under the
   ratios is gone; that is where it came from. */
.tool-checks {
  display: flex; align-items: center; gap: 18px;
  margin: 3px 0 10px;
}
.tool-block-crop .tool-head .tool-h { margin-bottom: 2px; }
.tool-block-crop .tool-head { min-height: 0; }
.tool-out {
  margin: 7px 0 0; padding: 8px 11px; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); font-size: 12.5px; font-variant-numeric: tabular-nums;
}
/* There was a "Drag the box to move it, a corner to resize" hint here, shown
   only while cropping. A control that appears the moment you start using the
   thing above it pushes everything below it down at exactly the wrong moment —
   the panel jerked as the crop box went on. A hint that costs a layout shift is
   worth less than the shift costs, and dragging a crop box needs no caption. */


.tool-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  padding-top: 11px;
}
.tool-actions .btn { width: 100%; min-width: 0; padding: 11px 8px; font-size: 13px; gap: 6px; }
.tool-actions .btn-icon { width: 15px; height: 15px; }
/* The point of the whole page, so it gets its own full-width row and enough
   room that the label stays on one line. Size and warmth are what mark it as
   the primary action next to two neutral ghost buttons; the colour is the
   shared wash, up beside .btn-primary. */
.ed-use.btn-primary {
  width: 100%; min-width: 0; margin-top: 8px;
  padding: 15px 14px; font-size: 15.5px; white-space: nowrap;
}
.ed-use .btn-icon { width: 18px; height: 18px; }
.tool-note { color: var(--muted); font-size: 10.5px; margin: 8px 0 0; line-height: 1.4; }
/* Double-click is invisible unless it is said out loud somewhere. */
.tool-tip {
  flex: 0 0 auto;
  color: var(--muted); font-size: 10.5px; line-height: 1.35;
  margin: 7px 0 0; padding-top: 7px; border-top: 1px dashed var(--border);
}

/* Full screen for the editor, CSS only for the same reason the upscaler's is:
   the real Fullscreen API puts a "press Esc to exit" banner over the image
   that no page is allowed to suppress. */
body.is-ed-full { overflow: hidden; }
body.is-ed-full .nav,
body.is-ed-full .footer,
body.is-ed-full .ed-head,
body.is-ed-full .ed-panel { display: none !important; }
body.is-ed-full .page[data-page="edit"] { zoom: 1; }
body.is-ed-full .shell-edit { max-width: none; padding: 0; }
body.is-ed-full .editor {
  grid-template-columns: minmax(0, 1fr); grid-template-rows: 100vh;
  gap: 0;
}
/* No panel on screen, so no centre to match and nothing to give back. */
body.is-ed-full .editor { --ed-lift: 0px; }
body.is-ed-full .ed-stage { grid-row: 1; }
body.is-ed-full .ed-stage { height: 100vh; align-items: center; }  /* the picture is the only thing on screen, so centre it */
body.is-ed-full #ed-preview { max-width: 100vw; max-height: 100vh; border-radius: 0; }
body.is-ed-full .ed-overlay { top: 18px; right: 18px; }
body.is-ed-full .ed-obtn { width: 40px; height: 40px; }

/* Media queries measure unscaled pixels, so a breakpoint for this page has to
   be written ~7% larger than the CSS width it is meant to catch. */

/* Narrow the panel before giving up on the side-by-side layout — that layout
   is what keeps the page off the scrollbar, so it is worth holding on to. */
@media (max-width: 1300px) {
  .editor { --panel-w: 300px; }
  .ed-panel { padding: 4px 12px 12px; }
}

@media (max-width: 950px) {
  .editor {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    gap: 18px;
  }
  /* One column, so the pill is already page-centred and the offset would push
     it off. */
  .ed-head .eyebrow { transform: none; }
  .ed-panel {
    position: static; width: auto;
    grid-column: 1; grid-row: 3;
  }
  .ed-stage { grid-row: 2; }
  /* The page scrolls in this layout anyway, so the stage takes a share of the
     viewport rather than the measured leftover — measureEditor bails out at the
     same width for the same reason. The panel is under the stage here, not
     beside it, so there are no centre lines left to line up: the lift is
     cleared rather than left over from the last wide measurement. */
  .editor { --ed-lift: 0px; }
  .ed-stage { height: min(62vh, 560px); }
  #ed-preview { max-height: min(62vh, 560px); }
  .ed-panel { height: auto; padding: 4px 16px 12px; }
  .ed-panel-scroll { overflow-y: visible; }
}

/* ------------------------------ immersive ------------------------------- */
/* Nothing scrolls. Magnifying is done by the viewer's own zoom, not by growing
   the page, so there is nothing for a scrollbar to reach — and a page left
   scrolled sideways was what made the nav look half-missing on the way out. */
body.is-immersive { overflow: hidden; }
body.is-immersive .nav,
body.is-immersive .footer,
body.is-immersive .tasks,
body.is-immersive .headline,
body.is-immersive .controls,
body.is-immersive .stage-hint,
body.is-immersive .stage-name,
body.is-immersive .assurance,
body.is-immersive .notice { display: none !important; }

/* The page's own scale would shrink the viewport units, so it is unwound —
   on every page, not just the upscaler. `zoom` on an ancestor multiplies the
   coordinates of a `position: fixed` descendant, and the viewer below is
   fixed. */
body.is-immersive .page { zoom: 1; }
body.is-immersive .shell-wide { max-width: none; padding: 0; }
body.is-immersive .work-layout { grid-template-columns: minmax(0, 1fr); gap: 0; }
body.is-immersive .workspace {
  padding: 0; border: 0; border-radius: 0;
  background: #000; box-shadow: none;
}
body.is-immersive .workspace::before { display: none; }
/* Exactly the window, at any browser zoom, always — which is what makes both
   entering and leaving full screen a no-op for the rest of the page.
   This was briefly sized in frozen px, to stop `100vh` holding the picture at
   one physical size while the px-sized buttons grew with the browser's zoom.
   The viewer's own wheel zoom answers that properly now, and viewport units
   buy back the thing that mattered more: the page can never end up scrolled,
   so it can never come back from full screen looking cut in half. */
body.is-immersive .stage { height: 100vh; width: 100vw; }
/* The scaled frame is clipped rather than spilling over the page, and the
   transform origin is the top-left so the pan offsets computed in JS mean what
   they say. */
body.is-immersive .stage { overflow: hidden; }
/* Fixed to the viewport, not sized from the stage.
   Full screen has to be the window exactly, and every version of this that
   measured its way there through .stage -> .canvas -> .compare-shell could be
   thrown off by any one of them — a padding, a grid track, a page zoom, an
   ancestor that had not been reset yet. Out of flow and pinned to the four
   edges there is nothing left to get wrong, and `place-items: center` is the
   whole of the centring. */
body.is-immersive .compare-shell {
  position: fixed; inset: 0; z-index: 30;
  width: auto; height: auto; max-width: none;
  display: grid; place-items: center;
  overflow: hidden;
  background: #000;
}
/* Both dimensions written out, and the ratio switched off.
   `height: auto` + `aspect-ratio` is the obvious spelling and leans on the
   browser resolving a ratio against a clamp; `height: 100%` + `max-width: 100%`
   is worse still — with the height definite the clamp wins, the ratio loses,
   and a 2.4:1 panorama came out as a 1.78:1 box with the picture stretched into
   it. Two min()s need nothing resolved: the box is the largest one of the
   picture's shape that fits the window, by arithmetic. */
body.is-immersive .compare-frame {
  width: min(100vw, calc(100vh * var(--frame-aspect, 1.6)));
  height: min(100vh, calc(100vw / var(--frame-aspect, 1.6)));
  max-width: none; max-height: none;
  aspect-ratio: auto;
}
/* The picture moves under the slider; the slider does not move with it. */
.compare-layer { transform-origin: 0 0; }
.compare-frame.is-panning { cursor: grabbing; }
body.is-immersive .compare-frame {
  border-radius: 0;
  transform-origin: 0 0;
  will-change: transform;
}
/* Zoomed in, dragging the picture moves it; the handle still moves the split.
   At 1x there is nothing to pan, so the frame keeps its slider cursor. */
body.is-immersive .compare-frame.is-zoomed { cursor: ew-resize; }
body.is-immersive .compare-frame.is-panning { cursor: grabbing; }
body.is-immersive .compare-frame.is-zoomed .compare-divider { cursor: ew-resize; }
body.is-immersive .stage-full {
  position: fixed; top: 18px; right: 18px; width: 40px; height: 40px; z-index: 31;
}
/* Full screen owns the corner while it is open: the one thing wanted from that
   button up there is the way out, and clearing the result is not undoable. */
body.is-immersive .canvas-clear { display: none; }

/* -------------------------------- toast --------------------------------- */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%);
  z-index: 60; max-width: min(90vw, 460px);
  background: var(--surface-3); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 13px 18px; font-size: 14px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, .55);
}
.toast[data-tone="error"] { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); }

/* ------------------------------ responsive ------------------------------ */
/* Below this the tasks column has nowhere useful to sit, so it moves under the
   workspace and the page is allowed to scroll normally. */
@media (max-width: 1100px) {
  .work-layout { grid-template-columns: minmax(0, 1fr); }
  .tasks { position: static; max-height: 380px; }
  .shell-wide { padding: 24px 24px 40px; }
}

@media (max-width: 1080px) {
  /* Sharing a row costs more than it buys once the page is this narrow: a 9:16
     sample beside a 16:9 one would be about a thumb wide. One per row. */
  #demo-stack { gap: 64px; }
  .show-row { grid-template-columns: minmax(0, 1fr); gap: 56px; max-width: none; }
}

/* Two stages, because the nav runs out of room in two stages. First the
   strapline goes — it is the only part that is decoration rather than
   navigation. Then the links drop to a row of their own.

   1300, not the 1180 this started at. Two links have been added since (FAQ,
   then About) and each pushed the wrap point up. Measured with six links and
   the strapline showing, the row holds at 1280 and wraps at 1270, so anything
   lower leaves a band where the nav is TWO rows at 1270 and back to one at
   1240: the wrong way round, and it jumps visibly on the way past. 1300 is the
   measured figure plus a margin, so a slightly wider font metric on another
   machine cannot reopen the gap.

   ADDING A SEVENTH LINK MEANS MEASURING THIS AGAIN. It is the one number in
   the nav that a new tab silently invalidates. */
@media (max-width: 1300px) {
  .nav-subtitle, .nav-left .nav-sep { display: none; }
}

@media (max-width: 1100px) {
  .nav-inner {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "left right" "links links";
    row-gap: 10px;
  }
  .nav-left  { grid-area: left; }
  .nav-right { grid-area: right; gap: 10px; }
  .nav-links { grid-area: links; flex-wrap: wrap; gap: 4px; }
  .nav-links a { padding: 7px 14px; font-size: 13.5px; }
}

@media (max-width: 980px) {
  .shell { padding: 30px 16px 60px; }
  .workspace { padding: 22px 18px; border-radius: 20px; }
  .footer-inner { flex-direction: column; align-items: flex-start; gap: 14px; }
  .footer-links { position: static; transform: none; }
  .footer-copy { margin: 0; }
}

/* Below this the brand mark, the balance and the avatar are all that fit on
   one row. The wordmark and the word "Credits:" are the two things that can go
   without losing anything the icons do not already say. */
@media (max-width: 560px) {
  .nav-brand-text, .nav-credits-word { display: none; }
  /* The cards give up their generous padding before they give up fitting. */
  .tier { padding: 24px 16px 18px; }
  .tier-facts li { overflow-wrap: anywhere; }
  .tier-credit-pill { flex-wrap: wrap; }
  .nav-inner { padding: 10px 14px; gap: 10px; }
  .nav-credits { padding-left: 12px; padding-right: 12px; }
}

@media (max-width: 860px) {
  .nav-account-text { display: none; }
  /* Stack the controls so both the sizes and the action stay easy to tap. */
  .controls { flex-direction: column; align-items: stretch; gap: 18px; }
  .sizes { justify-content: center; }
  .size { min-width: 0; width: 100%; }
  .size-col { flex: 1; }
  .action, .btn { width: 100%; min-width: 0; }
  .dropzone-title { font-size: 17px; }
  .contact-inner { flex-direction: column; align-items: flex-start; }
}

/* Below ~430px the four size buttons and their credit costs are wider than the
   window, and the page scrolls sideways on the one screen that must not: the
   upscaler, on a phone. `.size-col` is a flex item, and a flex item's default
   `min-width: auto` refuses to shrink below its own content — the same failure
   as a bare `1fr` track, and the same fix. Letting it shrink is only half of
   it; the picker also has to have less to hold, or four columns of 19px type
   and 18px of side padding still will not fit.

   Measured overflow before: 8px at 375, 23px at 360, 63px at 320 — 360 and 375
   being the two commonest phone widths there are. After: none, down to 320.
   Everything here only ever makes the row SHORTER, so --controls-h reserves as
   much room as it always did and the canvas does not move. */
@media (max-width: 430px) {
  .size-col { min-width: 0; }
  .sizes { gap: 8px; }
  .size { padding: 12px 6px; font-size: 17px; }
  .size-cost { gap: 4px; font-size: 12.5px; }
  .size-cost .coin-sm { width: 13px; height: 13px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* The Before/After chips that were sized here are gone. They were tuned twice —
   matched to the credit pill, then made bigger because the page's 1.07 zoom and
   a loud photograph behind them made that read small — and the second pass is
   what showed they were the wrong idea: a badge that has to grow to be noticed,
   over a photograph, to name something the divider under it already says. */


/* --------------------------- language picker ---------------------------- */
/* Bottom-right of the WINDOW, not of the page. Three of the four pages scroll,
   and the one thing this control must be is findable without hunting for it —
   which is also why it is the same object in the same corner on every page
   rather than a row in the footer that only Pricing scrolls far enough to see.

   It sits outside .page on purpose, next to the nav and the footer, so it never
   picks up a page's zoom or its accent. A language switcher is chrome.

   **It sits BELOW the footer's rule, inside the footer band.** The footer is
   13px of padding around a 12.5px line, so its inner strip is about 44px tall
   and its top border is the first line the eye meets coming down the page. A
   taller pill at `bottom: 16px` crossed that border, which read as a control
   floating loose over the page's last edge rather than as part of the strip it
   was sitting in. Everything below — the smaller face, the tighter padding —
   follows from having to fit under that line with room to spare. */
.lang {
  position: fixed; right: 14px; bottom: 9px; z-index: 60;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
/* Both the upscaler's immersive view and the editor's own full screen exist to
   put nothing but the photograph on the screen. */
body.is-immersive .lang,
body.is-ed-full .lang { display: none; }

.lang-button {
  display: inline-flex; align-items: center; gap: 6px;
  /* 26px tall against the footer strip's 44: clear of the rule above it and
     level with the legal links it sits beside. */
  padding: 4px 9px; border-radius: 999px;
  font-family: var(--font-display); font-size: 12px; font-weight: 600;
  line-height: 1.5;
  color: var(--muted);
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  border: 1px solid var(--border);
  backdrop-filter: blur(8px);
  transition: border-color .15s var(--ease), background .15s var(--ease),
              color .15s var(--ease);
}
.lang-button:hover {
  border-color: var(--accent); background: var(--surface);
  color: var(--text-on-surface-strong);
}
.lang-button svg {
  width: 14px; height: 14px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.lang-chev { width: 11px !important; height: 11px !important; opacity: .7; }
.lang-button[aria-expanded="true"] .lang-chev { transform: rotate(180deg); }

/* Opens UPWARD, because the button is already at the bottom of the window and
   eleven rows below it would be off the screen. No max-height and no scroll:
   the list is sized to fit, and a language menu that hides languages behind a
   scroll bar is the one failure it cannot have. That is what fixes the count
   at eleven — see LANGUAGES in i18n.js. */
.lang-menu {
  display: grid; gap: 1px;
  min-width: 196px; padding: 5px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 13px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .5);
}
/* An author display rule outranks the UA's [hidden] { display: none }. */
.lang-menu[hidden] { display: none; }
/* Real links, not buttons: each carries the URL the server would answer, so a
   crawler can follow them and a visitor can open one in a new tab. */
.lang-menu a {
  display: flex; align-items: baseline; gap: 8px;
  text-decoration: none;
  width: 100%; padding: 6px 9px; border-radius: 8px;
  text-align: left; color: var(--text-on-surface); font-size: 12.5px;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.lang-menu a:hover {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text-on-surface-strong);
}
/* The language in force is marked by the accent and the weight, not a tick:
   a tick column would have to be reserved on all eleven rows for one of them. */
.lang-menu a[aria-checked="true"] {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--link);
}
/* The native name is what a reader scanning this list is looking for, so it
   leads; the English name is for anyone who cannot read the script yet. */
.lang-native { font-weight: 600; }
.lang-english { margin-left: auto; font-size: 11px; opacity: .6; }

/* Below 560px the footer links wrap onto two rows, so the strip grows and the
   corner is no longer a quiet place to sit. The control keeps its distance and
   narrows; it does not shrink further, because it still has to be tappable. */
@media (max-width: 560px) {
  .lang { right: 10px; bottom: 8px; }
  .lang-button { padding: 5px 9px; }
  .lang-menu { min-width: 168px; }
}
