/* ============================================================================
   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; }

/* ===========================================================================
   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) {
    /* WEARWUUF brand bar + announcement strip stay locked at the top
       of the viewport so the customer always has brand context while
       scrolling. Stacked: announcement first (top: 0), header below
       it (top: 64px ≈ announcement height). Scoped to photo-templates
       pages because this rule lives in photo_templates.css which only
       loads on the editor route (the URL has a wildcard in it — careful
       not to repeat the slash-star-slash CSS-comment hazard).

       Combined height (announcement 64 + header 55 ≈ 119px) drives
       the .preview-panel sticky top below. */
    /* The three sticky bars stack at the top of the viewport on scroll.
       Heights vary by device (iPhone notch adds ~47px to the
       announcement; landscape changes header padding; some browsers
       round differently), so we measure them in JS on load + resize
       and write the values to CSS custom properties:
         --pt-ann-h     ← announcement-bar.offsetHeight
         --pt-header-h  ← header.offsetHeight
       Hardcoded `top` values are kept as fallback defaults for the
       brief window before JS runs. */
    .announcement-bar {
        position: sticky;
        top: 0;
        z-index: 100;
        /* Reserve safe-area for the iPhone notch INSIDE the announcement
           bar (since it's now the topmost element of the visible page).
           Without this the announcement content would sit behind the
           notch on iPhones with a top inset. */
        padding-top: max(0.45rem, env(safe-area-inset-top));
    }
    .header {
        position: sticky;
        top: var(--pt-ann-h, 27px);
        z-index: 99;
        background: var(--color-bg);
        /* style.css sets padding-top: max(--spacing-sm, env(safe-area-
           inset-top)) which assumes the header is the topmost element
           and needs to clear the iPhone notch. With the announcement
           bar sitting above it as sticky, the notch is already handled
           — keeping that 47px notch padding produced extra space on
           iPhone (invisible in Playwright because there's no notch).
           Override to plain --spacing-sm. */
        padding-top: var(--spacing-sm);
    }
    .pt-editor-grid .preview-panel {
        position: sticky;
        /* Stack below the announcement + header — both measured in JS
           and written to CSS variables. Fallback 82 ≈ 27 + 55. */
        top: calc(var(--pt-ann-h, 27px) + var(--pt-header-h, 55px));
        z-index: 10;
        background: var(--color-bg);
        /* Band height: 55vh gives the customer a generous t-shirt
           preview area (was 40vh — too short, the band felt cramped).
           Larger viewing area, still leaves enough room below for
           multiple option rows on a phone screen. */
        max-height: 55vh;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
        /* Defence-in-depth: clip any inner element that overshoots the
           viewport width (the zoomed .pt-preview-frame at width: calc(100%
           * --zoom) is the obvious source). Combined with the sitewide
           .configurator { overflow-x: clip } above this prevents the
           customer from ever dragging the page horizontally. */
        overflow: hidden;
        /* Break out of .container's 24px horizontal padding so the band
           spans the full screen width — gives the customer a generous
           viewing area instead of a 270×270 square cramped in the middle.
           Explicit width is required because .preview-panel is a CSS
           Grid child of .configurator-grid; grid track-sizing pins its
           width to the track and ignores negative margins alone. The +48
           is (.container left padding 24) + (.container right padding 24)
           so the band's content area is exactly viewport-wide. The
           max-width: none override is critical — without it,
           configurator-grid's track-width acts as an implicit cap and
           the explicit width gets clamped back. */
        width: calc(100% + 48px);
        max-width: none;
        margin-left: -24px;
        margin-right: -24px;
        /* Breathing space above the t-shirt so the customer can see the
           print-boundary at the top — without padding, the mockup hugs
           the band edge and the "where does my design end" cue is lost. */
        padding-top: 12px;
    }
    .pt-editor-grid .preview-card {
        margin-bottom: 0;
    }
    .pt-editor-grid .preview-image-container {
        /* Drop the inherited aspect-ratio: 1 so the stage can take the
           full band width instead of being a 270×270 square. The img
           inside has object-fit: contain — it scales to fit and stays
           centred even at the wider frame. */
        aspect-ratio: auto;
        width: 100%;
        max-width: 100%;
        max-height: calc(55vh - 90px);   /* ~70px toolbar + ~12px padding-top + breathing room */
        margin-left: auto;
        margin-right: auto;
    }
    /* T-shirt centring (issue #2): style.css mobile rules give the img
       `width: auto; margin: 0 auto; display: block` which centers in a
       block-flow parent BUT inside .pt-preview-frame (flex) the auto
       margins are no-ops and the img can drift left when its intrinsic
       width is smaller than the frame. Reasserting explicit centring
       here keeps it pinned dead-centre.
       max-height is ONLY applied at zoom=1 — at zoom > 1 we want the
       image to follow the (now-larger) frame, not be height-capped. The
       data-zoom="1" selector below scopes it precisely to the un-zoomed
       state. */
    .pt-editor-grid .preview-image {
        margin-left: auto;
        margin-right: auto;
        display: block;
        /* The mockup PNGs (assets/tshirt/{color}-front-shirt.png) have
           ~12-15% white margin baked in around the shirt graphic.
           configurator.css applies transform: scale(1.25) to crop some
           of that margin so the shirt fills more of the visible area,
           but on the photo-templates fixed-band layout that's still not
           enough — visible whitespace appears below the shirt before
           the zoom toolbar. Bumping to 1.5 clips an extra ring of the
           margin via the stage's overflow: hidden. */
        transform: scale(1.5);
        transform-origin: center;
    }
    .pt-editor-grid .pt-preview-stage[data-zoom="1"] .preview-image {
        max-height: calc(55vh - 70px);
    }
    /* 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;
    }
    /* Lock the frame's width to the stage so the calc(100% * --zoom)
       rule at zoom=1 (= 100%) doesn't pick up rounding/sub-pixel drift
       and offset the centred image. At zoom > 1, the frame still expands
       (we want it scrollable inside the stage) — the override applies
       only at zoom=1 via the data-zoom attribute selector. */
    .pt-editor-grid .pt-preview-stage[data-zoom="1"] .pt-preview-frame {
        width: 100%;
    }
    .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;
    }
}
