/* ==========================================================
   OUTPAINT.APP — LAYOUT (frozen across skins)

   Everything that affects element size, position, visibility,
   or JS-readable structure lives here. Skin authors do NOT
   touch this file. Reskin = swap app-skin-{name}.css only.

   The split principle:
   - LAYOUT: display, position, top/right/bottom/left, width,
     height, padding, margin, gap, grid/flex props, z-index,
     overflow, [hidden] handling, JS-required dimensions
     (e.g. job-card thumb 40×40), focus-visible outlines for
     a11y, animation keyframes, transition timings.
   - SKIN: colors, fonts, backgrounds, borders, box-shadows
     (decorative), text-shadows, decorative pseudo-elements,
     hover transforms (rotate/scale), border-radius.

   Border-radius is in skin because the visual rounding belongs
   to the theme. Border-width is in skin too — paired with a
   global `box-sizing: border-box` so border-width doesn't shift
   layout regardless of the value the skin picks.
   ========================================================== */

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

/* The [hidden] override is non-negotiable. JS toggles `hidden`
   on the aspect menu, jobs strip, wand controls, gallery view,
   tab views, lightbox, toast, modals, login/logout buttons, and
   gallery state messages. Author rules with `display: <X>` win
   over the UA's `[hidden] { display: none }`, which silently
   breaks show/hide. Reset that here, !important to win the
   cascade against any skin rule. */
[hidden] { display: none !important; }

/* Theme-neutral motion tokens. Layout owns animation timings;
   skins reference these or override them by redefining --op-*. */
:root {
  --op-dur-fast: 120ms;
  --op-dur-mid: 240ms;
  --op-dur-slow: 380ms;
  --op-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
  --op-ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
}

/* ----- Page shell ----- */
html, body { height: 100%; }
body { margin: 0; min-height: 100vh; }

/* ==========================================================
   APP BAR
   ========================================================== */
.op-appbar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 8px 22px;
  z-index: 5;
}
.op-appbar__brand { flex: 0 0 auto; }
.op-appbar__title { display: inline-block; }

/* The menu container wraps the tabs + HOW TO USE + auth so we can
   collapse the whole right-side cluster into a hamburger drawer
   on phones. On desktop it's just a flex row that fills the bar. */
.op-appbar__menu {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 18px;
}

.op-tabs {
  flex: 1 1 auto;
  display: flex;
  justify-content: center;
  gap: 12px;
}
.op-tab { position: relative; padding: 6px 18px; cursor: pointer; }
/* "HOW TO USE" sits outside .op-tabs in the appbar flex flow so it
   reads as a separate utility tab, off to the side of the main pair.
   margin-right pushes the auth area further right, which has the
   side effect of shifting HOW TO USE leftward in the layout. */
.op-tab--aside { flex: 0 0 auto; margin-right: 40px; }

.op-appbar__auth { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; }

/* Hamburger toggle. Three stacked bars; flips to an X via the
   .is-open state set by JS. Hidden on desktop, shown ≤760px. */
.op-appbar__hamburger {
  display: none;          /* mobile-only — turned on in the @media block */
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  margin-left: auto;      /* pushes itself to the right edge of the bar */
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  position: relative;
  z-index: 6;
}
.op-appbar__hamburger-bar {
  position: absolute;
  left: 10px;
  width: 24px;
  height: 3px;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--op-dur-mid) var(--op-ease),
              opacity var(--op-dur-fast) linear,
              top var(--op-dur-mid) var(--op-ease);
}
.op-appbar__hamburger-bar:nth-child(1) { top: 14px; }
.op-appbar__hamburger-bar:nth-child(2) { top: 21px; }
.op-appbar__hamburger-bar:nth-child(3) { top: 28px; }
.op-appbar__hamburger.is-open .op-appbar__hamburger-bar:nth-child(1) {
  top: 21px; transform: rotate(45deg);
}
.op-appbar__hamburger.is-open .op-appbar__hamburger-bar:nth-child(2) {
  opacity: 0;
}
.op-appbar__hamburger.is-open .op-appbar__hamburger-bar:nth-child(3) {
  top: 21px; transform: rotate(-45deg);
}

/* Theme picker — dropdown trigger + popover menu. Skin handles
   colors and decorative treatment. JS in theme.js owns open/close
   on the trigger via #btnThemePicker; menu is #themeMenu and
   carries [hidden] when closed. */
.op-theme-dropdown {
  position: relative;
  display: inline-block;
}
.op-theme-trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  cursor: pointer;
  font-size: 11px;
  white-space: nowrap;
}
.op-theme-trigger__current { font-weight: 700; }
.op-theme-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  min-width: 160px;
  z-index: 60;
}
.op-theme-option {
  text-align: left;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 11px;
  white-space: nowrap;
}
.op-auth__credits { padding: 4px 14px; }
.op-auth__email   { padding: 4px 12px; }

/* Tighten button sizing for our compact slots */
.op-btn { padding: 10px 22px; }
.op-btn.ttyd-btn--sm,
.op-btn.splat-btn--sm,
.op-btn--sm { padding: 6px 16px; }
.op-btn span { display: inline-block; }
.op-btn:disabled,
.op-btn[disabled] { opacity: 0.5; cursor: not-allowed; transform: none !important; }

/* ==========================================================
   EDITOR SHELL + STAGE
   ========================================================== */
.op-shell {
  display: block;
  padding: 20px 22px 80px;
  margin: 0;
}

.op-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  height: calc(100vh - 64px - 80px);
  min-height: 540px;
}

.op-stage__frame {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
}

/* The drawable canvas — absolute-centered, capped at 92% of
   frame to leave breathing room. Skin can change visuals but
   must not change positioning or max-width/max-height. */
.op-canvas {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  max-width: 92%;
  max-height: 92%;
}

/* Drop hint covers the frame; pointer-events: none so drags
   pass through to the canvas. The glyph re-enables pointer
   events on itself for click-to-add-image. */
.op-drop-hint {
  position: absolute; inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  pointer-events: none;
  text-align: center;
}
.op-drop-hint__glyph {
  width: 72px; height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  cursor: pointer;
  padding: 0; line-height: 1;
}
.op-drop-hint.is-hidden { display: none; }

/* ==========================================================
   STAGE PILLS (mode + view)

   Layout contract: mode pill = right-vertical column, view pill
   = bottom-right horizontal row of the frame. Skins must not
   move these.
   ========================================================== */
.op-pill {
  position: absolute;
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  z-index: 4;
  pointer-events: auto;
}
.op-mode-pill { right: 14px; top: 50%; transform: translateY(-50%); flex-direction: column; }
.op-view-pill { right: 14px; bottom: 14px; flex-direction: row; }
/* History pill — same row as the view pill, immediately to its left.
   View pill is 3 × 36px buttons + gap + padding ≈ 124px wide; leave
   ~10px between them. */
.op-history-pill { right: 148px; bottom: 14px; flex-direction: row; }
/* Undo/redo icons are stroke-only outlines. Override the default
   .op-pill__btn svg { fill: currentColor } so the curved arrows
   don't fill into solid teardrops. */
/* Undo/redo SVGs are stroke-only outlines — they have to keep
   fill:none even after JS DOM-moves them into .op-view-pill on
   mobile. Scope the rule by button ID so it travels with the
   buttons instead of being tied to a specific pill container. */
#btnUndo svg,
#btnRedo svg { fill: none; }

.op-pill__btn {
  width: 36px; height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.op-pill__btn svg { width: 18px; height: 18px; fill: currentColor; }
.op-pill__btn[disabled] {
  opacity: 0.32;
  cursor: not-allowed;
  pointer-events: none;
}

/* ==========================================================
   FLOATING CARDS

   .op-floating is the absolute-positioned host. .op-card is a
   surface treatment that MUST NOT set `position` (would override
   .op-floating's absolute and collapse cards into normal flow).
   The buy modal card sets its own `position: relative` separately
   for its own absolute children.
   ========================================================== */
.op-floating {
  position: absolute;
  z-index: 5;
}

/* Add Layer card — top-left of stage. Layer list on top, ADD LAYER
   button + count row below. The list is empty until the user adds
   a layer; flex-grow keeps the card height stable as cards stack. */
.op-floating--add {
  top: 18px; left: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 14px 12px;
  min-width: 240px;
  max-width: 300px;
}
.op-floating__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

/* Layer-card list — vertical stack. JS renders cards in REVERSE
   state.layers order (top of list = top of z-stack). */
.op-layer-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Used by the drag-reorder code: parsed off computedStyle to
     compute the per-step distance. Don't change unit without
     updating app.js too. */
}
.op-layer-list:empty { display: none; }

/* Card row anatomy: handle | thumb | label | remove. */
.op-layer-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  cursor: pointer;
  user-select: none;
  /* Smooth gap-fill animation while reordering. The dragged card
     gets is-dragging which kills the transition so it tracks the
     cursor 1:1 instead of catching up to it. */
  transition: transform 200ms ease;
}
.op-layer-card.is-dragging {
  transition: none;
  cursor: grabbing;
}

.op-layer-card__handle {
  flex: 0 0 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  /* Prevent the browser from interpreting touch drags as a scroll. */
  touch-action: none;
  opacity: 0.55;
}
.op-layer-card__handle:hover { opacity: 1; }
.op-layer-card.is-dragging .op-layer-card__handle { cursor: grabbing; }

.op-layer-card__thumb {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  object-fit: cover;
  display: block;
}

.op-layer-card__label {
  flex: 1 1 auto;
  min-width: 0;             /* lets ellipsis work in flex item */
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.op-layer-card__remove {
  flex: 0 0 18px;
  width: 18px; height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  line-height: 1;
  font-size: 14px;
  background: transparent;
  border: 0;
}

/* Aspect dropdown — top-right of stage */
.op-floating--aspect {
  top: 18px; right: 18px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
.op-aspect-trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  cursor: pointer;
  min-width: 140px;
  justify-content: space-between;
}
.op-aspect-menu {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  min-width: 160px;
}
.op-aspect-option {
  text-align: left;
  padding: 6px 12px;
  cursor: pointer;
}

/* Prompt card — bottom-center, straddling canvas edge */
.op-floating--prompt {
  bottom: 64px;
  left: 50%;
  transform: translate(-50%, 50%);
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 460px;
  padding: 16px 16px 14px;
  z-index: 6;
}
.op-prompt-field { position: relative; }
.op-prompt-input {
  width: 100%;
  padding: 10px 12px;
  padding-right: 40px;
  resize: vertical;
  min-height: 64px;
  outline: none;
}
.op-prompt-chevron {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 28px; height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.op-prompt-chevron svg { width: 14px; height: 14px; }

/* Collapse animation — grid-template-rows 0fr → 1fr.
   This animation timing is a layout invariant; skins can change
   the duration via --op-dur-mid but the animation itself stays. */
.op-prompt-collapse {
  display: grid;
  grid-template-rows: 0fr;
  margin-bottom: -10px;
  transition: grid-template-rows var(--op-dur-mid) var(--op-ease-soft),
              margin-bottom var(--op-dur-mid) var(--op-ease-soft);
}
.op-prompt-collapse.is-open {
  grid-template-rows: 1fr;
  margin-bottom: 0;
}
.op-prompt-collapse__inner {
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.op-prompt-counter { text-align: right; }

.op-floating--prompt #btnSubmit { width: 100%; }

.op-prompt-toggle {
  align-self: flex-end;
  padding: 4px 14px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.op-prompt-toggle__sign {
  display: inline-block;
  line-height: 1;
  width: 10px;
  text-align: center;
}
/* Hide the toggle when the prompt is expanded — the chevron
   inside the textarea handles re-collapse. */
.op-prompt-collapse.is-open ~ .op-prompt-toggle { display: none; }

/* ==========================================================
   JOBS STRIP — bottom-left of stage

   Strip itself is a column flex with capped width. Each card
   is a fixed-width row with strict thumb dimensions — without
   them the JS-injected <img> would render at the source's
   natural pixel size and "consume the screen".
   ========================================================== */
.op-floating--jobs {
  bottom: 60px; left: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: auto;
  max-width: 260px;
}

.op-job-card {
  position: relative;
  width: 240px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
}
.op-job-card__thumb {
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  object-fit: cover;
  display: block;
}
.op-job-card__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.op-job-card__status {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.op-job-card__bar {
  position: relative;
  height: 5px;
  overflow: hidden;
}
.op-job-card__bar-fill {
  position: absolute;
  inset: 0;
  width: 30%;
  animation: op-jobs-progress 1.4s ease-in-out infinite;
}
@keyframes op-jobs-progress {
  0%   { transform: translateX(-100%); }
  50%  { transform: translateX(150%); }
  100% { transform: translateX(350%); }
}
.op-job-card.is-done { cursor: pointer; }
.op-job-card.is-done .op-job-card__bar-fill,
.op-job-card.is-failed .op-job-card__bar-fill {
  animation: none;
  width: 100%;
}

/* ==========================================================
   STAGE FOOTER + WAND CONTROLS
   ========================================================== */
.op-stage__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 4px;
  flex: 0 0 auto;
}
.op-stage__actions { display: flex; align-items: center; gap: 10px; }

.op-tol-wrap {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px;
}
/* Range track + thumb. -webkit-appearance: none strips the OS skin,
   so without an explicit background the thumb paints nothing — the
   handle was invisible until we added these defaults. Skins can
   still recolor via accent-color or direct overrides. */
.op-tol-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 120px;
  height: 6px;
  outline: none;
  background: currentColor;
  opacity: 0.85;
  cursor: pointer;
}
.op-tol-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px;
  cursor: pointer;
  background: currentColor;
  border: 2px solid #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.op-tol-slider::-moz-range-thumb {
  width: 16px; height: 16px;
  cursor: pointer;
  background: currentColor;
  border: 2px solid #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.op-tol-value {
  min-width: 22px;
  text-align: center;
}

/* Brush preview ring overlaid on the canvas while eraser mode is
   active. Sized + positioned by JS in screen pixels (size slider is
   canvas-pixel diameter; we scale by canvas display ratio). pointer-
   events: none so the actual canvas still receives the mousedown. */
.op-brush-cursor {
  position: absolute;
  pointer-events: none;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.95);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  transform: translate(-50%, -50%);
  z-index: 6;
  left: 0;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.op-brush-cursor__inner {
  position: absolute;
  border-radius: 50%;
  border: 1px dashed rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}

/* Floating tool-options panel — sits inside the stage frame,
   above the view-pill. Positioning lives here (layout contract);
   skins paint the surface via .op-card. */
/* Right-side vertical stack from bottom up:
     view-pill          → bottom: 14px,  height ≈ 44px  → top ≈ 58px
     alpha toggle       → bottom: 74px,  height ≈ 40px  → top ≈ 114px
     tool-options panel → bottom: 130px
   16px gap between each container. */
.op-floating--alpha {
  right: 14px;
  bottom: 74px;
  display: flex;
}
.op-alpha-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.08em;
  background: transparent;
  border: 0;
  color: inherit;
  text-transform: uppercase;
}
.op-alpha-label { opacity: 0.8; }
.op-alpha-state { font-weight: 700; }

.op-floating--tools {
  right: 14px;
  bottom: 130px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  min-width: 220px;
}
.op-floating--tools .op-tol-wrap {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 2px 0;
}
.op-floating--tools .op-tol-slider { flex: 1 1 auto; min-width: 100px; }
.op-floating--tools__actions {
  display: flex;
  gap: 6px;
  margin-top: 4px;
}
.op-floating--tools__actions > * { flex: 1 1 0; }

/* ==========================================================
   HOW TO USE

   Three step cards across the top in a single row, then one
   full-width looping demo video underneath.
   ========================================================== */
.op-howto {
  padding: 32px 40px 60px;
  overflow-y: auto;
  height: 100%;
  box-sizing: border-box;
}
.op-howto__header {
  text-align: center;
  margin-bottom: 28px;
}
.op-howto__title { font-size: 36px; letter-spacing: 0.04em; }
.op-howto__sub   { font-size: 14px; opacity: 0.75; margin-top: 6px; }

.op-howto__steps {
  list-style: none;
  /* Negative bottom margin lets the cards tuck down over the demo
     video below; z-index keeps them rendering on top. */
  margin: 0 auto -14px;
  padding: 0 4px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 18px;
  max-width: 1040px;
  position: relative;
  z-index: 2;
}
.op-howto__step {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 20px 16px;
  position: relative;
}
.op-howto__step-num {
  font-size: 34px;
  line-height: 1;
  font-weight: 700;
}
.op-howto__step-title { font-size: 22px; letter-spacing: 0.06em; line-height: 1.1; }
.op-howto__step-desc  { font-size: 14px; line-height: 1.45; margin: 0; }

.op-howto__demo {
  margin: 0 auto;
  max-width: 1100px;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  position: relative;
}
.op-howto__demo-video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

@media (max-width: 760px) {
  .op-howto__steps { grid-template-columns: 1fr; }
}

/* ==========================================================
   GALLERY
   ========================================================== */
.op-gallery { padding: 28px 32px 60px; }
.op-gallery__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}
.op-gallery__title { display: inline-block; }
.op-gallery__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 18px;
}
.op-chip {
  padding: 6px 14px;
  cursor: pointer;
}
.op-gallery__empty,
.op-gallery__signin,
.op-gallery__loading {
  padding: 24px;
  margin: 24px 0;
  text-align: center;
}
.op-gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}
.op-gallery__item {
  position: relative;
  overflow: hidden;
  cursor: pointer;
}
.op-gallery__item img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  display: block;
}
.op-gallery__loadmore {
  display: flex;
  justify-content: center;
  margin-top: 24px;
}

/* ==========================================================
   APPBAR GEAR — settings affordance next to the email pill.
   ========================================================== */
.op-auth__settings {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  border-radius: 6px;
}
.op-auth__settings svg {
  width: 20px; height: 20px;
  color: currentColor;
}

/* ==========================================================
   SETTINGS MODAL — same overlay shape as Buy Credits, lighter
   contents. Layout only here; skin paints the surface.
   ========================================================== */
.op-settings {
  position: fixed; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 24px;
}
.op-settings__card {
  position: relative;
  width: min(520px, 100%);
  padding: 28px 28px 24px;
  max-height: 90vh;
  overflow-y: auto;
}
.op-settings__close {
  position: absolute;
  top: 8px; right: 12px;
  width: 32px; height: 32px;
  cursor: pointer;
  z-index: 2;
  padding: 0;
}
.op-settings__title { display: inline-block; margin-bottom: 18px; }
.op-settings__body { display: flex; flex-direction: column; gap: 18px; }
.op-settings__section { display: flex; flex-direction: column; gap: 6px; }
.op-settings__section-label { font-size: 11px; letter-spacing: 0.14em; }
.op-settings__section-body { font-size: 14px; line-height: 1.5; }

/* ==========================================================
   WELCOME MODAL — first-login one-shot. Layout only here; skin
   paints the surface.
   ========================================================== */
.op-welcome {
  position: fixed; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 110;            /* above settings + buy-credits modals */
  padding: 24px;
}
.op-welcome__card {
  position: relative;
  width: min(480px, 100%);
  padding: 32px 30px 26px;
  text-align: center;
  max-height: 90vh;
  overflow-y: auto;
}
.op-welcome__title { display: inline-block; margin-bottom: 18px; }
.op-welcome__credits {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  margin: 4px 0 18px;
}
.op-welcome__credits-num { font-size: 64px; line-height: 1; font-weight: 700; }
.op-welcome__credits-label { font-size: 13px; letter-spacing: 0.18em; }
.op-welcome__body {
  font-size: 14px;
  line-height: 1.55;
  margin: 0 auto 12px;
  max-width: 380px;
}
.op-welcome__body strong { font-weight: 700; }
.op-welcome__link { font-weight: 700; text-decoration: none; }
.op-welcome__link:hover { text-decoration: underline; }
.op-welcome__actions {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}
.op-welcome__cta,
.op-welcome__skip {
  flex: 1 1 0;
  padding: 12px 14px;
  /* Both buttons sit as clean rectangles — no chewed splat-blob clip,
     no idle rotate. */
  clip-path: none;
  transform: none;
  border-radius: 10px;
  min-width: 0;
}
.op-welcome__cta { flex: 2 1 0; }
.op-welcome__cta::after,
.op-welcome__skip::after { content: none; }

/* ==========================================================
   SIGN-IN PROMPT — fired when an unauthenticated visitor tries
   to add a layer. Layout mirrors the welcome modal so the two
   feel like one family.
   ========================================================== */
.op-signin {
  position: fixed; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 110;
  padding: 24px;
}
.op-signin__card {
  position: relative;
  width: min(440px, 100%);
  padding: 30px 28px 24px;
  text-align: center;
  max-height: 90vh;
  overflow-y: auto;
}
.op-signin__title { display: inline-block; margin-bottom: 14px; }
.op-signin__body {
  font-size: 14px;
  line-height: 1.55;
  margin: 0 auto 12px;
  max-width: 360px;
}
.op-signin__body strong { font-weight: 700; }
.op-signin__actions {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}
.op-signin__cta,
.op-signin__skip {
  flex: 1 1 0;
  padding: 12px 14px;
  clip-path: none;
  transform: none;
  border-radius: 10px;
  min-width: 0;
}
.op-signin__cta { flex: 2 1 0; }
.op-signin__cta::after,
.op-signin__skip::after { content: none; }

/* ==========================================================
   BUY CREDITS MODAL
   ========================================================== */
.op-buy {
  position: fixed; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 24px;
}
.op-buy__card {
  position: relative;
  width: min(560px, 100%);
  padding: 28px 28px 24px;
  text-align: center;
  max-height: 90vh;
  overflow-y: auto;
}
.op-buy__close {
  position: absolute;
  top: 8px; right: 12px;
  width: 32px; height: 32px;
  cursor: pointer;
  z-index: 2;
  padding: 0;
}
.op-buy__title { display: inline-block; margin-bottom: 14px; }
.op-buy__subtitle { max-width: 440px; margin: 0 auto 18px; }
.op-buy__banner   { padding: 8px 14px; margin-bottom: 14px; }
.op-buy__loading  { padding: 24px; }
.op-buy__packs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 14px;
  margin-top: 8px;
}
.op-buy__pack {
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
}
.op-buy__pack button {
  width: 100%;
  padding: 8px 14px;
  cursor: pointer;
}

/* ==========================================================
   LIGHTBOX (full-screen compare)

   buildCompareSlider in app.js measures the parent's
   clientWidth/clientHeight and sets explicit pixel width/height
   on .op-compare to match the after-image's aspect ratio. The
   parent therefore needs a real bounding box (not flex auto-
   sizing) and both <img> elements must fill the box absolutely.
   ========================================================== */
.op-lightbox {
  position: fixed; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 24px;
}
.op-lightbox__toolbar {
  position: absolute;
  top: 18px; right: 18px;
  display: flex;
  gap: 10px;
  z-index: 2;
}
.op-lightbox__close { padding: 8px 14px; }

.op-lightbox__compare {
  position: relative;
  width: min(96vw, 1400px);
  height: min(86vh, 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Prompt-info card. --info-right is set by openLightbox() once the
   compare slider has been sized to the image's aspect — it equals
   "wrapper gutter minus a touch", so the card hugs the image's
   right edge and overlaps it slightly. */
.op-lightbox__info {
  position: absolute;
  right: var(--info-right, 0px);
  top: var(--info-top, 60px);
  width: min(190px, 22vw);
  max-height: 60vh;
  padding: 12px 14px;
  z-index: 3;
  overflow-y: auto;
  pointer-events: auto;
}
.op-lightbox__info-title {
  font-size: 14px;
  letter-spacing: 0.14em;
  margin-bottom: 8px;
}
.op-lightbox__info-body {
  font-size: 14px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
}
.op-lightbox__info-body--empty {
  font-style: italic;
  opacity: 0.7;
}

.op-compare {
  position: relative;
  overflow: hidden;
  user-select: none;
  /* Stop the browser from interpreting a single-finger drag inside
     the slider as page-scroll / pinch-zoom on touch devices. Without
     this, the first pointermove on mobile gets cancelled and the
     drag never updates --pos. */
  touch-action: none;
  --pos: 50%;
}
.op-compare img {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
.op-compare__after {
  clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0);
}
.op-compare__label {
  position: absolute;
  top: 12px;
  padding: 6px 15px;
  z-index: 3;
  pointer-events: none;
}
.op-compare__label--before { left: 12px; }
.op-compare__label--after  { right: 12px; }

.op-compare__divider {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--pos, 50%);
  width: 3px;
  z-index: 2;
}
.op-compare__handle {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 50px; height: 50px;
  cursor: ew-resize;
}

/* ==========================================================
   TOAST
   ========================================================== */
.op-toast {
  position: fixed;
  bottom: 36px; left: 50%;
  transform: translateX(-50%);
  padding: 12px 22px;
  z-index: 250;
  animation: op-toast-in 0.42s var(--op-ease);
}
@keyframes op-toast-in {
  from { transform: translate(-50%, 30px); opacity: 0; }
  to   { transform: translate(-50%, 0);    opacity: 1; }
}

/* ==========================================================
   LEGAL LINKS

   The page-level footer was removed so the editor view doesn't
   require scroll. The Privacy / Terms / Contact links now live
   inside the How-To view (.op-legal) and the Buy Credits modal
   (.op-legal.op-legal--inline). Only structural rules here;
   skin owns colors / typography.
   ========================================================== */
.op-legal {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 36px 22px 24px;
}
.op-legal--inline {
  padding: 14px 0 0;
  font-size: 12px;
}

/* ==========================================================
   FOCUS RINGS — accessibility scaffolding (frozen).
   Skin can change the COLOR via --op-focus-ring (default is a
   neutral cyan), but the ring itself must remain visible.
   ========================================================== */
:root { --op-focus-ring: #00d2ff; }
.op-pill__btn:focus-visible,
.op-aspect-trigger:focus-visible,
.op-aspect-option:focus-visible,
.op-prompt-toggle:focus-visible,
.op-prompt-chevron:focus-visible,
.op-drop-hint__glyph:focus-visible,
.op-tab:focus-visible,
.op-chip:focus-visible {
  outline: 2px dashed var(--op-focus-ring);
  outline-offset: 2px;
}

/* ==========================================================
   TOUCH / POINTER

   touch-action: none on interactive surfaces stops the browser
   from scrolling/zooming the page when a finger drags inside
   them. The drag-handle on layer cards already has this; the
   canvas needs it too so single-finger drags don't double up
   as page-scroll on phones. The brush cursor preview is
   pointer-events: none so it never intercepts touches.
   ========================================================== */
.op-canvas { touch-action: none; }
.op-brush-cursor { pointer-events: none; }

/* ==========================================================
   RESPONSIVE — phone breakpoint

   ≤760px: collapse appbar nav into a hamburger drawer; tighten
   gallery grid; bump hit targets; stack How To Use cards;
   relocate the lightbox info card below the image. Editor's
   floating cards stay where they are — drawer-ifying that is a
   separate pass.
   ========================================================== */
@media (max-width: 760px) {

  /* Hide the parallax canvas on phones — the editor takes the
     full viewport; no decorative bg should peek through. */
  #fxParallaxBg { display: none; }

  /* Editor canvas runs flush to the viewport edges on phones.
     Drop the .op-shell padding, raise .op-canvas to 100% of the
     frame, recompute the stage height to subtract only the appbar. */
  .op-shell { padding: 0; }
  .op-stage { height: calc(100vh - 50px); min-height: 0; }
  .op-canvas { max-width: 100%; max-height: 100%; }

  /* Appbar — show hamburger, hide menu by default. The menu drops
     down as a panel below the bar when open. */
  .op-appbar { padding: 8px 14px; gap: 10px; }
  .op-appbar__hamburger { display: block; }
  .op-appbar__menu {
    position: absolute;
    top: 100%; left: 0; right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    padding: 14px 18px;
    z-index: 5;
    /* Slide + fade. .is-open is toggled by JS in tandem with
       the hamburger's is-open class. */
    transform: translateY(-8px);
    opacity: 0;
    pointer-events: none;
    transition: transform var(--op-dur-mid) var(--op-ease),
                opacity var(--op-dur-mid) linear;
  }
  .op-appbar__menu.is-open {
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
  }
  .op-tabs {
    flex: 0 0 auto;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .op-tab--aside { margin-right: 0; }
  .op-appbar__auth {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }

  /* Hit targets — keep the mode pill (V/W/E) tappable but slightly
     smaller than desktop's 44px so it intrudes less on the canvas;
     view + history pills are utilities and shrink further. */
  .op-pill__btn { width: 38px; height: 38px; }
  .op-mode-pill { right: 6px; }       /* push closer to the viewport edge */
  .op-mode-pill .op-pill__btn { width: 38px; height: 38px; }
  .op-view-pill .op-pill__btn,
  .op-history-pill .op-pill__btn { width: 32px; height: 32px; }
  .op-view-pill .op-pill__btn svg,
  .op-history-pill .op-pill__btn svg { width: 15px; height: 15px; }
  .op-pill { padding: 4px; gap: 5px; }
  .op-view-pill, .op-history-pill { padding: 3px; gap: 3px; }
  /* History pill normally sits left of the view pill at right:148.
     With smaller buttons that gap is wrong — recompute. */
  .op-history-pill { right: 116px; }

  /* Gallery — denser grid, header stacks. */
  .op-gallery { padding: 18px 14px 50px; }
  .op-gallery__header {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  .op-gallery__grid {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 10px;
  }
  .op-gallery__filters { gap: 8px; }

  /* How To Use — stack cards vertically and kill the demo overlap
     (which only reads as overlap when the cards are side-by-side).
     On phones we also reorder so the demo video shows FIRST (right
     under the title), with the step cards stacking below — easier
     to grok the flow when you can see it move first. */
  .op-howto {
    padding: 22px 14px 40px;
    display: flex;
    flex-direction: column;
  }
  .op-howto__header { order: 0; }
  .op-howto__demo   {
    order: 1;
    /* aspect-ratio + flex stretch can collapse to a few px on
       phones without an explicit width — pin to 100% of the
       column so the video actually shows. */
    width: 100%;
    max-width: 100%;
    margin-bottom: 18px;
  }
  .op-howto__steps {
    order: 2;
    grid-template-columns: 1fr;
    margin: 0 0 18px;
    gap: 14px;
  }
  .op-howto .op-legal { order: 3; }
  .op-howto__title { font-size: 28px; }

  /* Lightbox — info card moves below the image. JS still sets
     --info-right + --info-top, but we override here to position
     statically and full-width-ish under the slider. */
  .op-lightbox { padding: 12px; }
  .op-lightbox__compare { flex-direction: column; gap: 12px; }
  .op-lightbox__info {
    position: static;
    transform: none;
    width: 100%;
    max-width: 460px;
    margin: 0 auto;
    /* Drop the JS-set top/right offsets — they're meaningless when
       the card is back in normal flow. */
    right: auto !important;
    top: auto !important;
  }

  /* Editor — minor tightening. Drawer-ification is a future pass.
     For now: shrink the layer panel hard (drop the filename label
     + count, compact +ADD LAYER), lower the prompt card, hide the
     drop-hint sub-text.

     The mobile layer card carries three things, in order:
       drag-handle dots → thumb → × remove
     and the panel hugs the card's natural width via fit-content. */
  .op-floating--add {
    width: max-content;
    /* The desktop rule sets min-width: 240px — that wins over
       width: max-content and stretches the panel ~220 px wide
       even when its content is the slim 100 px layer card. Reset. */
    min-width: 0;
    max-width: 112px;
    padding: 6px;
    gap: 4px;
  }
  /* +ADD LAYER row defaults to justify-content: space-between
     (which spreads the lone "+" button across the full row width).
     Override so the row hugs the button. */
  .op-floating--add .op-floating__row {
    justify-content: flex-start;
    gap: 8px;
  }
  /* Card width is pinned at 100 px on mobile — the natural content
     width was ~80, which left the row looking unbalanced and gave
     the thumb less room to read. */
  .op-floating--add .op-layer-card { width: 100px; padding: 4px 5px; gap: 4px; }
  .op-floating--add .op-layer-card__handle { flex: 0 0 14px; }
  .op-floating--add .op-layer-card__handle svg { width: 12px; height: 12px; }
  .op-floating--add .op-layer-card__thumb { width: 26px; height: 26px; }
  .op-floating--add .op-layer-card__remove {
    flex: 0 0 16px;
    width: 16px; height: 16px;
    font-size: 13px;
    /* Push the × to the card's right edge so it's centered in the
       breathing room after the thumb (instead of pinned 4 px after
       the thumb with empty space to its right). */
    margin-left: auto;
  }
  .op-floating--add .op-layer-card__label { display: none; }
  .op-floating--add .op-layer-count { display: none; }
  /* Compact +ADD LAYER → "+ IMAGE". CSS swap so we don't touch
     HTML or JS for a viewport-only label change. */
  .op-floating--add #btnAddImage span { display: none; }
  .op-floating--add #btnAddImage::before {
    content: '+ IMAGE';
    font-size: 11px;
    letter-spacing: 0.05em;
    line-height: 1;
  }
  .op-floating--add #btnAddImage { padding: 5px 10px; min-width: 0; }

  /* Hide the canvas placeholder (yellow square + DROP AN IMAGE
     line) on phones — busy on small viewports, and the +IMAGE
     button in the layer panel covers the same affordance. */
  .op-drop-hint { display: none; }

  /* Tool options (wand TOL / eraser SIZE+FEATHER + APPLY/CANCEL)
     move to the top-right of the stage frame on phones — under
     the aspect dropdown, vertically aligned with the SHOW ALPHA
     toggle below. Width pinned to match the alpha toggle so the
     two utility panels read as a paired column. */
  .op-floating--tools {
    top: 76px;
    bottom: auto;
    left: auto;
    right: 42px;
    width: 184px;
    box-sizing: border-box;
    min-width: 0;
    padding: 8px 12px;
  }
  /* Slider's desktop min-width: 100 forces the value text to spill
     out the right side of a 184-px panel. Loosen it on mobile so
     the slider compresses and the value sits inside the box. */
  .op-floating--tools .op-tol-slider { min-width: 0; }
  .op-floating--tools .op-tol-wrap { gap: 6px; }
  .op-floating--tools .op-tol-value { min-width: 22px; text-align: right; }

  /* PAINT IT card sits flush with the viewport bottom on phone.
     Drop the desktop's translate(-50%, 50%) — the card no longer
     straddles the canvas edge here. With column-flex order
     (collapse → button → toggle), tapping "Optional Prompt"
     opens the textarea above PAINT IT and grows the card upward
     while the bottom stays anchored. */
  .op-floating--prompt {
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    max-width: none;
    transform: none;
    border-radius: 14px 14px 0 0;
  }

  /* Bottom-right cluster rides on top of the prompt card. The
     pills live inside .op-stage__frame; the prompt card lives in
     .op-stage as a sibling of the frame. The frame's bottom edge
     sits some px above the card's top (the stage footer separates
     them). JS measures (frame.bottom - card.top) into
     --card-top-frame so pill bottoms anchor to the card's top
     edge regardless of the footer offset.

     On mobile, JS DOM-moves undo+redo into the view-pill so it
     becomes one continuous container of 5 buttons. The history
     pill is .hidden — no positioning needed. */
  /* Pill sits flush with the card's top edge (no gap). Alpha
     stacks ~4 px above the pill, sized to match its width
     (pill width ≈ 5×32 + 4×3 + 2×3 + 2×3 border = 184 px). */
  .op-view-pill { bottom: var(--card-top-frame, 90px); right: 10px; }
  .op-floating--alpha {
    bottom: calc(var(--card-top-frame, 90px) + 42px);
    right: 10px;
    width: 184px;
    box-sizing: border-box;
    justify-content: center;
  }
  .op-alpha-toggle {
    justify-content: center;
    width: 100%;
    /* Smaller font + tighter padding so "SHOW ALPHA: OFF" fits on
       one line at the 184 px width. */
    font-size: 10px;
    padding: 6px 10px;
    gap: 6px;
    letter-spacing: 0.06em;
  }

  .op-drop-hint__sub { display: none; }

  /* Active job cards — anchor to the prompt card's top edge so
     the strip slides up/down with PAINT IT just like the alpha
     + pill cluster on the right does. The strip lives in
     .op-stage (sibling of the prompt card), NOT in
     .op-stage__frame, so use --prompt-card-h directly — the
     card's height = its top edge's distance from stage bottom,
     because the card itself is anchored bottom: 0. */
  .op-floating--jobs {
    bottom: calc(var(--prompt-card-h, 90px) + 10px);
    left: 10px;
    max-width: 120px;
    gap: 6px;
  }
  .op-job-card { width: 120px; padding: 5px 7px; gap: 6px; }
  .op-job-card__thumb { width: 30px; height: 30px; }
  .op-job-card__status { font-size: 11px; }
}
