/* PuzzleDuo — create.css
   The workbench: a 380px control rail on paper, a dark cutting-mat stage, and a
   sticky action bar that never leaves the rail's bottom edge. (§0.2, §0.3) */

.pf-create { background: var(--paper-1); }

/* ========================================================== workspace ===== */
.pf-ws {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  gap: var(--s6);
  max-width: var(--w-workspace);
  margin-inline: auto;
  padding: var(--s4) var(--gutter) var(--s6);
  align-items: start;
}
@media (min-width: 1600px) {
  .pf-ws { max-width: 1600px; }
}

/* =============================================================== rail ===== */
.pf-rail {
  position: sticky;
  top: calc(56px + var(--s3));
  display: flex;
  flex-direction: column;
  height: calc(100vh - 56px - var(--s6));
  background: var(--paper-0);
  border: 1px solid var(--line-200);
  border-radius: var(--r-panel);
  box-shadow: var(--e0);
  overflow: hidden;
}
.pf-rail__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-100);
  flex: none;
}
.pf-rail__title {
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--ink-900);
  margin: 0;
}
.pf-rail__scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.pf-rail__scroll:focus-visible { outline-offset: -3px; }

/* ============================================================== group ===== */
.pf-group { border-bottom: 1px solid var(--line-100); }
.pf-group:last-of-type { border-bottom: 0; }
.pf-group__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: 10px 16px 0;
}
.pf-group__title {
  font-family: var(--font-ui);
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-500);
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}
.pf-group__body { padding: 8px 16px 14px; display: flex; flex-direction: column; gap: 10px; }
.pf-group__helper {
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-500);
  margin: 0;
}
.pf-group[data-dim="true"] { opacity: 0.62; }

/* accordion variant (tablet / phone only) */
.pf-acc { border-bottom: 1px solid var(--line-100); }
.pf-acc > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 56px;
  padding: 0 16px;
  cursor: pointer;
  list-style: none;
}
.pf-acc > summary::-webkit-details-marker { display: none; }
.pf-acc__plus { color: var(--clay-600); transition: transform var(--dur-med) var(--ease); flex: none; }
.pf-acc[open] .pf-acc__plus { transform: rotate(45deg); }
.pf-acc > .pf-group__body { padding-top: 0; }

@media (min-width: 1024px) {
  /* Desktop: seven groups scan in one screen, so the headers stop being toggles. */
  .pf-acc > summary { pointer-events: none; padding-bottom: 0; min-height: 34px; }
  .pf-acc > summary .pf-acc__plus { display: none; }
  .pf-acc > .pf-group__body { display: flex !important; }
}

/* ========================================================== dropzone ===== */
.pf-drop {
  display: grid;
  place-items: center;
  gap: 6px;
  min-height: 112px;
  padding: 16px;
  text-align: center;
  background: var(--paper-0);
  border: 1px dashed var(--line-300);
  border-radius: var(--r-card);
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
  cursor: pointer;
}
.pf-drop[hidden] { display: none; }
.pf-drop:hover, .pf-drop:focus-visible { border-color: var(--clay-500); background: var(--clay-100); }
.pf-drop[data-drag="true"] {
  border: 2px dashed var(--clay-600);
  background: var(--clay-100);
}
.pf-drop__icon { color: var(--clay-600); }
.pf-drop__title {
  font-family: var(--font-ui);
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink-900);
  margin: 0;
}
/* ink-700 in EVERY state, deliberately. ink-500 measures 5.17:1 on paper-0 but
   only 4.34:1 on clay-100, and clay-100 is the dropzone's hover *and* drag
   background — so a lighter helper would fail AA at the exact moment someone is
   dragging their photo onto it. Written as a state rule, never as a
   .dropzone:hover override: a colour that only exists in a hover branch is what a
   refactor drops six months from now. ink-700 is 10.30:1 / 8.6:1. */
.pf-drop__sub { font-size: 13px; color: var(--ink-700); margin: 0; line-height: 1.4; }
.pf-drop { position: relative; }
.pf-drop input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.pf-thumbrow {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 72px;
  padding: 8px;
  border: 1px solid var(--line-200);
  border-radius: var(--r-card);
  background: var(--paper-0);
}
.pf-thumbrow[hidden] { display: none; }
.pf-thumb {
  position: relative;
  width: 56px; height: 56px;
  flex: none;
  border-radius: var(--r-xs);
  border: 1px solid var(--line-200);
  overflow: hidden;
  background: var(--paper-2);
  touch-action: none;
  cursor: grab;
}
.pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
/* Discarded area = one veil in two layers plus a faint hatch. The hatch alpha is
   luminance-conditional (--hatch, 5% base / 8% on light regions) because a flat
   5% is cosmetic exactly where the affordance matters most. */
.pf-thumb__band {
  position: absolute;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(45deg,
      rgba(253, 250, 244, var(--hatch, 0.05)) 0 1px,
      rgba(253, 250, 244, 0) 1px 6px),
    linear-gradient(rgba(253, 250, 244, 0.10), rgba(253, 250, 244, 0.10)),
    linear-gradient(rgba(42, 33, 27, 0.42), rgba(42, 33, 27, 0.42));
  opacity: 1;
  transition: opacity 600ms var(--ease);
}
/* three states, never a per-frame recompute: full on first peek / keyboard focus,
   50% while a handle is dragged, out otherwise */
.pf-thumb[data-veil="half"] .pf-thumb__band { opacity: 0.5; transition: opacity 120ms linear; }
.pf-thumb[data-veil="off"] .pf-thumb__band { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .pf-thumb__band,
  .pf-thumb[data-veil="half"] .pf-thumb__band { transition: none; }
}
.pf-thumb__dot {
  position: absolute;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: var(--paper-0);
  border: 2px solid var(--clay-600);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}
.pf-thumb__body { min-width: 0; flex: 1 1 auto; }
.pf-thumb__name {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-900);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}
.pf-thumb__meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-500);
  margin: 2px 0 0;
  letter-spacing: 0.02em;
}
.pf-thumb__acts { display: flex; gap: 2px; flex: none; }
.pf-linkbtn {
  border: 0;
  background: transparent;
  color: var(--ink-700);
  font-size: 13px;
  font-weight: 600;
  padding: 8px;
  border-radius: var(--r-xs);
  min-height: 44px;
}
.pf-linkbtn:hover { background: var(--clay-100); color: var(--clay-700); }

.pf-cropnote {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-700);
  background: var(--paper-2);
  border-radius: var(--r-xs);
  padding: 8px 10px;
}
.pf-cropnote svg { flex: none; color: var(--ink-500); margin-top: 1px; }
.pf-cropnote[hidden] { display: none; }

/* ======================================================== segmented ====== */
.pf-seg {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  padding: 3px;
  background: var(--paper-2);
  border-radius: var(--r-btn);
  border: 1px solid var(--line-300);
}
/* Hidden inputs must still be the real hit area: a 1px radio is unusable with a
   thumb (and with switch access). They sit transparently over their label.
   `isolation: isolate` on each parent is load-bearing, not decoration: a positive
   z-index on a positioned child escapes to the ROOT stacking context when the
   parent does not create one, so these inputs were competing with page chrome —
   at ≤1023 the .pf-seg radios (z-index 2) painted over the sticky preview panel
   (z-index 1) and cut a card-shaped hole out of the sheet. Scoping them fixes the
   class of bug: after this, a rail component may use any z-index it likes. */
.pf-seg label { position: relative; isolation: isolate; display: block; }
.pf-seg input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: pointer;
  z-index: 2;
}
.pf-swatch { position: relative; isolation: isolate; }
.pf-swatch input {
  position: absolute;
  top: 0;
  left: 50%;
  width: 32px;
  height: 32px;
  margin: 0 0 0 -16px;
  opacity: 0;
  cursor: pointer;
  z-index: 3;
}
.pf-toggle { position: relative; isolation: isolate; }
.pf-toggle input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 2;
}
.pf-seg span {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 8px;
  border-radius: var(--r-xs);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink-700);
  cursor: pointer;
  text-align: center;
  line-height: 1.15;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pf-seg label:hover span { background: rgba(253, 250, 244, 0.55); }
.pf-seg input:checked + span {
  background: var(--paper-0);
  color: var(--ink-900);
  box-shadow: var(--e1);
}
.pf-seg input:focus-visible + span {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}
.pf-seg--stack { grid-auto-flow: row; }
.pf-seg--sm span { min-height: 32px; font-size: 12px; }

/* ============================================================ chips ====== */
.pf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pf-chip {
  height: 32px;
  padding: 0 12px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-300);
  background: var(--paper-0);
  color: var(--ink-700);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.pf-chip:hover { background: var(--clay-100); border-color: rgba(194, 96, 60, 0.4); }
.pf-chip[aria-pressed="true"] {
  background: var(--clay-100);
  border: 1.5px solid var(--clay-600);
  color: var(--ink-900);
  font-weight: 700;
  padding: 0 11.5px;
}
.pf-chips { transition: opacity 120ms linear; }
.pf-chips[data-fade="true"] { opacity: 0.45; }

/* Preset chips carry two lines: the grid in mono, the count beneath. Two lines
   because "8 x 11" alone does not tell you whether that is a lot of cutting, and
   the counts differ between orientations, so the label cannot be static. */
.pf-chip--grid {
  height: 44px;
  padding: 0 10px;
  border-radius: 10px;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 1px;
  line-height: 1.15;
}
.pf-chip--grid[aria-pressed="true"] { padding: 0 9.5px; }
.pf-chip__grid {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-900);
}
.pf-chip__count { font-size: 10px; font-weight: 600; color: var(--ink-500); }
.pf-chip--grid[aria-pressed="true"] .pf-chip__count { color: var(--clay-600); }

/* Custom-grid marker: a label micro-tag, inline in the readout. A sighted user
   reads a state marker; a screen reader reads the word "Custom". Not a badge,
   not a fifth chip, not prose. */
.pf-tag {
  display: inline-block;
  margin-left: 6px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-500);
}
.pf-tag[hidden] { display: none; }

/* "fills the width and leaves N mm" — a number that states the loss, max two lines */
.pf-group__hint {
  margin: -2px 0 8px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-500);
  max-width: 40ch;
}

/* =========================================================== stepper ===== */
.pf-stepper-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.pf-stepper-wrap { display: flex; flex-direction: column; gap: 6px; }
.pf-stepper-wrap > .label { font-size: 11px; }
.pf-stepper {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  height: 44px;
  background: var(--paper-2);
  border: 1px solid var(--line-300);
  border-radius: var(--r-xs);
  overflow: hidden;
}
.pf-stepper button {
  height: 44px;
  border: 0;
  background: var(--paper-0);
  color: var(--ink-900);
  font-size: 18px;
  line-height: 1;
  display: grid;
  place-items: center;
  transition: background-color var(--dur-fast) var(--ease);
}
.pf-stepper button:first-child { border-right: 1px solid var(--line-200); }
.pf-stepper button:last-child { border-left: 1px solid var(--line-200); }
.pf-stepper button:hover { background: var(--clay-100); }
.pf-stepper button:disabled { color: rgba(122, 103, 84, 0.45); pointer-events: none; }
.pf-stepper input {
  width: 100%;
  height: 100%;
  border: 0;
  background: transparent;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  font-weight: 500;
  color: var(--ink-900);
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
  appearance: textfield;
}
.pf-stepper input::-webkit-outer-spin-button,
.pf-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.pf-readout {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-900);
  letter-spacing: var(--ls-mono);
}
.pf-readout--muted { color: var(--ink-500); }

/* =========================================================== swatches ==== */
.pf-swatches {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
}
.pf-swatch { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.pf-swatch__dot {
  width: 32px; height: 32px;
  border-radius: var(--r-xs);
  border: 1px solid var(--line-300);
  outline: 2px solid transparent;
  outline-offset: 2px;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), outline-color var(--dur-fast) var(--ease);
  position: relative;
  display: grid;
  place-items: center;
}
.pf-swatch__dot:hover, .pf-swatch:hover .pf-swatch__dot { transform: scale(1.06); }
.pf-swatch input:checked + .pf-swatch__dot {
  outline: 2px solid var(--clay-600);
}
.pf-swatch input:checked + .pf-swatch__dot::after {
  content: "";
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--sage-600);
  opacity: 0.9;
  box-shadow: 0 0 0 2px rgba(253, 250, 244, 0.9);
}
.pf-swatch input:focus-visible + .pf-swatch__dot { outline-color: var(--focus-ring); outline-offset: 3px; }
.pf-swatch__name {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--ink-700);
  text-align: center;
  line-height: 1.2;
}
.pf-swatch__grey {
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--ink-500);
  /* the luminance figure is a separate run, not part of the name: without this
     it renders flush against it ("Charcoal25%", measured gap 0.00px on all five)
     and reads as a broken glyph. */
  margin-left: 3px;
}

/* ============================================================= slider ==== */
.pf-slider-row { display: flex; align-items: center; gap: 10px; }
.pf-slider { position: relative; flex: 1 1 auto; height: 44px; display: flex; align-items: center; }
.pf-slider input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 44px;
  background: transparent;
  outline: none;
  margin: 0;
  cursor: pointer;
}
/* the rail is drawn on the runnable track so the input itself can stay 44px tall
   and still look like a 6px rail */
.pf-slider input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--paper-3);
}
.pf-slider input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--paper-3);
}
.pf-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  margin-top: -7px;
  border-radius: 50%;
  background: var(--paper-0);
  border: 2px solid var(--clay-600);
  box-shadow: var(--e1);
  transition: width var(--dur-fast) var(--ease), height var(--dur-fast) var(--ease), margin-top var(--dur-fast) var(--ease);
}
.pf-slider input[type="range"]:hover::-webkit-slider-thumb {
  width: 22px;
  height: 22px;
  margin-top: -8px;
}
.pf-slider input[type="range"]::-moz-range-thumb {
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--paper-0);
  border: 2px solid var(--clay-600);
  box-shadow: var(--e1);
}
.pf-slider input[type="range"]:focus-visible { outline-offset: 6px; }
.pf-slider__out {
  flex: none;
  width: 56px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  color: var(--ink-900);
  font-variant-numeric: tabular-nums;
}

/* A control that is not in play dims and stops responding. Used by the corner
   fillet when the cut shape is Classic, or when the grid is square — both cases
   where a fillet would change nothing at all. */
.pf-slider-row[data-off="true"] { opacity: 0.42; }
.pf-slider-row[data-off="true"] .pf-slider input[type="range"] { cursor: not-allowed; }
.pf-slider-row[data-off="true"] input[type="range"]::-webkit-slider-thumb {
  border-color: var(--paper-3);
  box-shadow: none;
}

/* discrete width scale: 1 / 1.5 / 2 / 3 / 4 */
.pf-lw {
  display: flex;
  align-items: center;
  gap: 12px;
}
.pf-lw__rail { position: relative; flex: 1 1 auto; padding-top: 18px; }
.pf-lw__stops {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  align-items: end;
}
.pf-lw__stop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  height: 40px;
  justify-content: flex-end;
  background: transparent;
  border: 0;
  padding: 0 0 2px;
  color: var(--ink-500);
}
.pf-lw__stop i {
  display: block;
  width: 3px;
  background: var(--line-300);
  border-radius: 2px;
  transition: background-color var(--dur-fast) var(--ease), height var(--dur-fast) var(--ease);
}
.pf-lw__stop[aria-pressed="true"] i { background: var(--clay-600); width: 4px; }
.pf-lw__stop span {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-500);
}
.pf-lw__stop[aria-pressed="true"] span { color: var(--ink-900); font-weight: 700; }
.pf-lw__stop:focus-visible { outline-offset: 4px; }
.pf-lw__sample {
  flex: none;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  background: var(--paper-0);
  border: 1px solid var(--line-300);
  border-radius: var(--r-xs);
}
.pf-lw__sample i { display: block; width: 26px; border-radius: 2px; background: var(--ink-900); }

/* ============================================================= select ==== */
.pf-select {
  position: relative;
  display: block;
}
.pf-select select {
  width: 100%;
  height: 44px;
  padding: 0 34px 0 12px;
  border: 1px solid var(--line-300);
  border-radius: var(--r-xs);
  background: var(--paper-0);
  color: var(--ink-900);
  font-size: var(--fs-small);
  font-weight: 600;
  appearance: none;
  -webkit-appearance: none;
}
.pf-select svg {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--ink-500);
}

/* ============================================================= toggle ==== */
/* NOTE: .pf-toggle has a second, earlier declaration in the hidden-input group
   near the top of this file (position: relative; isolation: isolate) plus
   .pf-toggle input { z-index: 2 } there. The isolation is load-bearing — without
   it the input's z-index escapes and paints over the sticky preview panel. Read
   that comment before deleting either block. */
.pf-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  cursor: pointer;
}
.pf-toggle input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.pf-toggle__track {
  position: relative;
  width: 40px; height: 22px;
  flex: none;
  background: var(--paper-3);
  border-radius: var(--r-pill);
  transition: background-color var(--dur-fast) var(--ease);
}
.pf-toggle__track::after {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--paper-0);
  box-shadow: var(--e1);
  transition: transform var(--dur-fast) var(--ease);
}
.pf-toggle input:checked + .pf-toggle__track { background: var(--clay-600); }
.pf-toggle input:checked + .pf-toggle__track::after { transform: translateX(18px); }
.pf-toggle input:focus-visible + .pf-toggle__track { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.pf-toggle__label { font-size: 14px; font-weight: 600; color: var(--ink-700); }

/* ===================================================== inline messages === */
.pf-msg {
  position: relative;
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px 12px 16px;
  background: var(--paper-0);
  border: 1px solid rgba(154, 46, 32, 0.4);
  border-radius: 12px;
  border-left: 3px solid var(--danger-600);
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-700);
}
.pf-msg[hidden] { display: none; }
.pf-msg__title { font-weight: 700; color: var(--danger-600); margin: 0 0 2px; font-size: 13px; }
.pf-msg__body { margin: 0; }
.pf-msg__icon { flex: none; color: var(--danger-600); margin-top: 1px; }
.pf-msg--warn { border-color: rgba(176, 124, 46, 0.42); border-left-color: var(--warn-600); }
.pf-msg--warn .pf-msg__title { color: var(--warn-700); }
.pf-msg--warn .pf-msg__icon { color: var(--warn-600); }

/* ============================================================ crop UI ==== */
.pf-focus { display: flex; flex-direction: column; gap: 8px; }
.pf-focus__row { display: grid; grid-template-columns: 56px 1fr; align-items: center; gap: 8px; }
.pf-focus__row .label { font-size: 10px; }

/* ========================================================= action bar ==== */
.pf-bar {
  flex: none;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--paper-0);
  border-top: 1px solid var(--line-200);
  box-shadow: var(--e4);
}
/* Two short rows rather than one long one: "48 pieces · A4 portrait · finished
   190 x 253 mm · Sample photo" is 52 characters of mono and wraps in the 320px
   rail, and a wrapped number is a number nobody reads. */
.pf-bar__summary {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-500);
  letter-spacing: 0.02em;
  margin-bottom: 8px;
}
.pf-bar__meta { display: flex; gap: 6px; white-space: nowrap; max-width: 100%; overflow: hidden; }
.pf-bar__meta > span { overflow: hidden; text-overflow: ellipsis; }
.pf-bar__summary strong { color: var(--ink-900); font-weight: 500; }
/* One height for the whole bar (v3). A 48/44 mix made the primary look like a
   different component and cost 4px of the rail's most valuable space. Labels are
   Print / Download / Play: no ellipsis, no responsive swap, never break-all. */
.pf-bar__pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
.pf-bar .pf-btn { height: 44px; padding: 0 12px; font-size: 14px; font-weight: 600; }
.pf-bar .pf-btn--44 { height: 44px; }

/* ============================================================== stage ==== */
.pf-stage {
  position: sticky;
  top: calc(56px + var(--s3));
  /* DEFINITE height, for the same reason as the ≤1023 block further down: the
     canvas is flex-basis:auto, so a sheet's own height propagates straight back
     up into the panel. With only min-height here the stage grew to 686px in a
     700px viewport (sheet 504 + 182 padding) and its bottom chrome — zoom pills,
     Reset — was pushed below the fold while the panel was pinned. A sticky
     preview pane must be bounded by the viewport, never by its content. */
  height: calc(100vh - 56px - var(--s6));
  min-height: 0;
  border-radius: var(--r-panel);
  background: var(--stage-900);
  color: var(--stage-100);
  box-shadow: inset 0 1px 0 var(--stage-hair);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* ---- chrome safe area -------------------------------------------------
     fitSheet() sizes the sheet from .pf-stage__canvas, and the chrome (side
     pills, zoom pills, Reset, sample line) is absolutely positioned *over*
     the stage. So this padding is the only thing keeping the paper off its
     own controls: top pills sit at top:16px and are 32–46px tall, bottom
     pills + Reset sit at bottom:64px and reach 110px, sample at bottom:12px.
     64px / 118px clears the tallest of those in both pointer modes.
     It used to read `var(--s8) var(--s6) var(--s10)` — --s10 is not in the
     token scale, and one undefined var() invalidates the WHOLE shorthand, so
     padding silently computed to 0 and the sheet grew straight over the zoom
     pills at 1024–1279. Never write a spacing token here that tokens.css does
     not define; check with `getComputedStyle(stage).paddingTop`. */
  padding: 64px var(--s6) 118px;
  overflow: hidden;
  isolation: isolate;
}
.pf-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(var(--stage-grain) 1px, transparent 1.2px),
    radial-gradient(var(--stage-grain) 1px, transparent 1.2px);
  background-size: 4px 4px, 9px 9px;
  background-position: 0 0, 2px 3px;
}

.pf-stage__canvas {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  /* flex-basis 0, not auto: `auto` lets the sheet dictate this box, which is the
     feedback loop one level down from the stage's own height */
  flex: 1 1 0;
  min-height: 0;
}
.pf-stage__frame {
  position: relative;
  background: var(--paper-0);
  border-radius: var(--r-sheet);
  box-shadow: var(--e-stage);
  overflow: hidden;
  width: 100%;
  aspect-ratio: var(--sheet-ar, 0.707);   /* JS replaces this with exact px */
  max-width: 100%;
}
.pf-stage__frame svg { display: block; width: 100%; height: 100%; }
.pf-stage__stack {
  position: absolute;
  inset: 8px -8px -8px 8px;
  background: var(--paper-0);
  opacity: 0.5;
  border-radius: var(--r-sheet);
  z-index: -1;
}
.pf-stage__frame { z-index: 1; }
/* The frame is the lightbox trigger (role=button), so it needs the two things a
   button has: a pointer, and a visible focus ring. The ring sits outside the
   sheet so it never trims the sheet's own crop marks. */
.pf-stage__frame { cursor: zoom-in; }
.pf-stage__frame:focus-visible {
  outline: 2px solid var(--paper-0);
  outline-offset: 3px;
}

/* the sheet's own proportions are set from JS; this is the reserved box so
   nothing shifts when the SVG arrives */
.pf-stage__frame[data-ratio] { aspect-ratio: var(--sheet-ar, 0.707); }
.pf-stage__frame > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
#preview { position: absolute; inset: 0; }

.pf-stage__chrome {
  position: absolute;
  z-index: 4;
  display: flex;
  gap: 8px;
  align-items: center;
}
.pf-stage__chrome--tl { top: 16px; left: 16px; }
.pf-stage__chrome--bl { bottom: 64px; left: 16px; }
.pf-stage__chrome--br { bottom: 64px; right: 16px; }

/* Reset view: labelled while there is room, a 44x44 glyph below 401px. At 360
   the zoom pills are 189px and the label 124px, which cannot share a 296px
   content row (they met by 5px), and relocating it to the top row would put it
   under the sheet's top corner. The label is visually hidden rather than
   display:none, so the accessible name stays "Reset view". */
.pf-reset-glyph { display: none; }

.pf-pills {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--paper-0);
  border-radius: var(--r-pill);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}
.pf-pills button {
  height: 26px;
  padding: 0 10px;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink-900);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}
.pf-pills button:hover { background: var(--clay-100); }
.pf-pills button[aria-pressed="true"] { background: var(--clay-100); color: var(--ink-900); }

.pf-stage__status {
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 12px;
  border-radius: var(--r-pill);
  background: var(--stage-800);
  color: var(--stage-70);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  white-space: nowrap;
  z-index: 4;
}

/* Sample disclosure: one line under the status row, on paper-0 pill chrome rather
   than on the dark surface (ink-500 on stage-900 would be unreadable). Quiet, not
   a banner and not an overlay. */
.pf-stage__sample {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 12px;
  margin: 0;
  border-radius: var(--r-pill);
  background: var(--paper-0);
  color: var(--ink-500);
  font-size: 13px;
  white-space: nowrap;
  z-index: 4;
}
.pf-stage__sample[hidden] { display: none; }

/* 2px indeterminate bar while the preview recomputes */
.pf-stage__busy {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  overflow: hidden;
  z-index: 5;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.pf-stage__busy[data-on="true"] { opacity: 1; }
.pf-stage__busy i {
  position: absolute;
  top: 0; bottom: 0;
  width: 40%;
  background: var(--clay-500);
  animation: pf-indet 900ms var(--ease) infinite;
}
@keyframes pf-indet {
  0% { left: -40%; }
  100% { left: 100%; }
}

.pf-stage__empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--s6);
  text-align: center;
  color: var(--stage-70);
  font-size: var(--fs-small);
  z-index: 3;
  pointer-events: none;
}
.pf-stage__empty[hidden] { display: none; }

/* ==================================================== responsive ========= */
@media (max-width: 1023px) {
  .pf-ws {
    grid-template-columns: 1fr;
    gap: var(--s4);
    padding-bottom: 0;
  }
  /* DEFINITE height, not an auto-height flex column. This is the whole fix for
     the "preview shrinks 40px per click" loop: with min-height:0 + max-height the
     stage's height was the frame's height, the canvas inherited it, and fitSheet()
     measured a box it had itself just sized — a positive feedback loop that ate
     pad*2 on every render. A fixed height breaks the loop at the source, so the
     measurement is independent of the thing being measured. */
  .pf-stage {
    order: -1;
    top: 56px;
    position: sticky;
    height: 52vh;
    min-height: 0;
    max-height: none;
    /* order:-1 is a PAINT ORDER instruction, not just a layout one. The stage is
       sticky (positioned, z-index:auto) and so is every position:relative card in
       the rail (.pf-drop, .pf-seg …), and same-level positioned elements are
       painted in order-modified DOM order — so order:-1 drew the stage FIRST and
       the rail's cards on top of the paper. Below 1023 the two columns become one
       and actually overlap, which is why this only broke on phones. z-index:1
       lifts the panel out of that comparison entirely.
       Do NOT raise this above .pf-bar (z-index 30) or .pf-header (60): the
       primary action and the site header always win. Verified by
       qa/stage-overlap.mjs section B, which samples elementFromPoint across the
       stage rect at scrollY 0/300/600 and allows nothing foreign to be painted
       inside it. */
    z-index: 1;
    padding: var(--s4);
  }
  /* flex-basis 0 as well as min-height 0: `1 1 auto` still lets content dictate
     the box, which is the same trap one level down */
  .pf-stage__canvas { flex: 1 1 0; min-height: 0; }
  /* no max-height on the frame, ever: fitSheet() writes exact px on both axes,
     and a max-height on one axis is a silent aspect-ratio change (12.5% at
     1024–1279, i.e. an A4 that isn't A4). Do not replace it with object-fit or a
     transform either — that splits preview geometry from print geometry. */
  .pf-stage__frame { max-height: none; }
  .pf-stage__chrome--bl, .pf-stage__chrome--br { bottom: 12px; }
  .pf-stage__status { display: none; }
  /* room under the sheet for two rows of chrome: the sample line sits above the
     bottom pills instead of on top of them. 60px = pills top edge (12 + 46 on a
     coarse pointer) + 2px, so it holds at 1240 too, not just at the nominal size. */
  .pf-stage { padding-bottom: 92px; }
  .pf-stage__sample { bottom: 60px; height: auto; min-height: 24px; font-size: 12px; padding: 0 10px; max-width: calc(100% - 20px); }
  .pf-rail {
    position: static;
    height: auto;
    max-height: none;
    border-radius: var(--r-panel) var(--r-panel) 0 0;
  }
  .pf-rail__scroll { overflow: visible; }
  .pf-bar { position: sticky; bottom: 0; z-index: 30; }
}

@media (max-width: 767px) {
  /* 52vh, not 46vh. The top band below costs 48px and has to come from
     somewhere; taking it out of the sheet is the same bug we just fixed in
     another costume. Measured sheet sizes, band vs no band:
       360x640 120x170 (overlapping the pills) → 114x161
       390x600 107x152 (overlapping the pills) →  99x140
       375x667 129x183 (overlapping the pills) → 124x175
     i.e. the paper gives up ~5% linear and stops being painted over. Going to
     54vh would recover that 5% but hand 24px of rail to a sticky panel on the
     shortest phones, which is the wrong side of the trade. */
  .pf-stage { height: 52vh; max-height: none; padding-top: 64px; }
  /* the 64px is a top band for the side pills: on a phone the stage is full
     width, so the centred sheet is 114–129px and the Front/Back group is 107px —
     they want the same rows and meet by 10px at 375. 64 = pills top 10 + 46
     tall + 8 gap. */
  .pf-stage__frame { max-height: none; }
  .pf-stage__chrome--tl { top: 10px; left: 10px; }
  .pf-stage__chrome--bl, .pf-stage__chrome--br { bottom: 10px; }
  .pf-stage__chrome--bl { left: 10px; }
  .pf-stage__chrome--br { right: 10px; }
  .pf-pills button { height: 24px; padding: 0 8px; font-size: 11px; }
  .pf-bar { position: fixed; left: 0; right: 0; bottom: 0; width: 100%; }
  .pf-create { padding-bottom: 116px; }
  .pf-stepper-row { grid-template-columns: 1fr; }
}

@media (max-width: 400px) {
  .pf-bar__pair { grid-template-columns: 1fr 1fr; }
  .pf-bar .pf-btn { padding: 0 10px; font-size: 14px; }
  /* icon-only Reset: see .pf-reset-glyph comment. 44x44 tap target kept. */
  #reset-view { width: 44px; padding: 0; }
  .pf-reset-glyph { display: inline; font-size: 17px; line-height: 1; }
  .pf-reset-text {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* Controls grow for thumbs. 32px swatches are a mouse affordance, not a finger
   one, and a 40px chip row is a game of darts on a phone. */
@media (max-width: 767px), (pointer: coarse) {
  .pf-chip:not(.pf-chip--grid) { height: 44px; padding: 0 14px; }
  .pf-chip[aria-pressed="true"] { padding: 0 13.5px; }
  .pf-seg span { min-height: 44px; }
  .pf-swatch__dot { width: 44px; height: 44px; }
  .pf-swatch input { width: 44px; height: 44px; margin-left: -22px; }
  .pf-swatch__name { font-size: 10px; }
  .pf-lw__stop { height: 48px; min-width: 44px; }
  .pf-lw__rail { padding-top: 20px; }
  .pf-stepper { height: 48px; }
  .pf-stepper button { height: 48px; }
  .pf-pills button { height: 40px; min-width: 44px; padding: 0 12px; }
  .pf-linkbtn { min-height: 44px; min-width: 44px; }
}

/* Narrowest breakpoint, deliberately LAST: the thumb-sizing block above
   re-declares .pf-swatch__name at 10px, so a ≤359 rule placed beside the swatch
   definitions is silently overridden (that cost me one round of "the fix did
   nothing"). Five mono labels at 10px need 287px; the row is viewport−66, so it
   only fits from 353 up. Before the 3px name/luminance gap existed the row already
   overflowed at 320 and "White100%" ended 11px outside the card.
   At 9px the same five labels need 236px, which leaves 18px of slack at 320 —
   tightening the row and stepping the type down are the only levers that keep
   every label on ONE line; breaking "Charcoal / 25%" is worse than a tight gap. */
@media (max-width: 359px) {
  .pf-swatches { gap: 4px; }
  .pf-swatch__name { font-size: 9px; letter-spacing: 0; }
  .pf-swatch__grey { font-size: 8px; letter-spacing: 0; }
}

/* faint example puzzle behind the empty stage (state matrix: Empty) */
.pf-stage__example {
  position: absolute;
  inset: 0;
  padding: 9%;
  opacity: 0.55;
  pointer-events: none;
}
.pf-stage__example svg { width: 100%; height: 100%; }

/* ============================================================ lightbox ====
   The full-sheet viewer. Its own stacking island (isolation:isolate), its own
   scroll lock, its own scrim — it must not inherit anything from the stage,
   because it is not in the stage.

   z-index 70 — suspended layer (20-99), the same band as the sticky header and
   the bottom bar, which are exactly the two things a full-screen viewer has to
   beat: 60 for .pf-header (or the top 56px of the scrim paints under a header
   that is still clickable, and aria-modal becomes a lie) and 30 for .pf-bar.
   It stays below every modal-layer value on purpose: 90 [data-tip], 220
   .pf-modal, 240 .pf-menu, 260 .pf-toasts, 300 .skip-link. A viewer is a place
   you are looking at something, not a thing that owns the screen — a toast about
   a failed download must still reach you while it is open. One custom property
   on purpose, because this is the number that gets argued about.
   Layer table (docs/VISUAL-SPEC.md): content 0-9 · floating 20-99 · modal
   200-249 · system 250-300. */
.pf-lightbox {
  --z-lightbox: 70;
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  isolation: isolate;
  touch-action: none;
}
.pf-lightbox[hidden] { display: none; }

.pf-lightbox__scrim {
  position: absolute;
  inset: 0;
  /* ink-900 at 86%. Not black: the sheets are warm paper, and a neutral black
     backdrop pulls them grey. No backdrop-filter — it drops frames on low-end
     Android, and the one thing this screen exists for is reading a jigsaw edge. */
  background: rgba(43, 32, 24, 0.86);
  cursor: zoom-out;
}

.pf-lightbox__viewport {
  position: absolute;
  inset: 0;
  /* The padding box is the sheet's stage, so it has to pay for the two things
     that float over it: 56px of close button at the top (+8 breathing) and 80px
     of bottom pill (+8). A symmetric 24px top padding is what put the close
     button on top of the sheet at 844x390 — a landscape phone is 390 tall, the
     portrait sheet then fills the height, and its top edge landed 32px above the
     close button's bottom. The sheet shrinks by 5% at 1440 as a result; that is
     the correct trade for never burying a control. */
  padding: calc(64px + env(safe-area-inset-top, 0px)) 24px calc(88px + env(safe-area-inset-bottom, 0px));
  overflow: hidden;
  /* transparent to pointers so that a click on empty space lands on the scrim
     and closes; the sheet re-enables its own pointer events */
  pointer-events: none;
}

.pf-lightbox__sheet {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  background: var(--paper-0);
  border-radius: 2px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  pointer-events: auto;
  cursor: grab;
  will-change: transform;
}
.pf-lightbox[data-grabbing] .pf-lightbox__sheet { cursor: grabbing; }
.pf-lightbox__sheet svg { display: block; width: 100%; height: 100%; }

/* ---- side arrows: 56x56, edge-mounted, out of the way past 1.2x ---- */
.pf-lightbox__nav {
  position: absolute;
  top: 50%;
  margin-top: -28px;
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.30);
  background: rgba(43, 32, 24, 0.66);
  color: #fff;
  cursor: pointer;
  transition: opacity 400ms var(--ease), background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
/* Ink-tinted, not paper-tinted. A 10% white disc with a 22% white ring is
   invisible on the two backgrounds these two buttons actually sit on: the sheet's
   own white margin at fit (left arrow, 390) and a pale wall inside the photograph
   at 3x (close button, whose glyph disappeared entirely). Dark chrome with a
   white glyph reads on white paper, on a bright photo and on the scrim alike,
   and it sits in the same ink-900 family as the rest of the page.

   0.66 is measured, not picked. The worst case is not a photograph, it is the
   sheet's own paper (#FDFAF4) — at fit on a 390 screen the left arrow sits
   almost entirely on it. 0.55 composited to rgb(137,133,127), a 3.66:1 contrast
   against the white glyph: past the 3:1 floor for non-text UI, thin for a 24 px
   chevron. 0.66 gives rgb(114,106,99) and 5.31:1, which is the ordinary text
   threshold and leaves the control legible on paper, on a pale wall inside a
   photo, and on the scrim. */
.pf-lightbox__nav:hover { background: rgba(43, 32, 24, 0.82); border-color: rgba(255, 255, 255, 0.42); }
.pf-lightbox__nav--prev { left: 16px; }
.pf-lightbox__nav--next { right: 16px; }
/* 400ms is the recover delay the gesture handler waits before clearing the flag,
   so the visual fade and the re-enable land together instead of the buttons
   appearing and then becoming clickable. */
.pf-lightbox[data-nav="hidden"] .pf-lightbox__nav { opacity: 0; pointer-events: none; }

/* ---- close ---- */
.pf-lightbox__close {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: 16px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.30);
  background: rgba(43, 32, 24, 0.66);
  color: #fff;
  cursor: pointer;
}
.pf-lightbox__close:hover { background: rgba(43, 32, 24, 0.82); border-color: rgba(255, 255, 255, 0.42); }

/* ---- bottom pill ---- */
.pf-lightbox__bar {
  position: absolute;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 999px;
  background: var(--paper-0);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
  max-width: calc(100vw - 24px);
}
.pf-lightbox__btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-700);
  cursor: pointer;
  font: inherit;
}
.pf-lightbox__btn:hover { background: rgba(43, 32, 24, 0.07); }
.pf-lightbox__btn--text { width: auto; padding: 0 14px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.02em; }
.pf-lightbox__pct {
  min-width: 48px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-700);
  font-variant-numeric: tabular-nums;
}
.pf-lightbox__side {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-500);
  padding: 0 8px 0 10px;
  border-left: 1px solid var(--line-200);
  white-space: nowrap;
}
.pf-lightbox__side[hidden] { display: none; }

/* one channel of announcements, never visible; the pct readout is aria-hidden so
   a screen reader does not narrate every intermediate step of a zoom */
.pf-lightbox__live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

html.pf-lightbox-open,
body.pf-lightbox-open { overflow: hidden; }

@media (max-width: 767px) {
  .pf-lightbox__viewport {
    padding: calc(64px + env(safe-area-inset-top, 0px)) 12px calc(88px + env(safe-area-inset-bottom, 0px));
  }
  .pf-lightbox__nav--prev { left: 8px; }
  .pf-lightbox__nav--next { right: 8px; }
  /* The bar keeps the specified 44px buttons and 8px gap at every width: it is
     292px wide at 390, which leaves 74px of slack. Shrinking it here would be
     fixing a problem that does not exist. */
}

@media (prefers-reduced-motion: reduce) {
  .pf-lightbox__nav, .pf-lightbox__close, .pf-lightbox__btn { transition: none; }
}
