.token-gate {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.token-gate__form {
  max-width: 360px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.token-gate__form h1 {
  font-family: var(--font-display);
  font-weight: 400;
  margin: 0;
}

.token-gate__hint {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1.5;
}

.token-gate__error {
  color: #f28b82;
  font-size: 0.85rem;
  margin: 0;
}

.token-gate__form input {
  padding: 0.75rem 1rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
}

.token-gate__form button {
  padding: 0.75rem 1rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: #1a1408;
  font-weight: 600;
}

.editor-app {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Scrim over a PHOTO site background (rendered behind the editor) so the
   editor chrome stays legible on top of arbitrary bright pixels. Only
   rendered for an image background — see EditorApp.js. It has to be its own
   fixed layer rather than a background on .editor-app: a negative-z child
   paints ABOVE its parent's background, so a parent background would have
   been hidden by the background layer instead of covering it. Same z-index
   as .background-layer-stack, later in DOM order, so it stacks on top of it
   and below all editor content. */
.editor-bg-scrim {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: rgba(13, 13, 18, 0.72);
}

.editor-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--color-border);
}

/* ⚙ / 🗄 moved out of the settings row and into the top bar, where the design
   puts them: they are app-level destinations, not part of how the site looks.
   44px so they are a real touch target. */
.editor-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font-size: 1rem;
}

.editor-icon-btn:hover {
  background: var(--color-surface-strong);
}

.editor-topbar__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Nothing in the top bar may wrap. Once ⚙ and 🗄 joined it, 375px was tight
   enough that "回到網站", "尚未儲存" and even "儲存" each broke across two
   lines and the bar grew to three rows. */
.editor-back-link {
  color: var(--color-text-muted);
  font-size: 0.9rem;
  white-space: nowrap;
}

.editor-back-link:hover {
  color: var(--color-text);
}

.editor-dirty-badge {
  font-size: 0.75rem;
  color: var(--color-accent-strong);
  background: rgba(232, 179, 77, 0.12);
  padding: 0.25rem 0.6rem;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

.editor-status,
.editor-save-btn {
  white-space: nowrap;
}

/* On a narrow phone the badge is the first thing to go: the 儲存 button is
   right beside it and already says whether there is anything to save. */
@media (max-width: 430px) {
  .editor-dirty-badge {
    display: none;
  }

  .editor-back-link {
    font-size: 0.82rem;
  }
}

.editor-status {
  font-size: 0.75rem;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
}

.editor-status--error {
  color: #f28b82;
  background: rgba(242, 139, 130, 0.12);
}

.editor-status--saved {
  color: #8fd19e;
  background: rgba(143, 209, 158, 0.12);
}

.editor-save-btn {
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: #1a1408;
  font-weight: 600;
}

.editor-save-btn:disabled {
  opacity: 0.5;
}

/* Storage guard (functions/_lib/quota.js). The banner stays up for as long as
   the lock does — the popup fires once on the transition, and without a
   standing reminder a user who dismissed it is left with silently dead upload
   buttons and no stated reason. */
.quota-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  background: rgba(242, 139, 130, 0.14);
  border-bottom: 1px solid rgba(242, 139, 130, 0.4);
  color: #f28b82;
  font-size: 0.78rem;
  line-height: var(--line-body);
}

.quota-banner button {
  flex-shrink: 0;
  padding: 0.35rem 0.8rem;
  border-radius: var(--radius-md);
  background: #f28b82;
  color: #2a0f0d;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}

.quota-sheet__body {
  margin: 0;
  font-size: 0.85rem;
  line-height: var(--line-body);
  color: var(--color-text);
}

.quota-sheet__body--muted {
  color: var(--color-text-muted);
  font-size: 0.78rem;
}

/* A disabled <label> file-picker gets no :disabled of its own — the state
   lives on the input inside it — so the look has to be driven by a class. */
.upload-picker.is-disabled,
.bg-image-picker.is-disabled,
.upload-primary-btn.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(1);
}

.upload-picker.is-disabled:hover,
.upload-primary-btn.is-disabled:hover {
  transform: none;
  background: var(--color-accent);
}

.editor-upload-status {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  margin: 0;
}

.editor-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Horizontal scrollable tab row — same pattern as the public viewer's
   project nav. A fixed-width left sidebar (the original design) ate ~40%
   of a phone screen's width before there was any room left for the actual
   editing surface; this works at any viewport width. */
.editor-projects {
  display: flex;
  flex-direction: row;
  overflow-x: auto;
  border-bottom: 1px solid var(--color-border);
  padding: 0.75rem 1rem;
  gap: 0.5rem;
  flex-shrink: 0;
}

.editor-projects__item {
  flex-shrink: 0;
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.editor-projects__item.is-active {
  background: var(--color-surface-strong);
  color: var(--color-text);
}

.editor-canvas {
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  overflow-y: auto;
}

/* The tap target that replaces the row on a phone. It has to state what the
   settings currently are, or collapsing them just hides information. */
.editor-site-summary {
  display: none;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  min-height: 56px;
  padding: 0 1.25rem;
  border-bottom: 1px solid var(--color-border);
  text-align: left;
}

.editor-site-summary__dot {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border);
}

.editor-site-summary__label {
  font-size: 0.95rem;
  color: var(--color-text);
}

.editor-site-summary__state {
  flex: 1;
  text-align: right;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.editor-site-summary__chev {
  font-size: 0.6rem;
  color: var(--color-text-muted);
}

.editor-site-settings {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--color-border);
}

.editor-site-settings__label {
  white-space: nowrap;
}

/* Phone: the row becomes a sheet under its own summary. white-space:nowrap on
   every label is the actual fix for the character-tower wrapping — a flex item
   allowed to shrink below its content width breaks CJK between any two
   characters, and 網站背景(全站一致) became a single-character column. */
@media (max-width: 900px) {
  .editor-site-summary {
    display: flex;
  }

  .editor-site-settings {
    display: none;
    flex-wrap: wrap;
    gap: 0.9rem 0.75rem;
    padding: 1rem 1.25rem 1.25rem;
  }

  .editor-site-settings.is-open {
    display: flex;
  }

  .editor-site-settings__label {
    width: 100%;
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
  }

  .editor-site-settings .bg-swatch {
    width: 40px;
    height: 40px;
  }

  .editor-site-settings > .color-wheel-picker,
  .editor-site-settings > .bg-image-picker,
  .editor-site-settings > .speed-slider {
    flex: 0 0 auto;
    white-space: nowrap;
  }

  .editor-site-settings > .speed-slider {
    width: 100%;
  }

  .editor-site-settings > .speed-slider input[type="range"] {
    flex: 1;
  }
}

.bg-swatch {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  border: 2px solid transparent;
}

.bg-swatch.is-active {
  border-color: var(--color-accent);
}

/* The custom-colour entry point looks like a colour wheel, not another
   plain swatch — it used to be a dark circle sitting in the row of dark
   swatches, visually indistinguishable from them. */
.color-wheel-picker {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
  cursor: pointer;
  width: fit-content;
}

.color-wheel-picker__wheel {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: conic-gradient(#f44, #fb3, #ee5, #5d5, #4cd, #55e, #c5e, #f44);
  box-shadow: inset 0 0 0 3px rgba(13, 13, 18, 0.55), 0 0 0 1px var(--color-border);
  flex-shrink: 0;
}

.color-wheel-picker input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.bg-image-picker {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.7rem;
  border: 1px dashed var(--color-border);
  border-radius: 999px;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  cursor: pointer;
}

.bg-image-picker input {
  display: none;
}

/* Primary action treatment: the one thing most edits start with (putting a
   picture on the canvas) reads as THE button, not a faint dashed outline
   lost under a row of swatches. */
.upload-primary-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.9rem 1rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: #1a1408;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 150ms, transform 150ms var(--ease-out-expo);
}

.upload-primary-btn:hover {
  background: var(--color-accent-strong);
  transform: translateY(-1px);
}

/* On a light site background --color-accent-strong is a dark gold (it is text
   there), so this fill has to be named explicitly or the button would go dark
   under its own near-black label on hover. */
:root.is-light-bg .upload-primary-btn:hover {
  background: #f0c064;
}

.upload-primary-btn input {
  display: none;
}

.upload-primary-btn__icon {
  font-size: 1.1rem;
}

/* 圖層 tab: "新增圖片" (a file-picker label) and "新增文字" (a plain button)
   sit side by side, sharing the same pill styling as the single full-width
   upload button elsewhere. */
.layer-add-row {
  display: flex;
  gap: 0.5rem;
}

.layer-add-row .upload-primary-btn {
  flex: 1;
  padding: 0.75rem 0.5rem;
  font-size: 0.85rem;
}

.upload-primary-btn--btn {
  border: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.layer-edit-block {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding-top: 0.25rem;
}

.speed-slider {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.speed-slider input {
  width: 120px;
}

.speed-slider__value {
  min-width: 2.2em;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

/* Three action levels, not four identical dashed pills. Adding a page is the
   one thing the toolbar exists for, so it is the only filled control on the
   row; the cloud twin sits beside it as an outline; 套用範本 and 分割成跨頁
   are occasional, so they drop to plain text. Before this everything on the
   row had the same weight and nothing said where to start. */
.upload-picker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 48px;
  padding: 0 1.25rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: #1a1408;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  width: fit-content;
  transition: background 150ms, transform 150ms var(--ease-out-expo);
}

.upload-picker:hover {
  background: var(--color-accent-strong);
  transform: translateY(-1px);
}

:root.is-light-bg .upload-picker:hover {
  background: #f0c064;
}

.upload-picker--cloud {
  background: none;
  color: var(--color-text);
  font-weight: 400;
  border: 1px solid var(--color-border);
}

.upload-picker--cloud:hover {
  background: var(--color-surface);
  transform: none;
}

.upload-picker input {
  display: none;
}

.editor-toolbar-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.editor-split-label {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  line-height: var(--line-body);
}

/* Panel section header: a short accent-coloured title with the explanation on
   its own muted line underneath. The panel used to be one continuous scroll of
   same-weight text where a section title and its caveat were the same
   sentence, so nothing marked where one group of controls ended. */
.panel-section-title {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--color-accent-strong);
}

.panel-section-desc {
  font-size: 0.72rem;
  line-height: var(--line-body);
  color: var(--color-text-muted);
  margin-top: -0.35rem;
}

/* Tertiary: text only. These sit on the same row as the filled 從裝置上傳 and
   must not compete with it. */
.editor-split-btn {
  padding: 0.5rem 0.5rem;
  border-radius: var(--radius-md);
  background: none;
  color: var(--color-text-muted);
  font-size: 0.88rem;
}

.editor-split-btn:hover {
  background: var(--color-surface);
  color: var(--color-text);
}

.editor-strip-hint {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin: 0;
}

/* --- Editable strip: same horizontal layout as the published carousel,
   but with a persistent, draggable divider between items. The divider is
   editor-only chrome — Carousel.js in the public viewer has none. --- */

/* Fixed strip HEIGHT; each slide's width follows its own image's aspect
   (inline aspect-ratio), mirroring the published page — full image, no
   crop. EditableStrip.js measures real rendered widths at drag time. */
.editable-strip {
  --strip-h: 170px;
  display: flex;
  align-items: stretch;
  overflow-x: auto;
  /* Breathing room on all four sides. The thumbnails used to sit flush
     against the container edges, so the first and last page looked cut off
     rather than placed, and the selection ring had nowhere to draw. */
  padding: 0.35rem 0.25rem 0.75rem;
  scrollbar-width: none;
}

.editable-strip::-webkit-scrollbar {
  display: none;
}

.editable-slide-wrap {
  display: flex;
  align-items: stretch;
  flex-shrink: 0;
}

.editable-slide {
  width: auto;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  cursor: pointer;
  /* Not pan-y. That reserved horizontal touch gestures for a drag the slide
     never had, so on a phone swiping across the thumbnails did nothing at all
     and the strip could not be scrolled. Dragging is on the grip, which opts
     out of scrolling for itself. */
  touch-action: auto;
}

.editable-slide.is-dragging {
  cursor: grabbing;
  z-index: 5;
}

.editable-slide.is-dragging .editable-slide__media {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
}

/* The page's own drag handle. Reordering is a drag on the PAGE, so the grip
   belongs on the page — sitting in its corner, out of the picture's way, with
   its own touch-action so the strip still scrolls everywhere else. */
.editable-slide__grip {
  position: absolute;
  top: 0.4rem;
  /* Top-LEFT: the 🎬 影片 badge already owns the top-right corner and the two
     sat on top of each other. 編輯中 is bottom-left, so this corner is free. */
  left: 0.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-pill);
  background: rgba(13, 13, 18, 0.72);
  color: #fff;
  font-size: 0.72rem;
  letter-spacing: -1px;
  line-height: 1;
  cursor: grab;
  touch-action: none;
}

.editable-slide__grip:hover {
  background: var(--color-accent);
  color: #1a1408;
}

.editable-slide__grip:active {
  cursor: grabbing;
}

/* Deliberately a BLOCK box, exactly like the viewer's .media-frame. It was
   a flex container, which silently broke every cross-page span slice: as a
   flex item the deliberately over-sized <img> (width:200%/300% for a span)
   got min-width:auto shrink-to-fit and was pulled back to the container
   width, and justify-content:center then offset whatever survived by half a
   slide. The published page, being a block box, was correct — so spans only
   ever looked wrong in the editor. */
.editable-slide__media {
  position: relative;
  height: var(--strip-h);
  /* width follows the inline per-image aspect-ratio */
  background: var(--color-surface);
  overflow: hidden;
  border-radius: var(--radius-lg);
  /* Matches .media-frame's container-type in viewer.css so a text layer's
     font-size:Ncqw looks the same size here as it will published. */
  container-type: inline-size;
}

.editable-slide__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same free-positioned text layer the published page draws (see
   .text-overlay in viewer.css). Sized inline in cqw, true-to-scale against
   this thumbnail, but floored at 0.5rem: the size is user-adjustable now, so
   over-scaling it by a fixed factor misrepresented big sizes badly, while
   true scale alone would be ~4px and unreadable at the default.
   The side panel's larger preview is where precise positioning happens. */
.editable-slide__text {
  position: absolute;
  max-width: 85%;
  font-family: var(--font-display);
  font-size: 0.6rem; /* fallback only — the inline cqw value wins */
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7), 0 1px 6px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

.editable-slide__hidden-badge {
  font-size: 0.7rem;
  color: #f28b82;
  background: rgba(242, 139, 130, 0.12);
  border-radius: 4px;
  padding: 0.1rem 0.4rem;
  width: fit-content;
}

/* outline follows border-radius, so the ring hugs the rounded thumbnail
   instead of cutting its corners square. */
.editable-slide.is-selected .editable-slide__media {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* Which page the side panel is currently editing. The gold ring alone was
   easy to lose among several thumbnails on a phone, especially once a photo
   in the strip happened to be warm-coloured. */
.editable-slide__editing {
  position: absolute;
  left: 0.4rem;
  bottom: 0.4rem;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  color: #1a1408;
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  pointer-events: none;
}

.editable-slide__caption {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  width: 0;
  min-width: 100%;
}

/* The gutter between two pages. By default it is exactly that — a mark
   showing where one canvas ends and the next begins — and nothing is dragged
   on it. Reordering moved onto the page's own grip, because a line between
   two things does not read as a control for either of them.
   The one exception is --span below, where the line genuinely does mean
   something you can move. */
.editable-divider {
  position: relative;
  flex-shrink: 0;
  width: 26px;
  align-self: stretch;
}

.editable-divider--gap {
  pointer-events: none;
}

.editable-divider--span {
  width: 34px;
  cursor: ew-resize;
  touch-action: none;
}

.editable-divider__line {
  position: absolute;
  left: 50%;
  /* Only as tall as the thumbnail, so it marks the canvas gap rather than
     running down through the caption too. */
  top: 0;
  height: var(--strip-h);
  width: 1px;
  transform: translateX(-50%);
  /* A dashed editor guide, not a hairline. Its whole job is to say "this is
     where one canvas ends and the next begins", and at --color-border it was
     too faint on a dark background to say anything. */
  background: repeating-linear-gradient(
    to bottom, var(--color-text-muted) 0 3px, transparent 3px 8px
  );
  opacity: 0.55;
}

.editable-divider--span .editable-divider__line {
  width: 2px;
  background: #5bc8d6;
}

/* Only the span divider has one. */
.editable-divider__handle {
  position: absolute;
  left: 50%;
  /* Vertically centered on the fixed-height media box. */
  top: calc(var(--strip-h) / 2);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 38px;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  box-shadow: 0 0 0 4px var(--color-bg);
  transition: transform 150ms var(--ease-out-expo);
}

.editable-divider__handle::after {
  content: "⋮⋮";
  font-size: 11px;
  letter-spacing: -1px;
  line-height: 1;
  color: #1a1408;
}

.editable-divider--span:hover .editable-divider__handle {
  transform: translate(-50%, -50%) scale(1.12);
}

/* A span-boundary divider (adjusts cross-page image alignment) reads
   distinctly from a plain reorder divider — solid cyan line + diamond
   handle, vs. the dashed gold line + round handle used for reordering. */
.editable-divider--span .editable-divider__line {
  background: #5bc8d6;
}

.editable-divider--span .editable-divider__handle {
  background: #5bc8d6;
}

.editable-divider--span .editable-divider__handle::after {
  content: "↔";
  font-size: 12px;
  letter-spacing: 0;
  color: #0b2a2f;
}

/* A real right-side function panel: fixed overlay, slides in from the
   edge, works identically at any viewport width instead of only appearing
   as a third grid column on wide screens (where it used to silently fall
   below the fold on narrow ones). */
.span-split-row {
  display: flex;
  gap: 0.5rem;
}

.span-split-btn {
  flex: 1;
  min-height: 44px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  font-size: 0.9rem;
}

.span-split-btn:hover {
  background: var(--color-surface);
}

.span-split-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.span-split-btn--go {
  flex: 0 0 auto;
  padding: 0 1.1rem;
  background: var(--color-accent);
  border-color: transparent;
  color: #1a1408;
  font-weight: 600;
}

.span-split-btn--go:hover:not(:disabled) {
  background: var(--color-accent-strong);
}

:root.is-light-bg .span-split-btn--go:hover:not(:disabled) {
  background: #f0c064;
}

.span-split-custom {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0 0.8rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  font-size: 0.85rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.span-split-custom input {
  flex: 1;
  min-width: 0;
  width: 100%;
  background: none;
  border: none;
  color: var(--color-text);
  font: inherit;
  font-size: 1rem;
  text-align: center;
}

.span-split-custom input:focus {
  outline: none;
}

.editor-side-panel-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 19;
  animation: backdrop-fade-in 200ms ease-out;
}

/* Desktop: the panel DOCKS. As an overlay it covered the toolbar and the page
   strip on a 1440px screen for no reason, and its backdrop turned every click
   outside it into "clear the selection and close" — which is what made
   分割成跨頁 impossible to click even when it was visible. Padding the app
   rather than restructuring the DOM keeps the panel's fixed positioning and
   its slide-in for narrow screens intact. */
@media (min-width: 1024px) {
  .editor-side-panel-backdrop {
    display: none;
  }

  .editor-app {
    padding-right: 440px;
  }

  .editor-side-panel {
    width: 440px;
    animation: none;
    box-shadow: none;
  }
}

@keyframes backdrop-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.editor-side-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  /* Wider than a typical drawer on purpose: the preview inside it is the
     surface used for precise text/crop placement, and it renders text at
     true published scale (2.44cqw), so a narrow panel would make that text
     genuinely too small to aim with. */
  width: min(92vw, 440px);
  background: var(--color-bg);
  border-left: 1px solid var(--color-border);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  overflow-y: auto;
  z-index: 20;
  animation: panel-slide-in 260ms var(--ease-out-expo);
}

@keyframes panel-slide-in {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

.editor-side-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Lets Wayne switch which photo he's editing without touching the strip —
   on a phone the panel covers ~92% of the screen, so the strip behind it
   is effectively unreachable while the panel is open (see render()). */
/* B / I toggles for the 疊字 and text 圖層 typography controls. */
.type-toggle-row {
  display: flex;
  gap: 0.5rem;
}

.type-toggle {
  width: 2.4rem;
  height: 2.2rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 1rem;
  font-family: Georgia, serif;
}

.type-toggle.is-active {
  background: var(--color-surface-strong);
  color: var(--color-text);
  outline: 1px solid var(--color-accent);
}

.editor-side-panel__item-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.editor-side-panel__item-nav button {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 1.1rem;
  line-height: 1;
}

.editor-side-panel__item-nav button:disabled {
  opacity: 0.35;
}

.editor-side-panel__item-pos {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.editor-side-panel__tabs {
  display: flex;
  gap: 0.5rem;
}

.editor-side-panel__tabs button {
  flex: 1;
  padding: 0.5rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-muted);
}

.editor-side-panel__tabs button.is-active {
  background: var(--color-surface-strong);
  color: var(--color-text);
}

.editor-side-panel__section {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* One preview box shared by the 背景 / 濾鏡 / 文字 / 動畫 tabs. It used to be
   three different boxes with three different fidelities — notably the 濾鏡
   tab previewed the grade on a fixed magenta gradient rather than the photo,
   so Noir looked like grey and Golden Hour like magenta. All of them now
   render the item's real media through MediaLayer. */
.canvas-preview {
  container-type: inline-size;
  position: relative;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface);
  touch-action: none;
}

.canvas-preview.is-draggable {
  cursor: grab;
}

.canvas-preview__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

canvas.canvas-preview__media.depth-canvas {
  pointer-events: auto; /* depth parallax follows the mouse */
}

/* Layers are draggable in the editor's 圖層 preview (inert on the
   published page — see viewer.css .canvas-layer). */
.canvas-preview .canvas-layer {
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
}

.canvas-preview .canvas-layer.is-selected {
  outline: 2px dashed var(--color-accent);
  outline-offset: 2px;
}

/* Box-select ("完整框選") rectangle, drawn while dragging on the empty
   canvas background rather than on a layer. */
.layer-marquee {
  position: absolute;
  border: 1px dashed var(--color-accent);
  background: rgba(232, 179, 77, 0.12);
  pointer-events: none;
  z-index: 5;
}

/* Snap guide lines: shown only while a drag lands within the snap
   threshold of the frame centre or another layer's centre (see
   EditorSidePanel's handleLayerPointerMove). */
.layer-guide {
  position: absolute;
  background: #4fd1e0;
  pointer-events: none;
  z-index: 6;
}

.layer-guide--v {
  top: 0;
  bottom: 0;
  width: 1px;
}

.layer-guide--h {
  left: 0;
  right: 0;
  height: 1px;
}

.canvas-preview__hint {
  position: absolute;
  bottom: 0.5rem;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.7);
  background: rgba(0, 0, 0, 0.4);
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  pointer-events: none;
  max-width: 92%;
  text-align: center;
}

/* Sized as a percentage of the preview's own width (container query unit)
   rather than a fixed rem, so the text occupies the same fraction of the
   frame that it will on the published page — 1.1rem within a 720px slide is
   2.44%. A fixed 0.95rem made it ~1.9x too large here, so captions wrapped
   while positioning and published on one line somewhere else. */
.canvas-preview__text {
  position: absolute;
  max-width: 80%;
  font-family: var(--font-display);
  font-size: 2.44cqw;
  line-height: 1.3;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0, 0, 0, 0.35);
  cursor: grab;
  outline: 1px dashed rgba(255, 255, 255, 0.4);
  outline-offset: 2px;
}

/* The 動畫 tab wraps the whole preview in .anim-preview so the entrance can
   be replayed by remounting it. It has to fill the frame the way
   .frame-content does on the published page — as an auto-height block, the
   media's height:100% resolved against nothing and the preview no longer
   matched what the slide actually looks like. */
.anim-preview {
  position: absolute;
  inset: 0;
}

/* Same free positioning as the published 疊字, minus the drag affordances —
   this preview is for watching the animation, not for moving the text. */
.anim-preview__text {
  position: absolute;
  max-width: 80%;
  font-family: var(--font-display);
  line-height: 1.3;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

.editor-restore-btn {
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-accent-strong);
  font-size: 0.8rem;
}

.editor-remove-media-btn {
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.8rem;
  border: 1px solid var(--color-border);
  width: fit-content;
}

/* Danger zone pinned at the bottom of the side panel, visually separated
   from the editing tabs so 刪除 is never one stray tap away. */
.editor-side-panel__danger {
  margin-top: auto;
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.editor-side-panel__danger-hint {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  line-height: 1.5;
}

.editor-delete-btn {
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius-md);
  background: rgba(242, 139, 130, 0.12);
  color: #f28b82;
  font-size: 0.85rem;
}

.filter-preset-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem;
}

.filter-preset-btn {
  font-size: 0.75rem;
  padding: 0.4rem;
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text-muted);
}

.filter-preset-btn.is-active {
  background: var(--color-accent);
  color: #1a1408;
}

.filter-slider {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.filter-slider input {
  width: 100%;
}

.editor-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.editor-field input {
  padding: 0.5rem 0.65rem;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
}

.editor-checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
}

.transition-option {
  padding: 0.5rem;
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.8rem;
}

.transition-option.is-active {
  background: var(--color-accent);
  color: #1a1408;
}

/* Mobile-style long-press action sheet — press and hold a project tab
   (~1s) to rename it. Slides up from the bottom, like a native iOS/Android
   action sheet, rather than a desktop-style right-click menu. */
.editor-projects__item {
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

.context-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 29;
  animation: backdrop-fade-in 200ms ease-out;
}

.context-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  /* An opaque base with the surface tint composited on top, rather than the
     tint alone. --color-surface-strong is 10% white; used by itself it left
     the sheet 90% transparent, so the page underneath read straight through
     it — the "選一個作品集開始編輯" placeholder sits at the vertical centre and
     landed on top of the buttons. It looked like a stacking bug and was not:
     the sheet was above, it simply was not covering anything. The other two
     overlays (.cloudpick__box, .stor-preview__box) already paint on
     --color-bg; this one is now the same shape. */
  background:
    linear-gradient(var(--color-surface-strong), var(--color-surface-strong)),
    var(--color-bg);
  border-radius: 16px 16px 0 0;
  padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  animation: sheet-slide-up 220ms var(--ease-out-expo);
}

@keyframes sheet-slide-up {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

.context-sheet__title {
  text-align: center;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  padding: 0.5rem 0 0.25rem;
}

/* The AI 金鑰 sheet carries three providers' key+model fields — needs more
   room than a one-line rename/confirm sheet, and scrolls internally rather
   than overflowing the viewport on a short phone screen. */
.context-sheet--wide {
  max-height: 85vh;
  overflow-y: auto;
}

.context-sheet__hint {
  font-size: 0.8rem;
  color: var(--color-accent);
  background: rgba(232, 179, 77, 0.12);
  border-radius: var(--radius-md);
  padding: 0.6rem 0.75rem;
  text-align: center;
}

.ai-provider-block {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--color-border);
}

.ai-provider-choice {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.85rem;
  padding-bottom: 0.25rem;
}

.ai-provider-choice label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Real-wait progress for 元件識別: spinner + elapsed seconds + a hint that
   the free endpoint queues, so a 30-second wait reads as "working" instead
   of "broken". The matching error block renders IN the panel — the topbar
   error line is covered by this full-screen panel on phones. */
.parts-progress {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.7rem 0.8rem;
  border-radius: var(--radius-md);
  background: rgba(232, 179, 77, 0.1);
  border: 1px solid rgba(232, 179, 77, 0.35);
}

.parts-progress__spinner {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid rgba(232, 179, 77, 0.25);
  border-top-color: var(--color-accent);
  animation: parts-spin 0.9s linear infinite;
}

@keyframes parts-spin {
  to { transform: rotate(360deg); }
}

.parts-progress__text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.82rem;
}

.parts-progress__hint {
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

.parts-error {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem 0.8rem;
  border-radius: var(--radius-md);
  background: rgba(226, 72, 61, 0.12);
  border: 1px solid rgba(226, 72, 61, 0.4);
  font-size: 0.82rem;
}

.parts-error__actions {
  display: flex;
  gap: 0.5rem;
}

@media (prefers-reduced-motion: reduce) {
  .parts-progress__spinner {
    animation: none;
  }
}

/* --- 動畫 tab v2 layout (design/exports/5-側欄-動畫-v2.png) --- */
.editor-divider {
  width: 100%;
  height: 1px;
  background: var(--color-border);
}

.ai-tools-label {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: 0.06em;
}

/* One AI feature = one card: title + provider chip, one-line description,
   the user's own steering prompt, the action button, and the feature's
   progress/error blocks all live INSIDE it — statuses used to float loose in
   the tab and read as belonging to nothing. */
.ai-tool-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.9rem;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.045);
}

.ai-tool-card__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

.ai-tool-card__title {
  font-size: 0.95rem;
  font-weight: 600;
}

.ai-tool-card__chip {
  font-size: 0.65rem;
  color: var(--color-text-muted);
  background: rgba(255, 255, 255, 0.07);
  padding: 0.25rem 0.5rem;
  border-radius: 7px;
  white-space: nowrap;
}

.ai-tool-card__desc {
  margin: 0;
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

.ai-tool-card__input {
  width: 100%;
  padding: 0.65rem 0.7rem;
  border-radius: 10px;
  border: 1px solid var(--color-border);
  background: rgba(0, 0, 0, 0.35);
  color: var(--color-text);
  font-size: 0.8rem;
}

.ai-tool-card__actions {
  display: flex;
  gap: 0.5rem;
}

.ai-tool-card__actions .upload-primary-btn {
  flex: 1;
  padding: 0.7rem 0.5rem;
  font-size: 0.85rem;
}

.ai-tool-card__info {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* 進場動畫: label + pill row, per the v2 board's 圖片/文字 rows. */
.anim-entrance-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.anim-entrance-row__label {
  flex: 0 0 2.4rem;
  padding-top: 0.55rem;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.anim-entrance-row__pills {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem;
}

/* Live remove.bg quota line under the 自動去背 checkbox. */
.removebg-credits {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

.removebg-credits__refresh {
  padding: 0.1rem 0.35rem;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--color-text-muted);
  font-size: 0.72rem;
  cursor: pointer;
}

.gen-success {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem 0.8rem;
  border-radius: var(--radius-md);
  background: rgba(94, 190, 120, 0.12);
  border: 1px solid rgba(94, 190, 120, 0.45);
  font-size: 0.82rem;
}

/* Marks a video page in the strip — the strip shows the poster frame, which
   is the ORIGINAL photo for generated clips, so without this badge a page
   with a freshly generated video looks identical to the untouched photo. */
.editable-slide__video-badge {
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 0.1rem 0.4rem;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 0.62rem;
  pointer-events: none;
}

.template-option {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  text-align: left;
}

.template-option__label {
  font-weight: 600;
}

.template-option__desc {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.ai-parts-info {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--color-text-muted);
  padding-top: 0.25rem;
}

.context-sheet__action {
  padding: 0.85rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 1rem;
  text-align: center;
}

.context-sheet__action--cancel {
  color: var(--color-text-muted);
}

.context-sheet__action--danger {
  color: #f28b82;
}

.proj-badge {
  margin-left: 0.4rem;
  font-size: 0.65rem;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
  vertical-align: middle;
}

.proj-badge--hidden {
  background: rgba(242, 139, 130, 0.15);
  color: #f28b82;
}

.editor-projects__add {
  flex-shrink: 0;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  border: 1px dashed var(--color-border);
  color: var(--color-text-muted);
  white-space: nowrap;
  font-size: 0.85rem;
}

.context-sheet__input {
  padding: 0.85rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 1rem;
  text-align: center;
}

/* --- Text controls, per design/text-controls.pen (Pencil, codex agent).
   Each control is its own surface block; gold appears only on selection and
   active state. The font picker replaces a native <select> so every choice
   can render in its own typeface with a size and a delete button, and it
   expands INLINE because the side panel is overflow-y:auto and would clip a
   floating list. --- */
.type-block {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.type-block__label {
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--color-text-muted);
}

.font-picker {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.font-picker__current {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  min-height: 2.9rem;
  padding: 0 0.9rem;
  border-radius: var(--radius-md);
  background: var(--color-surface-strong);
  color: var(--color-text);
  font-size: 1rem;
  text-align: left;
  cursor: pointer;
}

.font-picker__chevron {
  font-size: 0.6rem;
  color: var(--color-text-muted);
  flex-shrink: 0;
}

.font-picker__list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem;
  border-radius: var(--radius-md);
  background: var(--color-surface-strong);
}

.font-picker__group {
  padding: 0.2rem 0.15rem 0;
  font-size: 0.69rem;
  color: var(--color-text-muted);
}

.font-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem 0.6rem;
  padding: 0.5rem 0.65rem;
  border-radius: 12px;
  background: transparent;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

.font-row:hover {
  background: rgba(255, 255, 255, 0.05);
}

/* Selection is a gold WASH, not an outline — the row already carries a font
   sample, and a border would compete with it. */
.font-row.is-selected {
  background: rgba(232, 179, 77, 0.11);
}

.font-row__name {
  font-size: 0.92rem;
}

.font-row__sample {
  grid-column: 1 / -1;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.font-row__size {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
  align-self: center;
}

.font-picker__upload {
  position: relative;
  display: block;
  padding: 0.6rem 0.65rem;
  border-radius: 12px;
  background: rgba(232, 179, 77, 0.07);
  color: var(--color-accent);
  font-size: 0.85rem;
  cursor: pointer;
}

.font-picker__upload input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.font-manager {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.font-manager__head {
  font-size: 0.82rem;
  color: var(--color-text);
}

.font-manager__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.2rem;
}

.font-manager__name {
  flex: 1;
  font-size: 0.88rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.font-manager__size {
  font-size: 0.72rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.font-manager__del {
  width: 2rem;
  height: 2rem;
  border-radius: 14px;
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
  font-size: 0.85rem;
  cursor: pointer;
}

.font-manager__del:hover {
  color: #f28b82;
}

/* Confirm inline, in the row itself — the panel is too narrow for a modal. */
.font-manager__confirm {
  display: inline-flex;
  gap: 0.4rem;
  padding: 0 0.6rem;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
}

.font-manager__confirm button {
  font-size: 0.74rem;
  padding: 0.3rem 0.5rem;
  color: var(--color-text-muted);
  cursor: pointer;
}

.font-manager__danger {
  color: #f28b82 !important;
}

.font-manager__warn {
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

.rotate-readout {
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.rotate-reset {
  margin-left: 0.4rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
  font-size: 0.68rem;
  cursor: pointer;
}

/* Voice dictation for the AI prompt fields. */
.prompt-field {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.prompt-field .ai-tool-card__input {
  flex: 1;
  min-width: 0;
}

.mic-btn {
  width: 2.4rem;
  height: 2.4rem;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  background: var(--color-surface-strong);
  color: var(--color-text-muted);
  font-size: 0.95rem;
  cursor: pointer;
}

.mic-btn.is-listening {
  background: rgba(232, 179, 77, 0.16);
  color: var(--color-accent);
  animation: mic-pulse 1.4s ease-in-out infinite;
}

@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(232, 179, 77, 0.35); }
  50% { box-shadow: 0 0 0 6px rgba(232, 179, 77, 0); }
}

/* Dictation unavailable here (not HTTPS, or a browser without the API).
   Shown dimmed rather than hidden — a missing button reads as a missing
   feature; tapping it explains why and offers the keyboard mic instead. */
.mic-btn.is-unavailable {
  opacity: 0.45;
}

/* Cloud vs local video generation, model/duration pickers and the cost line.
   The price sits above the button on purpose: this is the only control in the
   editor that spends money when pressed. */
.video-settings {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem;
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.03);
}

.video-cost {
  font-size: 0.78rem;
  color: var(--color-accent);
}

/* A provider's balance, shown under its key field in the ⚙ sheet and inside
   whichever card spends it. */
.key-balance {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.6rem;
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.key-balance.is-low {
  background: rgba(242, 139, 130, 0.14);
  color: #f28b82;
}

.key-balance--unset {
  cursor: pointer;
  color: var(--color-accent-strong);
}

.key-balance__refresh {
  color: inherit;
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
}

/* Styled picker replacing native <select> in the video settings. Same
   language as the font picker: closed row shows the choice, list expands
   INLINE (the side panel scrolls and would clip a floating one), gold marks
   the selection. Each row carries a second line, which is the whole point —
   every option here has a price or a limit worth seeing before committing. */
.picker {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.picker__label {
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

.picker__current {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.55rem 0.8rem;
  border-radius: var(--radius-md);
  background: var(--color-surface-strong);
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

.picker__current-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.picker__current-main { font-size: 0.9rem; }
.picker__current-hint { font-size: 0.72rem; color: var(--color-text-muted); }
.picker__chevron { font-size: 0.55rem; color: var(--color-text-muted); flex-shrink: 0; }

.picker__list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.35rem;
  border-radius: var(--radius-md);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
}

.picker__row {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.5rem 0.6rem;
  border-radius: 10px;
  background: transparent;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

.picker__row:hover { background: rgba(255, 255, 255, 0.05); }
.picker__row.is-selected { background: rgba(232, 179, 77, 0.11); }

.picker__row-main { font-size: 0.88rem; display: inline-flex; align-items: center; gap: 0.4rem; }
.picker__row-hint { font-size: 0.74rem; color: var(--color-text-muted); }

.picker__badge {
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: rgba(232, 179, 77, 0.18);
  color: var(--color-accent-strong);
  font-size: 0.65rem;
}

.picker__note {
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}

/* Pick an existing R2 file as a layer, instead of uploading a second copy. */
.cloudpick {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.8);
  animation: backdrop-fade-in 160ms ease-out;
}

.cloudpick__box {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: min(100%, 760px);
  max-height: 88vh;
  padding: 1rem;
  border-radius: var(--radius-lg);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
}

.cloudpick__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.cloudpick__title { font-size: 1rem; }

.cloudpick__filters {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.cloudpick__hint { font-size: 0.72rem; color: var(--color-text-muted); }

.cloudpick__grid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 0.6rem;
}

.cloudpick__item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.4rem;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

.cloudpick__item:hover { background: var(--color-surface-strong); }
.cloudpick__item.is-busy { opacity: 0.5; }
.cloudpick__item:disabled { cursor: default; }

.cloudpick__thumb {
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: var(--color-surface-strong);
  display: flex;
  align-items: center;
  justify-content: center;
}

.cloudpick__thumb img { width: 100%; height: 100%; object-fit: cover; }
.cloudpick__play { font-size: 1.1rem; color: var(--color-text-muted); }

.cloudpick__name {
  font-size: 0.74rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cloudpick__meta {
  font-size: 0.66rem;
  color: var(--color-text-muted);
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.cloudpick__orphan { color: var(--color-accent-strong); }


/* Pairing code. Sized to be read off one screen while typing into another —
   that is the whole job, so the code itself is the largest thing on the row
   and the ambiguous characters were already excluded when it was minted. */
.pair-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  padding: 14px 18px;
  margin: 0 0 4px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.pair-panel__msg {
  flex: 1 1 260px;
  margin: 0;
  font-size: 0.85rem;
  line-height: var(--line-body);
  color: var(--color-text-dim);
}

.pair-panel__msg--error { color: var(--color-danger, #f28b82); }

.pair-panel__code {
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  padding: 8px 18px;
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-accent-strong, var(--color-accent));
  /* Selectable on purpose: on the desktop it is faster to copy than retype. */
  user-select: all;
}
