/* =====================================================================
   AF Proofing — shared styles
   Brand tokens mirror the main site (src/styles/global.css) so the
   proofing area feels like one continuous experience.
   ===================================================================== */

/* The `hidden` attribute must always win. Without this, any class that
   sets `display` (flex, inline, grid) out-ranks the browser's default
   `[hidden] { display: none }` and the element keeps rendering — which
   is silent, because the attribute looks correct in the DOM. */
[hidden] { display: none !important; }

:root {
  --bg:          #0b0b0c;
  --bg-2:        #111113;
  --bg-3:        #17171a;

  /* Elevation scale for the admin UI — deep neutral grays that step up
     as surfaces come forward, so panels read as layered rather than flat. */
  --surface-0:   #0e0e10;   /* app canvas behind panels */
  --surface-1:   #16161a;   /* cards, panels */
  --surface-2:   #1d1d22;   /* raised: table headers, hover */
  --surface-3:   #26262c;   /* highest: inputs, active rows */
  --sidebar:     #121216;

  /* Foreground ink as an RGB triplet, so tints can be mixed at any
     alpha and still follow the theme. */
  --ink:         244, 241, 236;
  /* Where the primary button goes on hover: brighter on dark, darker
     on light. A literal #fff here disappears in a light theme. */
  --btn-primary-hover: #ffffff;
  --text:        #f4f1ec;
  --text-muted:  rgba(var(--ink), 0.62);
  --text-faint:  rgba(var(--ink), 0.38);
  --line:        rgba(var(--ink), 0.10);
  --line-strong: rgba(var(--ink), 0.22);
  --accent:      #c9a227;
  --ok:          #7fc8a0;
  --err:         #d98a8a;
  /* Status accents. Pastels read well on the dark canvas and
     wash out on a light one, so light mode deepens them. */
  --status-blue:   #8fb8d8;
  --status-purple: #b9a7e0;
  --status-orange: #e0a56a;
  --font-body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
  --radius: 3px;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
  line-height: 1.6;
}

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; line-height: 1.15; margin: 0; }
a { color: inherit; }
.muted { color: var(--text-muted); }
.center { text-align: center; }

.eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0.4rem 0 0;
}

/* ---- Brand mark ---------------------------------------------------- */
.brand { display: flex; align-items: center; gap: 0.75rem; padding: 1.5rem 1.25rem; }
/* The logo artwork supplies its own circle and ring — no CSS border. */
.brand-mark {
  display: block;
  width: 38px; height: 38px;
  flex: 0 0 auto;
}
.brand-mark.lg { width: 72px; height: 72px; margin: 0 auto; }
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-weight: 500; font-size: 0.92rem; letter-spacing: 0.04em; }
.brand-text em {
  font-style: normal; font-size: 0.68rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-faint);
}

/* ---- Plain (login / error / client) -------------------------------- */
body.plain { display: grid; place-items: center; min-height: 100vh; padding: 2rem 1rem; }
.plain-main { width: 100%; max-width: 420px; }

.auth-card {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 2.5rem 2rem;
}
.auth-brand { text-align: center; margin-bottom: 2rem; }
.auth-brand h2 { font-size: 1.6rem; margin-top: 1rem; }
/* Secondary auth links: "Forgot password?", "Back to sign in". */
.auth-alt { text-align: center; margin: 1.1rem 0 0; font-size: 0.85rem; }
.auth-alt a { color: var(--text-muted); }
.auth-alt a:hover { color: var(--text); }

/* ---- Team pre-registration QR handout ------------------------------- */
.handout-bar { display: flex; gap: 0.75rem; justify-content: center; margin: 1.5rem 0; }
.handout-warn {
  max-width: 540px; margin: 0 auto 1rem; padding: 0.7rem 0.9rem;
  font-size: 0.85rem; color: var(--text-muted); text-align: center;
  background: rgba(201, 162, 39, 0.08); border: 1px solid rgba(201, 162, 39, 0.4);
  border-radius: var(--radius);
}
.handout-sheet {
  max-width: 540px; margin: 0 auto 2rem; padding: 2.75rem 2.5rem;
  text-align: center; background: #fff; color: #141414;
  border-radius: 10px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}
.handout-logo { display: block; max-width: 300px; max-height: 130px; object-fit: contain; margin: 0 auto 1.25rem; }
/* The code leads and the words follow it, so the gap that used to sit under
   the sub-copy now sits under the URL instead — see the view and HandoutPdf,
   which are laid out in the same order. */
.handout-qr { width: 300px; height: 300px; margin: 0 auto 1rem; }
.handout-qr svg { display: block; width: 100%; height: 100%; }
.handout-url { margin: 0 0 1.75rem; font-size: 0.8rem; color: #777; word-break: break-all; }
.handout-team { font-size: 1.5rem; font-weight: 700; }
.handout-heading { font-size: 1.55rem; font-weight: 600; margin: 0.6rem 0 0.5rem; color: #141414; }
.handout-sub { max-width: 380px; margin: 0 auto; font-size: 0.95rem; line-height: 1.5; color: #555; }

/* @page MUST stay at the top level — nested inside @media print, Safari's parser
   chokes on it and silently drops every rule that follows in the block (which
   previously killed the handout print fixes). Top-level @page only affects print. */
@page { margin: 1.2cm; }
@media print {
  .no-print { display: none !important; }
  body { background: #fff !important; }
  /* The plain layout centres with a 100vh grid; when printed, that grid track is
     one page tall and the browser spreads the handout across sheets. Flatten it
     to a normal, top-aligned block flow for print. */
  html, body.plain {
    display: block !important; min-height: 0 !important; height: auto !important; padding: 0 !important;
  }
  .plain-main { max-width: none !important; margin: 0 !important; }
  /* Keep the whole handout together on that one page. */
  .handout-sheet {
    max-width: none; margin: 0 auto; padding: 0; box-shadow: none;
    break-inside: avoid; page-break-inside: avoid;
  }
  .handout-logo { max-height: 96px; margin-bottom: 0.75rem; }
  .handout-url  { margin-bottom: 1rem; }
  .handout-qr   { width: 260px; height: 260px; break-inside: avoid; }
}

/* ---- Forms --------------------------------------------------------- */
.field { margin-bottom: 1.15rem; }
.field label {
  display: block;
  font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-muted); margin-bottom: 0.45rem;
}
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=url], input[type=tel], input[type=search],
input[type=date], input[type=time], input[type=datetime-local], select, textarea {
  width: 100%;
  padding: 0.7rem 0.85rem;
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--line-strong);
}

.btn {
  display: inline-block;
  padding: 0.7rem 1.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 0.78rem;
  /* Stated explicitly: <button> does not inherit line-height from the
     page, so without this an <a class="btn"> renders taller than a
     <button class="btn"> sitting next to it. */
  line-height: 1.15;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.btn:hover { background: rgba(var(--ink), 0.06); border-color: var(--text-faint); }
.btn-primary { width: 100%; background: var(--text); color: var(--bg); border-color: var(--text); }
.btn-primary:hover { background: var(--btn-primary-hover); color: var(--bg); }

/* ---- Flash --------------------------------------------------------- */
.flash {
  padding: 0.75rem 1rem;
  border: 1px solid var(--line);
  border-left-width: 2px;
  border-radius: var(--radius);
  margin-bottom: 1.25rem;
  font-size: 0.9rem;
}
.flash-success { border-left-color: var(--ok);  color: var(--ok); }
.flash-error   { border-left-color: var(--err); color: var(--err); }
.flash-info    { border-left-color: var(--accent); }

/* Admin flashes float as toasts (set up by admin.js) so a "Saved" message
   is visible wherever you're scrolled, now that saves hold your place. */
.flash-toasts {
  position: fixed; top: 1rem; left: 0; right: 0; z-index: 200;
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  padding: 0 1rem; pointer-events: none;
}
.flash-toasts .flash.ft-item {
  margin: 0; max-width: 34rem; pointer-events: auto; cursor: pointer;
  background: var(--surface-1); border-color: var(--line-strong);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.3);
  opacity: 0; transform: translateY(-10px);
  transition: opacity 0.22s ease, transform 0.22s ease;
}
.flash-toasts .flash.ft-item.ft-in  { opacity: 1; transform: none; }
.flash-toasts .flash.ft-item.ft-out { opacity: 0; transform: translateY(-10px); }
@media (prefers-reduced-motion: reduce) {
  .flash-toasts .flash.ft-item { transition: opacity 0.22s ease; transform: none; }
}

/* ---- Admin shell --------------------------------------------------- */
body.admin {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a plain 1fr track refuses to shrink below its
     content's minimum, so one wide table stretched the whole page past the
     window. The page then scrolled sideways and the sidebar slid off to the
     left with it. This lets the column shrink and hands the scrolling to
     the table's own wrapper, where it belongs. */
  grid-template-columns: 232px minmax(0, 1fr);
  min-height: 100vh;
  background: var(--surface-0);
}

.sidebar {
  background: var(--sidebar);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}
.side-nav { display: flex; flex-direction: column; padding: 0.5rem 0; flex: 1; }
.side-nav a {
  padding: 0.65rem 1.5rem;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
  border-left: 2px solid transparent;
}
.side-nav a:hover { color: var(--text); background: rgba(var(--ink), 0.03); }
.side-nav a.is-active { color: var(--text); border-left-color: var(--text); background: rgba(var(--ink), 0.05); }

.side-logout { padding: 1.25rem; border-top: 1px solid var(--line); }
.side-logout button {
  width: 100%; padding: 0.55rem;
  background: transparent; border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--text-muted);
  font-family: inherit; font-size: 0.72rem;
  letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer;
}
.side-logout button:hover { color: var(--text); border-color: var(--line-strong); }

.admin-main { padding: 2rem 2.5rem 4rem; max-width: 1200px; }
.admin-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding-bottom: 1.25rem; margin-bottom: 1.75rem;
  border-bottom: 1px solid var(--line);
}
.admin-head h1 { font-size: 2rem; }
/* Gallery format shown beside the title (e.g. "Portrait", "Team headshots"). */
.page-format {
  margin-left: 0.75rem; vertical-align: middle; white-space: nowrap;
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-muted); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 100px; padding: 0.3rem 0.7rem;
}
.who { font-size: 0.78rem; color: var(--text-faint); }

/* ---- Stat cards ---------------------------------------------------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}
.stat-card {
  background: linear-gradient(180deg, var(--surface-2) 0%, var(--surface-1) 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.35rem 1.25rem;
  display: flex; flex-direction: column; gap: 0.3rem;
  transition: border-color 0.2s ease, background 0.2s ease;
}
.stat-card:hover { border-color: var(--line-strong); }
.stat-card.is-warn {
  border-color: rgba(201, 162, 39, 0.45);
  background: linear-gradient(180deg, rgba(201,162,39,0.09) 0%, var(--surface-1) 100%);
}
.stat-value { font-family: var(--font-display); font-size: 2rem; line-height: 1; }
.stat-label { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted); }
.stat-sub { font-size: 0.74rem; color: var(--text-faint); }
/* Storage usage gauge — dashboard card (single fill) and Settings panel
   (segmented by category). Track = the plan; fill(s) = what is used. */
.storage-bar {
  width: 100%; height: 8px; margin: 0.2rem 0 0.1rem;
  display: flex;
  background: var(--bg-2); border: 1px solid var(--line);
  border-radius: 100px; overflow: hidden;
}
.storage-bar-fill {
  height: 100%; min-width: 2px;
  background: var(--accent); transition: width 0.3s ease;
}
/* Round only the outer ends so several segments read as one continuous bar. */
.storage-bar-fill:first-child { border-radius: 100px 0 0 100px; }
.storage-bar-fill:last-child  { border-radius: 0 100px 100px 0; }
.storage-bar-fill:only-child   { border-radius: 100px; }
/* When the plan is running low the dashboard card goes amber; match the fill.
   The segmented Settings bar keeps its category colours (the "running low"
   text carries the warning there instead). */
.stat-storage.is-warn .storage-bar-fill { background: #d9803a; }

/* ---- Storage page ---------------------------------------------------- */
.stat-card[data-storage-link] { cursor: pointer; }
.stat-more { margin-top: 0.5rem; font-size: 0.8rem; color: var(--accent); text-decoration: none; align-self: flex-start; }
.stat-more:hover { text-decoration: underline; }

.page-title { margin: 0 0 1.25rem; }

.storage-total { margin: 0 0 0.6rem; font-size: 1rem; }
.storage-bar.lg { height: 12px; margin: 0.4rem 0 0; }

.storage-breakdown { list-style: none; margin: 1.2rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.storage-breakdown li { display: grid; grid-template-columns: minmax(0, 1fr) 130px 92px; gap: 0.9rem; align-items: center; }
.sb-label { display: flex; flex-direction: column; min-width: 0; }
.sb-hint { font-size: 0.75rem; color: var(--text-faint); }
.sb-track { height: 8px; background: var(--bg-2); border: 1px solid var(--line); border-radius: 100px; overflow: hidden; }
.sb-fill { display: block; height: 100%; background: var(--accent); }
.sb-size { text-align: right; font-variant-numeric: tabular-nums; }

/* The action that follows the breakdown: it explains where the space went,
   this is where you go to reclaim it. */
.storage-manage {
  display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
  margin: 1.1rem 0 0; padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.storage-manage .hint { margin: 0; }

.sb-total { border-top: 1px solid var(--line); padding-top: 0.65rem; margin-top: 0.15rem; font-weight: 600; }
.sb-total .sb-track { border: 0; background: none; }

.storage-row { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 0.6rem; background: var(--surface-1); }
.storage-row > summary { list-style: none; cursor: pointer; padding: 0.7rem 0.9rem; display: flex; align-items: center; gap: 0.5rem; }
.storage-row > summary::-webkit-details-marker { display: none; }
.storage-row > summary::before { content: "\25B8"; color: var(--text-faint); display: inline-block; transition: transform 0.15s ease; }
.storage-row[open] > summary::before { transform: rotate(90deg); }
.sr-title { font-weight: 600; }
.sr-meta { margin-left: auto; color: var(--text-faint); font-size: 0.85rem; white-space: nowrap; }
.sr-body { padding: 0 0.9rem 0.85rem; border-top: 1px solid var(--line); }
.sr-files { list-style: none; margin: 0.6rem 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.sr-files li { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 0.8rem; align-items: center; font-size: 0.85rem; }
.srf-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A gallery with nobody under it gets the same row treatment minus the
   disclosure triangle — it should sit in the same rhythm as the groups
   around it, just without pretending there is something to open. */
.storage-row.is-flat {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.7rem 0.9rem;
}
/* The format, so "BEA Headshots" reads as a station and not a one-off. */
.sr-tag {
  margin-left: 0.5rem; font-weight: 400;
  font-size: 0.68rem; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--text-faint);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 0.1rem 0.45rem; white-space: nowrap;
}
.storage-row .sr-title a { color: inherit; }
.storage-row.is-flat .btn, .sr-files .btn { flex: none; }

.srf-size { color: var(--text-faint); font-variant-numeric: tabular-nums; }
.sr-files form { margin: 0; }
.sr-delete-all { margin: 0.6rem 0 0; }
.sr-delete-all button { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }
.sr-delete-all button:hover { background: color-mix(in srgb, var(--err) 12%, transparent); }

.storage-galleries { width: 100%; border-collapse: collapse; }
.storage-galleries th, .storage-galleries td { padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--line); text-align: left; }
.storage-galleries th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.storage-galleries .num { text-align: right; font-variant-numeric: tabular-nums; }
.storage-galleries tr:last-child td { border-bottom: 0; }

/* The company's own logo, leading their signup sheet. Capped so a large
   upload can't dwarf the form, and centred on a phone where the panel is
   the whole screen. */
/* The company's logo leads their sign-up sheet, centred at every width — this
   page is their front door, not a panel in the studio's layout. */
/* A client brokerage's mark in the gallery header, sized like the studio
   lockup it stands in for. */
.client-brand-img { display: block; max-height: 44px; width: auto; max-width: 190px; }

.station-signup-logo {
  display: block; max-width: 220px; max-height: 96px;
  width: auto; height: auto; object-fit: contain;
  margin: 0 auto 1.6rem;
}

/* Sign-up form: a stranger fills this in on a phone, standing in a lobby, so
   the fields get room to breathe rather than sitting flush against each other.
   (.gate-fields had no rule at all, which is why the block read as one lump.) */
.gate-fields {
  display: grid;
  gap: 0.7rem;
  margin: 1.15rem 0 1.35rem;
}
.gate-fields input { width: 100%; }
/* Sponsor credit on a sign-up page. Quiet and set apart — it is an
   acknowledgement, not an instruction, and shouldn't compete with the form. */
.sponsor-credit {
  margin: 1.1rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--text-faint);
}
/* Scoped to the station's own form: the pre-registration and sign-in forms on
   this same page keep their existing proportions. */
.station-signup-form .gate-note { margin: 0.2rem 0 0; }
.station-signup-form > .btn-primary { width: 100%; padding-block: 0.85rem; }

/* ---- Headshot Station queue ------------------------------------------ */
/* Sub-header for the roster, which folds independently of the panel around it.
   Quiet on purpose — the panel head above already carries the title and the
   waiting/done tally, so this only needs to be a handle. */
.station-list-head {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0 0 0.7rem; padding: 0.4rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.72rem; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--text-faint);
  cursor: pointer; user-select: none;
}
.station-list-head:hover { color: var(--text-muted); }
.station-list-head .count {
  font-family: var(--font-display); font-size: 0.95rem; letter-spacing: 0;
  color: var(--text-muted);
}

.station-panel { margin-bottom: 1.5rem; }
.station-tally { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.st-chip {
  font-size: 0.72rem; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 0.2rem 0.55rem; border-radius: 999px; border: 1px solid var(--line);
  color: var(--text-faint); white-space: nowrap;
}
.st-chip.st-done { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.st-chip.st-noshow { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }

.station-link {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.7rem 0.9rem; margin: 0 0 1rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
}
.station-link .link-value { flex: 1 1 260px; min-width: 0; overflow-x: auto; white-space: nowrap; }
.station-empty { margin: 0.5rem 0 1rem; }

.station-table { width: 100%; border-collapse: collapse; }
.station-table th, .station-table td { padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; }
.station-table th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint); }
.station-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.station-table .st-num { font-variant-numeric: tabular-nums; color: var(--text-faint); }
.station-table .sub { display: block; font-size: 0.78rem; color: var(--text-faint); }
.station-table tr:last-child td { border-bottom: 0; }
.station-row.is-done { opacity: 0.72; }
.station-row.is-no_show { opacity: 0.5; }

.station-badge { font-size: 0.72rem; padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--line); color: var(--text-faint); }
.station-badge.is-shooting { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
.station-badge.is-done { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 50%, transparent); }
.station-badge.is-no_show { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }

.station-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: flex-end; }
.station-actions form { margin: 0; }

.station-tools { margin-top: 1rem; }
.station-tools > summary { cursor: pointer; color: var(--text-faint); font-size: 0.85rem; padding: 0.35rem 0; }
.station-move { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin: 0.6rem 0; }
.station-move-arrow { color: var(--text-faint); }
.station-purge { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0.6rem 0 0; }
.station-purge .hint { margin: 0; }

.station-add {
  display: flex; align-items: flex-end; gap: 0.7rem; flex-wrap: wrap;
  margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid var(--line);
}
.station-add .field { margin-bottom: 0; flex: 1 1 190px; }

/* Category colours — shared by a bar segment and its breakdown dot. */
.sb-seg-gallery  { background: var(--accent); }
.sb-seg-delivery { background: #3f9d8b; }
.sb-seg-cache    { background: #7f8794; }
.sb-seg-branding { background: #b7a98c; }

/* ---- Settings storage panel ---------------------------------------- */
.storage-panel {
  max-width: 460px; margin: 0.35rem 0 0.25rem;
  padding: 0.95rem 1.05rem;
  background: var(--surface-0); border: 1px solid var(--line); border-radius: 10px;
}
.storage-meta {
  display: flex; justify-content: space-between; gap: 1rem;
  font-size: 0.82rem; color: var(--text-muted); margin-bottom: 0.5rem;
}
.storage-meta strong { color: var(--text); font-weight: 600; }
.storage-free.is-warn { color: #d9803a; }
.storage-breakdown { list-style: none; margin: 0.9rem 0 0; padding: 0; }
.storage-breakdown li {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.5rem 0; border-top: 1px solid var(--line);
}
.storage-breakdown li:first-child { border-top: 0; }
.sb-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; transform: translateY(1px); }
.sb-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.sb-label { font-size: 0.9rem; color: var(--text); }
.sb-blurb { font-size: 0.76rem; color: var(--text-faint); }
.sb-bytes { flex: none; font-variant-numeric: tabular-nums; font-size: 0.9rem; color: var(--text-muted); }
.text-faint { color: var(--text-faint); }

/* ---- Digital delivery: method toggle + master files ---------------- */
.dm-choice { display: grid; gap: 0.6rem; grid-template-columns: 1fr 1fr; }
@media (max-width: 560px) { .dm-choice { grid-template-columns: 1fr; } }
.dm-opt {
  display: flex; gap: 0.55rem; align-items: flex-start;
  padding: 0.7rem 0.8rem; border: 1px solid var(--line); border-radius: 8px;
  cursor: pointer; background: var(--surface-0);
}
.dm-opt:has(input:checked) { border-color: var(--ok); background: rgba(127, 200, 160, 0.08); }
.dm-opt input { margin-top: 0.2rem; flex: none; accent-color: var(--ok); }

/* Approval master-delivery card (gallery / team settings) */
.approval-delivery { margin-top: 1.25rem; }
.approval-delivery legend { font-size: 0.9rem; font-weight: 600; padding: 0 0.4rem; }
.approval-delivery .dm-choice { margin-top: 0.7rem; }
.approval-delivery .ad-url { margin-top: 0.7rem; }

/* High-res finals uploader: per-file green (accepted) / red (rejected) list. */
.up-results { list-style: none; margin: 0.8rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.82rem; }
.up-results li { padding: 0.32rem 0.6rem; border-radius: 6px; line-height: 1.4; }
.up-ok  { color: var(--ok); background: rgba(127, 200, 160, 0.10); }
.up-bad { color: #d98a63; background: rgba(217, 138, 99, 0.13); font-weight: 500; }
.up-results [data-upload-reload] { margin-top: 0.45rem; align-self: flex-start; }
.dm-text { display: flex; flex-direction: column; gap: 0.1rem; }
.dm-title { font-size: 0.92rem; color: var(--text); }
.dm-sub { font-size: 0.76rem; color: var(--text-faint); line-height: 1.4; }

/* Show only the sub-panel for the chosen method (JS flips data-method). */
.delivery-panel[data-method="link"]   [data-dm-panel="server"] { display: none; }
.delivery-panel[data-method="server"] [data-dm-panel="link"]   { display: none; }

.check-inline { display: flex; gap: 0.5rem; align-items: flex-start; font-size: 0.88rem; color: var(--text-muted); }
.check-inline input { margin-top: 0.15rem; flex: none; }

.master-list { list-style: none; margin: 0.6rem 0 0; padding: 0; border: 1px solid var(--line); border-radius: 8px; }
.master-list li { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.7rem; border-top: 1px solid var(--line); }
.master-list li:first-child { border-top: 0; }
.ml-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; }
.ml-size { flex: none; font-variant-numeric: tabular-nums; font-size: 0.82rem; color: var(--text-muted); }
.ml-del { flex: none; margin: 0; }
.master-summary { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-top: 0.6rem; font-size: 0.85rem; color: var(--text-muted); }
.master-summary form { margin: 0; }
.master-link { margin-top: 0.7rem; word-break: break-all; }
.master-up-status { font-size: 0.82rem; color: var(--text-muted); }
/* File picker + Upload button share one row instead of stacking. */
.mu-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.mu-row input[type="file"] { flex: 1 1 12rem; min-width: 0; font-size: 0.85rem; }
.mu-row .btn { flex: none; }
/* Set the upload area + file list apart from the settings form above. */
.master-up-form { margin-top: 1rem; padding-top: 1.1rem; border-top: 1px solid var(--line); }
.master-files { margin-top: 1rem; padding-top: 1.1rem; border-top: 1px solid var(--line); }

/* ---- Client master download page ---------------------------------- */
.dl-lead { color: var(--text-muted); margin-bottom: 1rem; }
.dl-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.1rem; }
.dl-expiry { font-size: 0.85rem; color: var(--text-faint); }
.dl-files { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 8px; }
.dl-files li { display: flex; align-items: center; gap: 0.9rem; padding: 0.65rem 0.85rem; border-top: 1px solid var(--line); }
.dl-files li:first-child { border-top: 0; }
.dl-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dl-size { flex: none; font-variant-numeric: tabular-nums; font-size: 0.85rem; color: var(--text-muted); }
.dl-foot { margin-top: 1rem; font-size: 0.82rem; color: var(--text-faint); }

/* ---- Panels -------------------------------------------------------- */
.panel {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.75rem;
  margin-bottom: 1.5rem;
}
.panel h2 { font-size: 1.3rem; margin-bottom: 0.6rem; }

/* ---- Responsive ---------------------------------------------------- */
@media (max-width: 820px) {
  body.admin { grid-template-columns: 1fr; }
  .sidebar { position: static; height: auto; flex-direction: row; flex-wrap: wrap; align-items: center; }
  .side-nav { flex-direction: row; flex-wrap: wrap; padding: 0 0.5rem 0.5rem; }
  .side-nav a { border-left: none; border-bottom: 2px solid transparent; padding: 0.5rem 0.75rem; }
  .side-nav a.is-active { border-left: none; border-bottom-color: var(--text); }
  .side-logout { border-top: none; padding: 0.5rem 1rem; margin-left: auto; }
  .admin-main { padding: 1.5rem 1.25rem 3rem; }
}

/* ---- Toolbar / filters --------------------------------------------- */
.toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem;
}
.toolbar-right { display: flex; align-items: center; gap: 0.75rem; }
.filters { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.filters a {
  font-size: 0.76rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint); text-decoration: none; padding-bottom: 0.2rem;
  border-bottom: 1px solid transparent;
}
.filters a:hover { color: var(--text-muted); }
.filters a.is-active { color: var(--text); border-bottom-color: var(--text); }
/* When the toolbar carries the far-right editor tab, let the filter nav
   fill the bar so that tab can sit hard against the right edge. */
.filters:has(a.is-right) { flex: 1 1 auto; }
.filters a.is-right { margin-left: auto; }

/* Gallery list: the "Filter View" board — a thin bordered panel holding the
   three combinable categories (Status / Format / Date). Each row is a category;
   the chips within are its sub-options, and a divider separates each category. */
.filter-board { padding: 0.75rem 1.1rem 0.9rem; margin-bottom: 1.5rem; }
.fb-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
.fb-head[data-fb-toggle] { cursor: pointer; user-select: none; }
.fb-caret { display: inline-flex; color: var(--text-muted); transition: transform 0.15s ease; }
.fb-head.is-collapsed .fb-caret { transform: rotate(-90deg); }
.fb-head.is-collapsed { margin-bottom: 0; }
.fb-title { margin: 0; font-size: 0.9rem; letter-spacing: 0.02em; }
.fb-reset { margin-left: auto; }
.fb-reset.is-disabled { opacity: 0.4; pointer-events: none; }

.gallery-filters { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.gf-cat {
  display: flex; align-items: baseline; gap: 1rem;
  padding: 0.55rem 0; border-top: 1px solid var(--line);
}
.gf-cat:first-child { border-top: 0; }
/* Category heading — visually distinct from the chips (its sub-options). */
.gf-label {
  flex: 0 0 8rem; font-size: 0.72rem; letter-spacing: 0.1em;
  text-transform: uppercase; font-weight: 600; color: var(--text);
}
.gf-cat .filters { gap: 0.5rem 1rem; }
@media (max-width: 640px) {
  .gf-cat { flex-direction: column; gap: 0.35rem; }
  .gf-label { flex-basis: auto; }
}

/* ---- Data table ----------------------------------------------------- */
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
table.data { width: 100%; border-collapse: collapse; font-size: 0.9rem; min-width: 700px; }
table.data th {
  text-align: left; padding: 0.85rem 1rem;
  font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint); font-weight: 400; border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
table.data td { padding: 0.9rem 1rem; border-bottom: 1px solid var(--line); vertical-align: top; }
table.data tr:last-child td { border-bottom: none; }
table.data tbody tr:nth-child(even) td { background: rgba(var(--ink), 0.014); }
table.data tbody tr:hover td { background: var(--surface-2); }
/* Rows that navigate on click (the whole row is a link to the record). */
table.data tr[data-href] { cursor: pointer; }
table.data .num { text-align: right; font-variant-numeric: tabular-nums; }
/* A $0 "included retouching" order reads as credited, not a zero-dollar sale. */
.money-included { font-size: 0.78rem; font-style: italic; color: var(--text-muted); }
td .strong { display: block; color: var(--text); text-decoration: none; font-weight: 500; }
td .strong:hover { text-decoration: underline; }
td .sub { display: block; font-size: 0.76rem; color: var(--text-faint); margin-top: 0.15rem; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; }
.row-actions a { font-size: 0.78rem; color: var(--text-muted); }
.text-err { color: var(--err); }

/* Folder-restore status: progress stays a quiet inline hint, but an error
   becomes a full-width red box so it can't be missed. */
.restore-folder-status.is-error {
  display: block;
  margin-top: 0.55rem;
  padding: 0.6rem 0.75rem;
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.45;
  color: var(--err);
  background: rgba(217, 138, 138, 0.1);
  border: 1px solid rgba(217, 138, 138, 0.4);
  border-radius: 8px;
}

/* ---- Badges --------------------------------------------------------- */
.badge {
  display: inline-block; padding: 0.2rem 0.6rem;
  font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase;
  border: 1px solid var(--line-strong); border-radius: 100px; white-space: nowrap;
}
.badge-live             { color: var(--ok);  border-color: rgba(127,200,160,0.5); }
.badge-pre_registration { color: var(--accent); border-color: rgba(201,162,39,0.5); }
.badge-draft            { color: var(--text-faint); }
.badge-expired,
.badge-archived         { color: var(--err); border-color: rgba(217,138,138,0.45); }
/* A badge carrying an explicit colour (a custom status, or a recoloured
   built-in) hands its hex to CSS as --badge-c. Outline here in dark; admin
   light mode turns every status badge into a solid pill (see is-light). The
   two-class selector wins over the single-class built-in colours above. */
.badge.badge-c { color: var(--badge-c); border-color: color-mix(in srgb, var(--badge-c) 55%, transparent); }

/* ---- Panels / forms -------------------------------------------------- */
.panel-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1.25rem; }
.panel-head h2 { font-size: 1.25rem; }
.panel-sub { margin: 0; font-size: 0.82rem; color: var(--text-faint); }
.count { font-size: 0.8rem; color: var(--text-faint); margin-left: 0.4rem; }
.empty { text-align: center; padding: 3rem 1.5rem; }
.empty-note { margin-top: 1.5rem; }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 700px) { .field-row { grid-template-columns: 1fr; } }
/* Large enough to scan down the form and see what is still required. */
.req { color: var(--accent); font-size: 1.15rem; line-height: 0; vertical-align: -0.1em; }
/* Sits inside an uppercase label, so it has to opt out of that styling
   or it reads as part of the field name. */
.opt {
  text-transform: none; letter-spacing: 0; font-size: 0.72rem;
  color: var(--text-faint); font-style: italic; margin-left: 0.35rem;
}
.hint { font-size: 0.74rem; color: var(--text-faint); margin: 0.35rem 0 0; }
.select-wrap select { appearance: none; padding-right: 2rem; }
/* The notes end the scroll; the button below is always on screen. */
/* Sits in the header, under the photo name — no rule above it, because
   the header already reads as one block. */
/* The size list above this scrolls. Without an opaque edge the last row bleeds
   through the note as you scroll, which is what made the panel look broken. */
.os-notes {
  margin-top: 0; padding-top: 0.7rem;
  /* #fff, not a token: .os-inner sets its own white ground, and the admin
     surface tokens would paint a dark strip across a light sheet. */
  border-top: 1px solid var(--line); background: #fff;
}
/* The framing tick and the button that finishes the order, on one line. */
.os-foot {
  margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem;
}
.os-foot .osv-save { margin: 0; flex: 1 1 14rem; }
.os-foot .btn.inline { flex: 0 0 auto; }
.os-foot .os-status { flex-basis: 100%; margin: 0; }
@media (max-width: 640px) {
  .os-foot { flex-direction: column; align-items: stretch; }
}
/* Beats .btn.inline, which is declared later. Auto width now that the framing
   tick shares the row; full width again once they stack on a phone. */
.os-foot .btn.inline { width: auto; min-width: 12rem; }
@media (max-width: 640px) { .os-foot .btn.inline { width: 100%; } }
/* Reassurance before the caveat: none of the cropping is required. */
.osv-optional {
  margin: 0; font-size: 0.85rem; line-height: 1.5; color: var(--text-muted);
}

/* Crop-in control under the print preview. */
.osv-zoom {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  margin: 0.75rem 0 0;
}
.osv-zoom-label {
  font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
}
.osv-zoom input[type="range"] { width: min(220px, 42vw); accent-color: var(--text); }
.osv-zoom-btn {
  width: 24px; height: 24px; line-height: 1; cursor: pointer;
  font-family: inherit; font-size: 0.95rem;
  color: var(--text-muted); background: transparent;
  border: 1px solid var(--line); border-radius: 50%;
}
.osv-zoom-btn:hover { color: var(--text); border-color: var(--line-strong); }
.osv-zoom-value {
  min-width: 2.6rem; font-size: 0.72rem; color: var(--text-faint); text-align: right;
}
/* The frame clips, so scaling the photo inside it crops in. The origin
   is set from the drag position in JS, not fixed here. */

/* Portrait / landscape switch in the print crop preview. */
.osv-orient {
  display: flex; align-items: center; justify-content: center; gap: 0.7rem;
  margin: 0.75rem 0 0;
}
.osv-orient-label {
  font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
}
.osv-orient-toggle { display: inline-flex; border: 1px solid var(--line); border-radius: 100px; overflow: hidden; }
.osv-orient-toggle button {
  padding: 0.32rem 0.8rem;
  font-family: inherit; font-size: 0.68rem; letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-muted); background: transparent; border: 0;
}
.osv-orient-toggle button:hover { color: var(--text); }
.osv-orient-toggle button.is-on { color: var(--bg); background: var(--text); }

/* Password field with a show/hide control tucked inside it. */
.pw-field { position: relative; display: block; flex: 1; }
.pw-field input { width: 100%; padding-right: 4.2rem; }
.pw-reveal {
  position: absolute; top: 50%; right: 0.55rem; transform: translateY(-50%);
  padding: 0.25rem 0.45rem;
  font-family: inherit; font-size: 0.68rem; letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-faint); background: transparent;
  border: 1px solid var(--line); border-radius: 4px;
}
.pw-reveal:hover { color: var(--text); border-color: var(--line-strong); }

/* Submission errors sit with the fields they refer to, not at the top of
   a page the client may have scrolled away from. */
.sel-errors {
  margin: 0 0 0.85rem; padding: 0.7rem 0.9rem;
  border: 1px solid rgba(217, 138, 138, 0.5);
  border-left-width: 3px;
  border-radius: 6px;
  background: rgba(217, 138, 138, 0.07);
}
.sel-errors p { margin: 0; font-size: 0.85rem; color: var(--err); }
.sel-errors p + p { margin-top: 0.35rem; }

/* ---- Client: compare selections side by side -------------------------- */
/* Pinned to the top of the scrolling selections panel. The panel's own
   padding scrolls away above it, so an opaque background is what keeps
   photos from showing through as they pass behind. One sticky wrapper
   holds every pinned control: separate sticky children would all stop at
   top:0 and overlap each other. */
.cmp-sentinel { height: 1px; margin-bottom: -1px; }
.sel-sticky {
  position: sticky; top: 0; z-index: 3;
  background: #fff;
  padding-bottom: 1rem;
  transition: box-shadow 0.18s ease;
  /* Compare bar and the "remove unlocked" button share one row — tidy pinned
     left (its own order:-1), Compare to the right when both are present. */
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.6rem 0.75rem; flex-wrap: wrap;
}
.sel-sticky[hidden] { display: none; }
/* Only once it is actually holding position — a shadow at rest would read
   as a raised card rather than a pinned toolbar. */
.sel-sticky.is-stuck { box-shadow: 0 6px 18px rgba(26, 26, 28, 0.12); }
.cmp-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin: 0; padding: 0.7rem 0.95rem;
  border: 1px solid var(--line-strong); border-radius: 8px;
}
.cmp-count { font-size: 0.85rem; color: var(--text-muted); }
.cmp-count strong { font-family: var(--font-display); font-size: 1.05rem; color: var(--text); }
.cmp-max { color: var(--text-faint); font-size: 0.78rem; }
.cmp-actions { display: flex; align-items: center; gap: 0.9rem; }
.cmp-actions .btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* Per-item marker in the selections grid. */
.sel-item { position: relative; }
/* Compare and Lock share the row under each thumbnail, equal width so
   neither reads as the primary of the two. */
.sel-actions { display: flex; gap: 0.35rem; margin-top: 0.35rem; }

/* A retouch note under each selected photo. */
.sel-note { margin-top: 0.35rem; display: flex; flex-direction: column; gap: 0.15rem; }
.sel-note-field {
  width: 100%; resize: vertical; min-height: 2.4rem;
  font: inherit; font-size: 0.8rem; line-height: 1.35;
  padding: 0.4rem 0.5rem;
  color: var(--text); background: var(--surface-3);
  border: 1px solid var(--line); border-radius: 8px;
}
.sel-note-field::placeholder { color: var(--text-faint); }
.sel-note-field:focus { outline: none; border-color: var(--line-strong); }
.sel-note-status {
  min-height: 0.9rem; font-size: 0.68rem; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ok);
}
.sel-cmp,
.sel-lock {
  flex: 1 1 0; min-width: 0;
  display: block; padding: 0.3rem 0.4rem;
  font-family: inherit; font-size: 0.66rem; letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-faint); background: transparent;
  border: 1px solid var(--line); border-radius: 4px;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
/* Green outline to offer, green fill once marked — the same outline-then-
   fill grammar the lock uses in red, so the pair reads as one system. */
.sel-cmp { color: var(--ok); border-color: var(--ok); }

/* Paid add-ons shown as a bar overlaid on the review thumbnail's bottom edge —
   matching the Approve / Changes bar on the approval tiles: semi-transparent
   dark by default, filling green when active. Bottom corners follow the image. */
.sel-imgwrap { position: relative; }
.sel-retouch, .sel-extra {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center; gap: 0.3rem;
  white-space: nowrap; overflow: hidden;   /* one line, so both bars match height */
  border: 0; padding: 0.5rem 0.4rem; color: #fff;
  font-size: 0.68rem; font-weight: 600; letter-spacing: 0.01em; line-height: 1.2;
  background: rgba(11, 11, 12, 0.62); backdrop-filter: blur(4px);
  border-bottom-left-radius: calc(var(--radius-img) - 2px);
  border-bottom-right-radius: calc(var(--radius-img) - 2px);
  transition: background 0.15s ease, color 0.15s ease;
}
.sel-retouch .sr-price, .sel-extra .sr-price { font-weight: 500; opacity: 0.85; }
/* Retouch is a toggle: dark by default, green once the pick is marked. */
.sel-retouch { cursor: pointer; }
.sel-retouch:hover { background: rgba(11, 11, 12, 0.82); }
.sel-item.is-retouch .sel-retouch { background: var(--ok); color: #fff; }
.sel-item.is-retouch .sel-retouch .sr-price { opacity: 0.95; }
/* The additional-headshot label is automatic — always green (a paid extra). */
.sel-extra { background: var(--ok); color: #fff; }
.sel-extra .sr-price { opacity: 0.95; }
.sel-cmp:hover { color: #276a47; border-color: #276a47; background: rgba(47, 125, 85, 0.07); }
/* Unlocked reads in red outline, locked fills that same red in. The lock
   is the one control here that guards against losing a photo, so it says
   so before it is used, not only after. */
.sel-lock { color: var(--err); border-color: var(--err); }
.sel-lock:hover { color: #96382f; border-color: #96382f; background: rgba(176, 71, 63, 0.06); }
.sel-item.is-comparing .sel-cmp {
  color: #fff; font-weight: 600;
  background: var(--ok); border-color: var(--ok);
}
.sel-item.is-comparing .sel-cmp:hover {
  color: #fff; background: #276a47; border-color: #276a47;
}
.sel-item.is-comparing img { outline: 2px solid var(--text); outline-offset: 2px; }

/* Red is this app's "stop" colour, which is exactly what a locked photo
   is saying. It also keeps the locked state from being confused with the
   comparing chip beside it, which is filled dark. Only the locked state
   is coloured — red on every unlocked thumbnail would read as an error
   across the whole panel. */
.sel-lock.is-on {
  color: #fff; font-weight: 600;
  background: var(--err); border-color: var(--err);
}
/* Darkens rather than lightens on hover, so the label keeps its contrast. */
.sel-lock.is-on:hover {
  color: #fff;
  background: #96382f; border-color: #96382f;
}
/* The remove control stays visible so it is clear what the lock protects,
   but it is plainly out of action. */
.sel-item.is-locked .sel-drop {
  opacity: 0.3; cursor: not-allowed;
}
.sel-item.is-locked .sel-drop:hover { background: rgba(255, 255, 255, 0.9); color: #1a1a1c; }

/* The comparison itself: as much room as the screen allows. */
.cmp-view {
  position: fixed; inset: 0; z-index: 1200;
  display: flex; flex-direction: column;
  background: var(--bg);
}
.cmp-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.9rem 1.25rem;
  border-bottom: 1px solid var(--line);
}
.cmp-title {
  font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint);
}
.cmp-stage {
  flex: 1; min-height: 0;
  display: grid; gap: 0.75rem;
  padding: 1rem 1.25rem 1.5rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cmp-stage[data-count="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cmp-item {
  display: flex; flex-direction: column; min-height: 0; margin: 0; gap: 0.5rem;
}
.cmp-item img {
  flex: 1; min-height: 0; width: 100%;
  object-fit: contain;          /* never crop — the framing is the point */
  border-radius: 4px;
}
.cmp-item figcaption {
  flex: none; text-align: center;
  font-size: 0.72rem; letter-spacing: 0.08em; color: var(--text-faint);
}
/* Lock a keeper without leaving the comparison. Matches the button in the
   selections panel so the same action looks the same in both places. */
.cmp-lock {
  flex: none; align-self: center;
  padding: 0.35rem 1.1rem;
  font-family: inherit; font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-faint); background: transparent;
  border: 1px solid var(--line); border-radius: 4px;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
/* Matches the panel's lock: red outline unlocked, red fill locked. */
.cmp-lock { color: var(--err); border-color: var(--err); }
.cmp-lock:hover { color: #96382f; border-color: #96382f; background: rgba(176, 71, 63, 0.06); }
.cmp-lock.is-on {
  color: #fff; font-weight: 600;
  background: var(--err); border-color: var(--err);
}
.cmp-lock.is-on:hover { color: #fff; background: #96382f; border-color: #96382f; }
/* Stacking three across a phone leaves slivers; go two-up, then one. */
@media (max-width: 820px) { .cmp-stage[data-count="3"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) {
  .cmp-stage, .cmp-stage[data-count="3"] { grid-template-columns: 1fr; overflow-y: auto; }
  .cmp-item img { min-height: 55vh; }
}

/* ---- Client: sub-gallery filter bar ----------------------------------- */
.sec-bar {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  margin: 0 0 1.5rem; padding-bottom: 0.15rem;
}
.sec-tab {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.5rem 0.95rem;
  font-family: inherit; font-size: 0.72rem; letter-spacing: 0.12em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 100px;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.sec-tab:hover { border-color: var(--line-strong); color: var(--text); }
.sec-tab .sec-n { font-size: 0.68rem; color: var(--text-faint); }
.sec-tab.is-on {
  color: var(--bg); background: var(--text); border-color: var(--text);
}
.sec-tab.is-on .sec-n { color: var(--bg); opacity: 0.65; }

/* ---- Admin: sub-gallery management ------------------------------------ */
.sections-block {
  margin-bottom: 1.5rem; padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--line);
}
.sections-head { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.75rem; }
.sections-head h3 {
  margin: 0; font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-muted); font-family: var(--font-body); font-weight: 500;
}
.muted.sm { font-size: 0.75rem; color: var(--text-faint); }

.section-list { list-style: none; margin: 0 0 0.85rem; padding: 0; }
.section-list li {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.4rem 0; border-top: 1px solid var(--line);
  background: transparent;
}
.section-list li:first-child { border-top: 0; }

/* Drag to reorder. The grip is the only drag surface so the name field
   inside the row stays usable. */
.section-grip {
  flex: none; cursor: grab; user-select: none;
  color: var(--text-faint); font-size: 0.95rem; line-height: 1;
  padding: 0.2rem 0.1rem;
}
.section-grip:active { cursor: grabbing; }
.section-list li:hover .section-grip { color: var(--text-muted); }
.section-list.is-sorting li { transition: transform 0.12s ease; }
/* The Approve sub-gallery: shown, but not editable — the phase toggle owns it,
   and renaming or removing it would empty the client's approve view. */
.section-row-locked { display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; }
.section-locked-name { color: var(--text-muted); }
.section-row-locked .hint { margin: 0; }
.section-list li.is-dragging {
  opacity: 0.55;
  background: rgba(var(--ink), 0.04);
  border-radius: 6px;
}
.section-list.is-saving { opacity: 0.6; pointer-events: none; }
.section-row { display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; }
.section-row input { flex: 1; min-width: 0; padding: 0.45rem 0.65rem; font-size: 0.85rem; }
.section-count {
  flex: none; min-width: 2rem; text-align: center; font-size: 0.72rem;
  color: var(--text-faint);
}
/* Quiet until the name is edited, then amber — the same "there is work
   here" signal used everywhere else. */
.section-save { flex: none; }
.section-save.is-dirty {
  color: var(--accent); border-color: var(--accent);
  background: rgba(201, 162, 39, 0.10);
}
.section-save.is-dirty:hover { background: rgba(201, 162, 39, 0.18); }
.section-save:disabled { opacity: 0.4; cursor: not-allowed; }
.section-tools { display: flex; align-items: center; gap: 0.7rem; flex: none; }
.section-tools form { display: inline; }

.section-add { display: flex; gap: 0.6rem; }
.section-add input { flex: 1; padding: 0.55rem 0.75rem; font-size: 0.85rem; }

/* ---- Gallery lifecycle stages ----------------------------------------
   A gallery moves Create & manage -> Launch -> Archive -> Delete, and the
   page is ordered to match so the next step is always the one below. */
.stage { margin-bottom: 2.75rem; }
.stage:last-child { margin-bottom: 1rem; }
.stage-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.1rem; }

/* Collapsible section header (settings section #1 on gallery edit + team hub):
   the header clicks to fold its body away. A caret on the right shows the state;
   admin.js persists the choice. */
.stage-head.is-collapsible, .panel-head.is-collapsible { cursor: pointer; user-select: none; }
.collapse-caret {
  margin-left: auto; align-self: center; flex: none;
  width: 0; height: 0;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 6px solid var(--text-muted);
  transition: transform 0.15s ease;
}
.is-collapsed .collapse-caret { transform: rotate(-90deg); }
.is-collapsible:hover .collapse-caret { border-top-color: var(--text); }
.stage-head h2 {
  margin: 0; font-size: 1.15rem; font-family: var(--font-body);
  font-weight: 500; letter-spacing: 0.02em; color: var(--text);
}
.stage-sub { margin: 0.2rem 0 0; font-size: 0.8rem; color: var(--text-faint); }
/* The step number carries the sequence, so it reads first — big enough to
   scan down the page without reading a word. */
.stage-n {
  flex: none; display: grid; place-items: center;
  width: 42px; height: 42px; border-radius: 50%;
  font-family: var(--font-display); font-size: 1.35rem;
  /* Playfair defaults to old-style figures: 3 and 4 drop below the
     baseline while 1 and 2 don't, so no single nudge can centre them all.
     Lining figures make every digit the same height, which is what a
     circle needs. The padding then corrects the whole set at once. */
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum" 1;
  line-height: 1; padding-bottom: 5px;
  color: var(--accent);
  background: rgba(201, 162, 39, 0.10);
  border: 1px solid rgba(201, 162, 39, 0.45);
}

/* Steps 1 and 2 report state: amber while outstanding, green once done.
   Archive and Delete are tools rather than steps, so they keep a fixed
   colour — blue for the safe one, red for the irreversible one. */
.stage-n.is-todo {
  color: var(--accent);
  background: rgba(201, 162, 39, 0.12);
  border-color: rgba(201, 162, 39, 0.55);
}
.stage-n.is-done {
  color: var(--ok);
  background: rgba(127, 200, 160, 0.12);
  border-color: rgba(127, 200, 160, 0.55);
}
.stage-n.is-archive {
  color: #7aa7d9;
  background: rgba(122, 167, 217, 0.12);
  border-color: rgba(122, 167, 217, 0.55);
}
.stage-n.is-delete {
  color: var(--err);
  background: rgba(217, 138, 138, 0.12);
  border-color: rgba(217, 138, 138, 0.55);
}
.stage-danger .stage-head h2 { color: var(--err); }
/* Amber prompts on the tabs during setup — what this tab still wants. */
.tab-todo {
  display: inline-block; margin-left: 0.45rem;
  padding: 0.12rem 0.45rem; border-radius: 100px;
  font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: #1a1a1c; background: var(--accent);
  vertical-align: middle;
}
/* Neutral flag — an optional step, not an outstanding one (e.g. photos while
   a gallery is only collecting pre-registrations). Muted, never amber. */
.tab-opt {
  display: inline-block; margin-left: 0.45rem;
  padding: 0.12rem 0.45rem; border-radius: 100px;
  font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint); background: rgba(var(--ink), 0.08);
  vertical-align: middle;
}

/* The Create button sits below both tabs: greyed while anything is
   outstanding, green once both are done. Still clickable throughout —
   a gallery you fill in later is perfectly legitimate. */
.create-actions { border-top: 1px solid var(--line); margin-top: 1.5rem; }
/* The duplicate action bar at the top of the create page: divider below it
   instead of above, and no big top gap since it sits right under the tabs. */
.create-actions-top { margin-top: 0.9rem; padding-top: 0; padding-bottom: 1.1rem; border-top: none; border-bottom: 1px solid var(--line); }

/* Compact "new-gallery defaults" controls, tucked into the top-right beside the
   stage header: capture the form's settings as the New-Gallery starting point
   (or restore the built-ins), with the always-on required-fields reminder. */
.stage-head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem 1.5rem; flex-wrap: wrap; margin-bottom: 1.1rem; }
.stage-head-row .stage-head { margin-bottom: 0; flex: 1 1 20rem; }
.create-defaults-compact {
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.3rem;
  text-align: right; max-width: 24rem; margin-left: auto;
}
.cdc-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: flex-end; }
.cdc-note { margin: 0; font-size: 0.75rem; }
.create-defaults-compact .cd-warn { margin: 0; font-size: 0.72rem; }
/* The warning turns to the accent colour when a save was blocked for it. */
.create-defaults-compact .cd-warn.is-shown { color: var(--accent); font-weight: 600; }
@media (max-width: 720px) {
  .create-defaults-compact { align-items: flex-start; text-align: left; margin-left: 0; }
  .cdc-actions { justify-content: flex-start; }
}

/* Daily-cron heartbeat line in Settings ▸ Galleries: green edge when the task
   ran recently, amber when it's been over a day (or never). */
.daily-status {
  margin: 0 0 1.1rem; padding: 0.5rem 0.8rem; border-radius: 8px;
  font-size: 0.82rem; background: var(--panel);
  border: 1px solid var(--line); border-left: 3px solid var(--ok);
}
.daily-status.is-stale { border-left-color: var(--accent); }
.daily-status .ds-summary { display: block; margin-top: 0.15rem; color: var(--text-muted); font-size: 0.76rem; }

/* Team master set panel (hub admin): approval progress + send state. */
.tm-progress { margin: 0 0 0.85rem; font-size: 0.88rem; color: var(--text-muted); }
.tm-progress strong { color: var(--text); }
.tm-sent  { color: var(--ok); font-weight: 600; }
.tm-ready { color: var(--ok); font-weight: 600; }

/* Team master set folded into the settings panel: a divided block under the
   owner's-view sharing. And a generic sub-section (e.g. the finals uploader
   folded into Manage Members) — a lighter heading inside an existing panel. */
.tm-block { margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.th-subsection { margin: 0.2rem 0 0.4rem; }
.th-subhead { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.55rem; font-size: 1.02rem; }

/* Retouched-finals uploader, folded in below the member list: a divided block,
   greyed + inert until a retouch member has submitted a selection. */
.finals-subsection { margin-top: 1.3rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.finals-subsection.is-locked .finals-batch { opacity: 0.45; }
.finals-subsection.is-locked .dropzone { pointer-events: none; }
.finals-lock-note { color: var(--text-muted); }

/* Pre-registration scheduler — settings card live preview + off state. */
.sched-fields.is-off { opacity: 0.5; }
.sched-preview { margin: 0.4rem 0 0; font-weight: 500; color: var(--text); }
.sched-preview:empty { display: none; }
/* The four config fields as an even 2-column grid, all one height + type size
   so date / time / dropdown / number line up and read consistently. */
.sched-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.7rem 0.9rem; }
.sched-grid .field { margin: 0; min-width: 0; }
.sched-in { width: 100%; box-sizing: border-box; min-height: 46px; height: 46px;
  padding: 0 0.7rem; font-size: 1rem; line-height: 1.2; }
select.sched-in { padding-right: 1.8rem; }
@media (max-width: 520px) { .sched-grid { grid-template-columns: 1fr; } }

/* Schedule roster (admin): the booked-slot table + PDF button. */
.sched-roster-tools { margin-top: 1rem; display: flex; flex-direction: column; gap: 0.7rem; align-items: flex-start; }
/* The PDF and the email are the same roster by two routes, so they read as a
   pair on one line. The column above them is for the expandable table, which
   wants the full width to itself. */
.sched-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.75rem; }
.sched-actions .sched-sent { margin: 0; }
.sched-roster { width: 100%; }
.sched-roster > summary { cursor: pointer; font-weight: 600; padding: 0.3rem 0; user-select: none; }
.sched-table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; font-size: 0.88rem; }
.sched-table th, .sched-table td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--line); }
.sched-table th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); font-weight: 600; }
.sched-table .st-time { white-space: nowrap; font-variant-numeric: tabular-nums; }
.sched-table .st-email { color: var(--text-muted); word-break: break-all; }

/* Client sign-up slot picker (Team Headshot scheduler). */
.slot-picker { border: 0; padding: 0; margin: 0.2rem 0 1rem; min-inline-size: 0; }
.slot-picker legend { padding: 0; margin: 0 0 0.6rem; font-weight: 600; font-size: 0.95rem; }
.slot-full { color: var(--text-muted); margin: 0.2rem 0 0.4rem; }
.slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(94px, 1fr)); gap: 0.5rem; }
.slot { position: relative; display: flex; align-items: center; justify-content: center;
        min-height: 44px; padding: 0.4rem 0.5rem; border: 1px solid var(--line);
        border-radius: 10px; cursor: pointer; text-align: center;
        transition: border-color .12s ease, background .12s ease; }
.slot input { position: absolute; opacity: 0; pointer-events: none; }
.slot .slot-time { font-size: 0.9rem; }
.slot:hover { border-color: var(--line-strong); }
.slot:has(input:checked) { border-color: var(--ok); background: rgba(127, 200, 160, 0.12); font-weight: 600; }
.slot:has(input:focus-visible) { outline: 2px solid var(--ok); outline-offset: 2px; }
.slot.is-taken { cursor: not-allowed; opacity: 0.4; text-decoration: line-through; }

/* Client team-master download page: one file group per team member. */
.dl-person { margin: 1.2rem 0 0; }
.dl-person + .dl-person { border-top: 1px solid var(--line); padding-top: 0.6rem; }
.dl-person-name { margin: 0 0 0.35rem; font-size: 1rem; }
.dl-person-meta { margin-left: 0.5rem; font-weight: 400; font-size: 0.8rem; color: var(--text-muted); }
.btn.is-todo {
  background: transparent; border-color: var(--line); color: var(--text-faint);
}
.btn.is-todo:hover { background: rgba(var(--ink), 0.05); border-color: var(--line-strong); color: var(--text-muted); }
.btn.is-ready {
  background: var(--ok); border-color: var(--ok); color: #1a1a1c;
}
.btn.is-ready:hover { background: #94d4b1; border-color: #94d4b1; color: #1a1a1c; }
/* Both rules above assume the admin's --ok, a pale green on a dark page,
   so they pair it with near-black text. The client page is light and its
   --ok is a dark green, where that same text falls to 3.5:1 — white holds
   5:1. Hover darkens rather than lightens, for the same reason. */
body.client .btn.is-ready { color: #fff; }
body.client .btn.is-ready:hover { background: #276a47; border-color: #276a47; color: #fff; }

/* Save-state cues (set by admin.js): amber = unsaved changes waiting on a
   click, green = the save just registered. Two classes beat .btn-primary. */
.btn.is-dirty {
  background: #d99a2b; border-color: #d99a2b; color: #1a1a1c;
  animation: btn-dirty-pulse 1.9s ease-in-out infinite;
}
.btn.is-dirty:hover { background: #e6ac3e; border-color: #e6ac3e; color: #1a1a1c; }
@keyframes btn-dirty-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(217, 154, 43, 0); }
  50%      { box-shadow: 0 0 0 5px rgba(217, 154, 43, 0.22); }
}
.btn.is-saved, .btn.is-saved:hover {
  background: #3f9d6a; border-color: #3f9d6a; color: #fff; animation: none;
}
@media (prefers-reduced-motion: reduce) { .btn.is-dirty { animation: none; } }

/* Affirmative "send to the studio" actions (the client's "Send to Adam"
   buttons) — always green with white text, in every review state, so the
   send action reads as the positive one. Declared after .btn-primary so it
   wins the shared single-class specificity; the client's --ok is a dark green
   where white holds ~5:1, and hover darkens to match .is-ready. */
.btn-send { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn-send:hover:not(:disabled),
.btn-send:focus-visible:not(:disabled) { background: #276a47; border-color: #276a47; color: #fff; }
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }

/* Hero selection — nothing on the page says "pick one" until this does. */
.hero-prompt {
  margin: 0 0 1.1rem; padding: 0.75rem 1rem;
  font-size: 0.85rem; color: var(--text-muted);
  background: rgba(201, 162, 39, 0.07);
  border: 1px solid rgba(201, 162, 39, 0.4);
  border-radius: 8px;
}
.hero-prompt strong { color: var(--accent); }
.hero-set { margin-top: 0.35rem; }
.hero-set.is-wanted { border-color: rgba(201, 162, 39, 0.55); color: var(--accent); }
.hero-tag {
  display: inline-block; margin-top: 0.35rem; padding: 0.25rem 0.6rem;
  font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: #fff; background: var(--accent); border-radius: 4px;
}
.photo.is-hero { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Staged photos on the create form, for choosing the hero up front. */
.staged-hero { margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.staged-grid {
  display: grid; gap: 0.6rem;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
}
.staged-thumb {
  position: relative; display: block; padding: 0; cursor: pointer;
  background: none; border: 1px solid var(--line); border-radius: 6px;
  overflow: hidden;
}
.staged-thumb img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
.staged-tag {
  display: block; padding: 0.3rem 0.4rem;
  font-family: inherit; font-size: 0.62rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-faint);
}
.staged-thumb:hover { border-color: var(--line-strong); }
.staged-thumb:hover .staged-tag { color: var(--text); }
.staged-thumb.is-hero { border-color: var(--accent); }
.staged-thumb.is-hero .staged-tag { color: #1a1a1c; background: var(--accent); }

/* Photos staged on the create form, before the gallery exists. */
.staged-note { margin-top: 1rem; }
.staged-list {
  list-style: none; margin: 0.75rem 0 0; padding: 0.75rem 0 0;
  border-top: 1px solid var(--line);
}
.staged-list li { font-size: 0.8rem; color: var(--text-muted); padding: 0.15rem 0; }

.archive-restore { margin: 0 0 1.25rem; }
/* The stage heading names the step, so the panel inside doesn't repeat it. */
.danger-zone p.muted { margin: 0; }

/* Filter the admin photo grid by sub-gallery.
   Ruled off from the upload form above it: adding photos and sorting the
   ones already here are two different jobs. */
.photo-filter {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  margin: 1.75rem 0 1.25rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
}
.pf-tab {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.4rem 0.85rem;
  font-family: inherit; font-size: 0.7rem; letter-spacing: 0.12em;
  text-transform: uppercase; cursor: pointer;
  color: var(--text-muted); background: transparent;
  border: 1px solid var(--line); border-radius: 100px;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.pf-tab:hover { border-color: var(--line-strong); color: var(--text); }
.pf-tab .pf-n { font-size: 0.68rem; color: var(--text-faint); }
.pf-tab.is-on { color: var(--bg); background: var(--text); border-color: var(--text); }
.pf-tab.is-on .pf-n { color: var(--bg); opacity: 0.65; }
/* Unsorted photos are work outstanding — say so until the count is zero,
   then confirm it in green. The named sub-galleries stay neutral. */
.pf-tab.is-todo { border-color: rgba(201, 162, 39, 0.5); color: var(--accent); }
.pf-tab.is-todo .pf-n { color: var(--accent); }
.pf-tab.is-todo.is-on { background: var(--accent); border-color: var(--accent); color: #1a1a1c; }
.pf-tab.is-todo.is-on .pf-n { color: #1a1a1c; }

.pf-tab.is-clear { border-color: rgba(127, 200, 160, 0.5); color: var(--ok); }
.pf-tab.is-clear .pf-n { color: var(--ok); }
.pf-tab.is-clear.is-on { background: var(--ok); border-color: var(--ok); color: #1a1a1c; }
.pf-tab.is-clear.is-on .pf-n { color: #1a1a1c; }
.pf-empty { margin: 0 0 1rem; font-size: 0.85rem; color: var(--text-faint); }

/* Bulk move bar — only present once something is ticked. */
/* Always present, so the controls don't jump in and out of the page.
   Neutral until something is selected, then amber like every other
   "there is work here" signal. */
.bulk-bar {
  display: flex; align-items: center; gap: 0.9rem 1.75rem; flex-wrap: wrap;
  /* Standing clear of the upload button above and the cover-photo prompt
     below — it used to sit flush against both. */
  margin: 1.6rem 0 1.6rem; padding: 1.05rem 1.3rem;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 8px;
  /* background-COLOR, not the `background` shorthand: the shorthand animates
     every background property at once, which is both wasteful and the sort of
     thing engines interpolate inconsistently. */
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
/* Red, not amber: this bar's business end is Delete selected, so the moment it
   has something to act on it should read as destructive rather than merely
   active. Amber is used everywhere else for "unsaved / needs attention", which
   is a different thing entirely. */
/* Plain rgba, NOT color-mix(): this bar transitions `background`, and the
   engine cannot interpolate a color-mix() target — it sticks at transparent
   and the bar renders with no colour at all. Same red as --err (#d98a8a),
   written the way .btn-danger already writes it. */
.bulk-bar.has-selection {
  background: rgba(217, 138, 138, 0.09);
  border-color: rgba(217, 138, 138, 0.5);
}
.bulk-links { display: flex; gap: 1.4rem; }
/* The rule now sits before the count, since the actions come first. */
.bulk-count {
  font-size: 0.8rem; color: var(--text-faint); white-space: nowrap;
  padding-left: 1.75rem; border-left: 1px solid var(--line);
}
.bulk-count strong {
  font-family: var(--font-display); font-size: 1.05rem;
  color: var(--text-faint); margin-right: 0.15rem;
}
/* Delete sits apart from Move, pushed to the far end — destructive actions
   shouldn't be a slip of the mouse away from an ordinary one. */
.bulk-bar .bulk-delete { margin-left: auto; padding-inline: 1.15rem; }
.bulk-bar .bulk-delete[disabled] { opacity: 0.45; cursor: not-allowed; }
.bulk-bar.has-selection .bulk-count { color: var(--text-muted); border-left-color: rgba(217, 138, 138, 0.35); }
.bulk-bar.has-selection .bulk-count strong { color: var(--err); }
/* Nothing selected means nothing to move. */
.bulk-bar button[type="submit"]:disabled { opacity: 0.4; cursor: not-allowed; }
.bulk-target { display: flex; align-items: center; gap: 0.6rem; margin-left: auto; font-size: 0.78rem; color: var(--text-faint); }
.bulk-target .select-wrap { min-width: 11rem; }
@media (max-width: 760px) {
  .bulk-count { padding-left: 0; border-left: 0; }
  .bulk-target { margin-left: 0; }
}

/* Tick box on each tile. Sits over the image, always reachable. */
.photo { position: relative; }
/* The whole tile selects, so it reads as clickable — and shift-clicking
   across it must not drag a stripe of selected text with it. */
.photo img { cursor: pointer; }
.photo .photo-name { cursor: pointer; user-select: none; }
.photo-tick { position: absolute; top: 0.5rem; left: 0.5rem; z-index: 2; cursor: pointer; }
.photo-tick input { position: absolute; opacity: 0; width: 0; height: 0; }
.photo-tick span {
  display: block; width: 20px; height: 20px; border-radius: 4px;
  background: rgba(11, 11, 12, 0.55);
  border: 1px solid rgba(var(--ink), 0.55);
  transition: background 0.12s ease, border-color 0.12s ease;
}
.photo-tick span::after {
  content: ''; display: block; width: 5px; height: 10px; margin: 2px auto 0;
  border: solid #1a1a1c; border-width: 0 2px 2px 0;
  transform: rotate(45deg); opacity: 0;
}
.photo-tick input:checked + span { background: var(--accent); border-color: var(--accent); }
.photo-tick input:checked + span::after { opacity: 1; }
.photo-tick input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.photo:has(.photo-tick input:checked) { outline: 2px solid var(--accent); outline-offset: 2px; }

.dz-folder { margin: 0.75rem 0 0; }
.dz-folder .inline-hint { display: block; margin-top: 0.3rem; }
.dz-folder [data-folder-pick] { text-transform: none; letter-spacing: 0; font-size: 0.82rem; }

.upload-actions { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; }
.upload-target { display: flex; align-items: center; gap: 0.6rem; font-size: 0.75rem; color: var(--text-faint); }
.photo-section { margin: 0.35rem 0 0; }
.photo-section .select-wrap select { width: 100%; }

/* ---- Dashboard: client update ---------------------------------------- */
.client-update { margin-bottom: 2rem; }
.cu-heading {
  font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint); margin: 0 0 0.9rem; font-family: var(--font-body);
  font-weight: 500;
}
.cu-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.9rem; margin-bottom: 1rem; }
/* The cards hold very little text, so three stay comfortable well down
   the range before they need to stack. */
@media (max-width: 680px) { .cu-cards { grid-template-columns: 1fr 1fr; } }
@media (max-width: 460px) { .cu-cards { grid-template-columns: 1fr; } }

.cu-card {
  display: block; padding: 1.25rem 1.35rem;
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: 10px; text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.cu-card:hover { border-color: var(--line-strong); transform: translateY(-1px); }
.cu-card .cu-value {
  display: block; font-family: var(--font-display); font-size: 2rem;
  line-height: 1.1; color: var(--text-faint);
}
.cu-card .cu-label {
  display: block; margin-top: 0.35rem; font-size: 0.74rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-muted);
}
.cu-card .cu-sub { display: block; margin-top: 0.2rem; font-size: 0.75rem; color: var(--text-faint); }
/* Anything above zero earns the accent — it is the difference between
   "nothing to do" and "someone is waiting". */
.cu-card.is-live { border-color: rgba(201, 162, 39, 0.45); background: rgba(201, 162, 39, 0.05); }
.cu-card.is-live .cu-value { color: var(--accent); }
.cu-card.is-live:hover { border-color: var(--accent); }

/* The action card. Reads as a doorway rather than a reading — dashed
   edge so it is never mistaken for a count that happens to say "+". */
.cu-action { border-style: dashed; }
.cu-action .cu-value { color: var(--text-faint); font-size: 2.1rem; line-height: 1; }
.cu-action:hover { border-style: solid; border-color: var(--text-faint); }
.cu-action:hover .cu-value,
.cu-action:hover .cu-label { color: var(--text); }

.cu-list-wrap {
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: 10px; padding: 1.1rem 1.35rem 1.25rem;
}
.cu-list-head { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.35rem; }
.cu-list-head h3 {
  margin: 0; font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-muted); font-family: var(--font-body); font-weight: 500;
}
.cu-count {
  font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 100px;
  background: var(--accent); color: #1a1a1c; font-weight: 600;
}
.cu-clear { margin: 0.4rem 0 0; font-size: 0.88rem; color: var(--text-faint); }
.cu-list { list-style: none; margin: 0; padding: 0; }
.cu-list li + li { border-top: 1px solid var(--line); }

.cu-item {
  display: flex; align-items: center; gap: 0.85rem;
  padding: 0.85rem 0; text-decoration: none; color: inherit;
}
.cu-item:hover .cu-item-title { color: var(--accent); }
.cu-item:hover .cu-item-go { transform: translateX(3px); color: var(--text); }
.cu-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.cu-dot-order { background: #7fb069; }
.cu-dot-ticket { background: #c9a227; }
.cu-item-main { flex: 1; min-width: 0; }
.cu-item-label {
  display: block; font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
}
.cu-item-title {
  display: block; margin-top: 0.15rem; font-size: 0.92rem; color: var(--text);
  transition: color 0.15s ease;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cu-item-sub {
  display: block; margin-top: 0.1rem; font-size: 0.75rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cu-item-when { font-size: 0.75rem; color: var(--text-faint); flex: none; }
.cu-item-go { color: var(--text-faint); flex: none; transition: transform 0.15s ease, color 0.15s ease; }
.cu-note { margin: 0.9rem 0 0; font-size: 0.72rem; color: var(--text-faint); }

/* ---- In-page confirmation (replaces window.confirm) ------------------- */
.cfm-overlay {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center; padding: 1.5rem;
  background: rgba(10, 10, 12, 0.62);
}
.cfm-box {
  width: 100%; max-width: 420px;
  padding: 1.6rem 1.6rem 1.25rem;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
.cfm-message {
  margin: 0 0 1.35rem; font-size: 0.95rem; line-height: 1.55; color: var(--text);
}
.cfm-actions { display: flex; justify-content: flex-end; gap: 0.6rem; }
body.client .cfm-overlay { background: rgba(26, 26, 28, 0.42); }

/* What a gallery is for — three cards, one choice. Bigger than a dropdown
   because it changes what the client actually sees. */
/* The picker has two steps (formats, then the Headshots sub-choice), so the
   card grid lives on each step rather than the outer container. */
.purpose-choice { display: block; }
.purpose-step { display: grid; gap: 0.6rem; grid-template-columns: repeat(3, 1fr); }
/* The headshot step lists four genuinely different formats, so it reads better
   two-up; the session sub-steps are short labels and stay three-up. */
.purpose-step[data-purpose-step="headshots"] { grid-template-columns: 1fr 1fr; }
@media (max-width: 700px) {
  .purpose-step,
  .purpose-step[data-purpose-step="headshots"] { grid-template-columns: 1fr; }
}
/* The question above a sub-step's cards. Spans the row — without this it takes
   a grid cell and sits *beside* the first card. */
/* The question above a sub-step's cards ("What kind of portrait session is
   this?"). Spans the row — without the grid-column it takes a cell and sits
   *beside* the first card. */
.purpose-lead {
  grid-column: 1 / -1;
  margin: 0; font-size: 0.9rem; color: var(--text-muted);
}
/* A back link spanning the row above the sub-options. */
.purpose-back {
  grid-column: 1 / -1; justify-self: start;
  background: none; border: 0; padding: 0.2rem 0; margin: 0;
  color: var(--text-muted); font: inherit; font-size: 0.8rem; cursor: pointer;
}
.purpose-back:hover { color: var(--text); }
/* The "Headshots" launcher is itself a .purpose-opt <label> (with role=button,
   no radio), so it inherits the card styling and admin label font exactly —
   the reason a <button> or <div> could never match: they don't pick up the
   label-scoped font-size, so they render 16px/taller. It only needs a keyboard
   focus ring of its own, since it has no inner input to carry one. */
.purpose-opt[role="button"]:focus-visible { outline: 2px solid var(--ok); outline-offset: 3px; }
/* A session sub-option is a one-line label, not a format explainer — tighter. */
.purpose-opt.is-sub { padding: 0.7rem 0.85rem; }
.purpose-opt.is-sub .po-desc { margin-top: 0.15rem; }
/* Flagged when the studio tries to create without picking a format. */
.purpose-choice.is-missing .purpose-opt { border-color: var(--err); }
.field-error { margin: 0.45rem 0 0; color: var(--err); font-size: 0.8rem; font-weight: 500; }
.purpose-opt {
  display: block; cursor: pointer; padding: 0.85rem 0.9rem;
  border: 1px solid var(--line); border-radius: 8px;
  background: rgba(var(--ink), 0.02);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.purpose-opt:hover { border-color: var(--line-strong); }
.purpose-opt input { position: absolute; opacity: 0; pointer-events: none; }
.purpose-opt .po-title {
  display: block; font-size: 0.86rem; font-weight: 600; color: var(--text-muted);
  text-transform: none; letter-spacing: 0;
}
.purpose-opt .po-desc {
  display: block; margin-top: 0.3rem; font-size: 0.72rem; line-height: 1.45;
  color: var(--text-faint); text-transform: none; letter-spacing: 0;
}
/* Badge and title share the top line; the description sits under both. */
.purpose-opt .po-head { display: flex; align-items: center; gap: 0.55rem; }
/* Numbered like the lifecycle badges, but white rather than gold: each card
   is tagged with the client-facing phase(s) it switches on. A pill, so a
   single digit reads as a circle and "1 & 2" widens without breaking shape.
   flex:none keeps it at its own width so a long title wraps beside it rather
   than crushing the badge. Lining figures + a hair of bottom padding centre
   the display face, the same correction the stage numbers need. */
.purpose-opt .po-num {
  flex: none;
  display: inline-grid; place-items: center;
  min-width: 34px; height: 34px; padding: 0 0.5rem 2px;
  border-radius: 999px;
  font-family: var(--font-display); font-size: 1.1rem; line-height: 1;
  font-variant-numeric: lining-nums; font-feature-settings: "lnum" 1;
  color: var(--text);
  background: rgba(var(--ink), 0.07);
  border: 1px solid rgba(var(--ink), 0.32);
}
/* Nothing is lit by default; the card the photographer picks lights green —
   a settled decision. (The Settings tab still flags the choice as needed via
   the container's is-unconfirmed class until one is made.) */
.purpose-opt.is-on { border-color: rgba(127, 200, 160, 0.55); background: rgba(127, 200, 160, 0.08); }
.purpose-opt.is-on .po-title { color: var(--ok); }
.purpose-opt:has(input:focus-visible) { outline: 2px solid var(--ok); outline-offset: 3px; }
/* On edit the format is fixed, so it shows as a locked header, not a picker. */
.format-locked { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 0.9rem; border: 1px solid rgba(127, 200, 160, 0.4); border-radius: 8px; background: rgba(127, 200, 160, 0.06); }
.format-locked .format-info { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 0; }
.format-locked .format-name { font-size: 0.9rem; font-weight: 600; color: var(--text); }
.format-locked .format-lock { flex: none; font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); border: 1px solid var(--line); border-radius: 999px; padding: 0.2rem 0.55rem; }
/* Format indicator chip in the galleries list, beside each gallery title. */
.format-chip {
  display: inline-block; margin-left: 0.5rem; vertical-align: 1px; white-space: nowrap;
  font-size: 0.66rem; font-weight: 600; letter-spacing: 0.02em;
  color: var(--text-muted); border: 1px solid var(--line);
  border-radius: 999px; padding: 0.1rem 0.5rem;
}

/* "Manage master files" sits above the uploader — what is already there, then
   what you add. No outer frame of its own: the fold underneath already draws
   one, and two nested boxes read as a mistake. */
.purpose-masters { grid-column: 1 / -1; margin: 1.6rem 0 0.9rem; }
.purpose-masters .oa-master-fold { margin: 0; }
.purpose-masters .oa-master-sum strong { color: var(--text); font-weight: 600; }

/* The session sub-label beside the format chip: the same shape, but quieter —
   the format is what the gallery IS, the session is only how it is filed. */
.session-chip {
  display: inline-block; margin-left: 0.35rem; vertical-align: 1px; white-space: nowrap;
  font-size: 0.66rem; font-weight: 500; letter-spacing: 0.02em;
  color: var(--text-muted); background: rgba(var(--ink), 0.05);
  border-radius: 999px; padding: 0.1rem 0.5rem;
}

/* "Kind of session" on the settings page — a plain select under the locked
   format card, not part of the format control itself. */
.session-field { margin-top: 0.9rem; }
.session-field select { max-width: 18rem; }

.checkbox-field { display: flex; align-items: flex-end; }
/* Stacked variant: the checkbox + its one-line label sit above the hint,
   rather than the hint being squeezed beside them into a narrow column. */
.checkbox-stack { flex-direction: column; align-items: flex-start; gap: 0.4rem; }
.checkbox-stack .checkbox span { white-space: nowrap; }
/* Grouped landing (sign-in page) text, boxed with a thin line + a legend. */
/* ---- Settings groups ------------------------------------------------
   Every group on a gallery's settings page is a fold (see $section in
   admin/galleries/_fields.php). The page had become one unbroken column —
   42 buttons and 67 inputs on a Portrait gallery, with nothing to break it
   up — so each group can now be put away and stays that way (admin.js
   remembers it against data-group-key). */
.set-group {
  border: 1px solid var(--line); border-radius: 8px;
  margin: 0 0 0.9rem; min-width: 0; background: rgba(var(--ink), 0.015);
}
.set-group > summary {
  display: flex; align-items: baseline; gap: 0.7rem;
  padding: 0.7rem 1.1rem; cursor: pointer; list-style: none;
  border-radius: 8px;
}
.set-group > summary::-webkit-details-marker { display: none; }
.set-group > summary:hover { background: rgba(var(--ink), 0.03); }
.set-group > summary:focus-visible { outline: 2px solid var(--ok); outline-offset: -2px; }
.sg-title {
  font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase;
  font-weight: 600; color: var(--text);
}
.sg-sub { font-size: 0.82rem; color: var(--text-muted); min-width: 0; }
/* A group holding a required choice carries the marker on its own header,
   since the field inside no longer repeats the group's name. */
.set-group.is-required > summary .sg-title::after {
  content: " *"; color: var(--err); font-weight: 700;
}
@media (max-width: 620px) { .sg-sub { display: none; } }
/* The caret is baseline-aligned in a summary, unlike the stage headers where
   it centres against a two-line block. */
.set-group > summary .collapse-caret { align-self: center; }
.set-group:not([open]) > summary .collapse-caret { transform: rotate(-90deg); }
.sg-body { padding: 0.25rem 1.1rem 1rem; min-width: 0; }
/* Same trap the order folds and the template groups hit: a later rule gives
   the body a display, which defeats the browser's own hiding of a closed
   <details>. Hide it explicitly. */
.set-group:not([open]) > .sg-body { display: none; }
/* A field-row inside a group shouldn't add a top margin against the summary. */
.sg-body > .field-row:first-child,
.sg-body > .field:first-child { margin-top: 0; }
.landing-note { margin: 0.35rem 0 1rem; font-size: 0.82rem; color: var(--text-muted); }
/* Offer-email compose: the code + what it gives, shown above the send form. */
.offer-summary { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.4rem 0.9rem; padding: 0.8rem 1rem; margin-bottom: 1.4rem; border: 1px solid var(--line); border-radius: 6px; }
.offer-summary-code { font-weight: 600; font-size: 1.05rem; letter-spacing: 0.06em; }
.offer-summary-desc { color: var(--text-muted); }
/* Sub-tab nav (Products | Discounts). */
.subtabs { display: flex; gap: 0.4rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--line); }
.subtab { padding: 0.55rem 1rem; font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-muted); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.subtab:hover { color: var(--text); }
.subtab.is-active { color: var(--text); border-bottom-color: var(--text); }

/* Product-list tabs — a second, pill-style row below the lists creator on the
   Products screen. Deliberately different from the underline .subtabs above so
   the two levels don't read as one. Wraps rather than overflowing. */
.list-tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 1.2rem; }
.list-tab {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.4rem 0.85rem; font-size: 0.82rem; white-space: nowrap;
  color: var(--text-muted); text-decoration: none;
  background: var(--surface-1); border: 1px solid var(--line); border-radius: 999px;
}
.list-tab:hover { color: var(--text); border-color: var(--line-strong); }
.list-tab.is-active { background: var(--text); color: var(--bg); border-color: var(--text); }
.list-tab .n { font-variant-numeric: tabular-nums; font-size: 0.72rem; opacity: 0.6; }
.list-tab.is-active .n { opacity: 0.72; }
.list-tab.is-all { margin-left: 0.35rem; } /* the catch-all sits a touch apart */

/* Multi-select tables (batch add / remove) + their action bar. */
.check-col { width: 2.6rem; text-align: center; }
.check-col input { cursor: pointer; }
tr[data-batch-row] { cursor: pointer; }
.batch-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.7rem;
  margin: 0 0 0.9rem; padding: 0.7rem 0.85rem;
  background: var(--surface-1); border: 1px solid var(--line); border-radius: 10px;
}
.batch-bar .spacer { flex: 1 1 auto; }
.batch-bar .batch-count { font-size: 0.82rem; color: var(--text-muted); }
.batch-bar .batch-inline { font-size: 0.82rem; color: var(--text-muted); text-transform: none; letter-spacing: 0; }
.batch-bar .select-wrap { margin: 0; }
.batch-bar .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
/* A small radio pair (price mode, discount kind). Stacked with room to breathe;
   overrides the uppercase, tight `.field label` treatment so the option text
   reads normally and isn't crammed against the radio. */
.pricing-mode { display: flex; flex-direction: column; gap: 0.65rem; padding-top: 0.45rem; }
.pricing-mode .checkbox { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0; font-size: 0.9rem; text-transform: none; letter-spacing: 0; }
.pricing-mode .checkbox span { text-transform: none; letter-spacing: 0; }

/* Package contents repeater (admin package form): one row per product = a
   product select + a quantity + a remove link. */
.pkg-contents { border: 1px solid var(--line); border-radius: 12px; padding: 1.1rem 1.25rem; margin: 0 0 1.25rem; }
.pkg-contents legend { font-weight: 500; padding: 0 0.4rem; }
.pkg-rows { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.85rem 0; }
.pkg-row { display: flex; align-items: center; gap: 0.6rem; }
.pkg-row .select-wrap { flex: 1 1 auto; min-width: 0; }
.pkg-qty { width: 5rem; flex: none; }
.pkg-row [data-pkg-remove] { flex: none; }

/* Store-credit panel on the client page. */
.credit-add { margin-top: 0.75rem; }
.credit-ledger { margin-top: 1rem; font-size: 0.85rem; }
.credit-ledger .credit-pos { font-weight: 600; color: var(--ok, #1a7f5a); }
.credit-ledger .credit-neg { font-weight: 600; color: var(--danger, #b23c3c); }
/* Discount / gift code entry at checkout. */
.checkout-codes { margin: 1rem 0; padding: 0.9rem 0; border-top: 1px solid var(--line, #e5e2dc); border-bottom: 1px solid var(--line, #e5e2dc); }
.applied-code { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; font-size: 0.9rem; margin-bottom: 0.5rem; }
.applied-code form { margin: 0; }
.code-form { display: flex; gap: 0.5rem; }
.code-form input { flex: 1 1 auto; text-transform: uppercase; }
.code-error { color: var(--danger, #b23c3c); font-size: 0.85rem; margin: 0.25rem 0 0.5rem; }
.sum .sum-off { color: var(--ok, #1a7f5a); }

/* Checkboxes, system-wide: a white box when unchecked, filled green (--ok) with
   a dark tick when checked. Custom-drawn (appearance:none) so it looks the same
   in every browser on the dark UI. Explicit-size rules below still win (equal
   or higher specificity, later in the file). Radios and ranges are untouched —
   they keep their native accent-color rendering. */
input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; flex: none; vertical-align: middle;
  border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 4px;
  background: #fff; cursor: pointer;
}
input[type="checkbox"]:checked {
  background-color: var(--ok); border-color: var(--ok);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%231a1a1c' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.4l3 3 6-6.6'/%3E%3C/svg%3E");
  background-size: 78%; background-position: center; background-repeat: no-repeat;
}
input[type="checkbox"]:indeterminate {
  background-color: var(--ok); border-color: var(--ok);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%231a1a1c' stroke-width='2.5' stroke-linecap='round' d='M4 8h8'/%3E%3C/svg%3E");
  background-size: 78%; background-position: center; background-repeat: no-repeat;
}
input[type="checkbox"]:focus-visible { outline: 2px solid var(--ok); outline-offset: 1px; }

.checkbox { display: flex; align-items: center; gap: 0.6rem; text-transform: none; letter-spacing: 0; font-size: 0.9rem; color: var(--text-muted); }
.checkbox input { width: 16px; height: 16px; }
/* Prominent toggle (e.g. "Retouching included") — a bigger, green box and a
   green, bolder label the moment it's ticked, so its state is unmistakable. */
.checkbox-prominent input { width: 20px; height: 20px; accent-color: var(--ok); }
.checkbox-prominent:has(input:checked) span { color: var(--ok); font-weight: 600; }
.checkbox-prominent:has(input:checked) span::after { content: " ✓"; font-weight: 700; }
/* "View as client" on one line with a short note on what it does. */
.view-as-client-row { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; margin-top: 0.4rem; }
.view-as-client-row .hint { margin: 0; flex: 1 1 240px; }

.form-actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
/* Same bar, pinned at the top of a long form — line under it, not above. */
.form-actions-top { margin-top: 0; margin-bottom: 1.5rem; padding-top: 0; padding-bottom: 1.25rem; border-top: 0; border-bottom: 1px solid var(--line); }
/* Settings save bar: sits below the section cards, no divider line of its own. */
.settings-save { margin-top: 0; padding-top: 0; border-top: 0; }

/* Watermark placement preview — sits at the top-right of the Watermark card and
   scrolls with it (no sticky-follow). Click it to enlarge for a closer look. */
.wm-preview { float: right; width: 190px; margin: 0 0 1rem 1.5rem; }
.wm-preview img { display: block; width: 190px; height: 285px; object-fit: cover;
  background: #808080; border: 1px solid var(--line); border-radius: 10px; cursor: zoom-in; }
.wm-preview figcaption { text-align: center; margin-top: 0.4rem; }
@media (max-width: 700px) {
  .wm-preview { float: none; width: 100%; max-width: 220px; margin: 0 auto 1rem; position: static; }
}

/* Click-to-enlarge overlay for the watermark preview. */
.wm-lb { position: fixed; inset: 0; z-index: 1000; display: none; align-items: center;
  justify-content: center; padding: 3vh; background: rgba(0, 0, 0, 0.82); cursor: zoom-out; }
.wm-lb.is-open { display: flex; }
.wm-lb img { max-width: 92vw; max-height: 94vh; border-radius: 10px; box-shadow: 0 12px 44px rgba(0, 0, 0, 0.5); }

/* Generic click-to-enlarge lightbox (photo grid cover picker, etc.). */
.img-lb { position: fixed; inset: 0; z-index: 1000; display: none; align-items: center;
  justify-content: center; padding: 3vh; background: rgba(0, 0, 0, 0.82); cursor: zoom-out; }
.img-lb.is-open { display: flex; }
.img-lb-inner { display: flex; flex-direction: column; align-items: center; gap: 0.85rem; cursor: default; }
.img-lb img { max-width: 92vw; max-height: 84vh; border-radius: 10px; box-shadow: 0 12px 44px rgba(0, 0, 0, 0.5); }
.img-lb-actions { display: flex; align-items: center; gap: 0.85rem; }
.img-lb-btn, .img-lb-del { font: inherit; font-size: 0.85rem; cursor: pointer; border-radius: 999px;
  padding: 0.4rem 1rem; border: 1px solid rgba(255, 255, 255, 0.35);
  background: rgba(255, 255, 255, 0.14); color: #fff; }
.img-lb-btn:hover { background: rgba(255, 255, 255, 0.26); }
.img-lb-del { border-color: rgba(255, 120, 110, 0.5); color: #ff9b8f; background: rgba(255, 90, 80, 0.12); }
.img-lb-del:hover { background: rgba(255, 90, 80, 0.22); color: #ffb8ae; }
.img-lb-tag { color: #fff; font-size: 0.85rem; opacity: 0.82; letter-spacing: 0.02em; }
.photo-thumb { cursor: zoom-in; }
.btn.inline { width: auto; }
.btn-quiet { border-color: var(--line); color: var(--text-muted); }
.btn.sm { padding: 0.4rem 0.9rem; font-size: 0.7rem; }
.btn-danger { border-color: rgba(217,138,138,0.5); color: var(--err); }
.btn-danger:hover { background: rgba(217,138,138,0.1); }
/* "Remove unlocked photos" is destructive and belongs to the lock family it
   acts on — a soft red fill at rest, solid red on hover. Distinct from the
   green Compare and softer than the solid "Locked" badge. */
.btn.btn-remove {
  background: rgba(217, 138, 138, 0.16);
  border-color: rgba(217, 138, 138, 0.55);
  color: var(--err); font-weight: 600;
}
.btn.btn-remove:hover { background: var(--err); border-color: var(--err); color: #fff; }

/* ---- Client link panel ---------------------------------------------- */
.link-panel { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; padding: 1.1rem 1.5rem; }
.link-label { font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); }
.link-value { flex: 1; min-width: 200px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.82rem; color: var(--text-muted); overflow-wrap: anywhere; }

/* ---- Portrait Proofing: phase toggle ------------------------------- */
/* The phase toggle drops into the purpose grid as its own full-width row,
   sitting just beneath the Portrait proofing card. */
.purpose-phase { grid-column: 1 / -1; margin-top: 1.1rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.25rem; }
.phase-toggle { display: inline-flex; flex: none; padding: 3px; gap: 2px; border-radius: 100px; background: rgba(var(--ink), 0.06); }
.phase-toggle .phase-btn {
  border: 1px solid transparent; background: none; padding: 0.4rem 1rem; border-radius: 100px;
  font: inherit; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.phase-toggle .phase-btn:hover { color: var(--text); }
/* Active phase echoes the chosen purpose card — green accent — but takes a
   white stroke of its own so the segmented control reads as toggled. */
.phase-toggle .phase-btn.is-on {
  background: rgba(127, 200, 160, 0.12); color: var(--ok);
  border-color: rgba(var(--ink), 0.45); font-weight: 600;
}
/* A phase that isn't reachable yet reads as clearly locked, not just faint. */
.phase-toggle .phase-btn.is-locked {
  opacity: 0.4; cursor: not-allowed;
}
.phase-toggle .phase-btn.is-locked:hover { color: var(--text-muted); }
.phase-hint { margin: 0; flex: 1 1 240px; }
.phase-lock-note { flex: 1 0 100%; margin: 0.2rem 0 0; color: var(--text-faint); font-size: 0.78rem; }
/* Phase 2 drops the finals upload in right under the toggle; Team headshots
   reflows below it. It slides in so the change of phase reads as a change. */
.purpose-finals {
  grid-column: 1 / -1; display: flex; flex-direction: column; gap: 0.6rem;
  margin-top: 1.6rem;
  padding: 1rem 1.05rem; border: 1px solid rgba(var(--ink), 0.32); border-radius: 8px;
  animation: pp-finals-in 0.28s ease both;
}
.purpose-finals .hint { margin: 0; }
/* Uploader heading + the easy-to-read high-res instruction. */
.finals-title { margin: 0; font-size: 0.98rem; font-weight: 600; letter-spacing: -0.01em; color: var(--text); }
.finals-hires { margin: 0; font-size: 0.86rem; line-height: 1.5; color: var(--text); }
.finals-hires strong { color: var(--text); font-weight: 600; }
/* Preview / send the "finals ready" email — a decoupled action row, not part
   of the upload. */
.finals-email { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem 0.6rem; }
.finals-email form { margin: 0; display: inline-flex; }
.finals-email-label { font-size: 0.8rem; color: var(--text-muted); }
/* "Sent" tick beside Send email — green, stays until the next final upload. */
.finals-sent { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--ok); font-size: 0.8rem; font-weight: 600; }
.finals-sent svg { display: block; }
/* Fades before admin.js removes it — see the data-autoclear block there. */
.finals-sent { transition: opacity 0.4s ease; }
.finals-sent.is-clearing { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .finals-sent { transition: none; } }
@keyframes pp-finals-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .purpose-finals { animation: none; } }
.finals-upload { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
/* Keep the file input from stretching so the Upload button sits right beside
   it instead of being shoved to the far edge. */
.finals-file { flex: 0 1 auto; min-width: 0; max-width: 100%; font-size: 0.85rem; }
/* The Upload button recolors as it's used: amber once files are picked (set by
   admin.js), green the moment the upload lands (shared success flash). Restated
   at higher specificity than .btn-primary so it actually shows here. */
.finals-upload .btn.is-dirty { background: #d99a2b !important; border-color: #d99a2b !important; color: #1a1a1c !important; }
.finals-upload .btn.is-dirty:hover { background: #e6ac3e !important; border-color: #e6ac3e !important; }
.finals-upload .btn.is-saved,
.finals-upload .btn.is-saved:hover { background: #3f9d6a !important; border-color: #3f9d6a !important; color: #fff !important; }
/* Two numbered steps — upload, then notify — so the order of operations is
   obvious at a glance. */
.finals-steps { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.85rem; }
.finals-step { display: flex; gap: 0.7rem; align-items: flex-start; }
.finals-step-num {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center; margin-top: 0.1rem;
  background: rgba(var(--ink), 0.14); color: var(--text);
  font-size: 0.75rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.finals-step-body { display: flex; flex-direction: column; gap: 0.45rem; min-width: 0; flex: 1 1 auto; }
.finals-step-label { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }

/* Team hub section headings take the quieter sans face of the numbered
   lifecycle stage headings, not the default display serif — and each carries
   a green numbered badge, like a lifecycle stage. */
.team-hub .panel-head { align-items: center; }
.team-hub .panel-head h2 {
  font-family: var(--font-body); font-size: 1.15rem;
  font-weight: 500; letter-spacing: 0.02em; color: var(--text);
}
.th-title { display: flex; align-items: center; gap: 0.85rem; min-width: 0; }
/* Let the heading and its buttons drop onto separate lines instead of colliding.
   .panel-head is a nowrap flex row, and with a long heading ("Manage Team
   Member Galleries") beside a wide button ("View Pre-registration List (3)")
   there was nothing to stop the button being drawn over the last word. */
.panel-head:has(> .th-title) { flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; }
.panel-head:has(> .th-title) .th-title { flex: 1 1 auto; }
.panel-head:has(> .th-title) .th-head-actions { flex: 0 1 auto; }
.th-head-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.th-head-actions form { margin: 0; }
.oa-head-right { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }

/* ---- Team hub: expandable people list ------------------------------ */
/* Each member is a distinct, separated card rather than a flat row, so a long
   team reads as a clear list of people. */
.subject-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.subject-row {
  border: 1px solid var(--line-strong); border-radius: 10px; background: var(--surface-1);
  overflow: hidden; transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.subject-row.is-open { border-color: var(--text-faint); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); }
.subject-head {
  display: flex; align-items: center; gap: 0.75rem; width: 100%;
  padding: 1rem 1.1rem; background: none; border: 0; cursor: pointer;
  font: inherit; color: var(--text); text-align: left;
}
.subject-head:hover { background: var(--surface-2); }
/* Once a member row is expanded, tint its title bar green so it's clear which
   person you're editing (beats :hover via specificity, so it stays green). */
.subject-row.is-open .subject-head { background: rgba(127, 200, 160, 0.12); }
.subject-row.is-open .subject-name { color: var(--ok); }
.subject-name { font-weight: 600; font-size: 0.98rem; flex: 1 1 auto; min-width: 0; }
.subject-meta { display: flex; align-items: center; gap: 0.6rem; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; }
.subject-caret { flex: none; color: var(--text-faint); transition: transform 0.15s ease; }
.subject-row.is-open .subject-caret { transform: rotate(180deg); }
.subject-body { padding: 0.75rem 1.1rem 1.2rem; border-top: 1px solid var(--line); }
.subject-body .link-panel { padding: 0.9rem 0; }
.subject-buttons { margin-top: 0.75rem; }

/* Pre-registration list popup. */
.prereg-modal {
  position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center;
  padding: 1.5rem; background: rgba(8, 8, 9, 0.5);
}
.prereg-modal[hidden] { display: none; }
.prereg-inner {
  /* Wide enough for what the four columns actually need, measured rather than
     guessed: a full private-relay address wants 328px, the longest names about
     150px, and Remove + Save side by side 145px. At 860px something always had
     to be cut off; at 920px nothing does. */
  position: relative; width: 100%; max-width: 920px; max-height: 80vh; overflow: auto;
  background: var(--surface-1); border: 1px solid var(--line-strong); border-radius: 12px;
  padding: 1.5rem 1.6rem; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.prereg-table td, .prereg-table th { padding: 0.5rem 0.6rem; vertical-align: top; }
/* Fixed layout so a long email can't stretch its column and shove the Copy
   actions off the edge — the email wraps instead, actions keep their place. */
.prereg-table { table-layout: fixed; width: 100%; }
/* Three columns plus the row actions. These percentages have to add up to 100
   for the SHAPE OF TABLE ACTUALLY BEING DRAWN: written for three columns and
   then given a fourth (Time, when the scheduler is on), the actions column was
   pushed past 100% and off the right edge of the modal — which is why the Save
   button appeared to be missing entirely. .has-time is set by the view. */
.prereg-table th:nth-child(1), .prereg-table td:nth-child(1) { width: 26%; }
.prereg-table th:nth-child(2), .prereg-table td:nth-child(2) { width: 48%; overflow-wrap: anywhere; }
.prereg-table th:nth-child(3), .prereg-table td:nth-child(3) { width: 26%; }
/* Sized from what the content measures, not by eye. A column gives its input a
   content box about 34px narrower than itself: names want ~150px, a
   private-relay address ~330px, "9:20 AM" ~60px, and Remove + Save on one line
   145px. Getting this wrong does not wrap — the table is fixed-layout, so it
   silently cuts a name or an address in half. */
.prereg-table.has-time th:nth-child(1), .prereg-table.has-time td:nth-child(1) { width: 23%; }
.prereg-table.has-time th:nth-child(2), .prereg-table.has-time td:nth-child(2) { width: 45%; }
.prereg-table.has-time th:nth-child(3), .prereg-table.has-time td:nth-child(3) { width: 11%; }
.prereg-table.has-time th:nth-child(4), .prereg-table.has-time td:nth-child(4) { width: 21%; }
/* The heading sat directly on the table's own header row, so the two read as
   one overlapping block. */
.prereg-inner > h2 { margin: 0 2rem 1rem 0; }
.prereg-actions { text-align: right; white-space: nowrap; }
/* Both controls on one line, pinned right and centred against the row.
   Stacked, they left two ragged right edges — Remove sits inside its own
   <form>, which shrank to its content and finished 109px short of Save — and
   made every row 80px tall to hold two small controls. */
.pr-act { display: flex; align-items: center; justify-content: flex-end; gap: 0.85rem; }
.pr-act form { margin: 0; }
.pr-act .linkish { margin: 0; }
/* Editable pre-registration list: name/email are inputs; the row actions stack
   Save over Remove. */
.prereg-table td .pr-in { width: 100%; }
.prereg-table td { vertical-align: middle; }
.prereg-actions .btn { display: inline-block; width: auto; margin: 0; }

/* Pre-registration QR popup. Explicit width because the content is narrow — a
   flex item's width:100% collapses to content width, shrinking the card. */
.qr-modal-inner { width: 380px; max-width: 100%; text-align: center; }
.qr-preview { width: 260px; max-width: 100%; margin: 0.75rem auto; }
.qr-preview svg { display: block; width: 100%; height: auto; background: #fff; padding: 12px; border-radius: 8px; }
.qr-url { margin: 0 0 1rem; font-size: 0.78rem; color: var(--text-muted); word-break: break-all; }
.qr-actions { display: flex; justify-content: center; gap: 0.6rem; flex-wrap: wrap; }
.qr-warn {
  margin: 1rem 0 0.85rem; padding: 0.6rem 0.8rem; text-align: left; font-size: 0.82rem;
  color: var(--text-muted); background: rgba(201, 162, 39, 0.08);
  border: 1px solid rgba(201, 162, 39, 0.4); border-radius: 8px;
}

/* Team Global Settings — each sub-setting sits in its own separated block so
   retouching, the logo, and the owner's view don't read as one jumble. */
/* The hub's settings blocks are folds now (see .set-group). They sit inside a
   panel rather than free on the page, so they drop the card background and keep
   the hairline rule that separated them before. */
.th-fold {
  border: 0; border-top: 1px solid var(--line);
  margin: 0; border-radius: 0; background: none;
}
.th-fold > summary { padding: 0.85rem 0 0.6rem; border-radius: 0; }
.th-fold > summary:hover { background: none; }
.th-fold > summary:hover .sg-title { color: var(--text-muted); }
.th-fold > .sg-body { padding: 0 0 0.9rem; }
.th-fold > summary .sg-title {
  font-size: 0.72rem; letter-spacing: 0.06em; color: var(--text-muted); font-weight: 600;
}
/* Legacy non-fold blocks (none left on the hub, kept for any other user). */
.th-field-block { border-top: 1px solid var(--line); margin-top: 1.1rem; padding-top: 1.1rem; }
.th-block-label { display: block; font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem; }
.subject-send { margin-top: 0.75rem; display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* Team subject: the two-phase (Select → Approve) controls, shown once
   retouching applies, sit below the send-ready action. */
.subject-phase-block { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 0.65rem; }
.subject-phase-block .purpose-finals { grid-column: auto; }
/* .phase-hint is sized for the horizontal PP toggle row (flex-basis 240px);
   in this vertical column that basis becomes height and opens a big gap. */
.subject-phase-block .phase-hint { flex: 0 0 auto; }
.subject-locked-note { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.th-retouch-hint { margin: 0.4rem 0 0; max-width: 64ch; }

/* ---- Upload dropzone ------------------------------------------------ */
.upload-form { display: flex; flex-direction: column; gap: 1rem; align-items: flex-start; }
/* Team-headshots folder instruction, sitting above the create drop zone.
   Green-accented to echo the Team card the photographer just chose. */
.dz-team-note {
  margin: 0 0 0.75rem; padding: 0.7rem 0.85rem;
  font-size: 0.85rem; line-height: 1.45; color: var(--text-muted);
  background: rgba(127, 200, 160, 0.08); border-left: 2px solid var(--ok);
  border-radius: var(--radius);
}
.dz-team-note strong { color: var(--text); }

.dropzone {
  width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
  padding: 2.5rem 1.5rem; text-align: center; cursor: pointer;
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  background: rgba(var(--ink), 0.015); transition: background 0.2s ease, border-color 0.2s ease;
}
.dropzone:hover, .dropzone.is-over { background: rgba(var(--ink), 0.05); border-color: var(--text-faint); }
.dz-title { font-size: 0.95rem; }
.dz-sub { font-size: 0.74rem; color: var(--text-faint); }
.dz-chosen { font-size: 0.8rem; color: var(--ok); margin-top: 0.4rem; }

/* ---- Photo grid ----------------------------------------------------- */
/* "Warm images" — pre-build derivatives so the client's first load is instant. */
.warm-bar { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; margin: 1.25rem 0 0; }
.warm-status { font-size: 0.82rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.warm-bar .warm-hint { margin: 0; font-size: 0.78rem; }

/* Email screen: Resend connection status + one-click test send. */
.email-setup { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap; margin: 0 0 1.5rem; }
.email-conn { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.85rem; font-weight: 600; }
.email-conn .ec-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.email-conn.is-on { color: var(--ok); }
.email-conn.is-off { color: var(--text-faint); }
.email-test { display: inline-flex; align-items: center; gap: 0.5rem; }
.email-test input { max-width: 15rem; }

.photo-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 1rem; margin-top: 1.75rem;
}
.photo { margin: 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--bg-3); }
.photo.is-hero { border-color: var(--accent); }
.photo img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }
.photo figcaption { padding: 0.6rem 0.7rem; display: flex; flex-direction: column; gap: 0.4rem; }
.photo-name { font-size: 0.76rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.photo-actions { display: flex; align-items: center; gap: 0.75rem; }
.photo-actions form { display: inline; }
.linkish {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: inherit; font-size: 0.72rem; letter-spacing: 0.06em;
  color: var(--text-faint); text-transform: uppercase;
}
.linkish:hover { color: var(--text); }
.linkish.danger:hover { color: var(--err); }

/* ---- Danger zone ---------------------------------------------------- */
.danger-zone { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; border-color: rgba(217,138,138,0.25); }
.danger-zone h2 { font-size: 1.1rem; margin-bottom: 0.25rem; }
/* Drop a standalone delete panel clear of the Save/Cancel bar above it, so the
   destructive action reads as its own thing rather than crowding the actions. */
.danger-zone--spaced { margin-top: 2.5rem; }

/* =====================================================================
   Client-facing gallery
   ===================================================================== */
body.client { display: flex; flex-direction: column; min-height: 100vh; }

.client-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1.1rem clamp(1rem, 4vw, 2.5rem);
  border-bottom: 1px solid var(--line);
}
.client-brand { display: flex; align-items: center; gap: 0.7rem; text-decoration: none; }
.client-main { flex: 1; padding: 0 clamp(1rem, 4vw, 2.5rem); }
.client-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  padding: 2rem clamp(1rem, 4vw, 2.5rem);
  border-top: 1px solid var(--line);
  font-size: 0.78rem; color: var(--text-faint);
}
.client-foot a { text-decoration: none; }
.client-foot a:hover { color: var(--text); }
.pad { padding: 4rem 1rem; }

/* ---- Split landing: photo left, sign-in right ------------------------ */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
  min-height: calc(100vh - 170px);   /* header + footer */
  margin: 0 calc(-1 * clamp(1rem, 4vw, 2.5rem));
}
.split.no-image { grid-template-columns: 1fr; }

/* The cover never grows past the screen: caps a long-panel desktop split and a
   tall mobile aspect-ratio box alike. dvh tracks the real viewport on mobile. */
.split-media { position: relative; overflow: hidden; background: var(--bg-2); max-height: 100vh; max-height: 100dvh; }
/* Show the WHOLE cover at its own ratio (no crop); it scales with the space and
   any leftover fills with the studio background. Capped at the screen height. */
.split-media img {
  display: block; width: 100%; height: 100%;
  object-fit: contain; object-position: center;
}

.split-panel {
  display: grid; place-items: center;
  padding: clamp(2.5rem, 6vw, 5rem) clamp(1.5rem, 5vw, 4rem);
}
.split-inner { width: 100%; max-width: 420px; }
.split-inner h1 { font-size: clamp(1.9rem, 3.4vw, 2.9rem); margin: 0.7rem 0 0; }
.split-lead { color: var(--text-muted); margin: 1.1rem 0 0; }
.split-expiry {
  margin-top: 1.5rem; font-size: 0.74rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-faint);
}

.gate-form { margin-top: 2rem; }
.gate-row { display: flex; gap: 0.6rem; margin-bottom: 0.7rem; }
.gate-row input { flex: 1; }
.gate-row .btn { white-space: nowrap; }
.gate-note { font-size: 0.78rem; color: var(--text-faint); margin: 0.9rem 0 0; }
.checkbox.subtle { font-size: 0.78rem; margin-top: 0.9rem; }

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; min-height: 0; }
  /* Stacked above the form: the whole cover at its own ratio, full width, and
     never taller than the screen. */
  .split-media { aspect-ratio: auto; }
  .split-media img { height: auto; max-height: 100dvh; object-position: center; }
}

/* ---- Gallery bar ----------------------------------------------------- */
/* Pinned so Order and the selection count stay reachable from anywhere in
   a long gallery — checking out shouldn't mean scrolling back to the top.
   The side bleed cancels .client-main's padding, so photos pass behind the
   bar instead of alongside it. */
/* Sits under the bar and grows by exactly what the bar loses, so the two
   together are always the bar's full height. Written by gallery.js in the same
   frame as --gb-p, from the bar's real measured height — not from a formula,
   because how much height the bar sheds depends on how its flex rows land. */
.gb-spacer { height: var(--gb-give, 0px); }
/* --gb-p is how far the bar has collapsed, 0..1, written by the scroll
   handler once per frame. Every value below is interpolated off it, so the
   bar tracks the scroll instead of animating on its own clock. There is
   deliberately NO transition here: a timed transition chasing a per-frame
   value is what made this snap in the first place. */
.gallery-bar {
  --gb-p: 0;
  position: sticky; top: 0; z-index: 20;
  background: var(--bg);
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  /* 1.75rem -> 0.7rem on top, 1.25rem -> 0.7rem on the bottom. */
  padding-top: calc(1.75rem - (1.05rem * var(--gb-p)));
  padding-bottom: calc(1.25rem - (0.55rem * var(--gb-p)));
  padding-left: clamp(1rem, 4vw, 2.5rem);
  padding-right: clamp(1rem, 4vw, 2.5rem);
  margin: 0 calc(-1 * clamp(1rem, 4vw, 2.5rem)) 1.75rem;
  border-bottom: 1px solid var(--line);
  box-shadow: 0 6px 20px rgba(26, 26, 28, calc(0.10 * var(--gb-p)));
}
/* Deliberately plain `position: sticky` above — NOT fixed-and-toggled-by-JS.
   A sticky box does still occupy its flow height, and every pixel it sheds
   used to be a pixel the page jumped up; .gb-spacer below it gives those
   pixels straight back, which solves that without taking the bar out of the
   flow. Toggling `position: fixed` from a scroll handler solved it too, but
   the browser paints the scroll before the handler runs, so at the moment of
   pinning the bar was drawn a frame out of place and snapped. The browser's
   own sticky has no such gap. */
/* The confirmation, sitting IN the bar rather than in a band above it. Sized
   to its own text — full width it was a stripe across the page for one short
   sentence, and the row of links below it was a second stripe for two words. */
.gb-flash {
  flex: 0 1 auto; min-width: 0;
  margin: 0 auto 0 0;              /* hugs the left, links stay right */
  padding: 0.42rem 0.85rem;
  font-size: 0.84rem; line-height: 1.35;
  background: var(--bg);
}
/* Wraps to its own line on a narrow screen rather than crushing the links. */
@media (max-width: 640px) {
  .gb-flash { flex-basis: 100%; margin-right: 0; order: -1; }
}

/* Nothing on the left to give height to: the bar is just the help and
   sign-out links, so it should read as a thin strip. */
.gallery-bar--slim {
  padding-top: calc(0.85rem - (0.3rem * var(--gb-p)));
  padding-bottom: calc(0.85rem - (0.3rem * var(--gb-p)));
  margin-bottom: 1rem;
}
/* On the slim bar .gb-left is always empty — no count, no note, no compare
   banner while waiting. :empty did not match it (the view leaves whitespace
   between the PHP tags), so it still claimed a wrapped row of its own. */
.gallery-bar--slim .gb-left { display: none; }
/* The waiting screen's bar is one row: confirmation, then the links. */
.gallery-bar--slim { align-items: center; }
/* Anything that steps aside as the bar tightens. --gb-h is its measured
   natural height; height goes to 0 in step with the scroll rather than
   vanishing in one frame, and the fade finishes early (2x) so text is gone
   before the row is too short to hold it. */
.gb-shrink {
  height: calc(var(--gb-h, 0px) * (1 - var(--gb-p)));
  opacity: calc(1 - (2 * var(--gb-p)));
  overflow: hidden;
  pointer-events: none;
}
.gallery-bar:not(.is-stuck) .gb-shrink { pointer-events: auto; }
/* On a phone the full bar wraps to three lines and pinned it took a fifth
   of the screen. Keep the two actions worth pinning; the count, help link
   and sign-out come back the moment they scroll to the top. */
@media (max-width: 760px) {
  .gallery-bar .gb-right form {
    opacity: calc(1 - (2 * var(--gb-p)));
    max-width: calc(12rem * (1 - var(--gb-p)));
    overflow: hidden;
  }
  .gallery-bar.is-stuck .gb-right { width: 100%; justify-content: space-between; gap: 0.75rem; }
}
.gb-left { display: flex; align-items: baseline; gap: 0.9rem; flex-wrap: wrap; }
.gb-left h1 { font-size: clamp(1.4rem, 3vw, 2rem); }
.gb-count { font-size: 0.74rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-faint); }
.gb-right { display: flex; align-items: center; gap: 1.25rem; }
.pick-counter { font-size: 0.76rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
.pick-counter strong { font-family: var(--font-display); font-size: 1.05rem; color: var(--text); }
.pick-counter.is-full strong { color: var(--accent); }
.gallery-note { color: var(--text-muted); max-width: 60ch; margin: 0 0 1.75rem; }
/* Welcome note relocated into the gallery bar, sitting under the photo count.
   When a note is present .gb-left stacks (count above note); the note steps
   aside once the bar pins so the pinned toolbar stays compact. */
.gb-left.has-note { flex-direction: column; align-items: flex-start; gap: 0.3rem; }
.gb-note { margin: 0.1rem 0 0; font-size: 0.9rem; line-height: 1.5; }
/* Phase 2 guidance — set off from the studio's welcome so it reads as
   instructions, with a green edge tying it to the approve action. */
.approve-guide {
  max-width: 60ch; margin: 0 auto 2rem; padding: 0.9rem 1.25rem;
  font-size: 0.9rem; line-height: 1.55; color: var(--text-muted); text-align: center;
  background: rgba(var(--ink), 0.03);
  border: 1px solid var(--line); border-radius: 10px;
}
.approve-guide strong { color: var(--text); }

/* ---- Proof grid ------------------------------------------------------ */
/* Masonry. Columns rather than grid: mixing landscape and portrait in a
   row grid leaves ragged gaps, whereas columns let each photo take the
   height it wants and the next one closes up behind it. */
/* Masonry as explicit columns rather than CSS `columns`.
   Multi-column flows top-to-bottom down column one before starting column
   two, so a gallery sorted by file name read 001, 002, 003 straight down
   the left edge. The columns are filled round-robin in JS instead, which
   puts consecutive photos beside each other and keeps the ragged-bottom
   masonry look. */
.proof-grid {
  display: flex; align-items: flex-start;
  gap: clamp(0.5rem, 1.5vw, 1rem);
  padding-bottom: 3rem;
}
.pg-col {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column;
  gap: clamp(0.5rem, 1.5vw, 1rem);
}
/* Cap how large a preview can render. Thumbnails are 600px on the long edge,
   so a portrait is only ~400px wide — a column wider than that would upscale
   the image and soften it. Past the cap the grid keeps adding columns (up to
   COL_MAX in gallery.js) and centres whatever space is left over, instead of
   stretching a handful of columns fat. Mobile (< 640px) keeps its
   edge-to-edge columns, where a single column never exceeds native width. */
@media (min-width: 640px) {
  .proof-grid { justify-content: center; }
  /* Default 320px; the thumbnail-size slider raises/lowers this via --pg-col-max
     (set on the grid by gallery.js) so columns actually reach the chosen width. */
  .pg-col { max-width: var(--pg-col-max, 320px); }
}

/* ---- Approve phase: focused review of a few finals -------------------- */
/* gallery.js gives this grid fewer, wider columns (see isApprove); here we cap
   the block, centre it, and enlarge a lone final into a hero so the review
   never reads as empty. Widths sit near the finals' size to stay crisp. */
.approve-grid { max-width: min(100%, 1600px); margin-inline: auto; }
.approve-grid .pg-col { max-width: 460px; }
.approve-grid--single { max-width: 520px; }
.approve-grid--single .pg-col { max-width: 480px; }
/* Approving is THE task here, not a browse — so the Approve / Changes bar
   stays visible on every final rather than waiting for a hover. */
.approve-grid .proof-approve { opacity: 1; transform: none; }

/* ---- Thumbnail-size slider (client, sits above the proof grid) -------- */
.grid-tools { display: flex; justify-content: flex-end; margin: 0 0 1rem; }
.gsize { display: inline-flex; align-items: center; gap: 0.6rem; color: var(--text-faint); }
.gsize-ic { display: inline-flex; }
.gsize-ic svg { display: block; }
.gsize-ic--sm { opacity: 0.8; }
.gsize-range {
  -webkit-appearance: none; appearance: none;
  width: 118px; height: 4px; border-radius: 999px;
  background: var(--surface-3); outline: none; cursor: pointer;
}
.gsize-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--text); border: none; cursor: pointer;
}
.gsize-range::-moz-range-thumb {
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--text); border: none; cursor: pointer;
}
.gsize-range:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.proof {
  position: relative; background: var(--bg-2); overflow: hidden;
  display: block; width: 100%;
  /* Each proof is a <figure>, and the browser's default for one is
     `margin: 1em 40px` — that 40px pushed every photo out of its column
     and threw the whole grid off centre. Column gaps come from the flex
     `gap` above, so the figure itself wants none. */
  margin: 0;
}
.proof-open { display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; }
.proof img {
  /* aspect-ratio is set per image from its real dimensions, so nothing
     is cropped — the client judges the photo as shot. */
  display: block; width: 100%; height: auto; object-fit: cover;
  transition: opacity 0.25s ease, transform 0.5s ease;
}
.proof:hover img { opacity: 0.88; transform: scale(1.015); }
.proof.is-selected { outline: 3px solid var(--ok); outline-offset: -3px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok) 45%, transparent); }

.proof-pick {
  position: absolute; top: 0.5rem; right: 0.5rem;
  display: grid; place-items: center; width: 34px; height: 34px;
  border: none; border-radius: 50%; cursor: pointer;
  background: rgba(11,11,12,0.55); color: rgba(var(--ink), 0.85);
  backdrop-filter: blur(4px);
  opacity: 0; transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.proof:hover .proof-pick, .proof-pick:focus-visible { opacity: 1; }
/* A check reads by its stroke, so it is never filled. */
.proof.is-selected .proof-pick { opacity: 1; background: var(--ok); color: #fff; }
.proof.is-selected .proof-pick svg path { stroke-width: 2; }

/* Per-image retouch add-on: a pill at the foot of a chosen proof. Hidden
   until the photo is selected, since only a chosen photo can be retouched;
   lights up gold — the paid-extra motif — once it's marked. */
.proof-retouch {
  position: absolute; left: 0.5rem; bottom: 0.5rem;
  display: none; align-items: center; gap: 0.32rem;
  padding: 0.3rem 0.6rem; border: none; border-radius: 999px; cursor: pointer;
  font-size: 0.72rem; font-weight: 600; line-height: 1; letter-spacing: 0.01em;
  background: rgba(11,11,12,0.62); color: rgba(var(--ink), 0.9);
  backdrop-filter: blur(4px);
  transition: background 0.2s ease, color 0.2s ease;
}
.proof.is-selected .proof-retouch { display: inline-flex; }
.proof-retouch .pr-price { font-weight: 500; opacity: 0.78; }
.proof-retouch:hover { background: rgba(11,11,12,0.8); }
.proof.is-retouch .proof-retouch { background: var(--ok); color: #08130d; }
.proof.is-retouch .proof-retouch .pr-price { opacity: 0.9; }

/* ---- The mark + web downloads --------------------------------------- */
/* The mark (top-right) and a web-file download (top-left) on each photo,
   mirroring .proof-pick's placement + reveal-on-hover. An event gallery carries
   no selection/approve controls, so on one of those these own the corners. */
.proof-fav, .proof-dl {
  position: absolute; top: 0.5rem;
  display: grid; place-items: center; width: 34px; height: 34px;
  border: none; border-radius: 50%; cursor: pointer; text-decoration: none;
  background: rgba(11,11,12,0.55); color: rgba(var(--ink), 0.85);
  backdrop-filter: blur(4px);
  opacity: 0; transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.proof-fav { right: 0.5rem; }
.proof-dl  { left: 0.5rem; }
/* A gallery doing selections AND downloads puts two controls in the top-left
   corner: the download lands exactly on the remove button. Both are invisible
   until hover, so on a phone the client taps what looks like one and silently
   hits the other. The download drops to a second row.

   THERE IS NO SUCH RULE FOR THE MARK any more, because there is no longer a
   gallery that carries both: marking is off wherever selecting is on (see
   Gallery::allowsFavorites). Moving it to the opposite corner was tried first
   and was not enough — two identical checks on one photo are two identical
   checks wherever they sit, and only one of them moves the Submit count. */
.proof.has-pick .proof-dl { top: calc(0.5rem + 34px + 0.35rem); }
.proof:hover .proof-fav, .proof:hover .proof-dl,
.proof-fav:focus-visible, .proof-dl:focus-visible { opacity: 1; }
.proof-fav:hover, .proof-dl:hover { background: rgba(11,11,12,0.82); color: #fff; }
/* A marked photo keeps its check lit. Green, like every other checked box in
   the app, and never filled — a check reads by its stroke, and filling the
   path floods the triangle it implies into a solid wedge. */
.proof.is-fav .proof-fav { opacity: 1; background: var(--ok); color: #fff; }
.proof.is-fav .proof-fav svg path { fill: none; stroke: #fff; }

/* Toolbar "Download favorites" — a count pill like the cart link. */
.dl-fav-link {
  display: inline-flex; align-items: center; gap: 0.4rem;
  color: var(--text-muted); text-decoration: none; white-space: nowrap;
  transition: color 0.2s ease;
}
.dl-fav-link:hover { color: var(--text); }
.dl-fav-link .dl-fav-count {
  min-width: 1.5em; padding: 0.05rem 0.4em; border-radius: 999px; text-align: center;
  background: var(--ok); color: #fff; font-size: 0.72rem; font-weight: 600;
}
.dl-fav-link.is-empty { opacity: 0.5; cursor: not-allowed; }
.dl-fav-link.is-empty .dl-fav-count { background: var(--surface-3); color: var(--text-faint); }

/* The large preview's mark — mirrors .lb-check, lit green when marked. */
.lb-fav {
  position: absolute; top: 0.7rem; right: 0.7rem;
  display: grid; place-items: center; width: 42px; height: 42px;
  border: 1.5px solid rgba(255,255,255,0.7); border-radius: 50%; cursor: pointer;
  background: rgba(11,11,12,0.5); color: #fff;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.lb-fav:hover { background: rgba(11,11,12,0.72); }
.lb-fav:active { transform: scale(0.94); }
.lb-fav[aria-pressed="true"] { background: var(--ok); border-color: var(--ok); }
.lb-fav[aria-pressed="true"] svg path { fill: none; stroke: #fff; }

/* ---- Gallery master files (Wedding & Event admin panel) -------------- */
.masters-panel .master-up-form { margin: 0 0 1.25rem; }
.master-list { list-style: none; margin: 0.75rem 0 0.35rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.master-list li { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; background: var(--surface-2); border-radius: 8px; }
.master-list .ml-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.master-list .ml-size { color: var(--text-faint); font-size: 0.8rem; white-space: nowrap; }
.master-list form, .master-purge form { margin: 0; }
.master-total { font-size: 0.82rem; color: var(--text-muted); margin: 0.5rem 0 1.1rem; }
.master-send { margin: 0 0 1rem; padding: 1.1rem 1.25rem; }
.master-send .np-buttons { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: 0.25rem; }
.master-purge { margin: 0; }

/* Folder-aware master uploader: drop a folder/files, or click to choose. */
.gm-upload { margin: 0 0 1.25rem; }
.gm-dropzone {
  border: 1.5px dashed var(--line-strong); border-radius: 12px;
  padding: 1.6rem 1.25rem; text-align: center; cursor: pointer; color: var(--text-muted);
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.gm-dropzone:hover, .gm-dropzone:focus-visible { border-color: var(--text-faint); color: var(--text); outline: none; }
.gm-dropzone.is-over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); color: var(--text); }
.gm-dropzone.is-busy { opacity: 0.7; pointer-events: none; }
.gm-dropzone svg { color: var(--text-faint); margin-bottom: 0.2rem; }
.gm-dz-lead { margin: 0.1rem 0 0.15rem; font-weight: 500; color: var(--text); }
.gm-dz-sub { margin: 0 0 0.8rem; font-size: 0.82rem; }
.gm-picks { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; }
.gm-status { margin: 0.6rem 0 0; font-size: 0.82rem; color: var(--text); min-height: 1em; }
.master-list .ml-folder { color: var(--text-faint); }

/* Automatic "additional retouched image" tag on picks beyond the allowance —
   always green (a paid extra), not a toggle. It replaces the retouch pill on
   those photos, since an extra is a retouched headshot by definition. */
.proof-extra {
  position: absolute; left: 0.5rem; bottom: 0.5rem;
  display: none; align-items: center; gap: 0.32rem;
  padding: 0.3rem 0.6rem; border-radius: 999px;
  font-size: 0.72rem; font-weight: 600; line-height: 1; letter-spacing: 0.01em;
  background: var(--ok); color: #08130d; backdrop-filter: blur(4px);
}
.proof.is-extra .proof-extra { display: inline-flex; }
.proof.is-extra .proof-retouch { display: none; }
.proof-extra .pr-price { font-weight: 500; opacity: 0.9; }

/* ---- Final-edit approval mode ---------------------------------------- */
/* Progress reads like the selections "Select, review & submit" pill: a
   rounded, bordered badge with a check that greens once every photo has a
   verdict. */
.approve-progress {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.38rem 0.85rem 0.38rem 0.45rem;
  background: var(--surface-1); border: 1px solid var(--line-strong); border-radius: 100px;
  font-size: 0.74rem; letter-spacing: 0.03em; color: var(--text-muted); white-space: nowrap;
  line-height: 1.2;
}
.ap-check {
  display: grid; place-items: center; width: 22px; height: 22px; flex: 0 0 auto;
  border-radius: 50%; background: var(--text); color: var(--bg);
}
.approve-progress .ap-count strong { font-weight: 600; color: var(--text); }
.approve-progress.is-complete { border-color: rgba(47, 125, 85, 0.6); }
.approve-progress.is-complete .ap-check { background: var(--ok); color: #fff; }
.approve-progress.is-complete .ap-count strong { color: var(--ok); }

.approve-send-form { margin: 0; }
/* Sits beside the progress pill, so it wants to be compact rather than the
   full-width primary button it is elsewhere. */
.approve-send.btn { width: auto; padding: 0.5rem 1rem; }

/* Verdict ribbon, top-left, mirrored by the photo's outline. */
.proof-verdict {
  position: absolute; top: 0.5rem; left: 0.5rem; z-index: 2;
  padding: 0.22rem 0.5rem; border-radius: 100px;
  font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: #fff; opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
}
.proof.av-approved .proof-verdict { opacity: 1; background: var(--ok); }
.proof.av-changes  .proof-verdict { opacity: 1; background: #b0651a; }
.proof.av-approved { outline: 2px solid var(--ok);   outline-offset: -2px; }
.proof.av-changes  { outline: 2px solid #b0651a;      outline-offset: -2px; }

/* Server-mode master delivery: a download pill on an approved final (top-right,
   opposite the "Approved" badge). Hidden until the photo is approved. */
.proof-master-dl {
  position: absolute; top: 0.5rem; right: 0.5rem; z-index: 3;
  display: none; align-items: center; gap: 0.3rem;
  padding: 0.3rem 0.6rem; border-radius: 100px;
  background: var(--ok); color: #08130d; text-decoration: none;
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
}
.proof.av-approved .proof-master-dl { display: inline-flex; }
.proof-master-dl:hover { filter: brightness(1.07); }
.proof-master-dl svg { display: block; }

/* Approve / request-changes bar, revealed on hover (always on for touch). */
.proof-approve {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; display: flex; gap: 1px;
  opacity: 0; transform: translateY(4px); transition: opacity 0.2s ease, transform 0.2s ease;
}
.proof:hover .proof-approve, .proof:focus-within .proof-approve { opacity: 1; transform: none; }
.pa-btn {
  flex: 1 1 0; border: 0; cursor: pointer; padding: 0.5rem 0.4rem;
  font-size: 0.72rem; letter-spacing: 0.04em; color: #fff;
  background: rgba(11, 11, 12, 0.62); backdrop-filter: blur(4px);
}
.pa-btn:hover { background: rgba(11, 11, 12, 0.82); }
.pa-approve.is-on { background: var(--ok); }
.pa-changes.is-on { background: #b0651a; }
@media (max-width: 620px) { .proof-approve { opacity: 1; transform: none; } }

/* Change-requested photos show an always-visible "withdraw" action at the top,
   in place of the passive "Changes requested" badge, so it's obvious the client
   can clear the request once its revision is approved — no hover needed. The
   bottom bar keeps just Approve / Changes. */
.proof.av-changes .proof-verdict { display: none; }
/* "Click to compare with previous version" — on a revised photo only. Held
   back until hover so a grid of finals stays a grid of finals; it is an offer,
   not an instruction. It replaced Withdraw, which existed only because BOTH
   versions used to be shown at once. */
/* Sits just above the Approve / Changes bar, where the other things you can DO
   to this photo live. The top-left corner is the verdict badge's, and
   full-width up there the two collided.
   It is a label for the whole tile as much as a button: on a revised photo,
   clicking the PHOTO opens the comparison too (see gallery.js). A thin strip
   at the bottom is easy to miss, and the photo above it is the obvious thing
   to click — which used to open the plain lightbox instead. */
.proof-compare {
  position: absolute; left: 0.5rem; right: 0.5rem; bottom: 2.6rem; z-index: 4;
  border: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  padding: 0.45rem 0.5rem; border-radius: 100px;
  font-family: inherit;
  font-size: 0.64rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: #1a1a1c; background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 2px 10px rgba(26, 26, 28, 0.22);
  opacity: 0; transform: translateY(4px);
  transition: opacity 0.16s ease, transform 0.16s ease;
}
.proof-compare svg { flex: none; }
.proof:hover .proof-compare,
.proof-compare:focus-visible { opacity: 1; transform: none; }
/* The whole photo is the target, so say so with the cursor. */
.proof:has(.proof-compare) .proof-open { cursor: zoom-in; }
.proof-compare:hover { background: #fff; }
/* No hover state on touch, so it simply stays put. */
@media (hover: none) {
  .proof-compare { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .proof-compare { transition: none; }
}

/* Request-changes modal reuses the selections modal shell. */
.approve-inner { max-width: 520px; }
.approve-note-field {
  width: 100%; resize: vertical; min-height: 6rem; font: inherit;
  margin-top: 0.4rem; padding: 0.6rem 0.7rem;
  color: var(--text); background: var(--surface-3);
  border: 1px solid var(--line-strong); border-radius: 10px;
}
.approve-note-field:focus { outline: none; border-color: var(--text); }
.approve-modal-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1rem; }


/* ---- Admin light mode --------------------------------------------------
   Chosen in Settings and applied as a class on <body>. Everything else in
   the admin reads from these variables, so switching the palette switches
   the whole interface — the only rules that needed changing first were the
   handful that had baked the dark theme's ink into a literal.

   The values match the client-facing light theme, so the two halves of the
   product look like one product. */
body.admin.is-light {
  --ink:         26, 26, 28;
  --btn-primary-hover: #000000;

  --bg:          #e0dcd3;
  --bg-2:        #e1dfd9;
  --bg-3:        #d9d7d0;

  /* Warm light greys rather than pure white, so panels sit softly on the
     canvas instead of glaring against it — pitched a touch deeper at both
     ends so the whole surface reads less washed out. The canvas behind panels
     runs deeper than the panels themselves so cards read as raised. */
  --surface-0:   #e0dcd3;   /* app canvas behind panels */
  --surface-1:   #f1efea;   /* cards, panels */
  --surface-2:   #e6e4de;   /* raised: table headers, hover */
  --surface-3:   #f5f3ee;   /* highest: inputs, active rows */
  --sidebar:     #edeae4;

  --text:        #1a1a1c;
  --text-muted:  rgba(26, 26, 28, 0.66);
  /* Deeper than the client theme's 0.45: dark ink at 45% on white lands
     at 2.8:1, below where the dark theme's faint text sits. 0.55 brings
     it to 3.9:1 while still reading as secondary. */
  --text-faint:  rgba(26, 26, 28, 0.55);
  --line:        rgba(26, 26, 28, 0.12);
  --line-strong: rgba(26, 26, 28, 0.28);
  /* Brand gold for light mode. Brighter than the ink-safe shade it replaced,
     for a warmer highlight (used on links, "pending payment" badges, etc.). */
  --accent:      #c2a344;
  --ok:          #2f7d55;
  --err:         #b0473f;
  --status-blue:   #2f6f9e;
  --status-purple: #6a4fa3;
  --status-orange: #b5651d;
}

/* Light-mode status badges: soft tinted pills. The status colour carries
   the text and the stroke, and the fill is a low-saturation wash of that
   same colour (a ~16% tint over the surface). Grouped by colour so every
   order/ticket/gallery badge matches. */
body.admin.is-light .badge-live,
body.admin.is-light .badge-sr-resolved,
body.admin.is-light .badge-order-paid            { color: var(--ok); border-color: var(--ok); background: color-mix(in srgb, var(--ok) 16%, transparent); }
body.admin.is-light .badge-pre_registration,
body.admin.is-light .badge-sr-new,
body.admin.is-light .badge-order-pending_payment { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
body.admin.is-light .badge-sr-open,
body.admin.is-light .badge-order-editing,
body.admin.is-light .badge-order-in_production   { color: var(--status-blue); border-color: var(--status-blue); background: color-mix(in srgb, var(--status-blue) 16%, transparent); }
body.admin.is-light .badge-order-waiting_changes { color: var(--status-orange); border-color: var(--status-orange); background: color-mix(in srgb, var(--status-orange) 16%, transparent); }
body.admin.is-light .badge-sr-waiting,
body.admin.is-light .badge-order-shipped,
body.admin.is-light .badge-order-submitted_approval,
body.admin.is-light .badge-order-ready_for_pickup { color: var(--status-purple); border-color: var(--status-purple); background: color-mix(in srgb, var(--status-purple) 16%, transparent); }
body.admin.is-light .badge-expired,
body.admin.is-light .badge-archived,
body.admin.is-light .badge-order-cancelled,
body.admin.is-light .badge-order-refunded        { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 16%, transparent); }
body.admin.is-light .badge-draft,
body.admin.is-light .badge-sr-closed,
body.admin.is-light .badge-order-completed       { color: var(--text-muted); border-color: var(--text-muted); background: color-mix(in srgb, var(--text-muted) 16%, transparent); }
/* Explicit-colour badges last, so a recoloured status keeps its own colour. */
body.admin.is-light .badge.badge-c               { color: var(--badge-c); border-color: var(--badge-c); background: color-mix(in srgb, var(--badge-c) 16%, transparent); }

/* ---- Lightbox -------------------------------------------------------- */
.lightbox {
  position: fixed; inset: 0; z-index: 90;   /* above the review modal (70), so
                                               a thumbnail preview opened from it
                                               shows on top */
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  background: rgba(8,8,9,0.97); padding: clamp(0.5rem, 3vw, 2rem);
}
.lightbox[hidden] { display: none; }
.lb-stage { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 1rem; min-width: 0; }
.lb-stage img { max-width: 100%; max-height: 78vh; object-fit: contain; }
/* The grid's select checkmark, mirrored onto the large preview (top-left so it
   clears the close button). Always visible here — it's a primary action in the
   preview — and turns green when the photo is selected, like the grid. */
.lb-imgwrap { position: relative; display: inline-block; max-width: 100%; line-height: 0; }
.lb-check {
  position: absolute; top: 0.7rem; right: 0.7rem;
  display: grid; place-items: center; width: 42px; height: 42px;
  border: 1.5px solid rgba(255, 255, 255, 0.7); border-radius: 50%; cursor: pointer;
  background: rgba(11, 11, 12, 0.5); color: #fff;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.lb-check:hover { background: rgba(11, 11, 12, 0.72); }
.lb-check:active { transform: scale(0.94); }
.lb-check[aria-pressed="true"] { background: var(--ok); border-color: var(--ok); color: #fff; }
.lb-check[aria-pressed="true"] svg path { stroke-width: 2.4; }
.lb-caption { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 1.25rem; font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
/* Phase 2 large-preview verdict controls: the tile's Approve / Changes,
   mirrored into the lightbox so a final can be signed off while viewed big.
   Withdraw reuses the caption's .btn styling; the pills echo the tile's
   green-approved / orange-changes language on the dark backdrop. */
/* Approve / Changes mirrored onto the BOTTOM of the large preview image, like
   the thumbnail tile's bar — always visible here since it's the primary action
   in the approve view. Rounded bottom corners follow the image's radius; the
   two chips split the width 50/50 with a 1px hairline between (the tile look).
   Withdraw (shown only mid-change) rides the top of the cluster on its own row. */
.lb-approve {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  display: flex; flex-wrap: wrap; gap: 1px; overflow: hidden;
  border-bottom-left-radius: var(--radius-img);
  border-bottom-right-radius: var(--radius-img);
}
.lb-pa {
  flex: 1 1 0; border: 0; cursor: pointer; padding: 0.62rem 0.4rem;
  font-family: inherit; font-size: 0.74rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: #fff; background: rgba(11, 11, 12, 0.62); backdrop-filter: blur(4px);
  transition: background 0.2s ease;
}
.lb-pa:hover { background: rgba(11, 11, 12, 0.82); }
.lb-pa-approve.is-on { background: var(--ok); }
.lb-pa-changes.is-on { background: #b0651a; }
.lb-close {
  position: absolute; top: 1rem; right: 1.25rem;
  background: none; border: none; color: var(--text-muted);
  font-size: 2rem; line-height: 1; cursor: pointer;
}
.lb-close:hover { color: var(--text); }
.lb-nav {
  background: none; border: none; color: var(--text-muted);
  font-size: 3rem; line-height: 1; padding: 0 clamp(0.25rem, 2vw, 1.5rem); cursor: pointer;
}
.lb-nav:hover { color: var(--text); }
.lb-pick-on { background: var(--text); color: var(--bg); border-color: var(--text); }

/* ---- Notice card ----------------------------------------------------- */
.notice-card { max-width: 480px; margin: clamp(3rem, 12vh, 8rem) auto; text-align: center; }
.notice-card h1 { font-size: clamp(1.6rem, 4vw, 2.4rem); margin-bottom: 1rem; }
.notice-card p { margin: 0.5rem 0; }
.notice-card .btn { margin-top: 1.5rem; }

@media (max-width: 620px) {
  .gate-row { flex-direction: column; }
  .proof-pick { opacity: 1; }
}

/* =====================================================================
   Client-facing LIGHT theme
   Photos are judged against a light neutral surface — the proofing
   convention — while the photographic hero keeps its dark treatment.
   Tokens are re-declared on body.client so existing components adapt.
   ===================================================================== */
/* An invoice is a document, not a screen: it borrows the light palette so
   it reads on paper as well as on the dark admin. */
body.invoice-page,
body.client {
  --ink:         26, 26, 28;
  --btn-primary-hover: #000000;
  --bg:          #f2f1ef;   /* very light warm gray */
  --bg-2:        #eae9e6;
  --bg-3:        #e3e2de;
  --surface-1:   #ffffff;
  --surface-2:   #f7f6f4;
  --surface-3:   #ffffff;

  --text:        #1a1a1c;
  --text-muted:  rgba(26, 26, 28, 0.66);
  --text-faint:  rgba(26, 26, 28, 0.45);
  --line:        rgba(26, 26, 28, 0.12);
  --line-strong: rgba(26, 26, 28, 0.30);
  --ok:          #2f7d55;
  --err:         #b0473f;

  background: var(--bg);
  color: var(--text);
}

body.client .client-head,
body.client .client-foot { background: var(--bg); }

/* The sign-in panel sits on the light page, so the form uses the normal
   light-theme controls — no scrim, no overlay treatment. */
body.client .gate-form input { background: var(--surface-1); }
body.client .gate-form input::placeholder { color: var(--text-faint); }

/* ---- Gallery grid on light ----------------------------------------- */
body.client .gallery-bar { border-bottom-color: var(--line); }
body.client .proof {
  background: var(--bg-2);
  box-shadow: 0 1px 2px rgba(26, 26, 28, 0.06);
}
body.client .proof.is-selected { outline-color: var(--ok); }
body.client .proof-pick {
  background: rgba(255, 255, 255, 0.78);
  color: #1a1a1c;
}
/* Selected badge sits lightly over the photo rather than stamping on it —
   the blur keeps the check legible against busy areas. */
body.client .proof.is-selected .proof-pick {
  background: var(--ok);
  color: #fff;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* ---- Lightbox on light --------------------------------------------- */
/* Slightly transparent so the grid stays visible behind the preview. */
body.client .lightbox { background: rgba(242, 241, 239, 0.84); }
body.client .lb-stage img { box-shadow: 0 6px 34px rgba(26, 26, 28, 0.18); }
body.client .lb-close,
body.client .lb-nav { color: var(--text-faint); }
body.client .lb-close:hover,
body.client .lb-nav:hover { color: var(--text); }
body.client .lb-pick-on { background: #1a1a1c; color: #f4f1ec; border-color: #1a1a1c; }

/* ---- Toast/flash on light ------------------------------------------ */
body.client .flash { background: var(--surface-1); }
body.client .flash.toast {
  position: fixed; top: 1rem; left: 50%; transform: translateX(-50%);
  z-index: 60; box-shadow: 0 4px 20px rgba(26, 26, 28, 0.18);
}

/* ---- Tabs (gallery detail) ------------------------------------------ */
.tabs {
  display: flex; gap: 0.25rem;
  border-bottom: 1px solid var(--line);
  margin-bottom: 1.5rem;
}
.tab {
  background: none; border: none; cursor: pointer;
  padding: 0.7rem 1.1rem; margin-bottom: -1px;
  font-family: inherit; font-size: 0.76rem;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint);
  border-bottom: 2px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.tab:hover { color: var(--text-muted); }
.tab.is-active { color: var(--text); border-bottom-color: var(--text); }
.tab .count { margin-left: 0.35rem; }
/* Delete is a tab like the others — same size, same row — but it is the one
   that cannot be undone, so it says so on hover and while it is open rather
   than sitting there in red waiting to be misread as an error. */
.tab-danger:hover { color: var(--err); }
.tab-danger.is-active { color: var(--err); border-bottom-color: var(--err); }
/* A line of orientation at the top of a panel that has no heading of its own. */
.tab-lead { margin: 0 0 1rem; }

/* ---- Galleries: grid or list --------------------------------------------
   Both are rendered and one is hidden, so switching is a class rather than a
   reload — the filters, the scroll position and the back button all survive. */
.gviews.is-grid .table-wrap { display: none; }
.gviews.is-list .gcards { display: none; }

.view-switch { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.vs-btn {
  display: grid; place-items: center; width: 2.1rem; height: 2.1rem;
  background: none; border: 0; cursor: pointer; color: var(--text-faint);
  transition: background 0.15s ease, color 0.15s ease;
}
.vs-btn + .vs-btn { border-left: 1px solid var(--line); }
.vs-btn:hover { color: var(--text-muted); }
.vs-btn.is-on { background: var(--surface-3); color: var(--text); }

.gcards {
  display: grid; gap: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
/* The card is one link with one control parked on top of it — a <button> may
   not live inside an <a>, and the whole card being clickable is the point. */
.gcard {
  position: relative; display: flex; flex-direction: column;
  background: var(--surface-1); border: 1px solid var(--line); border-radius: 10px;
  overflow: hidden; transition: border-color 0.15s ease, transform 0.15s ease;
}
.gcard:hover { border-color: var(--text-faint); }
.gcard-hit { display: flex; flex-direction: column; text-decoration: none; color: inherit; }
.gcard-cover { display: block; aspect-ratio: 4 / 3; background: var(--surface-3); overflow: hidden; }
/* DIRECT child only. The blank cover's brand mark is also an <img> inside
   .gcard-cover, and a plain descendant rule stretched the lockup to fill the
   whole card — object-fit: cover on a wordmark is a smear. */
.gcard-cover > img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
/* A portrait cover keeps its head. See the note in the view. */
.gcard-cover > img.is-tall { object-position: center 15%; }
/* No cover chosen: the studio's mark and the gallery's name, not a grey box. */
.gcard-blank {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.7rem; height: 100%; padding: 1rem; text-align: center; background: var(--surface-2);
}
.gcard-mark { display: block; width: auto; max-width: 8rem; height: auto; opacity: 0.8; }
.gcard-blank-title { font-family: var(--font-display); font-size: 1rem; color: var(--text-muted); line-height: 1.3; }

.gcard-body { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.8rem 0.85rem 0.7rem; }
.gcard-title {
  font-weight: 500; font-size: 0.95rem; color: var(--accent); line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gcard-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0.05rem 0 0.15rem; }
.gcard-line {
  display: flex; justify-content: space-between; gap: 0.6rem;
  font-size: 0.78rem; color: var(--text-muted);
}
.gcard-line > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gcard-line > span + span { text-align: right; flex: none; max-width: 55%; }
.gcard-foot { margin-top: 0.45rem; }
/* Sits over the card's own link, bottom-right, where the screenshot's kebab is. */
.gcard-copy {
  position: absolute; right: 0.6rem; bottom: 0.6rem; z-index: 2;
  background: none; border: 0; cursor: pointer; padding: 0.2rem 0.3rem;
  font: inherit; font-size: 0.74rem; color: var(--text-faint);
}
.gcard-copy:hover { color: var(--text); }

/* WHAT KIND OF VISITOR. Three words, one column, so a send can be aimed:
   the Client is the person the gallery is for, a Team Member is named on a
   member gallery beneath a hub, and a Guest is everyone else who pre-registered
   or typed the password. Quiet by design — it is a fact about a row, not a
   status anyone has to act on. */
.vkind {
  display: inline-block; font-size: 0.68rem; letter-spacing: 0.08em;
  text-transform: uppercase; white-space: nowrap;
  padding: 0.12rem 0.45rem; border-radius: 3px;
  background: var(--surface-3); color: var(--text-muted);
}
.vkind-client { background: rgba(var(--ink), 0.1); color: var(--text); font-weight: 600; }
.vkind-member { background: rgba(58, 118, 172, 0.16); color: #3a76ac; }
/* Beside the kind, not instead of it: buying cuts across all three. Gold, the
   same motif every paid thing in this app already uses. */
.vkind-customer { background: rgba(201, 162, 39, 0.18); color: #9a7a14; margin-left: 0.25rem; }

/* The people table used to be a tab of its own; now it sits under the release
   email inside Communicate, with a rule between them. */
.visitors-block { margin-top: 1.75rem; border-top: 1px solid var(--line); padding-top: 1.5rem; }

/* The gallery's row is EIGHT tabs, where every other tab row in the app is
   three or four. Tightened just enough that the set fits on one line on a
   laptop — at the old padding it came to 1131px in a 1120px column, so Delete
   dropped to a line of its own — and scoped to that row so the shorter rows
   elsewhere keep the roomier spacing they were drawn with. It still wraps when
   the window is genuinely too narrow, rather than scrolling sideways and
   hiding the last two. */
.tabs { flex-wrap: wrap; }
.tabs[data-tabs] .tab { padding-left: 0.8rem; padding-right: 0.8rem; letter-spacing: 0.1em; }

/* The Photos tab's live progress: a short bar INLINE, beside the count.
   It started as a sliver along the tab's bottom edge and that was wrong — the
   active tab is marked with a 2px bottom border, so a coloured line under an
   inactive tab read as "this one is selected". Beside the word, it cannot be
   mistaken for anything else. */
.tab-prog {
  display: inline-block; vertical-align: middle;
  width: 2.6rem; height: 4px; margin-left: 0.45rem;
  overflow: hidden; border-radius: 2px; background: var(--surface-3);
}
.tab-prog > i { display: block; height: 100%; width: 0; border-radius: 2px; }
/* Measured: warming knows exactly how many are left. */
.tab-prog.is-preparing > i { background: var(--ok); transition: width 0.35s ease; }
/* Indeterminate: nothing declares how many photos an export will send, so this
   travels rather than fills. Claiming a percentage here would be inventing one. */
.tab-prog.is-arriving > i {
  background: var(--accent); width: 35%;
  animation: tab-prog-slide 1.5s ease-in-out infinite;
}
@keyframes tab-prog-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(300%); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-prog.is-arriving > i { animation: none; width: 100%; opacity: 0.5; }
}
/* "uploading…" / "12 new — reload to see them". Lower case and small: it is a
   footnote to the number, not a second heading. */
.tab-note {
  margin-left: 0.4rem; font-size: 0.68rem; letter-spacing: 0.04em;
  text-transform: none; color: var(--text-faint);
}

/* Setup state on the tabs. Declared after .is-active so the status colour
   still shows on whichever tab is currently open. */
.tab.has-todo, .tab.has-todo:hover { color: var(--accent); }
.tab.has-todo.is-active { border-bottom-color: var(--accent); }
.tab.is-ready, .tab.is-ready:hover { color: var(--ok); }
.tab.is-ready.is-active { border-bottom-color: var(--ok); }
.tab.is-ready .count { color: var(--ok); }
.tab-ok {
  margin-left: 0.35rem; font-size: 0.78rem; line-height: 1;
  color: var(--ok);
}

/* ---- Rounded image corners ------------------------------------------ */
:root { --radius-img: 7px; }

/* Admin photo grid */
.photo { border-radius: var(--radius-img); }
.photo img { border-top-left-radius: var(--radius-img); border-top-right-radius: var(--radius-img); }

/* Client proof grid + lightbox */
.proof { border-radius: var(--radius-img); }
.proof img { border-radius: var(--radius-img); }
.lb-stage img { border-radius: var(--radius-img); }

/* ---- Client header lockup ------------------------------------------- */
.brand-lockup {
  display: block;
  height: 46px; width: auto;   /* aspect preserved from the artwork */
}
@media (max-width: 620px) {
  .brand-lockup { height: 38px; }
}
/* An uploaded logo replaces the built-in artwork; contain it so any aspect
   ratio fits its slot, and cap the lockup so a wide logo can't run away. */
.brand-custom { object-fit: contain; }
.brand-lockup.brand-custom { max-width: 260px; }

/* Current-logo preview in Settings. */
.logo-current { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.7rem; flex-wrap: wrap; }
.logo-preview {
  max-height: 54px; max-width: 200px; object-fit: contain;
  padding: 8px 10px; background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 8px;
}
.logo-preview.is-icon { max-width: 54px; }
/* Preview each variant on the background it is designed for, so light
   artwork and dark artwork both read in the settings panel. */
.logo-preview.on-dark  { background: #1a1a1c; border-color: rgba(255, 255, 255, 0.14); }
.logo-preview.on-light { background: #ffffff; border-color: rgba(0, 0, 0, 0.14); }

/* =====================================================================
   Store: cart, checkout, order tracking
   ===================================================================== */
.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;
}

.cart-link {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.76rem; letter-spacing: 0.1em; text-transform: uppercase;
  text-decoration: none; color: var(--text-muted);
}
.cart-link:hover { color: var(--text); }
.cart-count {
  display: inline-grid; place-items: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 100px; background: #1a1a1c; color: #f4f1ec;
  font-size: 0.68rem; letter-spacing: 0;
}

/* ---- Two-column order layout ---------------------------------------- */
.order-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) 320px;
  gap: clamp(1.25rem, 3vw, 2.5rem); align-items: start;
  padding-bottom: 4rem;
}
@media (max-width: 860px) { .order-layout { grid-template-columns: 1fr; } }

.panel-light {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.5rem; margin-bottom: 1.25rem;
}
.panel-light h2 { font-size: 1.15rem; margin-bottom: 1rem; }

/* ---- Order lines ----------------------------------------------------- */
.order-lines { display: flex; flex-direction: column; }
.order-line {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto auto auto;
  gap: 1rem; align-items: center;
  padding: 0.9rem 0; border-bottom: 1px solid var(--line);
}
.order-line:last-child { border-bottom: none; }

/* A built package on the cart/checkout lines — a bundle with no per-line qty. */
.order-pkg { grid-template-columns: 64px minmax(0, 1fr) auto auto; align-items: flex-start; }
.ol-pkg-tag { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); border: 1px solid var(--line-strong); border-radius: 6px; padding: 0.05rem 0.4rem; margin-left: 0.35rem; vertical-align: middle; }
.ol-pkg-contents { list-style: none; margin: 0.45rem 0 0; padding: 0; font-size: 0.8rem; color: var(--text-muted); }
.ol-pkg-contents li { margin: 0.12rem 0; }
.ol-pkg-extra { color: var(--text); }
.ol-pkg-extra-price { color: var(--text-muted); margin-left: 0.25rem; }
.ol-thumb {
  width: 64px; height: 80px; object-fit: cover;
  border-radius: calc(var(--radius-img) - 2px); background: var(--bg-2);
}
.ol-thumb-empty { display: block; }
.ol-info { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.ol-name { font-size: 0.95rem; }
.ol-sub { font-size: 0.78rem; color: var(--text-faint); }
.ol-unit { font-size: 0.76rem; color: var(--text-muted); }
.ol-qty { display: flex; align-items: center; gap: 0.4rem; }
.ol-qty input { width: 62px; text-align: center; padding: 0.4rem; }
.ol-total { font-size: 0.95rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 620px) {
  .order-line { grid-template-columns: 56px minmax(0,1fr); row-gap: 0.5rem; }
  .ol-qty, .ol-total, .ol-remove { grid-column: 2; justify-self: start; }
}

/* ---- Summary --------------------------------------------------------- */
.order-summary {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.5rem; position: sticky; top: 1.5rem;
}
.order-summary h2 { font-size: 1.15rem; margin-bottom: 1rem; }
.sum-items { list-style: none; margin: 0 0 1rem; padding: 0 0 1rem; border-bottom: 1px solid var(--line); }
.sum-items li { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.85rem; color: var(--text-muted); padding: 0.2rem 0; }
.sum { display: grid; grid-template-columns: 1fr auto; gap: 0.45rem 1rem; margin: 0 0 1.25rem; font-size: 0.9rem; }
.sum dt { color: var(--text-muted); }
.sum dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.sum .grand {
  padding-top: 0.6rem; border-top: 1px solid var(--line);
  font-family: var(--font-display); font-size: 1.15rem; color: var(--text);
}
.checkout-form { min-width: 0; }
.checkout-actions { margin-top: 0.5rem; }
.checkout-actions .hint { margin-top: 0.75rem; }
button[disabled] { opacity: 0.5; cursor: not-allowed; }

/* ---- Order tracker --------------------------------------------------- */
.track { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 1.5rem; }
.track li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--text-faint); }
.track-dot {
  width: 10px; height: 10px; border-radius: 50%;
  border: 1px solid var(--line-strong); background: transparent;
}
.track li.is-done { color: var(--text); }
.track li.is-done .track-dot { background: var(--text); border-color: var(--text); }
.track-when { color: var(--text-faint); font-size: 0.74rem; }
.status-flat { margin: 0; color: var(--text-muted); }
.tracking { margin: 1rem 0 0; font-size: 0.86rem; color: var(--text-muted); }
.ship-to { margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.ship-to h3 { font-family: var(--font-body); font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); margin: 0 0 0.5rem; }
.ship-to p { margin: 0; font-size: 0.86rem; color: var(--text-muted); line-height: 1.5; }

/* ---- Order sheet (product picker over the lightbox) ------------------ */
.order-sheet {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  background: rgba(26, 26, 28, 0.45); padding: 1rem;
}
.order-sheet[hidden] { display: none; }

/* ---- Print package: offer banner + build wizard (Portrait Order-prints) ---- */
.pkg-offer {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem 1.5rem;
  flex-wrap: wrap; margin: 0 0 1.5rem; padding: 1.1rem 1.4rem;
  border: 1px solid var(--line-strong); border-radius: 14px; background: var(--surface-1);
}
.pkg-offer-info h2 { margin: 0; font-size: 1.15rem; }
.pkg-offer-price { color: var(--text-muted); font-weight: 500; margin-left: 0.4rem; }
.pkg-offer-desc { margin: 0.3rem 0 0; color: var(--text-muted); font-size: 0.9rem; }
.pkg-offer-slots { margin: 0.3rem 0 0; color: var(--text-faint); font-size: 0.85rem; }
.pkg-offer-built { margin: 0.45rem 0 0; color: var(--ok); font-size: 0.85rem; }

.pkg-wizard { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; background: rgba(26,26,28,0.45); padding: 1rem; }
.pkg-wizard[hidden] { display: none; }
.pkw-inner {
  position: relative; background: var(--surface-1); color: var(--text); border-radius: 16px;
  width: 100%; max-width: 720px; max-height: 90vh; display: flex; flex-direction: column;
  overflow: hidden; box-shadow: 0 24px 70px rgba(0,0,0,0.4);
}
.pkw-head { padding: 1.4rem 1.5rem 0.5rem; }
.pkw-head h2 { margin: 0; font-size: 1.3rem; }
.pkw-sub { margin: 0.3rem 0 0; color: var(--text-muted); font-size: 0.9rem; }
.pkw-body, .pkw-picker { padding: 0.6rem 1.5rem 1rem; overflow-y: auto; }
.pkw-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.7rem; margin: 0.6rem 0 1rem; }
.pkw-slot {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem; text-align: center;
  padding: 0.7rem; border: 1px solid var(--line); border-radius: 12px; cursor: pointer;
  background: var(--surface-2); transition: border-color 0.12s;
}
.pkw-slot:hover { border-color: var(--line-strong); }
.pkw-slot.is-filled { border-color: var(--ok); }
.pkw-slot-num { align-self: flex-start; font-size: 0.7rem; color: var(--text-faint); }
.pkw-slot-thumb { width: 100%; aspect-ratio: 1; border-radius: 8px; background: var(--surface-3) center/cover no-repeat; display: block; }
.pkw-slot-name { font-size: 0.85rem; font-weight: 500; }
.pkw-slot-status { font-size: 0.75rem; color: var(--text-faint); }
.pkw-slot.is-filled .pkw-slot-status { color: var(--ok); }
.pkw-extras { border-top: 1px solid var(--line); padding-top: 0.9rem; }
.pkw-extras h3 { margin: 0 0 0.6rem; font-size: 0.95rem; }
.pkw-extra-pct { color: var(--ok); font-size: 0.8rem; margin-left: 0.3rem; }
.pkw-extra-rows { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.6rem; }
.pkw-extra-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.pkw-extra-row .select-wrap { flex: 1 1 12rem; }
.pkw-extra-qty { width: 4.5rem; }
.pkw-extra-photo.has-photo { color: var(--ok); border-color: var(--ok); }
.pkw-picker-head { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 0.7rem; }
.pkw-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 0.5rem; }
.pkw-photo { padding: 0; border: 2px solid transparent; border-radius: 8px; overflow: hidden; cursor: pointer; background: none; }
.pkw-photo:hover { border-color: var(--accent); }
.pkw-photo img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }
.pkw-foot { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; padding: 0.9rem 1.5rem; border-top: 1px solid var(--line); background: var(--surface-2); }
.pkw-total-label { color: var(--text-muted); font-size: 0.85rem; }
.pkw-total { font-size: 1.2rem; font-weight: 600; margin-right: auto; }
.pkw-status:not(:empty) { width: 100%; margin: 0; font-size: 0.8rem; color: var(--err); }
/* Wide enough for the sizes and the crop preview to sit side by side,
   with room for the footer — at 460px the two-column layout could never
   be used and everything stacked into a long scroll. */
.os-inner {
  position: relative; width: 100%; max-width: 900px;
  max-height: 92vh;
  /* A column with a fixed head and foot: only the middle scrolls, so the
     caveat stays on screen at any window height instead of being tuned
     out of the way with viewport units that break on the next screen. */
  display: flex; flex-direction: column; overflow: hidden;
  background: #fff; border-radius: 6px; padding: 1.75rem;
  box-shadow: 0 12px 48px rgba(26, 26, 28, 0.28);
}
.os-head { flex: none; }
.os-foot { flex: none; }
.os-inner h2 { font-size: 1.3rem; }
/* No bottom margin: the header's own padding sets the gap to its rule. */
.os-photo { margin: 0.2rem 0 0; font-size: 0.82rem; color: var(--text-faint); }
.os-close {
  position: absolute; top: 0.5rem; right: 0.8rem;
  background: none; border: none; font-size: 1.6rem; line-height: 1;
  color: var(--text-faint); cursor: pointer;
}
.os-close:hover { color: var(--text); }
.os-products { display: flex; flex-direction: column; gap: 0.5rem; }
.os-product {
  display: grid; grid-template-columns: 1fr auto; gap: 0.15rem 1rem;
  text-align: left; cursor: pointer;
  padding: 0.8rem 1rem; font-family: inherit;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.os-product:hover { border-color: var(--line-strong); background: var(--bg-2); }
/* Retouching that already comes with the session: shown for reassurance, but
   dimmed and unpickable so it can't be paid for twice. */
.os-product.is-unavailable { opacity: 0.55; cursor: default; }
.os-product.is-unavailable:hover { border-color: var(--line); background: var(--bg); }
.os-product.is-unavailable .osp-price {
  font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint);
}
.osp-name { font-size: 0.95rem; color: var(--text); }
.osp-price { font-size: 0.95rem; font-variant-numeric: tabular-nums; }
.osp-desc { grid-column: 1 / -1; font-size: 0.76rem; color: var(--text-faint); }
.os-status { margin: 1rem 0 0; font-size: 0.85rem; color: var(--ok); min-height: 1.2em; }

/* ---- Admin: aspect ratio pair ---------------------------------------- */
.aspect-pair { display: flex; align-items: center; gap: 0.6rem; }
.aspect-pair input { width: 90px; }
.aspect-x { color: var(--text-faint); }

/* ---- Icon button (cart remove) --------------------------------------- */
.icon-btn {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px; padding: 0;
  background: none; border: 1px solid transparent; border-radius: var(--radius);
  color: var(--text-faint); cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.icon-btn:hover { color: var(--err); border-color: rgba(176, 71, 63, 0.35); background: rgba(176, 71, 63, 0.06); }

/* Counter is a button now — strip the default chrome. */
button.pick-counter {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: inherit; text-align: inherit;
}
button.pick-counter:hover { color: var(--text); }

/* ---- Order sheet: two-column with crop preview ------------------------ */
/* Wider than the old 860: two columns of sizes and a larger preview both
   need the room, and there is plenty of it on a laptop. */
.os-inner { max-width: 1040px; }
.os-head {
  margin-bottom: 1.25rem; padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--line);
}
/* Takes its natural height and shrinks to whatever the head and foot
   leave — so a short catalogue gets a short sheet instead of a 92vh box
   with an empty list in it. The sheet does not scroll; the size list
   inside it does, which is the one place a client expects to. */
.os-body {
  flex: 0 1 auto; min-height: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: 1.75rem; align-items: stretch;
}
/* Left column: list on top taking the slack, caveat fixed beneath it. */
.os-sizes { display: flex; flex-direction: column; min-height: 0; }
/* The one scroller in the sheet. min-height:0 is what lets it shrink
   inside the flex column rather than pushing the caveat off the bottom. */
.os-products {
  flex: 1; min-height: 0; overflow-y: auto;
  padding-right: 0.4rem;
}
/* Height comes from the row, so the preview can sit centred in it rather
   than clinging to the top while the list runs long. */
.os-preview { justify-content: center; }
.os-product.is-chosen {
  border-color: #1a1a1c;
  background: #fff;
  box-shadow: inset 0 0 0 1px #1a1a1c;
}

.os-preview { display: flex; flex-direction: column; gap: 0.75rem; }
.osv-stage {
  display: grid; place-items: center;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.75rem; min-height: 220px;
}
/* The frame must be the print's true shape. Height drives the box and
   width follows from aspect-ratio — setting width:100% with a max-height
   lets the height cap win and silently distorts the ratio. This relies on
   .osv-stage being a centred grid so the item doesn't stretch. */
.osv-frame {
  position: relative;
  height: clamp(200px, 36vh, 400px);
  width: auto;
  max-width: 100%;
  overflow: hidden; border-radius: calc(var(--radius-img) - 2px);
  cursor: grab; background: #000;
  box-shadow: 0 2px 12px rgba(26, 26, 28, 0.18);
}
.osv-frame.is-dragging { cursor: grabbing; }
.osv-frame[hidden] { display: none; }
.osv-frame img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; user-select: none; -webkit-user-drag: none;
}
.osv-empty { margin: 0; font-size: 0.84rem; color: var(--text-faint); text-align: center; }
.osv-hint { margin: 0; font-size: 0.76rem; color: var(--text-faint); text-align: center; }
/* Deliberately alerting: clients must register that the final crop is
   the photographer's, not exactly what they see here. */
/* Easy to skim past when it was a quiet paragraph, so it now carries a
   mark, a heading and enough weight to register before ordering. */
.osv-disclaimer {
  display: flex; align-items: flex-start; gap: 0.7rem;
  margin: 0; padding: 0.85rem 1rem;
  font-size: 0.82rem; line-height: 1.55;
  color: #8a3a33;
  background: rgba(176, 71, 63, 0.10);
  border: 1px solid rgba(176, 71, 63, 0.45);
  border-left: 4px solid #b0473f;
  border-radius: var(--radius);
}
.osv-disclaimer-icon {
  flex: none; display: grid; place-items: center;
  width: 20px; height: 20px; margin-top: 0.1rem;
  font-family: var(--font-body); font-size: 0.78rem; font-weight: 700;
  color: #fff; background: #b0473f; border-radius: 50%;
}
.osv-disclaimer-body { display: block; }
.osv-disclaimer-head {
  display: block; margin-bottom: 0.15rem;
  font-size: 0.85rem; font-weight: 600; color: #7d322c;
}

@media (max-width: 760px) {
  .os-body { grid-template-columns: 1fr; gap: 1.25rem; }
  /* Dissolve the left column so its two parts can be ordered separately:
     the caveat belongs at the top on a phone, the list at the bottom. */
  .os-sizes { display: contents; }
  .os-notes { order: 1; }
  .os-preview { order: 2; }
  .os-products { order: 3; flex: none; overflow: visible; padding-right: 0; }

  /* A phone has no room to freeze the caveat: held at the top it took
     half the sheet and left a sliver to choose a size in. It is still
     the first thing read — it just scrolls away afterwards. Only the
     button stays put, which is what has to be reachable throughout. */
  .os-inner { display: block; overflow-y: auto; padding-bottom: 0; }
  .os-foot {
    position: sticky; bottom: 0; background: #fff;
    /* Bleed into the sheet's padding so nothing shows through beside it. */
    margin: 1.25rem -1.75rem 0;
    padding: 1.25rem 1.75rem 1.75rem;
  }
}

/* ---- Selections review modal ------------------------------------------ */
.sel-modal {
  position: fixed; inset: 0; z-index: 70;
  display: grid; place-items: center;
  background: rgba(26, 26, 28, 0.45); padding: 1rem;
}
.sel-modal[hidden] { display: none; }
/* The request-changes note is the LAST step of a decision, and it can be
   opened from three places: the tile, the lightbox (z 90) and the compare
   view (z 70, but later in the DOM, so it wins the tie). At the shared 70 it
   opened *behind* the other two and read as a dead button — you could even
   type into a textarea you could not see. It outranks both now. */
.sel-modal[data-approve-modal] { z-index: 100; }
/* Head, scrolling grid, pinned foot. The panel itself no longer scrolls:
   the submit button used to sit at the bottom of one long scroll, so with
   two photos in the list you still had to scroll past them to reach it. */
.sel-inner {
  position: relative; width: 100%; max-width: 780px;
  max-height: 92vh; overflow: hidden;
  display: flex; flex-direction: column;
  background: #fff; border-radius: 6px;
  /* No top padding: a sticky child pins below it, which left a 28px strip
     for photos to show through above the pinned compare bar. The heading
     carries that spacing instead, so the strip does not exist. */
  padding: 0 1.75rem 1.75rem;
  box-shadow: 0 12px 48px rgba(26, 26, 28, 0.28);
}
/* Everything that is not the photo list holds its place. */
.sel-inner > h2,
.sel-inner > .sel-sub,
.sel-inner > .sel-scroll-hint,
.sel-inner > .sel-sticky,
.sel-inner > .sel-submit { flex: none; }
/* flex: 0 1 auto — takes only the height its photos need, so one row leaves
   the panel short rather than stretching it to 92vh. min-height: 0 is what
   lets it shrink instead of pushing the foot off the bottom. */
.sel-scroll {
  flex: 0 1 auto; min-height: 0; overflow-y: auto;
  margin: 0 -0.4rem; padding: 0 0.4rem;
}
.sel-inner h2 { font-size: 1.3rem; margin-top: 1.75rem; }
.sel-sub { margin: 0.2rem 0 1.25rem; font-size: 0.82rem; color: var(--text-faint); }
.sel-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 0.85rem;
}
.sel-item { position: relative; margin: 0; }
.sel-item img {
  display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: calc(var(--radius-img) - 2px); background: var(--bg-2);
}
.sel-item figcaption {
  margin-top: 0.35rem; font-size: 0.72rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sel-drop {
  position: absolute; top: 0.35rem; right: 0.35rem;
  width: 26px; height: 26px; display: grid; place-items: center;
  border: none; border-radius: 50%; cursor: pointer;
  background: rgba(255, 255, 255, 0.9); color: #1a1a1c;
  font-size: 1.1rem; line-height: 1;
}
.sel-drop:hover { background: #1a1a1c; color: #fff; }
.sel-empty { margin: 1.5rem 0 0; text-align: center; color: var(--text-faint); font-size: 0.9rem; }

/* ---- Larger selection review tiles ----------------------------------- */
.sel-inner { max-width: 1060px; }
.sel-grid {
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 1.15rem;
  /* Tiles differ in height once the photos keep their own shape; top-align
     so a row of them reads as a row rather than drifting apart. */
  align-items: start;
}
.sel-item img {
  /* aspect-ratio is set per image from the grid, so the review shows the
     photo as framed rather than a uniform crop. */
  aspect-ratio: auto;
  object-fit: cover;
  /* Capped against the viewport so a single row — photo, Remove and note —
     fits the panel whole. width:auto with the cap keeps the true shape: a
     tall photo goes narrower than its column rather than being cropped. */
  width: auto; max-width: 100%;
  height: auto; max-height: min(42vh, 340px);
  margin-inline: auto;
}
.sel-item figcaption { margin-top: 0.45rem; font-size: 0.76rem; }
.sel-drop { width: 30px; height: 30px; font-size: 1.25rem; }

@media (max-width: 560px) {
  .sel-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* Crop hint in the cart */
.ol-crop { font-size: 0.72rem; color: var(--ok); }

/* ---- Lightbox caption: original inline row, bolder type --------------- */
/* Body copy is weight 300, so 600 here makes the actions read clearly
   without changing the layout. */
body.client .lb-caption .btn {
  font-weight: 600;
  color: var(--text);
  border-color: var(--line-strong);
}
body.client .lb-caption .btn:hover {
  border-color: var(--text);
  background: rgba(26, 26, 28, 0.05);
}
/* Selected state must beat the rule above, or the label goes dark-on-dark. */
body.client .lb-caption .btn.lb-pick-on,
body.client .lb-caption .btn.lb-pick-on:hover {
  background: var(--text);
  color: var(--bg);
  border-color: var(--text);
}
body.client .lb-caption { font-weight: 600; }

/* ---- Save-framing checkbox ------------------------------------------- */
.osv-save {
  display: flex; align-items: center; gap: 0.6rem;
  font-size: 0.86rem; color: var(--text);
  padding: 0.6rem 0.75rem;
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--radius); cursor: pointer;
}
.osv-save[hidden] { display: none; }
.osv-save input { width: 17px; height: 17px; accent-color: #1a1a1c; cursor: pointer; }

/* =====================================================================
   Admin: order detail
   ===================================================================== */
.order-admin {
  display: grid; grid-template-columns: minmax(0, 1fr) 320px;
  gap: 1.5rem; align-items: start;
}
@media (max-width: 900px) { .order-admin { grid-template-columns: 1fr; } }
/* Writing a client email in a 320px column is a slot, not a page. The
   sidebar takes the extra room only while the composer is open, and gives
   it back the moment it closes. */
.order-admin.composing { grid-template-columns: minmax(0, 1fr) 480px; }
@media (max-width: 900px) { .order-admin.composing { grid-template-columns: 1fr; } }
.oa-main { min-width: 0; }
.oa-side .panel { margin-bottom: 1.25rem; }

/* Order status badges */
.badge-order-pending_payment  { color: var(--accent); border-color: rgba(201,162,39,0.5); }
.badge-order-paid             { color: var(--ok); border-color: rgba(127,200,160,0.5); }
.badge-order-in_production,
.badge-order-editing          { color: var(--status-blue); border-color: color-mix(in srgb, var(--status-blue) 55%, transparent); }
.badge-order-waiting_changes  { color: var(--status-orange); border-color: color-mix(in srgb, var(--status-orange) 55%, transparent); }
.badge-order-shipped,
.badge-order-submitted_approval,
.badge-order-ready_for_pickup { color: var(--status-purple); border-color: color-mix(in srgb, var(--status-purple) 55%, transparent); }
.badge-order-completed        { color: var(--text-muted); }
.badge-order-cancelled,
.badge-order-refunded         { color: var(--err); border-color: rgba(217,138,138,0.45); }

/* Items */
.oa-items { display: flex; flex-direction: column; }
.oa-retouch-note { margin: 0 0 0.6rem; }
.oa-item {
  display: grid; grid-template-columns: 72px minmax(0,1fr) auto;
  gap: 1rem; align-items: start;
  padding: 1rem 0; border-bottom: 1px solid var(--line);
}
.oa-item:last-child { border-bottom: none; }
.oa-thumb-wrap { position: relative; }
.oa-thumb {
  width: 72px; height: 90px; object-fit: cover;
  border-radius: calc(var(--radius-img) - 2px); background: var(--bg-2);
  display: block;
}
.oa-thumb-empty { display: block; }
.oa-crop-tag {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2px 0; text-align: center;
  font-size: 0.56rem; letter-spacing: 0.08em; text-transform: uppercase;
  background: rgba(201, 162, 39, 0.9); color: #17170f;
  border-bottom-left-radius: calc(var(--radius-img) - 2px);
  border-bottom-right-radius: calc(var(--radius-img) - 2px);
}
.oa-item-info { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.oa-item-name { font-size: 0.95rem; }
.oa-crop-note {
  margin-top: 0.35rem; font-size: 0.74rem; line-height: 1.45;
  color: var(--accent);
}
.oa-item-total { font-size: 0.95rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 620px) {
  .oa-item { grid-template-columns: 64px minmax(0,1fr); }
  .oa-item-total { grid-column: 2; }
}

/* Sidebar meta */
.oa-meta { margin: 0 0 0.6rem; font-size: 0.88rem; line-height: 1.5; }
.oa-meta.sub { font-size: 0.8rem; color: var(--text-muted); }
.oa-side .btn-primary { width: 100%; }

/* Change requests raised on the finals — a callout atop the retouch card so
   the redo work reads first. Amber, matching the "changes" status language. */
.oa-changes {
  margin: 0 0 1.15rem; padding: 0.85rem 1rem;
  border: 1px solid rgba(224, 165, 106, 0.45); border-radius: 10px;
  background: rgba(224, 165, 106, 0.08);
}
.oa-changes-head { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.75rem; }
.oa-changes-badge {
  font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
  color: #17170f; background: var(--status-orange); padding: 0.2rem 0.55rem; border-radius: 999px;
}
.oa-changes-count { font-size: 0.78rem; color: var(--text-muted); }
.oa-changes-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.8rem; }
.oa-change { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 0.8rem; align-items: start; }
.oa-change-info { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.oa-change-note { font-size: 0.85rem; line-height: 1.5; color: var(--text); white-space: pre-wrap; }
.oa-change-note.muted { font-style: italic; }
/* Per-photo "answer this change request" uploader: file picker + button on one
   line, wrapping on narrow panels. Auto-named vN server-side, so no name field. */
.oa-change-rev { margin: 0.35rem 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.55rem; }
.oa-change-rev .oa-rev-file { font-size: 0.78rem; max-width: 100%; color: var(--text-muted); }
.oa-change-rev .oa-rev-file::file-selector-button {
  margin-right: 0.5rem; padding: 0.25rem 0.6rem; border-radius: 6px;
  border: 1px solid var(--line); background: var(--panel-2, var(--panel)); color: var(--text);
  font-size: 0.78rem; cursor: pointer;
}
/* Same amber-when-armed / green-when-sent cue as the finals uploader button. */
.oa-change-rev .btn.is-dirty { background: #d99a2b !important; border-color: #d99a2b !important; color: #1a1a1c !important; }
.oa-change-rev .btn.is-dirty:hover { background: #e6ac3e !important; border-color: #e6ac3e !important; }

/* Order "Submit for Client Approval": the finals uploader leads, the delivery
   controls follow under a labeled step so the two stages read as one flow. */
.oa-approve-submit { margin-top: 0.25rem; }
/* Delivery method leads the panel — divide it from the finals uploader below. */
.oa-deliver-method { margin: 0 0 1.2rem; padding-bottom: 1.1rem; border-bottom: 1px solid var(--line); }
.oa-deliver-method .field { margin: 0; }
.oa-step-div { border: 0; border-top: 1px solid var(--line); margin: 1.25rem 0 0.9rem; }
.oa-deliver-head { margin: 0 0 0.2rem; font-size: 0.98rem; }
/* A panel that folds away. The summary IS the panel head, so an open fold
   looks exactly like a plain panel and only the caret gives it away. */
.oa-fold-head { list-style: none; cursor: pointer; gap: 0.6rem; align-items: center; }
.oa-fold-head::-webkit-details-marker { display: none; }
/* One rule for every fold's caret, whatever its summary is called. */
.oa-fold[open] > summary .cg-caret { transform: rotate(90deg); }
.oa-fold:not([open]) > .oa-fold-head { margin-bottom: 0; }
/* Same trap the template groups hit: something later in this sheet gives the
   body a display, which defeats the browser's own hiding of a closed
   <details>. Hide it explicitly rather than relying on the UA rule. */
.oa-fold:not([open]) > *:not(summary) { display: none; }
.oa-fold-head h2 { flex: 1; min-width: 0; }
/* The delivery note sitting inside step 2, with its own save beside it. */
.oa-note-inline { margin: 0.35rem 0 0; }
.oa-note-inline label { display: block; margin-bottom: 0.3rem; }
.oa-note-save { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.45rem; }
.oa-note-save .hint { margin: 0; }
/* Master files, folded away behind their own count. */
.oa-master-fold { margin: 0.25rem 0 1.25rem; }
.oa-master-sum {
  display: flex; align-items: center; gap: 0.5rem;
  list-style: none; cursor: pointer;
  font-size: 0.82rem; color: var(--text-muted);
  padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius);
  transition: background 0.15s ease;
}
.oa-master-sum::-webkit-details-marker { display: none; }
.oa-master-sum:hover { background: var(--surface-2); color: var(--text); }
.oa-master-fold[open] > .oa-master-sum { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.oa-master-fold > .master-files {
  padding: 0.9rem 0.7rem 0.7rem;
  border: 1px solid var(--line); border-top: none;
  border-radius: 0 0 var(--radius) var(--radius);
}
.oa-deliver-sub { margin: 0 0 0.9rem; }

/* History */
.oa-history { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.oa-history li { display: flex; flex-direction: column; gap: 0.1rem; font-size: 0.82rem; }
.oah-status { color: var(--text); }
.oah-when { color: var(--text-faint); font-size: 0.74rem; }
.oah-note { color: var(--text-muted); font-size: 0.78rem; }
/* The preview list is a button: clickable, with a hover cue that it opens. A
   long note is clamped so the preview stays compact — the popup shows it whole. */
.oa-history--preview {
  cursor: pointer; border: 1px solid transparent; border-radius: 8px;
  margin: 0 -0.4rem; padding: 0.4rem; transition: background 0.12s ease, border-color 0.12s ease;
}
.oa-history--preview:hover,
.oa-history--preview:focus-visible { background: rgba(var(--ink), 0.04); border-color: var(--line); outline: none; }
.oa-history--preview .oah-note {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.oa-hist-more {
  display: inline-flex; align-items: center; gap: 0.35rem; margin-top: 0.75rem; padding: 0;
  background: none; border: none; cursor: pointer;
  font-size: 0.76rem; color: var(--text-muted);
}
.oa-hist-more:hover { color: var(--text); }
.oa-hist-more svg { opacity: 0.75; }
/* Readable full history in the popup — roomier rows, notes wrap in full. */
.hist-inner { max-width: 560px; }
.oa-history--full { gap: 1.1rem; }
.oa-history--full li {
  font-size: 0.9rem; gap: 0.35rem;
  padding-bottom: 1.1rem; border-bottom: 1px solid var(--line);
}
.oa-history--full li:last-child { padding-bottom: 0; border-bottom: none; }
.ohf-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.oa-history--full .oah-status { font-weight: 600; }
.oa-history--full .oah-when { font-size: 0.8rem; }
.ohf-note { margin: 0; font-size: 0.88rem; line-height: 1.55; color: var(--text-muted); white-space: pre-wrap; }

/* ---- Order activity timeline ------------------------------------------
   One list of everything behind an order: the gallery's own milestones, what
   the client did, and every email. A dot per row against a vertical rule, so
   the four kinds are told apart by colour rather than by reading each label. */
/* Heading block: the title, its count and the one-line explanation stack
   properly instead of the count riding up into the serif ascenders and the
   subtitle running under it. The close button sits top-right, so leave it
   room rather than letting a long subtitle slide beneath it. */
.oa-tl-head { margin: 0 2.5rem 1.5rem 0; }
.oa-tl-head h2 { margin: 0 0 0.3rem; line-height: 1.2; }
.oa-tl-head .count { margin-left: 0.5rem; vertical-align: 0.25em; }
.oa-tl-sub { margin: 0; max-width: 46ch; }
.oa-timeline { position: relative; gap: 0; }
.oa-timeline .oa-tl-day {
  position: sticky; top: 0; z-index: 1;
  margin: 1.25rem 0 0.6rem; padding: 0.3rem 0 0.35rem;
  font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint); background: var(--surface-1);
  border-bottom: none;
}
.oa-timeline > .oa-tl-day:first-child { margin-top: 0; }
.oa-timeline .oa-tl-item {
  position: relative; flex-direction: row; align-items: flex-start; gap: 0.75rem;
  padding: 0 0 0.9rem 0; border-bottom: none;
}
/* The rule joining the dots. It stops at the last row rather than trailing
   off the end of the list. */
.oa-timeline .oa-tl-item::before {
  content: ""; position: absolute; left: 3px; top: 0.85rem; bottom: -0.1rem;
  width: 1px; background: var(--line);
}
.oa-timeline .oa-tl-item:last-child { padding-bottom: 0; }
.oa-timeline .oa-tl-item:last-child::before { display: none; }
.oa-timeline .oa-tl-dot {
  flex: none; width: 7px; height: 7px; margin-top: 0.42rem; border-radius: 50%;
  background: var(--text-faint); position: relative; z-index: 1;
}
.oa-timeline .oa-tl-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.oa-tl-item .ohf-row { align-items: baseline; }

/* Kind: gallery = the studio's own setup, client = something they did,
   email = something that left the building, studio = internal notice. */
.oah-gallery .oa-tl-dot { background: var(--text-muted); }
.oah-client  .oa-tl-dot { background: var(--ok); }
.oah-email   .oa-tl-dot { background: var(--accent); }
.oah-studio  .oa-tl-dot { background: var(--line); }
.oah-order   .oa-tl-dot { background: var(--text); }
.oah-bad     .oa-tl-dot { background: var(--err); }
.oah-bad .oah-status { color: var(--err); }
/* The preview list has no dots, so failures are marked on the label there. */
.oa-history--preview .oah-bad .oah-status { color: var(--err); }

/* Filter tab counts */
.filters .count {
  margin-left: 0.3rem; font-size: 0.68rem; color: var(--text-faint);
}

/* ---- Selection submission -------------------------------------------- */
.pick-counter.is-over { color: var(--err); }
.pick-counter.is-over strong { color: var(--err); }
.over-tag {
  margin-left: 0.45rem; padding: 0.1rem 0.45rem;
  font-size: 0.68rem; letter-spacing: 0.04em;
  border-radius: 100px;
  background: rgba(176, 71, 63, 0.12); color: #8a3a33;
  border: 1px solid rgba(176, 71, 63, 0.3);
}
.over-tag[hidden] { display: none; }

.flash-locked {
  border-left-color: var(--text-faint);
  color: var(--text-muted);
  background: var(--surface-1);
}

.sel-submit {
  margin-top: 1.75rem; padding-top: 1.5rem;
  border-top: 1px solid var(--line);
}
.sel-locked { margin: 0; font-size: 0.9rem; color: var(--text-muted); }
.sel-locked strong { color: var(--text); font-weight: 600; }

.sel-cost {
  display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.8rem 0.95rem; margin-bottom: 1rem;
  font-size: 0.86rem;
  color: #8a3a33;
  background: rgba(176, 71, 63, 0.07);
  border: 1px solid rgba(176, 71, 63, 0.22);
  border-left: 3px solid #b0473f;
  border-radius: var(--radius);
}
.sel-cost[hidden] { display: none; }
.sel-cost-note { font-size: 0.74rem; opacity: 0.85; }

.sel-submit-fields { display: flex; gap: 0.6rem; margin-bottom: 0.85rem; }
.sel-submit-fields[hidden] { display: none; }
.sel-submit-fields input { flex: 1; }
.sel-submit-form .btn-primary { width: 100%; }
.sel-submit-note { margin: 0.7rem 0 0; font-size: 0.76rem; color: var(--text-faint); }

@media (max-width: 560px) { .sel-submit-fields { flex-direction: column; } }

/* Locked-selections notice in gallery settings */
.locked-notice {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.25rem; flex-wrap: wrap;
  padding: 1rem 1.15rem; margin-top: 1.5rem; margin-bottom: 1.15rem;
  background: rgba(201, 162, 39, 0.08);
  border: 1px solid rgba(201, 162, 39, 0.3);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}
.locked-notice p { margin: 0.2rem 0 0; font-size: 0.82rem; }
[data-mode-fields][hidden] { display: none; }

/* ---- Crop inspector (admin order) ------------------------------------ */
button.oa-thumb-wrap {
  position: relative; padding: 0; border: none; background: none;
  cursor: zoom-in; display: block; line-height: 0;
}
.oa-zoom {
  position: absolute; top: 4px; right: 4px;
  display: grid; place-items: center; width: 22px; height: 22px;
  border-radius: 50%; background: rgba(11,11,12,0.6); color: rgba(var(--ink), 0.9);
  opacity: 0; transition: opacity 0.15s ease;
}
button.oa-thumb-wrap:hover .oa-zoom,
button.oa-thumb-wrap:focus-visible .oa-zoom { opacity: 1; }

.crop-modal {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center;
  background: rgba(8, 8, 9, 0.9); padding: 1.5rem;
}
.crop-modal[hidden] { display: none; }
.cm-inner {
  position: relative; width: 100%; max-width: 900px;
  max-height: 92vh; overflow-y: auto;
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: 6px; padding: 1.75rem;
}
.cm-head { margin-bottom: 1.15rem; padding-right: 2rem; }
.cm-head h2 { font-size: 1.25rem; }
.cm-sub { margin: 0.2rem 0 0; font-size: 0.8rem; color: var(--text-faint); }

.cm-stage { display: grid; place-items: center; background: #000; border-radius: var(--radius-img); padding: 0.75rem; }
.cm-frame {
  position: relative; overflow: hidden;
  height: clamp(260px, 52vh, 560px); width: auto; max-width: 100%;
  border-radius: calc(var(--radius-img) - 2px);
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.cm-frame img { display: block; width: 100%; height: 100%; }

.cm-controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.25rem; flex-wrap: wrap; margin-top: 1.15rem;
}
.cm-toggle { display: flex; gap: 0.25rem; }
.cm-tab {
  padding: 0.5rem 0.95rem; cursor: pointer;
  font-family: inherit; font-size: 0.74rem;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
  background: transparent; border: 1px solid var(--line);
  border-radius: 100px;
}
.cm-tab:hover { color: var(--text-muted); }
.cm-tab.is-active { color: var(--bg); background: var(--text); border-color: var(--text); }
.cm-tab[hidden] { display: none; }
.cm-note { margin: 0; flex: 1; min-width: 220px; font-size: 0.78rem; color: var(--text-muted); }

/* ---- Notify panel + email screen ------------------------------------- */
.notify-panel {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap;
}
.np-text h2 { font-size: 1.1rem; margin-bottom: 0.3rem; }
.np-text p { margin: 0; font-size: 0.82rem; max-width: 46ch; }
/* Now carries a template picker and a password as well as the send, so it
   stacks its own column rather than trying to sit on one line. */
.np-action {
  display: flex; flex-direction: column; gap: 0.75rem;
  min-width: 280px;
}
.np-action .field { margin: 0; }
.np-buttons { display: flex; gap: 0.6rem; }
.np-buttons .btn { flex: 1 1 0; }

.email-kinds { margin: 0; padding-left: 1.1rem; color: var(--text-muted); font-size: 0.88rem; }
.email-kinds li { margin-bottom: 0.35rem; }
.email-kinds strong { color: var(--text); font-weight: 500; }

@media (max-width: 700px) {
  .np-action { width: 100%; }
  .np-action input { flex: 1; width: auto; }
}

/* ---- Email template editor ------------------------------------------- */
.tpl-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) 400px;
  gap: 1.5rem; align-items: start;
}
@media (max-width: 1000px) { .tpl-layout { grid-template-columns: 1fr; } }
.tpl-editor { min-width: 0; }

.tpl-type-row {
  display: flex; align-items: center; gap: 0.85rem; flex-wrap: wrap;
  padding: 0.75rem 0; border-bottom: 1px solid var(--line);
}
.tpl-type-row:last-of-type { border-bottom: none; }
.ttr-label {
  width: 74px; flex: 0 0 auto;
  font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-muted);
}
.select-wrap.sm { flex: 1; min-width: 150px; }
.select-wrap.sm select { padding: 0.45rem 2rem 0.45rem 0.6rem; font-size: 0.85rem; }
.ttr-size, .ttr-color {
  display: flex; align-items: center; gap: 0.4rem;
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint);
}
.ttr-size input { width: 62px; padding: 0.45rem; text-align: center; }
.ttr-color input[type=color] {
  width: 34px; height: 30px; padding: 2px; cursor: pointer;
  background: var(--surface-3); border: 1px solid var(--line); border-radius: var(--radius);
}

.token-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.token {
  padding: 0.32rem 0.6rem; cursor: pointer;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.72rem;
  color: var(--text-muted); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 100px;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.token:hover { color: var(--text); border-color: var(--line-strong); }
.token.is-copied { color: var(--ok); border-color: rgba(127,200,160,0.5); }

/* Preview pane */
.tpl-preview {
  position: sticky; top: 1.5rem;
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.25rem;
}
.tpv-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.9rem; }
.tpv-head h2 { font-size: 1.1rem; }
.tpv-subject {
  display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.6rem 0.75rem; margin-bottom: 0.85rem;
  background: var(--surface-2); border-radius: var(--radius);
  font-size: 0.86rem; color: var(--text);
}
.tpv-label { font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); }
.tpv-frame {
  width: 100%; height: 620px; border: 1px solid var(--line);
  border-radius: var(--radius); background: #f2f1ef;
}

/* Template cards on the email screen */
.tpl-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.tpl-card {
  display: flex; flex-direction: column; gap: 0.35rem;
  padding: 1.15rem 1.25rem; text-decoration: none;
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.tpl-card:hover { border-color: var(--line-strong); background: var(--surface-2); }
.tc-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.tc-name { font-size: 0.98rem; color: var(--text); }
.tc-desc { font-size: 0.78rem; color: var(--text-faint); line-height: 1.45; }
.tc-subject {
  margin-top: 0.35rem; padding-top: 0.5rem; border-top: 1px solid var(--line);
  font-size: 0.76rem; color: var(--text-muted);
}

/* Visitors tab */
.visitor-send {
  display: flex; align-items: flex-end; gap: 1rem; flex-wrap: wrap;
  margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
}
.visitor-send .field { flex: 1; min-width: 210px; margin-bottom: 0; }
table.data input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--text); cursor: pointer; }

/* ---- Primary button hover on the light client theme ------------------
   The shared rule hovers to #fff with color var(--bg); on the light
   theme --bg is near-white, so the label all but disappeared. Darken
   on hover instead, and give it a clear lift. */
body.client .btn-primary {
  transition: background 0.15s ease, border-color 0.15s ease,
              box-shadow 0.15s ease, transform 0.1s ease;
}
body.client .btn-primary:hover,
body.client .btn-primary:focus-visible {
  background: #000;
  color: #ffffff;
  border-color: #000;
  box-shadow: 0 3px 12px rgba(26, 26, 28, 0.28);
}
body.client .btn-primary:active { transform: translateY(1px); box-shadow: none; }
body.client .btn-primary:focus-visible { outline: 2px solid #1a1a1c; outline-offset: 2px; }
body.client .btn-primary[disabled],
body.client .btn-primary[disabled]:hover {
  background: var(--text); color: var(--bg); box-shadow: none; transform: none;
}

/* Current gallery password */
.pw-current {
  display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap;
  margin-top: 0.55rem; padding: 0.55rem 0.75rem;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.pw-label { font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-faint); }
.pw-value {
  flex: 1; min-width: 90px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9rem; letter-spacing: 0.06em; color: var(--text);
}
.pw-none { color: var(--accent); }

/* Inline contact correction */
.contact-edit[hidden] { display: none; }
.contact-edit { border-color: var(--line-strong); }
td .linkish { display: block; margin-top: 0.25rem; }

/* Assisting banner on the client side */
.assist-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: 0.7rem clamp(1rem, 4vw, 2.5rem);
  background: #c9a227; color: #17170f;
  font-size: 0.85rem;
}
.assist-bar strong { font-weight: 600; }
.assist-bar a { color: #17170f; font-weight: 600; }

/* Visitor session panel */
.session-panel[hidden] { display: none; }
.session-panel { border-color: var(--line-strong); }
.row-links { display: flex; gap: 0.75rem; margin-top: 0.3rem; }
.row-links .linkish { display: inline; margin-top: 0; }
tr[data-visitor-row] { cursor: pointer; }
tr[data-visitor-row]:hover > td { background: color-mix(in srgb, var(--text) 5%, transparent); }
.sp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1.5rem; margin-top: 0.85rem; }
.sp-h {
  font-family: var(--font-body); font-size: 0.7rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-faint); margin: 0 0 0.6rem;
}
.sp-list { list-style: none; margin: 0; padding: 0; font-size: 0.84rem; }
.sp-list li { padding: 0.2rem 0; color: var(--text-muted); }
/* Clients: each "Emails sent" row is a button that opens a re-render. */
.sp-emails li { padding: 0; }
.sp-email {
  display: flex; align-items: baseline; gap: 0.5rem; width: 100%;
  background: none; border: 0; padding: 0.4rem 0.5rem; margin: 0 -0.5rem;
  font: inherit; color: var(--text); text-align: left; cursor: pointer;
  border-radius: 8px;
}
.sp-email:hover { background: var(--panel-2, rgba(127,127,127,0.08)); }
.sp-email-subj { flex: 1 1 auto; min-width: 0; }
/* Clients: edit name/email form on the detail page. */
.client-edit { margin: 0 0 0.9rem; }
.client-edit .field { margin-bottom: 0.6rem; }
.client-edit .form-actions { margin: 0; }
/* Clients: search box over the grouped list. */
.client-search { flex: 1 1 auto; max-width: 26rem; }
.client-search input {
  width: 100%; padding: 0.5rem 0.75rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel); color: var(--text); font: inherit;
}
.client-list-note { margin: 0 0 1.25rem; }
.sp-note { display: block; color: var(--accent); font-style: italic; font-size: 0.8rem; }
/* Marks an image the client paid to have retouched (Team add-on). */
.sp-retouch {
  display: inline-block; font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 0.05rem 0.4rem; margin-left: 0.25rem; border-radius: 100px;
  background: var(--accent); color: #1a1a1c;
}
.sp-empty { font-size: 0.84rem; margin: 0; }
.ap-verdict {
  display: inline-block; font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 0.05rem 0.4rem; border-radius: 100px; margin-right: 0.3rem; color: #fff;
}
.ap-verdict.ap-approved { background: var(--ok); }
.ap-verdict.ap-changes  { background: #b0651a; }
.sp-stuck { color: var(--accent); font-size: 0.76rem; }
.sp-note {
  margin: 1.25rem 0 1rem; padding: 0.7rem 0.85rem;
  font-size: 0.78rem; color: var(--text-muted);
  background: var(--surface-2); border-left: 2px solid var(--line-strong);
  border-radius: var(--radius);
}

/* Cart inside the visitor session panel */
.sp-cart { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.sp-cart-total { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px solid var(--line); color: var(--text); }

/* Clients grouped by gallery */
.cgroup { margin-bottom: 1rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.cgroup-head {
  display: flex; align-items: center; gap: 0.85rem; width: 100%;
  padding: 1rem 1.25rem; cursor: pointer; text-align: left;
  background: var(--surface-1); border: none; font-family: inherit;
  transition: background 0.15s ease;
}
.cgroup-head:hover { background: var(--surface-2); }
.cg-caret { display: grid; place-items: center; color: var(--text-faint); transition: transform 0.15s ease; }
.cgroup-head[aria-expanded="true"] .cg-caret { transform: rotate(90deg); }
.cg-title { font-size: 1rem; color: var(--text); flex: 1; min-width: 0; }
.cg-count { font-size: 0.76rem; color: var(--text-faint); white-space: nowrap; }
.cgroup-body { padding: 0 1.25rem 1.25rem; background: var(--surface-1); }
.cgroup-body[hidden] { display: none; }
.cg-link { margin: 0 0 0.85rem; font-size: 0.8rem; }
.cg-actions { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.85rem; }
.cg-actions .cg-link { margin: 0; }
/* Team sub-groups: each member nested inside its team's client group. */
.cg-tag {
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 0.12rem 0.5rem; border-radius: 100px; white-space: nowrap;
  background: rgba(201, 162, 39, 0.14); color: #b98900; border: 1px solid rgba(201, 162, 39, 0.4);
}
.cg-sublabel { margin: 0.2rem 0 0.5rem; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-faint); }
.csub { margin: 0.6rem 0 0; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.csub > .cgroup-head { padding-top: 0.7rem; padding-bottom: 0.7rem; }
.csub .cg-title { font-size: 0.9rem; }
.csub > .cgroup-body { background: var(--surface-2); }

/* Archive / restore */
/* Matches the blue of its step badge, the way the delete panel matches red. */
.archive-panel { border-color: rgba(122,167,217,0.35); }
.archive-steps { margin: 0; padding-left: 1.2rem; color: var(--text-muted); font-size: 0.88rem; }
.archive-steps li { margin-bottom: 0.85rem; }
.archive-steps strong { color: var(--text); font-weight: 500; }
.archive-steps p { margin: 0.6rem 0 0; }
.restore-panel[hidden] { display: none; }
.restore-row { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; }
.restore-row input[type=file] { flex: 1; min-width: 220px; color: var(--text-muted); font-size: 0.86rem; }

/* Two ways to restore, side by side: the .zip, or the unzipped folder. */
.restore-options { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-top: 1rem; }
@media (max-width: 720px) { .restore-options { grid-template-columns: 1fr; } }
.restore-option { margin: 0; padding: 1rem 1.15rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
.restore-option h3 { margin: 0 0 0.15rem; font-size: 0.9rem; }
.restore-option .hint { margin: 0 0 0.8rem; }
.restore-folder-status { flex-basis: 100%; margin: 0.25rem 0 0; }

/* Automatic email triggers */
.trigger-list { display: flex; flex-direction: column; }
.trigger {
  display: grid; grid-template-columns: 28px minmax(0,1fr) minmax(190px, 260px);
  gap: 1rem; align-items: start;
  padding: 1rem 0; border-bottom: 1px solid var(--line);
}
.trigger:last-child { border-bottom: none; }
.trigger-on { margin: 0; }
.trigger-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.trigger-name { font-size: 0.95rem; color: var(--text); }
.trigger-about { font-size: 0.8rem; color: var(--text-muted); line-height: 1.5; }
.trigger-to { font-size: 0.72rem; color: var(--text-faint); }
.trigger-tpl { display: flex; flex-direction: column; gap: 0.4rem; align-items: flex-start; }
@media (max-width: 760px) {
  .trigger { grid-template-columns: 28px minmax(0,1fr); }
  .trigger-tpl { grid-column: 2; }
}

/* Support */
.support-inner { max-width: 520px; }
.sr-message { margin: 0; font-size: 0.95rem; line-height: 1.65; color: var(--text-muted); white-space: pre-wrap; }
.badge-sr-new      { color: var(--accent); border-color: rgba(201,162,39,0.5); }
.badge-sr-open     { color: var(--status-blue); border-color: color-mix(in srgb, var(--status-blue) 55%, transparent); }
.badge-sr-waiting  { color: var(--status-purple); border-color: color-mix(in srgb, var(--status-purple) 55%, transparent); }
.badge-sr-resolved { color: var(--ok); border-color: rgba(127,200,160,0.5); }
.badge-sr-closed   { color: var(--text-faint); }
body.client [data-support-modal] .os-status { color: var(--ok); }

/* ---- Client header with centred gallery title ------------------------ */
.client-head.has-title {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
}
.client-head-title {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.9vw, 1.95rem);
  font-weight: 400;
  line-height: 1.2;
  text-align: center;
  margin: 0;
  min-width: 0;
}
.client-head-right { display: flex; justify-content: flex-end; }
@media (max-width: 760px) {
  .client-head.has-title { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .client-head.has-title .client-brand { justify-self: center; }
  .client-head-right { display: none; }
}

/* ---- Selections call to action --------------------------------------- */
/* Every child sits on one centred axis, so nothing rides high or low. */
.pick-cta {
  display: inline-flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 0.85rem 0.5rem 0.6rem;
  font-family: inherit; cursor: pointer;
  color: var(--text);
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  line-height: 1.2;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.pick-cta:hover {
  border-color: var(--text);
  box-shadow: 0 2px 10px rgba(26, 26, 28, 0.10);
}
.pc-check {
  display: grid; place-items: center;
  width: 24px; height: 24px; flex: 0 0 auto;
  border-radius: 50%;
  background: var(--text); color: var(--bg);
}
.pc-label { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; text-align: left; }
.pc-action {
  font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  line-height: 1.25;
}
.pc-count {
  display: inline-flex; align-items: baseline; gap: 0.3rem;
  font-size: 0.74rem; color: var(--text-muted); line-height: 1.25;
}
.pc-count strong { font-weight: 600; color: var(--text); }
.pc-arrow { font-size: 1rem; color: var(--text-faint); line-height: 1; }
.pick-cta:hover .pc-arrow { color: var(--text); }

/* They have chosen exactly what the gallery allows: the list is ready to
   send. Declared before .is-over so going over still wins on equal
   specificity — over-allowance is the more urgent thing to say. */
.pick-cta.is-complete { border-color: rgba(47, 125, 85, 0.6); }
.pick-cta.is-complete .pc-check { background: var(--ok); color: #fff; }
.pick-cta.is-complete .pc-count strong { color: var(--ok); }
.pick-cta.is-complete:hover { border-color: var(--ok); }

.pick-cta.is-over { border-color: rgba(176, 71, 63, 0.55); }
.pick-cta.is-over .pc-check { background: #b0473f; color: #fff; }
.pick-cta.is-over .pc-count strong { color: #8a3a33; }
.pick-cta .over-tag { margin-left: 0.15rem; }
/* On a phone the label is too long to sit beside the other links — give
   the call to action its own full-width row rather than let it wrap to
   four cramped lines. */
@media (max-width: 620px) {
  .gb-right { flex-wrap: wrap; gap: 0.9rem 1.1rem; width: 100%; }
  .pick-cta {
    order: -1; width: 100%; justify-content: center;
    padding-top: 0.7rem; padding-bottom: 0.7rem;
  }
  .pick-cta .pc-label { display: flex; align-items: baseline; gap: 0.45rem; flex-wrap: wrap; justify-content: center; }
  .pick-cta .pc-action { white-space: nowrap; }
}

@media (max-width: 620px) {
  .pc-action { font-size: 0.72rem; }
}

/* ---- Scroll-to-submit hint ------------------------------------------- */
.sel-scroll-hint {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0 0 1.1rem;
  padding: 0.6rem 0.85rem;
  font-size: 0.82rem;
  color: #8a3a33;
  background: rgba(176, 71, 63, 0.07);
  border: 1px solid rgba(176, 71, 63, 0.22);
  border-radius: var(--radius);
}
.sel-scroll-hint[hidden] { display: none; }

/* ---- Email preview modal ---------------------------------------------- */
/* Used by the gallery Launch panel and the order screen. The frame is a
   sandboxed iframe, so email HTML renders without running anything. */
.ep-overlay {
  position: fixed; inset: 0; z-index: 1300;
  display: grid; place-items: center;
  background: rgba(26, 26, 28, 0.55); padding: 1rem;
}
.ep-inner {
  position: relative;
  width: 100%; max-width: 720px;
  /* A definite height, not a max: an iframe has no intrinsic height, so
     flex:1 on the frame had nothing to divide and the sheet collapsed
     around it. */
  height: min(860px, 92vh);
  display: flex; flex-direction: column; overflow: hidden;
  background: #fff; border-radius: 6px;
  box-shadow: 0 12px 48px rgba(26, 26, 28, 0.34);
}
.ep-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1.1rem 1.4rem;
  border-bottom: 1px solid var(--line);
}
.ep-head h2 { margin: 0; font-size: 1.05rem; color: #1a1a1c; }
.ep-inner .os-close { position: static; font-size: 1.5rem; }
/* The email decides its own width; the frame just gives it a viewport. */
.ep-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #f2f1ef; }
.ep-foot {
  flex: none; margin: 0; padding: 0.85rem 1.4rem;
  font-size: 0.78rem; color: #6b6b70;
  border-top: 1px solid var(--line); background: #fff;
}

/* ---- Order: resend rows and payment actions --------------------------- */
/* Shared by shipping and digital delivery — both offer the same thing:
   send this to the client, and see it first. */
.resend-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-top: 1.25rem; padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.resend-text { display: flex; flex-direction: column; gap: 0.15rem; }
/* Buttons only, no label beside them — keep them where they always sat
   rather than letting space-between strand them at the left edge. */
.resend-row-bare { justify-content: flex-end; }
.resend-text .hint { margin: 0; }
.resend-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.resend-actions form { margin: 0; }
.pay-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  margin-top: 1rem; padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.pay-actions form { margin: 0; }

/* Manual payment-link collection, inside the order Payment panel. */
.pay-link {
  margin-top: 1rem; padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.pay-link h3 { margin: 0 0 0.3rem; font-size: 0.95rem; }
.pay-link .hint { margin: 0 0 0.7rem; }
.pay-link-save { display: flex; gap: 0.5rem; margin: 0 0 0.6rem; }
.pay-link-save input { flex: 1 1 auto; min-width: 0; }
.pay-link-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.pay-link-actions form { margin: 0; }
.pay-link-paid { margin: 0.85rem 0 0; padding-top: 0.85rem; border-top: 1px dashed var(--line); }
.pay-paid-notify { display: flex; align-items: center; gap: 0.45rem; margin: 0 0 0.6rem; font-size: 0.85rem; }
.status-preview { margin: 0 0 0.9rem; }
/* The custom-message composer: indented behind a rule so it reads as
   belonging to the checkbox that revealed it, not as more of the form. */
.custom-email {
  margin: 0 0 1rem; padding: 0.15rem 0 0.15rem 0.9rem;
  border-left: 2px solid var(--line);
}
.custom-email textarea { min-height: 8.5rem; resize: vertical; }

/* ---- Invoice ---------------------------------------------------------- */
.invoice { max-width: 720px; margin: 0 auto; padding: 2rem 0; }
.inv-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap;
  padding-bottom: 1.25rem; border-bottom: 2px solid var(--text);
}
.inv-head h1 { margin: 0; font-size: 1.8rem; }
.inv-num { margin: 0.2rem 0 0; font-size: 0.85rem; color: var(--text-muted); }
.inv-studio { display: flex; flex-direction: column; gap: 0.15rem; text-align: right; font-size: 0.85rem; }
.inv-logo { height: 44px; width: auto; align-self: flex-end; margin-bottom: 0.5rem; }
.inv-logo.brand-custom { max-width: 220px; object-fit: contain; }
.inv-meta {
  display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;
  margin: 1.5rem 0 2rem;
}
.inv-meta h2 {
  font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint); margin: 0 0 0.35rem;
}
.inv-meta p { margin: 0 0 1rem; font-size: 0.9rem; line-height: 1.5; }
/* Stacked rather than two columns: side by side, a date wrapped onto two
   lines and a gallery title onto four. */
.inv-dl { margin: 0; font-size: 0.9rem; }
.inv-dl dt {
  font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-faint);
}
.inv-dl dd { margin: 0 0 0.7rem; }
.inv-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.inv-table th, .inv-table td { padding: 0.6rem 0.5rem; text-align: left; }
.inv-table thead th {
  font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint); border-bottom: 1px solid var(--line);
}
.inv-table tbody td { border-bottom: 1px solid var(--line); vertical-align: top; }
.inv-table .num { text-align: right; white-space: nowrap; }
.inv-sub { display: block; font-size: 0.78rem; color: var(--text-faint); }
.inv-table tfoot th { text-align: right; font-weight: 400; color: var(--text-muted); }
.inv-table tfoot td { text-align: right; }
.inv-total th, .inv-total td {
  font-weight: 600; color: var(--text); font-size: 1rem;
  border-top: 2px solid var(--text);
}
.inv-status { margin: 1.5rem 0 0; font-size: 0.9rem; color: var(--text-muted); }
.inv-actions { display: flex; gap: 0.6rem; margin-top: 2.5rem; }

@media print {
  /* Only the invoice itself goes on paper. */
  .inv-actions, .flash { display: none !important; }
  .plain-main { padding: 0; }
  .invoice { max-width: none; padding: 0; }
}

/* ---- Email template groups -------------------------------------------- */
.tpl-groups { margin-bottom: 2rem; }
.cat-list { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.cat-list.is-saving { opacity: 0.6; pointer-events: none; }
.cat-row {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  padding: 0.6rem 0; border-bottom: 1px solid var(--line);
}
.cat-row:last-child { border-bottom: 0; }
.cat-row.is-dragging { opacity: 0.45; }
.cat-list.is-sorting .cat-row { cursor: grabbing; }
.cat-grip { cursor: grab; color: var(--text-faint); font-size: 1rem; line-height: 1; user-select: none; }
.cat-rename { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 320px; min-width: 0; }
.cat-rename input { flex: 1 1 auto; min-width: 0; }
.cat-count { font-size: 0.78rem; color: var(--text-faint); white-space: nowrap; }
.cat-row form { margin: 0; }
.cat-add { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: center; }
.cat-add input { flex: 1 1 260px; min-width: 0; }

/* Template groups borrow the client-group card wholesale, so the two
   screens stay identical. Only the <details> specifics are added here:
   a native summary draws its own marker, and the caret has to key off
   [open] rather than the aria-expanded a button would carry. */
.tpl-group > .cgroup-head { list-style: none; }
.tpl-group > .cgroup-head::-webkit-details-marker { display: none; }
.tpl-group[open] > .cgroup-head .cg-caret { transform: rotate(90deg); }
/* The card grid sets display:grid, which defeats the browser's own hiding
   of a closed <details>. Hide the content explicitly instead. */
.tpl-group:not([open]) > *:not(summary) { display: none; }
.tpl-group-empty { margin: 0; }

/* ---- Product lists ----------------------------------------------------- */
.pg-cell { max-width: 220px; }
.pg-tag {
  display: inline-block; margin: 0 0.25rem 0.2rem 0; padding: 0.15rem 0.5rem;
  font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--line); border-radius: 999px;
}
.pg-choices {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--line); border-radius: var(--radius);
}

/* ---- Selections: clear the unlocked ----------------------------------- */
.sel-tidy {
  display: flex; align-items: center; gap: 0.85rem; flex-wrap: wrap;
  margin: 0; order: -1;   /* always the left-most control in .sel-sticky */
}
.sel-tidy[hidden] { display: none; }

/* ---- Dashboard: open tickets and open orders --------------------------- */
/* Two boards side by side under the To-action list; they stack when the
   column is too narrow to read them next to each other. */
.cu-boards {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem; margin-top: 1.25rem;
}
@media (max-width: 900px) { .cu-boards { grid-template-columns: 1fr; } }
.cu-board {
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.25rem 1.4rem;
}
.cu-board .cu-list-head { margin-bottom: 0.5rem; }
.cu-board .cu-list { margin: 0; }
.cu-board .cu-clear { margin: 0; }
/* The status badge sits between the detail and the arrow. */
.cu-board .cu-item { align-items: center; gap: 0.85rem; }
.cu-board .cu-item .badge { flex: none; }
.cu-board .cu-note { margin: 0.75rem 0 0; }
.cu-board .cu-note a { color: var(--text-muted); }
.cu-board .cu-note a:hover { color: var(--text); }

/* ---- Appearance chooser ------------------------------------------------ */
/* Same shape as the gallery purpose chooser, with a small mock of the
   interface instead of a description alone — the choice is visual, so the
   control should be too. */
.theme-choice { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
@media (max-width: 700px) { .theme-choice { grid-template-columns: 1fr; } }
.theme-opt {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.9rem 1rem; cursor: pointer;
  background: var(--surface-1);
  border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.theme-opt:hover { border-color: var(--line-strong); }
.theme-opt input { position: absolute; opacity: 0; pointer-events: none; }
/* The green highlight follows the checked radio alone — the saved theme
   is checked on load, so clicking the other option moves the highlight
   immediately, and only ever one is green at a time. */
.theme-opt:has(input:checked) { border-color: var(--ok); background: var(--surface-2); }
.theme-opt:has(input:checked) .theme-name { color: var(--ok); }
.theme-opt input:focus-visible + .theme-swatch { outline: 2px solid var(--ok); outline-offset: 3px; }

/* A two-tone chip standing in for the sidebar and the canvas. */
.theme-swatch {
  flex: none; display: flex; width: 46px; height: 34px;
  border: 1px solid var(--line-strong); border-radius: 4px; overflow: hidden;
}
.ts-bar  { width: 34%; }
.ts-body { flex: 1; }
.theme-swatch-dark  .ts-bar  { background: #121216; }
.theme-swatch-dark  .ts-body { background: #0e0e10; }
.theme-swatch-light .ts-bar  { background: #edeae4; }
.theme-swatch-light .ts-body { background: #e0dcd3; }

.theme-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.theme-name { font-size: 0.88rem; font-weight: 600; color: var(--text); }
.theme-blurb { font-size: 0.78rem; color: var(--text-faint); }

/* ---- Status editor ----------------------------------------------------- */
.status-list { list-style: none; margin: 0 0 1rem; padding: 0; }
.status-row {
  display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
  padding: 0.65rem 0; border-top: 1px solid var(--line);
}
.status-row:first-child { border-top: 0; }
.status-show { flex: none; margin: 0; }
/* The badge shows the live name in its real colour, so the effect of a
   rename is visible before saving. */
.status-swatch { flex: none; min-width: 8.5rem; text-align: center; }
/* Fixed width, not flex-grow, so every name field is the same width whether
   or not its row also carries a bucket picker and a delete button. */
.status-name { flex: 0 0 16rem; min-width: 0; max-width: 100%; }
.status-key {
  flex: none; font-size: 0.72rem; color: var(--text-faint);
  letter-spacing: 0.04em;
}
@media (max-width: 700px) {
  .status-swatch { min-width: 0; }
  .status-key { width: 100%; }
}

/* A custom status with no colour picked falls back to a neutral badge; a
   colour, when set, is applied inline and wins over this. */
.badge-custom { color: var(--text-muted); }

/* Colour picker: a compact native swatch plus an optional "Default" reset
   for built-ins (custom statuses always keep a colour). */
.status-color { flex: none; display: inline-flex; align-items: center; gap: 0.45rem; }
.status-color-pick {
  width: 2rem; height: 2rem; padding: 0; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: 8px; background: none;
}
.status-color-pick::-webkit-color-swatch-wrapper { padding: 2px; }
.status-color-pick::-webkit-color-swatch { border: none; border-radius: 6px; }
.status-color-pick::-moz-color-swatch { border: none; border-radius: 6px; }
.status-color-reset {
  font-size: 0.72rem; letter-spacing: 0.04em; color: var(--text-faint);
  background: none; border: 0; padding: 0; cursor: pointer;
}
.status-color-reset:hover { color: var(--text-muted); }

.status-bucket { flex: none; max-width: 9.5rem; }

.status-del {
  flex: none; font-size: 0.72rem; letter-spacing: 0.04em; cursor: pointer;
  color: var(--err); background: none;
  border: 1px solid transparent; border-radius: 6px; padding: 0.25rem 0.55rem;
}
.status-del:hover { border-color: rgba(217, 138, 138, 0.45); }

/* Add-a-status panel: one row that mirrors the editor's columns. */
.status-add { margin-top: 1.25rem; }
.status-add-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.status-add-row .status-name { flex: 0 0 16rem; min-width: 0; max-width: 100%; }

/* -- API access page (Lightroom plugin tokens) -------------------------- */
.steps { margin: 0.4rem 0 0; padding-left: 1.2rem; }
.steps > li { margin: 0.5rem 0; line-height: 1.5; }

.kv { margin: 0.6rem 0 0; display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1rem; }
.kv dt { color: var(--text-muted); }
.kv dd { margin: 0; }

.token-new { border-color: var(--accent); }
.token-reveal { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.5rem; }
.token-value {
  flex: 1 1 auto; min-width: 0; padding: 0.55rem 0.7rem;
  background: var(--surface-3); border: 1px solid var(--line-strong);
  border-radius: var(--radius); color: var(--text); letter-spacing: 0.02em;
}

.token-form { display: flex; gap: 0.6rem; align-items: center; margin: 0.5rem 0 1.1rem; flex-wrap: wrap; }
.token-form input[type="text"] { flex: 1 1 20rem; min-width: 0; }

.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.85rem; }
.btn.is-copied { color: var(--ok); border-color: var(--ok); }

/* -- Grid tools: Filter by Favorites + Compare mode (Portrait / Headshots) -- */
.gb-tool {
  -webkit-appearance: none; appearance: none;
  border: 1px solid var(--line-strong); background: transparent; color: var(--text);
  font: inherit; font-size: 0.9rem; line-height: 1.2;
  padding: 0.4rem 0.85rem; border-radius: 999px; cursor: pointer; white-space: nowrap;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.gb-tool:hover { border-color: var(--text-muted); }
/* On = green (the client's --ok is a dark green, so white text holds). */
body.client .gb-tool.is-on { background: var(--ok); border-color: var(--ok); color: #fff; }

/* Favorites tally on the Filter button. */
.gb-tool-count {
  display: inline-block; min-width: 1.35em; padding: 0 0.4em; margin-left: 0.15em;
  border-radius: 999px; text-align: center;
  background: rgba(var(--ink), 0.12); font-size: 0.82em; font-variant-numeric: tabular-nums;
}
body.client .gb-tool.is-on .gb-tool-count { background: rgba(255, 255, 255, 0.28); color: #fff; }

/* Compare-mode controls, hosted in the (sticky) gallery bar so they share the
   Submit row and pin as the grid scrolls. The message is a green pill. */
.gb-compare { display: flex; align-items: center; gap: 0.7rem; flex: 0 1 auto; min-width: 0; }
/* Direction, not a control — and it sits with the studio's other guidance now,
   not between two buttons where it read as a third one. */
.cmp-mode-banner {
  background: none; color: var(--text-muted);
  padding: 0; border-radius: 0; margin: 0.35rem 0 0;
  font-size: 0.86rem; font-style: italic;
}
.cmp-mode-banner[hidden] { display: none; }
.cmp-mode-open { border-radius: 999px; }
.cmp-mode-open:disabled { opacity: 0.5; cursor: default; }
/* Pulse once 1-3 images are chosen, to invite the click. */
.cmp-mode-open.is-ready { animation: afp-cmp-pulse 2.2s ease-in-out infinite; }
@keyframes afp-cmp-pulse {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(127, 200, 160, 0.9); }
  50%  { transform: scale(1.1);  box-shadow: 0 0 0 12px rgba(127, 200, 160, 0.12); }
  100% { transform: scale(1);    box-shadow: 0 0 0 18px rgba(127, 200, 160, 0); }
}
@media (prefers-reduced-motion: reduce) { .cmp-mode-open.is-ready { animation: none; } }

/* In compare mode every visible tile shows a grey frame; a chosen one turns
   green and gets a "Comparing" banner. The pick controls step aside so the
   whole tile is the compare target. */
body.client .proof-grid.cmp-mode .proof {
  cursor: pointer;
  outline: 3px solid color-mix(in srgb, var(--text) 32%, transparent);
  outline-offset: -3px; box-shadow: none;
}
body.client .proof-grid.cmp-mode .proof.is-comparing { outline-color: var(--ok); }
body.client .proof-grid.cmp-mode .proof.is-comparing::after {
  content: 'Comparing';
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
  background: var(--ok); color: #fff;
  text-align: center; padding: 0.32rem 0;
  font-size: 0.8rem; letter-spacing: 0.06em; text-transform: uppercase;
  pointer-events: none;
}
.proof-grid.cmp-mode .proof-pick,
.proof-grid.cmp-mode .proof-remove,
.proof-grid.cmp-mode .proof-retouch { display: none !important; }

/* Remove-from-favorites, top-left of each tile, only while filtering. */
.proof-remove {
  position: absolute; top: 0.5rem; left: 0.5rem; z-index: 3;
  width: 1.85rem; height: 1.85rem; border-radius: 50%;
  border: none; cursor: pointer; display: none;
  align-items: center; justify-content: center;
  background: rgba(20, 20, 22, 0.62); color: #fff; transition: background 0.15s;
}
.proof-remove:hover { background: var(--err); }
body.client .proof-grid.filtering .proof-remove { display: inline-flex; }

/* Review-window "Remove" (replaces the old Compare + × there). */
.sel-remove {
  -webkit-appearance: none; appearance: none;
  border: 1px solid var(--line-strong); background: transparent; color: var(--text-muted);
  font: inherit; font-size: 0.82rem; padding: 0.25rem 0.7rem;
  border-radius: 999px; cursor: pointer;
}
.sel-remove:hover { border-color: var(--err); color: var(--err); }

/* "Remove from Favorites" — a red banner across the bottom of each image in
   the Comparing view, revealed on hover (white text). */
.cmp-imgwrap { position: relative; flex: 1; min-height: 0; display: flex; }
.cmp-remove.is-on { background: rgba(190, 45, 45, 0.92); }
.cmp-remove {
  position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); z-index: 3;
  width: 100%; max-width: 100%;   /* JS narrows this to the rendered image width */
  -webkit-appearance: none; appearance: none; border: none; margin: 0;
  /* Green to add, red to remove — the same pairing the grid uses, so the
     colour says which way the click goes before you read the words. */
  background: rgba(47, 107, 79, 0.94); color: #fff;
  font: inherit; font-size: 0.85rem; letter-spacing: 0.02em;
  padding: 0.5rem; text-align: center; cursor: pointer;
  opacity: 0; transition: opacity 0.15s ease;
  border-radius: 0 0 4px 4px;
}
.cmp-imgwrap:hover .cmp-remove,
.cmp-remove:focus-visible { opacity: 1; }

/* Phase 2 with nothing filed yet: the client has just submitted and is waiting
   on the retouch. Replaces the approve UI entirely (see $awaitingFinals in
   client/gallery.php) rather than showing "these are your finals" over nothing. */
/* The waiting screen: news on the left, the photos it refers to on the
   right. Stacked, the message card was a full screen of its own and the
   images sat below the fold. Column widths follow the number of picks. */
.await-wrap {
  display: grid; gap: clamp(1.25rem, 3vw, 2.5rem);
  /* stretch, not start: both columns take the full height so the message can
     sit in the middle of its own and the photo column can own a scroller. */
  align-items: stretch;
  margin: 1.5rem 0 1.5rem;
  /* gallery.js sizes this to exactly the space left below it, so the PAGE has
     nothing to scroll and only the photos do. The cap is the no-JS fallback. */
  max-height: 78vh;
}
/* Thirds: the message takes one, the photos take two. */
.await-wrap { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
/* Nothing to sit beside: keep the old centred card. */
.await-wrap--alone { grid-template-columns: minmax(0, 34rem); justify-content: center; }
/* Centred in its third, vertically and horizontally: held at the top it hung
   off the corner of a tall column of photos with nothing under it. */
.await-finals {
  align-self: center;
  padding: 1.5rem 1.5rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: rgba(var(--ink), 0.03);
  text-align: center;
}
.await-wrap--alone .await-finals { margin: 3rem auto; padding: 2rem 1.75rem; }
.await-finals h2 {
  margin: 0 0 0.6rem;
  font-size: 1.15rem;
  font-weight: 500;
}
.await-finals p {
  margin: 0;
  color: var(--text-muted);
  line-height: 1.6;
}

/* Per-tile "Order a print" (phase 3 and prints-only galleries). The store used
   to be reachable only from inside the lightbox, so a client who never opened a
   photo had no route to it. Shares the approve bar's shape and position — the
   two never appear together (phase 2 vs phase 3). */
.proof-order {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  border: 0; cursor: pointer; padding: 0.5rem 0.4rem;
  font-size: 0.72rem; letter-spacing: 0.04em; color: #fff;
  background: rgba(11, 11, 12, 0.62); backdrop-filter: blur(4px);
  opacity: 0; transform: translateY(4px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.proof-order svg { display: block; }
.proof-order:hover { background: rgba(11, 11, 12, 0.82); }
.proof:hover .proof-order, .proof:focus-within .proof-order { opacity: 1; transform: none; }
/* Touch has no hover, so keep it visible there — same rule the approve bar uses. */
@media (max-width: 620px) { .proof-order { opacity: 1; transform: none; } }

/* "Don't have the link?" on the sign-in gate. Only shown for a gallery that
   belongs to one named person (see $tokenEntry) — a Headshot Station registrant
   is never sent a password at all, so the gate above it was asking for
   something that does not exist. Folded away so it stays secondary to the
   normal way in. */
.gate-resend { margin-top: 1.25rem; }
.gate-resend > summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--text-muted);
  list-style: none;
}
.gate-resend > summary::-webkit-details-marker { display: none; }
.gate-resend > summary:hover { color: var(--text); }
.gate-resend[open] > summary { margin-bottom: 0.75rem; }
.gate-resend .gate-note { margin-bottom: 0.6rem; }

/* Headshot Station: the confirmation shown straight after joining the queue.
   The number is the whole point — it is called out on the day and it names the
   person's files — so it gets display type instead of a small flash line. */
.station-joined { text-align: center; }
.sj-lead { margin: 0 0 1.5rem; font-size: 1.05rem; }
.sj-label {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.sj-number {
  margin: 0.15rem 0 1.25rem;
  font-family: var(--font-display, Georgia, serif);
  font-size: clamp(3.5rem, 18vw, 5.5rem);
  line-height: 1;
  font-weight: 500;
  letter-spacing: 0.02em;
}
.sj-note, .sj-wrong {
  max-width: 34ch;
  margin: 0 auto 0.9rem;
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--text-muted);
}
.sj-wrong { margin-bottom: 1.5rem; }
.sj-note strong { color: var(--text); font-weight: 500; }

/* ---- Clear test data -------------------------------------------------
   A destructive page, so it reads as one: the rules up front, a lock where a
   checkbox would be on anything that cannot go, and the reason next to it. */
.cl-intro .cl-rules {
  margin: 0.75rem 0 0; padding-left: 1.1rem;
  font-size: 0.9rem; line-height: 1.65; color: var(--text-muted);
}
.cl-intro .cl-rules li { margin-bottom: 0.3rem; }
.cl-intro .cl-rules strong { color: var(--text); }

.cl-table th.cl-tick, .cl-table td.cl-tick { width: 3.2rem; text-align: center; }
.cl-table td.cl-tick .checkbox { display: inline-flex; margin: 0; }
/* A row that cannot be removed steps back rather than shouting — the lock and
   the reason carry it. */
.cl-keep { opacity: 0.62; }
.cl-locked { font-size: 0.85rem; opacity: 0.75; cursor: help; }
.cl-why {
  display: block; margin-top: 0.15rem;
  font-size: 0.72rem; line-height: 1.4; color: var(--err); font-weight: 500;
}
.cl-note {
  display: block; margin-top: 0.1rem;
  font-size: 0.72rem; color: var(--text-muted); font-style: italic;
}

/* The count and the button travel together — the count is what makes the
   button safe to press, so it follows you down a long list rather than sitting
   at the bottom where you would press Remove without having read it. */
.cl-actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  position: sticky; bottom: 1rem; z-index: 4;
  padding: 1rem 1.25rem;
  /* Opaque: it floats over the last rows of the table. */
  background: var(--surface-2);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28);
}
.cl-count { margin: 0; font-size: 0.92rem; color: var(--text-muted); }
.cl-count strong { color: var(--text); }
/* Room for the bar to rest over, so the last row is never stuck behind it. */
[data-cleanup-form] { padding-bottom: 1rem; }

/* The one-click way in: the same front page, with a button where the three
   fields would be. Centred and given room, because it is the only thing to do
   on the page. */
.gate-oneclick { display: flex; flex-direction: column; align-items: flex-start; gap: 0.8rem; }
.gate-oneclick .btn { min-width: 12rem; }
.gate-oneclick .gate-note { margin: 0; }

/* The proofs the client chose, shown while they wait for the retouched
   versions. Read-only — no tick, no hover controls, nothing to press. They are
   there so a submitted gallery does not look like an empty one. */
/* The photo column: label held at the top, photos scrolling under it. */
.await-picks-col { display: flex; flex-direction: column; min-height: 0; }
.await-picks-head {
  flex: none;
  max-width: 34rem; margin: 0 auto 0.9rem; text-align: center;
  font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-muted);
}
/* Its own grid, not the masonry .proof-grid — that is a flex row, so with two
   picks each one stretched to half the page. The track count comes from the
   view (--await-cols, set from how many there are) and every track is 1fr, so
   the photos always fill the two-thirds they are given: a pair sit side by
   side at full size, a dozen tile at four across. auto-fill could not do this —
   it sizes tracks to a fixed minimum and leaves the remainder empty. */
/* The one scroller on this screen. min-height:0 is what lets it shrink inside
   the flex column instead of pushing the label off the top. */
.await-picks {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: grid; gap: clamp(0.6rem, 1.5vw, 1rem);
  grid-template-columns: repeat(var(--await-cols, 2), minmax(0, 1fr));
  align-items: start; margin-top: 0;
  /* THE PHOTOS OVERLAPPED WITHOUT THESE TWO. This grid has a definite height
     (it is the scroller), and `auto` rows in a definite-height container are
     stretched to divide it — two rows each took half the box, so a row holding
     a taller photo spilled 138px into the row beneath it. align-content alone
     does NOT fix that: the stretching happens when the tracks are SIZED, before
     anything is aligned. max-content sizes each row to its tallest tile;
     align-content: start then keeps them packed at the top when the photos do
     not fill the box.
     Invisible while every photo is the same shape — which is why it took a real
     gallery to show it. The rows only disagree once the crops do. */
  grid-auto-rows: max-content;
  align-content: start;
  /* Room for the last row to clear the bottom edge. */
  padding: 0 0.4rem 0.4rem;
}
/* The tile spans its whole track and is transparent; the PICTURE inside is
   what gets centred and sized. A fixed background here would show as a slab
   either side of a photo that is narrower than its track. */
.await-picks .proof { margin: 0; width: 100%; max-width: none; background: transparent; }
/* A ROW MUST ALWAYS FIT. --await-max-h is the height left in the visible area
   (set by gallery.js), so a photo can never be taller than the space it is
   being shown in — you should never have to scroll to see the bottom of one
   picture. Scrolling then only ever means "there is another row below".
   Both dimensions are MAX constraints against an auto size, so the ratio is
   carried between them and the picture is never cropped or squashed. That is
   also why the cap is not `height`: a definite height with .proof img's
   object-fit: cover is what quietly cropped these before. */
.await-picks .proof img {
  display: block;
  /* The widest this photo can be and still fit the height it is shown in.
     Height then follows from aspect-ratio, so it lands on the cap exactly and
     the box keeps the picture's true shape. */
  width: min(100%, calc(var(--await-max-h, 100vh) * var(--ar, 0.75)));
  height: auto;
  margin-inline: auto;
}
.proof.is-chosen-proof { cursor: default; }
/* Sizing for these lives in .await-picks .proof img, which is the only place
   .is-chosen-proof is used. This carried its own width:100% at the same
   specificity and simply came later in the file, so it silently won and the
   height cap could never take effect. Only the look is left here. */
.proof.is-chosen-proof img { display: block; border-radius: 3px; }
.proof.is-chosen-proof figcaption {
  margin-top: 0.35rem; font-size: 0.75rem; color: var(--text-muted); text-align: center;
}

/* Where a retouched file stands: a proof until the client approves it, a master
   file after. Shown per row so the two can never be confused for each other. */
.ml-state {
  display: inline-block; margin-right: 0.5rem;
  font-size: 0.64rem; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;
  padding: 0.08rem 0.4rem; border-radius: 3px; border: 1px solid currentColor;
}
.ml-state-ok   { color: var(--ok); }
.ml-state-redo { color: var(--err); }
.ml-state-wait { color: var(--text-muted); }

/* Waiting screen stacks on a phone: two columns of that width do not fit. */
@media (max-width: 780px) {
  /* No room for thirds on a phone: stack, let the page scroll as one, and cap
     the photos at two across however many there are. */
  .await-wrap {
    grid-template-columns: minmax(0, 1fr); justify-content: stretch;
    max-height: none; height: auto !important;
  }
  .await-finals { align-self: auto; }
  .await-picks {
    grid-template-columns: repeat(min(2, var(--await-cols, 2)), minmax(0, 1fr));
    overflow-y: visible; flex: none;
  }
}

/* ---- Change requests, shown with their photo --------------------------- */
/* The notes used to sit in one block above the image list, so matching a note
   to a photo meant reading two lists against each other. Each photo now
   carries its own, and the row is tinted so a redo is visible at a glance. */
.oa-item.has-change {
  background: rgba(224, 165, 106, 0.07);
  border-left: 3px solid var(--status-orange);
  padding-left: 0.7rem;
}
.oa-change-badge {
  align-self: flex-start;
  font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
  color: #17170f; background: var(--status-orange);
  padding: 0.15rem 0.5rem; border-radius: 999px;
}
.oa-item .oa-change-note { margin-top: 0.1rem; }

/* The change request reads in the empty space to the RIGHT of the photo rather
   than stacked under its name, so the row uses the width it already had.
   Narrow windows keep the old stack: name, then note, then the uploader. */
.oa-note-col { grid-column: 2; min-width: 0; }
.oa-item.has-note { row-gap: 0.35rem; }
.oa-item.has-note .oa-change-rev { grid-column: 2; }
.oa-note-col .oa-change-note { overflow-wrap: anywhere; }
/* Wide enough for two columns of text: the note moves into the empty space
   beside the photo, and the Upload-revision row drops under BOTH of them.
   That last part is what makes the split work — the picker and its button
   need about 400px side by side, so leaving them in the middle column would
   either wrap them or squeeze the note down to a ribbon.
   The cut-off is where an even split still leaves room for the badge:
   the row runs roughly the viewport less 720px (the 320px sidebar and the
   page chrome), so 1300px of viewport is about 290px a column. */
@media (min-width: 1300px) {
  .oa-item.has-note {
    grid-template-columns: 72px minmax(0, 1fr) minmax(0, 1fr);
  }
  .oa-item.has-note .oa-note-col   { grid-column: 3; grid-row: 1; }
  .oa-item.has-note .oa-change-rev { grid-column: 2 / -1; grid-row: 2; }
}
/* Answered and approved: the note is kept as the record of what was asked for,
   so its heading stays — but amber means "still owed", and this no longer is.
   Outlined and muted, same language as .ml-state-wait in the master list. */
.oa-change-badge.is-settled {
  color: var(--text-muted); background: none;
  border: 1px solid currentColor; border-radius: 3px;
  padding: 0.08rem 0.4rem; font-size: 0.6rem;
}
.oa-changes.is-settled {
  border-color: var(--line); background: none;
}
.oa-changes.is-settled .oa-changes-badge {
  color: var(--text-muted); background: none;
  border: 1px solid currentColor; border-radius: 3px; padding: 0.1rem 0.45rem;
}

/* A retouched proof a revision has replaced: still listed for the record,
   dimmed so it never reads as something the client can act on. */
.master-list li.ml-old { opacity: 0.62; }
.master-list li.ml-old .ml-name { text-decoration: line-through; }

/* Expiry + auto-delete, moved up beside the master files they govern. */
.oa-exp-field { margin-top: 0.9rem; }
.oa-exp-save { margin: 0.5rem 0 0; }

/* ---- Compare a revision with the original retouched proof --------------- */
/* Only the newest version of a photo is live in the grid, so this is the one
   place the edit it replaced can still be seen. Original left, latest right,
   same size, so the difference is the only thing that moves. */
.vc-inner {
  position: relative; width: 100%; max-width: 1180px;
  max-height: 92vh; overflow: hidden;
  display: flex; flex-direction: column;
  background: #fff; border-radius: 6px;
  padding: 1.5rem 1.75rem 1.75rem;
  box-shadow: 0 12px 48px rgba(26, 26, 28, 0.28);
}
.vc-inner > h2 { flex: none; font-size: 1.15rem; margin: 0 2rem 1rem 0; }
.vc-pair {
  flex: 0 1 auto; min-height: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem;
  align-items: start;
}
.vc-side { margin: 0; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.vc-side figcaption {
  flex: none; margin-top: 0.6rem; text-align: center;
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
}
.vc-side-new figcaption { color: var(--text); font-weight: 600; }
/* The image sizes itself against the panel, so both sides match however tall
   the photo is and neither pushes the buttons off the bottom. */
/* Shrink-wraps the image. Left at column width, the buttons — which are
   positioned against this box — ran wider than the portrait photo they are
   meant to sit on, overhanging it on both sides. */
.vc-stage {
  position: relative; min-height: 0;
  width: fit-content; max-width: 100%; margin-inline: auto;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-2); border-radius: calc(var(--radius-img) - 2px);
  overflow: hidden;
}
.vc-stage img {
  display: block; max-width: 100%; max-height: min(66vh, 620px);
  width: auto; height: auto; object-fit: contain;
}
/* The decision sits ON the new version — there is then no question which of
   the two photos is being approved. */
.vc-actions {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; gap: 1px;
}
.vc-actions .pa-btn { flex: 1 1 0; border-radius: 0; }
@media (max-width: 760px) {
  .vc-inner { padding: 1.25rem 1rem 1.5rem; }
  .vc-pair { grid-template-columns: 1fr; gap: 1rem; overflow-y: auto; }
  .vc-stage img { max-height: 42vh; }
}

/* "Replaces […]" on a retouched proof that arrived without saying what it
   answers. Sits on its own line under the file, so the row stays readable. */
.master-list li:has(.ml-links) { flex-wrap: wrap; }
.ml-links {
  display: flex; align-items: center; gap: 0.4rem;
  flex-basis: 100%; margin-top: 0.35rem;
}
.ml-links-label {
  font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint);
}
.ml-links-sel {
  flex: 1 1 auto; min-width: 0; max-width: 22rem;
  font: inherit; font-size: 0.8rem; padding: 0.25rem 0.4rem;
  color: var(--text); background: var(--surface-3);
  border: 1px solid var(--line); border-radius: 6px;
}

/* An approved photo whose master copy is not made yet — real, but not on the
   server. Muted so it never reads as a file you could delete or hand over. */
.master-list li.ml-pending { opacity: 0.72; }
.master-list li.ml-pending .ml-name { font-style: italic; }

/* ---- Favorites, by person (admin, Wedding & Event) ------------------ */
/* Added 083. One fold per visitor, biggest list first and open; the rest
   collapsed, because a forwarded wedding link can produce a dozen lists and
   only the couple's is usually wanted. Uses the same .panel shell as Master
   files so the two read as siblings on the page. */
.fav-panel .panel-head { display: flex; align-items: baseline; gap: 0.5rem 1rem; flex-wrap: wrap; }
.fav-tally { font-size: 0.82rem; color: var(--text-muted); white-space: nowrap; }
.fav-empty { color: var(--text-muted); font-size: 0.9rem; margin: 0.4rem 0 0; max-width: 62ch; }

.fav-person { border-top: 1px solid var(--line); }
.fav-person:last-of-type { border-bottom: 1px solid var(--line); }
.fav-person > summary {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem 1rem;
  flex-wrap: wrap; padding: 0.7rem 0.2rem; cursor: pointer; list-style: none;
}
.fav-person > summary::-webkit-details-marker { display: none; }
/* The disclosure arrow is drawn rather than inherited so it can sit tight to
   the name instead of at the row's left edge. */
.fav-person > summary::before {
  content: "▸"; display: inline-block; width: 1em; margin-right: 0.15rem;
  color: var(--text-faint); transition: transform 0.15s ease;
}
.fav-person[open] > summary::before { transform: rotate(90deg); }
.fav-person > summary:hover { color: var(--text); }
/* margin-right: auto, NOT text-align. The row is a flex line with THREE items —
   the drawn arrow, the name, the count — and `justify-content: space-between`
   pins the outer two and strands the middle one in the centre. Nothing was
   asking for the name to be centred; it was just the third item in a row built
   for two. An auto margin here takes the slack instead, so the name sits
   against the arrow and the count stays on the right. */
.fav-who {
  font-weight: 500; display: inline-flex; align-items: baseline;
  gap: 0.5rem; flex-wrap: wrap; margin-right: auto;
}
.fav-badge {
  font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 0.1rem 0.4rem; border-radius: 3px;
  background: var(--surface-3); color: var(--text-muted);
}
.fav-meta { font-size: 0.82rem; color: var(--text-muted); white-space: nowrap; }

.fav-body { padding: 0 0 1rem 1.15rem; }
.fav-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.fav-email { font-size: 0.84rem; color: var(--text-muted); }
.fav-hint { font-size: 0.82rem; color: var(--text-muted); margin: 0 0 0.85rem; max-width: 64ch; }

/* Wide enough to recognise a photo, small enough that forty fit without the
   panel becoming the page. Scrolls inside itself rather than widening it. */
.fav-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 0.6rem; max-height: 340px; overflow-y: auto;
}
.fav-cell { margin: 0; min-width: 0; }
.fav-cell img {
  width: 100%; aspect-ratio: 1; object-fit: cover; display: block;
  border-radius: 3px; background: var(--surface-3);
}
.fav-cell figcaption {
  font-size: 0.68rem; color: var(--text-faint); margin-top: 0.2rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* "Send my list" — the submit step marking does not otherwise have (083).
   Sits in the gallery bar beside "Download favorites" and borrows its quiet
   weight: this is a helpful signal, not the main event on the page. */
.fav-send {
  appearance: none; border: 1px solid var(--line); background: none;
  color: var(--text-muted); font: inherit; font-size: 0.85rem;
  padding: 0.22rem 0.7rem; border-radius: 999px; cursor: pointer;
  white-space: nowrap; transition: color 0.2s ease, border-color 0.2s ease;
}
.fav-send:hover { color: var(--text); border-color: var(--text-faint); }
/* Disabled-with-a-hint rather than hidden, so the ability is discoverable
   before there is anything to send — the same choice .dl-fav-link makes. */
.fav-send.is-empty { opacity: 0.5; cursor: not-allowed; }
.fav-send.is-empty:hover { color: var(--text-muted); border-color: var(--line); }
.fav-send.is-sent {
  color: var(--ok); border-color: var(--ok); cursor: default; opacity: 1;
}
/* "They pressed Send" — the completion signal marking has no other way to give. */
.fav-sent {
  color: var(--ok); font-size: 0.78rem;
  padding-left: 0.35rem; border-left: 1px solid var(--line); margin-left: 0.35rem;
}

/* ---- Wedding album panel (admin, Wedding & Event) -------------------- */
/* Added 084. The album is laid out in Fundy; this panel holds the exported
   spreads, checks their shape and keeps their order. Shares the .panel shell
   with Favorites and Master files so the three read as siblings. */
.album-panel .panel-sub strong { color: var(--text); font-weight: 500; }

/* The document specification — page trim, spread trim, bleed, dpi, the
   expected ratios and the fields behind them — used to live here. It came out
   on 2026-09-20 along with the checks it described: the design happens in
   Fundy, and this screen only has to show what came out of it. */
.ab-settings-row { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: flex-end; }
.ab-settings-row .field { margin: 0; flex: 1 1 12rem; }
.ab-settings-row .field.ab-narrow { flex: 0 0 7rem; }

/* The album's own name, on one line, where the settings form used to be. */
.ab-titleform { display: flex; gap: 0.5rem; align-items: center; margin: 0 0 0.9rem; }
.ab-titleform input { flex: 1 1 auto; min-width: 0; }
.ab-titleform .btn { flex: none; }

/* Dropzone, mirroring .gm-dropzone's shape so the two uploaders on this screen
   look like the same tool. */
.ab-dropzone {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  padding: 1.5rem 1rem; border: 1.5px dashed var(--line); border-radius: 6px;
  color: var(--text-muted); cursor: pointer; text-align: center;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.ab-dropzone:hover, .ab-dropzone:focus-visible, .ab-dropzone.is-over {
  border-color: var(--accent); color: var(--text); background: var(--surface-2);
}
.ab-dz-sub { font-size: 0.8rem; color: var(--text-faint); }
.ab-progress { font-size: 0.85rem; color: var(--text-muted); margin: 0.6rem 0 0; }
.ab-errors {
  list-style: none; margin: 0.6rem 0 0; padding: 0;
  font-size: 0.84rem; color: var(--err);
}
.ab-errors li { padding: 0.15rem 0; }
.ab-warn { font-size: 0.86rem; color: var(--status-orange); margin: 0.8rem 0 0; }
.ab-empty { color: var(--text-faint); font-size: 0.88rem; margin: 0.9rem 0 0; }

/* One row per spread. Grid rather than flex so the number, the thumbnails and
   the actions keep the same columns down the list however tall a row gets. */
.ab-list { list-style: none; margin: 1rem 0 0; padding: 0; }
.ab-row {
  display: grid; align-items: center; gap: 0 0.75rem;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  padding: 0.6rem 0; border-top: 1px solid var(--line);
}
.ab-row:last-child { border-bottom: 1px solid var(--line); }
.ab-row.is-partial { background: color-mix(in srgb, var(--status-orange) 7%, transparent); }
.ab-list.is-sorting .ab-row { transition: none; }
.ab-row.is-dragging { opacity: 0.6; }
.ab-grip {
  background: none; border: 0; padding: 0.3rem; color: var(--text-faint);
  cursor: grab; display: grid; place-items: center;
  /* Without this the browser claims the gesture and the row never moves on a
     touch screen. The up/down path is the guaranteed one either way. */
  touch-action: none;
}
.ab-grip:active { cursor: grabbing; }
.ab-num {
  font-variant-numeric: tabular-nums; font-size: 0.82rem; color: var(--text-muted);
  min-width: 1.6em; text-align: right;
}
.ab-thumbs { display: flex; gap: 0.4rem; align-items: flex-start; min-width: 0; }
.ab-thumb { margin: 0; }
.ab-thumb img {
  height: 54px; width: auto; max-width: 190px; object-fit: cover;
  border-radius: 3px; background: var(--surface-3); display: block;
}
.ab-thumb figcaption {
  font-size: 0.66rem; color: var(--text-faint); margin-top: 0.15rem; white-space: nowrap;
}
.ab-gone { font-size: 0.75rem; color: var(--text-faint); font-style: italic; }
.ab-meta { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.ab-name {
  font-size: 0.8rem; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ab-actions { display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.ab-inline { display: inline; margin: 0; }
.ab-foot { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-top: 1rem; }
.ab-note {
  font-size: 0.82rem; color: var(--text-faint); margin: 1rem 0 0;
  max-width: 68ch; line-height: 1.55;
}
.ab-note strong { color: var(--text-muted); }

@media (max-width: 640px) {
  .ab-row { grid-template-columns: auto auto minmax(0, 1fr); }
  .ab-actions { grid-column: 1 / -1; justify-content: flex-start; padding-left: 2.6rem; }
}

/* ---- Album pins (shared: couple's page, studio panel, print) ---------- */
/* Added 085. ONE set of rules for the one partial all three include, so a
   marker cannot sit in a different place on your screen than on theirs —
   which would be worse than no marker, because you would both trust it. */
.alp-stage { position: relative; display: block; width: 100%; background: var(--surface-3); }
.alp-img { width: 100%; height: 100%; object-fit: contain; display: block; }
.alp-stage.is-tappable { cursor: crosshair; }
/* The layer is sized by JS against the PAINTED image. inset:0 is already exact
   on a stage carrying the proof's own aspect-ratio, which is why stored pins
   render correctly with JavaScript off — the measuring only matters where
   something gives the image a max-height and object-fit letterboxes it. */
.alp-pins { position: absolute; inset: 0; pointer-events: none; }
.alp-pin {
  position: absolute;
  /* left/top place the marker's CORNER; the transform re-centres it on the
     point, so the marker's size never shifts its anchor and the touch target
     below can grow without moving the pin. */
  transform: translate(-50%, -50%);
  width: clamp(20px, 2.4vw, 30px); aspect-ratio: 1; border-radius: 50%;
  border: 2px solid #fff; background: var(--accent); color: #fff;
  font-size: 0.72rem; font-weight: 600; line-height: 1;
  display: grid; place-items: center; padding: 0; cursor: pointer;
  pointer-events: auto; box-shadow: 0 1px 4px rgba(0,0,0,0.45);
  font-variant-numeric: tabular-nums;
}
/* A 44px target on a 20px disc, without moving the anchor. */
.alp-pin::before { content: ''; position: absolute; inset: -12px; border-radius: 50%; }
.alp-pin:hover, .alp-pin:focus-visible { transform: translate(-50%, -50%) scale(1.12); }
.alp-pin.is-ghost { background: var(--text-faint); pointer-events: none; opacity: 0.85; }
.alp-pin.is-draft { background: var(--status-orange); }

/* ---- The couple's album review ----------------------------------------
   Every rule in here is either making the picture bigger or keeping a control
   on screen beside it. The page is furniture around a photograph being judged,
   and the furniture used to cost 356px above the picture on a 900px screen. */

/* ONE ROW: the album's name, where it is up to, and the two page-wide
   controls. The standard gallery bar spends 86px on the title alone. */
.ar-bar {
  padding-top: calc(0.65rem - (0.1rem * var(--gb-p)));
  padding-bottom: calc(0.65rem - (0.1rem * var(--gb-p)));
  margin-bottom: 0.85rem;
  align-items: center;
}
.ar-bar .gb-left { align-items: baseline; gap: 0.7rem; }
.ar-bar .gb-left h1 { font-size: clamp(1.05rem, 1.9vw, 1.45rem); margin: 0; }
.ar-bar .gb-count { white-space: nowrap; }
.ar-bar-tools { gap: 0.9rem; flex-wrap: wrap; }
.ar-tool { display: inline-flex; align-items: center; gap: 0.45rem; margin: 0; }
.ar-filter-label {
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint);
}
.ar-tool select, .ar-side-pick select { font-size: 0.85rem; }
.ar-rail-btn { white-space: nowrap; }

.ar-message, .ar-stamp {
  max-width: 82ch; margin: 0 auto 0.85rem; padding: 0 1rem;
  color: var(--text-muted); font-size: 0.92rem; line-height: 1.5;
}
.ar-stamp { color: var(--ok); font-weight: 500; }
.ar-empty { text-align: center; color: var(--text-faint); padding: 3rem 1rem; }
.ar-none {
  text-align: center; color: var(--text-muted); font-size: 0.92rem;
  padding: 4rem 1rem; margin: 0;
}
.ar-none em { font-style: normal; color: var(--text); }

/* The gap only ever shows with JavaScript off, where all of them stack. */
.ar-spreads { display: flex; flex-direction: column; gap: 2.5rem; }
.ar-spread { width: 100%; min-width: 0; }

/* A FIXED box, whatever is in it. The note area scrolls inside itself rather
   than growing, so everything below the picture is the same height on every
   layout — which is what lets the picture be the same size on every layout.
   A stage that shrank by 90px whenever a spread happened to carry two notes
   made pressing Next feel like the page was slipping. The reserved space is
   not wasted either: it is the box you type in, waiting.

   The box comes FIRST, right under the picture, because tapping the picture
   puts the cursor in it. */
/* TWO COLUMNS, SIDE BY SIDE: the box you type in on the left, the notes
   already written on the right. Stacked, they were a narrow strip with half
   the page empty beside them, and the two heights added up against the
   picture. Beside each other they cost it the height of ONE of them.

   Both are capped. Run either the full width of a 27" monitor and the delete
   cross ends up most of a screen from the note it belongs to, which is the
   thing that brought the notes over here in the first place. */
.ar-underside { margin-top: 0.55rem; }
.ar-underside.is-open {
  display: flex; align-items: stretch; gap: 1.25rem;
  height: var(--ar-under-h, 8rem);
}
.ar-underside.is-open .ar-compose {
  order: -1; flex: 1 1 28rem; min-width: 0; max-width: 34rem;
  align-self: flex-start;
}
.ar-underside.is-open .ar-notes {
  flex: 1 1 20rem; min-width: 0; max-width: 32rem;
  min-height: 0; max-height: none; overflow-y: auto;
}
.ar-notes {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.35rem;
  max-height: 8.5rem; overflow-y: auto;
}
.ar-note {
  display: flex; align-items: flex-start; gap: 0.55rem;
  font-size: 0.9rem; line-height: 1.5; color: var(--text);
  padding: 0.3rem 0.5rem; border-radius: 4px;
}
.ar-note.is-lit { background: var(--surface-2); }
.ar-note.is-old { color: var(--text-muted); }
.ar-note.is-old em { font-style: normal; color: var(--text-faint); }
.ar-marker {
  flex: none; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  background: var(--accent); color: #fff; font-size: 0.7rem; font-weight: 600;
  display: grid; place-items: center; font-variant-numeric: tabular-nums;
}
.ar-note.is-draft .ar-marker { background: var(--status-orange); }
.ar-marker.is-flat { background: none; color: var(--text-faint); }
.ar-note-body { min-width: 0; }
/* Click the words to change them. An Edit button was tried first and it sat
   at the far end of a very wide row — a control you have to go looking for,
   for the one thing on the row you would obviously want to change. */
.ar-note-body.is-editable { cursor: text; border-radius: 3px; }
.ar-note-body.is-editable:hover { box-shadow: 0 1px 0 0 var(--text-faint); }
.ar-note-body.is-editable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The cross sits against the note, not against the far edge of the column.
   Pushed right it was the same problem the Edit button had: a control for one
   short line of text, parked where you have to go looking for it. A ragged
   right edge is the price and it is worth paying. */
.ar-note-del {
  margin-left: 0.15rem; flex: none;
  background: none; border: 0; color: var(--text-faint);
  cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.3rem;
}
.ar-note-del:hover { color: var(--err); }
/* Editing happens in place, in the row the note already occupies, so the list
   does not jump and you can still see which marker you are rewording. */
.ar-note-edit-box { flex: 1 1 auto; min-width: 0; display: flex; gap: 0.4rem; align-items: flex-start; }
.ar-note-edit-box textarea {
  flex: 1 1 auto; min-height: 2.1rem; resize: none; overflow: hidden;
  font: inherit; font-size: 0.88rem;
}
.ar-note-edit-box .ar-note-save,
.ar-note-edit-box .ar-note-cancel {
  background: none; border: 1px solid var(--line); border-radius: 3px; cursor: pointer;
  font: inherit; font-size: 0.78rem; padding: 0.2rem 0.5rem; color: var(--text-muted);
}
.ar-note-edit-box .ar-note-save { border-color: var(--ok); color: var(--ok); }

/* Inline, not a pop-up: there is no layer to go wrong, and it still works with
   JavaScript off. */
/* The button is the height of the TEXT BOX, not of the column the box sits
   in. As a stretched child of a full-height compose block it grew to whatever
   the notes list beside it needed, which made it a tall slab next to a one-line
   field. The block shrink-wraps the box; the button stretches to the block. */
.ar-compose { display: flex; gap: 0.6rem; align-items: stretch; }
/* No resize grip. Everything else on this page that can be resized has a
   proper margin to drag; a textarea corner inside a fixed-height box would
   just clip against it. It grows as you type instead, which is what the grip
   was being used for anyway. */
.ar-compose textarea {
  flex: 1 1 auto; min-height: 2.1rem; resize: none; overflow: hidden;
  font: inherit; font-size: 0.9rem;
}
.ar-compose button { white-space: nowrap; align-self: stretch; }
.ar-album-notes { margin: 0.9rem 0 0; }
.ar-album-notes > summary {
  cursor: pointer; font-size: 0.85rem; color: var(--text-muted); padding: 0.2rem 0;
}
.ar-album-notes > summary:hover { color: var(--text); }
.ar-album-notes .ar-notes { margin-top: 0.35rem; }

.ar-inline { display: flex; gap: 0.4rem; align-items: center; margin: 0; }
/* Disabled-with-a-hint rather than hidden, so the ability is discoverable
   before there is anything to send. */
.ar-send.is-empty { opacity: 0.55; cursor: not-allowed; }
/* The deep bottom padding is load-bearing. The action bar sticks to the
   bottom of the window but stops at the end of its containing block, so
   without this the last thing in that block — these two links, one of which is
   the printable version — would sit under the bar and never be reachable. */
.ar-help {
  text-align: center; font-size: 0.85rem; color: var(--text-faint);
  padding: 1.25rem 1rem 6rem; margin: 0;
}
.ar-print-link { margin-left: 0.75rem; }

/* ---- The printable album proof --------------------------------------- */
.ap-doc { max-width: 1000px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
.ap-head h1 { margin: 0 0 0.2rem; font-size: 1.4rem; }
.ap-sub { color: var(--text-muted); font-size: 0.88rem; margin: 0 0 0.8rem; }
.ap-hint { font-size: 0.85rem; color: var(--text-muted); margin: 0 0 1.5rem; }
.ap-spread { margin-bottom: 2rem; }
.ap-num { font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase;
          color: var(--text-faint); margin: 0 0 0.4rem; }
.ap-notes { margin: 0.6rem 0 0; padding-left: 0; list-style: none;
            font-size: 0.88rem; line-height: 1.55; }
.ap-notes li { display: flex; gap: 0.5rem; align-items: flex-start; margin-bottom: 0.3rem; }
.ap-mk { flex: none; width: 1.3rem; height: 1.3rem; border-radius: 50%;
         background: var(--accent); color: #fff; font-size: 0.7rem; font-weight: 600;
         display: grid; place-items: center; }
.ap-round { color: var(--text-faint); font-style: italic; }

@media print {
  /* No @page here. A second top-level one would change orientation for every
     printable page in the app, and nested inside this block Safari drops every
     rule that follows it — which has already killed the handout's print fixes
     once. The on-screen hint asks for Landscape instead. */
  .no-print { display: none !important; }
  .ap-doc { max-width: none; padding: 0; }
  .ap-spread { break-inside: avoid; break-after: page; }
  .ap-spread:last-child { break-after: auto; }
  /* Markers must actually print. Safari needs the prefix, and Safari is the
     browser this gets printed from. */
  .alp-pin, .ap-mk, .ar-marker {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .alp-stage { background: none; }
}

/* "See round N": the earlier picture swaps into the SAME box. */
.ar-versions { position: relative; }
.ar-compare {
  position: absolute; top: 0.6rem; right: 0.6rem; z-index: 15;
  background: rgba(11,11,12,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.35);
  border-radius: 999px; padding: 0.2rem 0.7rem; font-size: 0.78rem; cursor: pointer;
  backdrop-filter: blur(4px);
}
.ar-compare:hover { background: rgba(11,11,12,0.8); }
.ar-compare[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); }

/* Where retouching comes from, now that the book has no list of its own:
   what the couple marked. A plain line rather than a fold — there is nothing
   here to open, only somewhere to go. */
.ab-retouch-hint {
  margin: 0.9rem 0 0; padding: 0.7rem 0.85rem; background: var(--surface-2);
  border-radius: 4px; color: var(--text-muted); font-size: 0.86rem; line-height: 1.55;
}
.ab-retouch-hint strong { color: var(--text); }

/* What the extras come to, said before they approve. */
.ar-balance {
  max-width: 62ch; margin: 1.5rem auto 0; padding: 0.85rem 1rem;
  background: var(--surface-2); border-radius: 4px;
  color: var(--text-muted); font-size: 0.9rem; line-height: 1.55; text-align: center;
}
.ab-price-fold > summary { cursor: pointer; padding: 0.5rem 0; font-size: 0.88rem; color: var(--text-muted); }
.ab-price-fold > summary:hover { color: var(--text); }
.ab-price .checkbox { margin: 0.3rem 0 0.6rem; font-size: 0.88rem; }

/* ---- Album review: the stage, the deck, the rail, the strip, the viewer ----
   The album sits LEFT and the wedding sits right, because the album is the
   thing being judged and the panel is the reference you glance at. Reversing
   them would put the reference where the eye lands first. */
.ar-layout {
  display: grid; gap: 1.25rem;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--ar-rail-w, clamp(14rem, 21vw, 23rem)));
  align-items: start;
}
/* The wedding costs the album about a quarter of its width. On a laptop that
   is the difference between a big picture and a small one, so hiding it is one
   named button away and the answer is remembered per browser. */
.ar-layout.no-rail { grid-template-columns: minmax(0, 1fr); }
.ar-layout.no-rail .ar-side { display: none; }
.ar-main { min-width: 0; }

/* ---- THE ONE MEASUREMENT ------------------------------------------------
   --ar-stage-h is what the window has left once the furniture above and below
   is counted; gallery.js measures it for real and the clamp is what a browser
   running no JavaScript gets. The box is a FIXED height, not one that shrinks
   to each picture: pressing Next twenty times should not make the buttons
   underneath jump as a single page follows a full spread. */
.ar-stagebox {
  --fit-h: var(--ar-stage-h, clamp(13rem, calc(100vh - 25rem), 64rem));
  height: var(--fit-h);
  display: flex; align-items: center; justify-content: center;
}

/* The contain fit, done in CSS and exact: the narrower of the width the height
   allows and the width there is, with aspect-ratio giving the height back.
   Clamping a full-height box with max-width instead leaves the BOX too wide
   while the picture letterboxes inside it — and the box is what the markers
   are positioned on.

   A page-mode album's left and right pages are ONE opening and sit side by
   side, flush, the way the book opens. Stacked vertically — which is what
   happened before — they were two pictures of nothing in particular. */
.ar-opening {
  display: flex; gap: 0; align-items: stretch; justify-content: center;
  margin: 0 auto;
  width: min(100%, calc(var(--fit-h) * var(--open-ar)));
  aspect-ratio: var(--open-ar);
}
.ar-stagebox .ar-versions { flex: 1 1 0; min-width: 0; height: 100%; }
.ar-stagebox .ar-versions > div { height: 100%; }
.ar-stagebox .alp-stage { width: 100%; height: 100%; }

/* ---- One row under the picture: where you are, how to move, what you think */
/* ONE cluster, hugging the left, not two groups pushed to opposite ends. The
   couple presses Next and then Choose over and over; with the arrows at one
   end of a 1000px row and the verdicts at the other, every decision was a
   journey across the screen. */
.ar-controls {
  display: flex; align-items: center; gap: 0.6rem 1.5rem; flex-wrap: wrap;
  margin-top: 0.7rem;
}
.ar-nav { display: inline-flex; align-items: center; gap: 0.4rem; }
.ar-arrow {
  background: none; border: 1px solid var(--line); border-radius: 4px;
  width: 2.2rem; height: 2.2rem; line-height: 1; font-size: 1.25rem;
  color: var(--text-muted); cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.ar-arrow:hover:not(:disabled) { color: var(--text); border-color: var(--text-faint); }
.ar-arrow:disabled { opacity: 0.35; cursor: default; }
.ar-deck-at {
  font-size: 0.85rem; color: var(--text-muted);
  min-width: 9.5rem; text-align: center; font-variant-numeric: tabular-nums;
}

/* Lit state is colour AND a word beside them: a couple deciding on twenty-five
   layouts should not have to remember which shade meant which. */
.ar-verdict {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}
.ar-pick.is-on { border-color: var(--ok); color: var(--ok); }
.ar-drop.is-on { border-color: var(--err); color: var(--err); }
.ar-verdict-say { font-size: 0.82rem; color: var(--text-muted); }

/* ---- The wedding, beside the album ---- */
.ar-side {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.75rem; position: sticky; top: 5.5rem;
}

/* ---- The two margins you can drag --------------------------------------
   Both sit IN the gap between panels rather than inside either one, so the
   hit area is bigger than the line it draws and nothing moves when you grab
   it. Invisible until you go near: a page with two permanent grab-bars on it
   reads as a tool, and this is a page for looking at photographs. */
.ar-grip { position: absolute; z-index: 5; background: none; border: 0; padding: 0; }
.ar-grip::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  background: var(--text-faint); border-radius: 2px; opacity: 0;
  transition: opacity 0.15s ease;
}
.ar-grip:hover::after, .ar-grip:focus-visible::after, .ar-grip.is-dragging::after { opacity: 0.55; }
.ar-grip:focus-visible { outline: none; }
.ar-grip-col {
  top: 0; bottom: 0; left: -0.75rem; width: 0.9rem; cursor: col-resize;
}
.ar-grip-col::after { width: 3px; height: 2.5rem; }
.ar-grip-row {
  left: 0; right: 0; top: -0.45rem; height: 0.9rem; cursor: row-resize;
}
.ar-grip-row::after { height: 3px; width: 3rem; }
/* While a drag is in flight the cursor belongs to the drag, and nothing may be
   selected under it. The pointer is CAPTURED by the grip rather than the page
   being made inert — crossing a photograph mid-drag would otherwise hand the
   gesture to the browser's own picture-dragging and the panel would stick. */
body.ar-dragging { user-select: none; cursor: col-resize; }
body.ar-dragging.is-row { cursor: row-resize; }
.ar-side-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.ar-side-head h2 { font-size: 0.95rem; margin: 0; }
.ar-side-count { font-size: 0.78rem; color: var(--text-faint); }
.ar-side-pick { margin: 0.6rem 0 0.5rem; display: flex; flex-direction: column; gap: 0.25rem; }
/* As tall as the picture beside it, so the two columns end together. */
.ar-side-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 0.4rem; overflow-y: auto;
  /* gallery.js replaces this with the real number once it knows how tall the
     album beside it came out. This is the answer before it does. */
  max-height: 26rem;
}
.ar-photo { margin: 0; min-width: 0; }
/* CONTAIN, not cover. This panel answers "could we use a different picture
   here?", and a square crop of a tall photograph answers it wrongly. */
.ar-photo-open {
  display: block; width: 100%; padding: 0; border: 1px solid transparent;
  background: var(--surface-3); border-radius: 3px; cursor: zoom-in;
}
.ar-photo-open:hover { border-color: var(--text-faint); }
.ar-photo-open img { display: block; width: 100%; height: 104px; object-fit: contain; }
.ar-photo figcaption {
  font-size: 0.66rem; color: var(--text-faint); margin-top: 0.15rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- The bottom of the window is the book ------------------------------
   The strip is what makes twenty-five layouts navigable: "Spread 7 of 25" is
   a number, and a couple remembers a picture. Each thumbnail keeps its own
   shape, so the single pages at the front and back of a Fundy book read as
   different from the spreads between them, and carries its verdict colour, so
   what has been decided so far reads across the whole book at a glance.

   One band, not two: navigation and the two buttons share a row, because every
   pixel this bar takes is a pixel off the picture above it. */
.ar-actions {
  --ar-chip-h: 96px;
  position: sticky; bottom: 0; z-index: 20;
  /* STRETCH, not centre: the strip takes its height from the stack of controls
     beside it, so the stack is the bar's minimum height. */
  display: flex; gap: 1rem; align-items: stretch;
  padding: 0.45rem clamp(1rem, 4vw, 2.5rem) 0.5rem;
  margin: 0.9rem calc(-1 * clamp(1rem, 4vw, 2.5rem)) 0;
  background: var(--surface-1); border-top: 1px solid var(--line);
}
.ar-strip-wrap { position: relative; flex: 1 1 auto; min-width: 0; display: flex; }
/* A layout half off the end of the strip should LOOK half off the end, rather
   than stopping at a hard edge that reads as the end of the album. Painted on
   the wrapper, not the strip, or they would scroll away with the pictures. */
.ar-strip-wrap::before,
.ar-strip-wrap::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 3rem;
  pointer-events: none; z-index: 2; opacity: 0;
  transition: opacity 0.2s ease;
}
.ar-strip-wrap::before {
  left: 0;
  background: linear-gradient(to right, var(--surface-1), rgba(0, 0, 0, 0));
}
.ar-strip-wrap::after {
  right: 0;
  background: linear-gradient(to left, var(--surface-1), rgba(0, 0, 0, 0));
}
.ar-strip-wrap.has-left::before,
.ar-strip-wrap.has-right::after { opacity: 1; }

.ar-strip {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 0.28rem;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: thin;
  padding: 0.2rem 0 0.3rem; scroll-behavior: smooth;
}
.ar-chip {
  position: relative; flex: none; padding: 0; line-height: 0; cursor: pointer;
  background: var(--surface-3); border: 2px solid transparent; border-radius: 3px;
}
.ar-chip img { display: block; height: var(--ar-chip-h); width: auto; border-radius: 1px; }
.ar-chip-n {
  position: absolute; left: 2px; bottom: 2px;
  font-size: 0.6rem; line-height: 1; padding: 1px 3px; border-radius: 2px;
  background: rgba(11, 11, 12, 0.55); color: #fff; font-variant-numeric: tabular-nums;
}
.ar-chip:hover { border-color: var(--text-faint); }
/* The strip is a row of pictures with no text, so a focus ring is the only
   thing telling a keyboard user where they are in it. */
.ar-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ar-arrow:focus-visible, .ar-lb-arrow:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* Verdict on the BORDER and "you are here" on an OUTLINE outside it, so a
   chosen layout you happen to be looking at still shows that it was chosen. */
.ar-chip[data-verdict="chosen"] { border-color: var(--ok); }
.ar-chip[data-verdict="remove"] { border-color: var(--err); }
.ar-chip.is-at { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ONE COLUMN: the count of the book, then Send, then Approve. Stacked, they
   take a third of the width they took side by side — which all goes to the
   filmstrip — and their height becomes the floor for the strip beside them,
   which is what makes the bar read as one band rather than a row of buttons
   with some pictures next to it. */
.ar-actions-do {
  flex: none; display: grid; grid-template-columns: minmax(11rem, auto);
  gap: 0.35rem; align-items: stretch; justify-items: stretch;
}
.ar-actions-do .btn { width: 100%; justify-content: center; }

/* How big the book is, against what the album includes. Same shape and the
   same two states as the selection counter on a portrait gallery, because it
   answers the same question and there is no reason for it to look different. */
.ar-tally {
  display: flex; align-items: baseline; justify-content: center; gap: 0.3rem;
  font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-muted); white-space: nowrap;
}
.ar-tally strong {
  font-family: var(--font-display); font-size: 1.05rem; color: var(--text);
  letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums;
}
.ar-tally.is-complete strong { color: var(--ok); }
.ar-tally.is-over strong { color: var(--status-orange); }
.ar-tally .over-tag {
  font-size: 0.72rem; letter-spacing: 0; text-transform: none;
  color: var(--status-orange); font-variant-numeric: tabular-nums;
}

/* ---- The viewer ---- */
/* Deliberately close to the edges. On a 1366x700 laptop the page can only
   give a single page 280px; here the same picture is 600. This IS the review
   surface on a small screen, which is why the arrows work inside it. */
.ar-lightbox {
  position: fixed; inset: 0; z-index: 1400;
  background: rgba(11, 11, 12, 0.94);
  /* A COLUMN. The picture and the note box under it are siblings in here, and
     as a row they sat beside each other — the note box quietly taking a third
     of the width the picture was supposed to get. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 1.25rem 4.25rem;
}
body.ar-lb-open { overflow: hidden; }
/* WIDTH 100%, not shrink-to-fit. The opening inside sizes itself with
   min(100%, ...) — with a shrink-to-fit ancestor that 100% has nothing to
   resolve against and the picture comes out several hundred pixels narrower
   than the window it was given. */
.ar-lb-stage {
  margin: 0; width: 100%; max-height: 100%; text-align: center;
  display: flex; flex-direction: column; align-items: center; min-height: 0;
}
.ar-lb-pic { max-width: 100%; min-height: 0; width: 100%; }
.ar-lb-pic > img { max-width: 100%; max-height: 82vh; display: block; margin: 0 auto; }
/* `display: block` above outranks the hidden attribute's own display: none. */
.ar-lb-pic > img[hidden] { display: none; }
/* The deck's own box, fitted to the window instead of to what is left of the
   page. Everything inside it — the opening, the stages, the pin layers — is
   already handled by the .ar-stagebox rules above. */
.ar-lb-box { --fit-h: var(--ar-lb-h, 76vh); }
.ar-lightbox.has-note .ar-lb-box { --ar-lb-h: 66vh; }
.ar-lb-box .alp-stage { background: none; }
.ar-lb-note {
  display: flex; gap: 0.5rem; align-items: flex-start;
  width: min(46rem, 100%); margin: 0.8rem auto 0;
}
.ar-lb-note textarea {
  flex: 1 1 auto; min-height: 2.1rem; resize: none; overflow: hidden;
  font: inherit; font-size: 0.9rem;
  background: rgba(255, 255, 255, 0.95); border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 4px; padding: 0.4rem 0.55rem; color: #14141a;
}
/* The house quiet button is near-black on near-white. On this overlay it was
   near-black on near-black — the button was there the whole time and simply
   could not be seen. */
.ar-lb-note .btn {
  flex: none; white-space: nowrap; align-self: stretch;
  background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.35);
  color: rgba(255, 255, 255, 0.9);
}
.ar-lb-note .btn:hover { background: rgba(255, 255, 255, 0.2); color: #fff; }
.ar-lb-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 3rem; height: 3.4rem; display: none; place-items: center;
  background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 4px; color: rgba(255, 255, 255, 0.85);
  font-size: 1.6rem; line-height: 1; cursor: pointer;
}
.ar-lightbox.is-walking .ar-lb-arrow { display: grid; }
.ar-lb-arrow:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
.ar-lb-arrow.is-prev { left: 0.6rem; }
.ar-lb-arrow.is-next { right: 0.6rem; }
.ar-lb-stage figcaption {
  color: rgba(255,255,255,0.75); font-size: 0.82rem; margin-top: 0.6rem; flex: none;
}
.ar-lb-close {
  position: absolute; top: 1rem; right: 1.25rem;
  background: none; border: none; color: rgba(255,255,255,0.8);
  font-size: 2rem; line-height: 1; cursor: pointer;
}
.ar-lb-close:hover { color: #fff; }

/* Below the width where a rail still leaves room for a spread, the wedding
   goes underneath rather than beside — and the album gets the whole width.
   1150px, not 1000: at 1024 the rail is down to its 13rem floor and still
   takes a quarter of the row, which left a 24x9 spread 712px wide when the
   same window gives it 933 with the wedding stacked underneath. */
@media (max-width: 1150px) {
  .ar-layout { grid-template-columns: minmax(0, 1fr); }
  .ar-side { position: static; }
  .ar-side-grid { max-height: 22rem; }
}
/* A short window is the hard case: the furniture is a fixed cost and the
   picture pays it. These are the pixels that can be spared without losing
   anything — and "View larger" is the real answer at this size. */
@media (max-height: 780px) {
  .ar-actions { --ar-chip-h: 56px; }
  .ar-message { font-size: 0.86rem; margin-bottom: 0.55rem; line-height: 1.4; }
  .ar-controls { margin-top: 0.5rem; }
  .ar-underside { margin-top: 0.4rem; }
}

@media (max-width: 760px) {
  /* No room for two columns, so they go back to being one. */
  .ar-underside.is-open { flex-direction: column; gap: 0.5rem; height: auto; }
  .ar-underside.is-open .ar-compose,
  .ar-underside.is-open .ar-notes { max-width: none; flex: 0 0 auto; }
  .ar-underside.is-open .ar-notes { max-height: 9rem; }
  .ar-controls { gap: 0.5rem; }
  .ar-deck-at { min-width: 0; }
  .ar-compose { flex-direction: column; }
  .ar-compose textarea { width: 100%; }
  .ar-actions { flex-wrap: wrap; }
  .ar-actions-do { width: 100%; justify-content: center; }
}
