/**
 * DevVN Frame Designer — DOM-based frontend (Phase 3 refactor).
 * Slots are absolutely positioned DIVs over a mockup IMG; the shape is
 * cut via CSS clip-path; the photo fills via object-fit; frame style is
 * an overlay <div> with a stretched border PNG.
 */

/* Stage holds mockup + slot layer. aspect-ratio set inline from the
   product's real_w_cm × real_h_cm so slots can position in % and the
   layout stays fluid at any container width. */
.devvn-fd-stage {
    position: relative;
    width: 100%;
    background: var(--devvn-fd-bg-soft, #F9FAFB);
    overflow: hidden;
}
/* Parent class so `.woocommerce img` (0,1,1) doesn't override our
   height:100% — keeps the mockup filling the stage's aspect-locked box
   on shops where the mockup's own aspect doesn't match the canvas. */
.devvn-fd-stage .devvn-fd-stage__mockup {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    user-select: none;
    -webkit-user-drag: none;
}
.devvn-fd-stage__slots {
    position: absolute;
    inset: 0;
}
.devvn-fd-stage__free-canvas {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.devvn-fd-stage__free-canvas[hidden] { display: none; }

/* Empty slot defaults — white card, dashed teal border, drop shadow. */
.devvn-fd-slot {
    position: absolute;
    transform-origin: top left;
    cursor: pointer;
    will-change: transform;
    /* WordPress 6.1+ global styles inject:
         html :where([style*="border-color"]) { border-style: solid; }
       It matches ANY element whose inline `style` merely CONTAINS the substring
       "border-color" — and a "Có viền" slot carries `--photo-border-color` inline
       (set by photo-style-applier-dom.js). That accidentally drew a solid,
       medium-width, currentColor (dark) border around the WHOLE slot bbox,
       OUTSIDE the frame PNG — the "viền đen bao phủ bên ngoài" bug, visible only
       when a bordered photo style was picked. We never use a real CSS border on
       the slot (the placeholder outline is an SVG), so force it off. The class
       selector (0,1,0) outranks the injected `html :where(...)` rule (0,0,1). */
    border: 0;
}
.devvn-fd-slot__inner {
    position: relative;
    width: 100%;
    height: 100%;
    background: transparent;
    box-sizing: border-box;
    overflow: hidden;
}

/* Inline SVG outline — only used now as the "empty / preview" dashed
   placeholder. Frames are PNGs (see .devvn-fd-slot__frame-png), so we
   no longer need the variable-driven stroke system that used to swap
   in colored frame strokes. Hardcoded width + dash; non-scaling-stroke
   keeps it visually uniform regardless of slot aspect.

   Filled & has-frame slots hide the outline entirely — the photo (and
   PNG frame) carry the visual. */
.devvn-fd-slot__outline {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
}
.devvn-fd-slot__outline > * {
    fill: #fff;
    stroke: var(--devvn-fd-accent, #2EC4B6);
    stroke-width: 2;
    stroke-dasharray: 6 4;
    vector-effect: non-scaling-stroke;
}
.devvn-fd-slot.is-filled .devvn-fd-slot__outline,
.devvn-fd-slot.has-frame .devvn-fd-slot__outline {
    display: none;
}

/* Photo "viền" — driven by the picked photo style's border_config
   (e.g. "Có viền" enables it, "Không viền" disables). The matting
   colour lives on the photo WRAP (not the slot inner) so it's clipped
   to the slot's silhouette — for a hex slot the white margin only
   shows inside the hex, not in the bbox corners.

   Variables the JS sets per slot:
       --photo-padding-pct   — uniform padding (% of slot width)
       --photo-border-color  — matting fill colour */
.devvn-fd-slot.has-photo-border .devvn-fd-slot__photo-wrap {
    background: var(--photo-border-color, #FFFFFF);
}

/* Frame PNG overlay — sits ABOVE the photo so its opaque borders cover
   any photo overflow. Stretched to slot bbox via 100% sizing; the PNG
   itself defines the silhouette via its alpha channel.
   z-index=3 so it stacks above photo (default 0) and matting (z=1). */
/* Parent class bumps specificity past WC core's `.woocommerce img`
   (0,1,1) so the frame PNG stretches to the slot bbox via height:100%
   instead of falling back to its natural aspect. */
.devvn-fd-slot .devvn-fd-slot__frame-png {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: fill;
    pointer-events: none;
    user-select: none;
    -webkit-user-drag: none;
    z-index: 3;
}
.devvn-fd-slot .devvn-fd-slot__frame-png[hidden] { display: none; }

/* Drop shadow for framed slots — uses `filter: drop-shadow` (not
   `box-shadow`) so the shadow follows the slot's silhouette via
   clip-path on descendants (hex shadow on hex slot, etc.). Stronger
   than the plain `.is-filled` shadow below to convey weight of frame.
   Selector includes `.is-filled` to bump specificity above the bare
   `.is-filled` rule that comes later — has-frame implies is-filled,
   so framed slots win. Note: `box-shadow` ignores clip-path, which
   would leak rectangular bbox shadow around hex/triangle/rhombus
   slots — that's the bug this avoids. Filter doesn't support inset,
   so the 3D "sunk-in" inset shadows are dropped here. */
.devvn-fd-slot.is-filled.has-frame .devvn-fd-slot__inner {
    filter: drop-shadow(0 6px 12px rgba(15, 23, 42, 0.25));
}

/* Photo wrapper — owns positioning, shape clipping, and "viền" padding
   so the IMG itself can stay simple (just fills the wrap with object-fit).
   Wrap takes the slot's full bbox; clip-path / border-radius shape it
   per slot type. When viền is on, padding shrinks the wrap's content
   box; the IMG inside contain-fits within that box.

   Explicit width/height: 100% in addition to inset:0 so the IMG's
   percentage height resolves correctly against the wrap's content box
   (some browsers flake on percentage height when the parent's height
   is only inset-derived). box-sizing: border-box keeps the padding
   inside the 100% dims. */
.devvn-fd-slot__photo-wrap {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    pointer-events: none;
}

.devvn-fd-slot.has-photo-border .devvn-fd-slot__photo-wrap {
    padding: var(--photo-padding-pct, 0%);
}

/* Parent class bumps specificity to (0,2,0) — beats WooCommerce core's
   `.woocommerce img { height: auto }` (0,1,1) so our slot photos
   actually fill their wrap and `object-fit: cover` can crop. Without
   this, the WC rule wins, the IMG takes its natural aspect, and a
   freshly-uploaded photo renders contain-style with letterboxing
   instead of cover-cropping to the slot silhouette. */
.devvn-fd-slot .devvn-fd-slot__photo {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    transform-origin: center center;
    pointer-events: none;
    user-select: none;
    -webkit-user-drag: none;
}
.devvn-fd-slot .devvn-fd-slot__photo[hidden] { display: none; }

/* Viền on: photo COVER-fits the wrap's content box (the padded box).
   This means the photo is cropped to the wrap's content aspect (which
   matches the slot's silhouette), giving a UNIFORM viền on all four
   edges from the wrap's padding regardless of the photo's original
   aspect ratio. The cropper modal lets the customer pick which part
   of the photo lands inside the crop area. (object-fit:contain would
   letterbox on only two sides when aspects differ — not what we want.) */
.devvn-fd-slot.has-photo-border .devvn-fd-slot__photo {
    object-fit: cover;
}

/* Shape clipping — applied to THREE places, all with the same path:
 *   1. .__photo-wrap     — so the matting colour follows the slot's
 *                          silhouette (white viền inside hex/etc.,
 *                          not in the bbox corners).
 *   2. .__photo          — so the photo silhouette is the slot's shape.
 *                          Combined with the wrap's padding, the photo
 *                          becomes a SMALLER copy of the slot shape
 *                          (e.g. smaller hex inside larger hex), and the
 *                          gap between them is the visible viền.
 *   3. .__frame-png      — so a rectangular frame PNG also clips to the
 *                          slot silhouette (looks like a hex frame on
 *                          hex slots even from a rectangular PNG). */
.devvn-fd-slot--rectangle .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--rectangle .devvn-fd-slot__photo,
.devvn-fd-slot--rectangle .devvn-fd-slot__frame-png,
.devvn-fd-slot--square    .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--square    .devvn-fd-slot__photo,
.devvn-fd-slot--square    .devvn-fd-slot__frame-png { border-radius: calc(var(--slot-radius-cm, 0) * 1px); }

.devvn-fd-slot--circle    .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--circle    .devvn-fd-slot__photo,
.devvn-fd-slot--circle    .devvn-fd-slot__frame-png,
.devvn-fd-slot--ellipse   .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--ellipse   .devvn-fd-slot__photo,
.devvn-fd-slot--ellipse   .devvn-fd-slot__frame-png { border-radius: 50%; overflow: hidden; }

.devvn-fd-slot--rhombus   .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--rhombus   .devvn-fd-slot__photo,
.devvn-fd-slot--rhombus   .devvn-fd-slot__frame-png { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }

.devvn-fd-slot--triangle[data-variant="right"]    .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--triangle[data-variant="right"]    .devvn-fd-slot__photo,
.devvn-fd-slot--triangle[data-variant="right"]    .devvn-fd-slot__frame-png { clip-path: polygon(0% 0%, 100% 100%, 0% 100%); }

.devvn-fd-slot--triangle[data-variant="equilateral"] .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--triangle[data-variant="equilateral"] .devvn-fd-slot__photo,
.devvn-fd-slot--triangle[data-variant="equilateral"] .devvn-fd-slot__frame-png,
.devvn-fd-slot--triangle:not([data-variant])      .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--triangle:not([data-variant])      .devvn-fd-slot__photo,
.devvn-fd-slot--triangle:not([data-variant])      .devvn-fd-slot__frame-png,
.devvn-fd-slot--triangle[data-variant="isosceles"] .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--triangle[data-variant="isosceles"] .devvn-fd-slot__photo,
.devvn-fd-slot--triangle[data-variant="isosceles"] .devvn-fd-slot__frame-png { clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }

.devvn-fd-slot--hexagon[data-orientation="flat_top"]   .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--hexagon[data-orientation="flat_top"]   .devvn-fd-slot__photo,
.devvn-fd-slot--hexagon[data-orientation="flat_top"]   .devvn-fd-slot__frame-png { clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); }

.devvn-fd-slot--hexagon:not([data-orientation])        .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--hexagon:not([data-orientation])        .devvn-fd-slot__photo,
.devvn-fd-slot--hexagon:not([data-orientation])        .devvn-fd-slot__frame-png,
.devvn-fd-slot--hexagon[data-orientation="pointy_top"] .devvn-fd-slot__photo-wrap,
.devvn-fd-slot--hexagon[data-orientation="pointy_top"] .devvn-fd-slot__photo,
.devvn-fd-slot--hexagon[data-orientation="pointy_top"] .devvn-fd-slot__frame-png { clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); }

/* Plus icon centered in empty slots. Hidden once a photo or admin
   preview is shown. Sized via container queries (cqmin = smaller of the
   slot's own width/height) so the icon stays a sensible fraction of the
   slot regardless of how big or small admin made it. */
.devvn-fd-slot {
    /* size containment unlocks `cqmin` for children (resolves to slot's
       smaller dimension); slot already has explicit width+height in %, so
       size containment doesn't break flow. */
    container-type: size;
}
.devvn-fd-slot__plus {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--devvn-fd-accent-dark, #1E8E84);
    /* ~25% of the slot's smaller side, clamped 8–28px so it stays
       readable on tiny slots and doesn't dominate big ones. */
    font-size: clamp(8px, 25cqmin, 28px);
    font-weight: 400;
    line-height: 1;
    pointer-events: none;
    user-select: none;
}
.devvn-fd-slot.is-filled .devvn-fd-slot__plus,
.devvn-fd-slot.is-preview .devvn-fd-slot__plus {
    display: none;
}

/* Drop shadow lifts the photo card off the mockup once the slot has
   real content (preview state stays flat — it's a sample). */
.devvn-fd-slot.is-filled .devvn-fd-slot__inner {
    filter: drop-shadow(0 4px 8px rgba(15, 23, 42, 0.20));
}

/* Preview-only — sample image at 0.7 opacity, dashed outline kept. */
.devvn-fd-slot.is-preview .devvn-fd-slot__photo {
    opacity: 0.7;
}

/* Hover affordance — drop-shadow filter (not box-shadow) so it tracks
   the slot silhouette like the resting-state shadow. */
.devvn-fd-slot:hover .devvn-fd-slot__inner {
    filter: drop-shadow(0 6px 14px rgba(15, 23, 42, 0.28));
}

/* Upload-state indicator — small pulsing dot in the slot corner while
   the background pipeline is mid-flight. Soft enough to ignore yet
   visible enough that a confused customer can tell something is
   happening. Cleared once /upload-image returns. */
.devvn-fd-slot.is-uploading .devvn-fd-slot__inner::after {
    content: "";
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--devvn-fd-accent, #2EC4B6);
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
    animation: devvn-fd-upload-pulse 1.2s ease-in-out infinite;
    pointer-events: none;
    z-index: 5;
}
.devvn-fd-slot.is-upload-error .devvn-fd-slot__inner::after {
    content: "!";
    position: absolute;
    top: 4px;
    right: 4px;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: #DC2626;
    color: #FFFFFF;
    font: 700 10px/14px system-ui, sans-serif;
    text-align: center;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
    pointer-events: none;
    z-index: 5;
}
@keyframes devvn-fd-upload-pulse {
    0%, 100% { opacity: 0.45; transform: scale(0.85); }
    50%      { opacity: 1;    transform: scale(1.05); }
}

/* Free layout — slots that the customer adds are draggable / resizable.
   Phase 3 ships the same DOM widget; canvas-toolbar palette spawns a
   new <div> here and free-handler attaches drag/resize listeners. */
.devvn-fd-slot--free {
    cursor: move;
}
.devvn-fd-slot--free.is-active .devvn-fd-slot__inner {
    outline: 2px solid var(--devvn-fd-accent-dark, #1E8E84);
    outline-offset: 2px;
}

/* ─────────────────────────────────────────────────────────────
   Dimension overlay (DOM version) — CAD-style measurement lines
   ───────────────────────────────────────────────────────────── */
.devvn-fd-dim-overlay-dom {
    --fd-dim-color: var(--devvn-fd-accent-dark, #1E8E84);
    --fd-dim-line: 1.5px;
    --fd-dim-cap: 10px;
    /* Gap between the slots' bounding box and the dim line. Bumped up so
     * the dimension bars sit clearly outside the slot perimeter and the
     * cap markers don't visually overlap the slot's dashed outline. */
    --fd-dim-gap: 40px;
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 4;
}
.devvn-fd-dim-overlay-dom[hidden] { display: none; }

/* Container box for one dimension; positioned in % over the slot bbox.
   We push the bar OUTSIDE the bbox via translateX/Y so the geometry math
   stays simple — left/top are the bbox edge, transform offsets the line. */
.devvn-fd-dim-line {
    position: absolute;
    color: var(--fd-dim-color);
}
.devvn-fd-dim-line[hidden] { display: none; }
.devvn-fd-dim-line__bar {
    position: absolute;
    background: var(--fd-dim-color);
}
.devvn-fd-dim-line__cap {
    position: absolute;
    background: var(--fd-dim-color);
}

/* Horizontal — drawn under the slots' bounding box */
.devvn-fd-dim-line--h {
    height: 0;
    transform: translateY(var(--fd-dim-gap));
}
.devvn-fd-dim-line--h .devvn-fd-dim-line__bar {
    left: 0;
    right: 0;
    top: 50%;
    height: var(--fd-dim-line);
    transform: translateY(-50%);
}
.devvn-fd-dim-line--h .devvn-fd-dim-line__cap {
    width: var(--fd-dim-line);
    height: var(--fd-dim-cap);
    top: 50%;
    transform: translateY(-50%);
}
.devvn-fd-dim-line--h .devvn-fd-dim-line__cap--start { left: 0; }
.devvn-fd-dim-line--h .devvn-fd-dim-line__cap--end   { right: 0; }

/* Vertical — drawn to the right of the slots' bounding box */
.devvn-fd-dim-line--v {
    width: 0;
    transform: translateX(var(--fd-dim-gap));
}
.devvn-fd-dim-line--v .devvn-fd-dim-line__bar {
    top: 0;
    bottom: 0;
    left: 50%;
    width: var(--fd-dim-line);
    transform: translateX(-50%);
}
.devvn-fd-dim-line--v .devvn-fd-dim-line__cap {
    height: var(--fd-dim-line);
    width: var(--fd-dim-cap);
    left: 50%;
    transform: translateX(-50%);
}
.devvn-fd-dim-line--v .devvn-fd-dim-line__cap--start { top: 0; }
.devvn-fd-dim-line--v .devvn-fd-dim-line__cap--end   { bottom: 0; }

/* Numeric label — white pill centered on the bar. JS may shrink
   `font-size` when the bar is too short to host the natural 12px size
   (dimensions-overlay-dom.js → _fitLabelToContainer). Padding is in
   `em` so the pill stays visually balanced at any shrunk size. */
.devvn-fd-dim-line__label {
    position: absolute;
    background: #fff;
    color: var(--fd-dim-color);
    font-size: 12px;
    font-weight: 700;
    padding: 0.18em 0.65em;
    border-radius: 6px;
    border: 1px solid var(--fd-dim-color);
    white-space: nowrap;
    line-height: 1.4;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.devvn-fd-dim-line--h .devvn-fd-dim-line__label {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}
.devvn-fd-dim-line--v .devvn-fd-dim-line__label {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}

/* Slot center label — clean white pill with brand-dark text. JS shrinks
   `font-size` for tiny slots (dimensions-overlay-dom.js →
   _fitLabelToContainer), capping at 13px and bottoming out at 8px so the
   text never spills outside the slot. Padding in `em` keeps the pill
   proportional at any shrunk size. */
.devvn-fd-slot__dim-label {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #fff;
    color: var(--devvn-fd-accent-dark, #1E8E84);
    font-size: 13px;
    font-weight: 700;
    padding: 0.23em 0.6em;
    border-radius: 6px;
    border: 1px solid var(--devvn-fd-accent-dark, #1E8E84);
    pointer-events: none;
    white-space: nowrap;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
    z-index: 3;
}

/* ─────────────────────────────────────────────────────────────
   Dim mode — canvas-zone gets `.is-dim-mode` while the dimension
   toggle is on. Strips the customer's design back to the empty
   white-card state so the measurement labels read cleanly:
     - hide the CUSTOMER photo (real upload, .is-filled) and the
       custom frame PNG,
     - bring back the dashed outline on filled / framed slots,
     - drop the elevation filter so slots look "flat" again,
     - cancel the photo-style "viền" inset.
   The dimension label is the only thing painted inside the white
   card; the centered "+" stays hidden in dim mode so it doesn't
   clash with the label.

   Exception: if the customer ALSO toggled the preview button on,
   the slot carries `.is-preview` — in that combo we keep the
   default preview image visible so it reads as a sample over the
   white card, with the dimension label sitting on top via z=3. */
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.is-filled:not(.is-preview) .devvn-fd-slot__photo,
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot__frame-png {
    display: none;
}
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.is-filled .devvn-fd-slot__outline,
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.has-frame .devvn-fd-slot__outline {
    display: block;
}
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.is-filled .devvn-fd-slot__inner,
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.is-filled.has-frame .devvn-fd-slot__inner {
    filter: none;
}
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot.has-photo-border .devvn-fd-slot__photo-wrap {
    background: transparent;
    padding: 0;
}
.devvn-fd-canvas-zone.is-dim-mode .devvn-fd-slot__plus {
    display: none;
}

/* ─────────────────────────────────────────────────────────────
   Cropper modal — modern flat redesign
   ───────────────────────────────────────────────────────────── */
.devvn-fd-cropper-modal {
    --fd-cm-radius: 16px;
    --fd-cm-border: #EEF0F3;
    --fd-cm-text: #0F172A;
    --fd-cm-muted: #64748B;
    --fd-cm-soft: #F4F6F9;
    --fd-cm-danger: #DC2626;
    --fd-cm-danger-soft: #FEF2F2;
    position: fixed;
    inset: 0;
    z-index: 100050;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    animation: devvn-fd-cm-fade 180ms ease-out both;
}
.devvn-fd-cropper-modal[hidden] { display: none !important; }

@keyframes devvn-fd-cm-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes devvn-fd-cm-pop {
    from { opacity: 0; transform: translateY(8px) scale(0.98); }
    to   { opacity: 1; transform: translateY(0)   scale(1); }
}

.devvn-fd-cropper-modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    cursor: pointer;
}

.devvn-fd-cropper-modal__panel {
    position: relative;
    background: #fff;
    border-radius: var(--fd-cm-radius);
    width: min(760px, 100%);
    max-height: calc(100vh - 32px);
    display: flex;
    flex-direction: column;
    box-shadow:
        0 1px 2px rgba(15, 23, 42, 0.04),
        0 12px 32px rgba(15, 23, 42, 0.18),
        0 32px 80px rgba(15, 23, 42, 0.22);
    overflow: hidden;
    animation: devvn-fd-cm-pop 220ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

/* Header */
.devvn-fd-cropper-modal__header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 20px;
    border-bottom: 1px solid var(--fd-cm-border);
}
.devvn-fd-cropper-modal__title {
    margin: 0;
    flex: 1;
    font-size: 17px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--fd-cm-text);
    letter-spacing: -0.01em;
}
.devvn-fd-cropper-modal__close {
    width: 36px;
    height: 36px;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 10px;
    color: var(--fd-cm-muted);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background-color 120ms ease, color 120ms ease;
    min-height: auto;
    margin: 0;
}
.devvn-fd-cropper-modal__close:hover {
    background: var(--fd-cm-soft);
    color: var(--fd-cm-text);
}
.devvn-fd-cropper-modal__close:focus-visible {
    outline: 2px solid var(--devvn-fd-accent, #2EC4B6);
    outline-offset: 2px;
}

/* Body + stage */
.devvn-fd-cropper-modal__body {
    padding: 20px;
    overflow-y: auto;
    flex: 1;
}
.devvn-fd-cropper-modal__stage {
    background: var(--fd-cm-soft);
    border: 1px solid var(--fd-cm-border);
    border-radius: 12px;
    overflow: hidden;
    aspect-ratio: 4 / 3;
    max-height: 60vh;
}
.devvn-fd-cropper-modal__stage img {
    display: block;
    max-width: 100%;
    height: auto;
}
/* Shape silhouette overlay injected inside cropper-view-box */
.devvn-fd-cropper-shape-overlay {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    display: block;
    z-index: 5;
}
.devvn-fd-cropper-modal__hint {
    text-align: center;
    color: var(--fd-cm-muted);
    font-size: 13px;
    margin-top: 12px;
    line-height: 1.5;
}

/* Footer + actions */
.devvn-fd-cropper-modal__footer {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 20px;
    border-top: 1px solid var(--fd-cm-border);
    background: #fff;
}
.devvn-fd-cropper-modal__spacer { flex: 1; }

.devvn-fd-cropper-btn {
    appearance: none;
    border: 1px solid var(--fd-cm-border);
    background: #fff;
    color: var(--fd-cm-text);
    padding: 10px 18px;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 40px;
    margin: 0;
}
.devvn-fd-cropper-btn:hover {
    background: var(--fd-cm-soft);
    border-color: #D8DDE5;
}
.devvn-fd-cropper-btn:active { transform: translateY(1px); }
.devvn-fd-cropper-btn:focus-visible {
    outline: 2px solid var(--devvn-fd-accent, #2EC4B6);
    outline-offset: 2px;
}

.devvn-fd-cropper-btn--danger {
    color: var(--fd-cm-danger);
    border-color: #FECACA;
}
.devvn-fd-cropper-btn--danger:hover {
    background: var(--fd-cm-danger-soft);
    border-color: #FCA5A5;
    color: var(--fd-cm-danger);
}

.devvn-fd-cropper-btn--primary {
    background: linear-gradient(90deg, var(--devvn-fd-accent, #2EC4B6) 0%, var(--devvn-fd-cta-end, #1E3A8A) 100%);
    color: #fff;
    border-color: transparent;
    border: 0;
    padding: 10px 22px;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(46, 196, 182, 0.25);
}
.devvn-fd-cropper-btn--primary:hover {
    background: linear-gradient(90deg, var(--devvn-fd-accent-dark, #1E8E84) 0%, #172E6E 100%);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 2px 4px rgba(15, 23, 42, 0.1), 0 6px 16px rgba(46, 196, 182, 0.32);
}

/* Mobile: full-screen modal, sticky footer */
@media (max-width: 600px) {
    .devvn-fd-cropper-modal { padding: 0; }
    .devvn-fd-cropper-modal__panel {
        width: 100%;
        height: 100%;
        max-height: 100vh;
        border-radius: 0;
    }
    .devvn-fd-cropper-modal__header { padding: 8px 16px; }
    .devvn-fd-cropper-modal__title { font-size: 16px; }
    .devvn-fd-cropper-modal__body { padding: 16px; }
    .devvn-fd-cropper-modal__stage {
        aspect-ratio: auto;
        height: calc(100vh - 220px);
        max-height: none;
    }
    .devvn-fd-cropper-modal__footer {
        padding: 12px 16px;
        gap: 8px;
        flex-wrap: wrap;
    }
    .devvn-fd-cropper-btn {
        padding: 10px 14px;
        font-size: 13px;
        flex: 1 1 auto;
    }
    .devvn-fd-cropper-modal__spacer { display: none; }
    .devvn-fd-cropper-btn--primary { flex: 1 1 100%; order: -1; }
}

/* ---- Whole-canvas frame overlay (frame_scope=canvas) ---- */
.devvn-fd-canvas-frame {
    position: absolute;
    inset: 0;
    box-sizing: border-box;
    pointer-events: none;
    /* Above slots (z 2-3) and text layers (z 4) — the physical frame is
     * the outermost element around the whole artwork. */
    z-index: 6;
    border: 0 solid transparent;
}
.devvn-fd-canvas-frame[hidden] {
    display: none;
}

/* ---- Text layers (admin-defined, customer-editable) ---- */
.devvn-fd-text-layers {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 4;
}
.devvn-fd-text-layer {
    position: absolute;
    white-space: nowrap;
    /* Match Fabric's IText default so admin canvas and frontend agree. */
    line-height: 1.16;
    visibility: hidden;
}
.devvn-fd-text-layer.is-sized {
    visibility: visible;
}
/* Customer-draggable caption — re-enable pointer events over the
 * pointer-events:none stage layer, and hint that it can be moved. */
.devvn-fd-text-layer.is-draggable {
    pointer-events: auto;
    cursor: move;
    touch-action: none;
    outline: 1px dashed transparent;
    outline-offset: 3px;
    transition: outline-color .12s ease;
}
.devvn-fd-text-layer.is-draggable:hover,
.devvn-fd-text-layer.is-dragging {
    outline-color: var(--devvn-fd-accent, #2EC4B6);
}

/* ---- "Text content" form section ---- */
.devvn-fd-text-fields {
    margin: 12px 0;
}
.devvn-fd-text-fields__row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 8px 0;
}
/* Shared control look — mirrors the picker chips. NOTE: background-color
 * only (never the `background` shorthand — it wipes the native <select>
 * arrow, see project CSS gotchas). */
.devvn-fd-text-fields__row input[type="text"],
.devvn-fd-text-fields__row input[type="number"],
.devvn-fd-text-fields__row select {
    height: 40px;
    padding: 6px 12px;
    border: 2px solid #c3c3c3;
    border-radius: var(--devvn-fd-radius, 10px);
    background-color: var(--devvn-fd-bg-soft, #fff);
    font-size: 14px;
    line-height: 1.3;
    color: #1f2937;
    box-shadow: none;
    transition: border-color .12s ease;
}
.devvn-fd-text-fields__row input[type="text"]:hover,
.devvn-fd-text-fields__row input[type="number"]:hover,
.devvn-fd-text-fields__row select:hover {
    border-color: rgba(46, 196, 182, 0.5);
}
.devvn-fd-text-fields__row input[type="text"]:focus,
.devvn-fd-text-fields__row input[type="number"]:focus,
.devvn-fd-text-fields__row select:focus {
    border-color: var(--devvn-fd-accent, #2EC4B6);
    background-color: #fff;
    outline: none;
    box-shadow: 0 0 0 3px rgba(46, 196, 182, 0.15);
}
.devvn-fd-text-fields__content {
    flex: 1 1 auto;
    min-width: 0;
}
.devvn-fd-text-fields__size {
    width: 76px;
    flex: 0 0 auto;
    text-align: center;
}
.devvn-fd-text-fields__font {
    max-width: 160px;
    flex: 0 0 auto;
}
.devvn-fd-text-fields__color {
    width: 40px;
    height: 40px;
    padding: 3px;
    border: 2px solid #c3c3c3;
    border-radius: var(--devvn-fd-radius, 10px);
    background-color: #fff;
    flex: 0 0 auto;
    cursor: pointer;
    transition: border-color .12s ease;
}
.devvn-fd-text-fields__color:hover {
    border-color: rgba(46, 196, 182, 0.5);
}
.devvn-fd-text-fields__drag-hint {
    font-size: 12px;
    color: #6b7280;
    font-style: italic;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 0 1 auto;
}
.devvn-fd-text-fields__drag-hint::before {
    content: "\2725"; /* four-pointed star / move glyph */
    color: var(--devvn-fd-accent, #2EC4B6);
    font-style: normal;
}
/* A layer whose only permitted edit is dragging: no visible control, just
 * the hint carrying its hidden position inputs. Tighten the empty row. */
.devvn-fd-text-fields__row--drag-only {
    margin: 4px 0;
}
@media (max-width: 540px) {
    .devvn-fd-text-fields__row {
        flex-wrap: wrap;
    }
    .devvn-fd-text-fields__content {
        flex-basis: 100%;
    }
}
