/* ---- design tokens --------------------------------------------------
   Palette/radius/spacing lifted from a design reference the operator
   provided (indigo primary, warm-gray background, amber/blue/green status
   colors, Pretendard type). Kept as custom properties so every component
   below reads from one place instead of repeating hex values — which is
   also what makes the dark theme below possible without touching every
   rule that uses color. */
:root {
  --bg: #f6f7fb;
  --card: #ffffff;
  --border: #e5e7eb;
  --text: #111827;
  --text2: #6b7280;
  --text3: #9ca3af;
  --primary: #4f46e5;
  --primary-dark: #4338ca;
  --primary-bg: #eef2ff;
  --primary-border: #c7d2fe;
  --amber: #92400e;
  --amber-bg: #fffbeb;
  --amber-border: #fde68a;
  --amber-chip: #fde68a;
  --amber-text: #78350f;
  --green: #059669;
  --green-bg: #ecfdf5;
  --blue-bg: #eff6ff;
  --blue-border: #bfdbfe;
  --blue-text: #1e3a8a;
  --danger: #dc2626;
  --shadow-pop: 0 10px 30px rgba(17, 24, 39, .08);
  --shadow-soft: 0 6px 16px rgba(17, 24, 39, .06);
  /* Subtle interactive fills — button/row hover, a resting "dashed" button,
     a track/groove background (segmented control, dashed:hover). Three near-
     identical light grays in the original design (#fafafa/#f9fafb/#f3f4f6)
     collapsed into two tokens; dark mode needs its own values for these
     regardless, since "very slightly off-white" has no dark equivalent. */
  --bg-soft: #f9fafb;
  --bg-well: #f3f4f6;
  /* Checkbox/radio/toggle-track border — deliberately a shade stronger than
     --border, which is tuned for card/divider lines, not a control outline. */
  --control-border: #d1d5db;
}

/* ---- dark theme --------------------------------------------------------
   Light tokens live on bare :root above (the default — no explicit opt-in
   needed, and the only set an SSR/no-JS render ever sees). Dark values are
   layered on in two passes that intentionally overlap:
     1. OS preference (prefers-color-scheme), guarded by :not([data-theme=
        "light"]) so the in-page toggle can still force light on a dark-mode
        OS.
     2. An explicit [data-theme="dark"] — set by the toggle below — so the
        toggle works the same regardless of what the OS prefers.
   Same values in both; the guard is what changes, not the palette. Preview
   panels (.cart-preview / .cancel-preview-box) are deliberately NOT part of
   this — see the scoped re-pin further down — because they render what an
   actual (light) storefront page looks like, not this admin tool's own UI. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d0e14;
    --card: #17181f;
    --border: #2b2d3a;
    --text: #f1f2f6;
    --text2: #a4a7b8;
    --text3: #74778a;
    --primary: #6366f1;
    --primary-dark: #4338ca;
    --primary-bg: #232544;
    --primary-border: #3730a3;
    --amber: #fbbf24;
    --amber-bg: #2e2410;
    --amber-border: #6b4c14;
    --amber-chip: #4a3410;
    --amber-text: #fcd34d;
    --green: #34d399;
    --green-bg: #0f2b22;
    --blue-bg: #16233d;
    --blue-border: #2d4470;
    --blue-text: #93c5fd;
    --danger: #f87171;
    --shadow-pop: 0 10px 30px rgba(0, 0, 0, .5);
    --shadow-soft: 0 6px 16px rgba(0, 0, 0, .4);
    --bg-soft: #1e2029;
    --bg-well: #24263a;
    --control-border: #454759;
  }
}
:root[data-theme="dark"] {
  --bg: #0d0e14;
  --card: #17181f;
  --border: #2b2d3a;
  --text: #f1f2f6;
  --text2: #a4a7b8;
  --text3: #74778a;
  --primary: #6366f1;
  --primary-dark: #4338ca;
  --primary-bg: #232544;
  --primary-border: #3730a3;
  --amber: #fbbf24;
  --amber-bg: #2e2410;
  --amber-border: #6b4c14;
  --amber-chip: #4a3410;
  --amber-text: #fcd34d;
  --green: #34d399;
  --green-bg: #0f2b22;
  --blue-bg: #16233d;
  --blue-border: #2d4470;
  --blue-text: #93c5fd;
  --danger: #f87171;
  --shadow-pop: 0 10px 30px rgba(0, 0, 0, .5);
  --shadow-soft: 0 6px 16px rgba(0, 0, 0, .4);
  --bg-soft: #1e2029;
  --bg-well: #24263a;
  --control-border: #454759;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
  color: var(--text);
  background: var(--bg);
  transition: background .15s, color .15s;
}

/* Preview panels (.cart-preview wraps the cart/cycle-popup/product-page
   mocks; .cancel-preview-box wraps the retention-popup mocks) render what an
   ACTUAL storefront page looks like — mostly light-background Cafe24 skins,
   confirmed against real screenshots throughout this file's mock-* rules.
   They are not part of this admin tool's own UI, so they must not go dark
   with it; a dark cart mockup would misrepresent what shoppers actually see.
   Re-pinning the tokens these subtrees read (rather than hardcoding every
   mock-* color individually) means: the rules below keep using var(--text)
   etc. exactly as already written, any preview element added later inherits
   the pin automatically, and this stays the ONE place that opts a whole
   subtree out of theming instead of that decision being scattered. */
.cart-preview, .cancel-preview-box {
  --bg: #f6f7fb;
  --card: #ffffff;
  --border: #e5e7eb;
  --text: #111827;
  --text2: #6b7280;
  --text3: #9ca3af;
  --primary: #4f46e5;
  --shadow-pop: 0 10px 30px rgba(17, 24, 39, .08);
  --shadow-soft: 0 6px 16px rgba(17, 24, 39, .06);
  /* Re-pinning the custom properties above only changes what var(--text)
     RESOLVES TO for anything that explicitly writes color: var(--text)
     inside this scope. Plain `color` is an inherited property, so anything
     here that never sets its own color — .mock-section-title, .rd-preview-
     popup h2, etc. — was still inheriting the computed (possibly dark-mode)
     color from body instead of picking up the pin. This line is what
     actually re-establishes the inherited value from here down. */
  color: var(--text);
}
/* Mobile-first: base rules below target a phone-width admin session (the
   common case — operators check/tweak this mid-conversation on their
   phone at least as often as at a desk), then min-width queries layer on
   desktop-only room-to-breathe instead of the other way around. */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  color: var(--text);
  flex-wrap: wrap;
}
.topbar-left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-width: 0; }
.topbar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.topbar h1 { font-size: 14.5px; font-weight: 800; margin: 0; white-space: nowrap; }
@media (min-width: 481px) {
  .topbar { padding: 14px 24px; }
  .topbar h1 { font-size: 16px; }
}
/* App mark — cart handle doubling as a repeat/cycle arrow, looping back into
   the basket ("장바구니 → 반복 구매"). Same tile shape/spirit as the design
   reference's sidebar/mobile-header logo tile. The svg inherits `color` via
   currentColor, so it recolors for free anywhere this class is reused. */
.topbar-mark {
  width: 30px; height: 30px; border-radius: 9px; background: var(--primary);
  color: #fff; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.badge {
  font-size: 12px; font-weight: 600;
  background: var(--green-bg);
  color: var(--green);
  padding: 4px 10px 4px 8px;
  border-radius: 999px;
  display: inline-flex; align-items: center; gap: 6px;
}
.badge::before {
  content: ''; width: 6px; height: 6px; border-radius: 999px; background: var(--green);
}
.topbar-btn {
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.topbar-btn:hover { background: var(--bg-soft); }
@media (min-width: 481px) {
  .topbar-btn { padding: 8px 16px; font-size: 13px; }
}
/* "재인증" is the one topbar action that means "something needs your
   attention" (missing/expired scope) — amber, matching .warn-banner's
   language, so it reads as a status action rather than a plain nav button. */
#reauthBtn { background: var(--amber-bg); border-color: var(--amber-border); color: var(--amber); }
#reauthBtn:hover { background: var(--amber-chip); }

/* Theme toggle — icon-only, square instead of .topbar-btn's text padding.
   All three icons stay in the DOM always; exactly one is shown, chosen by
   the STORED preference (data-theme absent = 자동, "light", "dark") — NOT
   by which theme is currently rendered. Those are different questions: in
   자동 mode the page might be rendering dark right now (OS says so), but
   the icon should still show "자동", or the toggle would look like it's
   stuck on dark with no way back to 자동. So this deliberately does NOT
   consult prefers-color-scheme at all, unlike the color tokens above. */
#themeToggleBtn.icon-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px; }
.theme-icon-light, .theme-icon-dark { display: none; }
:root[data-theme="light"] #themeToggleBtn .theme-icon-system { display: none; }
:root[data-theme="light"] #themeToggleBtn .theme-icon-light { display: inline-flex; }
:root[data-theme="dark"] #themeToggleBtn .theme-icon-system { display: none; }
:root[data-theme="dark"] #themeToggleBtn .theme-icon-dark { display: inline-flex; }

/* Widened from 760px, then 900px, then 1080px — on an actual desktop
   monitor 1080px still left a lot of unused side margin and forced the PC
   cart mock (see .mock-pc-layout) to scale down more than it needed to.
   1440px lets a real desktop window show the PC preview at (near) natural
   size; very wide monitors get a bit more still via the vw-based rule
   below. Mobile is unaffected either way — max-width only caps, it never
   forces a minimum, so a narrower viewport still gets its full width. */
.container { max-width: 1440px; margin: 24px auto; padding: 0 16px; }
@media (min-width: 1700px) {
  .container { max-width: 92vw; }
}
.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 20px;
  margin-bottom: 20px;
}
.card h2 { font-size: 15px; font-weight: 800; margin: 0 0 16px; }
@media (min-width: 681px) {
  .card { border-radius: 20px; padding: 28px 32px; }
  .card h2 { font-size: 16px; }
}
label { display: block; font-size: 13px; color: var(--text2); margin-bottom: 12px; }
label.row { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 13.5px; }
label.row input { margin: 0; }
/* Gotcha (found live, narrow-screen only): everything after the input becomes
   its own flex item — a <b>/<strong> tag followed by plain text as a SIBLING
   creates two (or more) items that shrink independently, and on a narrow
   screen the short bold item can get squeezed to a near-zero column while
   the other item hogs space, wrapping Korean text one character per line.
   Fix: wrap ALL of a label's post-input content (bold tag included) in ONE
   <span> so it's a single flex item and just wraps as one normal paragraph. */
input[type="text"], input[type="number"], textarea, select {
  width: 100%;
  margin-top: 4px;
  padding: 10px 13px;
  border: 1px solid var(--border);
  border-radius: 10px;
  font: inherit;
  color: var(--text);
  background: var(--card);
  transition: border-color .12s, box-shadow .12s;
  /* iOS Safari force-zooms the page on focus for any form control under
     16px, and never zooms back out on its own — labels around these fields
     are 13px (see `label` above), which `font: inherit` was pulling straight
     through. 16px overrides just the size while keeping the inherited family. */
  font-size: 16px;
}
input[type="text"]:focus, input[type="number"]:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-bg);
}
textarea { resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--text3); font-style: normal; }
/* --primary, not --primary-dark: --primary-dark is tuned to be a hover/
   pressed shade, which in dark mode sits close in value to --primary-bg
   (both deep indigo) and loses contrast as inline-code text on top of it.
   --primary stays legible against --primary-bg in both themes. */
code { background: var(--primary-bg); color: var(--primary); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
hr { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.actions { display: flex; gap: 10px; margin: 8px 0 16px; flex-wrap: wrap; }
/* Any element combining the `hidden` attribute with a class that sets its
   own `display` (like .actions's display:flex above) needs this — author
   CSS always beats the browser's default `[hidden] { display: none }` rule
   at equal specificity, so #setupNav/#globalActions (both class="actions",
   both toggled via el.hidden = ...) rendered visible in BOTH modes despite
   JS correctly setting hidden=true/false. `hidden` is supposed to mean
   "not displayed, no exceptions" — !important here just enforces that. */
[hidden] { display: none !important; }
button {
  padding: 11px 22px;
  border: none;
  border-radius: 10px;
  background: var(--primary);
  color: #fff;
  font: inherit;
  font-weight: 700;
  font-size: 13.5px;
  cursor: pointer;
  transition: background .12s;
}
button:hover { background: var(--primary-dark); }
button.secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); font-weight: 600; }
button.secondary:hover { background: var(--bg-soft); }
/* Low-emphasis, "won't normally need this" actions (script reinstall,
   add-a-row) — dashed border sets them apart from both the primary save
   action and the bordered secondary buttons. */
button.dashed {
  background: var(--bg-soft); color: var(--text); border: 1px dashed var(--border); font-weight: 600;
}
button.dashed:hover { background: var(--bg-well); }
button:disabled { opacity: .5; cursor: default; }
button:disabled:hover { background: var(--primary); }
button.secondary:disabled:hover, button.dashed:disabled:hover { background: var(--card); }
/* The one action a confirm modal ever needs beyond primary/secondary — a
   destructive/stopping action (turning the whole app off), so it reads as a
   warning color instead of the routine primary blue. */
button.danger { background: var(--danger); }
button.danger:hover { background: var(--danger); filter: brightness(0.9); }
.status { font-size: 13px; color: var(--text2); }

/* Custom confirm dialogs — only used where window.confirm()'s title (the
   browser forces the page URL, unstylable) would be wrong or confusing.
   Centered fixed overlay: immune to the "?" popover's off-screen overflow
   bug class by construction, since it never anchors to a trigger element's
   position. */
.modal-overlay {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(0, 0, 0, .5);
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
.modal-box {
  background: var(--card); color: var(--text); border-radius: 14px;
  box-shadow: var(--shadow-pop); padding: 22px 24px; width: 420px; max-width: 100%;
  max-height: calc(100vh - 40px); overflow-y: auto;
}
.modal-box h3 { margin: 0 0 12px; font-size: 17px; }
.modal-body p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.6; color: var(--text2); }
.modal-body ul { margin: 0 0 10px; padding-left: 1.2em; font-size: 13.5px; line-height: 1.6; color: var(--text2); }
.modal-body li { margin-bottom: 4px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
.warn-banner {
  font-size: 13.5px;
  line-height: 1.7;
  background: var(--amber-bg);
  border: 1px solid var(--amber-border);
  color: var(--amber-text);
  padding: 16px 18px;
  border-radius: 14px;
  margin-bottom: 16px;
}
.warn-banner code { background: var(--amber-chip); color: var(--amber-text); }
/* A checked-but-locked box (saved value preserved, execution blocked — see
   CLAUDE.md's "저장은 허용, 실행만 막음") used to be conveyed by opacity:.45
   alone, which a compressed phone photo in dark mode can't reliably show —
   a real screenshot from production was mistaken for "still on" because of
   this. Grayscale + not-allowed cursor + the .lock-badge chip below give a
   second and third independent signal that don't depend on spotting a subtle
   opacity difference. */
.plan-locked-box { opacity: .45; filter: grayscale(1); cursor: not-allowed; }
.plan-locked-box * { cursor: not-allowed !important; }
.lock-badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 700; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  background: var(--amber-bg); color: var(--amber-text); border: 1px solid var(--amber-border);
}
.hint { font-size: 12px; color: var(--text2); }
/* An inline text-button for "다시 시도" style actions sitting inside a
   sentence — a real <button> (not <a href="#">) so it doesn't need a dummy
   href, but reads as a link, not a boxed control. */
.linklike {
  background: none; border: none; padding: 0; margin: 0; font: inherit;
  color: var(--primary); text-decoration: underline; cursor: pointer;
}
.linklike:hover { color: var(--primary-dark); }

/* Brief flash on a field a "바로가기" link just scrolled to, so the operator's
   eye lands on the right control instead of having to hunt the whole card. */
@keyframes rd-jump-flash {
  0%, 100% { background: transparent; }
  25%, 75% { background: var(--primary-bg); }
}
.jump-highlight { animation: rd-jump-flash 1.4s ease-in-out; border-radius: 8px; }

/* Reusable "?" — replaces most of the always-visible explanatory <p class="hint">
   paragraphs that used to sit under nearly every field. Reported live: too
   much text on screen means none of it gets read. Collapsed by default
   (native <details>, no JS needed), floats over the page instead of
   pushing content down when opened, so browsing the page with several
   opened doesn't reflow anything. */
details.help {
  display: inline-block; position: relative; margin-left: 4px; vertical-align: middle;
}
details.help > summary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%; background: var(--primary-bg); color: var(--primary);
  font-size: 11px; font-weight: 700; cursor: pointer; list-style: none;
  user-select: none;
}
details.help > summary::-webkit-details-marker { display: none; }
details.help > summary::marker { content: ''; }
details.help > summary::before { content: '?'; }
details.help[open] > summary { background: var(--primary); color: #fff; }
details.help .help-body {
  position: absolute; z-index: 30; top: 22px; left: 0;
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 12px; width: 260px; max-width: 80vw; box-shadow: var(--shadow-pop);
  font-size: 12px; line-height: 1.5; color: var(--text2); font-weight: 400;
}
details.help .help-body code { background: var(--bg-well); }
/* A row/label already has its own left-to-right flow; the button sits right
   after the text it explains instead of on its own line. */
label.row details.help, label details.help, .hint details.help { margin-left: 6px; }
/* The app's own origin story — why the cart (not the product page) is
   where this app puts the subscription pitch. Sits above everything else,
   including #status, so it's the very first thing an operator reads,
   before the page has even finished loading settings. Deliberately
   heavier than .card/.feature-intro (dark background, larger heading) —
   this is the one thing on the page meant to be read as a headline, not
   a setting to configure. <details>, not a plain <section> — after the
   first read there's nothing actionable in it, so it collapses down to
   just the one-line question instead of permanently occupying the top
   of the page on every visit. */
.app-thesis {
  /* Hardcoded, not var(--text): this is a deliberately-always-dark accent
     card (see comment below), not a surface that should track the page
     theme. var(--text) is near-white in dark mode, which would flip this
     into a near-white box with light-gray text — the opposite of intended. */
  background: #111827; color: #f2f2f2;
  border-radius: 20px; padding: 20px 24px; margin-bottom: 20px;
}
.app-thesis summary {
  font-size: 15px; font-weight: 800; color: #fff; cursor: pointer;
  list-style: revert; line-height: 1.4;
}
.app-thesis[open] summary { margin-bottom: 12px; }
.app-thesis p { font-size: 13px; line-height: 1.7; margin: 0 0 8px; color: #d5d5d5; }
.app-thesis p:last-child { margin-bottom: 0; }
.app-thesis b { color: #fff; }

.feature-intro {
  font-size: 13px;
  line-height: 1.6;
  background: var(--blue-bg);
  border: 1px solid var(--blue-border);
  border-radius: 14px;
  padding: 16px 18px;
  margin-bottom: 16px;
  color: var(--blue-text);
}
.feature-intro p { margin: 0 0 8px; }
.feature-intro p:last-child { margin-bottom: 0; }
.feature-intro code { background: #dbeafe; color: var(--blue-text); }
/* Was a plain inline <a> at the tail of a paragraph — default link-blue on
   this box's light-blue background gave it almost no contrast, and buried
   in running text it read as just more prose, not a thing to click.
   Reported live: "글씨에 묻혀서 잘 안 보여". Pulled out of the paragraph
   flow and styled like the app's other primary actions instead. */
.feature-cta {
  display: inline-block; margin: 4px 0 12px; padding: 10px 20px;
  background: var(--primary); color: #fff; font-size: 13px; font-weight: 700;
  border-radius: 10px; text-decoration: none;
}
.feature-cta:hover { background: var(--primary-dark); color: #fff; text-decoration: none; }
.feature-steps { margin: 0; padding-left: 20px; }
.feature-steps li { margin-bottom: 5px; }
.feature-steps li:last-child { margin-bottom: 0; }

.stepup-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.stepup-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.stepup-row .stepup-round { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 13px; color: var(--text2); }
.stepup-row .stepup-round-input { width: 64px; margin-top: 0; text-align: center; }
.stepup-row .stepup-group { flex: 1; margin-top: 0; padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px; font: inherit; font-size: 16px; }
.stepup-row .stepup-remove { padding: 6px 12px; font-size: 12px; white-space: nowrap; }
.stepup-row .coupon-round-input { width: 64px; margin-top: 0; text-align: center; }
.stepup-row .coupon-select { flex: 1; min-width: 160px; margin-top: 0; padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px; font: inherit; font-size: 16px; }
.stepup-row .coupon-repeat { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; font-size: 13px; color: var(--text2); }
.stepup-row .coupon-repeat-interval { width: 44px; margin-top: 0; padding: 4px 6px; text-align: center; font-size: 13px; }
/* ---- 정기배송 통계 (#cardStats) ---------------------------------------
   Read-only figures, so everything here is built from the shared tokens and
   carries no interactive state of its own. Wide content (the two tables and
   the round bars) sits inside .stat-scroll so a narrow phone scrolls the
   table instead of the page. */
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 12px; }
.stat-tile {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-soft);
}
.stat-tile .stat-label { flex: 1 0 100%; font-size: 12px; color: var(--text2); }
.stat-tile strong { font-size: 22px; font-weight: 800; color: var(--text); line-height: 1.2; }
.stat-tile .stat-unit { font-size: 12px; color: var(--text2); }
.stat-amount { margin: 0 0 4px; }

/* ---- 구독료 카드(#cardBilling) — 요금제 그리드 --------------------------- */
.plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin: 12px 0; }
.plan-card {
  display: flex; flex-direction: column; gap: 6px;
  padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-soft);
}
.plan-card.current { border-color: var(--primary-border); background: var(--primary-bg); }
.plan-card .plan-name { font-size: 12px; font-weight: 700; color: var(--text2); text-transform: uppercase; letter-spacing: .02em; }
.plan-card.current .plan-name { color: var(--primary-dark); }
.plan-card .plan-price { font-size: 18px; font-weight: 800; color: var(--text); }
.plan-card .plan-cap { font-size: 12px; color: var(--text2); }
.plan-card button { margin-top: 4px; }
/* 카페24 결제 화면은 데스크톱 전용 — 680px 이하에서만 보인다(공통 구현
   사항 체크리스트). 기본은 숨김, 이 파일의 다른 min-width 블록들과 반대
   방향(max-width)인 게 의도적이다: "모바일에서만 보이는" 예외이기 때문. */
.mobile-only-notice { display: none; }
@media (max-width: 680px) { .mobile-only-notice { display: block; } }

.stat-rounds { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
.stat-round-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
.stat-round-row .stat-round-label { width: 76px; flex: none; color: var(--text2); white-space: nowrap; }
/* The bar is a proportional track, not a chart library — width is set inline
   as a percentage of the busiest round. */
.stat-round-row .stat-bar-track { flex: 1; min-width: 40px; height: 10px; border-radius: 999px; background: var(--bg-well); overflow: hidden; }
.stat-round-row .stat-bar-fill { display: block; height: 100%; border-radius: 999px; background: var(--primary); }
.stat-round-row .stat-round-count { width: 92px; flex: none; text-align: right; color: var(--text); white-space: nowrap; }
.stat-round-row .stat-round-cum { color: var(--text3); }

.stat-scroll { overflow-x: auto; margin-bottom: 10px; }
.stat-table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
.stat-table th, .stat-table td { padding: 6px 10px; text-align: right; white-space: nowrap; border-bottom: 1px solid var(--border); }
.stat-table th { font-weight: 700; color: var(--text2); font-size: 12px; }
.stat-table th:first-child, .stat-table td:first-child { text-align: left; }
.stat-table tr:last-child td { border-bottom: none; }
.stat-table td { color: var(--text); }
.stat-table .sortable-th { cursor: pointer; user-select: none; }
.stat-table .sortable-th:hover { color: var(--primary); }
.stat-table .sortable-th:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.stat-facts { margin: 0; padding-left: 20px; font-size: 13px; color: var(--text2); }
.stat-facts li { margin-bottom: 4px; }
.stat-facts li:last-child { margin-bottom: 0; }
.stat-facts b { color: var(--text); }

pre { background: #0d1117; color: #c9d1d9; padding: 14px; border-radius: 12px; overflow: auto; font-size: 12px; }
fieldset.groupbox { border: 1px solid var(--border); border-radius: 14px; padding: 16px 18px; margin: 0 0 14px; }
fieldset.groupbox legend { font-size: 12.5px; font-weight: 700; color: var(--text); padding: 0 6px; }
/* Options that belong to one specific radio choice above them (not the whole
   fieldset) — a left border + indent shows which choice they nest under. */
.suboptions { margin: 6px 0 16px; padding: 2px 0 2px 20px; border-left: 2px solid var(--border); }
.checklist { display: flex; flex-wrap: wrap; gap: 8px 16px; font-size: 13px; }
.checklist label { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--text); }
.checklist .empty { color: var(--text2); font-size: 12px; }

.picker-search { display: flex; gap: 8px; }
.picker-search input { margin-top: 0; }
.picker-search button { padding: 10px 18px; white-space: nowrap; }
.picker-results {
  margin-top: 8px; max-height: 220px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 10px; padding: 6px 10px;
}
.picker-results label { display: flex; align-items: center; gap: 8px; font-size: 13px; margin: 0; padding: 5px 0; color: var(--text); }
.picker-results label input { margin: 0; }
.picker-results .empty { color: var(--text2); font-size: 12px; padding: 4px 0; }
.picker-results .pno { color: var(--text3); font-size: 11px; }
.chip-list { display: flex; flex-wrap: wrap; gap: 6px 8px; }
.chip-list .empty { color: var(--text2); font-size: 12px; }
.chip-list .sel-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--primary-bg); border: none; border-radius: 999px;
  padding: 5px 6px 5px 14px; font-size: 12px; font-weight: 600; color: var(--primary);
}
.chip-list .sel-chip button { padding: 2px 9px; font-size: 11px; border-radius: 999px; background: var(--card); color: var(--primary); border: none; font-weight: 700; }
.cat-tree { font-size: 13px; }
.cat-tree label {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 0; white-space: nowrap; color: var(--text);
}
.cat-tree label input { margin: 0; flex-shrink: 0; }
/* 자식이 있는 분류를 접었다 펼 수 있게 — 실사용 신고: 목록이 길어서 접을
   수 있으면 좋겠다는 요청. 화살표(토글) + 라벨을 한 행에 두되, 토글은
   <label> 밖에 둔다 — 안에 두면 브라우저가 그 클릭도 체크박스 토글로
   여길 수 있어서다. */
.cat-tree .cat-row { display: flex; align-items: center; }
.cat-tree .cat-row label { flex: 1; }
.cat-toggle, .cat-toggle-spacer {
  width: 18px; height: 18px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.cat-toggle {
  background: none; border: none; padding: 0; margin-right: 2px;
  font-size: 9px; color: var(--text2); cursor: pointer;
}
.cat-toggle:hover { color: var(--text); }

.kw-display { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 0 0 12px; }
.kw-display .kw-chip {
  background: var(--primary-bg); border: none; border-radius: 999px;
  padding: 6px 14px; font-size: 12.5px; font-weight: 600; color: var(--primary);
}
.kw-display .empty { color: var(--text2); font-size: 12px; }

.detect-result {
  background: var(--primary-bg); border: 1px solid var(--primary-border); border-radius: 14px;
  padding: 16px 18px; margin: 4px 0 12px;
}
.detect-result p { margin: 0 0 8px; font-size: 13px; }
.detect-choice { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 6px; }
.detect-choice input { margin: 0; }
.detect-choice .new-badge {
  font-size: 11px; color: var(--amber); background: var(--amber-bg); border: 1px solid var(--amber-border);
  border-radius: 999px; padding: 1px 8px;
}

/* Dirty-state cue: any unsaved change turns every save button amber so it's
   obvious there's something to save, vs. clean where they revert to normal. */
button[type="submit"].needs-save { background: #d97706; color: #fff; }
details.advanced { margin-top: 10px; border-top: 1px dashed var(--border); padding-top: 10px; }
details.advanced summary { cursor: pointer; font-size: 12px; color: var(--text3); }
details.advanced summary:hover { color: var(--text2); }
details.advanced > label, details.advanced > div { margin-top: 12px; }

/* Cancel section: both scenario previews shown side by side (stacked on
   narrow screens) above the settings fields, so the operator sees the
   current popups before scrolling down to edit them. */
.cancel-preview-box { min-width: 240px; }

.rd-preview-overlay {
  background: #f3f4f6;
  border-radius: 14px;
  padding: 24px;
  display: flex;
  justify-content: center;
}
.rd-preview-popup {
  background: #fff;
  border: none;
  box-shadow: var(--shadow-soft);
  padding: 22px;
  border-radius: 14px;
  width: 100%;
  max-width: 320px;
  box-sizing: border-box;
  text-align: center;
}
.rd-preview-popup h2 { font-size: 16px; font-weight: 800; padding: 0 0 1em; margin: 0; }
.rd-preview-popup .preview-body { text-align: initial; font-size: 13px; line-height: 1.5; color: var(--text2); }
.rd-preview-popup .preview-body p { margin: 0 0 .6em; }
.rd-preview-popup .preview-body strong { color: var(--text); }
/* 확인창/완료 메시지 미리보기: 앱이 직접 그리는 작은 대화상자라(더 이상
   네이티브 confirm()/alert()가 아님) 줄바꿈도 그 안에서와 똑같이 \n 그대로 */
.rd-preview-popup .preview-body-plain { white-space: pre-line; }
.rd-preview-actions { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 14px; }
.rd-preview-btn-cancel, .rd-preview-btn-keep {
  font-size: 12.5px;
  font-weight: 600;
  padding: 10px;
  border-radius: 9px;
  cursor: default;
  border: none;
  font: inherit;
  flex: 1;
}
.rd-preview-btn-cancel { background: #fff; border: 1px solid var(--border); color: var(--text); text-decoration: none; }
.rd-preview-btn-keep { background: #111827; color: #fff; font-weight: 700; }

/* Side-by-side layout for cards that pair a live preview with the fields
   that drive it: preview stays visible (sticky) while scrolling the fields
   instead of needing a scroll back up to check it. flex-wrap alone handles
   the mobile case — below the combined min-width it drops to today's
   stacked layout with no separate breakpoint needed. */
/* container-type lets the sticky rule below query THIS element's own
   rendered width instead of the viewport's — see the comment there for why
   that distinction is the actual fix, not a style preference. */
.card-split { display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; container-type: inline-size; }
/* flex-basis 420 (not the old 320) so on a wide desktop viewport this
   column naturally grows past the ~408px .mock-pc-layout needs (its own
   360px min-width + .cart-preview's 48px padding) without relying on a
   min-width floor — a min-width: 360px here previously forced this SAME
   column wider than an actual mobile screen, pushing the whole card off
   the right edge (reported live). Narrow screens now shrink this column
   as far as flex-wrap needs to; fitPcPreview() (admin.js) scales the mock
   itself down to fit whatever width that ends up being, on any device.
   Grow ratio changed from 1:2 to 1:1 — with .container now wide enough to
   have real spare space on a desktop window, an equal split lets the
   preview actually claim enough of it to render the PC mock near its
   natural size instead of losing most of the extra room to the fields
   column. */
.card-split-preview { flex: 1 1 420px; min-width: 240px; }
.card-split-fields { flex: 1 1 380px; min-width: 240px; }
/* Sticky only makes sense once the two columns actually sit side by side.
   Below that point flex-wrap stacks them into one column, and a stuck
   preview would stay pinned at the top while the fields scroll UNDER it —
   visually overlapping the very controls it's supposed to help with
   (reported live: "미리보기가 설정을 가려", confirmed via a real diagnostic
   dump — preview and fields had IDENTICAL x/width, i.e. already stacked,
   while sticky was still active and covering the fields column beneath it).
   This used to be a plain viewport media query at 680px, which is not
   actually when wrapping happens: flex-wrap triggers off the container's
   OWN available width against the two columns' combined flex-basis+gap
   (420 + 380 + 24 = 824px here), and the container is narrower than the
   viewport by however much .container/.card padding eats — two numbers
   with no reason to move together, so the 680px guess drifted out of sync
   with the real wrap point and silently reopened this gap once (this is
   the second time it's been fixed — see the git log for the first).
   A container query asks the right question directly — "is MY box wide
   enough for both columns at their natural size" — so it can't drift out
   of sync with flex-wrap's own decision the way a viewport breakpoint can.
   840px = 824px flex-basis sum + a small safety margin for rounding. */
@container (min-width: 840px) {
  .card-split-preview { position: sticky; top: 16px; }
}

/* Cart card: trigger-button mockup + the cycle-selection popup it opens.
   Gray "page backdrop" box, matching .rd-preview-overlay's language — without
   it the button preview read as plain hint text with a stray button in it,
   not as a distinct live preview panel. */
.cart-preview {
  background: #f3f4f6; border-radius: 16px; padding: 22px 24px; text-align: center;
}
/* A white "cart page" card inside the gray backdrop, modeled on a real
   Cafe24 cart screen (breadcrumb / product rows / order amount / bottom
   order bar) — an operator sent an actual screenshot of their cart and this
   mirrors that structure. Product rows stay skeleton-style (gray bars, no
   real numbers) so nothing here is mistaken for real data; only the generic
   Cafe24 UI chrome (breadcrumb text, "선택상품주문"/"전체상품주문" labels)
   is shown verbatim since those are the same on every Cafe24 mall, not
   mall-specific data. */
.mock-cart-page { background: #fff; border-radius: 14px; padding: 18px; text-align: left; position: relative; box-shadow: 0 1px 2px rgba(0,0,0,.05); }
.mock-crumb { font-size: 11px; color: var(--text3); margin: 0 0 12px; text-align: center; }
.mock-section-title { font-weight: 800; font-size: 13.5px; margin: 0 0 12px; }
.mock-product-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.mock-check { width: 16px; height: 16px; border: 1.5px solid #d1d5db; border-radius: 5px; flex-shrink: 0; }
.mock-thumb { width: 40px; height: 40px; border-radius: 8px; background: #e5e7eb; flex-shrink: 0; }
.mock-lines { display: flex; flex-direction: column; gap: 6px; flex: 1; }
.mock-line-lg, .mock-line-sm { height: 8px; border-radius: 4px; background: #e5e7eb; }
.mock-line-lg { width: 65%; }
.mock-line-sm { width: 35%; }
.mock-qty { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); white-space: nowrap; }
.mock-qty-num { width: 16px; text-align: center; }
.mock-divider { border: none; border-top: 1px dashed var(--border); margin: 4px 0 12px; }
.mock-order-amount { display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: var(--text2); margin: 0 0 12px; }
.mock-line-amount { width: 64px; height: 10px; border-radius: 4px; background: #e5e7eb; }
.mock-order-bar { display: flex; gap: 8px; }
.mock-order-btn {
  flex: 1; padding: 13px; border-radius: 10px; font: inherit; font-size: 13px;
  font-weight: 700; border: none; cursor: default;
}
.mock-order-btn-outline { background: #fff; border: 1px solid var(--border); color: var(--text2); font-weight: 600; }
.mock-order-btn-solid { background: #111827; color: #fff; }

.rd-preview-cart-btn {
  padding: 12px 26px; border-radius: 10px; background: #000; color: #fff;
  font: inherit; font-size: 14px; font-weight: 700; border: none; cursor: default;
}
/* 스킨에 직접 넣기: real position depends on where the operator pasted it —
   just show it in normal document flow, near the order bar, as one example. */
.mock-cart-page .rd-preview-cart-btn.mode-skin { display: block; width: 100%; margin: 12px 0 0; }
/* 고정 버튼: mirrors the real fixed-position CSS (position:fixed; bottom:16px)
   by absolutely positioning within this mock "page", including the same
   three placements — this is what actually demonstrates the overlap the
   "겹치면 오른쪽/가로 전체로 바꿔 보세요" hint warns about, instead of just
   describing it in words. */
.mock-cart-page .rd-preview-cart-btn.mode-floating {
  position: absolute; bottom: 12px; margin: 0; z-index: 1;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .25);
}
.mock-cart-page .rd-preview-cart-btn.mode-floating.pos-bottomCenter { left: 50%; transform: translateX(-50%); }
.mock-cart-page .rd-preview-cart-btn.mode-floating.pos-bottomRight { right: 16px; }
.mock-cart-page .rd-preview-cart-btn.mode-floating.pos-bottomWide { left: 16px; right: 16px; text-align: center; }
.rd-preview-list { margin: 0 0 10px; padding-left: 1.2em; font-size: 13px; line-height: 1.6; color: var(--text2); }
.rd-preview-list-muted { color: var(--text3); font-size: 12px; }
.rd-preview-list:empty, .rd-preview-list-muted:empty { display: none; }

/* Cart button mode picker: two mutually exclusive placements. The unselected
   mode's options are hidden (not dimmed) — unlike a master/sub toggle, only
   one side is ever meaningful, and hiding keeps collect() reading real values. */
#cartButtonMode label.row { align-items: flex-start; line-height: 1.5; }
#cartSkinSnippet, #buyNowSkinSnippet {
  white-space: pre-wrap; user-select: all;
  margin: 0 0 8px; padding: 12px 14px; font-size: 12px;
  /* pre-wrap alone only breaks at whitespace, same as normal text — a long
     unbroken run (a URL, or minified JS like the buy-now snippet's setTimeout
     line) still forces the box wider than its container, which on a narrow
     screen pushes the whole fieldset (and the paragraph text around it) off
     the right edge of the viewport. Reported live on mobile. overflow-wrap
     lets a line break mid-token only when it has no better option, and the
     max-width/overflow-x are a hard backstop for anything that still
     doesn't fit. */
  max-width: 100%; overflow-wrap: anywhere; overflow-x: auto;
}
/* The cart snippet is one long unbroken line end-to-end (no natural break
   points at all), so it gets the stronger break-all on top of the above. */
#cartSkinSnippet { word-break: break-all; }
#cart_buttonSelectorError { color: var(--danger); }

/* ---- guided first-setup mode --------------------------------------------
   The wizard is a filter over the same DOM, not a second screen: cards keep
   their ids and their own save buttons, and only visibility changes. */
#setupBar {
  background: var(--card); border: 1px solid var(--border); border-radius: 16px;
  padding: 18px 20px; margin-bottom: 20px;
}
.setup-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.setup-head strong { font-size: 16px; }
.setup-phase {
  font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px;
  background: var(--primary-bg); color: var(--primary); white-space: nowrap;
}
.setup-count { margin-left: auto; font-size: 12px; color: var(--text2); white-space: nowrap; }
.setup-progress { height: 5px; background: var(--bg-well); border-radius: 999px; margin: 12px 0 0; overflow: hidden; }
#setupProgressFill { height: 100%; background: var(--green); width: 0; transition: width .2s; }
#setupBar .hint { margin: 10px 0 0; }
/* In setup mode each card is shown alone, so its own inline save button would
   duplicate the wizard's 저장하고 다음 button — hide it and let the nav drive. */
body[data-mode="setup"] .section-save { display: none; }
/* 목차(#tocBar)는 마법사 모드에서도 항상 보인다(applyMode()) — 이전/
   처음으로/다음만으로는 인접하지 않은 단계로 바로 못 가서 실사용에서
   불편하다는 피드백을 받았다. 예전엔 data-mode 선택자로 마법사 모드 내내
   숨겨져 있었으나 지금은 그 자리가 없다. */

/* 최상위 메뉴(요약/구독료/설정): tocBar(설정 안의 8단계 목차)보다 한 단계
   위에 있는 개념이라, 크고 진한 톤으로 확실히 구별되게 만들었다. 공통 구현
   사항 체크리스트 기준 — 데스크톱은 좌측 세로 사이드바(접기 지원,
   Ctrl+B·#pageNavCollapseBtn), 모바일은 하단 고정 바. 다른 @media 블록과
   같은 모바일-퍼스트 순서: 기본값이 모바일(하단 바)이고, 681px 이상에서
   데스크톱(좌측 세로)로 바뀐다.
   .app-layout이 pageNav + page-body를 감싸는 flex 컨테이너 — 모바일에서는
   column(사실상 pageNav가 fixed라 순서 의미는 약하다), 데스크톱에서는 row. */
.app-layout { display: flex; flex-direction: column; }
.page-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; flex-direction: row; gap: 2px;
  background: var(--card); border-top: 1px solid var(--border);
  padding: 6px 6px calc(6px + env(safe-area-inset-bottom, 0px));
}
.page-nav-item {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  font: inherit; font-size: 11px; font-weight: 700; color: var(--text2);
  background: transparent; border: none; border-radius: 10px;
  padding: 6px 4px; cursor: pointer; transition: background .12s, color .12s;
  white-space: nowrap; overflow: hidden; text-align: center;
}
.page-nav-icon { flex-shrink: 0; }
.page-nav-item:hover { background: var(--primary-bg); color: var(--primary); }
.page-nav-item.active { background: var(--primary); color: #fff; }
/* 모바일엔 "접기"라는 개념이 없다 — 하단 바는 항상 펼쳐진 상태뿐. */
.page-nav-collapse { display: none; }
/* 하단 고정 바에 콘텐츠 마지막 줄이 가려지지 않도록 여유를 둔다. */
.page-body { padding-bottom: 64px; }

@media (min-width: 681px) {
  .app-layout { flex-direction: row; align-items: flex-start; gap: 20px; }
  .page-nav {
    position: sticky; top: 20px; left: auto; right: auto; bottom: auto;
    flex-direction: column; gap: 2px; width: 176px; flex-shrink: 0;
    border-top: none; border: 1px solid var(--border); border-radius: 14px;
    padding: 10px; transition: width .15s;
  }
  /* .collapsed는 admin.js가 상호작용(클릭/Ctrl+B) 때 토글하는 class.
     html[data-menu-collapsed]는 index.html의 인라인 <head> 스크립트가
     첫 페인트 전에(테마/글자크기와 같은 타이밍) 찍는 속성 — admin.js가
     로드되기 전에도 저장된 접힘 상태로 바로 그려지게 한다(깜빡임 방지 +
     헤드리스 새로고침 타이밍 이슈 회피, admin.js 주석 참고). */
  .page-nav.collapsed,
  html[data-menu-collapsed="1"] .page-nav { width: 56px; }
  .page-nav-item {
    flex-direction: row; justify-content: flex-start; text-align: left;
    font-size: 14px; padding: 11px 14px;
  }
  .page-nav.collapsed .page-nav-label,
  html[data-menu-collapsed="1"] .page-nav-label { display: none; }
  .page-nav-collapse {
    display: flex; align-items: center; justify-content: center;
    margin-top: 6px; padding: 8px;
    border: 1px solid var(--border); border-radius: 10px;
    background: transparent; color: var(--text2); cursor: pointer;
  }
  .page-nav-collapse:hover { background: var(--bg-soft); }
  .page-body { flex: 1; min-width: 0; padding-bottom: 0; }
}

/* 목차: sticky pill row so a long settings page can be jumped to directly
   instead of scrolled through. Number keys 1–7 do the same (see admin.js) —
   the num badge doubles as the shortcut hint. */
.toc-bar {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  background: var(--card); border: 1px solid var(--border); border-radius: 14px;
  padding: 8px 10px; margin-bottom: 20px;
}
.toc-item {
  display: flex; align-items: center; gap: 6px; flex-shrink: 0;
  font: inherit; font-size: 12px; font-weight: 600; color: var(--text2); white-space: nowrap;
  background: transparent; border: 1px solid transparent; border-radius: 999px;
  padding: 7px 12px 7px 8px; cursor: pointer; transition: background .12s, color .12s;
}
.toc-item:hover { background: var(--primary-bg); color: var(--primary); }
.toc-item.active { background: var(--primary); color: #fff; }
.toc-item.active .toc-num { background: #fff; color: var(--primary); }
.toc-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%; background: var(--text3); color: #fff;
  font-size: 10px; font-weight: 700; flex-shrink: 0;
}
/* Keyboard-shortcut hint — meaningless on a touch screen, so it's off by
   default and only added back once there's a keyboard to use it with. */
.toc-hint { display: none; font-size: 11px; color: var(--text3); white-space: nowrap; margin-left: 4px; flex-shrink: 0; }
@media (min-width: 681px) { .toc-hint { display: inline; } }
#setupNav { align-items: center; }
#setupDone {
  background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 20px;
}

/* Button design controls + preview states (mirrors the storefront's
   .rd-tpl-* / .rd-has-image treatments so the preview stays truthful). */
.color-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; }
.color-row label { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; white-space: nowrap; color: var(--text2); }
.color-row input[type="color"] { width: 44px; height: 32px; padding: 2px; margin: 0; cursor: pointer; border: 1px solid var(--border); border-radius: 8px; }
.color-row input[type="number"] { width: 72px; margin: 0; text-align: center; }

.slider-field { margin-bottom: 16px; }
.slider-value { font-weight: 700; color: var(--text); }
/* overflow-x:auto is a backstop only, not the fix — see the range input rule
   below for what actually prevents this from needing to engage. */
.slider-row { display: flex; align-items: center; gap: 10px; margin-top: 4px; overflow-x: auto; }
/* min-width: 0 overrides the flex item default of `auto`, which for a native
   range input resolves to its own intrinsic rendered width (~129px in
   Chrome) and refuses to shrink below that despite flex-shrink — the same
   class of bug as the grid columns above, one level down. Without it, in the
   narrower of the two 따로 지정 columns the slider visibly pushes its own
   number input out into the other column (confirmed: number input rendered
   25px into the neighbouring column at a 1024px viewport). */
.slider-row input[type="range"] { flex: 1; min-width: 0; margin: 0; accent-color: var(--primary); }
.slider-row input[type="number"] { width: 64px; margin: 0; text-align: center; flex-shrink: 0; }

.rd-preview-cart-btn.tpl-solid { border-radius: 10px; }
.rd-preview-cart-btn.tpl-outline { border-radius: 10px; border: 2px solid currentColor; }
.rd-preview-cart-btn.tpl-pill { border-radius: 999px; padding: 12px 32px; }
.rd-preview-cart-btn.has-image {
  padding: 0; background: none !important; border: none; box-shadow: none;
}
.rd-preview-cart-btn.has-image img { display: block; max-width: 160px; height: auto; }
/* Custom placement is driven by inline bottom/left percentages set in JS. */
.mock-cart-page .rd-preview-cart-btn.pos-custom { position: absolute; margin: 0; z-index: 1; }

/* Card 3 (배송주기 팝업) preview: the real popup is position:fixed, centered
   over whatever the shopper was looking at — no dimmed backdrop behind it
   (confirmed against a real screenshot). Overlaying it on the same mock cart
   page card 2 uses, instead of showing it alone in a detached box, is what
   actually demonstrates that. */
/* Both children occupy the same grid cell, so the stage's height is the
   TALLER of the two. Absolute-positioning the popup instead made it spill
   out of the preview box whenever the popup was taller than the mock page
   (which it usually is once discount rows + notes are filled in). */
.mock-stage { display: grid; }
.mock-stage > * { grid-area: 1 / 1; }
.mock-stage > .mock-cart-page { align-self: start; }
.mock-popup-overlay {
  align-self: center;
  justify-self: center;
  width: calc(100% - 32px);
  box-sizing: border-box;
  z-index: 2;
  box-shadow: var(--shadow-pop);
  border-radius: 16px;
}
.mock-select {
  display: flex; align-items: center; justify-content: space-between;
  border: 1px solid var(--border); border-radius: 10px; padding: 11px 13px;
  font-size: 13px; color: var(--text); background: #fff;
}
.mock-select-caret { color: var(--text3); font-size: 11px; margin-left: 8px; }

/* Card 5 (상품 페이지) preview: modeled on a real Cafe24 product detail page
   (blue "정기배송" badge above the title, "구매방법" radio row, buy-now +
   cart + wishlist button row) — an operator sent an actual screenshot and
   this mirrors that structure the same way card 2/3 mirror the cart page. */
.mock-product-page { background: #fff; border-radius: 14px; padding: 18px; text-align: left; box-shadow: 0 1px 2px rgba(0,0,0,.05); }
.mock-badge {
  display: inline-block; background: #1a73e8; color: #fff; font-size: 11px;
  font-weight: 700; padding: 3px 9px; border-radius: 5px; margin: 0 0 12px;
}
.mock-product-title { display: flex; flex-direction: column; gap: 8px; margin: 0 0 14px; }
.mock-info-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--text2); margin-bottom: 8px; }
.mock-purchase-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 10px 0; transition: opacity .15s; }
.mock-purchase-row.rd-row-hidden { opacity: .35; }
.mock-purchase-label { font-size: 12px; color: var(--text2); margin-right: 4px; }
.mock-radio { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text); }
.mock-radio-dot { width: 15px; height: 15px; border-radius: 50%; border: 1.5px solid #d1d5db; position: relative; flex-shrink: 0; }
.mock-radio-dot.checked { border-color: var(--primary); border-width: 5px; }
.mock-radio-dot.checked::after { content: none; }
.mock-product-buttons { display: flex; gap: 8px; }
.mock-product-buttons .mock-order-btn { flex: 1; }


/* PC cart mock: real Cafe24 desktop carts are a 2-column layout (product
   list + a separate order-summary box), not just a wider version of the
   mobile 1-column layout — confirmed against a real elshop.kr screenshot.
   The preview panel itself is narrower than a real desktop viewport, so this
   scrolls horizontally rather than squeezing the 2 columns illegibly. */
.mock-cart-page-pc { overflow-x: auto; position: relative; }
.mock-pc-layout { display: flex; gap: 10px; min-width: 320px; align-items: flex-start; }
.mock-pc-left { flex: 3; min-width: 0; }
.mock-pc-right { flex: 2; min-width: 165px; }
.mock-pc-subheader { background: var(--bg); font-size: 11px; color: var(--text2); padding: 6px 8px; margin: 0 0 10px; border-radius: 6px; }
.mock-pc-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.mock-pc-row-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; }
.mock-change-btn {
  font-size: 11px; padding: 5px 8px; border-radius: 6px; border: 1px solid var(--border);
  background: #fff; color: var(--text2); cursor: default; font: inherit; font-size: 11px; white-space: nowrap;
}
.mock-pc-shipgroup { font-size: 11px; color: var(--text3); margin: 0 0 8px; }
.mock-pc-bottom-actions { display: flex; gap: 4px; }
.mock-pc-order-box { border: 1px solid var(--border); border-radius: 10px; padding: 10px; margin-bottom: 10px; }
.mock-pc-order-box .mock-info-row { font-size: 11px; white-space: nowrap; }
.mock-pc-total-row {
  display: flex; justify-content: space-between; align-items: center;
  font-weight: 700; font-size: 12px; margin: 0 0 12px; white-space: nowrap; gap: 6px;
}
.mock-pc-pay-row { display: flex; gap: 6px; margin-bottom: 6px; }
.mock-pc-pay-btn {
  padding: 10px; border-radius: 8px; font-size: 12px; font-weight: 700;
  border: none; cursor: default; font: inherit; font-size: 12px;
}
.mock-pc-pay-naver { flex: 1; background: #03c75a; color: #fff; }
.mock-pc-pay-kakao { width: 100%; background: #fee500; color: #3c1e1e; }
.mock-pc-pay-heart { flex-shrink: 0; width: 34px; border-radius: 8px; border: 1px solid var(--border); background: #fff; cursor: default; }

/* Mobile cart mock: single column, inherently phone-narrow by structure
   (not by forcibly capping a shared markup's width). No order-bar buttons
   here (see index.html) — reserve their old space anyway so the floating
   button (position:absolute, bottom:12px within this short box) has clear
   room below the order-amount row instead of sitting on top of it. On an
   actual phone screen this isn't needed (plenty of height below); it's
   only this compact preview box that's short enough to need it. */
.mock-cart-page-mobile { max-width: 260px; margin: 0 auto; padding-bottom: 76px; }

/* ---- custom checkbox / radio -----------------------------------------
   Pure CSS (appearance:none + pseudo-elements) — no markup changes, so
   every existing input[id] admin.js reads still works exactly as before.
   Indigo fill/ring instead of the browser's native OS-styled control. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  width: 19px; height: 19px; flex-shrink: 0;
  border: 1.5px solid var(--control-border); background: var(--card);
  cursor: pointer; position: relative; margin: 0; vertical-align: middle;
  transition: background .12s, border-color .12s;
}
input[type="checkbox"] { border-radius: 6px; }
input[type="checkbox"]:checked { background: var(--primary); border-color: var(--primary); }
input[type="checkbox"]:checked::after {
  content: ''; position: absolute; left: 6px; top: 2px;
  width: 5px; height: 10px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
input[type="radio"] { border-radius: 999px; }
input[type="radio"]:checked { border-width: 6px; border-color: var(--primary); }
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled { opacity: .5; cursor: default; }

/* Master on/off switches (앱 사용 · 해지 붙잡기 사용 · 회원등급 연계 사용) —
   the same underlying <input type="checkbox"> admin.js already reads/sets,
   just rendered as a sliding pill instead of a checkbox. Opt-in via class
   so ordinary condition checkboxes elsewhere keep the checkmark style. */
input.toggle[type="checkbox"] {
  width: 44px; height: 24px; border-radius: 999px; border: none; background: var(--control-border);
}
input.toggle[type="checkbox"]::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px; border-radius: 999px; background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: left .15s;
}
input.toggle[type="checkbox"]:checked { background: var(--primary); }
input.toggle[type="checkbox"]:checked::after { left: 23px; }

/* Segmented control (버튼 디자인 템플릿 선택) — three radios styled as one
   pill-shaped tab bar instead of a vertical list. `:has()` lets the tab
   itself react to its own radio's checked state with no admin.js changes;
   every evergreen browser Cafe24 operators use supports it. */
/* Full-width tab row by default — a phone screen has no spare space for an
   inline-sized control to sit off to one side, and equal-width tabs give
   each option a bigger, easier thumb target. Desktop gets the more
   compact, content-sized pill once there's room for it to not dominate
   the row. */
.segmented {
  display: flex; background: var(--bg-well); padding: 4px; border-radius: 12px; gap: 2px;
}
.segmented label {
  display: flex; align-items: center; justify-content: center; flex: 1;
  padding: 9px 8px; border-radius: 9px; margin: 0;
  font-size: 13px; font-weight: 600; color: var(--text2); cursor: pointer;
  transition: background .12s, color .12s, box-shadow .12s;
}
.segmented label input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.segmented label:has(input:checked) { background: var(--card); color: var(--primary); box-shadow: 0 1px 3px rgba(0,0,0,.1); }
@media (min-width: 481px) { .segmented { display: inline-flex; } .segmented label { flex: none; padding: 9px 18px; } }

/* PC/모바일 버튼 위치: side by side once there's room, instead of the second
   block just trailing below the first (reported live as harder to compare
   the two at a glance). One column by default — both on a narrow admin
   viewport (radios+sliders squeezed into a half-width column read worse
   than just stacking) and whenever only the PC column is showing at all
   (같은 자리 mode never gets the .split class, so it never needs two). */
.pos-grid { display: grid; grid-template-columns: 1fr; gap: 4px; }
@media (min-width: 601px) {
  /* minmax(0, 1fr), not bare 1fr: a bare `fr` track still won't shrink past
     its content's min-content width (labels, the slider's number input,
     "화면 크기 기준 비율입니다." hint text), and nothing here clips that
     overflow — it just pushes the grid, its ancestors, and the whole PAGE
     wider, which is exactly the horizontal scrollbar reported live the
     moment 따로 지정 revealed this grid on a normal laptop-width window
     (measured: 95px of page overflow at 1024px viewport). The 0 floor lets
     each column shrink to whatever room is actually available; long labels
     wrap instead of forcing the page wide. */
  .pos-grid.split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 28px; }
  .pos-grid.split .pos-col:first-child { border-right: 1px solid var(--border); padding-right: 28px; }
}
