/* Shared styles for the advertiser portal (signup, login, dashboard,
   campaign form) -- same design tokens as the rest of the site
   (see advertise.html), split into its own file since these four pages
   share far more markup/behavior with each other than with the marketing
   pages. */
:root {
  color-scheme: light dark;
  --accent: #5985E8;
  --accent-dim: #E6ECFB;
  --merge: #D69429;
  --merge-dim: #FBF0DD;
  --bg: #ffffff;
  --bg-alt: #F6F7F8;
  --card: #ffffff;
  --text: #000000;
  --text-secondary: #55555A;
  --border: #E3E3E5;
  --danger: #C4453C;
  --danger-dim: #FBDDDD;
  --success: #2E8B57;
  --success-dim: #E1F3E9;
  --shadow: 0 20px 60px -20px rgba(0,0,0,0.25);
}
@media (prefers-color-scheme: dark) {
  :root {
    --accent: #759EF7;
    --accent-dim: #1C2A4A;
    --merge: #F2B045;
    --merge-dim: #3A2C10;
    --bg: #121214;
    --bg-alt: #18181B;
    --card: #1C1C1F;
    --text: #F2F2F5;
    --text-secondary: #A6A6AD;
    --border: #303034;
    --danger: #E37167;
    --danger-dim: #3A1E1C;
    --success: #5FBE86;
    --success-dim: #17301F;
    --shadow: 0 20px 60px -20px rgba(0,0,0,0.6);
  }
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* var(--bg-alt), not var(--bg) (2026-08-25, Ben's "too white" note) --
     every card on these pages (.auth-card/.form-card/.review-card/
     .campaign-card/etc.) uses var(--card), which is pure white. Against a
     white page they were nearly indistinguishable from the background
     they sat on; against this light-grey page they actually read as
     cards. Elements that already used var(--bg-alt) for their own fill
     (.btn-secondary, .review-thumb-empty, .perf-stat, etc.) all keep a
     1px border, so they stay visible rather than blending into the page. */
  background: var(--bg-alt);
  color: var(--text);
  line-height: 1.6;
}
a { color: var(--accent); text-decoration: none; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px; }

header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 85%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}
header .wrap { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.wordmark { font-size: 19px; font-weight: 800; letter-spacing: -0.02em; color: var(--text); }
.wordmark span { color: var(--accent); }
nav { display: flex; align-items: center; gap: 20px; }
nav a { color: var(--text-secondary); font-size: 14px; font-weight: 600; }
nav a:hover { color: var(--text); }
.nav-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--accent-dim); color: var(--accent);
  font-size: 14px; font-weight: 800; overflow: hidden;
}
.nav-avatar:hover { opacity: 0.85; }
.nav-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

.btn-primary, .btn-secondary, .btn-danger {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-size: 15px; font-weight: 700; padding: 13px 22px; border-radius: 100px;
  border: none; cursor: pointer; font-family: inherit;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { opacity: 0.9; }
.btn-primary:disabled { opacity: 0.55; cursor: default; }
.btn-secondary { background: var(--bg-alt); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { opacity: 0.9; }
.btn-danger:disabled { opacity: 0.55; cursor: default; }

.auth-shell {
  min-height: calc(100vh - 65px);
  display: flex; align-items: center; justify-content: center;
  padding: 40px 24px;
}
.auth-card {
  width: 100%; max-width: 420px;
  background: var(--card); border: 1px solid var(--border); border-radius: 24px;
  padding: 36px 32px;
}
.auth-card h1 { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 6px; }
.auth-card .sub { font-size: 14px; color: var(--text-secondary); margin: 0 0 26px; }

.field { margin-bottom: 16px; }
.field label { display: block; font-size: 13px; font-weight: 700; margin-bottom: 6px; }
.field .hint { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
.char-count { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0; text-align: right; }
.char-count.near-limit { color: var(--merge); }
input[type=text], input[type=email], input[type=password], input[type=url], textarea, select {
  width: 100%; font-family: inherit; font-size: 15px; padding: 11px 14px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--bg);
  color: var(--text);
}
textarea { resize: vertical; min-height: 90px; }
input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Show/hide toggle on password fields -- wraps the input so the button can
   sit on top of it rather than taking its own layout space. Selector
   targets any input inside the wrapper, not input[type=password]
   specifically, since the JS toggle flips the type to "text" on click and
   the padding needs to stay reserved either way. */
.password-field { position: relative; }
.password-field input { padding-right: 44px; }
.password-toggle {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer; color: var(--text-secondary); padding: 0;
}
.password-toggle:hover { color: var(--text); }

/* Strip the native OS dropdown chrome so a <select> reads as a plain text
   field like everything else in the form -- still a real dropdown
   underneath (businesses.category is a fixed 5-value enum, see
   SUPABASE_SCHEMA_TODO.md §190), just restyled rather than swapped for
   free text, which would let someone type a value the database rejects. */
select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2355555A' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 34px;
}
select:invalid { color: var(--text-secondary); }
select option { color: var(--text); background: var(--bg); }

.tier-choice { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 4px; }
.tier-option {
  border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px; cursor: pointer;
  position: relative;
}
.tier-option input { position: absolute; top: 14px; right: 14px; }
.tier-option .tier-name { font-weight: 700; font-size: 14px; margin-bottom: 3px; }
.tier-option .tier-price { font-size: 13px; color: var(--text-secondary); }
.tier-option:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
@media (max-width: 480px) { .tier-choice { grid-template-columns: 1fr; } }

.banner { border-radius: 12px; padding: 12px 16px; font-size: 14px; margin-bottom: 18px; }
.banner.error { background: var(--danger-dim); color: var(--danger); }
.banner.success { background: var(--success-dim); color: var(--success); }

.foot-link { text-align: center; font-size: 14px; color: var(--text-secondary); margin-top: 18px; }

/* Dashboard */
.dash-header { display: flex; align-items: center; justify-content: space-between; padding: 44px 0 28px; gap: 16px; flex-wrap: wrap; }
.dash-header h1 { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 4px; }
.dash-header .sub { font-size: 14px; color: var(--text-secondary); margin: 0; }
.dash-actions { display: flex; gap: 10px; }

.campaign-list { display: flex; flex-direction: column; gap: 16px; padding-bottom: 60px; }
.campaign-card {
  border: 1px solid var(--border); border-radius: 18px; padding: 20px 24px;
  display: flex; align-items: center; gap: 18px; background: var(--card);
  text-decoration: none; color: inherit; transition: border-color 0.15s, transform 0.15s;
}
/* Status-colored left border, keyed off the .status-pill class already
   rendered inside each card -- :has() means this needed zero JS changes
   on either page that uses these cards (2026-08-25, Ben's "too plain"
   note). Same 3-way grouping as the color legend already in use
   elsewhere (status-pending_review/approved+paid+live/rejected+expired). */
.campaign-card:has(.status-pending_review) { border-left: 3px solid var(--merge); }
.campaign-card:has(.status-approved), .campaign-card:has(.status-paid), .campaign-card:has(.status-live) { border-left: 3px solid var(--success); }
.campaign-card:has(.status-rejected), .campaign-card:has(.status-expired) { border-left: 3px solid var(--danger); }
.campaign-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.campaign-card img { width: 56px; height: 56px; border-radius: 12px; object-fit: cover; background: var(--bg-alt); flex-shrink: 0; }
.campaign-card .info { flex: 1; min-width: 0; }
.campaign-card .headline { font-weight: 700; font-size: 16px; margin: 0 0 4px; }
.campaign-card .meta { font-size: 13px; color: var(--text-secondary); margin: 0; }
.status-pill { font-size: 12px; font-weight: 700; padding: 5px 12px; border-radius: 100px; white-space: nowrap; }
.status-pending_review { background: var(--merge-dim); color: var(--merge); }
.status-approved, .status-paid, .status-live { background: var(--success-dim); color: var(--success); }
.status-rejected, .status-expired { background: var(--danger-dim); color: var(--danger); }

.empty-state {
  border: 1px dashed var(--border); border-radius: 18px; padding: 56px 24px; text-align: center;
  color: var(--text-secondary); font-size: 15px;
}
/* CSS-only checkmark icon above every empty-state message (2026-08-25) --
   ::before/mask means this applies everywhere .empty-state is already
   used (admin-review.html's 2 empty states, advertise-dashboard.html's
   "no campaigns yet") with no JS/markup changes at any call site. Masked
   shape filled with var(--text-secondary) at low opacity so it reads as
   a quiet icon, not competing with the message text under it. */
.empty-state::before {
  content: "";
  display: block;
  width: 36px; height: 36px;
  margin: 0 auto 14px;
  background: var(--text-secondary);
  opacity: 0.32;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.3l2.4 2.4 4.6-5"/></svg>') center / contain no-repeat;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.3l2.4 2.4 4.6-5"/></svg>') center / contain no-repeat;
}

/* Campaign detail page */
.detail-rows { display: flex; flex-direction: column; gap: 14px; }
.detail-row { display: flex; flex-direction: column; gap: 3px; font-size: 14px; }
.detail-row .detail-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-secondary);
}

/* Wraps the campaign media on the detail page so it displays at a
   consistent 4:5 crop. A custom expand button + wrapper-level
   requestFullscreen() override was tried here to keep that same crop when
   enlarged (a raw <video>'s native fullscreen ignores object-fit entirely
   and shows the source at its own intrinsic orientation/size), but it
   collided with Safari's own native fullscreen button -- which
   `controlslist` can't hide there -- and left the page in a broken,
   fully-zoomed state on load. Reverted: just the browser's native
   `<video controls>` fullscreen now, uncropped when enlarged but reliable. */
.media-frame {
  position: relative; width: 100%; aspect-ratio: 4 / 5; border-radius: 16px;
  overflow: hidden; background: #000; margin-bottom: 20px;
}
.media-frame video, .media-frame img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}

/* Admin review page (admin-review.html) -- one card per campaign_submissions
   row, media + text on top, status-dependent action row underneath. Reuses
   .status-pill/.campaign-card's headline/meta type scale rather than
   introducing new ones. */
.review-list { display: flex; flex-direction: column; gap: 16px; padding-bottom: 60px; }
.review-card {
  border: 1px solid var(--border); border-radius: 18px; padding: 20px 24px;
  background: var(--card);
}
/* Same status-colored left border as .campaign-card above, same reasoning. */
.review-card:has(.status-pending_review) { border-left: 3px solid var(--merge); }
.review-card:has(.status-approved), .review-card:has(.status-paid), .review-card:has(.status-live) { border-left: 3px solid var(--success); }
.review-card:has(.status-rejected), .review-card:has(.status-expired) { border-left: 3px solid var(--danger); }
.review-card-top { display: flex; align-items: flex-start; gap: 16px; }
.review-thumb { width: 64px; height: 64px; border-radius: 12px; object-fit: cover; background: var(--bg-alt); flex-shrink: 0; }
.review-thumb-empty { background: var(--bg-alt); }
.review-card-info { flex: 1; min-width: 0; }
.review-card-info .headline { font-weight: 700; font-size: 16px; margin: 0 0 4px; }
.review-card-info .meta { font-size: 13px; color: var(--text-secondary); margin: 0; }
.review-promo-address { color: var(--merge); font-weight: 600; }
.review-card-actions {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);
}
.review-order-note { font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
.review-rejected-note { color: var(--danger); }

/* Status filter tabs at the top of admin-review.html -- plain toggle
   buttons rather than a <select>, since the count-per-status badge (e.g.
   "Pending (3)") only really works as always-visible chips. */
.review-filter-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.review-filter-tab {
  font-size: 13px; font-weight: 600; padding: 7px 14px; border-radius: 100px;
  border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-secondary);
  cursor: pointer; font-family: inherit;
}
.review-filter-tab:hover { border-color: var(--accent); color: var(--text); }
.review-filter-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Business/company search on admin-review.html -- sits in the static
   dash-header area, outside #content, so render() can fully replace
   #content on every keystroke without losing input focus/cursor
   position (see the JS comment on businessSearchInput). */
.review-search-row { position: relative; max-width: 360px; margin: 0 0 20px; }
.review-search-row input { padding-right: 34px; }
.review-search-clear {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer; color: var(--text-secondary); padding: 0;
  font-size: 18px; line-height: 1; font-family: inherit;
}
.review-search-clear:hover { color: var(--text); }

/* Status pill + "waiting X days" tag stack in the top-right of a review
   card (2026-08-25) -- previously just the bare status-pill span. */
.review-status-col { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.review-pending-age {
  font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 100px;
  background: var(--merge-dim); color: var(--merge); white-space: nowrap;
}
.review-pending-age-severe { background: var(--danger-dim); color: var(--danger); }

.review-copy-link {
  font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 100px;
  border: 1px solid var(--border); background: var(--bg); color: var(--text-secondary);
  cursor: pointer; font-family: inherit; margin-left: 6px;
}
.review-copy-link:hover { border-color: var(--accent); color: var(--accent); }

/* Groups .review-list by advertiser -- one header + one .review-list per
   business, groups sorted alphabetically so the page order stays stable
   as submissions change status underneath. */
.review-group { margin-bottom: 28px; }
.review-group-header {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding-bottom: 10px; margin-bottom: 14px; border-bottom: 1px solid var(--border);
}
.review-group-name { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.review-group-name a { color: inherit; }
.review-group-name a:hover { color: var(--accent); }
.review-group-meta { font-size: 13px; color: var(--text-secondary); }

/* Business detail page (admin-business-detail.html) -- stat cards +
   simple CSS-grid tables for order/submission history. Deliberately not
   <table> markup -- these rows need a status-pill component inline, which
   is simpler as grid cells than fighting table cell vertical-align. */
/* max-width matches .form-card's, but flush-left (margin: 0, not auto) --
   every .form-card on this page (business-info, Live right now, Order
   history, Submission history) carries an inline `margin: 0 0 24px`
   that zeroes its horizontal margins and overrides the .form-card class's
   own `margin: 0 auto`, so those cards actually sit flush against
   #content's left edge, not centered. Centering just this grid made it
   the only element on the page NOT flush-left, which is what pushed it
   visibly to the right of everything else. */
.biz-stat-grid {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 14px; margin: 0 0 24px; max-width: 620px;
}
@media (max-width: 640px) {
  .biz-stat-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Rendered as <button> so a stat tile can drill into the order/submission
   table below it -- reset the native button chrome back to plain-card
   styling, same visual as before this was clickable. */
.biz-stat {
  border: 1px solid var(--border); border-radius: 14px; padding: 16px 18px;
  background: var(--card); display: flex; flex-direction: column; gap: 4px;
  font-family: inherit; text-align: left; cursor: pointer; width: 100%;
}
.biz-stat:hover { border-color: var(--accent); }
.biz-stat-value { font-size: 22px; font-weight: 800; letter-spacing: -0.01em; }
.biz-stat-label { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
.biz-stat-warn .biz-stat-value { color: var(--merge); }

/* Campaign performance card (advertise-campaign-detail.html) -- same
   stat-tile typography as .biz-stat above, but plain <div>s (nothing to
   drill into here, unlike the admin business-detail stats) and only ever
   3 tiles (impressions/clicks/CTR), so no responsive column-count switch
   is needed the way .biz-stat-grid needs one. */
.perf-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 480px) {
  .perf-stat-grid { grid-template-columns: 1fr; }
}
.perf-stat {
  border: 1px solid var(--border); border-radius: 14px; padding: 16px 18px;
  background: var(--bg-alt); display: flex; flex-direction: column; gap: 4px;
}
.perf-stat-breakdown { font-size: 12px; color: var(--text-secondary); margin: 12px 0 0; }

.biz-table { display: flex; flex-direction: column; }
.biz-table-head, .biz-table-row {
  display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr; gap: 12px; align-items: center;
  padding: 10px 0; font-size: 13px;
}
/* Submission history has one fewer column (no Amount) than order history --
   same component, different column count. */
.biz-table-4col.biz-table-head, .biz-table-4col.biz-table-row { grid-template-columns: 2fr 1fr 1fr 1fr; }
.biz-table-head { color: var(--text-secondary); font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
.biz-table-row { border-bottom: 1px solid var(--border); }
.biz-table-row:last-child { border-bottom: none; }
.biz-table-row .status-pill { justify-self: start; }
@media (max-width: 640px) {
  .biz-table-head { display: none; }
  .biz-table-row, .biz-table-4col.biz-table-row { grid-template-columns: 1fr 1fr; row-gap: 4px; }
}

/* Toggleable rendered-ad preview per card, reusing .preview-frame/.pf-*
   /.pm-* from advertise-new-campaign.html's live preview verbatim -- see
   renderPreviewHTML() in admin-review.html. */
.review-preview {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
  gap: 20px; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);
}
/* Each column groups a heading with its own frame -- without this wrapper
   the heading and frame elements are flat grid siblings, so auto-fit
   places them in separate columns instead of stacking a heading above
   its matching frame. */
.review-preview-col { display: flex; flex-direction: column; min-width: 0; }

.form-card {
  max-width: 620px; margin: 0 auto; background: var(--card); border: 1px solid var(--border);
  border-radius: 24px; padding: 36px 32px; margin-bottom: 60px;
}
.form-card h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 6px; }
.form-card .sub { font-size: 14px; color: var(--text-secondary); margin: 0 0 26px; }
.back-link { display:inline-flex; align-items:center; gap:6px; font-size:14px; font-weight:600; color:var(--text-secondary); margin: 24px 0 0; }
.back-link:hover { color: var(--accent); }

.upload-row { display: flex; align-items: center; gap: 14px; }
.upload-preview { width: 64px; height: 64px; border-radius: 12px; object-fit: cover; background: var(--bg-alt); border: 1px solid var(--border); flex-shrink: 0; }

/* Campaign builder: two-column layout, form + live preview */
.campaign-layout { display: grid; grid-template-columns: minmax(0, 620px) 340px; gap: 32px; align-items: start; margin-bottom: 60px; }
@media (max-width: 980px) { .campaign-layout { grid-template-columns: 1fr; } }
.form-column .form-card { margin-bottom: 0; }
.preview-column { position: sticky; top: 84px; }
.preview-heading { font-size: 13px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 10px; }
.preview-heading:not(:first-child) { margin-top: 26px; }

.preview-frame { border: 1px solid var(--border); border-radius: 20px; overflow: hidden; background: var(--bg); box-shadow: var(--shadow); }

/* Feed ad preview -- mirrors Sponsoredpostcard.swift's header/media/content layout */
.pf-header { display: flex; align-items: flex-start; gap: 10px; padding: 12px 16px; }
.pf-logo { width: 44px; height: 44px; border-radius: 12px; background: var(--bg-alt); flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); overflow: hidden; }
.pf-logo img { width: 100%; height: 100%; object-fit: cover; }
.pf-names { flex: 1; min-width: 0; }
.pf-bizname { font-size: 15px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pf-sponsored { font-size: 12px; color: var(--text-secondary); }
.pf-dots { color: var(--text-secondary); font-weight: 700; padding-top: 2px; }
.pf-media { width: 100%; aspect-ratio: 4 / 5; background: var(--bg-alt); display: none; overflow: hidden; }
.pf-media.has-media { display: block; }
.pf-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Video is never cropped -- .pf-media-bg is a blurred, scaled-up copy
   filling the box behind .pf-media-fg, which stays object-fit: contain so
   the whole frame is always visible. Both are the same source file/blob
   URL and just autoplay/loop independently -- no JS sync needed, a few
   frames of drift is imperceptible on something already blurred this much.
   Mirrors the app's own letterbox-with-blurred-backdrop treatment
   (Sponsoredpostcard.swift/Feedpostcard.swift). */
.pf-media-bg {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: blur(20px) brightness(0.65); transform: scale(1.15); display: block;
}
.pf-media-fg {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block;
}
.pf-body { padding: 12px 16px 16px; border-top: 1px solid var(--border); }
.pf-headline { font-size: 14px; font-weight: 600; margin: 0 0 4px; overflow-wrap: anywhere; }
.pf-desc { font-size: 13px; color: var(--text-secondary); margin: 0; overflow-wrap: anywhere; }
.pf-cta { display: none; margin-top: 10px; text-align: center; font-size: 13px; font-weight: 700; color: #fff; background: var(--accent); border-radius: 10px; padding: 9px; }
.pf-cta.shown { display: block; }
.pf-not-included { padding: 32px 16px; text-align: center; color: var(--text-secondary); font-size: 13px; }

/* Map pin preview -- mirrors SponsoredMapPin + SponsoredPinDetailSheet */
.pm-pin-row { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 24px 16px; background: var(--bg-alt); }
.pm-pin-badge { position: relative; width: 40px; height: 40px; }
.pm-pin-circle { width: 40px; height: 40px; border-radius: 50%; background: var(--card); border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.25); display: flex; align-items: center; justify-content: center; color: var(--text-secondary); font-size: 16px; }
.pm-pin-ad-tag { position: absolute; top: -4px; right: -6px; font-size: 8px; font-weight: 800; color: #fff; background: var(--text-secondary); padding: 2px 4px; border-radius: 100px; }
.pm-pin-name { font-size: 11px; font-weight: 600; background: var(--card); padding: 2px 8px; border-radius: 100px; white-space: nowrap; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
.pm-detail { padding: 16px; border-top: 1px solid var(--border); }
.pm-detail-header { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
.pm-detail-logo { width: 44px; height: 44px; border-radius: 12px; background: var(--bg-alt); flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); }
.pm-detail-name { font-size: 15px; font-weight: 700; }
.pm-detail-sub { font-size: 12px; color: var(--text-secondary); }
.pm-detail-headline { font-size: 14px; font-weight: 600; margin: 0 0 4px; overflow-wrap: anywhere; }
.pm-detail-desc { font-size: 13px; color: var(--text-secondary); margin: 0; overflow-wrap: anywhere; }
.pm-detail-caption { font-size: 12px; color: var(--text-secondary); text-align: center; padding: 8px 16px 2px; }

/* Photo cropper (Move & Scale) -- mirrors the app's own Imagecropperview.swift
   convention: drag to reposition, a slider to zoom, within a 4:5 mask. */
.crop-modal {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 200;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.crop-modal-inner {
  background: var(--card); border-radius: 20px; padding: 24px; max-width: 340px; width: 100%;
  box-shadow: var(--shadow);
  /* Flex items default to min-width: auto, which lets a large-intrinsic-size
     child (a photo or video at its native resolution) force this wider than
     max-width regardless -- this is what let the video review modal blow
     out to fill the screen instead of staying inside its 340px card.
     min-width: 0 is the standard fix, overriding that default so max-width
     actually clamps it. */
  min-width: 0;
}
.crop-modal-title { font-size: 15px; font-weight: 700; margin: 0 0 16px; text-align: center; }
.crop-viewport {
  position: relative; width: 100%; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 12px;
  background: var(--bg-alt); touch-action: none; cursor: grab; margin-bottom: 16px;
}
.crop-viewport:active { cursor: grabbing; }
.crop-viewport img {
  position: absolute; top: 0; left: 0; transform-origin: 0 0;
  user-select: none; -webkit-user-drag: none; max-width: none;
}
.crop-modal input[type="range"] { width: 100%; margin: 0 0 20px; }
.crop-modal-actions { display: flex; gap: 10px; }
.crop-modal-actions .btn-primary, .crop-modal-actions .btn-secondary, .crop-modal-actions .btn-danger { flex: 1; padding: 11px; }

/* Reject-reason modal (admin-review.html, 2026-08-25) -- quick-fill chips
   above the reason textarea, reusing .crop-modal/.crop-modal-inner for the
   overlay + card chrome. Chip styling mirrors .review-filter-tab (same
   pill shape/weight) rather than introducing a third button variant. */
.reject-reason-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.reject-reason-chip {
  font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 100px;
  border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-secondary);
  cursor: pointer; font-family: inherit;
}
.reject-reason-chip:hover { border-color: var(--accent); color: var(--accent); }

/* Video review modal -- reuses .crop-modal/.crop-modal-inner for the
   overlay + card chrome, own rules just for the player + scrub controls.
   `contain` -- this is a watch-the-whole-clip step, not a crop preview;
   video is never cropped anywhere now (see .pf-media-bg/.pf-media-fg
   above), so there's nothing to preview here beyond the clip itself. */
.video-review-viewport {
  position: relative; width: 100%; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 12px;
  background: #000; margin-bottom: 12px;
}
.video-review-viewport video {
  width: 100%; height: 100%; object-fit: contain; background: #000;
  -webkit-user-drag: none; user-select: none;
}
.video-review-controls { display: flex; gap: 10px; margin-bottom: 16px; }
.video-review-controls button { flex: 1; padding: 9px; font-size: 13px; }

/* "Edit" button overlaid on an already-picked photo/video in the live
   preview, letting a business reopen the cropper/review modal instead of
   having to clear and re-pick the file from scratch. */
.pf-media { position: relative; }
.media-edit-btn {
  position: absolute; top: 10px; right: 10px; width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.55); border: none; border-radius: 50%; color: #fff; cursor: pointer;
}
.media-edit-btn:hover { background: rgba(0,0,0,0.75); }

/* Media type toggle + video upcharge note */
.media-type-toggle { display: flex; gap: 10px; margin-bottom: 12px; }
.media-type-toggle label { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; border: 1px solid var(--border); border-radius: 12px; padding: 10px; font-size: 14px; font-weight: 600; cursor: pointer; }
.media-type-toggle label:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
.media-type-toggle input { margin: 0; }
.upcharge-tag { font-size: 11px; font-weight: 700; color: var(--merge); }
.tier-disabled-note { font-size: 13px; color: var(--text-secondary); font-style: italic; padding: 4px 0 16px; }

/* Audience picker (advertise-new-campaign.html) -- same checkbox-as-card
   pattern as .tier-option/.media-type-toggle above, just a denser grid
   since there are 9 options instead of 2. */
.audience-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
.audience-option {
  display: flex; align-items: center; gap: 8px; border: 1px solid var(--border);
  border-radius: 10px; padding: 9px 12px; font-size: 13px; font-weight: 600; cursor: pointer;
}
.audience-option:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
.audience-option input { margin: 0; }
@media (max-width: 480px) { .audience-grid { grid-template-columns: 1fr; } }

/* Target-campus autocomplete (advertise-new-campaign.html) -- same shape
   as the app's own sign-up campus picker. */
.campus-suggestions {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--card); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow);
  z-index: 20; max-height: 220px; overflow-y: auto; display: none;
}
.campus-suggestions.open { display: block; }
.campus-suggestion { padding: 10px 16px; font-size: 14px; cursor: pointer; }
.campus-suggestion:hover { background: var(--bg-alt); }
