/* ============================================================================
   Photo Templates editor — two-column layout (mirrors /customize)

   Left:  preview stack (t-shirt mockup at top + larger design canvas below)
   Right: options panel (template picker → colours → photo thumbs → captions
          → size grid → pricing → cart)

   Builds on top of style.css — reuses .configurator-grid, .preview-card,
   .options-form, .form-group, .color-swatches, .size-quantity-grid, etc.
   ============================================================================ */

/* Tighter hero — the editor doesn't need the full-bleed hero treatment. */
.hero--compact {
    margin-top: var(--spacing-md);
    margin-bottom: var(--spacing-lg);
}

/* ===========================================================================
   Sticky compact top bar — appears when customer scrolls past the preview
   ===========================================================================
   Three states:
   1. Default (at top): bar is hidden via translateY(-100%), announcement +
      header render normally.
   2. .pt-scrolled (body class added by JS): bar slides into view; the
      announcement bar + main header hide so the bar replaces them at the
      top of the viewport.
   3. Mobile breakpoints work the same way — no breakpoint-specific changes
      needed beyond grid sizing.
   =========================================================================== */

.pt-sticky-topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 1rem;
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
    /* Hide by default. JS toggles body.pt-scrolled to slide it down. */
    transform: translateY(-100%);
    transition: transform 0.22s ease;
    pointer-events: none;
}
body.pt-scrolled .pt-sticky-topbar {
    transform: translateY(0);
    pointer-events: auto;
}
body.pt-scrolled .announcement-bar,
body.pt-scrolled .header {
    /* Hide the normal site chrome — the sticky bar replaces them. */
    display: none;
}
.pt-sticky-topbar__back {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--color-border);
    border-radius: 50%;
    background: transparent;
    color: var(--color-text);
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.pt-sticky-topbar__back:hover {
    background: var(--color-bg-alt);
    border-color: var(--color-text-muted);
}
.pt-sticky-topbar__thumb {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm, 6px);
    background: var(--color-bg-alt);
    cursor: pointer;
    overflow: hidden;
}
.pt-sticky-topbar__thumb img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}
.pt-sticky-topbar__title {
    flex: 1 1 auto;
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pt-sticky-topbar__price {
    flex: 0 0 auto;
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--color-primary);
    font-variant-numeric: tabular-nums;
}
.pt-sticky-topbar__cta {
    flex: 0 0 auto;
    padding: 0.5rem 1rem;
    background: var(--color-primary);
    color: #ffffff;
    border: 0;
    border-radius: var(--radius-md);
    font-weight: 600;
    font-size: 0.875rem;
    cursor: pointer;
    transition: background 0.15s ease;
}
.pt-sticky-topbar__cta:hover {
    background: var(--color-brand-orange-dark, #c9521b);
}
@media (max-width: 480px) {
    /* On phones, drop the price column so the title + CTA have room. The
       customer can see the price in the panel below. */
    .pt-sticky-topbar { padding: 0.4rem 0.75rem; gap: 0.5rem; }
    .pt-sticky-topbar__price { display: none; }
    .pt-sticky-topbar__cta { padding: 0.4rem 0.75rem; font-size: 0.8rem; }
    .pt-sticky-topbar__back { width: 32px; height: 32px; }
    .pt-sticky-topbar__thumb { width: 32px; height: 32px; }
    .pt-sticky-topbar__title { font-size: 0.85rem; }
}

/* The .configurator-grid in style.css already gives us 2fr/1fr at ≥1280px.
   .pt-editor-grid is a styling hook for editor-only tweaks (sticky right
   column, design-card spacing, etc.) without monkey-patching .configurator-grid. */
.pt-editor-grid .options-panel {
    /* Long forms feel less scrolly when the right panel can move with content. */
    align-self: start;
}

/* ===========================================================================
   Left — t-shirt preview with zoom + grid toolbar
   ===========================================================================
   The DESIGN PREVIEW bottom canvas was retired 2026-06-06. The t-shirt
   mockup is now the only preview surface. A compact toolbar above it lets
   customers zoom in (1× → 1.5× → 2×) and toggle a positioning grid. Both
   states are pure CSS transforms — no Konva, no canvas engine.
   =========================================================================== */

.pt-view-toolbar {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 8px 10px;
    margin-top: var(--spacing-sm);
    background: var(--color-bg-alt);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}
.pt-view-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-sm, 6px);
    background: transparent;
    color: var(--color-text);
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.pt-view-btn:hover:not(:disabled) {
    background: var(--color-bg);
    border-color: var(--color-border);
}
.pt-view-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}
.pt-view-btn--toggle {
    width: auto;
    padding: 0 10px;
    gap: 6px;
    font-size: 0.8125rem;
    font-weight: 600;
}
.pt-view-btn--toggle.is-active {
    background: rgba(228, 99, 43, 0.12);
    border-color: var(--color-primary);
    color: var(--color-primary);
}
.pt-view-btn__label {
    /* On small screens just the icon — the label hides to keep the toolbar
       compact under the mockup. */
    display: inline;
}
@media (max-width: 480px) {
    .pt-view-btn__label { display: none; }
}
.pt-view-zoom-label {
    min-width: 44px;
    text-align: center;
    color: var(--color-text-muted);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
}

/* The stage wraps the mockup img. Zoom state lives on the stage's
   data-zoom attribute. The inner .pt-preview-frame grows wider+taller
   than the stage at zoom > 1×, which makes the stage scrollable. We
   don't use transform: scale() because scaled elements don't create
   overflow (visual blows up but layout box stays the same → no
   scrollbars, nothing to pan). Width/height scaling produces real
   overflow.

   The display: block override is load-bearing: .preview-image-container
   from configurator.css is display: flex, which shrinks our frame back
   to 100% even when we set width: 150% (flex's default flex-shrink: 1).
   Block layout lets the frame overflow freely. */
.pt-preview-stage {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--radius-md);
    /* Cursor reflects mode: default at 1× (nothing to do), grab when
       zoomed (drag to pan). pt-preview-stage--dragging swaps to
       grabbing while a pointer drag is in flight. */
}
/* Any data-zoom > 1 turns on overflow + pan cursor. At 0.75 the frame is
   smaller than the stage (centred whitespace around it) so there's
   nothing to pan — keep default cursor. The explicit per-level width
   rules below cover the active zoom set; --zoom is still mirrored to a
   custom property by JS for any future helpers. */
.pt-preview-stage[data-zoom="1.25"],
.pt-preview-stage[data-zoom="1.5"] {
    overflow: auto;
    cursor: grab;
    scrollbar-width: thin;
    /* iOS Safari: declare the gesture intent so the browser doesn't
       oscillate between scrolling the page and panning the stage. Without
       these the customer's finger drag feels janky — sometimes the page
       moves, sometimes the shirt does. With them, the stage owns x+y
       panning when zoomed and the page scroll is unaffected. */
    touch-action: pan-x pan-y;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}
.pt-preview-stage.pt-preview-stage--dragging {
    cursor: grabbing;
}
.pt-preview-frame {
    position: relative;
    width: 100%;
    height: 100%;
    transition: width .18s ease, height .18s ease;
    /* Recentre the frame on the cross-axis at every zoom so the design
       stays roughly centred in the viewport instead of clinging to the
       top-left. */
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Per-level zoom rules — explicit selectors are more reliable than
   calc(var(--zoom)) which silently no-ops in some browsers if the custom
   property cascade gets tripped up. JS still sets --zoom for any future
   pan helpers; CSS reads data-zoom on the stage. */
.pt-preview-stage[data-zoom="0.75"] .pt-preview-frame { width: 75%; height: 75%; }
.pt-preview-stage[data-zoom="1.25"] .pt-preview-frame { width: 125%; height: 125%; }
.pt-preview-stage[data-zoom="1.5"]  .pt-preview-frame { width: 150%; height: 150%; }
/* The Border toggle in the toolbar controls show_border on the
   server-side /render-mockup call, so the boundary box is drawn (or
   not) into the mockup PNG itself. No client-side overlay needed. */

/* ===========================================================================
   Right — options panel
   =========================================================================== */

/* ----- Template picker -------------------------------------------------- */
.pt-template-picker {
    display: flex;
    gap: var(--spacing-sm);
    flex-wrap: wrap;
}

.pt-template-card {
    flex: 1 1 140px;
    background: var(--color-bg);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--spacing-sm) var(--spacing-md);
    text-align: left;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    gap: 4px;
    transition: border-color .15s ease, transform .12s ease;
}
.pt-template-card:hover { border-color: var(--color-text-muted); }
.pt-template-card.is-active {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 2px rgba(228, 99, 43, 0.12) inset;
}
.pt-template-card__name {
    font-weight: 600;
    font-size: 0.875rem;
}
.pt-template-card__slots {
    color: var(--color-text-muted);
    font-size: 0.75rem;
}

/* ----- Multi-upload button + progress ---------------------------------- */
.pt-upload-row {
    display: flex;
    align-items: stretch;
    gap: 8px;
    margin-bottom: var(--spacing-sm);
}
.pt-upload-all {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 14px;
    background: var(--color-bg-alt);
    border: 1.5px dashed var(--color-text-muted);
    border-radius: var(--radius-md);
    color: var(--color-text);
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .12s ease, border-color .12s ease, transform .1s ease;
}
.pt-clear-all {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    background: transparent;
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    cursor: pointer;
    transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.pt-clear-all:hover:not(:disabled) {
    background: rgba(185, 28, 28, 0.08);  /* gentle red wash */
    border-color: var(--color-error, #b91c1c);
    color: var(--color-error, #b91c1c);
}
/* Disabled state — button stays visible (so customers always know the
   action is here) but goes greyed-out + unclickable when there are no
   photos to clear. Triggered via the `disabled` attribute, not a class,
   so the browser blocks pointer events natively. */
.pt-clear-all:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* Swap toggle — sits between Upload-all and Clear-all in the action row.
   Matches the row's grey-outlined treatment but adds an `is-active`
   orange-filled state so customers see they're in swap mode. */
.pt-swap-toggle {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 0 12px;
    height: 44px;
    background: transparent;
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.pt-swap-toggle:hover:not(:disabled) {
    background: var(--color-bg);
    border-color: var(--color-primary);
    color: var(--color-primary);
}
.pt-swap-toggle.is-active {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #fff;
}
.pt-swap-toggle:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.pt-swap-toggle__label {
    line-height: 1;
}

/* Swap-mode hint banner — visible only when swap toggle is active.
   Small instructional text above the photo grid. */
.pt-swap-hint {
    margin: 8px 0 0;
    padding: 8px 12px;
    background: rgba(228, 99, 43, 0.08);
    border: 1px solid rgba(228, 99, 43, 0.25);
    border-radius: var(--radius-md);
    color: var(--color-primary);
    font-size: 0.8125rem;
    font-weight: 500;
}

/* In swap mode: filled thumbs get a "tap me to select" affordance, and
   the source (first slot picked) gets a strong orange ring so customers
   know "this is the one I'm moving". The body class makes it easy to
   toggle all these states at once. */
body.pt-swap-mode .pt-thumb {
    cursor: pointer;
}
body.pt-swap-mode .pt-thumb.is-filled {
    cursor: pointer;
}
body.pt-swap-mode .pt-thumb:not(.is-filled) {
    /* Empty slots can still receive a moved photo, so keep them clickable
       but de-emphasised — the source is the photo, the empty slot is
       just a destination. */
    opacity: 0.6;
}
.pt-thumb--swap-source {
    outline: 3px solid var(--color-primary);
    outline-offset: -3px;
    box-shadow: 0 0 0 1px #fff inset;
}
.pt-upload-all:hover {
    background: var(--color-bg);
    border-color: var(--color-primary);
    color: var(--color-primary);
}
.pt-upload-all:active { transform: scale(0.99); }

.pt-upload-progress {
    margin: 0 0 var(--spacing-sm);
}
.pt-upload-progress.hidden { display: none; }
.pt-upload-progress__bar {
    height: 6px;
    background: var(--color-bg-alt);
    border-radius: 999px;
    overflow: hidden;
    border: 1px solid var(--color-border);
}
.pt-upload-progress__fill {
    height: 100%;
    width: var(--progress, 0%);
    background: var(--color-primary);
    border-radius: inherit;
    transition: width .25s ease;
}
.pt-upload-progress__label {
    margin-top: 4px;
    font-size: 0.75rem;
    color: var(--color-text-muted);
    text-align: center;
}

/* ----- Photo thumbnail grid -------------------------------------------- */
.pt-thumb-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--spacing-sm);
}

.pt-thumb {
    position: relative;
    aspect-ratio: 1;
    background: var(--color-bg-alt);
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    overflow: hidden;
    transition: border-color .15s ease, transform .12s ease;
    padding: 0;
}
.pt-thumb:hover { border-color: var(--color-text-muted); }
.pt-thumb.is-filled {
    border-style: solid;
    border-color: var(--color-border);
    cursor: grab;             /* hint that filled thumbs are draggable */
}
.pt-thumb.is-filled:active {
    cursor: grabbing;
}

/* Drag-to-swap visual feedback. Source thumb dims while being dragged
   so the user sees it's "lifted"; target thumb gets a dashed outline +
   subtle orange wash so they can see exactly where it'll drop. The
   floating preview follows the cursor (position: fixed, body-level). */
.pt-thumb--dragging {
    opacity: 0.4;
    cursor: grabbing;
}
.pt-thumb--drop-target {
    outline: 3px dashed var(--color-primary);
    outline-offset: -3px;
    background: rgba(228, 99, 43, 0.08);
}
.pt-thumb-drag-preview {
    position: fixed;
    pointer-events: none;
    z-index: 9999;
    width: 64px;
    height: 64px;
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
    overflow: hidden;
    transform: translate(-50%, -50%);
    background: var(--color-bg-alt);
}
.pt-thumb-drag-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.pt-thumb__file {
    /* Visually hidden; the whole .pt-thumb button is the click target. */
    position: absolute;
    width: 1px; height: 1px;
    opacity: 0;
    pointer-events: none;
}
.pt-thumb__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.pt-thumb__placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-text-muted);
}
.pt-thumb.is-filled .pt-thumb__placeholder { display: none; }
.pt-thumb__index {
    position: absolute;
    top: 4px;
    left: 6px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 0.6875rem;
    font-weight: 700;
    border-radius: 999px;
    padding: 1px 6px;
    line-height: 1.3;
}

/* ----- Caption list (right panel) -------------------------------------- */
/* Captions are organised as one bordered panel that contains BOTH the
   disclosure button (always visible header) AND the expanded body
   (visibility checkbox + caption rows). The button and body share the
   panel's single border so when open they read as one continuous card. */
.pt-captions-panel {
    background: var(--color-surface, #fafafa);
    border: 1px solid var(--color-border, #e5e5e5);
    border-radius: 8px;
    overflow: hidden;
    transition: border-color 0.15s ease;
}
.pt-captions-panel:hover {
    border-color: var(--color-border-hover, #d4d4d4);
}
/* Header row holds the disclosure button (stretches) + the "Show
   captions" checkbox (right-aligned). Both share the panel header so
   the checkbox is reachable WITHOUT expanding the panel. */
.pt-captions-header {
    display: flex;
    align-items: stretch;
    gap: 4px;
}
/* The disclosure button is the left side of the header. Borderless and
   transparent so the panel's outer border + background take over. */
.pt-captions-disclosure {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-sm);
    flex: 1 1 auto;
    min-width: 0;
    padding: 10px 12px;
    background: transparent;
    border: none;
    border-radius: 0;
    font: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--color-text, #222);
    cursor: pointer;
    user-select: none;
    transition: background 0.15s ease;
}
.pt-captions-disclosure:hover {
    background: var(--color-surface-hover, #f1f1f1);
}
.pt-captions-disclosure__label {
    flex: 1 1 auto;
    text-align: left;
}
.pt-captions-disclosure__chevron {
    flex-shrink: 0;
    font-size: 1.1rem;
    line-height: 1;
    color: var(--color-text-muted);
    transition: transform 0.18s ease;
    transform: rotate(90deg); /* points down when collapsed */
}
.pt-captions-disclosure[aria-expanded="true"] {
    background: var(--color-surface-hover, #f1f1f1);
    border-bottom: 1px solid var(--color-border, #e5e5e5);
}
.pt-captions-disclosure[aria-expanded="true"] .pt-captions-disclosure__chevron {
    transform: rotate(270deg); /* points up when expanded */
}
.pt-caption-list {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-xs);
    background: #fff;
    padding: 10px 12px 12px;
}
/* "Show captions" checkbox — lives in the panel header next to the
   disclosure button, NOT inside the expanded body. Sized to feel like
   a control chip rather than a label. */
.pt-captions-visible-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 0 12px;
    font-size: 0.8rem;
    color: var(--color-text-muted);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    flex-shrink: 0;
}
.pt-captions-visible-toggle input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
}
/* Collapse the input rows when the disclosure is closed. The button
   itself stays visible so the customer can re-open it. Chained selector
   (.pt-caption-list.pt-caption-list--hidden) has higher specificity than
   the bare .pt-caption-list rule above, so `display: none` wins
   regardless of source order. */
.pt-caption-list.pt-caption-list--hidden {
    display: none;
}

.pt-caption-row {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}

.pt-caption-row__index {
    flex-shrink: 0;
    width: 22px;
    text-align: center;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 700;
}

.pt-panel-caption {
    flex: 1 1 auto;
    font-size: 0.875rem;
    padding: 8px 10px;
}

/* ----- Admin-only action row (Download print PNG) ---------------------- */
/* Shown only when the visitor is authenticated as admin (template
   conditional). Slightly faded so it doesn't distract from the
   customer's primary CTA. */
.pt-dev-actions {
    margin-top: var(--spacing-xs);
}
.pt-save-print {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    font-size: 0.8125rem;
    font-weight: 500;
    opacity: 0.75;
}
.pt-save-print:hover {
    opacity: 1;
}

/* ----- Status line ----------------------------------------------------- */
.pt-editor__status {
    text-align: center;
    color: var(--color-text-muted);
    font-size: 0.8125rem;
    margin-top: var(--spacing-sm);
    min-height: 1.5em;
}
.pt-editor__status--error { color: var(--color-error, #b91c1c); }

/* ----- Border colour picker --------------------------------------------- */
.pt-border-swatches {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
/* Card variant — used by Film Strip's constrained variant picker so the
   three options (Classic / Dark / Retro Pop) read as labeled cards with
   a single palette dot inside, matching the Style picker design. */
.pt-border-swatches--cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 10px;
    align-items: stretch;
}
/* Style picker — same swatch dot as the border-colour picker, but each
   swatch is wrapped so a "★ Best" badge can sit underneath when the
   current shirt colour pairs with this style. */
/* Style picker — card layout.
   Each style is a flat white card. Inside: a 3-dot palette preview
   (text / frame / caption colour) and the style name. Active card has a
   thick black border. A "Recommended" pill sits at top-right when the
   current shirt colour matches the style's recommend_for_shirts. */
.pt-style-picker {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 10px;
    align-items: stretch;
}
.pt-style-swatch-wrap {
    position: relative;
    display: block;
}
.pt-border-swatch.pt-style-card {
    /* Override the small-circle .pt-border-swatch base rules below by
       coming after them in source order and re-declaring everything. */
    width: 100%;
    height: 100%;
    background: #fff;
    border: 1.5px solid var(--color-border, #e5e5e5);
    border-radius: 12px;
    padding: 14px 10px 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.pt-border-swatch.pt-style-card:hover {
    border-color: #c5c5c5;
    transform: none;
}
.pt-border-swatch.pt-style-card.is-active {
    border-color: #1a1a1a;
    border-width: 2px;
    padding: 13.5px 9.5px 11.5px; /* compensate the +0.5px border */
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.pt-style-palette {
    display: flex;
    gap: 8px;
    align-items: center;
    justify-content: center;
}
.pt-style-palette__dot {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.08);
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
    flex-shrink: 0;
}
.pt-style-label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--color-text, #222);
    white-space: nowrap;
    letter-spacing: 0.01em;
}
.pt-style-recommend {
    position: absolute;
    top: -8px;
    right: 8px;
    z-index: 2;
    background: #fdf1d8;
    color: #946a16;
    font-size: 0.65rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 999px;
    border: 1px solid #f1d191;
    line-height: 1;
    white-space: nowrap;
    display: none;
}
.pt-style-recommend.visible {
    display: block;
}

/* Legacy small-circle .pt-border-swatch — still used by the border-colour
   picker (Photo Study, Film Strip variants) where each swatch IS just a
   single coloured disc. Style-picker buttons override this with
   .pt-border-swatch.pt-style-card above. */
.pt-border-swatch {
    width: 32px; height: 32px;
    border-radius: 50%;
    border: 2px solid var(--color-border);
    cursor: pointer;
    padding: 0;
    transition: transform .12s ease, border-color .15s ease;
}
.pt-border-swatch:hover { transform: scale(1.08); }
.pt-border-swatch.is-active {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 2px rgba(228, 99, 43, 0.18);
}
/* Override legacy rules when the swatch IS a style card. */
.pt-border-swatch.pt-style-card,
.pt-border-swatch.pt-style-card.is-active {
    transform: none !important;
}

/* T-shirt color picker — card tiles.
   Scoped to #pt-color-swatches so /customize (which uses the bare
   .color-swatches class) keeps the legacy small-swatch row. */
#pt-color-swatches.color-swatches {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(85px, 1fr));
    gap: 10px;
}
#pt-color-swatches .color-swatch {
    background: #fff;
    border: 1.5px solid var(--color-border, #e5e5e5);
    border-radius: 12px;
    padding: 14px 8px 12px;
    gap: 8px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
#pt-color-swatches .color-swatch:hover {
    background: #fff;
    border-color: #c5c5c5;
}
#pt-color-swatches .color-swatch.active {
    border-color: #1a1a1a;
    border-width: 2px;
    padding: 13.5px 7.5px 11.5px; /* compensate the +0.5px border */
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
#pt-color-swatches .swatch-inner {
    width: 46px;
    height: 46px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10);
}
#pt-color-swatches .swatch-label {
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--color-text, #222);
}
.pt-border-color-custom {
    width: 36px; height: 36px;
    border: 1px dashed var(--color-border);
    background: var(--color-bg);
    padding: 2px;
    cursor: pointer;
}

/* ===========================================================================
   Adjust Photo modal — per-slot pan + zoom
   ===========================================================================
   Opens when the customer taps a thumbnail that already has a photo.
   The preview frame copies the slot's shape (rounded_rect / circle / heart)
   so the customer drags their photo through the SAME crop window that will
   appear in the final print. The photo is just an <img> with a CSS
   transform; JS keeps state.slots[id].tx/ty/scale in sync as the customer
   drags / pinches / scrolls / drags the slider.
   =========================================================================== */
.pt-adjust-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
}
.pt-adjust-modal.hidden { display: none; }
.pt-adjust-modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(31, 26, 18, 0.55);
}
.pt-adjust-modal__panel {
    position: relative;
    width: min(420px, 92vw);
    max-height: 92vh;
    overflow: auto;
    background: var(--color-bg);
    border-radius: var(--radius-lg);
    box-shadow: 0 30px 60px rgba(0, 0, 0, 0.35);
    padding: var(--spacing-lg);
    display: flex;
    flex-direction: column;
    gap: var(--spacing-md);
}
.pt-adjust-modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-sm);
}
.pt-adjust-modal__title {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--color-text);
}
.pt-adjust-modal__close {
    background: transparent;
    border: 0;
    padding: 6px;
    color: var(--color-text-muted);
    cursor: pointer;
    border-radius: var(--radius-sm, 6px);
    transition: background .12s ease, color .12s ease;
}
.pt-adjust-modal__close:hover {
    background: var(--color-bg-alt);
    color: var(--color-text);
}

/* Preview stage — neutral backdrop so the customer's photo stands out. */
.pt-adjust-stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-xs);
}
/* Outer frame — represents the SLOT bounds (sticker outline / film mat).
   Width + height set inline by JS to match the slot's aspect ratio so
   square Sticker Sheet slots stay square while tall Film Strip slots
   render taller-than-wide. Capped at ~320×380 so the modal stays
   compact at any aspect. Just a thin guide colour around the print-area
   indicator — captures pointer events for drag/pinch. */
.pt-adjust-frame {
    position: relative;
    width: 320px;          /* JS overrides per-slot */
    height: 320px;
    background: rgba(31, 26, 18, 0.04);
    touch-action: none;                      /* swallow native swipe so drag works */
    cursor: grab;
    border-radius: 6%;
    user-select: none;
}
.pt-adjust-frame:active { cursor: grabbing; }
.pt-adjust-frame[data-slot-shape="circle"] { border-radius: 50%; }
.pt-adjust-frame[data-slot-shape="heart"] {
    border-radius: 0;
}

/* Inner crop — the photo-bounds rectangle that ACTUALLY prints. The
   2px white outline + dark shadow makes it stand out from the photo
   so the customer sees exactly what gets cropped vs kept. Positioned
   absolutely inside the frame using inset percentages computed by JS
   from slot.photoInset (server's get_photo_inset).

   overflow:hidden clips the photo to the crop edges, mirroring the
   server's mask. data-slot-shape inherits the heart/circle clip so
   non-rectangular slots show the actual mask shape. */
.pt-adjust-crop {
    position: absolute;
    overflow: hidden;
    background: rgba(245, 237, 221, 0.4);     /* cream paper, matches empty slots */
    border-radius: 6%;
    box-shadow:
        0 0 0 2px #fff,
        0 0 0 3px rgba(31, 26, 18, 0.55);
    pointer-events: none;                     /* gestures hit the frame */
}
.pt-adjust-crop[data-slot-shape="circle"] { border-radius: 50%; }
.pt-adjust-crop[data-slot-shape="heart"] {
    border-radius: 0;
    clip-path: url(#pt-heart-clip);
}

/* The photo is positioned with JS-applied inline style:
       transform: translate(tx_css, ty_css) scale(customer_scale);
       width: <auto-fit covering width>
       height: <auto-fit covering height>
   Centred in the crop so transforms read relative to crop centre.
   pointer-events: none so all drag events target the outer frame. */
.pt-adjust-photo {
    position: absolute;
    left: 50%;
    top: 50%;
    transform-origin: center center;
    transform: translate(-50%, -50%) scale(1);
    pointer-events: none;
    max-width: none;     /* override global img scaling */
    user-select: none;
}
.pt-adjust-hint {
    margin: 4px 0 0;
    font-size: 0.75rem;
    color: var(--color-text-muted);
    text-align: center;
}

.pt-adjust-slider-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--spacing-sm);
}
.pt-adjust-slider-label {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--color-text);
}
.pt-adjust-slider {
    width: 100%;
    accent-color: var(--color-primary);
}
.pt-adjust-slider-value {
    min-width: 44px;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-size: 0.8125rem;
    color: var(--color-text-muted);
}

.pt-adjust-modal__footer {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: var(--spacing-sm);
}
.pt-adjust-modal__footer .btn {
    width: 100%;
    padding: 10px 12px;
    font-size: 0.875rem;
}
@media (max-width: 380px) {
    .pt-adjust-modal__footer {
        grid-template-columns: 1fr;
    }
    /* Frame dimensions are JS-driven; the mobile breakpoint just shrinks
       the panel via .pt-adjust-modal__panel max-width. The JS size cap
       (320×380) clamps automatically because the panel's max width is
       smaller. */
}

/* ===========================================================================
   Confirm modal — replaces window.confirm() for destructive actions
   ===========================================================================
   Shares the visual language of .pt-adjust-modal (backdrop + panel) so the
   editor reads as one cohesive UI rather than the OS-native confirm popup
   that doesn't match the brand. JS swaps the title / body / button labels
   on each call so one static markup serves every prompt.
   =========================================================================== */
.pt-confirm-modal {
    position: fixed;
    inset: 0;
    z-index: 1100;                /* above the photo-adjust modal */
    display: flex;
    align-items: center;
    justify-content: center;
}
.pt-confirm-modal.hidden { display: none; }
.pt-confirm-modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(31, 26, 18, 0.55);
}
.pt-confirm-modal__panel {
    position: relative;
    width: min(380px, 92vw);
    background: var(--color-bg);
    border-radius: var(--radius-lg);
    box-shadow: 0 30px 60px rgba(0, 0, 0, 0.35);
    padding: var(--spacing-lg);
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
    /* Gentle pop-in so the modal feels responsive without being noisy. */
    animation: pt-confirm-pop 0.18s ease-out;
}
@keyframes pt-confirm-pop {
    from { transform: scale(0.95); opacity: 0; }
    to   { transform: scale(1);    opacity: 1; }
}
.pt-confirm-modal__title {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    color: var(--color-text);
    line-height: 1.3;
}
.pt-confirm-modal__body {
    margin: 0;
    font-size: 0.9375rem;
    line-height: 1.5;
    color: var(--color-text-muted);
}
.pt-confirm-modal__footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--spacing-sm);
    margin-top: var(--spacing-sm);
}
.pt-confirm-modal__footer .btn {
    min-width: 100px;
    padding: 10px 16px;
    font-size: 0.875rem;
}
/* Danger variant — used when the OK action is destructive (Clear all
   photos, etc.). Tints the primary button red instead of brand orange so
   the customer registers the irreversibility. */
.pt-confirm-modal--danger .pt-confirm-ok {
    background: var(--color-error, #b91c1c);
    border-color: var(--color-error, #b91c1c);
    color: #fff;
}
.pt-confirm-modal--danger .pt-confirm-ok:hover {
    filter: brightness(0.92);
}
@media (max-width: 380px) {
    .pt-confirm-modal__footer {
        flex-direction: column-reverse;
    }
    .pt-confirm-modal__footer .btn {
        width: 100%;
    }
}

/* ----- Font picker ------------------------------------------------------ */
/* Per-template curated set. Each swatch is a self-contained button that
   shows "Aa" rendered in the actual typeface, plus the brand label below.
   .pt-font-swatch--<key> is set via inline style="font-family: 'pt-<key>'"
   in the template so each preview reads in its own face. */
.pt-font-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
}
.pt-font-swatch {
    /* Grid cells handle sizing now — no flex basis needed. min-width: 0
       lets the cell shrink without an intrinsic floor when the panel is
       narrow on mobile. */
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 12px 8px 8px;
    background: var(--color-bg);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: border-color .15s ease, transform .12s ease;
}
.pt-font-swatch:hover { border-color: var(--color-text-muted); }
.pt-font-swatch.is-active {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 2px rgba(228, 99, 43, 0.12) inset;
}
.pt-font-swatch__preview {
    font-size: 1.875rem;
    line-height: 1;
    color: var(--color-text);
}
.pt-font-swatch__label {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/* ===========================================================================
   Font faces — caption-view legacy + customer-picker preview faces
   ===========================================================================
   The customer-facing preview swatches in #pt-font-grid render in the
   exact font they describe. Each `pt-<key>` family below MUST match the
   `key` field on the template's fonts.title list in registry.py — same
   binding the server uses when validating state.font_key. Adding a font:
     1. Drop the .ttf/.otf into assets/fonts/
     2. Add a `pt-<key>` @font-face block here
     3. Add the font dict to the template's `fonts.title` in registry.py
   =========================================================================== */
@font-face {
    font-family: 'KeeponTruckin';
    src: url('/assets/fonts/KeeponTruckin.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'pt-keepon';
    src: url('/assets/fonts/KeeponTruckin.ttf') format('truetype');
    font-display: swap;
}
@font-face {
    font-family: 'pt-abril';
    src: url('/assets/fonts/AbrilFatface-Regular.otf') format('opentype');
    font-display: swap;
}
@font-face {
    font-family: 'pt-recoleta';
    src: url('/assets/fonts/Recoleta-Regular.otf') format('opentype');
    font-display: swap;
}
@font-face {
    font-family: 'pt-parisienne';
    src: url('/assets/fonts/Parisienne-Regular.ttf') format('truetype');
    font-display: swap;
}

/* ===========================================================================
   Sitewide horizontal-scroll guards
   ===========================================================================
   Mirrors style.css's body rule (overflow-x: clip). `clip` is critical
   instead of `hidden` because `overflow: hidden` on an ancestor BREAKS
   `position: sticky` on its descendants — the sticky tries to anchor
   inside the hidden ancestor's scroll context, which doesn't exist, so
   the sticky element never sticks to the viewport. `clip` clips the
   overflow visually without making the element a scroll container, so
   sticky descendants (our mobile .preview-panel) work normally.

   style.css already has body { overflow-x: clip } so we just need the
   .configurator clip here.
   =========================================================================== */
.configurator { overflow-x: clip; }

/* Compact editor back button — the site header is hidden on phones, so this
   is the in-page way back. Hidden on desktop (the full header is present). */
.pt-editor-back { display: none; }

/* ===========================================================================
   Mobile polish — phones only (≤767px, where .configurator-grid stacks)
   ===========================================================================
   The two-column desktop layout stacks at 768px so .preview-panel sits
   above .options-panel. On a tall phone screen, the customer scrolls
   through the options panel and loses sight of the t-shirt mockup.

   position: sticky keeps the WUUF header + announcement bar + hero
   visible at the top of the page on initial paint. As the customer
   scrolls, the header scrolls away naturally and the preview "sticks"
   at top of viewport once it would scroll past. Options scroll under it.
   Earlier attempt with position: fixed at top:0 covered the header
   entirely and required a 40vh padding-top reservation that produced a
   giant blank gap — both gone.
   =========================================================================== */
@media (max-width: 767px) {
    /* Full-space editor on phones: drop the site header + announcement bar
       entirely (they're only in the sitewide chrome, not needed while
       designing). A compact back button (.pt-editor-back) on the preview
       handles navigation; the full site header stays on desktop. */
    .announcement-bar,
    .header {
        display: none;
    }
    /* ===================================================================
       App-shell layout (2026-07-13) — the real cure for the iOS preview
       stutter. iOS repositions `position: sticky` elements on the MAIN
       thread while page scroll runs on the COMPOSITOR thread, so a sticky
       preview always lags the scroll a frame or two — no amount of GPU
       promotion fixes that (we tried: commits 9d1fb32/9da0e65/4a1f3f2/
       4a729b9). Instead we make the page itself un-scrollable: the body is
       a fixed 100svh flex column, the preview is a STATIC row (never
       moves), and only .options-panel scrolls in its own overflow
       container. Nothing repositions during scroll → stutter is
       structurally impossible, and the preview stays permanently visible.
       All scoped to body.pt-editor-shell so only the three editor pages
       (which set that body class) are affected; desktop is untouched. */
    body.pt-editor-shell .footer,
    body.pt-editor-shell .pt-sticky-topbar {
        display: none;
    }
    /* The page never scrolls, so the Safari URL bar never collapses and
       100svh is the stable correct height. Do NOT use dvh — with a static
       body it only reflows during the (never-triggered) bar animation. */
    html:has(body.pt-editor-shell) {
        height: 100%;
        overflow: hidden;
        overscroll-behavior: none;
    }
    body.pt-editor-shell {
        height: 100vh;   /* fallback for pre-svh browsers */
        height: 100svh;
        overflow: hidden;
        overscroll-behavior: none;
        /* body is already display:flex; flex-direction:column (style.css:147) */
    }
    body.pt-editor-shell .main {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
        padding: 0;
    }
    body.pt-editor-shell .configurator,
    body.pt-editor-shell .configurator > .container {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    /* Kill configurator.css's mobile `.configurator { padding-top: spacing-lg }`
       — in the shell the preview band is pinned to the very top, we don't want
       24px of dead space above it. */
    body.pt-editor-shell .configurator {
        padding-top: 0;
    }
    /* The compact hero (title/subtitle) would eat ~90px of the shell — hide
       it on phones; the preview itself is the visual anchor now. */
    body.pt-editor-shell .hero--compact {
        display: none;
    }
    body.pt-editor-shell .pt-editor-grid {
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-height: 0;
        gap: 0;
    }

    /* Preview band — a truly static flex row (no sticky/fixed, no GPU
       hacks — nothing repositions on scroll anymore). position: relative
       only so the absolute .pt-editor-back button anchors to it. The
       full-bleed width breakout (see original note below) is retained. */
    .pt-editor-grid .preview-panel {
        position: relative;
        /* Neutralise the `top: calc(60px + safe-area + spacing-lg)` inherited
           from the base (desktop-sticky) .preview-panel rule — on a
           position:relative element that inherited top would shove the whole
           band ~84px down, opening a big blank gap above it. */
        top: auto;
        flex: 0 0 auto;
        z-index: 2;                    /* band shadow renders above the scroller */
        background: var(--color-bg);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
        /* Clip anything that overshoots (the zoomed .pt-preview-frame) so
           the band never introduces horizontal drag. */
        overflow: hidden;
        /* Break out of .container's 24px padding to span the full screen
           width. max-width: none is critical — without it the (former)
           grid track-width caps the explicit width back. */
        width: calc(100% + 48px);
        max-width: none;
        margin-left: -24px;
        margin-right: -24px;
        /* Breathing space above the shirt + clear the iPhone notch. */
        padding-top: calc(env(safe-area-inset-top, 0px) + 12px);
    }
    .pt-editor-grid .preview-card {
        margin-bottom: 0;
    }

    /* Options panel — the ONLY scroller in the shell. min-height: 0 is
       load-bearing: without it a flex child refuses to shrink below its
       content size and the whole page grows/scrolls again. */
    .pt-editor-grid .options-panel {
        flex: 1 1 auto;
        min-height: 0;
        align-self: stretch;           /* override align-self:start (line ~138) */
        overflow-y: auto;
        /* Explicit overflow-x: hidden is required. With only overflow-y set,
           WebKit computes overflow-x as `auto`, so any sub-pixel horizontal
           overshoot becomes a draggable horizontal scroll — the "options slide
           left/right" bug. Clip it: the panel must never scroll sideways. */
        overflow-x: hidden;
        overscroll-behavior: contain;  /* keep momentum inside the list */
        /* .pt-style-recommend pill overhangs card tops by 8px — pad so the
           first row isn't clipped by the scroll container. */
        padding-top: 14px;
        scroll-padding-top: 14px;
        padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
    }

    /* Show the back button, pinned top-left over the (now static) preview
       band — it's absolutely positioned inside .preview-panel (which is
       position: relative). Clears the iPhone notch. */
    .pt-editor-back {
        display: inline-flex;
        position: absolute;
        top: max(10px, env(safe-area-inset-top, 0px));
        left: 12px;
        z-index: 12;
        width: 40px;
        height: 40px;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.92);
        border: 1px solid var(--color-border);
        color: var(--color-text);
        box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    /* Stage gets a DEFINITE height (the shell fixes the band; the stage no
       longer sizes off viewport-height caps). This makes the zoom % sizing
       robust and gives the preview a generous, stable viewing area. Tune
       34svh–42svh on-device if the shirt feels cramped/too large. */
    .pt-editor-grid .preview-image-container {
        aspect-ratio: auto;
        width: 100%;
        max-width: 100%;
        height: 38svh;
        max-height: none;
        margin-left: auto;
        margin-right: auto;
    }
    /* The image fills the stage; object-fit: contain preserves aspect and
       centres it. scale(1.5) crops the ~12–15% white margin baked into the
       mockup PNGs via the stage's overflow: hidden (do NOT drop it — the
       shirt would float in dead whitespace). No GPU-promotion hacks: with
       the app-shell nothing repositions during scroll, so translateZ/
       will-change/backface-visibility are dead weight (and will-change
       wastes memory). */
    .pt-editor-grid .preview-image {
        display: block;
        width: 100%;
        height: 100%;
        max-width: none;
        max-height: none;
        object-fit: contain;
        transform: scale(1.5);
        transform-origin: center;
    }
    /* When zoomed, force the image to fill the frame edge-to-edge. style.css
       mobile sets width:auto + max-width:100% so the image stays at its
       natural pixel size (~1024px) — past zoom ≈ 250% the frame outgrows
       that natural size and going to 400% just adds whitespace instead of
       more image. Locking the image to 100% × 100% of the (now-larger)
       frame fixes that. object-fit: contain still preserves aspect ratio. */
    .pt-editor-grid .pt-preview-stage:not([data-zoom="1"]) .preview-image {
        width: 100%;
        max-width: none;
        height: 100%;
        max-height: none;
        object-fit: contain;
    }
    .pt-editor-grid .pt-view-toolbar {
        margin-top: 6px;
        padding: 6px 8px;
    }

    /* ----- Touch targets — bump anything under the iOS 44pt minimum -- */
    /* View toolbar buttons. Default desktop 36px → 40px on phones. */
    .pt-view-btn {
        width: 40px;
        height: 40px;
    }
    .pt-view-btn--toggle {
        width: auto;
        min-width: 40px;
        height: 40px;
    }
    /* Font swatch — short by default because flex-grow stretches width
       not height. Set an explicit comfortable min-height. */
    .pt-font-swatch {
        min-height: 72px;
    }
    /* Border colour swatch — 32px circle is too small for a fingertip.
       Bump the visible swatch + add hit padding so the actual touch area
       comfortably crosses 44px. */
    .pt-border-swatch {
        width: 40px;
        height: 40px;
    }
    /* Photo thumb grid — default 4 cols at this width get tight on
       <360px phones, but keep 4 cols on standard phones. The thumb
       buttons fill the cells and aspect-ratio: 1 gives them height. */
    .pt-thumb {
        min-height: 64px;
    }

    /* Upload-row uniformity: keep the three action buttons the SAME height
       on phones. The "Upload all photos" label wraps to two lines on narrow
       screens (esp. in English), making that button taller than Swap/Clear
       and shifting the row in Y. Pin it to 44px + nowrap, and collapse the
       Swap button to an icon-only 44px square (like Clear) so there's enough
       width for the label to stay on one line even at 320px. */
    .pt-upload-all {
        height: 44px;
        white-space: nowrap;
        min-width: 0;
    }
    .pt-swap-toggle__label {
        display: none;
    }
    .pt-swap-toggle {
        padding: 0;
        width: 44px;
        justify-content: center;
    }
}
