/* Client portal — tokenized link, no login. Read-mostly view of a booking:
   payments, timeline, document vault, and a two-way notes channel.

   Restyled to match the dashboard concept's design system (ui_concept.css):
   Fraunces display type, DM Sans body, IBM Plex Mono for numbers/timestamps,
   flat hairline-bordered panels (no drop shadows), pill status badges, and
   the same accent/success/danger palette — so the portal reads as a
   continuation of the product UI, not a separate document style. */

:root{
  --doc-ink:#0F172A;                    /* ui_concept --ink */
  --doc-soft:#475569;                   /* ui_concept --ink-soft */
  --doc-paper:#F5F6F8;                  /* ui_concept --page (outer background) */
  --doc-card:#FFFFFF;                   /* ui_concept --paper (panel background) */
  --doc-well:#EDEFF2;                   /* ui_concept --well (hover / field fill) */
  --doc-line:rgba(15,23,42,0.10);       /* ui_concept --hairline */
  --doc-brass:#2D64BC;                  /* ui_concept --accent */
  --doc-brass-light:#5285D6;            /* ui_concept --accent-light */
  --doc-accent-wash:#EAF0FA;            /* ui_concept --accent-wash */
  --doc-green:#10B981;                  /* ui_concept --success */
  --doc-red:#B4304A;                    /* ui_concept --danger */
  --doc-amber:#B87A1E;                  /* ui_concept --warning */
  --doc-amber-wash:#FBF3E4;             /* ui_concept --warning-wash */
  /* Text-on-wash variants. --doc-green/--doc-red are the fill colours; these
     are the inks that stay legible set ON their own wash, mirroring
     ui_concept's semantic ink pairs. `color:` takes an -ink, never a fill. */
  --doc-green-ink:#0B6E4F;              /* ui_concept --success-ink */
  --doc-green-line:#B8DCCB;             /* ui_concept --success-line */
  --doc-green-wash:#E7F5EF;             /* ui_concept --success-wash */
  --doc-red-ink:#A32E46;                /* ui_concept --danger-ink */
  --doc-red-line:#E9C7BC;               /* ui_concept --danger-line */
  --doc-red-wash:#FAEEEA;               /* ui_concept --danger-wash */
  --doc-radius:3px;
  --doc-radius-ctl:8px;
  --doc-radius-lg:12px;
  --doc-btn-radius:8px;
  --doc-shadow-lg:0 16px 40px rgba(15,23,42,.09);
  --font-display:'Fraunces',Georgia,'Iowan Old Style',ui-serif,serif;
  --font-body:'DM Sans','Segoe UI',ui-sans-serif,system-ui,sans-serif;
  --font-mono:'IBM Plex Mono','SFMono-Regular',Consolas,ui-monospace,monospace;
}
*{ box-sizing:border-box; }
button, input, textarea, select, optgroup, option, kbd{ font-family:inherit; }

/* Custom scrollbar — thin, subtle gray, no arrows. Same treatment as the
   rest of the site (see base.css). WebKit/Blink only: the standard
   scrollbar-width/scrollbar-color props are intentionally omitted — setting
   them makes Chromium ignore these ::-webkit-scrollbar rules (and re-adds
   the native arrow buttons). Firefox falls back to its own scrollbars,
   which have no arrows anyway. */
@supports selector(::-webkit-scrollbar){
  *{ scrollbar-width:auto; }
}
*::-webkit-scrollbar{ width:8px; height:8px; }
*::-webkit-scrollbar-track{ background:transparent; }
*::-webkit-scrollbar-thumb{
  background:rgba(128,128,140,.4);
  border-radius:20px;
  border:2px solid transparent;
  background-clip:padding-box;
}
*::-webkit-scrollbar-thumb:hover{
  background:rgba(128,128,140,.65);
  background-clip:padding-box;
}
*::-webkit-scrollbar-button,
*::-webkit-scrollbar-button:single-button,
*::-webkit-scrollbar-button:start,
*::-webkit-scrollbar-button:end,
*::-webkit-scrollbar-button:horizontal,
*::-webkit-scrollbar-button:vertical{
  display:none; width:0; height:0;
}
*::-webkit-scrollbar-corner{ background:transparent; }

body{
  margin:0;
  background:var(--doc-paper);
  color:var(--doc-ink);
  font-family:var(--font-body);
  font-size:14px;
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
}
::selection{ background:color-mix(in srgb, var(--doc-brass) 35%, transparent); color:var(--doc-ink); }

.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;
}

/* Off-screen until it receives keyboard focus. */
.skip-link{
  position:absolute; left:8px; top:-48px; z-index:1000;
  background:var(--doc-card); color:var(--doc-ink);
  padding:10px 16px; border-radius:var(--doc-radius-ctl);
  box-shadow:var(--doc-shadow-lg);
  font-size:14px; font-weight:600; text-decoration:none;
  transition:top .15s ease;
}
.skip-link:focus{ top:8px; }
.doc-wrap{ max-width:680px; margin:0 auto; padding:36px 20px 80px; }

.doc-vendor{ display:flex; align-items:center; gap:12px; margin-bottom:14px; }
.doc-vendor .avatar{
  width:44px; height:44px; border-radius:50%;
  object-fit:cover; background:linear-gradient(135deg, var(--doc-brass), var(--doc-brass-light));
  color:#fff; display:flex; align-items:center; justify-content:center;
  font-family:var(--font-display); font-weight:600; font-size:17px; flex-shrink:0;
}
.doc-vendor .who{ flex:1; min-width:0; }
.doc-vendor .who .nm{ font-family:var(--font-display); font-weight:600; font-size:16.5px; }
.doc-vendor .who .sub{ font-size:12.5px; color:var(--doc-soft); }
/* Matches the dashboard's .iq-btn (booking-page header actions) — same
   padding, radius, and weight — instead of a bespoke pill shape. */
.doc-vendor-contact{ display:flex; gap:8px; flex-shrink:0; }
.doc-vendor-contact a{
  display:inline-flex; align-items:center;
  font-size:13px; font-weight:600; color:var(--doc-ink);
  padding:9px 15px; border:1px solid var(--doc-line); border-radius:var(--doc-radius-ctl);
  text-decoration:none;
}
.doc-vendor-contact a:hover{ background:var(--doc-well); }

/* Notifications bell + unread counter, top-right of the portal header. */
.portal-notify{ position:relative; flex-shrink:0; }
.portal-notify-btn{
  position:relative; display:flex; align-items:center; justify-content:center;
  width:38px; height:38px; padding:0; cursor:pointer;
  color:var(--doc-soft); background:none;
  border:none; border-radius:var(--doc-radius-ctl);
}
.portal-notify-btn:hover{ color:var(--doc-ink); background:var(--doc-well); }
.portal-notify-btn svg{ width:18px; height:18px; }
.portal-notify-count{
  position:absolute; top:6px; right:6px; min-width:16px; height:16px;
  padding:0 4px; display:flex; align-items:center; justify-content:center;
  font-size:10px; font-weight:700; line-height:1; color:#fff;
  background:var(--doc-red); border-radius:999px; border:1.5px solid var(--doc-paper);
}
.portal-notify-count[hidden]{ display:none; }
.portal-notify-panel{
  position:absolute; top:calc(100% + 10px); right:0; z-index:20;
  width:288px; max-width:78vw; padding:0;
  background:var(--doc-card); border:1px solid var(--doc-line);
  border-radius:var(--doc-radius-lg); box-shadow:var(--doc-shadow-lg);
}
.portal-notify-panel[hidden]{ display:none; }
.portal-notify-head{
  font-family:var(--font-body); font-size:10.5px; font-weight:600; letter-spacing:.06em; text-transform:uppercase;
  color:var(--doc-soft); padding:12px 14px; border-bottom:1px solid var(--doc-line);
}
.portal-notify-list{ list-style:none; margin:0; padding:4px; max-height:340px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; }
.portal-notify-item{
  display:flex; gap:11px; align-items:flex-start;
  padding:11px 10px; border-radius:var(--doc-radius-ctl); cursor:pointer;
}
.portal-notify-item:hover{ background:var(--doc-well); }
.portal-notify-item.unread{ background:rgba(45,100,188,0.06); }
.portal-notify-dot{
  width:7px; height:7px; border-radius:50%; margin-top:6px; flex-shrink:0;
  background:var(--doc-soft);
}
.portal-notify-item.unread .portal-notify-dot{ box-shadow:0 0 0 3px rgba(45,100,188,0.14); }
.portal-notify-body{ flex:1; min-width:0; }
.portal-notify-text{ font-size:13px; color:var(--doc-ink); line-height:1.4; min-width:0; overflow-wrap:anywhere; }
.portal-notify-time{ font-family:var(--font-mono); font-size:10.5px; color:var(--doc-soft); margin-top:3px; }
.portal-notify-empty{ font-size:13px; color:var(--doc-soft); padding:20px 10px; text-align:center; }

.portal-lock-row{ margin-bottom:14px; text-align:right; }
.portal-lock-btn{
  background:none; border:none; padding:0;
  font-family:var(--font-body); font-size:12px; color:var(--doc-soft); text-decoration:underline; cursor:pointer;
}
.portal-lock-btn:hover{ color:var(--doc-ink); }

.portal-vendor-note{
  padding:12px 14px; margin-bottom:18px; border-radius:var(--doc-radius-ctl);
  background:var(--doc-accent-wash); color:var(--doc-brass);
  font-size:13px; font-weight:600;
}

/* Vendor-preview topbar — same fixed, blurred-backdrop chrome as the
   proposal/invoice editor toolbars, so previewing a client-facing page reads
   as "editor chrome", not part of what the client actually sees. */
body.has-vendor-bar{ padding-top:52px; }
body.has-vendor-bar .doc-wrap{ padding-top:20px; }
.portal-vendor-bar{
  position:fixed; top:0; left:0; right:0; z-index:40;
  background:rgba(245,246,248,0.86);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  border-bottom:1px solid var(--doc-line);
}
.portal-vendor-bar-inner{
  max-width:680px; margin:0 auto;
  display:flex; align-items:center; gap:14px;
  padding:14px 20px;
}
.portal-vendor-back{
  display:inline-flex; align-items:center; gap:5px; flex-shrink:0;
  font-family:var(--font-body); font-size:13px; font-weight:600; color:var(--doc-soft);
  text-decoration:none;
}
.portal-vendor-back svg{ width:15px; height:15px; flex-shrink:0; }
.portal-vendor-back:hover{ color:var(--doc-ink); }
.portal-vendor-bar-text{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-size:12.5px; color:var(--doc-brass);
}
.portal-vendor-bar-text b{ color:var(--doc-ink); font-weight:600; }
@media (max-width:560px){
  .portal-vendor-bar-inner{ padding:12px 16px; gap:10px; }
  .portal-vendor-bar-text{ font-size:11.5px; }
}

/* Enclosed panel head, matching the dashboard's .panel/.panel-head: the card
   itself carries no padding, so the head's own padding + border-bottom span
   its full width edge-to-edge instead of sitting inset from the card sides. */
.doc-card{
  background:none;
  border:1px solid var(--doc-line);
  border-radius:var(--doc-radius-lg);
  margin-bottom:18px;
  overflow:hidden;
}
.doc-card-head{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:16px 24px;
  border-bottom:1px solid var(--doc-line);
}
.doc-card-head h2{ margin:0; padding:0; border-bottom:none; }
.doc-card-body{ padding:16px 24px; }
/* Foot strip below the body — its own box (not a border tacked onto the last
   body element) so its divider spans edge-to-edge without negative margins. */
.doc-card-foot{ padding:14px 24px; border-top:1px solid var(--doc-line); }
@media (max-width:560px){
  .doc-card-head{ padding:14px 18px; }
  .doc-card-body{ padding:14px 18px; }
  .doc-card-foot{ padding:12px 18px; }
}

/* Panel-head action link — matches the dashboard's .link.outline-btn. */
.link{
  font-size:12.5px; color:var(--doc-brass); font-weight:600; text-decoration:none; cursor:pointer; background:none; border:none;
}
.link.outline-btn{
  border:1px solid var(--doc-line); border-radius:var(--doc-radius-ctl);
  padding:5px 12px;
}
.link.outline-btn:hover{ background:var(--doc-well); border-color:var(--doc-brass); }

h1{ font-family:var(--font-display); font-weight:600; font-size:26px; letter-spacing:-0.01em; margin:0; }
h2{ font-family:var(--font-display); font-weight:500; font-size:17px; margin:0; }
/* Plain page title above the panels — matches the dashboard's .iq-head:
   no card border/background, just the title and its status pill. */
.doc-head{ margin:0 0 18px; }
.doc-head .doc-status{ margin-top:10px; }
.doc-meta{ font-size:13px; color:var(--doc-soft); margin:0 0 4px; }
.tl-foot-hint{ margin:0; }

/* Section empty states (Documents / Timeline / Notes) — an icon mark, a
   title, and a short line of context, matching the dashboard's .bk-empty
   shape so the portal reads as a continuation of that pattern rather than a
   bare "nothing here" caption. */
.portal-empty{
  display:flex; flex-direction:column; align-items:center; text-align:center;
  padding:24px 16px;
}
.portal-empty[hidden]{ display:none; }
.portal-empty-icon{
  width:40px; height:40px; flex-shrink:0;
  display:flex; align-items:center; justify-content:center;
  border-radius:50%; margin-bottom:12px;
  background:var(--doc-amber-wash); color:var(--doc-amber);
}
.portal-empty-title{ font-size:13.5px; font-weight:600; color:var(--doc-ink); margin:0 0 4px; }
.portal-empty-text{
  font-size:12px; color:var(--doc-soft); margin:0;
  max-width:30ch; line-height:1.5;
}
.tl-foot-hint a{ color:var(--doc-brass); font-weight:600; text-decoration:none; }
.tl-foot-hint a:hover{ text-decoration:underline; }

.doc-status{
  display:inline-flex; align-items:center; gap:6px;
  font-family:var(--font-body);
  padding:3px 10px 3px 8px; border-radius:20px;
  font-size:10.5px; font-weight:600; letter-spacing:.05em; text-transform:uppercase;
}
.doc-status::before{ content:""; width:6px; height:6px; border-radius:50%; background:currentColor; flex-shrink:0; }
.doc-status.accepted,.doc-status.paid,.doc-status.completed{ color:var(--doc-green); background:rgba(16,185,129,0.14); }
.doc-status.declined,.doc-status.void,.doc-status.cancelled{ color:var(--doc-red); background:rgba(180,48,74,0.14); }
.doc-status.sent,.doc-status.unpaid,.doc-status.pending,.doc-status.upcoming{ color:var(--doc-brass); background:rgba(45,100,188,0.14); }

/* Payment progress — balance hero, segmented track, next-payment callout,
   and the schedule list below it. */
.pay-balance{ display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; }
.pay-balance-figure{ font-family:var(--font-display); font-size:25px; font-weight:500; line-height:1; letter-spacing:-.01em; margin:2px 0 0; transform:translateY(-2.7px); }
.pay-stats{ display:flex; gap:22px; }
.pay-stat{ text-align:right; }
.pay-stat span{ display:block; font-size:11.5px; color:var(--doc-soft); margin-bottom:1px; }
.pay-stat b{ font-size:14px; font-weight:500; letter-spacing:-.01em; }

.pay-track{ display:flex; gap:2px; height:6px; margin:14px 0 8px; background:var(--doc-well); border-radius:999px; overflow:hidden; }
.pay-seg{ height:100%; }
.pay-seg.paid{ background:var(--doc-brass); }
.pay-seg.due{ background:var(--doc-brass); opacity:.32; }
.pay-legend{ display:flex; flex-wrap:wrap; gap:14px; font-size:11.5px; color:var(--doc-soft); }
.pay-legend i{ display:inline-block; width:7px; height:7px; border-radius:50%; margin-right:5px; vertical-align:1px; background:var(--doc-well); }
.pay-legend .k-paid i{ background:var(--doc-brass); }
.pay-legend .k-due i{ background:var(--doc-brass); opacity:.32; }

.pay-next{ display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-top:18px; padding:14px 16px; background:var(--doc-accent-wash); border-radius:var(--doc-radius-ctl); }
.pay-next-copy{ min-width:0; flex:1; }
.pay-next-kicker{ font-size:10.5px; font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--doc-brass); margin:0 0 3px; }
.pay-next-title{ font-size:13px; font-weight:600; margin:0 0 1px; letter-spacing:-.005em; overflow-wrap:anywhere; }
.pay-next-sub{ font-size:12.5px; color:var(--doc-soft); margin:0; }
.pay-overdue{ color:var(--doc-red); font-weight:600; }
.pay-next-cta{ display:flex; align-items:center; gap:12px; flex-shrink:0; }
.pay-next-amount{ font-family:var(--font-display); font-size:20px; font-weight:500; letter-spacing:-.01em; }

.sched{ list-style:none; margin:6px 0 0; padding:0; }
.sched-head{ font-size:10.5px; font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--doc-soft); margin:22px 0 4px; }
.row{ display:flex; align-items:center; gap:12px; padding:9px 0; border-bottom:1px solid var(--doc-line); }
.row:last-child{ border-bottom:0; padding-bottom:0; }
.row-date{ width:52px; flex:none; font-size:12px; font-weight:500; color:var(--doc-soft); }
.row-name{ flex:1; min-width:0; font-size:13px; font-weight:500; letter-spacing:-.005em; overflow-wrap:anywhere; }
.row-name small{ display:block; font-weight:400; font-size:11.5px; color:var(--doc-soft); }
.row-amt{ font-size:13px; font-weight:500; letter-spacing:-.01em; white-space:nowrap; }
.row.settled .row-name, .row.settled .row-amt{ color:var(--doc-soft); font-weight:400; }

.pill{
  font-size:10px; font-weight:600; letter-spacing:.07em; text-transform:uppercase;
  border-radius:999px; padding:3px 8px; white-space:nowrap;
  color:var(--doc-soft); background:var(--doc-well);
}
.pill.now{ color:var(--doc-brass); background:var(--doc-accent-wash); }
.pill.late{ color:var(--doc-red); background:rgba(180,48,74,0.12); }

/* Itemized payment ledger behind the schedule above — the individual
   payments (a milestone can only show one aggregate state, so a client who
   paid in two pieces still needs somewhere to see both). Collapsed by
   default via native <details>, no JS required. */
.pay-log{ margin-top:20px; padding-top:14px; border-top:1px solid var(--doc-line); }
.pay-log > summary{ display:flex; align-items:center; gap:8px; cursor:pointer; font-size:12.5px; color:var(--doc-soft); list-style:none; }
.pay-log > summary::-webkit-details-marker{ display:none; }
.pay-log > summary::marker{ content:""; }
.pay-log-caret{ display:flex; color:var(--doc-soft); transition:transform .18s ease; }
.pay-log[open] .pay-log-caret{ transform:rotate(90deg); }
.pay-log-head{ font-weight:600; color:var(--doc-ink); }
.pay-log-sum{ margin-left:auto; font-size:12px; }
.pay-log-rows{ margin-top:4px; }
.pay-log-row{ display:flex; align-items:center; gap:12px; padding:9px 0; border-bottom:1px solid var(--doc-line); }
.pay-log-row:last-child{ border-bottom:0; padding-bottom:0; }
.pay-log-info{ flex:1; min-width:0; }
.pay-log-ttl{ font-size:13px; font-weight:500; letter-spacing:-.005em; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pay-log-meta{ font-size:11.5px; color:var(--doc-soft); margin-top:1px; }
.pay-log-amount{ flex:none; }
@media (max-width:479px){ .pay-log-sum{ display:none; } }

@media (max-width:479px){
  .pay-next{ flex-direction:column; align-items:stretch; }
  .pay-next-cta{ justify-content:space-between; }
  .pill{ display:none; }
}

/* Below this width the date column crowds the milestone name/amount, so
   the date moves onto its own line above the name instead of beside it. */
@media (max-width:359px){
  .row{ flex-wrap:wrap; row-gap:3px; }
  .row-date{
    width:auto; flex:0 0 100%;
    font-size:10.5px; font-weight:600; letter-spacing:.04em; text-transform:uppercase;
  }
}

.portal-row{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:13px 0; border-bottom:1px solid var(--doc-line);
}
.portal-row:last-child{ border-bottom:none; padding-bottom:0; }
.portal-row .rlbl{ font-weight:600; font-size:14px; }
.portal-row .rsub{ font-family:var(--font-mono); font-size:11.5px; color:var(--doc-soft); margin-top:3px; overflow-wrap:anywhere; }
.portal-pay-form{ margin:0; flex-shrink:0; }

/* Event details — read-only key/value rows (event date, time, venue, guests,
   plus whatever the client submitted on the vendor's lead form). */
.portal-detail-list{
  display:flex; flex-direction:column;
  /* Measured by the stacking query below, so a narrow card stacks even when the
     viewport is wide (print sidebar, embedded portal). */
  container:portal-details / inline-size;
}
.portal-detail{
  display:flex; justify-content:space-between; gap:18px;
  padding:11px 0; border-bottom:1px solid var(--doc-line);
  font-size:13.5px; line-height:1.5;
}
.portal-detail:first-child{ padding-top:0; }
.portal-detail:last-child{ border-bottom:none; padding-bottom:0; }
.portal-detail .k{ color:var(--doc-soft); font-size:12.5px; flex-shrink:0; }
.portal-detail .v{ color:var(--doc-ink); font-weight:500; text-align:right; overflow-wrap:anywhere; }
/* Narrow cards: a long question label and its answer can't share a row without
   one of them being crushed, so the answer drops under the label, left-aligned. */
@container portal-details (max-width: 420px){
  .portal-detail{ flex-direction:column; align-items:flex-start; gap:3px; }
  .portal-detail .k{ flex-shrink:1; min-width:0; overflow-wrap:anywhere; }
  .portal-detail .v{ text-align:left; }
  .portal-swatches{ justify-content:flex-start; }
}
/* Free-text rows (e.g. the vendor's notes) stack the value under the label and
   read left-aligned rather than squeezing into a right-hand column. */
.portal-detail-block{ flex-direction:column; gap:5px; }
.portal-detail-block .v{ text-align:left; line-height:1.6; font-weight:400; }
.portal-stars{ color:#C17D12; letter-spacing:1px; }
/* Color-swatch values (vendor-added "Color swatches" detail). */
.portal-swatches{ display:inline-flex; gap:6px; flex-wrap:wrap; justify-content:flex-end; }
.portal-swatch{
  width:18px; height:18px; border-radius:50%;
  box-shadow:inset 0 0 0 1px rgba(15,23,42,0.15);
}

.doc-btn{
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  padding:9px 16px;
  font-family:var(--font-body); font-size:13px; font-weight:600;
  border-radius:var(--doc-btn-radius); border:1px solid var(--doc-line); cursor:pointer; text-decoration:none;
  background:none; color:var(--doc-ink);
  transition:background .12s ease, border-color .12s ease;
}
.doc-btn.pay{ color:#fff; background:var(--doc-brass); border-color:var(--doc-brass); }
.doc-btn.pay:hover{ background:var(--doc-brass-light); border-color:var(--doc-brass-light); }
.doc-btn.ghost{ color:var(--doc-soft); background:none; border-color:var(--doc-line); }
.doc-btn.ghost:hover{ color:var(--doc-ink); background:var(--doc-well); }
.doc-btn.small{ padding:6px 12px; font-size:12.5px; position:relative; }
/* ~31px tall as drawn; invisible overlay lifts the hit box to ~37px. */
.doc-btn.small::after{ content:""; position:absolute; inset:-3px 0; }
.doc-btn:disabled{ opacity:0.5; cursor:default; }
/* display:inline-flex above would otherwise beat the UA's [hidden]{display:none} */
.doc-btn[hidden]{ display:none !important; }

/* AJAX button feedback — spinner while in flight, then a green checkmark
   that holds for a beat before easing back. Same pattern/class names as the
   dashboard's shared .js-save-btn (ui_concept.css + save_button.js), ported
   to this page's own --doc-* tokens since the portal doesn't load
   ui_concept.css. Content cross-fades on each swap so the sequence reads as
   one continuous motion instead of a flicker. */
.js-save-btn{ transition:background-color .35s ease, border-color .35s ease, color .35s ease; }
.js-save-btn.is-saved,
.js-save-btn.is-saved:hover{ background:var(--doc-green); border-color:var(--doc-green); color:#fff; }
.js-save-btn .btn-label{ display:inline-flex; align-items:center; opacity:1; transition:opacity .18s ease; }
.js-save-btn .btn-label.fade{ opacity:0; }
.js-save-btn .btn-spinner{
  width:13px; height:13px; border:2px solid currentColor; border-top-color:transparent;
  border-radius:50%; display:inline-block; margin-right:6px; vertical-align:-2px;
  animation:portalBtnSpin .6s linear infinite;
}
@keyframes portalBtnSpin{ to{ transform:rotate(360deg); } }
.js-save-btn .btn-check{
  margin-right:6px; vertical-align:-2px;
  stroke-dasharray:16; stroke-dashoffset:16;
  animation:portalCheckDraw .3s ease .1s forwards;
}
@keyframes portalCheckDraw{ to{ stroke-dashoffset:0; } }
@media (prefers-reduced-motion: reduce){
  .js-save-btn, .js-save-btn .btn-label{ transition:none; }
  .js-save-btn .btn-spinner{ animation-duration:1s; }
  .js-save-btn .btn-check{ animation:none; stroke-dashoffset:0; }
}

/* Newly-added note/document row — fades and slides in with a brief accent
   highlight instead of just popping into the list, so adding one reads as
   one continuous motion alongside the button's own spinner->check feedback.
   Same shape as the dashboard's .bk-vault-row-enter (ui_concept.css). */
.portal-row-enter{ animation:portalRowIn .4s ease; }
@keyframes portalRowIn{
  from{ opacity:0; transform:translateY(-6px); background:var(--doc-accent-wash); }
  to{ opacity:1; transform:translateY(0); }
}
@media (prefers-reduced-motion: reduce){
  .portal-row-enter{ animation:none; }
}

/* Event timeline — ported straight from the vendor's booking pages
   (.tl2-* in ui_concept.css), read-only for the client: no edit, delete,
   add, or reminder controls. "Mark as seen" sits inline with the title,
   in place of the vendor's bell/kebab actions. */
.tl2-progress-row{
  display:flex; align-items:center; flex-wrap:wrap; row-gap:6px;
  padding:12px 24px; border-bottom:1px solid var(--doc-line);
}
@media (max-width:560px){ .tl2-progress-row{ padding:10px 18px; } }
.tl2-progress{ display:flex; align-items:center; gap:10px; font-size:12px; color:var(--doc-soft); }
.tl2-progress b{ color:var(--doc-ink); font-weight:600; }
.tl2-bar{ width:72px; height:3px; background:var(--doc-line); border-radius:999px; overflow:hidden; flex-shrink:0; }
.tl2-bar span{ display:block; height:100%; background:var(--doc-brass); border-radius:999px; }

.tl2-list{
  --tl2-rail-x:5px; --tl2-dot:10px; --tl2-row-h:18px;
  --tl2-dot-top:calc((var(--tl2-row-h) - var(--tl2-dot)) / 2); --tl2-step:22px;
  list-style:none; margin:0; padding:0; position:relative;
}
.tl2-list[hidden]{ display:none; }
.tl2-item{ position:relative; padding:0 0 var(--tl2-step) calc(var(--tl2-rail-x) + 26px); }
.tl2-item:last-child{ padding-bottom:8px; }
.tl2-item::before{
  content:""; position:absolute; left:var(--tl2-rail-x); top:var(--tl2-dot-top);
  width:var(--tl2-dot); height:var(--tl2-dot); border-radius:50%;
  background:var(--doc-card); border:1.5px solid var(--doc-line); box-shadow:inset 0 0 0 1px var(--doc-line);
  transform:translateX(-50%); z-index:1; transition:background .3s ease, border-color .3s ease;
}
.tl2-item::after{
  content:""; position:absolute; left:var(--tl2-rail-x);
  top:calc(var(--tl2-dot-top) + var(--tl2-dot)); bottom:calc(var(--tl2-dot-top) * -1);
  width:2px; background:var(--doc-line); transform:translateX(-50%);
}
.tl2-item:last-child::after{ display:none; }
.tl2-item.done::before{ background:var(--doc-brass); border-color:var(--doc-brass); box-shadow:none; }
.tl2-item.done::after{ background:var(--doc-brass); opacity:.35; }
.tl2-item.now::before{ background:var(--doc-brass); border-color:var(--doc-brass); box-shadow:inset 0 0 0 2.5px var(--doc-card); }
.tl2-item.overdue::before{ border-color:var(--doc-red); }
.tl2-item.tbd::before{ border-style:dashed; }

.tl2-date{ font-size:12px; font-weight:600; line-height:var(--tl2-row-h); color:var(--doc-soft); margin-bottom:2px; }
.tl2-item.done .tl2-date, .tl2-item.now .tl2-date{ color:var(--doc-brass); }
.tl2-item.overdue .tl2-date{ color:var(--doc-red); }
/* Shown next to the date only when the item actually has a time set. Inline
   here; the ≥620px rule below switches it back to stacked under the date's
   fixed-width column, which doesn't have the room. */
.tl2-time{ display:inline; font-size:10px; font-weight:500; color:var(--doc-soft); opacity:.8; }
.tl2-time::before{ content:"·"; margin:0 4px; opacity:.6; }
.tl2-time[hidden]{ display:none; }
.tl2-item.done .tl2-time, .tl2-item.now .tl2-time{ color:var(--doc-brass); }
.tl2-item.overdue .tl2-time{ color:var(--doc-red); }

.tl2-title{
  font-size:13px; font-weight:600; line-height:var(--tl2-row-h); letter-spacing:-.005em;
  margin:0 0 2px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; color:var(--doc-ink);
}
.tl2-item.done .tl2-title{ color:var(--doc-soft); font-weight:500; }
.tl2-desc{ margin:0; color:var(--doc-soft); font-size:12.5px; line-height:1.45; }

.tl2-tag{
  font-size:10px; font-weight:600; letter-spacing:.08em; text-transform:uppercase;
  color:var(--doc-brass); background:var(--doc-accent-wash);
  border-radius:999px; padding:2px 8px;
}

@media (min-width:620px){
  .tl2-list{ --tl2-rail-x:58px; }
  .tl2-item{ padding-left:calc(var(--tl2-rail-x) + 22px); }
  .tl2-date{ position:absolute; left:0; top:0; width:50px; text-align:left; margin:0; }
  .tl2-time{ display:block; }
  .tl2-time::before{ content:none; margin:0; }
  /* Placeholder rows use the template's relative wording ("14 days before
     event") instead of a short real date — too long for the fixed 50px
     date gutter, so fall back to the stacked (mobile) layout for these
     rather than letting it wrap and overlap the row below. */
  .tl2-item.tbd{ --tl2-rail-x:5px; }
  .tl2-item.tbd .tl2-date{ position:static; width:auto; margin:0 0 2px; }
}

/* Vault */
.portal-vault-row{ display:flex; align-items:center; gap:12px; padding:12px 0; border-bottom:1px solid var(--doc-line); }
.portal-vault-row:last-child{ border-bottom:none; padding-bottom:0; }
.portal-vault-row:first-child{ padding-top:0; }
.portal-vault-row .ico{
  width:30px; height:30px; flex-shrink:0; border-radius:var(--doc-radius-ctl);
  background:var(--doc-well); color:var(--doc-brass);
  display:flex; align-items:center; justify-content:center;
}
.portal-vault-row .ico svg{ width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.portal-vault-row .info{ flex:1; min-width:0; }
.portal-vault-row .nm{ font-size:13.5px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.portal-vault-row .sub{ font-family:var(--font-mono); font-size:11px; color:var(--doc-soft); margin-top:2px; }

/* Document center rows reuse the vault row, plus a status pill and a
   View / PDF pair. The name is allowed to wrap here (a document reads as
   "Invoice 0007: Final balance", not a filename), and the actions drop to
   their own line on narrow screens rather than squeezing the title to
   nothing. */
.portal-doc-row{ flex-wrap:wrap; row-gap:8px; }
.portal-doc-row .nm{ white-space:normal; overflow:visible; overflow-wrap:anywhere; }
.portal-doc-row .doc-status{ flex-shrink:0; }
.portal-doc-acts{ display:flex; gap:8px; flex-shrink:0; }
@media (max-width:479px){
  .portal-doc-row .info{ flex-basis:calc(100% - 42px); }
  .portal-doc-acts{ margin-left:auto; }
}

/* Notes — each row leads with an avatar so who wrote it reads at a glance
   without having to track which labeled section you're scrolled into.
   Vendor notes additionally get a tinted card treatment (their own
   background + border) so they read as a message *from* someone, distinct
   from the flat list rows of the client's own notes. */
.portal-note{
  display:flex; align-items:flex-start; gap:10px;
  padding:12px 0; border-bottom:1px solid var(--doc-line);
}
.portal-note:last-child{ border-bottom:none; padding-bottom:0; }
.portal-note-body{ flex:1; min-width:0; }
.portal-note .txt{ font-size:13.5px; line-height:1.55; white-space:pre-line; overflow-wrap:anywhere; }
.portal-note .meta{ font-size:11.5px; color:var(--doc-soft); margin-bottom:5px; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.portal-note-who{ font-size:12.5px; font-weight:600; color:var(--doc-ink); }
.portal-note-time{ font-family:var(--font-mono); font-size:10.5px; color:var(--doc-soft); }
.portal-note-avatar{
  flex-shrink:0; width:26px; height:26px; border-radius:50%; margin-top:1px;
  display:flex; align-items:center; justify-content:center; overflow:hidden;
  font-family:var(--font-body); font-size:11px; font-weight:700; text-transform:uppercase;
  background:var(--doc-well); color:var(--doc-soft);
}
img.portal-note-avatar{ object-fit:cover; }
.portal-note-avatar.you{ background:var(--doc-well); color:var(--doc-soft); border:1px solid var(--doc-line); }
.portal-note.vendor{
  background:var(--doc-accent-wash); border:1px solid rgba(45,100,188,0.16);
  border-radius:var(--doc-radius-ctl); padding:12px; margin-bottom:8px;
}
.portal-note.vendor:last-child{ margin-bottom:0; }
.portal-note.vendor .portal-note-avatar{ background:var(--doc-brass); color:#fff; }
.portal-note.vendor .portal-note-who{ color:var(--doc-brass); }
.portal-note-remove{
  background:none; border:none; color:var(--doc-soft); cursor:pointer; flex-shrink:0;
  padding:4px; border-radius:6px; display:flex;
}
.portal-note-remove:hover{ background:rgba(180,48,74,0.1); color:var(--doc-red); }
.portal-note-remove:disabled{ opacity:0.5; cursor:default; }
/* The portal's small icon buttons all sat at 21–26px — under the ≥36px tap
   floor the rest of the app holds. The hit box grows on an invisible overlay
   (these are drawn boxes whose visuals must not inflate); focus rings were
   absent from this file entirely, so they arrive here too. */
.portal-note-remove, .portal-upload-item-remove, .doc-modal-close{ position:relative; }
.portal-note-remove::after, .portal-upload-item-remove::after,
.doc-modal-close::after{ content:""; position:absolute; inset:-6px; }
/* .portal-info can't take the overlay — both its pseudos ARE the tooltip —
   so it grows the padding+negative-margin way instead: 24px → 36px box,
   pulled back so the row's rhythm doesn't move. */
.portal-info{ width:36px; height:36px; margin:-6px; }
.portal-note-remove:focus-visible, .portal-upload-item-remove:focus-visible,
.doc-modal-close:focus-visible, .portal-info:focus-visible,
.doc-btn:focus-visible, .portal-form button:focus-visible{
  outline:2px solid var(--doc-brass); outline-offset:2px;
}

.portal-notes-load-more{ width:100%; margin-bottom:14px; }
.portal-notes-load-more:last-of-type{ margin-bottom:0; }
.portal-notes-load-more:disabled{ opacity:0.6; cursor:default; }

.portal-notes-section-lbl{
  font-family:var(--font-body); font-size:10.5px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase;
  color:var(--doc-soft); margin:18px 0 6px;
}
.portal-notes-section-lbl:first-of-type{ margin-top:0; }

.note-badge{
  display:inline-flex; align-items:center; gap:4px;
  padding:2px 8px; border-radius:20px;
  font-family:var(--font-body); font-size:9.5px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase;
}
.note-badge.shared{ color:var(--doc-brass); background:rgba(45,100,188,0.12); }
.note-badge.private{ color:var(--doc-soft); background:var(--doc-well); border:1px solid var(--doc-line); }

.portal-form textarea, .portal-form input[type="text"], .portal-form input[type="tel"], .portal-form input[type="email"], .portal-form input[type="password"]{
  width:100%; font-family:var(--font-body); font-size:13.5px; padding:10px 12px;
  border:1px solid var(--doc-line); border-radius:var(--doc-radius); background:var(--doc-well); color:var(--doc-ink);
  resize:vertical;
}
.portal-form textarea:focus, .portal-form input:not([type="checkbox"]):not([type="radio"]):focus{ outline:none; border-color:var(--doc-brass); background:var(--doc-card); box-shadow:0 0 0 3px color-mix(in srgb, var(--doc-brass) 30%, transparent); }
.portal-form label{ display:block; font-family:var(--font-body); font-size:11px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; color:var(--doc-soft); margin-bottom:6px; }
.portal-form .field{ margin-bottom:14px; }

/* File picker: a clickable well (the <label> IS the control — clicking or
   dropping a file anywhere on it opens/fills the hidden input) instead of the
   browser's bare "Choose File" button. */
.portal-dropzone{
  display:flex; align-items:center; gap:12px; padding:12px 14px; cursor:pointer;
  border:1.5px dashed var(--doc-line); border-radius:var(--doc-radius-ctl); background:var(--doc-well);
  transition:border-color .12s ease, background .12s ease;
}
.portal-dropzone:hover, .portal-dropzone.dragover{ border-color:var(--doc-brass); background:var(--doc-accent-wash); }
.portal-dropzone-ico{
  width:36px; height:36px; flex-shrink:0; display:flex; align-items:center; justify-content:center;
  border-radius:var(--doc-radius-ctl); background:var(--doc-card); color:var(--doc-brass);
}
.portal-dropzone-ico svg{ width:18px; height:18px; }
.portal-dropzone-text{ min-width:0; flex:1; }
/* .portal-dropzone is itself a <label>, so it inherits .portal-form label's
   uppercase/tracked styling — reset it here since a real filename shouldn't
   be visually re-cased. */
.portal-dropzone-title{
  display:block; font-size:13px; font-weight:600; color:var(--doc-ink);
  text-transform:none; letter-spacing:normal;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.portal-dropzone-hint{
  display:block; font-size:11.5px; color:var(--doc-soft); margin-top:1px;
  text-transform:none; letter-spacing:normal;
}
.portal-dropzone.has-file .portal-dropzone-ico{ color:var(--doc-green); }
.portal-dropzone input[type="file"]{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
/* The clipped 1px input still receives keyboard focus, but any outline drawn
   on it is a ring around a 1-pixel box — invisible. Show focus on the frame
   the visitor actually sees (same pattern as the lead form's choice rows). */
.portal-dropzone:has(input:focus-visible){ border-color:var(--doc-brass); outline:2px solid var(--doc-brass); outline-offset:2px; }

/* Upload modal — matches the vendor's own "Upload documents" dialog on the
   booking page (ui_concept .bk-modal/.bk-dropzone), rebuilt on this page's
   doc-* tokens instead of the dashboard's --accent/--ink set. */
.doc-modal-backdrop{
  position:fixed; inset:0; background:rgba(15,23,42,.42);
  -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
  display:flex; align-items:center; justify-content:center; padding:24px;
  opacity:0; pointer-events:none; transition:opacity .16s ease; z-index:100;
}
.doc-modal-backdrop.open{ opacity:1; pointer-events:auto; }
/* Head/body/foot column that never grows past the backdrop's padded box, so a
   tall upload form scrolls INSIDE the card with its title and its Upload
   button pinned — the same anatomy .bk-modal has on the vendor side, which is
   also what lets the phone block below turn it into a sheet with no further
   structural work. The <form> is an extra layer between .doc-modal and the
   body/foot here (the vendor's isn't), so it has to pass the column through. */
.doc-modal{
  background:var(--doc-card); border-radius:var(--doc-radius-lg); box-shadow:var(--doc-shadow-lg);
  width:100%; max-width:400px;
  display:flex; flex-direction:column;
  max-height:100%;
  overflow:hidden;
}
.doc-modal > form{ display:flex; flex-direction:column; min-height:0; }
.doc-modal-head{
  display:flex; justify-content:space-between; align-items:center; gap:14px;
  padding:16px 20px; border-bottom:1px solid var(--doc-line);
  flex-shrink:0;
}
.doc-modal-head h3{ font-family:var(--font-display); font-size:16px; font-weight:600; margin:0; }
.doc-modal-close{ background:none; border:none; color:var(--doc-soft); cursor:pointer; padding:4px; border-radius:6px; display:flex; }
.doc-modal-close:hover{ background:var(--doc-well); color:var(--doc-ink); }
.doc-modal-body{
  padding:18px 20px; display:flex; flex-direction:column; gap:16px;
  overflow-y:auto; overscroll-behavior:contain; min-height:0;
}
.doc-modal-foot{
  display:flex; justify-content:flex-end; gap:10px;
  padding:14px 20px; border-top:1px solid var(--doc-line);
  flex-shrink:0;
}
.doc-modal-field{ display:flex; flex-direction:column; gap:7px; }
/* The dialog swaps between its "pick" and "result" phases by setting `hidden`
   on everything belonging to the other one. A `display:flex`/`inline-flex`
   author rule outranks the hidden attribute's UA rule, so every element the
   swap has to be able to hide needs to say so by name — the same guard
   .bk-dropzone and .bk-upload-queue carry on the vendor's side. */
.portal-dropzone[hidden],
.doc-modal-field[hidden],
.doc-btn[hidden]{ display:none; }
.doc-modal-field label{
  font-family:var(--font-body); font-size:11px; font-weight:600; letter-spacing:0.05em;
  text-transform:uppercase; color:var(--doc-soft);
}
.doc-modal-field select{
  border:1px solid var(--doc-line); border-radius:var(--doc-radius); background:var(--doc-well); color:var(--doc-ink);
  font-family:var(--font-body); font-size:13.5px; padding:10px 12px; width:100%;
  appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 10px center;
}
.doc-modal-field select:focus{ outline:none; border-color:var(--doc-brass); background-color:var(--doc-card); }
@media (prefers-reduced-motion: reduce){ .doc-modal-backdrop{ transition:none; } }

/* ---------- Upload modal becomes a bottom sheet on phones ----------
   Deliberately the same treatment, breakpoint and easing as every dialog on
   the vendor side (ui_concept.css, "Modals become bottom sheets on phones"),
   rebuilt on this page's doc-* tokens: the client's Upload file dialog should
   arrive from the bottom edge exactly the way the vendor's does on the booking
   page. Docking to the bottom also keeps Upload under the thumb. */
@media (max-width:720px){
  /* --kb-inset is how much of the screen the on-screen keyboard covers,
     measured off visualViewport by the page script. Neither vh nor dvh knows
     about the keyboard, so a sheet docked to bottom:0 would put its foot — the
     Upload button — underneath it. */
  .doc-modal-backdrop{ align-items:flex-end; padding:0; bottom:var(--kb-inset, 0px); }
  .doc-modal{
    max-width:none;
    border-radius:22px 22px 0 0;
    /* dvh, not vh: on a phone vh is the URL-bar-hidden height, so a 92vh sheet
       is taller than what you can see while the bar shows and its head sits
       above the top edge. The vh line is the fallback for engines without dvh;
       100% is the backdrop, already shortened by --kb-inset. */
    max-height:92vh;
    max-height:min(92dvh, 100%);
    transform:translateY(100%);
    transition:transform .3s cubic-bezier(.32,.72,0,1);
  }
  /* `none`, not `translateY(0)`: ANY transform other than none makes the sheet
     the containing block for its position:fixed descendants, and the upgraded
     Type dropdown's .cs-menu is placed in VIEWPORT coordinates — with a
     transform sitting on the settled sheet it renders offset and clipped. */
  .doc-modal-backdrop.open .doc-modal{ transform:none; }
  /* The drag handler rewrites the transform inline each frame, so the eased
     transition has to come off or the sheet trails the finger. */
  .doc-modal.dragging{ transition:none; }

  .doc-modal-head{ align-items:flex-start; }
  .doc-modal-close{ margin-top:2px; }
  .doc-modal-body{ padding-left:18px; padding-right:18px; }
  /* Cancel | Upload side by side, sharing the row equally — stacking them costs
     height the body needs and puts a full-width Cancel under the thumb. */
  .doc-modal-foot{
    flex-direction:row; flex-wrap:wrap; align-items:center; gap:10px;
    padding:12px 18px calc(12px + env(safe-area-inset-bottom, 0px));
  }
  .doc-modal-foot .doc-btn{ flex:1 1 0; min-width:0; }
}
@media (max-width:720px) and (prefers-reduced-motion: reduce){
  .doc-modal{ transition:none; }
}

/* ---------- Upload queue and batch report ----------
   The client's upload is the same flow the vendor gets on the booking page
   (ui_concept .bk-upload-queue / .bk-upload-result): one row per picked file
   carrying its own thumbnail and progress bar, then a report of what actually
   made it and what didn't. It replaces a one-file-at-a-time form behind a
   single shared meter, which could only ever name the last failure. Rebuilt on
   the doc-* tokens; the shapes, states and wording are deliberately identical.
   Same reasoning as the vendor's, and the comments there are the long form. */
.portal-upload-queue{ display:flex; flex-direction:column; gap:8px; }
/* display:flex above outranks the hidden attribute's UA rule, so say it. */
.portal-upload-queue[hidden]{ display:none; }
.portal-upload-queue-label{
  font-family:var(--font-body); font-size:11px; font-weight:600; letter-spacing:0.05em;
  text-transform:uppercase; color:var(--doc-soft);
}
.portal-upload-list{ display:flex; flex-direction:column; gap:8px; }
.portal-upload-item{
  display:flex; align-items:center; gap:10px;
  padding:8px 10px;
  border:1px solid var(--doc-line); border-radius:var(--doc-radius-ctl);
  background:var(--doc-card);
  animation:portalUploadItemIn .18s ease;
}
@keyframes portalUploadItemIn{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:translateY(0); } }
.portal-upload-item-icon{
  width:28px; height:28px; flex-shrink:0; overflow:hidden;
  display:flex; align-items:center; justify-content:center;
  border-radius:7px; background:var(--doc-well); color:var(--doc-soft);
}
/* Images get a real raster preview; everything else keeps the neutral glyph
   tile, differing by its icon rather than a colour invented per file type. */
.portal-upload-item-icon.is-image{ background:linear-gradient(135deg,var(--doc-accent-wash),var(--doc-well)); color:var(--doc-brass); }
.portal-upload-item-icon.has-img{ background:var(--doc-well); }
.portal-upload-item-icon img{ width:100%; height:100%; object-fit:cover; display:block; }
.portal-upload-item-info{ flex:1; min-width:0; display:flex; flex-direction:column; }
.portal-upload-item-name{
  display:block; font-size:12.5px; font-weight:600; color:var(--doc-ink);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.portal-upload-item-meta{ display:block; font-size:11.5px; color:var(--doc-soft); margin-top:1px; }
.portal-upload-item-bar{ height:4px; background:var(--doc-well); border-radius:20px; overflow:hidden; margin-top:6px; }
.portal-upload-item-bar > span{
  display:block; height:100%; width:0; border-radius:20px;
  background:linear-gradient(90deg, var(--doc-brass), var(--doc-brass-light));
  transition:width .15s linear;
}
.portal-upload-item.done .portal-upload-item-bar > span{ background:var(--doc-green); }
.portal-upload-item-pct{
  font-family:var(--font-mono); font-size:11px; color:var(--doc-soft);
  flex-shrink:0; width:32px; text-align:right;
}
.portal-upload-item-remove{
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  width:24px; height:24px; padding:0;
  border:none; background:none; border-radius:6px;
  color:var(--doc-soft); cursor:pointer;
}
.portal-upload-item-remove:hover{ background:var(--doc-well); color:var(--doc-red-ink); }
.portal-upload-item-remove:disabled{ opacity:.4; cursor:default; }
/* The bar and the pct readout are dead weight before the batch starts — an
   empty track under every row reads as "stuck at 0%" — so they only appear
   once something is actually uploading, and go away again on a row whose
   error has replaced the size in its meta line. */
.portal-upload-item-bar, .portal-upload-item-pct{ display:none; }
.portal-upload-queue.uploading .portal-upload-item-bar{ display:block; }
.portal-upload-queue.uploading .portal-upload-item-pct{ display:block; }
.portal-upload-item.error .portal-upload-item-bar,
.portal-upload-item.error .portal-upload-item-pct{ display:none; }
/* A finished row has to read as finished once the bars are gone, or a batch
   that half-failed shows its successes looking exactly like files that never
   started. */
.portal-upload-item.done{ border-color:var(--doc-green-line); }
.portal-upload-item.done .portal-upload-item-meta{ color:var(--doc-green-ink); }
.portal-upload-item.error{ border-color:var(--doc-red-line); background:var(--doc-red-wash); }
.portal-upload-item.error .portal-upload-item-meta{ color:var(--doc-red-ink); white-space:normal; }
.portal-upload-item.error .portal-upload-item-icon{ color:var(--doc-red-ink); }

/* ---- Result phase: what the finished batch actually did ---- */
.portal-upload-result{ display:flex; flex-direction:column; gap:14px; }
.portal-upload-result[hidden]{ display:none; }
.portal-upload-result-head{ display:flex; align-items:flex-start; gap:10px; }
.portal-upload-result-icon{
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  width:36px; height:36px; border-radius:50%;
  background:var(--doc-green-wash); color:var(--doc-green-ink);
}
.portal-upload-result.has-fails .portal-upload-result-icon{ background:var(--doc-amber-wash); color:var(--doc-amber); }
.portal-upload-result-title{ display:block; font-family:var(--font-display); font-size:15px; font-weight:600; color:var(--doc-ink); }
.portal-upload-result-sub{ display:block; font-size:12px; line-height:1.5; color:var(--doc-soft); margin-top:2px; }
.portal-upload-result-groups{ display:flex; flex-direction:column; gap:8px; }
/* One disclosure shape for both outcomes — what uploaded and what didn't are
   the same kind of statement, so they get the same block and differ only by
   which semantic pair colours them. */
.portal-upload-group{
  border:1px solid var(--doc-line); border-radius:var(--doc-radius-ctl);
  padding:8px 10px;
}
.portal-upload-group.is-ok{ border-color:var(--doc-green-line); }
.portal-upload-group.is-bad{ border-color:var(--doc-red-line); background:var(--doc-red-wash); }
.portal-upload-group > summary{
  display:flex; align-items:center; gap:8px; cursor:pointer;
  font-size:12.5px; color:var(--doc-ink); list-style:none;
}
.portal-upload-group > summary::-webkit-details-marker{ display:none; }
/* Own marker, so the row reads as a disclosure without the UA triangle. */
.portal-upload-group > summary::before{
  content:''; flex-shrink:0; width:0; height:0;
  border-left:5px solid currentColor; border-top:4px solid transparent; border-bottom:4px solid transparent;
  transition:transform .15s ease;
}
.portal-upload-group[open] > summary::before{ transform:rotate(90deg); }
.portal-upload-group > summary b{ font-weight:600; }
.portal-upload-group.is-ok > summary b{ color:var(--doc-green-ink); }
.portal-upload-group.is-bad > summary b{ color:var(--doc-red-ink); }
.portal-upload-group > summary span{ color:var(--doc-soft); }
.portal-upload-group-names{
  margin:8px 0 0; padding-left:13px;
  list-style:none; display:flex; flex-direction:column; gap:2px;
}
.portal-upload-group-names li{
  font-size:11.5px; color:var(--doc-soft);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
@media (prefers-reduced-motion: reduce){
  .portal-upload-item{ animation:none; }
  .portal-upload-group > summary::before{ transition:none; }
}

/* Email-updates opt-in inside the contact card. */
.portal-optin{
  margin:2px 0 16px; padding:14px; border:1px solid var(--doc-line);
  border-radius:var(--doc-radius-ctl); background:var(--doc-well);
}
label.portal-optin-check{ display:flex; align-items:center; gap:9px; cursor:pointer; margin-bottom:0; text-transform:none; letter-spacing:normal; }
.portal-optin-check input{ flex-shrink:0; }
.portal-optin-check span{ font-family:var(--font-body); font-size:13.5px; font-weight:600; color:var(--doc-ink); }
.portal-optin-hint{ margin:6px 0 12px 25px; font-size:12px; line-height:1.5; color:var(--doc-soft); }
.portal-optin .field{ margin-bottom:0; }
.portal-optin .field input{ background:var(--doc-card); }

/* Note visibility — segmented Private/Shared toggle + info tooltip, matching
   the vendor's booking-page note composer (ui_concept .bk-note-vis-btn / .bk-info). */
.portal-note-foot{ display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-top:14px; }
.portal-vis-group{ display:inline-flex; align-items:center; gap:8px; }
.portal-visibility{
  display:inline-flex; gap:2px;
  background:var(--doc-well); padding:3px; border-radius:var(--doc-radius-ctl);
}
.portal-form .portal-vis-opt{
  display:inline-flex; align-items:center; gap:5px; text-transform:none; letter-spacing:normal;
  margin-bottom:0; font-family:var(--font-body); font-size:11.5px; font-weight:600; color:var(--doc-soft);
  padding:6px 12px; border-radius:6px; border:1px solid transparent; cursor:pointer;
}
.portal-form .portal-vis-opt:has(input:checked){ background:var(--doc-card); border-color:var(--doc-line); color:var(--doc-ink); }
.portal-vis-opt input{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
/* Same invisible-1px-focus problem as the dropzone above: tabbing through
   Private/Shared showed nothing at all. */
.portal-form .portal-vis-opt:has(input:focus-visible){ outline:2px solid var(--doc-brass); outline-offset:2px; }
.portal-vis-lock{ flex-shrink:0; }

.portal-info{
  position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  width:24px; height:24px; padding:0;
  border:1px solid transparent; border-radius:50%;
  background:transparent; color:var(--doc-soft); cursor:pointer;
  flex:0 0 auto; transition:color .12s, border-color .12s;
}
.portal-info:hover, .portal-info.open{ color:var(--doc-ink); border-color:transparent; }
.portal-info::after{
  content:attr(data-tooltip);
  position:absolute; bottom:calc(100% + 9px); right:0; left:auto; transform:none;
  width:max-content; max-width:min(230px, calc(100vw - 48px));
  background:var(--doc-ink); color:#fff;
  font-family:var(--font-body); font-size:11.5px; font-weight:500; line-height:1.45; text-align:left;
  padding:8px 10px; border-radius:var(--doc-radius-ctl);
  box-shadow:var(--doc-shadow-lg);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .14s ease, visibility .14s;
  z-index:60;
}
.portal-info::before{
  content:""; position:absolute; bottom:calc(100% + 3px); right:7px; left:auto; transform:none;
  border:5px solid transparent; border-top-color:var(--doc-ink);
  opacity:0; visibility:hidden; transition:opacity .14s ease, visibility .14s;
  z-index:61;
}
.portal-info.open::after, .portal-info.open::before{ opacity:1; visibility:visible; }

/* input[type="email"] included: the opt-in email field is the one the AJAX
   error path most often flags, and it was missing from this list — so it
   could never show its error border. */
.portal-form .field.has-error textarea,
.portal-form .field.has-error input[type="text"],
.portal-form .field.has-error input[type="tel"],
.portal-form .field.has-error input[type="email"]{
  border-color:var(--doc-red); background:rgba(180,48,74,0.04);
}
.portal-form .field-error{
  display:none; margin-top:6px; font-size:12px; font-weight:600;
  color:var(--doc-red); text-transform:none; letter-spacing:normal;
}
.portal-form .field.has-error .field-error{ display:block; }

.doc-foot{ text-align:center; margin-top:10px; font-size:12px; color:var(--doc-soft); }
.doc-foot a{ color:var(--doc-brass); font-weight:600; text-decoration:none; }
.doc-foot a:hover{ text-decoration:underline; }

.portal-unlock-wrap{ max-width:380px; margin:80px auto; text-align:center; }
.portal-unlock-lock{
  width:52px; height:52px; margin:0 auto 16px;
  border-radius:50%; background:var(--doc-accent-wash);
  display:flex; align-items:center; justify-content:center;
}
.portal-unlock-lock svg{ width:24px; height:24px; fill:none; stroke:var(--doc-brass); stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
.portal-password-input{
  font-family:var(--font-mono);
  letter-spacing:0.06em; font-size:15px; font-weight:600; text-align:center;
}
/* Show/hide toggle riding inside the field — auth_open.js's .ao-pw-toggle
   behavior, drawn on this page's own tokens (the portal deliberately loads
   no auth_open.css). padding-right keeps centred text clear of it. */
.portal-pw-wrap{ position:relative; }
.portal-pw-wrap .portal-password-input{ padding-right:40px; padding-left:40px; }
.portal-pw-toggle{
  position:absolute; top:50%; right:2px; transform:translateY(-50%);
  width:36px; height:36px; display:flex; align-items:center; justify-content:center;
  background:none; border:none; border-radius:6px; cursor:pointer; color:var(--doc-soft);
}
.portal-pw-toggle:hover{ color:var(--doc-ink); }
.portal-pw-toggle:focus-visible{ outline:2px solid var(--doc-brass); outline-offset:2px; }
.portal-pw-toggle svg{ width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
.portal-unlock-error{ color:var(--doc-red); }

@media print{
  body{ background:#fff; }
  body.has-vendor-bar{ padding-top:0; }
  .doc-wrap{ padding:0; max-width:none; }
  .doc-card{ border:none; page-break-inside:avoid; }
  .portal-vendor-bar, .portal-lock-row, .doc-vendor-contact, .portal-notify,
  .portal-notes-load-more, .portal-form, .doc-btn.pay, .doc-btn.ghost,
  .doc-modal-backdrop,
  [data-ack-timeline], [data-delete-note] { display:none !important; }
}
