/* style.css - the storefront's real, hand-authored design system, ported
   from the MOBY HTML/ design mockup (fonts, palette, every component
   class - .top-nav/.card/.size-option/.modal-overlay/etc.), merged with
   this app's own still-load-bearing functional CSS that isn't decorative
   (crop tool canvas mechanics, TinyMCE prose rendering, a couple of
   specificity-race workarounds) - see CLAUDE.md's mobyphp2 restyle notes
   for the full story. This is the storefront's ONLY stylesheet now -
   Tailwind CDN was removed from partials/head.php for storefront pages
   (admin keeps it, unaffected - see that file's own comment) since no
   storefront markup uses Tailwind utility classes anymore. */

@font-face {
  font-family: 'Champ';
  src: url('https://clients.thedesignfactory.co.uk/vas_tests/mobyemailcss/Champ-Black.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('https://clients.thedesignfactory.co.uk/vas_tests/mobyemailcss/BricolageGrotesque-SemiBold.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}

:root {
  /* Brand accents - amber is the primary accent (buttons, selected
     states, links). Lime is a rare tertiary accent only. */
  --amber: #F7C20E;
  --amber-tint: rgba(247, 194, 14, 0.18);
  --lime: #C8F32A;
  --lime-tint: rgba(200, 243, 42, 0.18);
  --red: #FB4C3F;
  --purple: #B49BFF;
  /* "Deep" variants: same hue, tuned for AA contrast when used as text/borders/icons. */
  --amber-deep: #927205;
  --lime-deep: #657F07;
  --purple-deep: #8257FF;
  --red-deep: #E23D2E;
  --black: #FFFFFF;
  --panel: #FFFFFF;
  --panel-2: #F2F2EF;
  --line: #C7C7BC;
  --text: #0B0B0B;
  --text-dim: #67675F;
  --font-display: 'Champ', Arial Black, sans-serif;
  --font-body: 'Bricolage Grotesque', Arial, sans-serif;
}

* { box-sizing: border-box; }
/* Browsers don't inherit font-family into form controls by default (their
   own UA stylesheet sets a system font instead) - every <button>/<input>/
   <select>/<textarea> in this app needs this to actually render in
   Bricolage Grotesque rather than silently falling back to the OS default.
   Bug, fixed: #unsuitable-reupload-btn (configure.php's "Click here to
   upload a new image") was the reported case, but this is a sitewide gap,
   not a one-off - every plain-styled button (.link-accent/.link-danger
   etc., none of which set their own font-family) was equally affected. */
button, input, select, textarea { font-family: inherit; }
/* Every page in this app needs its own real, visible-to-crawlers <h1> -
   an established requirement from before this restyle, not new - but the
   MOBY HTML/ mockup's own pages don't show one anywhere in the body
   (relying on the nav's $navSubtitle line instead, which is presentational
   copy, not a real heading). Rather than either dropping the H1
   requirement or visually reintroducing a page title the design doesn't
   call for, each restyled page keeps a real <h1> in the DOM via this
   class - present for SEO/accessibility, not shown on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* The HTML `hidden` attribute must always win over any component's own
   `display` rule (flex/grid/etc.) - without this, elements like
   .summary-row or .size-option stay visible despite [hidden] being set. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--black);
  color: var(--text);
  font-family: var(--font-body);
  /* Explicit, not left to the UA default - see the Google Fonts <link>
     in partials/head.php for why a real Regular (400) weight file now
     exists for this family. Every element that sets its own heavier
     font-weight (buttons, labels, headings, etc.) is unaffected - this
     only changes what plain, unstyled body copy resolves to. */
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

.font-heading { font-family: var(--font-display); font-weight: 400; }

/* Announcement bar */
.announce {
  background: var(--amber);
  color: #0B0B0B;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 10px 16px;
}
.announce b { font-weight: 800; }
/* Two independent halves (the admin's own free-text banner message, then
   the active discount-offer text - see promo-banner.js) sit inline with a
   bullet between them on desktop, where there's room; on mobile the
   bullet is dropped and the second half wraps to its own line instead of
   competing for width on a narrow screen. */
.announce-inner {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: 4px;
}
@media (max-width: 640px) {
  .announce-inner { flex-direction: column; align-items: center; row-gap: 2px; }
  .announce-separator { display: none !important; }
}

.icon-btn {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--panel);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  text-decoration: none;
}
.icon-btn svg { width: 18px; height: 18px; }
.icon-btn.active {
  border-color: var(--amber-deep);
  box-shadow: 0 0 0 1px var(--amber-deep);
  color: var(--amber-deep);
  cursor: default;
}

/* Main layout */
main {
  max-width: 1180px;
  margin: 0 auto;
  padding: 32px 24px 80px;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 28px;
}
@media (max-width: 860px) {
  main { grid-template-columns: 1fr; }
}

/* Image panel */
.image-panel {
  background: var(--panel-2);
  border-radius: 20px;
  padding: 28px;
  position: relative;
  min-height: 480px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.panel-flat {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 28px;
  position: relative;
  min-height: 480px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.delete-btn {
  position: absolute;
  top: 20px;
  right: 20px;
  background: var(--black);
  color: var(--red-deep);
  border: 1px solid var(--red);
  padding: 9px 16px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
}
.frame {
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 20px 50px rgba(0,0,0,0.35);
  max-height: 400px;
  display: flex;
}
/* No max-width here (removed on purpose - see below) so a landscape image
   fills the available column width; max-height caps how tall a portrait
   image can get, so it never balloons past a reasonable size. width/
   height:auto + max-width/max-height together scale the image down to fit
   within whichever constraint binds first, preserving its aspect ratio -
   never upscaled, never cropped. */
.frame img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 400px;
}
.dims {
  margin-top: 14px;
  color: var(--text-dim);
  font-size: 13px;
}
/* Additive modifier, not a change to .dims itself - #loading-message also
   uses plain .dims for its own single-line text, which this shouldn't
   affect. Puts #image-meta's two spans (the dimension/PDF-page label, and
   an optional right-aligned note) on the same line, label left/note right -
   a lone left span with nothing on the right (the normal, non-PDF case)
   looks identical to the old plain single-line text. */
.dims-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
#image-meta-note { color: var(--text-dim); font-style: italic; }

/* Sidebar */
.sidebar {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 32px;
}
.sidebar h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 20px;
  margin: 0 0 16px;
  color: var(--text);
}

.size-filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 16px;
}
/* flex-wrap added for the 3rd "IKEA Sizes" pill (configure.js) - a narrow
   sidebar can't always fit three pills on one line ("Popular"/"IKEA
   Sizes"/"Show All"), so this wraps to a second row instead of overflowing
   or forcing a horizontal scrollbar. The original 2-pill layout still
   fits on one line at every width it always did. */
.size-filter { display: flex; flex-wrap: wrap; gap: 10px; }
/* Plain text unit links (mm/cm/in) - display only, see configure.js's
   formatSizeDims(). Deliberately not styled like .filter-pill: this
   doesn't change which sizes are shown, only how their dimensions read,
   so it shouldn't compete visually with the real filter above it. */
.unit-toggle { display: flex; gap: 10px; }
.unit-link {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
  cursor: pointer;
  text-decoration: none;
}
.unit-link:hover { color: var(--text); }
.unit-link.active { color: var(--text); text-decoration: underline; font-weight: 700; }
.filter-pill {
  border: 1.5px solid var(--text-dim);
  background: var(--panel-2);
  color: var(--text);
  border-radius: 999px;
  padding: 8px 16px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 13px;
  cursor: pointer;
  transition: background-color .2s ease, border-color .2s ease, color .2s ease;
}
.filter-pill:hover { border-color: #A8A89E; }
.filter-pill.selected,
.filter-pill--active {
  background: var(--amber);
  border-color: var(--amber);
  color: #0B0B0B;
}
.size-option.filtered-out { display: none; }

.size-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border: 1px solid var(--line);
  background: var(--panel-2);
  border-radius: 12px;
  padding: 14px 16px;
  margin-bottom: 10px;
  cursor: pointer;
  transition: border-color .15s ease;
}
.size-option:hover { border-color: #A8A89E; }
.size-option.selected {
  border-color: var(--amber-deep);
  box-shadow: 0 0 0 1px var(--amber-deep);
  background: color-mix(in srgb, var(--amber) 12%, var(--panel-2));
}
.size-option.disabled {
  cursor: not-allowed;
  background: #F4F4EF;
}
.size-option.disabled .name,
.size-option.disabled .price {
  opacity: 0.4;
}
.size-option.disabled .dims-small {
  opacity: 0.7;
}
.size-option .name {
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
}
/* Shown only under "Show All" (configure.js's renderSizeList()) - under
   the Popular/IKEA Sizes pills every card already belongs to that
   category, so a badge there would just repeat the pill's own label. */
.size-tag {
  display: inline-block;
  font-size: 9px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 7px;
  border-radius: 999px;
  margin-left: 6px;
  vertical-align: middle;
}
.size-tag--popular { background: var(--amber); color: #0B0B0B; }
.size-tag--ikea { background: #DCEBFF; color: #1D4ED8; }
.size-option .dims-small {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 2px;
}
.size-option .warn {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  color: var(--red-deep);
  background: rgba(251, 76, 63, 0.1);
  border: 1px solid rgba(251, 76, 63, 0.3);
  padding: 3px 8px;
  border-radius: 6px;
  margin-top: 6px;
}
.size-option .price {
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
}
.size-option.selected .price { color: var(--amber-deep); }

.section-label {
  font-weight: 700;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin: 22px 0 10px;
}

.fit-row {
  display: flex;
  gap: 22px;
}
.radio-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  cursor: pointer;
}
.radio-opt input { accent-color: var(--amber-deep); width: 16px; height: 16px; }

.paper-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border: 1px solid var(--line);
  background: var(--panel-2);
  border-radius: 12px;
  padding: 13px 16px;
  margin-bottom: 8px;
  cursor: pointer;
}
.paper-option.selected {
  border-color: var(--purple-deep);
  box-shadow: 0 0 0 1px var(--purple-deep);
}
.paper-option .dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--line);
  flex-shrink: 0;
}
.paper-option.selected .dot {
  border-color: var(--purple-deep);
  background: radial-gradient(var(--purple-deep) 0%, var(--purple-deep) 40%, transparent 45%);
}
.paper-option-inner { display:flex; align-items:center; gap:10px; font-size:14px; font-weight: 600; }

.price-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 24px 0 16px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
.price-row .label { color: var(--text-dim); font-size: 14px; }
.price-row .amount {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 26px;
  color: var(--text);
}

.add-cart-btn {
  width: 100%;
  background: var(--amber);
  color: #0B0B0B;
  border: none;
  border-radius: 999px;
  padding: 16px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 15px;
  letter-spacing: 0.01em;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  display: inline-block;
}
.add-cart-btn:hover { filter: brightness(1.05); }
.add-cart-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-outline {
  width: 100%;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 16px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  margin-bottom: 14px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
}
.btn-outline:hover { border-color: #A8A89E; }

/* Cart confirmation card */
.confirm-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  justify-content: center;
}
.checkmark-circle {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--amber-tint);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
}
.checkmark-circle svg { width: 28px; height: 28px; color: var(--amber-deep); }
.confirm-card h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 24px;
  margin: 0 0 8px;
  color: var(--text);
}
.confirm-sub {
  margin: 0 0 26px;
  color: var(--text-dim);
  font-size: 14px;
}
.confirm-actions { width: 100%; }

/* Checkout page */
main.checkout-layout {
  display: flex;
  flex-direction: column;
  gap: 28px;
  max-width: 760px;
}

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 32px;
}
.card h1,
.card h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 20px;
  margin: 0;
  color: var(--text);
}

.cart-btn { position: relative; }
.cart-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 50%;
  background: var(--purple);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--black);
}

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}
.link-accent {
  color: var(--amber-deep);
  text-decoration: none;
  font-weight: 600;
  font-size: 13px;
}
.link-accent:hover { text-decoration: underline; }
.link-danger {
  color: var(--red-deep);
  text-decoration: none;
  font-weight: 600;
  font-size: 13px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
}
.link-danger:hover { text-decoration: underline; }

.cart-item {
  display: flex;
  align-items: center;
  gap: 18px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 20px;
}
.cart-item:last-of-type { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.cart-item-thumb {
  width: 82px;
  height: 82px;
  border-radius: 8px;
  object-fit: cover;
  flex-shrink: 0;
}
.cart-item-thumb-placeholder {
  width: 82px;
  height: 82px;
  border-radius: 8px;
  flex-shrink: 0;
  background: var(--panel-2);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 9px;
  text-align: center;
  line-height: 1.2;
}
.cart-item-info { flex: 1; }
.cart-item-name { font-weight: 700; font-size: 14px; color: var(--text); }
.cart-item-price { font-size: 13px; color: var(--text-dim); margin-top: 2px; }
.cart-item-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
}

/* Quantity stepper - the one place in the app a customer can adjust
   quantity (see checkout.js's renderCartList()). Deliberately not a
   .filter-pill/.unit-link reuse - this needs a bounded numeric input
   between two buttons, a different interaction shape. */
.cart-item-qty {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}
.cart-item-qty-btn {
  width: 24px;
  height: 24px;
  border-radius: 999px;
  border: 1.5px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
  font-weight: 700;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: border-color .2s ease;
}
.cart-item-qty-btn:hover { border-color: var(--text-dim); }
.cart-item-qty-btn:disabled { opacity: .4; cursor: default; }
.cart-item-qty-input {
  width: 32px;
  text-align: center;
  border: none;
  background: none;
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
  font-family: var(--font-body);
  padding: 0;
  /* Without this, Chrome/Safari's native up/down spinner arrows eat the
     right half of the 32px box, which is what actually made the digit
     look left-aligned despite text-align:center above - the spinner, not
     the text, was occupying that space. */
  -moz-appearance: textfield;
  appearance: textfield;
}
.cart-item-qty-input::-webkit-outer-spin-button,
.cart-item-qty-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* "Add more copies and save X%" prompt, right where a customer can act on
   it. --lime-deep, not the raw --lime tint, since this sits on white/
   --panel-2 and needs real text contrast. */
.cart-item-qty-hint {
  font-size: 12px;
  font-weight: 700;
  color: var(--lime-deep);
  margin-top: 6px;
}
/* The "1 x £X + N x £Y (Z% off)" line-item breakdown, once quantity > 1 -
   the actual line-item requirement, not just decoration. */
.cart-item-breakdown {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 4px;
}
.cart-subtotal-row {
  display: flex;
  justify-content: space-between;
  font-weight: 700;
  font-size: 16px;
  color: var(--text);
  padding-top: 20px;
  /* Was 4px - fine while the last item's own content was just name+price,
     but .cart-item:last-of-type zeroes that item's own padding-bottom/
     border, so once the quantity stepper/hint became its last line of
     text, only 4px separated it from this divider - visibly cramped.
     20px matches the normal item-to-item gap (padding-bottom: 22px on
     .cart-item) so the last item gets the same breathing room as every
     other divider in this list. */
  margin-top: 20px;
  border-top: 1px solid var(--line);
}

.muted { color: var(--text-dim); font-size: 13px; margin: 6px 0 20px; }

.account-fields { display: flex; gap: 14px; flex-wrap: wrap; }
.input {
  min-width: 140px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 15px 16px;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 14px;
  width: 100%;
}
.input::placeholder { color: var(--text-dim); }
.input:focus { outline: none; border-color: var(--purple-deep); }
.account-fields .input { flex: 1; }

/* Password visibility toggle - checkout's login/create-password fields only.
   right:16px mirrors .input's own left content padding (padding: 15px
   16px), so the icon reads as a normal, symmetric part of the field
   rather than a stray floating button pushed inward to dodge something.
   A password-manager extension (LastPass, 1Password, etc.) draws its own
   icon in a separate overlay layer we don't control and can't reliably
   detect - those extensions already account for existing right-aligned
   input content and shift their own icon further out rather than the
   other way around, so this field doesn't need to reserve extra space
   for one on the assumption it'll be present. */
.password-field { position: relative; flex: 1; min-width: 140px; }
.password-field .input { padding-right: 46px; }
.account-fields .password-field { flex: 1; }
.password-toggle-btn {
  position: absolute;
  top: 50%;
  right: 16px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
}
.password-toggle-btn:hover { color: var(--text); }
.password-toggle-btn svg { width: 18px; height: 18px; }
.password-toggle-btn .icon-eye-off { display: none; }
.password-toggle-btn.revealed .icon-eye { display: none; }
.password-toggle-btn.revealed .icon-eye-off { display: block; }
.btn-compact {
  width: auto;
  padding: 15px 26px;
  margin-bottom: 0;
  white-space: nowrap;
}

.step-label {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 20px;
  color: var(--text);
  margin: 32px 0 16px;
}
.step-label:first-child { margin-top: 0; }

.radio-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border: 1px solid var(--line);
  background: var(--panel-2);
  border-radius: 12px;
  padding: 17px 20px;
  margin-bottom: 12px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text);
}
.radio-row-main { display: flex; align-items: center; gap: 12px; }
.radio-row input { accent-color: var(--amber-deep); width: 16px; height: 16px; }
.radio-row.selected { border-color: var(--amber-deep); box-shadow: 0 0 0 1px var(--amber-deep); }
/* A delivery option below its own admin-set minimum order value - same
   greyed-out/disabled treatment as .size-option.disabled (an unsuitable
   size), for visual consistency across the app's two "this choice isn't
   available right now" cases. */
.radio-row.disabled { cursor: not-allowed; background: #F4F4EF; }
.radio-row.disabled .radio-row-main { opacity: 0.4; }
.radio-row-min-order-badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  color: var(--red-deep);
  background: rgba(251, 76, 63, 0.1);
  border: 1px solid rgba(251, 76, 63, 0.3);
  padding: 3px 8px;
  border-radius: 6px;
}

.address-box {
  border: 1px solid var(--line);
  background: var(--panel-2);
  border-radius: 12px;
  padding: 17px 20px;
  font-size: 14px;
  color: var(--text);
  cursor: pointer;
}
.address-box:hover { border-color: #A8A89E; }

.order-summary { margin: 28px 0; padding-top: 22px; border-top: 1px solid var(--line); }
.summary-row {
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  color: var(--text-dim);
  margin-bottom: 10px;
}
.summary-row.total {
  color: var(--text);
  font-weight: 700;
  font-size: 16px;
  margin-bottom: 0;
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-dim);
  margin: 18px 0;
  cursor: pointer;
}
.checkbox-row input { accent-color: var(--amber-deep); width: 16px; height: 16px; flex-shrink: 0; }
.checkbox-row a { color: var(--amber-deep); text-decoration: underline; }

.test-mode-badge {
  display: inline-block;
  background: var(--amber);
  color: #0B0B0B;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding: 6px 12px;
  border-radius: 6px;
  margin-bottom: 20px;
}

/* Discount code */
.discount-placeholder { color: var(--text-dim); }
.discount-applied-text { color: var(--lime-deep); font-weight: 600; }
.discount-change-link { color: inherit; text-decoration: underline; }
.summary-row.discount-row { color: var(--lime-deep); }

/* Upload page */
main.upload-layout {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 24px;
}
/* Bug, fixed: a flex item's default min-width is `auto`, not 0 - which
   means it refuses to shrink below its content's own natural size even
   though .upload-card's `max-width:100%` (below) looks like it should
   cap it. Since .upload-card has a literal `width:420px`, that becomes
   this wrapping div's min-content floor - harmless on desktop (main's
   content area is always wider than 420px there) but on a real mobile
   device (confirmed on an iPhone 15 Pro Max, 430px viewport, main's own
   24px side padding leaves ~382px) the card stayed pinned at its full
   420px and started bleeding past main's own padding, sitting closer to
   the screen edge than the stepper's "1 2 3" row right above it.
   min-width:0 is the standard fix for this exact flexbox trap - it lets
   the item shrink to fit main's actual available width, at which point
   .upload-card's own max-width:100% correctly takes over. */
.upload-card-slot { min-width: 0; }
.upload-card {
  width: 420px;
  max-width: 100%;
  min-height: 500px;
  border-radius: 20px;
  padding: 32px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.upload-dropzone {
  border: 2px dashed var(--line);
  background: transparent;
  justify-content: center;
  cursor: pointer;
}
.upload-dropzone:hover { border-color: var(--purple-deep); }
.upload-dropzone.dragover { border-color: var(--purple-deep); background: var(--panel-2); }
.upload-icon-circle {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--panel-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  margin-bottom: 20px;
}
.upload-icon-circle svg { width: 22px; height: 22px; }
.upload-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 18px;
  color: var(--text);
  margin: 0 0 8px;
}
.upload-sub { color: var(--text-dim); font-size: 14px; }
/* Supported-format hint, one step further down the visual hierarchy than
   .upload-sub (smaller, same muted color) - common dropzone UI practice
   (Dropbox/Google Drive etc.) pairs the drag-and-drop instruction with a
   short, scannable list of accepted formats right underneath it, rather
   than leaving format support undiscoverable until an unsupported file is
   actually rejected. Desktop-only (mobile's camera/gallery picker has no
   equivalent "which formats" question - the OS handles that). No file-size
   limit mentioned alongside it - this app enforces none (chunked upload,
   see assets/js/upload.js), so stating one here would be fabricated, not
   informative. HEIC is called out explicitly (not just "JPG, PNG") since
   it's the default iPhone camera format and, as of the WASM decoder
   fallback in assets/js/dims.js, is now genuinely supported on every
   desktop browser, not just Safari - this line is what actually tells a
   desktop Chrome/Firefox/Edge user that up front, rather than them finding
   out only after dragging one in.
*/
.upload-formats { color: var(--text-dim); font-size: 12px; margin-top: 6px; }
.upload-mobile-only { display: none; }

.upload-mobile-card {
  background: var(--panel);
  border: 1px solid var(--line);
  justify-content: flex-start;
  padding-top: 40px;
}
.upload-mobile-card .upload-title { margin-bottom: 24px; }
.qr-box {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 20px;
}
.qr-box canvas,
.qr-box img { display: block; width: 200px; height: 200px; }

@media (max-width: 640px) {
  .upload-mobile-card { display: none; }
  .upload-desktop-only { display: none; }
  .upload-mobile-only { display: block; }
  .upload-card { min-height: 0; padding: 24px; }
  .upload-dropzone { min-height: 420px; justify-content: center; }
  .upload-icon-circle { width: 56px; height: 56px; margin-bottom: 20px; }
  .upload-icon-circle svg { width: 22px; height: 22px; }
  .upload-title { font-size: 18px; }
  .upload-sub { font-size: 15px; padding: 0 12px; }
}

/* Collapses the upload layout to a single centered column once the QR
   card is force-hidden (a QR scan arrived here directly), so the
   remaining upload card doesn't look like an orphaned half-width box in
   what was a two-card row. */
.upload-layout.single-upload-mode { justify-content: center; }

/* Auth pages */
main.auth-layout {
  max-width: 1180px;
  margin: 0 auto;
  padding: 40px 24px 80px;
}
.auth-card { max-width: 440px; }
.auth-card h2 { margin: 0 0 24px; }
.field-label {
  display: block;
  font-weight: 700;
  font-size: 13px;
  color: var(--text);
  margin: 0 0 8px;
}
.field-group { margin-bottom: 18px; }
/* Shown/hidden via the plain `hidden` attribute throughout this app's own
   JS (checkout.js/account.php/reset-password.php etc.) - not the mockup's
   own `.visible`-class convention, so this has no display:none of its own
   to fight the [hidden] rule with (same reasoning as .modal-overlay's own
   comment above). block by default; JS only ever toggles the `hidden`
   attribute, never a `.visible` class. */
.field-error {
  display: block;
  color: var(--red-deep);
  font-size: 13px;
  line-height: 1.5;
  margin: 10px 0 0;
}
/* An account_warning (checkout.js) is a non-fatal notice (e.g. a duplicate
   email degrading to guest checkout), not an error - same .field-error
   shell, amber instead of the default red. */
.field-error.warning { color: var(--amber-deep); }
/* .field-error's own margin is top-only (0 below), so a message that sits
   directly above a full-width button (checkout's terms-agree-error,
   account.php's login-error, reset-password.php's reset-error, etc.) has
   no breathing room before the button starts - reads as visually stuck to
   it. Opt-in modifier rather than changing .field-error's own margin
   globally, since plenty of other .field-error usages (inline next to a
   field, or already followed by more spaced-out content) don't need it. */
.field-error--gap { margin-bottom: 16px; }
.input.error { border-color: var(--red); }

/* configure.php's shared "you missed a required step" banner
   (#cart-validation-error) - used for both "Choose your fit" and the PDF
   sizing acknowledgment, positioned directly under the "Add to cart"
   button (see configure.js's showCartValidationError()). Deliberately a
   real bordered/tinted box with an icon, not a plain .field-error line -
   this message was getting missed entirely when it sat as thin text up
   near whichever field it referred to, since by the time the customer
   clicks the button they've usually scrolled well past that point. Kept
   in the site's existing red (not softened to amber) specifically so it
   still reads as "this needs fixing before you can continue", not just
   an FYI. */
.cart-validation-error {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: rgba(226, 61, 46, 0.08);
  border: 1.5px solid var(--red-deep);
  border-radius: 12px;
  padding: 13px 14px;
  margin-top: 12px;
}
.cart-validation-error svg { flex-shrink: 0; margin-top: 1px; color: var(--red-deep); }
.cart-validation-error p {
  margin: 0;
  color: var(--red-deep);
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.45;
}
/* A brief, one-time entrance cue (not a persistent/looping animation) so
   the banner registers even in a peripheral glance - re-triggered by JS
   toggling this class off and back on, since re-adding `hidden` and
   removing it again doesn't by itself replay a CSS animation. */
.cart-validation-error--shake { animation: cart-validation-shake 0.4s ease; }
@keyframes cart-validation-shake {
  0% { transform: translateX(0); opacity: 0; }
  20% { transform: translateX(-6px); opacity: 1; }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
  100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .cart-validation-error--shake { animation: none; }
}
.field-hint {
  font-size: 12px;
  color: var(--text-dim);
  margin: 6px 0 0;
}
.field-row-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}
.field-row {
  display: grid;
  grid-template-columns: 150px 1fr;
  align-items: center;
  gap: 14px;
  margin-bottom: 16px;
}
.field-row-narrow { grid-template-columns: 70px 1fr; }
.field-row .field-label,
.field-row-narrow .field-label { margin: 0; white-space: nowrap; }
.field-row .field-hint { margin: 6px 0 0; }

/* Shown/hidden via the plain `hidden` attribute (same convention as every
   other toggle in this restyle - see cart-badge.js/promo-banner.js's own
   comments), not a separate `.open` class - display:flex here is the
   *shown* state, [hidden]'s own !important display:none (this file's
   [hidden] rule near the top) always wins while the attribute is set. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 100;
}
.modal-card {
  position: relative;
  background: var(--panel);
  border-radius: 20px;
  max-width: 675px;
  width: 100%;
  max-height: min(600px, 90vh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 8px 0 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--line);
}
.modal-header h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 22px;
  color: var(--text);
  margin: 0;
}
.modal-required-note {
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}
.modal-close-x {
  display: none;
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 2;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
}
.modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 20px 32px;
}
.modal-account-section .checkbox-row { margin: 0; }
.modal-account-section #addrPasswordWrap { margin: 12px 0 0; }
.modal-footer {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px 32px;
  border-top: 1px solid var(--line);
}
.modal-footer-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.modal-card-simple {
  background: var(--panel);
  border-radius: 20px;
  padding: 32px;
  max-width: 400px;
  width: 100%;
}
.modal-card-simple h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 20px;
  color: var(--text);
  margin: 0 0 20px;
}
.modal-card-simple .input { margin-bottom: 4px; }
.discount-modal-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 20px;
}
.discount-modal-actions { display: flex; gap: 12px; margin-left: auto; }
.remove-discount-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--red-deep);
  font-size: 13px;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

@media (max-width: 640px) {
  .modal-overlay { padding: 0; }
  .modal-card { max-width: 100%; width: 100%; height: 100%; max-height: 100%; border-radius: 0; }
  .modal-required-note { display: none; }
  .modal-close-x { display: block; }
  .field-row,
  .field-row-narrow { grid-template-columns: 1fr; gap: 6px; margin-bottom: 14px; }
}
.password-row { position: relative; }
.password-row .input { padding-right: 44px; }
.password-toggle {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
}
.password-toggle:hover { color: var(--text); }
.password-toggle svg { width: 18px; height: 18px; }
.password-toggle .icon-eye-off { display: none; }
.password-toggle.showing .icon-eye { display: none; }
.password-toggle.showing .icon-eye-off { display: block; }

.auth-forgot {
  display: block;
  color: var(--amber-deep);
  font-size: 14px;
  text-decoration: none;
  margin: 16px 0 4px;
}
.auth-forgot:hover { text-decoration: underline; }

.auth-divider {
  border-top: 1px solid var(--line);
  margin-top: 24px;
  padding-top: 20px;
}
.auth-note {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.6;
  margin: 0;
}

/* Top nav */
.top-nav {
  max-width: 1180px;
  margin: 0 auto;
  padding: 18px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
/* display:flex here (rather than leaving .brand a plain block wrapping an
   inline-block <a>) sidesteps inline-formatting-context quirks (baseline
   alignment, phantom descender space under an inline-block) that were
   leaving the logo a couple of px off true-center against the icon
   circles even after the margin/height fixes below. */
.top-nav .brand { flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-start; }
.top-nav .brand p { margin: 0; color: var(--text-dim); font-size: 14px; }
.brand-logo-wrap { display: inline-block; margin: 0 0 6px; }
.brand-logo { display: block; height: 32px; width: auto; }
.top-nav-links {
  display: flex;
  align-items: center;
  gap: 32px;
  flex-wrap: wrap;
}
.top-nav-link {
  display: flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}
.top-nav-link:hover { color: var(--amber-deep); }
/* "Upload your file" is the one nav link that's actually a primary call
   to action, not a browse/menu link like the rest of .top-nav-links - a
   filled pill sets it apart from its plain-text siblings so it reads as
   the thing to click, matching the same amber/dark-text/999px-radius
   treatment .add-cart-btn already uses for this site's other primary
   actions, just sized for the nav bar. */
.top-nav-link--lozenge {
  background: #BA8AF8;
  color: #fff;
  padding: 10px 20px;
  border-radius: 999px;
}
.top-nav-link--lozenge:hover { color: #fff; filter: brightness(1.05); }
.top-nav-chevron { width: 14px; height: 14px; transition: transform .15s ease; }
.top-nav-dropdown { position: relative; }
.top-nav-dropdown-panel {
  display: none;
  position: absolute;
  top: calc(100% + 14px);
  left: -14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 16px 36px rgba(0,0,0,0.18);
  padding: 10px;
  min-width: 230px;
  z-index: 60;
}
.top-nav-dropdown.open .top-nav-dropdown-panel { display: block; }
.top-nav-dropdown.open .top-nav-chevron { transform: rotate(180deg); }
.top-nav-dropdown-item {
  display: block;
  padding: 10px 14px;
  border-radius: 8px;
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
}
.top-nav-dropdown-item:hover { background: var(--panel-2); }
.top-nav-icons { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.hamburger-btn { display: none; cursor: pointer; }

.mobile-nav-panel { display: none; }
.mobile-nav-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 18px 24px;
  border: none;
  border-top: 1px solid var(--line);
  border-radius: 0;
  background: none;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
  text-align: left;
}
.mobile-nav-accordion .top-nav-chevron { width: 16px; height: 16px; }
.mobile-nav-accordion.open .top-nav-chevron { transform: rotate(180deg); }
.mobile-nav-accordion-panel { display: none; padding: 0 24px 12px 24px; }
.mobile-nav-accordion.open .mobile-nav-accordion-panel { display: block; }
.mobile-nav-sub-link {
  display: block;
  padding: 10px 0 10px 16px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 14px;
  background: none;
  border: none;
  width: 100%;
  text-align: left;
  cursor: pointer;
}
.mobile-nav-sub-link:hover { color: var(--text); }

@media (max-width: 860px) {
  .top-nav-links { display: none; }
  .hamburger-btn { display: flex; }
  .mobile-nav-panel.open { display: block; border-top: 1px solid var(--line); }
  /* Bug, fixed: .brand is flex-shrink:0 (deliberately, so the logo itself
     never compresses) - but $navSubtitle (partials/nav.php) can be a full
     sentence long (e.g. upload.php's "Upload a photo, pick a size, print
     a poster."), and a flex-shrink:0 sibling with unconstrained text
     content pushed .top-nav-icons (cart/account/hamburger) off the right
     edge of the viewport entirely at narrow widths - confirmed live, the
     account icon and hamburger were rendering past x=375 on a 375px-wide
     screen. Hiding the subtitle below this same breakpoint the nav
     already collapses at is the simplest fix - the page's own <h1>
     already carries this information in the page body. */
  .top-nav .brand p { display: none; }
  /* Bug, fixed: on a real device (iPhone 15 Pro Max, 430px CSS viewport)
     the logo rendered ~303px wide at its desktop 32px height (the
     wordmark's own aspect ratio is very wide), crowding the cart/account/
     hamburger icons - confirmed live via getBoundingClientRect(). Shrink
     it at this same breakpoint the nav already collapses at. */
  .brand-logo { height: 22px; }
  /* .brand-logo-wrap's 6px bottom margin exists to separate the logo from
     the tagline <p> below it - but that <p> is hidden at this breakpoint
     (see above), so the margin was just dangling space skewing the logo
     ~5px above the icon circles' true vertical center (confirmed live:
     logo center at y=34 vs icon center at y=39, same .top-nav row).
     Zeroing it here makes .brand's box exactly the logo's own height, so
     .top-nav's align-items:center actually centers the logo itself
     against the icons, not a taller box with unused space in it. */
  .brand-logo-wrap { margin-bottom: 0; }
}

/* Footer */
.site-footer {
  border-top: 1px solid var(--line);
  background: var(--panel-2);
  margin-top: 60px;
}
.footer-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 56px 24px 36px;
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr 1fr;
  gap: 32px;
}
.footer-logo-wrap { display: inline-block; margin-bottom: 16px; }
.footer-logo { display: block; height: 26px; width: auto; }
.footer-desc {
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.6;
  margin: 0 0 14px;
  max-width: 280px;
}
.footer-email {
  display: inline-block;
  color: var(--amber-deep);
  font-size: 14px;
  text-decoration: none;
  margin-bottom: 18px;
}
.footer-email:hover { text-decoration: underline; }
.footer-social {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--text);
}
.footer-social svg { width: 18px; height: 18px; }
.footer-col-heading { margin: 0 0 16px; }
/* "Prices" (footer.php's Paper Types column) - a real link but styled
   like this column's own heading, not like the muted .footer-links list
   above it, so it reads as a second, equally-weighted way into that
   column's content rather than just another list item. */
.footer-col-heading-link { display: block; margin: 16px 0 0; text-decoration: none; cursor: pointer; }
.footer-col-heading-link:hover { color: var(--text); }
.footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.footer-links a {
  color: var(--text-dim);
  text-decoration: none;
  font-size: 14px;
}
.footer-links a:hover { color: var(--text); }
.footer-bottom {
  border-top: 1px solid var(--line);
  text-align: center;
  padding: 20px 24px;
  color: var(--text-dim);
  font-size: 13px;
}

@media (max-width: 700px) {
  .footer-inner { grid-template-columns: 1fr 1fr; }
  .footer-col-brand { grid-column: 1 / -1; }
}

/* Stepper */
.stepper {
  max-width: 1180px;
  margin: 28px auto 0;
  padding: 0 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
}
.stepper-step {
  display: flex;
  align-items: center;
  gap: 10px;
}
.stepper-index {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 700;
  border: 1.5px solid var(--line);
  background: var(--panel-2);
  color: var(--text-dim);
}
.stepper-label {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
}
.stepper-step.active .stepper-index {
  background: var(--amber);
  border-color: var(--amber);
  color: #0B0B0B;
}
.stepper-step.active .stepper-label { color: var(--text); }
.stepper-arrow {
  width: 16px;
  height: 16px;
  color: var(--line);
  flex-shrink: 0;
}

@media (max-width: 640px) {
  .stepper {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    margin: 20px auto 0;
  }
  .stepper-arrow { display: none; }
  .stepper-step {
    padding: 10px 14px;
    border-radius: 10px;
  }
  .stepper-step.active { background: var(--amber-tint); }
  .stepper-label { font-size: 14px; white-space: normal; }
  .stepper-index { width: 24px; height: 24px; font-size: 12px; }
}

/* --- Functional CSS below: not decorative, load-bearing for real
   interactions. Re-toned to the new palette, never deleted. --- */

/* QR frame - assets/js/upload-page.js's renderQR() draws the vendored
   generator's canvas straight into #qr-canvas inside .qr-box above; no
   separate frame class needed since .qr-box already provides it. */

/* Crop tool (assets/js/crop.js) - canvas mechanics, not decoration.
   Colors re-toned from the old sky-blue accent to amber-deep, matching
   this app's other "active selection" accents (.size-option.selected). */
.crop-stage {
  position: relative;
  display: inline-block;
  user-select: none;
  line-height: 0;
  border-radius: var(--radius-standard, 10px);
  overflow: hidden;
  background: #0f172a;
  /* preventDefault() on pointerdown isn't reliably enough on its own to
     suppress the browser's default pan/zoom touch gestures on every
     mobile browser - this is the belt-and-suspenders CSS fix. */
  touch-action: none;
}
.crop-stage canvas {
  display: block;
  max-width: 100%;
  height: auto;
}
.crop-dim {
  position: absolute;
  background: rgba(15, 23, 42, 0.6);
  pointer-events: none;
}
.crop-marquee {
  position: absolute;
  border: 2px solid #FFFFFF;
  cursor: move;
  box-sizing: border-box;
  touch-action: none;
}
.crop-handle {
  position: absolute;
  width: 24px;
  height: 24px;
  background: #FFFFFF;
  border: 2px solid var(--amber-deep);
  border-radius: 4px;
  transform: translate(-50%, -50%);
  touch-action: none;
  /* 24px, not the usual ~44px touch-target guidance: a crop tool is a
     precision control, and handles this large actively hurt fine-grained
     resizing on a small canvas - roughly what native photo-crop tools
     use for the same reason. */
}
.crop-handle--nw { top: 0; left: 0; cursor: nwse-resize; }
.crop-handle--ne { top: 0; left: 100%; cursor: nesw-resize; }
.crop-handle--sw { top: 100%; left: 0; cursor: nesw-resize; }
.crop-handle--se { top: 100%; left: 100%; cursor: nwse-resize; }

.chip-warning {
  background: #FEF3C7;
  color: #92400E;
  border-radius: 9999px;
}

/* Small utilities used by a handful of pages, not big enough to be their
   own named component - kept together here rather than scattered. */
.status-text { font-size: 13px; margin-top: 12px; color: var(--purple-deep); text-align: center; }
.status-text.error { color: var(--red-deep); }
.status-text a { color: inherit; text-decoration: underline; font-weight: 600; }
.dev-banner {
  background: #FEF3C7;
  border-bottom: 1px solid #FCD34D;
  color: #92400E;
  font-size: 13px;
  padding: 8px 24px;
  text-align: center;
}
.center-page { min-height: 60vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.card--narrow { max-width: 420px; margin: 0 auto; text-align: center; }
.info-btn { background: none; border: none; color: var(--text-dim); cursor: pointer; display: inline-flex; align-items: center; padding: 2px; }
.info-btn:hover { color: var(--amber-deep); }
.info-btn svg { width: 16px; height: 16px; }
.crop-warning { font-size: 13px; color: var(--red-deep); background: rgba(251, 76, 63, 0.1); border: 1px solid rgba(251, 76, 63, 0.3); border-radius: 8px; padding: 10px 14px; }

/* Deliberately a plain !important rule, not reused via a component's own
   `display` rule: any element mixing an unprefixed hidden/shown toggle
   with a competing responsive/state rule needs this to reliably win
   regardless of source order - the general version of the old
   `hidden` vs `sm:flex` Tailwind-specificity trap this app hit more than
   once before the Tailwind removal. */
.force-hidden { display: none !important; }

/* Minimal styling for admin-edited (TinyMCE) copy rendered raw via
   poster-size.php and good-to-know.php - without this the editor's own
   HTML (h2/h3/p/ul/ol/a/img) renders as flat, same-size text with no
   spacing between blocks. Scoped to .prose-content only. */
.prose-content h2 { font-family: var(--font-display); font-weight: 400; font-size: 1.375rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.prose-content h3 { font-family: var(--font-display); font-weight: 400; font-size: 1.125rem; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose-content p { margin-bottom: 1rem; line-height: 1.6; }
.prose-content ul, .prose-content ol { margin-bottom: 1rem; padding-left: 1.5rem; line-height: 1.6; }
.prose-content ul { list-style: disc; }
.prose-content ol { list-style: decimal; }
.prose-content a { color: var(--amber-deep); text-decoration: underline; font-weight: 600; }
/* An image inserted via TinyMCE's "Insert image" button is a bare
   <img src="..." alt=""> with no classes of its own - this is what gives
   every one the same rounded-corner + border card look as every other
   image in this app, rather than sitting oddly square-cornered. */
.prose-content img { border-radius: 16px; border: 1px solid var(--line); max-width: 100%; height: auto; display: block; margin-top: 1rem; margin-bottom: 1rem; }

/* Account page (account.php) - logged-in order history has no mockup of
   its own; extends the same .card vocabulary the rest of the app already
   uses (checkout.php's cards, etc.) rather than inventing a new look. */
main.account-layout { display: flex; flex-direction: column; gap: 24px; max-width: 760px; }
.page-heading { font-family: var(--font-display); font-weight: 400; font-size: 22px; color: var(--text); margin: 4px 0 0; }
.status-badge {
  display: inline-block;
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 5px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.account-order-items { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.account-order-item { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--panel-2); }
.account-order-item:first-child { border-top: none; }
.account-order-thumb { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; background: var(--panel-2); border: 1px solid var(--line); flex-shrink: 0; }
.account-order-thumb-placeholder { width: 48px; height: 48px; border-radius: 10px; background: var(--panel-2); border: 1px solid var(--line); flex-shrink: 0; display: flex; align-items: center; justify-content: center; text-align: center; }
.account-order-totals { font-size: 14px; }
.account-order-totals .summary-row { padding: 4px 0; }

/* Content pages (poster-size.php, good-to-know.php) - a single wide,
   border-free content block housing prose copy, reusing .account-layout's
   centered flex column rather than inventing a second near-identical
   wrapper class. .content-card is the same shape as .card (background/
   padding/radius) minus the border - these pages read as plain page
   content, not a bordered widget. Heading sizes are deliberately their
   own scale, not .card h1/h2's fixed 20px (that rule stays small on
   purpose for compact card headers elsewhere - auth cards, modals,
   confirmation - and isn't meant for a real page-level H1). */
.content-card { background: var(--panel); border-radius: 20px; padding: 32px; }
.content-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 28px;
  line-height: 1.15;
  color: var(--text);
  margin: 0 0 8px;
}
.content-subtitle { color: var(--text-dim); font-size: 16px; line-height: 1.5; margin: 0; }
/* A plain browser-default blue link inside .content-subtitle (faqs.php's
   mailto link) read as off-brand against this app's palette - same
   amber-deep + underline + bold treatment .prose-content a already uses
   elsewhere, so an inline link reads consistently everywhere it appears
   on the site rather than falling back to the browser default. */
.content-subtitle a { color: var(--amber-deep); text-decoration: underline; font-weight: 600; }
.content-hint { color: var(--text-dim); font-size: 14px; line-height: 1.5; margin: 10px 0 0; }
.content-h2 { font-family: var(--font-display); font-weight: 400; font-size: 20px; color: var(--text); margin: 0 0 14px; }
@media (max-width: 640px) {
  .content-card { padding: 20px; }
  .content-title { font-size: 22px; }
  .content-subtitle { font-size: 14px; }
  .content-hint { font-size: 13px; }
  .content-h2 { font-size: 18px; }
}

/* faqs.php's full FAQ accordion - reuses the exact same .hd-faq-question/
   .hd-faq-answer/.hd-faq-icon markup and assets/js/faq-accordion.js as
   index.php's home-page accordion (see that file's own header comment),
   but this page never loads index-design.css (that stylesheet is
   deliberately page-scoped to index.php - see CLAUDE.md), so these are
   full standalone rules, not overrides layered on top of anything -
   themed for a plain white .content-card instead of index.php's coral
   band, using this page's own --text/--text-dim/--line/--panel-2
   tokens. Scoped under .faq-page-list so index.php's own coral-band
   rules for the identical class names are completely unaffected. */
.faq-page-list .hd-faq-list { border-top: 1px solid var(--line); }
.faq-page-list .hd-faq-question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  background: none;
  border: none;
  border-bottom: 1px solid var(--line);
  padding: 14px 16px;
  margin: 0;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 17px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease;
}
.faq-page-list .hd-faq-question[aria-expanded="true"] {
  border-bottom-color: transparent;
  background: var(--panel-2);
}
.faq-page-list .hd-faq-icon { position: relative; width: 22px; height: 22px; flex-shrink: 0; }
.faq-page-list .hd-faq-icon::before,
.faq-page-list .hd-faq-icon::after {
  content: "";
  position: absolute;
  background: var(--text);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.faq-page-list .hd-faq-icon::before { width: 22px; height: 2px; }
.faq-page-list .hd-faq-icon::after { width: 2px; height: 22px; transition: transform 0.15s ease; }
.faq-page-list .hd-faq-question[aria-expanded="true"] .hd-faq-icon::after { transform: translate(-50%, -50%) rotate(90deg); opacity: 0; }
.faq-page-list .hd-faq-answer:not([hidden]) + .hd-faq-question { border-top: 1px solid var(--line); }
.faq-page-list .hd-faq-answer { padding: 14px 16px 26px; }
.faq-page-list .hd-faq-answer p {
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1.6;
  margin: 0;
  max-width: 620px;
}

.link-tile {
  display: block;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 14px 16px;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease;
}
.link-tile:hover { border-color: var(--amber-deep); }
.link-tile-label { display: block; font-weight: 700; font-size: 14px; color: var(--text); }
.link-tile-price { display: block; font-size: 13px; color: var(--text-dim); margin-top: 2px; }
.link-tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }

/* Poster-size comparison chart (poster-size.php, every size/combo page -
   config/catalog.php's render_size_comparison_chart()) - a real person
   for scale, not just a number. Fixed height so the row never fights the
   page for vertical space; width is intentionally left to the SVG's own
   natural size (never stretched to fill the card) since squeezing it
   would defeat the whole "true to scale" point - it scrolls horizontally
   in its own container instead once there are enough sizes to need it. */
.size-chart-wrap { overflow-x: auto; margin: 24px 0; }
/* Bug, fixed: 260px tall rendered the SVG's font-size:6/5 labels at
   under 8px on screen - unreadable. Bumped to 300px alongside doubling
   the label font sizes themselves (config/catalog.php) so the increase
   isn't fighting a small render box for room. */
.size-chart-svg { display: block; height: 300px; width: auto; max-width: none; margin: 0 auto; }
.size-chart-baseline { stroke: var(--line); }
.size-chart-rect { fill: var(--panel); stroke: var(--line); }
/* The size being viewed on this page, picked out from the rest - amber
   (this site's one accent colour for "the thing to notice/click")
   rather than a new colour, so it reads as consistent with every other
   selected/active state elsewhere in the app. */
.size-chart-rect--active { fill: var(--amber-tint); stroke: var(--amber-deep); }
.size-chart-human { fill: #4B4B49; }
.size-chart-label-name { font-weight: 700; fill: var(--text); font-family: var(--font-body); }
/* Must come after the base .size-chart-label-name rule above - same
   specificity (one class each), so source order is what decides the
   tie. Defined earlier than its base rule the first time this shipped,
   which silently lost every time (bug, fixed). */
.size-chart-label-name--active { fill: var(--amber-deep); }
.size-chart-label-dims { fill: var(--text-dim); font-family: var(--font-body); }

/* Size dimensions table (poster-size.php, directly under the comparison
   chart above - config/catalog.php's render_size_dimensions_table()).
   Same shape/rules as the price grid below (.price-table) - deliberately
   a separate set of classes rather than reusing that one directly, since
   this table has nothing to do with price and reusing a "price-table"
   class name here would read wrong to anyone editing this file later. */
.dims-table-wrap { overflow-x: auto; margin-top: 20px; }
.dims-table { width: 100%; border-collapse: collapse; }
.dims-table th,
.dims-table td { padding: 10px 16px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.dims-table thead th {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 14px;
  color: var(--text);
  background: var(--panel-2);
}
.dims-table thead th:first-child { border-top-left-radius: 12px; }
.dims-table thead th:last-child { border-top-right-radius: 12px; }
.dims-table tbody td { font-size: 14px; color: var(--text); }
.dims-table tbody tr:hover { background: var(--panel-2); }
.dims-table-size { font-weight: 700; }
/* The size this page is currently showing, picked out the same amber
   way as its matching rectangle in the chart above - same accent, same
   meaning, so the two read as one connected unit. */
.dims-table-row--active { background: var(--amber-tint); }
.dims-table-row--active .dims-table-size { color: var(--amber-deep); }
/* Extra qualifier (tr.) needed, not just repeating the class alone -
   .dims-table tbody tr:hover above has two element selectors
   (tbody/tr) to this rule's zero, so at equal class-count a bare
   .dims-table-row--active:hover would actually lose to it regardless of
   source order (specificity is compared before source order ever comes
   into play). Caught before shipping, not live this time. */
.dims-table tbody tr.dims-table-row--active:hover { background: var(--amber-tint); }

/* Full price grid (prices.php) - a real <table>, not .link-tile-grid
   cards, since this is genuinely two-dimensional data (size × paper)
   that a card grid can't represent. Wrapped in its own overflow-x:auto
   container rather than letting the table shrink/wrap its columns - a
   price grid misreading which column belongs to which paper is worse
   than a horizontal scrollbar on a narrow screen. */
.price-table-wrap { overflow-x: auto; margin-top: 28px; }
.price-table { width: 100%; border-collapse: collapse; }
.price-table th,
.price-table td { padding: 12px 18px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--line); }
.price-table thead th {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 15px;
  color: var(--text);
  background: var(--panel-2);
}
.price-table thead th:first-child { border-top-left-radius: 12px; }
.price-table thead th:last-child { border-top-right-radius: 12px; }
.price-table tbody td { font-size: 14px; color: var(--text); }
.price-table tbody tr:hover { background: var(--panel-2); }
.price-table-size { font-weight: 700; }
.price-table-na { color: var(--text-dim); }

/* Good to know / blog index card grid */
main.blog-index-layout { display: block; max-width: 1080px; }
.blog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; }
.blog-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  background: var(--panel);
  transition: border-color 0.15s ease;
}
.blog-card:hover { border-color: var(--amber-deep); }
.blog-card-thumb { width: 100%; aspect-ratio: 16/10; object-fit: cover; background: var(--panel-2); display: block; }
.blog-card-thumb-placeholder { width: 100%; aspect-ratio: 16/10; background: var(--panel-2); display: flex; align-items: center; justify-content: center; color: var(--text-dim); }
.blog-card-body { padding: 18px 20px 22px; }
.blog-card-title { font-family: var(--font-display); font-weight: 400; font-size: 18px; color: var(--text); margin: 0 0 8px; }
.blog-card-excerpt { color: var(--text-dim); font-size: 14px; line-height: 1.6; margin: 0 0 12px; }
.blog-card-cta { color: var(--amber-deep); font-weight: 700; font-size: 13px; }
