/* Meydn Web (1A) — visual identity from web/index.html + web/legal/legal.css */

:root {
  --bg: #0c0c0a;
  --bg-elevated: #131210;
  --ink: #f3ede2;
  --ink-dim: #a8a094;
  --ink-quiet: #6b665c;
  --accent: #c9a66b;
  --rule: rgba(243, 237, 226, 0.08);
  --rule-strong: rgba(243, 237, 226, 0.18);
  --err: #ff8080;
  --warn: #f4c150;

  --serif: 'Instrument Serif', 'Times New Roman', serif;
  --sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --mono: 'JetBrains Mono', 'Courier New', monospace;

  /* Motion language — must match EASE/DUR in motion.js exactly. */
  --ease-settle: cubic-bezier(0.2, 0, 0.2, 1);   /* arriving / resolving */
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);       /* leaving */
  --dur-1: 120ms;   /* micro: hovers, toggles, repeated actions */
  --dur-2: 220ms;   /* standard: most transitions */
  --dur-3: 400ms;   /* deliberate: entrances, seam dissolves */
  --dur-4: 640ms;   /* cinematic: signatures only */

  --glass-bg: rgba(19, 18, 16, 0.72);                 /* ~--bg-elevated, translucent */
  --glass-blur: 12px;                                 /* solid glass chrome (tool bar, pills) */
  --glass-blur-soft: 8px;                             /* gradient-fade headers (landing + workspace) */
  --glass-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);

  /* Height of the camera-roll band (thumb 66 + scroller padding 24). ONE source,
     because two things depend on it: the strip reserves it so an empty scroller
     doesn't jump when the first tile lands, and the low-credit nudge lifts clear
     of it. Those were independent numbers until MEY-93, which is how the nudge
     ended up lying across five of six thumbnails. */
  --roll-strip-h: 90px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }

/* [hidden] MUST WIN. One rule, so nobody has to remember the next one (MEY-42).
   Components here are styled with author `display` (flex and inline-flex almost
   everywhere), and an author declaration beats the UA stylesheet's
   `[hidden] { display: none }` on cascade origin. So every element toggled by
   the hidden attribute needed its own re-override, and the codebase had grown
   FIVE of them. That convention is empirically unsafe: the fourth one was
   missed and shipped to production permanently visible-but-dead — the account
   popover that never rendered and left Log out unreachable (MEY-40) — and it
   survived every test gate.

   The :not() guard preserves `hidden="until-found"`, which lets find-in-page
   reveal collapsed content. Nothing here uses it today; the guard costs one
   selector and keeps the rule from being wrong later.

   The trade-off, checked rather than assumed: `!important` blocks
   animate-while-hiding. Nothing does that. The only transitions on any
   hidden-toggled element are `color` and `border-color` hover effects on
   .free-copy-link / .signup-nudge, which never run during hide. A future
   hide/show animation wants a class, not this attribute. */
[hidden]:not([hidden="until-found"]) { display: none !important; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: inherit; text-decoration: none; }

/* film grain (marketing-page surfaces only — toggled by body class) */
body.grain::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 100;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}

/* — typography vocabulary, matching web/index.html — */

h1, h2, h3 {
  font-family: var(--serif);
  font-weight: 400;
  letter-spacing: -0.015em;
}

.logo {
  font-family: var(--serif);
  font-style: italic;
  font-size: 22px;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.eyebrow-rule {
  display: flex;
  align-items: center;
  gap: 12px;
}
.eyebrow-rule::before {
  content: '';
  width: 32px;
  height: 1px;
  background: var(--ink-dim);
}

.muted { color: var(--ink-dim); }
.quiet { color: var(--ink-quiet); }
.small { font-size: 13px; }

/* — pill button — matches nav-cta on the marketing site — */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  padding: 10px 18px;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  cursor: pointer;
  transition: all var(--dur-2) var(--ease-settle);
  font-weight: 400;
}
.pill:hover {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}
.pill:disabled {
  opacity: 0.5;
  cursor: default;
}
.pill:disabled:hover {
  background: transparent;
  color: var(--ink);
  border-color: var(--rule-strong);
}
.pill.pill-accent {
  border-color: var(--accent);
  color: var(--accent);
}
.pill.pill-accent:hover {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}
.pill-ghost {
  background: transparent;
  color: var(--ink-dim);
  border: none;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  padding: 6px 0;
  transition: color var(--dur-2) var(--ease-settle);
}
.pill-ghost:hover { color: var(--ink); }

/* PWA install affordance on the landing page (revealed by install.js only when
   the browser reports the app is installable). Icon + label, restrained. */
.install-pill { display: inline-flex; align-items: center; gap: 7px; }
.install-pill svg { opacity: 0.85; }
.install-ios {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-quiet); margin: 4px 0 0;
}
.install-ios strong { color: var(--ink-dim); font-weight: 400; }

/* Header action pills: glass, icon + label. */
.header-actions .pill {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--rule-strong);
  box-shadow: var(--glass-shadow);
}
.header-actions .pill .pill-icon { display: inline-flex; width: 15px; height: 15px; }
.header-actions .pill .pill-icon svg { width: 100%; height: 100%; }
.header-actions .pill.pill-accent { border-color: var(--accent); color: var(--accent); }
.header-actions .pill:hover { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.header-actions .pill.pill-accent:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }

/* 1C — credit cost suffix on the paid Export / Export-all pills. A dim mono tag
   appended after the label announcing the cost ("(1◆)" / "(13◆)"); suppressed
   for Pro (no per-export cost) via the .is-pro flag app.js toggles in
   refreshAccountUI. */
.cost-suffix {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  opacity: 0.7;
  margin-left: 6px;
}
#export-btn.is-pro .cost-suffix,
#export-all-btn.is-pro .cost-suffix { display: none; }

/* Free escape + anon nudge: small, de-emphasized text affordances beside the
   accent Export pills. free-copy-link = a no-cost watermarked download for a
   logged-in credit-holder; signup-nudge invites an anonymous user to full-res. */
.free-copy-link, .signup-nudge {
  align-self: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-dim);
  /* Ghost-pill shape so it reads as a de-emphasized sibling of the accent pills,
     not floating text. A faint rule border (dimmer than the accent pills) gives
     it a real pill outline; it firms up on hover. */
  padding: 9px 14px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  transition: color var(--dur-1) var(--ease-settle), border-color var(--dur-1) var(--ease-settle);
}
.free-copy-link:hover { color: var(--ink); border-color: var(--rule-strong); }
.signup-nudge { color: var(--accent); }
.signup-nudge:hover { border-color: var(--accent); }

/* — empty state — */

/* MEY-41: the landing header's right-hand group (back-link + account avatar).
   .empty-header is space-between with two children; the account slot joins the
   pill in this group rather than becoming a third child, which would have
   spread the two to opposite ends. Gap, not margins, so an empty slot (the
   anonymous case) collapses to nothing and the pill sits exactly where it
   always did. */
.empty-header-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}
.empty-header-actions .account-slot:empty { display: none; }
/* Three header actions (back, Pricing, account) at 320px wrapped "back to
   meydn.com" onto two lines against the logo. The short form keeps one line. */
@media (max-width: 400px) {
  .back-to-site .back-word { display: none; }
}

.empty-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 10;
  padding: 24px 40px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: linear-gradient(to bottom, rgba(12,12,10,0.85) 0%, rgba(12,12,10,0) 100%);
  backdrop-filter: blur(var(--glass-blur-soft));
  -webkit-backdrop-filter: blur(var(--glass-blur-soft));
}

.empty-state {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  padding: 120px 40px 80px;
}

.empty-eyebrow {
  margin-bottom: -8px;
}

.empty-headline {
  font-family: var(--serif);
  font-size: clamp(40px, 6vw, 84px);
  line-height: 1.0;
  letter-spacing: -0.025em;
  font-weight: 400;
  color: var(--ink);
  text-align: center;
  max-width: 18ch;
}
.empty-headline em {
  font-style: italic;
}

.empty-sub {
  font-family: var(--sans);
  font-size: clamp(15px, 1.3vw, 17px);
  color: var(--ink-dim);
  text-align: center;
  max-width: 52ch;
  margin-top: -16px;
}

/* Staged entrance: the landing composes itself top-down with the base settle
   curve. Targets the five meaningful children individually so #status (empty)
   never animates. Reduced-motion strips the transform via the global pass. */
@keyframes meydn-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.empty-eyebrow,
.empty-headline,
.empty-sub,
#upload-form,
#sample-form {
  animation: meydn-rise var(--dur-3) var(--ease-settle) both;
}
.empty-eyebrow { animation-delay: 0ms; }
.empty-headline { animation-delay: 60ms; }
.empty-sub { animation-delay: 120ms; }
#upload-form { animation-delay: 180ms; }
#sample-form { animation-delay: 240ms; }

.drop-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 520px;
  max-width: 90vw;
  height: 220px;
  border: 1px dashed var(--rule-strong);
  background: rgba(243, 237, 226, 0.02);
  border-radius: 4px;
  cursor: pointer;
  transition: border-color var(--dur-2) var(--ease-settle), background var(--dur-2) var(--ease-settle);
  position: relative;
}
.drop-zone::before {
  content: '';
  position: absolute;
  inset: 8px;
  border: 1px solid transparent;
  border-radius: 2px;
  pointer-events: none;
  transition: border-color var(--dur-2) var(--ease-settle);
}
.drop-zone:hover, .drop-zone.drag-over {
  border-color: var(--accent);
  background: rgba(201, 166, 107, 0.05);
}
.drop-zone:hover::before, .drop-zone.drag-over::before {
  border-color: rgba(201, 166, 107, 0.2);
}
/* Keyboard focus ring — an outline (not the ::before inset) so it survives the
   coarse-pointer block that hides ::before. Applies on all pointer types. */
.drop-zone:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.drop-zone-main {
  font-family: var(--serif);
  font-size: 22px;
  font-style: italic;
  color: var(--ink);
}
.drop-zone-sub {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}
/* Pointer-vs-touch home copy: drag-and-drop is meaningless on touch, so a
   coarse pointer swaps to tap/library wording (toggled in the coarse block). */
.for-touch { display: none; }

/* — workspace — */

/* Floating glass header over the top of the image (gradient fade, like the landing). */
.workspace-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  /* Above #preparing (z-7) so the logo/brand stays visible over the opaque
     "Initializing Meydn" loader (which sits above the tool bar at z-6). */
  z-index: 8;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* padding tuned so the .logo lands at the SAME (x=40, y=24) as the landing
     .empty-header — the taller action pills nudge the centered logo up ~1.5px,
     so 22.5px top (vs the landing's 24px) cancels it and the logo doesn't jump
     across the landing→workspace transition. */
  padding: 22.5px 40px;
}
/* The glass decoration (gradient + feathered blur) lives on a pseudo-layer, NOT
   on the header itself: mask-image is a group effect, so on .workspace-header it
   erased the whole subtree below the fade — paint AND hit-testing — including
   the account popover (MEY-40). On ::before it masks only this decoration;
   children escape. z-index:-1 paints it under the header content and relies on
   the header's own stacking context (position+z-index above — keep them). */
.workspace-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to bottom, rgba(12,12,10,0.82) 0%, rgba(12,12,10,0) 100%);
  -webkit-backdrop-filter: blur(var(--glass-blur-soft));
  backdrop-filter: blur(var(--glass-blur-soft));
  -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
}
/* The floating header spans the top crop edge; without this it swallowed touches
   there so crop-edge drag / hold-to-compare never reached the canvas (dead on
   mobile). Make the bar transparent to pointers and re-enable only its controls. */
.workspace-header { pointer-events: none; }
.workspace-header > * { pointer-events: auto; }

/* Workspace page fills exactly the viewport: header + workspace + roll stack in
   a flex column so the roll sits at the bottom instead of being pushed below the
   fold. The workspace (flex:1, min-height:0) shrinks to make room for the roll;
   min-height:0 is required or the grid child refuses to shrink below its content. */
body.workspace-page { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }

/* Edge-to-edge: the canvas fills the whole workspace; header + tool bar float over it. */
.workspace {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}
.workspace > .canvas-wrap { position: absolute; inset: 0; }

.canvas-wrap {
  background: var(--bg);
  position: relative;
  overflow: hidden;
}
#preview-canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  cursor: grab;
  /* Touch: own all gestures (crop-edge drag, hold-to-compare) — don't let the
     browser scroll, pan, or double-tap-zoom the page out from under them. */
  touch-action: none;
  /* No CSS transition — the JS compare-animation drives the transform
     directly per frame via applyViewportZoom, lerped by compareStrength.
     A CSS transition here would fight that loop and produce the
     "collapse to the side and back" stutter. */
}
#preview-canvas:active { cursor: grabbing; }

/* Reveal overlay — vanishing-point construction + corner readouts shown
   on first load while the raw image is on screen. Positioned absolute
   like the canvas; their CSS transform is set imperatively in lockstep
   with #preview-canvas's transform so the overlay tracks the viewport
   zoom. Default opacity 0 so they're invisible until the reveal
   sequence fades them in. */
#reveal-overlay {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease-settle);
}
#reveal-overlay .reveal-ray {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.25;
  /* Non-scaling-stroke keeps lines at 1.25 CSS pixels regardless of the
     canvas's CSS transform, so they don't get thick when the viewport
     zooms in. */
  vector-effect: non-scaling-stroke;
  opacity: 0.7;
}
#reveal-overlay .reveal-vp {
  fill: var(--accent);
  opacity: 0.85;
}
#reveal-readouts {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease-settle);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.reveal-readout {
  position: absolute;
  padding: 6px 10px;
  white-space: pre;
}
.reveal-readout.reveal-tl { top: 16px; left: 20px; }
.reveal-readout.reveal-tr { top: 16px; right: 20px; text-align: right; }
.reveal-readout.reveal-bl { bottom: 16px; left: 20px; }
.reveal-readout.reveal-br { bottom: 16px; right: 20px; text-align: right; }

.reveal-on #reveal-overlay,
.reveal-on #reveal-readouts { opacity: 1; }

/* Lines-only mode used by the click-hold compare gesture: rays show,
   readouts do not — the readouts are a "look at this measurement"
   beat that belongs only to the initial reveal. */
.reveal-lines #reveal-overlay { opacity: 1; }

/* Advanced controls parked for v1 (spec 2026-06-01). Markup + JS wiring kept
   intact so this is a one-line revert. */
#advanced { display: none; }
/* Diagnostics + reset are dev-facing; hidden from the product. Kept in the DOM
   so app.js writes to the meta readouts and the reset button never throw. */
#detection-meta { display: none; }
#reset-btn { display: none; }

.callout {
  background: rgba(244, 193, 80, 0.06);
  border: 1px solid rgba(244, 193, 80, 0.28);
  padding: 12px 14px;
  margin-bottom: 18px;
  border-radius: 3px;
  color: var(--warn);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.45;
}

.banner {
  background: rgba(201, 166, 107, 0.06);
  border: 1px solid var(--rule);
  padding: 10px 14px;
  margin-bottom: 16px;
  border-radius: 3px;
  color: var(--ink-dim);
  font-family: var(--sans);
  font-size: 13px;
}
.banner.err {
  background: rgba(255, 128, 128, 0.06);
  border-color: rgba(255, 128, 128, 0.3);
  color: var(--err);
}

#status:not(:empty) {
  position: fixed;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;
  /* Informational only — never swallow taps for the UI beneath it (on mobile the
     banner sits near the camera roll; without this an error banner blocked
     tapping a thumbnail to switch photos). */
  pointer-events: none;
}
#status .banner {
  background: var(--bg-elevated);
  border: 1px solid var(--rule-strong);
  padding: 14px 22px;
  margin: 0;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* — unsupported / not-supported page — */

.unsupported {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  text-align: center;
  padding: 120px 40px 80px;
}
.unsupported h1 {
  font-family: var(--serif);
  font-size: clamp(32px, 5vw, 56px);
  font-weight: 400;
  letter-spacing: -0.02em;
  max-width: 18ch;
  /* Evens the two lines instead of orphaning the last word. Unsupported
     browsers just wrap normally, which is what this page did before — and on
     this page in particular the visitor's browser is by definition an old or
     limited one, so the degradation path is the one that matters. */
  text-wrap: balance;
}
.unsupported p {
  font-family: var(--sans);
  font-size: 15px;
  color: var(--ink-dim);
  max-width: 50ch;
}

/* MEY-64: the unsupported page's actions row. Try again is the primary action
   on every platform; the App Store link only renders on iOS, so on Android and
   desktop this row is a single centred button and the gap collapses. */
.unsupported-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}
#retry-note { max-width: 46ch; }

.cta-app-store {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: var(--ink);
  color: var(--bg);
  padding: 16px 32px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  transition: all var(--dur-3) var(--ease-settle);
  border: 1px solid var(--ink);
}
.cta-app-store:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

/* Model-initializing loader. Fills the canvas (opaque --bg) so the image is
   hidden until the model is ready, then fades out as the image fades in.
   Shown/hidden by toggling .is-on (opacity), not display, so it can cross-fade. */
#preparing {
  position: absolute; inset: 0; z-index: 7; display: flex;
  flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  background: var(--bg);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-3) var(--ease-settle);
}
#preparing.is-on { opacity: 1; }
.preparing-label { font-family: var(--serif); font-size: 22px; color: var(--ink); }
.preparing-track {
  width: 240px; height: 3px; background: var(--rule);
  border-radius: 2px; overflow: hidden;
}
.preparing-fill { height: 100%; width: 0%; background: var(--accent); transition: width .15s linear; }
.preparing-pct { font-family: var(--mono); font-size: 12px; color: var(--ink-quiet); min-height: 1em; }
/* "23.1 of 55.6 MB, first visit only". Reserves its line so the overlay does
   not jump when the first progress event lands, and stays narrow enough that
   a phone wraps it at most once. */
.preparing-detail {
  font-family: var(--mono); font-size: 11px; color: var(--ink-quiet);
  min-height: 1em; max-width: min(78vw, 300px); text-align: center;
  text-wrap: balance;
}
/* Cached model: no real byte progress, so slide an indeterminate sliver. */
#preparing.indeterminate .preparing-fill {
  width: 38%; transition: none;
  animation: meydn-indet 1.1s ease-in-out infinite;
}
@keyframes meydn-indet {
  0%   { margin-left: -38%; }
  100% { margin-left: 100%; }
}

/* Analyzing progress bar — over the faded-in image (metaball shimmer behind).
   The fill width is driven per-frame from JS (fast→slow easing), so no width
   transition here. Shown/hidden via .is-on opacity. */
#analyzing {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 4; display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 34px 54px;
  /* Soft scrim so the indicator stays legible centered over any image content
     (the metaball already dims the photo; this is insurance over bright skies). */
  background: radial-gradient(62% 62% at 50% 50%, rgba(12, 12, 10, 0.55), rgba(12, 12, 10, 0) 72%);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-3) var(--ease-settle);
}
#analyzing.is-on { opacity: 1; }
.analyzing-label {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.7);
}
.analyzing-track {
  width: 240px; height: 3px; background: rgba(0, 0, 0, 0.4);
  border-radius: 2px; overflow: hidden; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
}
.analyzing-fill { height: 100%; width: 0%; background: var(--accent); }

/* — camera roll — */
.roll { border-top: 1px solid var(--rule-strong); background: #0a0a08; flex: 0 0 auto; }
/* Reserve the populated height (thumb 66 + scroller padding 24) so an empty
   scroller — e.g. the server-rendered first paint before JS adds tiles — doesn't
   collapse to the +-panel height and then jump when the first tile appears. */
.roll-body { display: flex; align-items: stretch; min-height: var(--roll-strip-h); }
.roll-scroller { display: flex; align-items: center; gap: 9px; overflow-x: auto;
  padding: 12px 14px; flex: 1; scroll-behavior: smooth; scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent; }
.roll-scroller::-webkit-scrollbar { height: 8px; }
.roll-scroller::-webkit-scrollbar-thumb { background: var(--rule-strong); border-radius: 4px; }
.roll-thumb { position: relative; flex: 0 0 auto; width: 88px; height: 66px;
  border-radius: 6px; overflow: hidden; cursor: pointer; background: var(--bg-elevated);
  box-shadow: inset 0 0 0 1px var(--rule);
  transition: transform var(--dur-1) var(--ease-settle), filter var(--dur-1) var(--ease-settle); }
.roll-thumb:hover { transform: scale(1.03); filter: brightness(1.07); z-index: 1; }
/* Absolutely positioned so the corrected image can cross-fade in OVER the raw
   preview before the raw one is removed. */
.roll-thumb img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; }
/* Raw preview (shown while detecting): a plain fade-in, dimmed + desaturated to
   echo the fullscreen "thinking" look. */
.roll-thumb img[data-kind="raw"] { animation: roll-fade-in var(--dur-2) var(--ease-settle) both; }
.roll-thumb.detecting img { filter: brightness(0.5) saturate(0.85); }
@keyframes roll-fade-in { from { opacity: 0; } to { opacity: 1; } }
/* SIGNATURE: the corrected thumb arrives slightly off-true and settles to
   plumb — the product's whole job, re-enacted on every save. */
.roll-thumb img[data-kind="corrected"] { animation: roll-settle-plumb var(--dur-3) var(--ease-settle) both; }
@keyframes roll-settle-plumb {
  from { opacity: 0; transform: rotate(1.2deg) scale(1.05); }
  to   { opacity: 1; transform: rotate(0deg) scale(1); }
}
.roll-thumb.active { outline: 2px solid var(--accent); outline-offset: 2px;
  animation: roll-select var(--dur-2) var(--ease-settle); }
@keyframes roll-select { from { outline-offset: 6px; } to { outline-offset: 2px; } }
/* "Saved to the roll" pulse: a one-shot gold ring as the corrected thumb lands. */
.roll-thumb.just-saved { animation: roll-saved 900ms ease-out; }
@keyframes roll-saved {
  0%   { box-shadow: inset 0 0 0 1px var(--rule),   0 0 0 0   rgba(201,166,107,0); }
  35%  { box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px rgba(201,166,107,.5); }
  100% { box-shadow: inset 0 0 0 1px var(--rule),   0 0 0 0   rgba(201,166,107,0); }
}
/* Detecting shimmer: a few drifting, blurred gold blobs that glow over the
   dimmed raw photo — a CSS evocation of the fullscreen metaball "thinking" flow. */
.roll-thumb.detecting::after { content: ""; position: absolute; inset: -20%; pointer-events: none;
  background:
    radial-gradient(40% 50% at 30% 40%, rgba(201,166,107,.50), transparent 70%),
    radial-gradient(45% 55% at 68% 58%, rgba(201,166,107,.42), transparent 72%),
    radial-gradient(34% 44% at 50% 78%, rgba(201,166,107,.34), transparent 70%);
  mix-blend-mode: screen; filter: blur(5px);
  animation: roll-shimmer 2.2s ease-in-out infinite; }
@keyframes roll-shimmer {
  0%   { opacity: .45; transform: translate(-3px,  1px) scale(1.00); }
  50%  { opacity: .95; transform: translate( 3px, -1px) scale(1.08); }
  100% { opacity: .45; transform: translate(-3px,  1px) scale(1.00); }
}
.roll-thumb.error { outline: 1.5px solid var(--err); }
.roll-thumb.error::after { content: "!"; position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center; color: var(--err);
  font-family: var(--mono); font-size: 18px; }
/* Trash — one delete language. Per-tile: appears (animated) on tile hover; click
   removes that photo. Delete-all: the strip trash previews it on EVERY tile. */
.roll-remove { position: absolute; top: 3px; right: 3px; width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center; padding: 0;
  border-radius: 50%; border: none; background: rgba(0,0,0,.55); color: #fff;
  cursor: pointer; opacity: 0; transition: opacity var(--dur-1) var(--ease-settle); }
.roll-remove svg { width: 13px; height: 13px; }
.roll-remove:hover { background: var(--err); }
.roll-thumb:hover .roll-remove,
.roll-scroller.previewing-delete-all .roll-remove { opacity: 1; animation: trash-pop var(--dur-3) var(--ease-settle); }
@keyframes trash-pop {
  0%   { opacity: 0; transform: scale(.4) rotate(-9deg); }
  55%  { opacity: 1; transform: scale(1.15) rotate(7deg); }
  78%  { transform: scale(.95) rotate(-4deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}
/* Delete-all preview: every tile reads as marked for deletion. */
.roll-scroller.previewing-delete-all .roll-thumb { box-shadow: inset 0 0 0 1.5px var(--err); }
.roll-scroller.previewing-delete-all .roll-thumb img { filter: brightness(.45) saturate(.7); }
.roll-add { flex: 0 0 auto; align-self: center; width: 88px; height: 66px; margin: 0 14px;
  border-radius: 6px; border: 1.5px dashed var(--rule-strong); display: flex;
  flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  cursor: pointer; color: var(--ink-quiet); border-left: 1px solid var(--rule);
  transition: border-color var(--dur-2) var(--ease-settle), color var(--dur-2) var(--ease-settle), background var(--dur-2) var(--ease-settle); }
.roll-add:hover { border-color: var(--accent); color: var(--accent);
  background: rgba(201,166,107,.05); }
.roll-plus { font-size: 26px; font-weight: 200; line-height: 1; }
.roll-add-t { font-family: var(--mono); font-size: 8px; letter-spacing: .08em; }
/* Drag-to-add preview: page-wide, the roll lights up as the drop target and
   shows one ghost tile per dragged file (count known mid-drag; folder = 1). */
body.dragging-files .roll { background: rgba(201,166,107,.05); box-shadow: inset 0 1px 0 0 var(--accent); }
body.dragging-files .roll-add { border-color: var(--accent); color: var(--accent); background: rgba(201,166,107,.05); }
/* The canvas is also a drop target — advertise it during a file drag. */
.canvas-wrap { transition: box-shadow var(--dur-2) var(--ease-settle); }
body.dragging-files .canvas-wrap { box-shadow: inset 0 0 0 2px rgba(201, 166, 107, 0.35); }

/* Floating vertical glass tool bar — crop segment on top, toggles below. */
.canvas-tools {
  position: absolute;
  top: 50%; right: 16px;
  transform: translateY(-50%);
  z-index: 6;
  display: flex; flex-direction: column; align-items: stretch; gap: 6px;
  padding: 6px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--rule); border-radius: 12px;
  box-shadow: var(--glass-shadow);
  /* The tool column overlaps the RIGHT crop edge; like the header it must not
     swallow touches meant for the canvas (only its buttons stay interactive). */
  pointer-events: none;
}
.ct-btn { pointer-events: auto; }
.ct-crop { display: flex; flex-direction: column; gap: 2px; }
.ct-divider { height: 1px; background: var(--rule); margin: 4px 4px; }
.ct-btn {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0;
  background: transparent; border: none; border-radius: 8px;
  color: var(--ink-quiet); cursor: pointer;
  transition: color var(--dur-1) var(--ease-settle),
              background var(--dur-1) var(--ease-settle),
              transform var(--dur-1) var(--ease-settle);
}
.ct-btn svg { width: 20px; height: 20px; }
.ct-btn:hover { color: var(--ink); background: rgba(243,237,226,0.06); }
.ct-btn:active { transform: scale(0.94); }
.ct-btn.active { color: var(--accent); background: rgba(201,166,107,0.12); }
/* Tooltip to the LEFT so it doesn't run off the right edge. */
.ct-btn::after {
  content: attr(aria-label);
  position: absolute; right: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; text-transform: lowercase;
  color: var(--ink); background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--rule); border-radius: 6px; padding: 5px 8px;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-1) var(--ease-settle);
}
.ct-btn:hover::after { opacity: 1; }

/* Warnings float at the top-left of the canvas, below the header. */
#callouts { position: absolute; top: 76px; left: 24px; z-index: 4; max-width: 320px; }
#callouts:empty { display: none; }
.roll-ghost { flex: 0 0 auto; width: 88px; height: 66px; border-radius: 6px;
  border: 1.5px dashed var(--accent); background: rgba(201,166,107,.06);
  animation: roll-ghost-in var(--dur-2) var(--ease-settle) both; }
@keyframes roll-ghost-in { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
.header-actions { display: flex; gap: 10px; align-items: center; }
/* Success flash on an action pill — the ✓ settles in via the accent fill. */
.pill.pill-confirmed { background: var(--accent); color: var(--bg); border-color: var(--accent);
  transition: background var(--dur-2) var(--ease-settle), color var(--dur-2) var(--ease-settle),
              border-color var(--dur-2) var(--ease-settle); }
.pill[disabled] { opacity: .5; cursor: default; }

/* Respect prefers-reduced-motion: collapse decorative animations/transitions to
   instant (keyframes jump to their settled end-state, so no transform motion),
   and stop smooth scrolling. JS-driven opacity fades are gentle and kept. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .roll-scroller { scroll-behavior: auto; }
  /* Strip the one decorative hover transform (snapping it instant still leaves
     the scale applied; reduced-motion wants no transform at all). */
  .roll-thumb:hover { transform: none; filter: none; }
}

/* ── Wall 1 account-gate modal (1B) ──────────────────────────────────── */
.wall1-backdrop {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background: rgba(12, 12, 10, 0.66);
  -webkit-backdrop-filter: blur(var(--glass-blur-soft));
  backdrop-filter: blur(var(--glass-blur-soft));
  animation: wall1-fade var(--dur-2) var(--ease-settle);
}
.wall1-card {
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur));
  backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--rule-strong);
  border-radius: 14px;
  box-shadow: var(--glass-shadow);
  padding: 32px 34px;
  width: min(92vw, 420px);
  display: flex; flex-direction: column; gap: 14px; text-align: center;
  animation: wall1-rise var(--dur-3) var(--ease-settle) both;
}
.wall1-title { font-family: var(--serif); font-size: 28px; margin: 0; color: var(--ink); }
.wall1-sub { margin: 0; color: var(--ink-dim); font-size: 14px; line-height: 1.5; text-wrap: balance; }
.wall1-card .wall1-cta { margin-top: 4px; justify-content: center; }   /* center the CTA label */
.wall1-card .wall1-dismiss { align-self: center; }
@keyframes wall1-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes wall1-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* (prefers-reduced-motion handled by the global rule above — collapses to instant.) */

/* ── Wall 2 out-of-credits modal (1C) ────────────────────────────────── */
/* Reuses the wall1 glass shell (.wall1-backdrop / .wall1-card) verbatim; these
   aliases add the Wall-2-specific two-card layout: Top up (credit pack) and
   Go Pro (unlimited, one-time) as co-equal options. */
.wall2-options { display: flex; gap: 12px; margin: 4px 0 2px; }
.wall2-option {
  flex: 1 1 0; display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  background: var(--glass-bg); border: 1px solid var(--rule-strong); border-radius: 8px;
  padding: 16px; cursor: pointer; text-align: left;
  transition: border-color var(--dur-2) var(--ease-settle), background var(--dur-2) var(--ease-settle);
}
.wall2-option:hover { border-color: var(--accent); }
.wall2-option.wall2-pro { border-color: var(--accent); }
.wall2-option-title { font-family: var(--serif); font-size: 19px; color: var(--ink); }
.wall2-option-sub { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
.wall2-card .wall2-dismiss { align-self: center; margin-top: 4px; }
/* Side by side, two cards at phone width squeeze their price labels onto three
   lines. Stacked, each reads as one line and is a full-width tap target. */
@media (max-width: 480px) {
  .wall2-options { flex-direction: column; }
}

/* Account: a circular icon button top-right (the seam for the 1C circular credits
   gauge — accent ring when signed in). Logged-in -> popover with email + Log out. */
.account-slot { position: relative; display: inline-flex; align-items: center; }
/* Account is a settings affordance, set apart from the export/download TOOLS by a
   divider — only when it actually renders (auth on), so auth-off shows no orphan. */
.account-slot:not(:empty)::before {
  content: ""; width: 1px; height: 20px; background: var(--rule-strong);
  margin: 0 16px 0 6px; align-self: center;
}
.account-btn {
  position: relative;   /* anchors the absolutely-positioned .credits-gauge SVG */
  width: 34px; height: 34px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--glass-bg); border: 1px solid var(--rule-strong); color: var(--ink-dim);
  cursor: pointer;
  -webkit-backdrop-filter: blur(var(--glass-blur-soft)); backdrop-filter: blur(var(--glass-blur-soft));
  transition: color var(--dur-1) var(--ease-settle), border-color var(--dur-1) var(--ease-settle);
}
.account-btn:hover { color: var(--ink); border-color: var(--ink-quiet); }
/* Icon sizing only — must NOT catch .credits-gauge (class+type specificity
   would beat its own 34px sizing and pin an 18px ring to the corner). */
.account-btn svg:not(.credits-gauge) { width: 18px; height: 18px; }
.account-btn-in { color: var(--accent); border-color: var(--accent); }   /* signed in = accent ring */
.account-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 80; min-width: 200px; padding: 12px;
  background: var(--glass-bg); border: 1px solid var(--rule-strong); border-radius: 12px; box-shadow: var(--glass-shadow);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  display: flex; flex-direction: column; gap: 10px;
}
.account-menu-email {
  font-family: var(--mono); font-size: 11px; color: var(--ink-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* 1C credits gauge — a thin SVG arc over the 34px account circle, drained via
   stroke-dashoffset, rotated -90deg so it fills from 12 o'clock. data-level
   recolours: ok=accent, low=warn, empty=red track, pro=full accent (unlimited). */
.credits-gauge { position: absolute; inset: 0; width: 34px; height: 34px; transform: rotate(-90deg); pointer-events: none; overflow: visible; }
.credits-gauge .gauge-track { fill: none; stroke: var(--rule-strong); stroke-width: 2; }
.credits-gauge .gauge-fill {
  fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round;
  transition: stroke-dashoffset var(--dur-2) var(--ease-settle), stroke var(--dur-1) var(--ease-settle);
}
.credits-gauge[data-level="low"] .gauge-fill { stroke: var(--warn); }
.credits-gauge[data-level="empty"] .gauge-fill { stroke: none; }
.credits-gauge[data-level="empty"] .gauge-track { stroke: var(--err); }
.account-btn-in { border-color: var(--rule-strong); }   /* SVG gauge replaces the static accent border */
.account-menu-credits { font-family: var(--sans); font-size: 12px; color: var(--ink-dim); }
.account-menu-credits[data-level="low"] { color: var(--warn); }
.account-menu-credits[data-level="empty"] { color: var(--err); }
.account-menu-tier { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-quiet); }

/* Low-credit nudge — its own persistent slot (decoupled from the transient #status
   toast channel the workspace churns), fixed bottom-left. Amber heads-up, not an
   error, with an inline dismiss.
   It sits ABOVE the camera roll, never over it (MEY-93). At bottom: 28px it lay
   across the strip and swallowed the clicks of four of six thumbnails at both
   768 and 1280 — on the one population that is being asked to pay, since the
   nudge only appears when clean credits run out. The mobile block below had
   already reached this conclusion and moved banners to the top; this is the same
   rule applied to the breakpoint that was missed.
   The lift is derived from --roll-strip-h, not repeated, so the two cannot drift
   apart again; the safe-area inset is included because .roll pads by it. */
#nudge-slot:not(:empty) {
  position: fixed; left: 28px; z-index: 30;
  bottom: calc(var(--roll-strip-h) + env(safe-area-inset-bottom, 0px) + 28px);
}
#nudge-slot .banner.nudge {
  display: inline-flex; align-items: center; gap: 14px;
  background: var(--bg-elevated); border: 1px solid var(--warn); border-radius: 3px;
  padding: 12px 18px; margin: 0; color: var(--ink);
  font-family: var(--sans); font-size: 13px; box-shadow: var(--glass-shadow);
}
.nudge-dismiss {
  flex: 0 0 auto; background: none; border: none; cursor: pointer;
  color: var(--ink-quiet); font-family: var(--mono); font-size: 12px; line-height: 1; padding: 2px;
}
.nudge-dismiss:hover { color: var(--ink); }

/* Delete-all trash at the left of the strip — the bulk version of the per-tile
   trash (roll controls live on the strip, not the app-level header). */
.roll-delete-all {
  flex: 0 0 auto; align-self: center; margin: 0 10px 0 14px;
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer; color: var(--ink-quiet); border-radius: 8px;
  transition: color var(--dur-1) var(--ease-settle);
}
.roll-delete-all svg { width: 18px; height: 18px; }
.roll-delete-all:hover { color: var(--err); }

/* Open-source credits link (CC-BY-4.0 obligation: GeoCalib weights ship here). */
.app-foot { text-align: center; padding: 14px 0 26px; }
.app-foot a { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-quiet); text-decoration: none; }
.app-foot a:hover { color: var(--ink-dim); }
/* Two links now (credits + the App Store, MEY-51). Wrap rather than overflow on
   a narrow phone, and hide the separator when they stack. */
.app-foot { display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 6px 10px; padding-left: 16px; padding-right: 16px; }
.foot-sep { font-family: var(--mono); font-size: 10px; color: var(--ink-quiet); opacity: 0.6; }

/* ── Mobile / PWA ──────────────────────────────────────────────────────
   The app is an installable, mobile-capable PWA. Two concerns handled here:
   (1) dynamic viewport height — 100vh is wrong on mobile browsers (it ignores
       the retracting URL bar and, in a standalone PWA, leaves a gap); 100dvh
       tracks the real visible height. Progressive enhancement: the 100vh rules
       above stay as the fallback, these override where dvh is supported.
   (2) safe-area insets — with viewport-fit=cover the chrome draws under the
       notch / home indicator, so float the header below the notch and pad the
       roll above the home indicator. max()/env() are no-ops on desktop (insets
       are 0), so these are safe everywhere. */
body.workspace-page { height: 100dvh; }
.empty-state { min-height: 100dvh; }
.unsupported { min-height: 100dvh; }

.workspace-header {
  padding-top: max(22.5px, env(safe-area-inset-top));
  padding-left: max(40px, env(safe-area-inset-left));
  padding-right: max(40px, env(safe-area-inset-right));
}
.roll { padding-bottom: env(safe-area-inset-bottom); }

/* Mobile bottom action bar (Compare + Export). Desktop hides it — the header
   pills are the source of truth there. It is a flex item after .roll (NOT
   position:fixed), so it sits in the column and never overlaps (cf MEY-40). */
.mobile-actionbar { display: none; }

/* Export bottom sheet — reuses the wall1 glass shell, docked to the bottom. */
.sheet.wall1-backdrop { align-items: flex-end; }
.sheet .wall1-card {
  width: 100%; max-width: 100%;
  border-radius: 16px 16px 0 0;
  padding: 18px 20px max(20px, env(safe-area-inset-bottom));
  text-align: left; gap: 2px;
  animation: sheet-up var(--dur-3) var(--ease-settle) both;
}
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
.sheet-grip { width: 36px; height: 4px; border-radius: 2px; background: var(--rule-strong); margin: 0 auto 14px; }
.sheet-title { font-family: var(--serif); font-size: 22px; margin: 0 0 6px; color: var(--ink); }
.sheet-row {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 2px; min-height: 56px; width: 100%;
  background: none; border: none; border-top: 1px solid var(--rule);
  cursor: pointer; text-align: left; color: var(--ink);
}
.sheet-row:first-of-type { border-top: none; }
.sheet-row .sr-ico { width: 20px; height: 20px; color: var(--accent); flex: 0 0 auto; display: inline-flex; }
.sheet-row .sr-ico svg { width: 100%; height: 100%; }
.sheet-row .sr-body { flex: 1 1 auto; display: flex; flex-direction: column; }
.sheet-row .sr-title { font-family: var(--sans); font-size: 15px; color: var(--ink); }
.sheet-row .sr-sub { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); margin-top: 2px; }
.sheet-row .sr-cost { font-family: var(--mono); font-size: 11px; color: var(--ink-dim); flex: 0 0 auto; }
.sheet-row.sr-accent .sr-title { color: var(--accent); }

/* One-time touch gesture hint over the canvas (shown once on coarse pointers). */
.touch-hint {
  position: absolute; left: 50%; bottom: 18%; transform: translateX(-50%);
  z-index: 5; pointer-events: none;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
  color: var(--ink-dim); background: rgba(12,12,10,0.62);
  -webkit-backdrop-filter: blur(var(--glass-blur-soft)); backdrop-filter: blur(var(--glass-blur-soft));
  border: 1px solid var(--rule); border-radius: 999px; padding: 8px 14px; white-space: nowrap;
  opacity: 0; transition: opacity var(--dur-3) var(--ease-settle);
}
.touch-hint.show { opacity: 1; }

/* Phones AND any touch device (tablets included) get the mobile layout, so an
   iPad isn't a hybrid of desktop chrome + touch behaviors. */
@media (max-width: 600px), (pointer: coarse) {
  /* TOP BAR → identity + account only. The export pills / free-copy / nudge are
     pulled out of the header (rehomed in the bottom bar + sheet) so the wordmark
     and account sit on one stable row — this kills the SE two-row wrap. */
  .workspace-header {
    padding: max(8px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
             10px max(16px, env(safe-area-inset-left));
    gap: 8px; align-items: center; min-height: 0;
  }
  .logo { font-size: 20px; }
  .header-actions { gap: 0; flex-wrap: nowrap; }
  .header-actions #export-btn,
  .header-actions #export-all-btn,
  .header-actions .free-copy-link,
  .header-actions .signup-nudge { display: none; }
  .account-slot:not(:empty)::before { display: none; }   /* account is alone now */

  /* BOTTOM ACTION BAR — primary actions in thumb reach. */
  .mobile-actionbar {
    display: flex; flex: 0 0 auto; align-items: stretch; gap: 10px;
    padding: 10px max(16px, env(safe-area-inset-right))
             max(10px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
    background: #0a0a08; border-top: 1px solid var(--rule-strong);
  }
  .mab-btn {
    flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    min-height: 50px; border-radius: 12px;
    font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
    background: var(--glass-bg); border: 1px solid var(--rule-strong); color: var(--ink);
  }
  .mab-btn .mab-ico { width: 17px; height: 17px; display: inline-flex; }
  .mab-btn .mab-ico svg { width: 100%; height: 100%; }
  .mab-btn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
  .mab-primary { flex: 1.4 1 0; border-color: var(--accent); color: var(--accent); }
  .mab-primary:active { background: var(--accent); color: var(--bg); }

  /* ON-CANVAS TOOLS → bottom-right horizontal cluster, off the right crop edge. */
  .canvas-tools {
    top: auto; bottom: 12px; right: 12px; transform: none;
    flex-direction: row; align-items: center; gap: 4px;
    padding: 5px; border-radius: 14px;
  }
  .ct-crop { flex-direction: row; gap: 2px; }
  .ct-divider { width: 1px; height: 24px; margin: 0 4px; }
  /* One compare control on mobile: keep the labeled bottom-bar Compare, drop the
     duplicate in the on-canvas cluster (which keeps crop + grid). */
  .canvas-tools .ct-btn[data-tool="compare"] { display: none; }

  /* ROLL — mobile-first camera carousel. Bigger rounded thumbs (reliable tap
     targets — the cramped strip was why selecting a photo felt dead), snap
     scroll, active ring, and decluttered controls. The action bar owns the
     bottom safe-area inset, so the roll drops its own to avoid doubling. */
  .roll { padding-bottom: 0; }
  /* Taller band: mobile tiles are 104x72 rather than 88x66. Override the token,
     not min-height, so anything keyed to the strip's height follows it
     (see --roll-strip-h). A custom property needs a selector even inside a
     media query — a bare declaration here is dropped silently. */
  :root { --roll-strip-h: 96px; }
  .roll-body { align-items: center; }
  .roll-scroller {
    gap: 10px; padding: 12px 14px;
    -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity;
  }
  .roll-thumb, .roll-add, .roll-ghost {
    width: 104px; height: 72px; border-radius: 10px; scroll-snap-align: start;
  }
  .roll-thumb { touch-action: manipulation; }
  .roll-thumb img { border-radius: 10px; }
  .roll-thumb.active { outline-width: 2.5px; }
  /* Per-thumb delete only on the ACTIVE tile (tap to select, then delete) — no
     always-on × cluttering every thumb. The bulk delete-all is a desktop power
     action, hidden on mobile. */
  .roll-thumb .roll-remove { opacity: 0; }
  .roll-thumb.active .roll-remove { opacity: 1; width: 28px; height: 28px; }
  .roll-remove svg { width: 14px; height: 14px; }
  .roll-delete-all { display: none; }
  .roll-add { margin: 0; touch-action: manipulation; }
  .roll-plus { font-size: 28px; }
  .roll-add-t { font-size: 9px; }

  .preparing-label { font-size: 19px; }       /* don't crowd narrow widths */
  .preparing-detail { font-size: 10px; }     /* the line most likely to wrap */
  #callouts { top: 60px; left: 16px; max-width: min(72vw, 320px); }

  /* Transient banners go to the TOP on mobile (below the header) so they never
     cover — or block taps on — the camera roll + action bar at the bottom.
     Stacked into separate bands so #callouts (top:60) / #status / #nudge-slot
     don't pile onto the same spot when more than one is live. */
  #status:not(:empty) { top: calc(116px + env(safe-area-inset-top)); bottom: auto; }
  #nudge-slot:not(:empty) { top: calc(116px + env(safe-area-inset-top)); bottom: auto; left: 16px; right: 16px; }

  /* Keep the back-link off the notch on the home page. */
  .empty-header {
    padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
             16px max(16px, env(safe-area-inset-left));
  }

  /* HOME — fit one screen, no scroll. The page becomes a flex column (fixed
     header floats above it) with the content filling and the credits footer
     pinned at the bottom, in-viewport. (min-height, not height, so the long
     credits page — also body.grain — can still grow and scroll.) */
  body.grain { display: flex; flex-direction: column; min-height: 100dvh; }
  .empty-state {
    min-height: 0; flex: 1 1 auto; gap: 16px;
    padding: calc(60px + env(safe-area-inset-top)) 20px 14px;
  }
  .empty-headline { font-size: clamp(30px, 8.5vw, 42px); max-width: 16ch; }
  .empty-sub { margin-top: -6px; max-width: 34ch; }
  .empty-eyebrow { margin-bottom: -4px; }
  .app-foot { flex: 0 0 auto; padding: 12px 0 calc(14px + env(safe-area-inset-bottom)); }
}

/* Touch devices (no hover): bigger tool targets + suppress the hover-only
   tooltip flyout that can never fire on touch. */
@media (pointer: coarse) {
  .ct-btn { width: 44px; height: 44px; border-radius: 10px; }
  .ct-btn svg { width: 22px; height: 22px; }
  .ct-btn::after, .ct-btn:hover::after { content: none; display: none; }
  /* Home copy: hide the drag wording, show the tap/library wording. */
  .for-pointer { display: none; }
  .for-touch { display: inline; }

  /* There is no drag on touch — so the home "drop zone" is just a primary
     button, not a dashed box. Filled accent = the clear primary action
     (the sample button stays the outline secondary). */
  .drop-zone {
    width: 100%; max-width: 360px; height: auto; min-height: 56px;
    flex-direction: row; gap: 0;
    border: none; border-radius: 999px;
    background: var(--accent);
    padding: 17px 28px;
  }
  .drop-zone::before { display: none; }
  .drop-zone:hover, .drop-zone.drag-over { background: var(--accent); }
  .drop-zone-main {
    font-family: var(--mono); font-style: normal;
    font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--bg);
  }
  .drop-zone-sub { display: none; }
}

/* ── Newsletter signup (MEY-75) ────────────────────────────────────────
   Below the primary actions and visually quieter than them: the job of this
   page is to get a photo corrected, and a signup that competes with that is
   selling against the product the visitor already opened. Rendered only when
   the endpoint is configured, so there is never a dead form here. */
.newsletter { display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 34px auto 0; max-width: 420px; padding: 0 16px; }
.newsletter-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-quiet); text-align: center; line-height: 1.7; }
.newsletter-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; width: 100%; }
.newsletter-input { flex: 1 1 200px; min-width: 0; background: transparent;
  border: 1px solid var(--rule); border-radius: 999px; padding: 10px 16px;
  color: var(--ink); font-family: var(--sans); font-size: 13px;
  transition: border-color var(--dur-1) var(--ease-settle); }
.newsletter-input::placeholder { color: var(--ink-quiet); }
.newsletter-input:focus { outline: none; border-color: var(--rule-strong); }
.newsletter-submit { flex: 0 0 auto; }
.newsletter-status { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-dim); text-align: center; margin: 0; }
.newsletter-status.err { color: var(--err); }
