/* Fridge Magnet Generator — runtime UI styles.
   Injected into the actual runtime mount root (Shadow-DOM safe). */

/* ══════════════════════════════════════════════════════════════════════
 * Atomm generator layout spec — Figma "生成器布局规范" v1.2, section
 * "4ad · 生成悬浮350 · Tab顶部居中 · 无右栏"
 * (file b2y2t6TeE2vTSuta75kS1Z, section 150:17947, frame Main 150:17948).
 *
 * The design tokens below are taken from that file. Do not hand-tune:
 * change the design, re-read it, then change this.
 * ══════════════════════════════════════════════════════════════════════ */
.pp-app {
  /* ---- design tokens (Figma) ---- */
  /* color/text */
  --t-primary: #171719;
  --t-secondary: #3d3e42;
  --t-tertiary: #929499;
  --t-body: #1c1c21;
  --t-label: #8c8c94;        /* "View all" */
  --t-inverse: #ffffff;
  /* color/stroke */
  --stroke: #d9dbde;
  --divider: rgba(61, 62, 66, 0.08);
  /* color/bg */
  --bg-default: #ffffff;
  --bg-subtlest: #fcfcfd;
  --bg-control: #f1f2f4;
  --bg-hover: rgba(61, 62, 66, 0.06);
  --bg-segmented-active: #ffffff;
  /* color/brand + warning */
  --brand: #292a2d;
  --warning: #ff6500;
  --warning-disabled: rgba(240, 106, 0, 0.4);
  /* radius / space */
  --r-sm: 4px; --r-md: 6px; --r-lg: 8px; --r-xl: 12px; --r-circle: 100px;
  --sp-50: 4px; --sp-75: 6px; --sp-100: 8px; --sp-150: 12px; --sp-200: 16px;
  /* effect/shadow-100 and the footer's shadow-200(-) */
  --shadow-100: 0 0 4px rgba(11, 11, 13, 0.06), 0 4px 12px rgba(11, 11, 13, 0.06);
  /* The footer's shadow-200(-) reads as `0 -2px 8px rgba(0,0,0,.1),
     0 -16px 32px rgba(0,0,0,.1)` on the spec node, but that frame has NO
     fill — Figma casts a drop shadow from an unfilled frame's content
     alpha, so in the design it is only a ~1% hint against the divider.
     Our bar has a white background, where the same numbers paint a 10%
     band 40px up over the scrolling list. Matched to the rendered design
     instead: the 1px divider does the separating, the shadow only hints
     that content runs underneath. */
  --shadow-200-up: 0 -1px 4px rgba(0, 0, 0, 0.04);

  /* ---- legacy aliases ----
     The generator predates the spec and its modal / dropzone / fridge chrome
     still reads these. Remapping here retones the whole app in one place; the
     components with explicit spec geometry are restyled individually below. */
  --bg: var(--bg-control);
  --surface: var(--bg-default);
  --border: var(--divider);
  --text: var(--t-primary);
  --muted: var(--t-tertiary);
  --accent: var(--t-primary);
  --accent-deep: var(--brand);
  --accent-soft: var(--bg-hover);
  --danger: #d05a4e;
  --radius: var(--r-lg);
  --shadow: var(--shadow-100);
  /* Width the floating style panel takes out of the stage: 16 + 360 + 16.
     The stage itself runs full-bleed underneath it, so every piece of canvas
     chrome centres against `50% + var(--panel-inset)/2` instead of 50%. */
  --panel-inset: 392px;

  position: relative;
  width: 100%;
  height: 100%;
  min-height: 560px;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 14px;
}

.pp-app *, .pp-app *::before, .pp-app *::after { box-sizing: border-box; }
.pp-app :where(button) { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
.pp-app :where(img) { display: block; max-width: 100%; }

/* ---------- stage ---------- */
.stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* The spec's canvas is a flat color/bg/control ground — no vignette. */

/* ---------- left panel ---------- */
/* Floating Sidebar (spec node 150:17952): 360 wide, inset 16, r12, shadow-100.
   Opaque — the sliding secondary screens have to cover the catalog they slide
   over, and dense style cards need a solid ground to stay legible. */
.side-panel {
  position: absolute;
  left: var(--sp-200);
  top: var(--sp-200);
  bottom: var(--sp-200);
  width: 360px;
  background: var(--bg-default);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-100);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  z-index: 5;
}
.side-views {
  position: relative;
  flex: 1;
  min-height: 0;
}
.side-view {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  transition: transform 0.28s ease;
}
.side-view.is-secondary,
.side-view.is-collection { transform: translateX(100%); }
.side-panel.show-history .side-view.is-primary,
.side-panel.show-collection .side-view.is-primary { transform: translateX(-24%); }
.side-panel.show-history .side-view.is-secondary { transform: translateX(0); }
.side-panel.show-collection .side-view.is-collection { transform: translateX(0); }
/* the collection screen must sit above the catalog it slid out of */
.side-view.is-collection { z-index: 2; }

/* Content block of the Floating Sidebar: p16, 20px between category blocks. */
.side-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--sp-200);
}
.side-scroll::-webkit-scrollbar { width: 6px; }
.side-scroll::-webkit-scrollbar-thumb { background: #d5d5db; border-radius: 3px; }

.cat-section { margin-bottom: 20px; }
.cat-section:last-child { margin-bottom: 0; }

/* Sidebar/Section Header (spec node 71:7307) */
.cat-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-100);
  margin-bottom: var(--sp-150);
}
.cat-icon {
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: var(--r-sm);
  background: var(--bg-control);
  display: grid;
  place-items: center;
  color: var(--t-secondary);
}
.cat-icon svg { width: 14px; height: 14px; display: block; }
.cat-title {
  display: flex;
  align-items: center;
  gap: var(--sp-100);
  flex: 1;
  min-width: 0;
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The spec header carries no item count. */
.cat-count { display: none; }
.cat-viewall {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-50);
  flex: none;
  padding: 0;
  font-size: 11px;
  font-weight: 400;
  line-height: 16px;
  color: var(--t-label);
}
.cat-viewall:hover { color: var(--t-secondary); }
.cat-viewall .chev { display: inline-flex; width: 16px; height: 16px; }
.cat-viewall .chev svg { width: 16px; height: 16px; display: block; }
/* Random-style dice at the top of the catalog. Not in the spec (24 styles
   invite decision paralysis, so the product keeps it) — retoned to the
   spec's neutral palette and given the section-header's metrics so it reads
   as part of the same sidebar. */
.surprise-bar {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-100);
  margin-bottom: 20px;
  padding: var(--sp-100);
  border-radius: var(--r-lg);
  background: var(--bg-control);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-primary);
  transition: background 0.15s ease;
}
.surprise-bar:hover { background: #e9eaec; }
.surprise-bar .grow { flex: 1; text-align: left; }
.surprise-bar .dice { display: inline-flex; transition: transform 0.3s ease; }
.surprise-bar .dice svg { width: 16px; height: 16px; display: block; }
.surprise-bar:hover .dice { transform: rotate(18deg) scale(1.12); }
.surprise-face {
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: var(--r-sm);
  background-color: var(--bg-hover);
  background-size: cover;
  background-position: center;
}

/* Items Row (spec node 150:18661): three equal columns, gap 8. The old
   design was a horizontal rail with a deliberately cropped card; the spec
   shows a fixed 3-up preview with "View all" as the way to see the rest. */
.style-rail {
  display: grid;
  /* minmax(0, 1fr) because a bare 1fr floors at min-content, and a long
     nowrap label would widen its whole column */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-100);
  align-items: start;
}

/* "View all" screen: two per row — three columns in a 360 panel makes the
   covers too small to judge (matches spec frame 150:17996). */
.style-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-100);
  align-items: start;
}
.style-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-75);
  text-align: center;
  padding: 0;
}
/* Image Frame (spec node 150:18649): r8 + a 1px divider hairline. The spec
   sizes the frame to the collection's own content aspect (its banknote row is
   99:64, its coin row 1:1) — magnets are square, so the frame is 1:1 and
   nothing is cropped. */
.style-thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--r-lg);
  background: var(--bg-default);
  border: 1px solid var(--divider);
  overflow: hidden;
  display: grid;
  place-items: center;
  transition: border-color 0.15s ease;
}
.style-card:hover .style-thumb { border-color: var(--stroke); }
.style-card.is-active .style-thumb { border: 2px solid var(--t-primary); }
.style-name {
  /* The spec's own labels are single-line; these are real style names
     ("Girl with a Pearl Earring"), so two lines are reserved and every card
     stays the same height. */
  height: 32px;
  font-size: 11px;
  font-weight: 400;
  line-height: 16px;
  color: var(--t-secondary);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* CSS-rendered die-cut thumbnail (fallback when a cover is missing) */
.art-thumb {
  width: 74%;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  position: relative;
  display: grid;
  place-items: center;
}
.art-thumb::before {
  content: '';
  position: absolute;
  inset: 8%;
  border-radius: 4px;
  border: 1.5px dashed rgba(255, 255, 255, 0.6);
  opacity: 0.7;
}
.art-thumb .art-glyph { font-size: 20px; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3)); }

/* pre-generated cover image inside a card thumb */
/* No radius on the cover: the frame clips it with overflow:hidden, and that clip
   follows the border's INNER curve (border-radius − border-width). Any radius
   here rounds harder than that curve — 8px against the 6px inner edge of the
   2px selected border — and notches all four corners. */
.style-cover { width: 100%; height: 100%; display: grid; place-items: center; }
/* skeleton under the cover: rails otherwise flash a column of white holes while
   50 thumbnails stream in */
.style-cover.is-loading {
  background: linear-gradient(100deg, #e9eaec 20%, #f4f5f6 38%, #e9eaec 60%);
  background-size: 300% 100%;
  animation: skeleton-sweep 1.3s linear infinite;
}
@keyframes skeleton-sweep { from { background-position: 150% 0; } to { background-position: -150% 0; } }
.style-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* my history entry (bottom of the panel) */
/* Footer + Sidebar/Footer Item (spec nodes 150:18683 / 71:7314): a 54px bar
   pinned to the bottom of the card, lifted off the list with a divider and
   shadow-200(-) so the catalog scrolls under it. */
.my-works-bar {
  flex: none;
  width: 100%;
  height: 54px;
  padding: var(--sp-100) 18px;
  display: flex;
  align-items: center;
  gap: var(--sp-100);
  background: var(--bg-default);
  border-top: 1px solid var(--divider);
  box-shadow: var(--shadow-200-up);
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--t-primary);
  transition: background 0.15s ease;
}
.my-works-bar:hover { background: var(--bg-hover); }
.my-works-bar .grow { flex: 1; text-align: left; }
.my-works-bar .bar-icon {
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: var(--r-sm);
  background: var(--bg-control);
  display: grid;
  place-items: center;
  color: var(--t-secondary);
}
.my-works-bar .bar-icon svg { width: 14px; height: 14px; display: block; }
.my-works-bar .chev { display: inline-flex; width: 16px; height: 16px; color: var(--t-label); }
.my-works-bar .chev svg { width: 16px; height: 16px; display: block; }

/* ---------- secondary screens (My history / View all) ---------- */
.history-head {
  display: flex;
  align-items: center;
  gap: var(--sp-100);
  padding: var(--sp-200) var(--sp-200) var(--sp-150);
}
.history-back {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--r-lg);
  display: grid;
  place-items: center;
  background: var(--bg-control);
  color: var(--t-secondary);
  font-size: 15px;
}
.history-back:hover { background: #e9eaec; }
.history-title {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--t-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.history-refresh {
  width: 32px; height: 32px; border-radius: 10px;
  display: grid; place-items: center; color: var(--muted);
}
.history-refresh:hover { background: #f6f2e8; color: var(--text); }

.history-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  padding: 4px 2px 12px;
}
.history-card {
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--bg-subtlest);
  /* magnets are square, and so is the card — nothing gets cropped */
  aspect-ratio: 1 / 1;
  position: relative;
  padding: 0;
}
.history-card img { width: 100%; height: 100%; object-fit: cover; }
.history-card:hover { box-shadow: 0 6px 16px rgba(20, 24, 32, 0.16); }
.history-card .badge {
  position: absolute;
  right: 6px;
  bottom: 6px;
  background: rgba(31, 36, 48, 0.72);
  color: #fff;
  font-size: 11px;
  border-radius: 8px;
  padding: 2px 7px;
}
.history-card .center-note {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 12px;
  padding: 8px;
  text-align: center;
}
.history-empty, .history-login {
  color: var(--muted);
  text-align: center;
  padding: 42px 18px;
  font-size: 13px;
  line-height: 1.6;
}
.history-more {
  display: block;
  width: 100%;
  padding: 10px;
  margin: 2px 0 12px;
  border-radius: 12px;
  background: #f6f2e8;
  color: var(--text);
  font-weight: 600;
}
.history-more:hover { background: #efe9d8; }

/* ---------- canvas area (infinite canvas) ---------- */
.canvas-area {
  /* Full-bleed on purpose: the gallery has to run edge to edge behind the
     floating panel. Reserving a column here left a strip of page background
     between the panel and the door, which read as a hard seam. */
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  overflow: hidden;
  /* The stage is a drag surface, so nothing in it may start a text selection:
     a pan that began here used to sweep a selection across whatever labels the
     pointer crossed — the view tabs most visibly. A native selection starts at
     the element the pointer went down on, so blocking it here is enough; the
     modal and its inputs live outside this box and keep theirs. */
  user-select: none;
  -webkit-user-select: none;
}
/* Embedded canvas target has no panel over it, so nothing to clear. */
.canvas-area.is-solo { --panel-inset: 0px; }
.canvas-area.has-content { cursor: grab; }
.canvas-area.is-panning { cursor: grabbing; }

/* transformed world layer — translate/scale via the view state */
.world-layer {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  will-change: transform;
}
/* bare images on the canvas — no card chrome, transparency floats directly */
.world-cell {
  position: absolute;
  display: block;
  padding: 0;
  background: transparent;
  border: none;
}

/* ---------- 2D view: the piece, bare ---------- */
/* NO background and NO overflow clip. The composite is transparent outside its
   silhouette, so any ground here paints a square card behind a die-cut piece —
   and `overflow: hidden` on top of that sliced the rim off wherever the design
   ran to the edge of its frame. The stage's own ground is the backdrop. */
.art-stack {
  --reveal: 0%;
  position: relative;
  width: 100%;
  height: 100%;
}
.art-stack img,
.art-stack canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* `contain`: the composite is padded by the rim, so cropping it to fill
     would cut the border back off at the frame edge. */
  object-fit: contain;
  pointer-events: none;
}
/* The original photo, wiped in from the left by the handle. This one IS a
   rectangle — it is the upload, not the piece — so it keeps its own clip. */
.art-reference {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 12px;
  clip-path: inset(0 calc(100% - var(--reveal)) 0 0);
}
/* The uploaded photo fills the cell — `contain` belongs to the composite,
   which carries its own rim padding; a letterboxed photo behind the wipe just
   reads as a smaller, offset picture. */
.art-reference img { object-fit: cover; }
.art-reference::after {
  content: 'PHOTO';
  position: absolute;
  left: 10px;
  top: 10px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 1.4px;
  color: #fff;
  background: rgba(0, 0, 0, 0.42);
  border-radius: 999px;
  padding: 3px 8px;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.art-stack:hover .art-reference::after,
.art-stack.is-dragging .art-reference::after { opacity: 1; }

.reveal-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  /* keep the grip fully inside the canvas at both extremes (it would be
     half-clipped by the stack's overflow at 0% / 100%) */
  left: clamp(23px, var(--reveal), calc(100% - 23px));
  width: 46px;
  margin-left: -23px;
  cursor: ew-resize;
  touch-action: none;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity 0.22s ease;
}
.art-stack:hover .reveal-handle,
.art-stack.is-dragging .reveal-handle { opacity: 1; }
.art-stack.is-revealing .reveal-handle { opacity: 0; }
.reveal-line {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
}
.reveal-grip {
  position: relative;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.96);
  color: #6b5520;
  font-size: 15px;
  display: grid;
  place-items: center;
  box-shadow: 0 4px 14px rgba(40, 30, 10, 0.35);
}
/* during the intro wipe the seam glows like wet paint */
.art-stack.is-revealing .reveal-line {
  opacity: 1;
  box-shadow: 0 0 18px 4px rgba(255, 233, 168, 0.85);
}

/* ---------- stage chrome: view tabs on top, restyle bar at the bottom ---------- */
.stage-topbar {
  position: absolute;
  top: 16px;
  left: calc(50% + var(--panel-inset) / 2);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  max-width: calc(100% - var(--panel-inset) - 32px);
  z-index: 4;
}

/* ---------- Edit: top-right of the stage ---------- */
/* Opposite the view tabs, on the same 16px line. Above them in z so the
   popover is never cut off by the tab pill. */
.stage-editbar {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 6;
}
.edit-wrap { position: relative; }
.pp-app .edit-btn {
  height: 32px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  border-radius: var(--r-md);
  /* opaque like the view tabs: this floats over brushed steel */
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(20, 24, 32, 0.12);
  font-size: 12px;
  font-weight: 500;
  color: var(--t-secondary);
  transition: color 0.14s ease, background 0.14s ease;
}
.pp-app .edit-btn:hover { color: var(--t-primary); background: rgba(255, 255, 255, 0.95); }
.pp-app .edit-btn.is-on { background: var(--bg-default); color: var(--t-primary); }
.edit-btn .ui-icon { line-height: 0; }

.edit-panel {
  position: absolute;
  top: 40px;
  right: 0;
  width: 268px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  border-radius: var(--r-xl);
  background: var(--bg-default);
  box-shadow: 0 16px 48px rgba(16, 20, 28, 0.22);
  animation: edit-pop 0.16s ease both;
}
@keyframes edit-pop { from { transform: translateY(-4px); opacity: 0; } }
.edit-panel[hidden] { display: none; }
.edit-row { display: flex; align-items: flex-start; gap: 12px; }
.edit-row-text { flex: 1; min-width: 0; }
.edit-row-label { font-size: 13px; font-weight: 500; color: var(--t-primary); line-height: 18px; }
.edit-row-hint { font-size: 11px; color: var(--t-tertiary); line-height: 15px; margin-top: 1px; }
.edit-divider { height: 1px; background: var(--divider); }

/* iOS-style switch, sized to the 13px label beside it */
.pp-app .switch {
  flex: none;
  width: 36px;
  height: 20px;
  margin-top: 1px;
  padding: 2px;
  border-radius: 999px;
  background: var(--bg-control);
  transition: background 0.16s ease;
}
.pp-app .switch.is-on { background: var(--brand); }
.switch-knob {
  display: block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(16, 20, 28, 0.3);
  transition: transform 0.16s ease;
}
.switch.is-on .switch-knob { transform: translateX(16px); }

.edit-slider-row { display: flex; align-items: center; gap: 10px; }
.edit-slider-label { font-size: 12px; color: var(--t-label); white-space: nowrap; }
.edit-value {
  min-width: 34px;
  text-align: right;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--t-secondary);
}
.edit-slider-row.is-off { opacity: 0.4; }
.edit-slider {
  flex: 1;
  min-width: 0;
  height: 4px;
  appearance: none;
  -webkit-appearance: none;
  border-radius: 999px;
  background: var(--bg-control);
  cursor: pointer;
}
.edit-slider::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 1px 4px rgba(16, 20, 28, 0.35);
}
.edit-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: none;
  border-radius: 50%;
  background: var(--brand);
}
.edit-slider:disabled { cursor: default; }
/* the door has its own orbit/zoom gestures — the world zoom row is meaningless */
.canvas-area.is-fridge .zoom-controls { display: none; }

.stage-actions {
  position: absolute;
  left: calc(50% + var(--panel-inset) / 2);
  /* clear of the zoom row so the two never collide on a narrow viewport */
  bottom: 78px;
  transform: translateX(-50%);
  max-width: calc(100% - var(--panel-inset) - 32px);
  z-index: 3;
  pointer-events: none;
}
.restyle-bar {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 16px 13px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid var(--border);
  box-shadow: 0 12px 34px rgba(64, 52, 20, 0.2);
  backdrop-filter: blur(8px);
}
.restyle-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}
.restyle-label { font-size: 13px; font-weight: 700; color: var(--text); }
.restyle-list { display: flex; align-items: flex-start; gap: 10px; }
.restyle-chip {
  width: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 0;
  border-radius: 12px;
}
.restyle-cover,
.restyle-dice {
  width: 56px;
  height: 56px;
  border-radius: 13px;
  background-size: cover;
  background-position: center;
  box-shadow: inset 0 0 0 1px rgba(143, 106, 30, 0.28);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.restyle-chip:hover .restyle-cover,
.restyle-chip:hover .restyle-dice {
  transform: translateY(-2px) scale(1.04);
  box-shadow: 0 6px 14px rgba(64, 52, 20, 0.25), inset 0 0 0 2px var(--accent);
}
.restyle-name {
  width: 100%;
  font-size: 10.5px;
  color: var(--muted);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.restyle-chip.is-dice .restyle-dice {
  display: grid;
  place-items: center;
  color: var(--accent-deep);
  background: linear-gradient(135deg, #fdf7e6, #f6e9c9);
}
.restyle-chip.is-dice:hover .restyle-dice { transform: translateY(-2px) rotate(12deg) scale(1.04); }

/* selection affordance only for legacy multi-image tasks */
/* Only ever shown for legacy multi-image tasks, where there is a grid to pick
   from; a lone result has nothing to be selected against. */
.world-cell.is-selected .art-stack {
  outline: 3px solid var(--accent);
  outline-offset: 6px;
  border-radius: 14px;
}

/* centered overlay for empty / generating / failed states */
.canvas-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 32px;
  /* empty / painting / failed states centre in the free half, not under the
     floating panel */
  padding-left: calc(32px + var(--panel-inset));
  pointer-events: none;
}
.canvas-overlay .btn-again { pointer-events: auto; }

/* Zoom Controls, bottom-left (spec node 71:6425). Bottom-right belongs to the
   shell's Buy Materials + Export block. 32×32 white tiles, r8, gap 4, no
   border or shadow — they sit on the flat color/bg/control ground. */
.zoom-controls {
  position: absolute;
  left: calc(var(--sp-200) + var(--panel-inset));
  bottom: var(--sp-200);
  display: flex;
  gap: var(--sp-50);
  z-index: 4;
}
.canvas-area:not(.has-content) .zoom-controls { display: none; }
.zoom-btn {
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: var(--r-lg);
  background: var(--bg-default);
  font-size: 14px;
  font-weight: 400;
  color: var(--t-secondary);
  display: grid;
  place-items: center;
  transition: background 0.14s ease;
}
.zoom-btn:hover { background: #e9eaec; }
.zoom-btn:active { transform: scale(0.94); }
.zoom-pct { width: 51px; font-size: 12px; }

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* The stage runs full-bleed under the floating style panel and centres its
     content on `50% + inset/2`, so the free half is what this has to fit in —
     a flat 440px overflowed the right edge on a narrow window. */
  max-width: min(440px, calc(100vw - var(--panel-inset) - 48px));
  max-height: 100%;
  min-height: 0;
}
/* ---------- empty stage: a few magnets already on the door ---------- */
/* The brushed panel they are stuck to, fading out rather than ending in a hard
   rectangle — a bordered box behind a box reads as a bug. A real element rather
   than a z-index:-1 pseudo: the overlay above it is its own stacking context,
   which a negative z-index would hide behind. */
.empty-panel {
  display: grid;
  place-items: center;
  /* An explicit width, not shrink-to-fit: the board inside asks for `100%` as
     its overflow guard, and against a shrink-to-fit parent that is circular —
     it resolved to nothing and the cluster collapsed to a dot. */
  width: 100%;
  padding: 8% 12%;
  margin-bottom: 14px;
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.55) 0 1px, rgba(20, 24, 32, 0.03) 1px 3px),
    linear-gradient(160deg, #eceef1, #dcdfe3 58%, #cdd1d6);
  -webkit-mask-image: radial-gradient(58% 58% at 50% 50%, #000 44%, rgba(0, 0, 0, 0) 92%);
  mask-image: radial-gradient(58% 58% at 50% 50%, #000 44%, rgba(0, 0, 0, 0) 92%);
}
/* Height-driven so a short viewport shrinks the cluster instead of clipping it:
   the overlay is centred inside a fixed-height stage with overflow hidden. The
   magnets are placed in % of this box, so they scale together. */
.empty-board {
  position: relative;
  width: min(330px, 38vh, 100%);
  /* aspect, not a second min(): the magnets are placed in % of this box, so
     width and height have to shrink together or the arrangement shears */
  aspect-ratio: 33 / 25;
  max-height: 29vh;
}
/* Each one is a real die-cut: white rim (the padded background), the cover
   clipped inside it, and the same offset contact shadow the 3D door casts. */
.empty-mag {
  position: absolute;
  border-radius: 14px;
  background-color: #fff;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  background-clip: content-box;
  padding: 5px;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.9) inset,
    0 10px 20px rgba(24, 28, 36, 0.22),
    0 2px 5px rgba(24, 28, 36, 0.14);
  animation: empty-bob 6s ease-in-out infinite;
}
/* A deliberately casual arrangement: overlapping, none of them square to the
   door. Perfectly aligned magnets look like a product grid, not a fridge. */
.empty-mag.m1 { left: 2%;  top: 16%; width: 40%; height: 53%; transform: rotate(-6deg); z-index: 3; }
.empty-mag.m2 { left: 36%; top: 0;   width: 36%; height: 47%; transform: rotate(4deg);  z-index: 2; animation-delay: -1.1s; }
.empty-mag.m3 { left: 33%; top: 42%; width: 39%; height: 52%; transform: rotate(-3deg); z-index: 4; animation-delay: -2.4s; }
.empty-mag.m4 { left: 68%; top: 12%; width: 31%; height: 41%; transform: rotate(7deg);  z-index: 1; animation-delay: -3.6s; }
.empty-mag.m5 { left: 70%; top: 55%; width: 28%; height: 37%; transform: rotate(-9deg); z-index: 2; animation-delay: -4.8s; }
/* The bob has to carry each magnet's own rotation through, or the keyframes
   would flatten them all to 0deg the moment the animation starts. */
@keyframes empty-bob {
  50% { translate: 0 -5px; }
}
.empty-title {
  font-size: clamp(20px, 2.4vh + 12px, 25px);
  font-weight: 650;
  letter-spacing: 0.1px;
  margin: 0 0 8px;
}
.empty-sub { color: var(--muted); margin: 0; line-height: 1.65; font-size: 13.5px; }

/* ---------- generating: the die-cut, happening ---------- */
/* Their photo inside a white rim, with a red cut line marching around the edge.
   Two CSS animations and one <rect> — no rAF loop, so a tab left asleep for a
   two-minute generation wakes up with nothing to catch up on. */
.gen-state { text-align: center; }
.gen-magnet {
  position: relative;
  width: min(220px, 25vh, 100%);
  aspect-ratio: 1 / 1;
  margin: 0 auto 26px;
  padding: 11px; /* the white rim */
  border-radius: 20px;
  background: #fff;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.9) inset,
    0 14px 28px rgba(24, 28, 36, 0.22),
    0 2px 6px rgba(24, 28, 36, 0.14);
  animation: gen-float 3.4s ease-in-out infinite;
}
@keyframes gen-float {
  50% { transform: translateY(-6px); }
}
.gen-magnet .gen-photo,
.gen-magnet .gen-blank {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 11px;
  object-fit: cover;
}
/* No photo (a text-only run): a shimmering blank in the rim rather than an
   empty white square, so the wait still reads as work in progress. */
.gen-blank {
  background: linear-gradient(100deg, #e7e9ec 20%, #f5f6f8 38%, #e7e9ec 60%);
  background-size: 300% 100%;
  animation: skeleton-sweep 1.4s linear infinite;
}
/* The print pass: one soft highlight travelling across the face. */
.gen-sheen {
  position: absolute;
  inset: 11px;
  border-radius: 11px;
  pointer-events: none;
  background: linear-gradient(105deg, rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, 0.78) 50%, rgba(255, 255, 255, 0) 62%);
  background-size: 280% 100%;
  animation: gen-sheen-move 2.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
@keyframes gen-sheen-move {
  0% { background-position: 150% 0; }
  60%, 100% { background-position: -70% 0; }
}
/* The cut line rides just outside the rim, where the real one does. */
.gen-cut {
  position: absolute;
  inset: -5px;
  pointer-events: none;
}
.gen-cut svg { width: 100%; height: 100%; display: block; overflow: visible; }
.gen-cut-path {
  fill: none;
  stroke: #ff3b30;
  stroke-width: 1.4;
  stroke-linecap: round;
  /* marching ants: the dash pattern slides by exactly one period, so the loop
     is seamless however long the wait runs */
  stroke-dasharray: 7 6;
  animation: gen-ants 0.9s linear infinite;
  vector-effect: non-scaling-stroke;
}
@keyframes gen-ants {
  to { stroke-dashoffset: -13; }
}
@media (prefers-reduced-motion: reduce) {
  .empty-mag, .gen-magnet, .gen-sheen, .gen-blank, .gen-cut-path { animation: none; }
}
.gen-title { font-size: 18px; font-weight: 700; margin: 0 0 6px; }
.gen-sub { color: var(--muted); margin: 0; }

.btn-again {
  background: var(--text);
  color: #fff;
  border-radius: 999px;
  padding: 8px 16px;
  font-weight: 600;
  font-size: 13px;
}
.btn-again:hover { background: #343b4d; }

/* failed */
.fail-state { text-align: center; max-width: 420px; }
.fail-icon { font-size: 40px; margin-bottom: 14px; }
.fail-title { font-size: 17px; font-weight: 700; margin: 0 0 8px; }
.fail-sub { color: var(--muted); margin: 0 0 18px; line-height: 1.6; }

/* ---------- modal ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(31, 36, 48, 0.45);
  display: grid;
  place-items: center;
  z-index: 60;
  animation: fade-in 0.18s ease;
}
@keyframes fade-in { from { opacity: 0; } }

/* ══════════════════════════════════════════════════════════════════════
 * Generate Modal — Figma "生成器布局规范原型", node 226:656
 * (file Zn2c5eGQP7jHjd4DW6hriV). Same modal appears in the spec file
 * b2y2t6TeE2vTSuta75kS1Z at 150:18033.
 * ══════════════════════════════════════════════════════════════════════ */
.modal-card {
  width: min(480px, calc(100vw - 48px));
  max-height: calc(100vh - 64px);
  background: var(--bg-default);
  border-radius: var(--r-xl);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.28);
  animation: pop-in 0.2s ease;
  /* Head, divider and foot stay pinned; only .modal-body scrolls, so the
     scrollbar never runs behind the close or Generate button. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
@keyframes pop-in { from { transform: scale(0.96); opacity: 0; } }

/* Header (226:657). The dark fill is one radial gradient, authored in the
   design as r=10 scaled by matrix(48, 40.517) at (86.4, 18.638) over the
   480×94 header — i.e. a 480×405px ellipse centred at 18%/19.8%, which is
   why the glow sits behind the thumbnail and falls off to the right. */
.modal-head {
  flex: none;
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: var(--sp-200) var(--sp-150) var(--sp-150) var(--sp-200);
  background: radial-gradient(100% 431% at 18% 19.8%, #45454e 0%, #303038 36%, #1c1c21 72%);
}
.head-thumb {
  width: 66px;
  aspect-ratio: 1 / 1;
  flex: none;
  /* 226:658 sets a loose 10px here — no radius token matches it */
  border-radius: 10px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
}
.head-thumb .style-cover,
.head-thumb .style-cover img { width: 100%; height: 100%; }
/* contain, not cover: a cover that ever ships at another ratio letterboxes
   instead of losing the edges of the card */
.head-thumb .style-cover img { border-radius: 0; object-fit: contain; }
.head-thumb .card-thumb { width: 100%; }
.head-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-100);
}
.modal-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 22px;
  color: var(--t-inverse);
}
/* Tags (226:662): output ratio + what the style asks of the user */
.head-tags { display: flex; flex-wrap: wrap; gap: var(--sp-75); }
.head-tag {
  height: 20px;
  display: inline-flex;
  align-items: center;
  padding: 2px var(--sp-75);
  border-radius: var(--r-sm);
  background: rgba(231, 233, 237, 0.5);
  font-size: 10px;
  font-weight: 500;
  line-height: 14px;
  color: var(--t-inverse);
  white-space: nowrap;
}
/* GhostButton (226:665): 24px hit area, 18px glyph */
.pp-app .modal-close {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  background: none;
  color: var(--t-inverse);
  transition: background 0.15s ease;
}
.pp-app .modal-close:hover { background: rgba(255, 255, 255, 0.14); }
.modal-close svg { display: block; }

/* Content (226:666) — the only scrolling region. A slim inset pill scrollbar
   (visible when an external mouse forces classic scrollbars); the 3px
   transparent border makes the thumb read as a 6px pill floating inside the
   body rather than a bar glued to the edge. */
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-150);
  padding: var(--sp-150) var(--sp-200) var(--sp-200);
  scrollbar-width: thin; /* Firefox */
  scrollbar-color: #d4d4d9 transparent;
}
.modal-body::-webkit-scrollbar { width: 12px; }
.modal-body::-webkit-scrollbar-track { background: transparent; }
.modal-body::-webkit-scrollbar-thumb {
  background: #d4d4d9;
  border-radius: 999px;
  border: 3px solid transparent;
  background-clip: padding-box;
}
.modal-body::-webkit-scrollbar-thumb:hover { background-color: #bcbcc4; background-clip: padding-box; }

/* Upload Area (226:669). `.pp-app` prefix needed: the dropzone is a
   <button>, so the generic `.pp-app button` reset would strip the dashed
   border and the fill. */
.pp-app .dropzone {
  width: 100%;
  height: 115px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-50);
  border: 1px dashed var(--divider);
  border-radius: var(--r-lg);
  background: var(--bg-surface);
  padding: var(--sp-150);
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.pp-app .dropzone:hover,
.pp-app .dropzone.is-drag {
  border-color: var(--t-tertiary);
  background: var(--bg-control);
}
.dropzone .dz-icon {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  color: var(--t-primary);
}
.dropzone .dz-icon svg { display: block; transition: transform 0.15s ease; }
.dropzone:hover .dz-icon svg, .dropzone.is-drag .dz-icon svg { transform: translateY(-2px); }
.dropzone .dz-main {
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-primary);
}
.dropzone .dz-sub {
  font-size: 11px;
  font-weight: 400;
  line-height: 16px;
  color: var(--t-tertiary);
}

/* Filled state of the Upload Area: not drawn in the spec, but it takes the
   same slot, so it keeps the Area's box (115px, r8, surface fill) and its
   buttons match the footer's Cancel. */
.upload-preview {
  display: flex;
  gap: var(--sp-150);
  align-items: center;
  height: 115px;
  border: 1px solid var(--divider);
  border-radius: var(--r-lg);
  padding: var(--sp-150);
  background: var(--bg-surface);
}
.upload-frame {
  width: 91px;
  height: 91px;
  flex: none;
  border-radius: var(--r-md);
  overflow: hidden;
  border: 1px solid var(--divider);
  background:
    conic-gradient(#eee 0 25%, #fff 0 50%, #eee 0 75%, #fff 0) 0 0 / 16px 16px;
}
.upload-frame img { width: 100%; height: 100%; object-fit: cover; }
.upload-actions { display: flex; flex-direction: column; gap: var(--sp-150); min-width: 0; }
.upload-filename {
  font-size: 11px;
  font-weight: 400;
  line-height: 16px;
  color: var(--t-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 260px;
}
.upload-btns { display: flex; gap: var(--sp-100); }
.pp-app .btn-ghost {
  height: 32px;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--stroke);
  border-radius: var(--r-md);
  padding: var(--sp-75) var(--sp-150);
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--t-secondary);
  background: var(--bg-default);
}
.btn-ghost:hover { background: var(--bg-hover); }
.btn-ghost.is-danger:hover { background: #fdeeee; border-color: #efc7c5; color: var(--danger); }

/* label + control pair; Content spaces the pairs 12px apart */
.field-group { display: flex; flex-direction: column; gap: var(--sp-100); }
/* Label (226:668 / 226:674): 12-medium, quiet. The design uses a loose
   #8a8a93 here; mapped to the token it rounds to, color/text/label. */
.modal-label {
  display: flex;
  align-items: center;
  gap: var(--sp-50);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-label);
}
.modal-label .opt { font-weight: 400; }
.prompt-input {
  width: 100%;
  height: 80px;
  border: none;
  border-radius: var(--r-lg);
  padding: 10px var(--sp-150);
  font: inherit;
  font-size: 12px;
  line-height: 16px;
  resize: vertical;
  background: var(--bg-control);
  color: var(--t-primary);
}
.prompt-input::placeholder { color: var(--t-tertiary); }
.prompt-input:focus { outline: 2px solid var(--stroke); outline-offset: -1px; }

/* magic wand inside the description textarea: taps in a ready-made example */
.prompt-wrap { position: relative; }
.prompt-wrap .prompt-input { padding-right: 42px; }
.pp-app .wand-btn {
  position: absolute;
  right: 8px;
  top: 8px;
  width: 24px;
  height: 24px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  font-size: 13px;
  background: var(--bg-default);
  transition: transform 0.12s ease, background 0.12s ease;
}
.pp-app .wand-btn:hover { background: var(--bg-hover); transform: rotate(-8deg) scale(1.08); }
.pp-app .wand-btn:active { transform: scale(0.92); }

/* guided slot fields: labeled inputs with example placeholders */
.slot-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 12px;
}
.slot-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.slot-label { font-size: 12px; font-weight: 500; line-height: 16px; color: var(--t-label); }
/* quieter "(optional)" suffix on every optional field label */
.slot-opt { font-weight: 400; }
.req-star { color: var(--accent-deep); font-weight: 700; }
.pp-app .slot-input {
  width: 100%;
  height: 32px;
  border: none;
  border-radius: var(--r-lg);
  padding: 0 var(--sp-150);
  font: inherit;
  font-size: 12px;
  line-height: 16px;
  background: var(--bg-control);
  color: var(--t-primary);
}
.pp-app .slot-input::placeholder { color: var(--t-tertiary); }
.pp-app .slot-input:focus { outline: 2px solid var(--stroke); outline-offset: -1px; }
/* a lone primary field spans the full width instead of sitting half-empty */
.slot-grid .slot-field:only-child { grid-column: 1 / -1; }
/* …same for a field left alone on its row after a full-row chips field */
.slot-grid .slot-field.slot-chips + .slot-field:last-child { grid-column: 1 / -1; }
/* chip fields (e.g. Tarot theme) always take the full row */
.slot-field.slot-chips { grid-column: 1 / -1; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
.pp-app .chip {
  border: none;
  border-radius: var(--r-circle);
  padding: var(--sp-75) var(--sp-150);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-primary);
  background: var(--bg-control);
  transition: background 0.12s ease, color 0.12s ease;
}
/* two-part chip labels ("Love · The Lovers"): meaning leads, card name recedes */
.chip .chip-sub { font-weight: 400; color: var(--t-tertiary); }
.pp-app .chip.is-on .chip-sub { color: rgba(255, 255, 255, 0.85); }
.pp-app .chip:hover { background: var(--bg-hover); }
.pp-app .chip.is-on {
  background: var(--brand);
  color: var(--t-inverse);
}
/* inline input revealed by the "Custom ✏️" chip */
.pp-app .chip-custom-input { margin-top: 8px; height: 36px; }

/* Frame shape (外框形状): preset chips FILL the text box below on tap (回填).
   The chips sit above the box so the eye reads "pick one, or write your own". */
/* The shape row is the same control as a chip slot now — its own classes went
   with the text box that used to sit open beneath it. */

/* "More details" disclosure keeps the few advanced slot fields out of the way */
.pp-app .adv-toggle {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-50);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-label);
}
.pp-app .adv-toggle:hover { color: var(--t-primary); }
.adv-toggle .adv-caret { font-size: 10px; }
.adv-section[hidden] { display: none; }

/* Divider (226:678) + Footer (226:679): note on the left, actions right */
.modal-divider { flex: none; height: 1px; background: #e3e3e8; }
.modal-foot {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-100);
  padding: 13px 18px;
}
.foot-note {
  flex: 1;
  min-width: 0;
  font-size: 11px;
  font-weight: 400;
  line-height: 16px;
  color: var(--t-tertiary);
}
.foot-actions { flex: none; display: flex; gap: var(--sp-100); }
/* `.pp-app` prefix needed on these buttons: the generic `.pp-app button`
   reset (higher specificity) would otherwise strip their backgrounds */
.pp-app .btn-cancel {
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-75) var(--sp-150);
  border: 1px solid var(--stroke);
  border-radius: var(--r-md);
  background: var(--bg-default);
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--t-secondary);
  transition: background 0.15s ease;
}
.pp-app .btn-cancel:hover { background: var(--bg-hover); }
.pp-app .btn-generate {
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-100);
  padding: 0 var(--sp-200);
  border-radius: var(--r-lg);
  background: var(--brand);
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  color: var(--t-inverse);
  transition: filter 0.15s ease;
}
/* the price rides inside the button (226:686) */
.btn-credits {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-50);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
}
.btn-credits:empty { display: none; }
.credits-icon { display: block; flex: 0 0 auto; }
.btn-generate:hover:not(:disabled) { filter: brightness(1.25); }
.btn-generate:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
.btn-generate:active:not(:disabled) { transform: scale(0.98); }

/* Free-quota pill — a copy of the shell's export hint so generation and export
   read the same. Source: generator-workbench `.export-credits-hint` /
   `.export-credits-hint-free-value`. Change both together. */
.free-hint {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  margin-left: 10px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  color: #ff7c23;
  white-space: nowrap;
}
.free-hint-value {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  height: 16px;
  padding: 2px 4px;
  border-radius: 4px;
  background: linear-gradient(90deg, #fce1bf, #ecb678 67.02%, #f9c991);
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  color: #581e06;
}
.restyle-head .free-hint { margin-left: 0; }
/* …and inside the dark Generate button, where the peach pill would vanish */
.btn-credits-free {
  display: inline-flex;
  align-items: center;
  height: 18px;
  padding: 0 6px;
  border-radius: 4px;
  background: linear-gradient(90deg, #fce1bf, #ecb678 67.02%, #f9c991);
  font-size: 11px;
  font-weight: 600;
  color: #581e06;
}

/* The overlay is mounted inside the runtime root, not the page: `absolute`
   keeps it inside the generator's own box (the shell owns the rest of the
   window), which is the one deviation from the spec's `fixed`. */
.pp-app .modal-overlay { position: absolute; z-index: 30; }

/* Tagline under the title — the one line that says what the style looks like
   before the cover has finished decoding. */
.modal-tagline {
  font-size: 12px;
  line-height: 16px;
  color: rgba(255, 255, 255, 0.72);
}

/* The header thumb is a square cover on a square tile — `cover` rather than the
   spec's `contain`, so it fills the tile instead of leaving grey margins. */
.head-thumb .style-cover img { object-fit: cover; }

/* The close glyph is text here, not an icon component. */
.pp-app .modal-close { font-size: 13px; line-height: 1; }

.btn-generate.is-busy { opacity: 0.6; cursor: progress; }

/* ---------- embed panel ---------- */
.embed-panel { padding: 16px; background: var(--surface); }
.embed-panel .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.embed-panel label { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.embed-panel select, .embed-panel input {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 10px;
  font: inherit;
  font-size: 13px;
  background: #fdfbf4;
  color: var(--text);
}

/* ---------- toast ---------- */
.toast-root {
  position: fixed;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  pointer-events: none;
}
.toast {
  background: rgba(31, 36, 48, 0.92);
  color: #fff;
  border-radius: 12px;
  padding: 10px 18px;
  font-size: 13px;
  max-width: 420px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  animation: toast-in 0.22s ease;
}
@keyframes toast-in { from { transform: translateY(8px); opacity: 0; } }

/* ------- 3D fridge door: the default presentation, inside the stage ------- */
.fridge-layer {
  position: absolute;
  inset: 0;
  display: none;
  background: #dcdde0;
}
.fridge-layer.is-on { display: block; }
.fridge-layer::after {
  /* kitchen falloff: a cool daylight pool over the magnet, dropping off to a
     soft vignette so the steel does not read as flat grey card */
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* centred on the magnet, which the camera pushes right of the layer centre to
     clear the floating panel — a vignette centred on the layer would light the
     door behind the panel instead */
  background:
    radial-gradient(120% 85% at calc(50% + var(--panel-inset) / 2) 32%, rgba(255, 253, 245, 0.16), rgba(255, 253, 245, 0) 46%),
    radial-gradient(115% 95% at calc(50% + var(--panel-inset) / 2) 40%, rgba(16, 20, 28, 0) 52%, rgba(16, 20, 28, 0.26) 100%);
}
.fridge-canvas {
  position: absolute;
  inset: 0;
  cursor: grab;
}
.fridge-canvas:active { cursor: grabbing; }
.fridge-canvas canvas { display: block; width: 100%; height: 100%; }
.fridge-status {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  align-content: center;
  gap: 10px;
  text-align: center;
}
.fridge-status.is-on { display: grid; }
/* shown only while the finished magnet's bitmap decodes */
.fridge-sticking {
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  display: none;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 600;
  color: #4e5158;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 6px 18px rgba(20, 24, 32, 0.18);
  backdrop-filter: blur(6px);
}
.fridge-sticking.is-on { display: inline-flex; }
.fridge-sticking .spinner { width: 15px; height: 15px; border-width: 2px; margin: 0; }
.fridge-status-text { color: #63656b; font-size: 13px; }

/* the magnet's wording, as a caption over the door */
.fridge-caption {
  position: absolute;
  left: 24px;
  bottom: 24px;
  display: none;
  color: #fff;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}
.fridge-caption.is-on { display: block; }
.caption-name {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}
.caption-style { font-size: 12.5px; opacity: 0.85; margin-top: 2px; letter-spacing: 0.6px; }

/* 2D / 3D switch + print-size chips, top centre of the stage */
/* Segmented control, top centre (spec node 71:6424): 32 high, p2, r6,
   active segment is white with a drop shadow. */
.view-tabs {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 2px;
  border-radius: var(--r-md);
  /* Opaque rather than the spec's 6% tint: this control floats over the 3D
     door, where a near-transparent track left the inactive label as grey text
     on grey steel. */
  background: rgba(255, 255, 255, 0.78);
  backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(20, 24, 32, 0.12);
}
.view-tab {
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-75) var(--sp-100);
  min-width: 80px;
  border-radius: 5px;
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--t-tertiary);
  white-space: nowrap;
  transition: color 0.14s ease;
}
.view-tab:hover { color: var(--t-secondary); }
.view-tab.is-on {
  background: var(--bg-segmented-active);
  color: var(--t-primary);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.fridge-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid var(--border);
  box-shadow: 0 4px 14px rgba(64, 52, 20, 0.14);
  backdrop-filter: blur(6px);
}
.fridge-chip {
  border-radius: 999px;
  padding: 6px 11px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}
.fridge-chip:hover { background: #f6f2e8; }
.fridge-chip.is-on {
  color: #fff;
  background: linear-gradient(135deg, #e0b04a, var(--accent-deep));
}
.fridge-chip.is-toggle { border: 1px dashed #e0d3aa; }
.fridge-chip.is-toggle.is-on { border-color: transparent; }

.ui-icon { display: inline-flex; line-height: 0; }
.cat-icon { color: var(--accent-deep); }
.bar-icon { display: inline-flex; color: var(--accent-deep); }
.surprise-bar .dice { display: inline-flex; color: var(--accent-deep); }

/* credits mark — one presentation everywhere a price appears */
.credit-icon { display: inline-flex; line-height: 0; }
.credit-cost {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
}
.credit-amount { letter-spacing: 0.2px; }
.credit-note { font-weight: 600; color: var(--muted); }
.btn-generate .credit-cost { color: #fff; font-size: 13px; }
.btn-generate .credit-note { color: rgba(255, 255, 255, 0.8); }
.btn-generate { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }

/* spinner */
.spinner {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2.5px solid #e3dcc6;
  border-top-color: var(--accent);
  animation: spin 0.8s linear infinite;
  margin: 0 auto;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* My history: skeleton cards instead of a lone spinner — the grid keeps its
   shape while the first page loads, so nothing jumps when the cards arrive. */
.history-skeleton {
  aspect-ratio: 1 / 1;
  border-radius: 14px;
  background: linear-gradient(100deg, #e7e9ec 20%, #f5f6f8 38%, #e7e9ec 60%);
  background-size: 300% 100%;
  animation: skeleton-sweep 1.3s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .style-cover.is-loading, .history-skeleton { animation: none; }
}

@media (max-width: 900px) {
  .side-panel { width: 280px; }
  .pp-app { --panel-inset: 316px; } /* 18 + 280 + 18 */
}

@media (prefers-reduced-motion: reduce) {
  .pp-app *, .pp-app *::before, .pp-app *::after { animation: none !important; transition: none !important; }
}

/* ---------- host shell overrides ---------- */
/* The Export control belongs to generator-workbench and the shell exposes no
   size option for it (only exportEnabled / exportRequiresResult / exportSvg…).
   It renders inside the workbench's shadow root — and so does this stylesheet,
   which is why the override has to live here and not in document.head.
   Shell markup: div.sidebar-footer-export.sidebar-footer-floating >
   span.sidebar-export-trigger-btn-wrap > button.sidebar-export-trigger-btn
   (utility classes set its default 236x32). !important because those utilities
   are injected at runtime and would otherwise win on order.
   TO RESIZE: change the two numbers below. */
/* Export Footer (spec node 150:18599, "无侧边栏情况" variant): a 169-wide block
   pinned bottom-right, p16 / gap 12, so both buttons come out 137 wide, 32 high
   and sit 16 from the viewport edges.
   TO RESIZE: change --footer-btn-w; every rule below is derived from it. */
.sidebar-footer-floating {
  --footer-btn-w: 137px;
  /* The container shrink-wraps its content, so the button width and the
     container width have to agree or the shell's `right` offset stops
     describing where the button actually lands. */
  right: 16px !important;
  bottom: 16px !important;
  width: var(--footer-btn-w) !important;
  /* The shell adds its own bottom padding, which would stack on top of the
     16px offset and push the block off the spec's grid. */
  padding: 0 !important;
  /* No `gap`: the shell appends a zero-height tooltip wrapper after Export, and
     gap hands that empty slot a full 12px, lifting Export 12px off the spec's
     baseline. Spacing rides on Buy Materials' own margin instead, so whatever
     the shell appends contributes nothing. */
  gap: 0 !important;
  justify-content: flex-end !important;
}
.sidebar-footer-floating .sidebar-export-trigger-btn-wrap {
  width: var(--footer-btn-w) !important;
}
.sidebar-footer-floating .sidebar-export-trigger-btn {
  width: var(--footer-btn-w) !important;
  min-width: var(--footer-btn-w) !important;
  height: 32px !important;
  padding: 6px 12px !important;
  gap: 6px !important;
  border-radius: 6px !important;
  font-size: 14px !important;
  font-weight: 500 !important;
  line-height: 20px !important;
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
}
/* The dropdown (Download / Open in Studio) is hard-coded to 268px by the shell
   and anchored topLeft on the trigger. It cannot just inherit the trigger's
   137px — "Open in Studio" wraps at that width — so it gets its own width and
   is pulled left by the difference to keep the two right edges flush.
   The two literals below are one measurement: 137 is --footer-btn-w. */
.sidebar-export-dropdown-menu {
  width: 180px !important;
  min-width: 180px !important;
  margin-left: calc(137px - 180px) !important;
}

/* ---------- Buy Materials (consumables entry) ---------- */
/* Button node 150:18602 of the same Export Footer: color/warning/default with a
   color/warning/disabled hairline, 32 high, r6, 14/20 medium, 16px cart. Sits
   directly above Export so the two read as one stack; `order: -1` keeps it
   there even if the shell re-renders and re-appends its own children after
   ours. Lives in this sheet because it is injected into the workbench shadow
   root, where the shell footer is. No dark rule: this generator has no dark
   theme. */
.sidebar-footer-floating .buy-materials {
  order: -1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: var(--footer-btn-w);
  min-width: var(--footer-btn-w);
  height: 32px;
  /* the block's 12px inner gap — see the `gap: 0` note on the footer */
  margin-bottom: 12px;
  padding: 6px 12px;
  border: 1px solid rgba(240, 106, 0, 0.4);
  border-radius: 6px;
  background: #ff6500;
  color: #ffffff;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  white-space: nowrap;
  cursor: pointer;
  flex: 0 0 auto;
  transition: background-color 0.15s ease;
}
.sidebar-footer-floating .buy-materials svg {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}
.sidebar-footer-floating .buy-materials:hover { background: #e85c00; }
.sidebar-footer-floating .buy-materials:active { transform: translateY(1px); }
