/* ProjectoFolio global stylesheet
   Loaded from Includes/Server/_header.php on every authenticated page.
   Page-specific styles live in code/css/<page>.css and are linked from
   the individual page. */

:root {
  --line: #eee;
  --muted: #888;
  --accent: #1a73e8;
  --accent-hover: #1763cc;
  --bg: #fafafa;
  --text: #222;
  --text-soft: #555;
  --danger: #c5221f;
  --danger-bg: #fdecec;
  --danger-border: #f7c8c5;
  --danger-text: #a4231b;
  --success-bg: #e6f4ea;
  --success-border: #cdecd6;
  --success-text: #1e7d3a;
  --warn-bg: #fff4dc;
  --warn-text: #8a5a06;
  --owner-bg: #e8f0fe;
  --viewer-bg: #f1f3f4;
  --viewer-text: #5f6368;
  --invite-bg: #fff8e1;
  --invite-border: #ffe082;
  --stale-bg: #fde7e7;
  /* Indigo, the Pro Extended tier identity. Ink and bg are the shipped card's
     exact ink/panel (code/Images/plans/src/README.md), the same way --accent /
     --owner-bg match Pro's card. That pair is the OKLCh midpoint of Pro and
     Business; the border keeps it, mixing bg 21.9% toward the ink in OKLCh. */
  --extended-bg: #e9ebfe;
  --extended-border: #c5ccfa;
  --extended-text: #455ae2;
  --border: #dadce0;
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --font-sans: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}

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

body {
  font-family: var(--font-sans);
  margin: 0;
  color: var(--text);
  background: #fff;
}

a {
  color: var(--accent);
  text-decoration: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
a:hover {
  text-decoration: underline;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- App header ---------- */
header.app {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 24px;
  border-bottom: 1px solid var(--line);
  background: #fff;
}
header.app .brand {
  display: flex;
  align-items: center;
  gap: 10px;
}
header.app .brand a {
  display: flex;
  align-items: center;
  gap: 10px;
  color: inherit;
}
header.app .brand a:hover {
  text-decoration: none;
}
header.app .brand img.logo {
  width: 30px;
  height: 30px;
  object-fit: contain;
}
header.app .brand strong {
  font-size: 1rem;
}
header.app .who {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 0.88rem;
  color: var(--text-soft);
  flex-wrap: wrap;
  justify-content: flex-end;
}
header.app .who img.avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
}
header.app .who a {
  color: var(--text-soft);
}

/* ---------- Account switcher (header dropdown) ----------
   A <details> so it opens by tap with no JS (Android WebView included). The
   menu anchors to the toggle on wide screens; at <=600px it anchors to the
   whole header instead (see the media query below) so it can never run off
   the left edge when .who wraps and the toggle lands on the left. */
.account-switcher {
  position: relative;
}
.account-switcher__toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 44px hit target without making the header row taller. */
  padding-block: 12px;
  margin-block: -12px;
  color: var(--text-soft);
  cursor: pointer;
  list-style: none;
  -webkit-user-select: none;
  user-select: none;
}
.account-switcher__toggle::-webkit-details-marker {
  display: none;
}
.account-switcher__toggle::after {
  content: "";
  width: 0;
  height: 0;
  border-inline: 4px solid transparent;
  border-block-start: 5px solid currentColor;
  transition: transform 0.15s ease;
}
.account-switcher__toggle:hover {
  color: var(--text);
}
.account-switcher[open] > .account-switcher__toggle {
  color: var(--text);
}
.account-switcher[open] > .account-switcher__toggle::after {
  transform: rotate(180deg);
}
.account-switcher__menu {
  position: absolute;
  inset-block-start: calc(100% + 10px);
  inset-inline-end: 0;
  z-index: 20;
  /* Chrome's ::details-content slot breaks the `*` inherit chain, so the menu
     and everything in it would fall back to content-box. */
  box-sizing: border-box;
  width: max-content;
  min-width: 220px;
  max-width: min(320px, calc(100vw - 32px));
  margin: 0;
  padding: 6px;
  list-style: none;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
.account-switcher__item {
  margin: 0;
}
.account-switcher__form {
  margin: 0;
}
.account-switcher__option {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: start;
  overflow-wrap: anywhere;
  cursor: pointer;
}
.account-switcher__option:hover {
  background: var(--bg);
}
.account-switcher__option:disabled {
  cursor: default;
}
.account-switcher__item.is-current .account-switcher__option {
  background: var(--owner-bg);
  color: var(--accent);
  font-weight: 600;
}
@media (max-width: 600px) {
  header.app {
    position: relative;
  }
  .account-switcher {
    position: static;
  }
  .account-switcher__menu {
    inset-block-start: 100%;
    inset-inline: 16px;
    width: auto;
    min-width: 0;
    max-width: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .account-switcher__toggle::after {
    transition: none;
  }
}

main {
  padding: 28px 32px;
  max-width: 1100px;
  margin: 0 auto;
}
/* Opt-in wide container, for pages whose content is a grid of figures rather
   than prose. EIGHT pages ask for it -- grep $pageWide: projects/index.php,
   projects/works.php, projects/_works_shot.php, projects/subcontractors.php,
   workforce/index.php, workforce/people.php, workforce/report_company.php and
   workforce/self_bill.php. Every one is a table screen, which is why raising
   this number is safe; it reaches all of them at once.
   DO NOT "simplify" this by widening bare main instead: help.php, privacy.php
   and terms.php live in the same container and are deliberately held to a
   58-68ch measure, because long lines of prose are measurably harder to read.
   Widening main would fix the tables and degrade every prose page at once.
   Arithmetic, re-measured 2026-09-19 and not guessed: main eats 32px of
   padding each side and the .card the table sits in eats 20px padding + 1px
   border each side, so the scroller is (viewport or cap) - 106.
   THE BINDING CASE IS THE CAP, NOT 1280 -- CORRECTED 2026-09-19. The earlier
   note here said the opposite, on the reasoning that a 1280px laptop never
   reaches the cap. True, and beside the point: the owner works on a large
   monitor, where the viewport is never the constraint and the cap is the only
   thing setting the width. At 1320 the scroller stopped at 1214px on a 1920px
   screen and 700px of desktop went unused.
   1520 IS MEASURED, NOT CHOSEN. 1520 - 106 = 1414px of scroller. The split
   table is width:100% (see the bare `table` rule above), so it never scrolls
   until the scroller drops under its 1125px min-content -- "it does not fit"
   means the project column crushed, not a scrollbar. With the owner's real
   names, the longest of which ("220 - Alexander Vandekendelaere  MONZA 40ft HC
   (9x3,5)") measures 390px on one line, the project column resolves to 168px
   and four wrapped lines at the old 1320 cap, 243px and two lines at 1500, and
   251px and two lines at 1520. 1500 is the knee and this keeps 20px over it.
   ABOVE 1520 NOTHING MEASURABLE IMPROVES: a one-line name needs the column at
   406px, which auto table layout only hands it near a 1900px cap, and the
   second line is already free -- the row is 73px tall either way, set by the
   extra-works and transport input stack, not by the name. Measured in headless
   Chromium on Linux, whose fallback face is about 70px wider over that name
   than the owner's Segoe UI, so the real figures have more slack, not less.
   WHEN A COLUMN IS ADDED, THE WIDTH STILL COMES OUT OF THE TABLE FIRST. The
   cap now has room in it, which is exactly why it is worth saying: see the
   gutter, context-column and control widths in workforce.css.
   No media query needed: below the cap the element is narrower than it anyway,
   so this rule is a no-op and .is-wide behaves exactly as main does today,
   including the <=600px padding override below. */
main.is-wide {
  max-width: 1520px;
}
/* FULL-BLEED, PROJECTS LIST ONLY (projects/index.php). That screen is a plain
   seven-column table with no figures measured against a cap, so it is asked to
   use the whole monitor; the other seven .is-wide pages KEEP 1520, because the
   workforce widths (see the gutter/context/control notes in workforce.css) are
   measured against that number and would move if the cap were lifted for all.
   Only max-width is dropped -- the 28px/32px from bare `main` is the intended
   breathing room and stays, as does the <=600px 20px/16px override above.
   Hook is [data-import-link] in the .page-toolbar, which the page already
   emits and nothing else in the app does; the toolbar is a direct child of
   <main> in BOTH the zero-projects state (which renders only a .card) and the
   populated one, so a .projects-table hook would have missed the empty state.
   Lives here rather than in projects-list.css because that file is <link>ed
   inside the non-empty branch only.
   :has() is progressive enhancement, same as workforce.css: a browser without
   it keeps the 1520px container, which is a narrower page and not a broken
   one. The 68ch prose and 36rem field caps below still apply and are what
   stops text and inputs stretching at 2560px. */
main.is-wide:has(> .page-toolbar [data-import-link]) {
  max-width: none;
}
/* WHAT MUST NOT GROW WITH THE CONTAINER. The cap exists for grids of figures;
   a paragraph and a text box are neither. Held to the same 68ch these pages
   already use, and the controls to 36rem -- 576px, which is what a field in a
   two-up .wf-form__grid measures on an uncapped 1280px laptop (578px), so the
   form looks the same at 1520 as it does on the machine it was built on.
   Inline .form-help spans (the per-project cap note in the split table's share
   cell) are unaffected: max-width does nothing to an inline box. */
main.is-wide .card > p.muted,
main.is-wide p.form-help {
  max-width: 68ch;
}
main.is-wide .form-field input[type=text],
main.is-wide .form-field input[type=email],
main.is-wide .form-field input[type=date],
main.is-wide .form-field input[type=number],
main.is-wide .form-field select,
main.is-wide .form-field textarea {
  max-width: 36rem;
}

/* ---------- Site footer (Includes/Server/_footer.php, every page) ---------- */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 20px 32px 28px;
  margin-top: 40px;
  background: var(--bg);
  font-size: 0.82rem;
  color: var(--muted);
}
.site-footer__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  max-width: 1100px;
  margin: 0 auto;
}
.site-footer__nav a {
  color: var(--text-soft);
}
.site-footer__legal {
  max-width: 1100px;
  margin: 10px auto 0;
}

@media (max-width: 600px) {
  header.app {
    padding: 10px 16px;
  }
  main {
    padding: 20px 16px;
  }
  .site-footer {
    padding: 18px 16px 24px;
  }
  header.app .brand strong {
    display: none;
  }
}

/* ---------- Typography ---------- */
h1.page {
  font-size: 1.4rem;
  margin: 0 0 18px;
}
.muted {
  color: var(--muted);
}

/* ---------- Pills ---------- */
.pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  /* Role labels (owner/editor/viewer) must stay on one line. */
  white-space: nowrap;
  /* Neutral default so a pill with no variant class (admin/security.php's
     reason / identifier-type labels) is still a legible chip rather than bare
     padded text. The variant rules below have the same specificity and come
     later, so they keep winning. */
  background: var(--viewer-bg);
  color: var(--viewer-text);
}
.pill.owner   { background: var(--owner-bg);   color: var(--accent); }
.pill.editor  { background: var(--success-bg); color: var(--success-text); }
.pill.viewer  { background: var(--viewer-bg);  color: var(--viewer-text); }
.pill.status-active { background: var(--success-bg); color: var(--success-text); }
.pill.status-stale  { background: var(--stale-bg);   color: var(--danger-text); }

/* ---------- Tables ---------- */
table {
  border-collapse: collapse;
  width: 100%;
  /* Prevent long unbreakable content (filenames, tokens, URLs) from
     pushing the table wider than its container. Combined with
     overflow-wrap on cells, this keeps every section the same width
     as <main>. */
  max-width: 100%;
}
th, td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: 0.92rem;
  vertical-align: middle;
  /* Long filenames / URLs / emails must break rather than overflow. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
th {
  background: var(--bg);
  font-weight: 600;
}
td.actions {
  text-align: right;
}
td.actions > * + * {
  margin-left: 8px;
}

/* ---------- Document image thumbnails (project documents table) ---------- */
.doc-thumb-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}
.doc-thumb {
  flex-shrink: 0;
  width: 96px;
  height: 96px;
  object-fit: cover;
  border: 1px solid var(--line, #eee);
  border-radius: var(--radius-sm, 4px);
  background-color: var(--bg, #f3f3f3);
  transition: transform 120ms ease, border-color 120ms ease;
}
.doc-thumb-link:hover .doc-thumb {
  transform: scale(1.03);
}
.doc-thumb-name {
  overflow-wrap: anywhere;
  word-break: break-word;
}
.doc-thumb-link:hover .doc-thumb-name {
  text-decoration: underline;
}
/* Wrapper for video doc thumbnails: anchors the play overlay over the
   <img> (or placeholder span). The wrapper itself takes no dimensions —
   the inner .doc-thumb keeps the 56/44 box. */
.doc-thumb-wrap {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  line-height: 0;
}
/* Placeholder tile when a video has no stored thumbnail. Inherits the
   .doc-thumb box (size, border, radius); only the background differs. */
.doc-thumb--video-placeholder {
  display: inline-block;
  background: linear-gradient(135deg, #2a2a2a, #4a4a4a);
  border-color: #1f1f1f;
}
/* Placeholder tile shown until JS swaps in a rendered <img> of the PDF's
   first page (or if rendering fails). Inherits the .doc-thumb box, so no
   layout shift on swap; only fill + the "PDF" cue differ. */
.doc-thumb--pdf-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--bg, #f3f3f3);
  border-color: var(--border, var(--line, #eee));
}
.doc-thumb--pdf-placeholder::after {
  content: 'PDF';
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--muted, #888);
}
.doc-thumb-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 0.7rem;
  line-height: 1;
  /* ▶ glyph is visually weighted left — nudge it to optical center. */
  padding-left: 2px;
  pointer-events: none;
  transition: transform 120ms ease, background-color 120ms ease;
}
.doc-thumb-link:hover .doc-thumb-play {
  transform: translate(-50%, -50%) scale(1.12);
  background: rgba(0, 0, 0, 0.8);
}
@media (max-width: 600px){
  .doc-thumb-link {
    gap: 8px;
  }
  /* Hiding the uploader/uploaded columns frees room to enlarge thumbnails
     so file content is legible on small screens. */
  table[data-category] .col-uploader,
  table[data-category] .col-uploaded {
    display: none;
  }
  /* Hide date / secondary columns (Members "Joined", Pending invites
     "Expires", Projects list "Owner" + "End date") to keep these tables
     narrow on small screens. */
  .col-joined,
  .col-expires,
  .col-owner,
  .col-end-date {
    display: none;
  }
  .doc-thumb {
    width: 80px;
    height: 80px;
  }
  .doc-thumb-play {
    width: 28px;
    height: 28px;
    font-size: 0.8rem;
  }
}
@media (prefers-reduced-motion: reduce){
  .doc-thumb {
    transition: none;
  }
  .doc-thumb-link:hover .doc-thumb {
    transform: none;
  }
  .doc-thumb-play {
    transition: none;
  }
  .doc-thumb-link:hover .doc-thumb-play {
    transform: translate(-50%, -50%);
  }
}

/* ---------- PDF preview modal (projects/view.php) ---------- */
.swal2-popup.swal-pdf-popup {
  max-width: 95vw;
  width: 95vw;
}
.swal2-popup.swal-pdf-popup .swal2-html-container {
  margin: 0;
  padding: 0;
  overflow: hidden;
}
.pdf-swal-frame {
  display: block;
  width: 100%;
  height: 82vh;
  border: 0;
  background: #525659;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-block;
  padding: 8px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: #fff;
  color: #1a1a1a;
  font-size: 0.9rem;
  cursor: pointer;
  text-decoration: none;
  min-height: 36px;
  line-height: 1.2;
}
.btn:hover {
  background: #f5f5f5;
  text-decoration: none;
}
.btn.primary {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.btn.primary:hover {
  background: var(--accent-hover);
}
.btn.danger {
  color: var(--danger);
  border-color: var(--danger-border);
}
.btn.danger:hover {
  background: var(--danger-bg);
}
/* Compact button — sizing-only modifier; composes with .btn, .btn.danger, .btn-whatsapp */
.btn-xs {
  padding: 3px 8px;
  font-size: 0.78rem;
  min-height: 0;
}
/* WhatsApp share button — color-only modifier; sizing stays with .btn / .btn-xs */
.btn-whatsapp {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  background: #25d366;
  color: #fff;
  border-color: #25d366;
}
.btn-whatsapp__icon {
  width: 1.1em;
  height: 1.1em;
  flex: 0 0 auto;
}
.btn-whatsapp:hover {
  background: #1ebe5d;
  border-color: #1da851;
}
.btn-whatsapp:focus-visible {
  outline-color: #1da851;
}
/* Toggle buttons used in permissions tables */
.btn--toggle {
  padding: 4px 10px;
  font-size: 0.82rem;
  min-height: 0;
}
.btn--toggle.is-excluded {
  background: var(--danger-bg);
  color: var(--danger-text);
  border-color: var(--danger-border);
}
.btn--toggle.is-visible {
  background: var(--success-bg);
  color: var(--success-text);
  border-color: var(--success-border);
}

/* ---------- Unavailable buttons ----------
   Matches BOTH :disabled and [aria-disabled="true"]: the app renders the two
   together on real buttons (workforce/tool.php's AI actions), while links and
   script-driven controls carry only the attribute. Styling one and not the
   other leaves half the app's disabled buttons looking live.
   Every variant converges on the SAME flat grey surface, so the cue is a change
   of lightness, not of hue -- it survives greyscale and colour blindness, which
   a desaturated accent fill would not. Text stays at --text-soft (#555), the
   app's readable soft ink, over #ececec: ~6.3:1, comfortably past AA.
   Deliberately placed after every .btn rule above so it wins the
   equal-specificity ties against .btn.primary / .btn.danger / .btn--toggle.*
   without !important. Page stylesheets load after this file, so export.css and
   import.css keep their intentional [aria-disabled] upsell styling. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: #ececec;
  color: var(--text-soft);
  border-color: #c4c4c4;
  box-shadow: none;
  cursor: not-allowed;
}
/* 0,3,0 and later in the file than .btn.primary:hover, .btn.danger:hover and
   .btn-whatsapp:hover, all of which are also 0,3,0 -- source order settles it,
   so no live hover fill can reappear on a control that cannot be pressed. */
.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  background: #ececec;
  color: var(--text-soft);
  border-color: #c4c4c4;
  text-decoration: none;
}

/* ---------- Forms ---------- */
form.inline {
  display: inline-block;
  margin: 0;
}
input[type=text],
input[type=email],
input[type=date],
input[type=file],
textarea,
select {
  font: inherit;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  min-height: 36px;
}
textarea {
  width: 100%;
  min-height: 80px;
}
.form-field {
  margin-bottom: 14px;
}
.form-field label {
  display: block;
  margin-bottom: 6px;
  font-weight: 500;
}
.form-field input[type=text],
.form-field textarea {
  width: 100%;
}
.form-help {
  font-size: 0.82rem;
  margin-top: 4px;
}
/* "Share with account" checkbox row on projects/create_project.php. */
.form-field .project-share-choice__label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  min-height: 44px;
  padding-block: 10px;
  margin-bottom: 0;
  cursor: pointer;
}
.project-share-choice__box {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.project-share-choice__help {
  margin-top: 0;
  padding-inline-start: 28px;
  max-width: 68ch;
}

/* ---------- Cards / rows ---------- */
.card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 20px;
  margin-bottom: 16px;
  background: #fff;
}
.card--invite {
  background: var(--invite-bg);
  border-color: var(--invite-border);
}
.card--danger {
  border-color: var(--danger-border);
}
.row {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.row--upload input[type=text],
.row--upload input[type=file] {
  flex: 1;
  min-width: 180px;
}
.row--invite input[type=email] {
  flex: 1;
  min-width: 200px;
}

/* ---------- Flash messages ---------- */
.flash {
  padding: 10px 14px;
  border-radius: var(--radius-md);
  margin-bottom: 12px;
}
.flash.error {
  background: var(--danger-bg);
  color: var(--danger-text);
}
.flash.success {
  background: var(--success-bg);
  color: var(--success-text);
}
.flash.warn {
  background: var(--warn-bg);
  color: var(--warn-text);
}

/* ---------- Inline code ---------- */
code {
  background: var(--bg);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
}
code.url {
  display: block;
  margin-top: 8px;
  word-break: break-all;
}
code.url--sm {
  font-size: 0.78rem;
  word-break: break-all;
  display: inline-block;
}
/* Collapse the pending-invites Link cell to just the copy icon. */
/* Placed after the base rule so source order wins at <=600px. */
@media (max-width: 600px){
  code.url--sm {
    display: none;
  }
}

/* ---------- Copy-link icon button (pending invites Link cell) ---------- */
.copy-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: 8px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: #fff;
  color: var(--muted);
  cursor: pointer;
  vertical-align: middle;
}
.copy-link:hover {
  background: #f5f5f5;
  border-color: #c4c7cc;
}
.copy-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.copy-link__icon {
  display: block;
  width: 16px;
  height: 16px;
}

.section-heading {
  font-size: 1.1rem;
  margin: 28px 0 10px;
}
.section-heading--sub {
  font-size: 1.05rem;
  margin: 28px 0 10px;
}
.section-heading--danger {
  color: var(--danger-text);
}
.subgroup-heading {
  font-size: 0.95rem;
  margin: 18px 0 6px;
  color: #444;
}
.subgroup-heading .muted {
  font-weight: normal;
}
.text-sm {
  font-size: 0.88rem;
}
.text-xs {
  font-size: 0.82rem;
}
.text-xxs {
  font-size: 0.78rem;
}
.empty-note {
  font-size: 0.85rem;
  margin: 0 0 8px;
}

/* ---------- Page-specific patterns used on multiple pages ---------- */

/* projects/index.php — toolbar above the project list */
.page-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}
.page-toolbar h1.page {
  margin: 0;
}

/* projects/view.php — title with inline status pill */
.project-title {
  margin-bottom: 6px;
}
.project-title .pill {
  margin-left: 8px;
}

/* projects/view.php — QR code floats top-right of the page */
.project-qr {
  float: right;
  text-align: center;
  margin-left: 24px;
}
.project-qr img {
  width: 140px;
  height: 140px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  display: block;
}
.project-qr a {
  font-size: 0.78rem;
  display: inline-block;
  margin-top: 4px;
}
@media (max-width: 600px) {
  .project-qr {
    float: none;
    margin: 0 0 16px;
  }
}

/* projects/view.php — Cover image control (in-page) */
.cover-image-control {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.cover-image-preview {
  width: 120px;
  height: 120px;
  object-fit: cover;
  border-radius: var(--radius-md, 6px);
  border: 1px solid var(--line, #eee);
  display: block;
}
.cover-image-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.cover-image-pick-btn--empty {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 22px;
  font-size: 1rem;
  border: 2px dashed var(--line, #eee);
  background: transparent;
  color: var(--accent, #1a73e8);
  transition: background-color 150ms ease, border-color 150ms ease;
}
.cover-image-pick-btn--empty:hover,
.cover-image-pick-btn--empty:focus-visible {
  background: var(--bg, #fafafa);
  border-color: var(--accent, #1a73e8);
}
.cover-image-pick-btn__plus {
  font-size: 1.4rem;
  line-height: 1;
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .cover-image-pick-btn--empty { transition: none; }
}

/* projects/view.php — Cover picker modal (SweetAlert2 body) */
.swal2-popup.swal-cover-picker-popup {
  max-width: 95vw;
}
.cover-picker-modal__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px;
  max-height: 60vh;
  overflow-y: auto;
  padding-right: 4px;
}
.cover-picker-modal__item {
  position: relative;
  aspect-ratio: 1 / 1;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius-sm, 4px);
  background: var(--bg, #fafafa);
  overflow: hidden;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 150ms ease;
}
.cover-picker-modal__item:hover,
.cover-picker-modal__item:focus-visible {
  border-color: var(--accent, #1a73e8);
}
.cover-picker-modal__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.cover-picker-modal__item--clear {
  background: #fff;
  border: 2px dashed var(--line, #eee);
  flex-direction: column;
  gap: 6px;
  color: var(--muted, #888);
}
.cover-picker-modal__clear-icon {
  font-size: 2rem;
  line-height: 1;
  color: var(--muted, #888);
}
@media (prefers-reduced-motion: reduce) {
  .cover-picker-modal__item { transition: none; }
}

/* ---------- Plan usage card (index.php — between toolbar and project list) ----------
   Mirrors the .card panel look and reuses the meter/progress pattern from
   home.css (.proj-card__progress). Lives here because the badge tier tints
   reuse the shared role/status palette. */
.plan-usage-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 16px 20px;
  margin-bottom: 18px;
  background: #fff;
}
.plan-usage-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
/* CTA to /plans.php sitting beside the plan badge. The base class is the quiet
   variant kept for paid tiers ("Manage plan") — a small accent link/pill, not a
   heavy button. Free-plan users get the promoted --cta variant below. */
.plan-usage-upgrade {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--accent);
  background: var(--owner-bg);
}
.plan-usage-upgrade:hover {
  background: var(--accent);
  color: #fff;
  text-decoration: none;
}
.plan-usage-upgrade:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Promoted Free-plan variant: a real button, matching .btn.primary's fill,
   radius and line-height, sized down slightly so it balances the small badge
   next to it. Higher specificity than the base rules, so hover/focus below must
   restate the button colours. */
.plan-usage-upgrade--cta {
  padding: 7px 14px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  font-size: 0.82rem;
  line-height: 1.2;
  min-height: 32px;
  color: #fff;
  background: var(--accent);
}
.plan-usage-upgrade--cta:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #fff;
}
.plan-usage-upgrade--cta:focus-visible {
  outline: 2px solid var(--accent-hover);
  outline-offset: 2px;
}
/* Supporting sentence under the header, Free plan only. */
.plan-usage-note {
  margin-block: 0 14px;
  margin-inline: 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--muted);
}
.plan-usage-badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  background: var(--viewer-bg);
  color: var(--viewer-text);
}
.plan-usage-badge.plan-free {
  background: var(--viewer-bg);
  color: var(--viewer-text);
}
.plan-usage-badge.plan-starter {
  background: var(--success-bg);
  color: var(--success-text);
  border: 1px solid var(--success-border);
}
.plan-usage-badge.plan-pro {
  background: var(--owner-bg);
  color: var(--accent);
}
.plan-usage-badge.plan-pro_extended {
  background: var(--extended-bg);
  color: var(--extended-text);
  border: 1px solid var(--extended-border);
}
/* Business tier = premium/gold accent, reusing the invite (amber) palette. */
.plan-usage-badge.plan-business {
  background: var(--invite-bg);
  color: #8a5a06;
  border: 1px solid var(--invite-border);
}
.plan-usage-meter + .plan-usage-meter {
  margin-top: 12px;
}
.plan-usage-meter-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}
.plan-usage-meter-name {
  font-weight: 500;
  font-size: 0.9rem;
}
.plan-usage-meter-value {
  font-size: 0.82rem;
  color: var(--text-soft);
}
.plan-usage-bar {
  width: 100%;
  height: 7px;
  background: var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.plan-usage-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--text-soft); /* fallback if no state modifier present */
  transition: width 250ms ease;
}
.plan-usage-bar-fill.usage-ok        { background: var(--success-text); }
.plan-usage-bar-fill.usage-warn      { background: var(--warn-text); }
.plan-usage-bar-fill.usage-over      { background: var(--danger-text); }
/* Unlimited: no inline width — span the track with a quiet striped fill that
   reads as "no limit" rather than alarming. */
.plan-usage-bar-fill.usage-unlimited {
  width: 100%;
  background: var(--muted);
  background-image: linear-gradient(
    45deg,
    rgba(255, 255, 255, 0.25) 25%, transparent 25%,
    transparent 50%, rgba(255, 255, 255, 0.25) 50%,
    rgba(255, 255, 255, 0.25) 75%, transparent 75%, transparent
  );
  background-size: 10px 10px;
}
@media (prefers-reduced-motion: reduce) {
  .plan-usage-bar-fill { transition: none; }
}

/* ---------- Recently deleted / trash sections ----------
   projects/view.php (recently-deleted) + projects/index.php
   (recently-deleted-projects). The data-section-key lives on the .section-heading;
   the intro <p> and <table> are following siblings. Styled as a quiet,
   secondary "archived" area via a top divider + muted background, reusing
   existing tokens — no new visual language. */
.section-heading[data-section-key="recently-deleted"],
.section-heading[data-section-key="recently-deleted-projects"] {
  margin-top: 36px;
  padding-top: 24px;
  border-top: 1px solid var(--line);
  color: var(--text-soft);
}
/* Tint the intro paragraph + table so the block reads as de-emphasised.
   ~ (general sibling) reaches the <p> and <table> after the heading; nothing
   else sits between the heading and its table in this markup. */
.section-heading[data-section-key="recently-deleted"] ~ p.muted,
.section-heading[data-section-key="recently-deleted-projects"] ~ p.muted,
.section-heading[data-section-key="recently-deleted"] ~ table,
.section-heading[data-section-key="recently-deleted-projects"] ~ table {
  background: var(--bg);
  border-radius: var(--radius-md);
}
.section-heading[data-section-key="recently-deleted"] ~ p.muted,
.section-heading[data-section-key="recently-deleted-projects"] ~ p.muted {
  padding: 8px 12px;
}
/* Header row already uses --bg; drop its bottom border so the tinted table
   reads as one quiet panel rather than a standard data table. */
.section-heading[data-section-key="recently-deleted"] ~ table th,
.section-heading[data-section-key="recently-deleted-projects"] ~ table th {
  border-bottom-color: var(--border);
}
/* "Time left" / days-remaining cell: keep the quiet .muted color but stop it
   wrapping ("3 days" -> two lines) and keep the column narrow. */
.section-heading[data-section-key="recently-deleted"] ~ table td.muted + td.muted {
  white-space: nowrap;
  width: 1%;
}
/* Restore action: keep the column tight and stop the inherited
   overflow-wrap/word-break (set on every td) from splitting "Restore". */
.section-heading[data-section-key="recently-deleted"] ~ table td.actions,
.section-heading[data-section-key="recently-deleted-projects"] ~ table td.actions {
  width: 1%;
  white-space: nowrap;
}
@media (max-width: 600px) {
  /* Match the responsive-table pattern: hide the secondary "Deleted by" /
     "Deleted at" columns. These plain tables lack data-category, so the
     existing col-* hide rules don't reach them — scope by section key. */
  .section-heading[data-section-key="recently-deleted"] ~ table .col-uploader,
  .section-heading[data-section-key="recently-deleted"] ~ table .col-uploaded,
  .section-heading[data-section-key="recently-deleted-projects"] ~ table .col-uploaded {
    display: none;
  }
}

/* ---------- Document multi-select + bulk actions (projects/view.php) ---------- */
/* Round custom checkboxes for the per-row / per-category / bulk selectors. */
.doc-select,
.doc-select-all,
.doc-bulk-selectall {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 20px;
  height: 20px;
  margin: 0;
  border: 2px solid var(--border);
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
  vertical-align: middle;
  flex-shrink: 0;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.doc-select:checked,
.doc-select-all:checked,
.doc-bulk-selectall:checked,
.doc-select-all:indeterminate,
.doc-bulk-selectall:indeterminate {
  background: var(--accent);
  border-color: var(--accent);
}
/* White tick on :checked (drawn with a rotated ::after box). */
.doc-select:checked::after,
.doc-select-all:checked::after,
.doc-bulk-selectall:checked::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 6px;
  width: 5px;
  height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* White horizontal dash on :indeterminate (partial category selection). */
.doc-select-all:indeterminate::after,
.doc-bulk-selectall:indeterminate::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 9px;
  height: 2px;
  background: #fff;
  transform: translate(-50%, -50%);
}
.doc-select:focus-visible,
.doc-select-all:focus-visible,
.doc-bulk-selectall:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .doc-select,
  .doc-select-all,
  .doc-bulk-selectall {
    transition: none;
  }
}

/* Narrow, centered leading checkbox column in document category tables. */
table[data-category] th:first-child,
table[data-category] td:first-child {
  width: 1%;
  white-space: nowrap;
  text-align: center;
}

/* Fixed bulk-action bar revealed by JS (toggles the [hidden] attribute).
   Fixed (not sticky) so it pins to the viewport bottom even as the last child. */
.doc-bulk-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 24px;
  background: #fff;
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
/* Guard with :not([hidden]) so the JS-driven [hidden] attribute still hides it. */
.doc-bulk-bar:not([hidden]) {
  display: flex;
}
.doc-bulk-count {
  font-weight: 600;
  color: var(--text-soft);
}
.doc-bulk-bar .doc-bulk-download,
.doc-bulk-bar .doc-bulk-move,
.doc-bulk-bar .doc-bulk-delete {
  margin-left: auto;
}
/* Buttons render conditionally on permissions: only the first action button present
   keeps the auto margin, every later one sits beside it. */
.doc-bulk-bar .doc-bulk-download ~ .doc-bulk-move,
.doc-bulk-bar .doc-bulk-download ~ .doc-bulk-delete,
.doc-bulk-bar .doc-bulk-download ~ .doc-bulk-clear,
.doc-bulk-bar .doc-bulk-move ~ .doc-bulk-delete,
.doc-bulk-bar .doc-bulk-move ~ .doc-bulk-clear,
.doc-bulk-bar .doc-bulk-delete ~ .doc-bulk-clear {
  margin-left: 0;
}
@media (max-width: 600px) {
  .doc-bulk-bar {
    padding: 10px 12px;
  }
  .doc-bulk-bar .doc-bulk-download,
  .doc-bulk-bar .doc-bulk-move,
  .doc-bulk-bar .doc-bulk-delete,
  .doc-bulk-bar .doc-bulk-clear {
    flex: 1 1 auto;
    text-align: center;
  }
}

/* ---------- Billing alert (failed payment) ----------
   Rendered above the plan cards on plans.php and as the first child of
   .plan-usage-card on index.php. Deliberately amber, not red: the danger
   palette means "destructive/deletion" everywhere else in the app, so a
   lapsed card reuses the --warn-* / --invite-border tints instead. Body copy
   stays on --text for readability; the tone is carried by the surface, the
   title and the leading edge. */
.billing-alert {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  margin-block: 0 20px;
  background: var(--warn-bg);
  border: 1px solid var(--invite-border);
  border-inline-start: 4px solid var(--warn-text);
  border-radius: var(--radius-lg);
}
.billing-alert__body {
  flex: 1 1 300px;
  min-width: 0;
}
/* Heading weight without a heading element (the banner sits inside other
   sections, so it must not claim a slot in the document outline). */
.billing-alert__title {
  display: block;
  margin-bottom: 4px;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--warn-text);
}
.billing-alert__text {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text);
}
.billing-alert__action {
  flex: 0 0 auto;
  margin: 0;
}
/* Nested inside the plan-usage card: the card already supplies the outer
   padding and border, so the banner only needs to space itself off the
   header below it — matching .plan-usage-header's own 14px rhythm. */
.plan-usage-card .billing-alert {
  margin-bottom: 14px;
}
@media (max-width: 600px) {
  .billing-alert {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    padding: 12px 14px;
  }
  /* flex-basis resolves against the block axis once stacked — drop it, or the
     body would be forced to a 300px-tall box. */
  .billing-alert__body {
    flex: 0 1 auto;
  }
  .billing-alert__action .btn {
    width: 100%;
    text-align: center;
  }
}

/* ---------- Switched-account banner ----------
   First child of <main> on every page while working in someone else's
   account. Same amber surface as .billing-alert so it reads as "pay
   attention" without borrowing the danger palette. */
.account-switch-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 16px;
  padding: 10px 14px;
  margin-block: 0 16px;
  background: var(--warn-bg);
  border: 1px solid var(--invite-border);
  border-inline-start: 4px solid var(--warn-text);
  border-radius: var(--radius-md);
  color: var(--text);
  font-size: 0.9rem;
  line-height: 1.4;
}
.account-switch-banner__text {
  flex: 1 1 240px;
  min-width: 0;
  overflow-wrap: anywhere;
}
.account-switch-banner__text strong {
  color: var(--warn-text);
}
.account-switch-banner__form {
  flex: 0 0 auto;
  margin: 0;
}
.account-switch-banner__back {
  min-height: 32px;
}

/* ---------- Utility helpers (project-scoped) ----------
   Kept LAST on purpose: same 0,1,0 specificity as component classes, so source
   order is what lets a u-* utility override a component (e.g. .section-heading).
   Add every new u-* rule here, and no component rules below this block. */
.u-mt-0  { margin-top: 0; }
.u-mb-0  { margin-bottom: 0; }
.u-mb-6  { margin-bottom: 6px; }
.u-mb-18 { margin-bottom: 18px; }
