/* =============================================================================
 * components.css — rules shared by several views.
 *
 * These were duplicated verbatim inside per-view <style> blocks: the same
 * upload-row, repeatable-row and hours-grid CSS copied into as many as nine
 * form templates, re-sent on every page render and free to drift apart.
 *
 * Only rules that were byte-identical in every copy were moved here, and only
 * where the selector does not also appear in another stylesheet — so the
 * cascade resolves exactly as before. Rules that genuinely differ per view
 * (admin vs member .settings-tab) and anything with server-rendered values
 * (:root brand colours) deliberately stayed where they were.
 *
 * Loaded after panel.css/auth.css so it keeps the precedence the inline blocks
 * had over the base stylesheets.
 * ========================================================================== */
.vcard-upload-row {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
}

.vcard-upload-preview {
  display: inline-flex;
  padding: .35rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 8px;
}

.vcard-upload-preview img {
  max-height: 56px;
  max-width: 90px;
  display: block;
  border-radius: 4px;
}

/* Same treatment for the per-link thumbnail preview in the Link-in-Bio
   editors, slightly smaller: it sits inside a dense link row, not a form
   group. (The class shipped in the views long before it had a rule.) */
.linkbio-thumb-preview {
  display: inline-flex;
  padding: .35rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 8px;
}

.linkbio-thumb-preview img {
  max-height: 40px;
  max-width: 64px;
  display: block;
  border-radius: 4px;
}

.settings-section-head-like {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-4);
  margin: 1.5rem 0 0.75rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid color-mix(in srgb, var(--text-1) calc(.06 * 100%), transparent);
}

.biz-facility-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: .5rem .75rem;
}

.menu-section-name {
  font-weight: 700;
  font-size: .85rem;
  color: var(--gold-text, #C9A24B);
  margin: 1rem 0 .5rem;
}

.menu-item-name-input {
  flex: 1 1 160px;
}

.menu-item-price-input {
  width: 90px;
}

.menu-item-sort-input {
  width: 60px;
}

.menu-item-desc-input {
  flex: 1 1 100%;
  min-height: 32px;
}


.cv-section-row {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: .75rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 8px;
  margin-bottom: .5rem;
}

.cv-section-dates {
  display: flex;
  gap: 1rem;
  align-items: center;
  flex-wrap: wrap;
  font-size: .82rem;
  color: var(--text-3);
}

.cv-section-dates input[type="date"] {
  padding: .3rem .5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: 6px;
  color: var(--text-1);
}

.form-field-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .5rem;
  padding: .5rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 8px;
}

.form-field-label-input {
  flex: 1 1 160px;
}

.form-field-type-select {
  width: 140px;
}

.form-field-sort-input {
  width: 60px;
}

.form-field-options-input {
  flex: 1 1 100%;
  min-height: 32px;
}

.form-field-required {
  font-size: .8rem;
  white-space: nowrap;
}

.form-field-row input, .form-field-row select, .form-field-row textarea {
  padding: .35rem .5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: 6px;
  color: var(--text-1);
}

/* The advance note above a create button — it names the extras card that only
   exists once the item is saved. A field hint (`.form-group small`) is the
   right tone but needs a .form-group wrapper owning no control, and
   .form-caption is an uppercase heading, not a sentence. */
/* Boxed, not muted body text. As plain --text-3 fine print this read as a
   disclaimer and members missed it — which is the very reason the extra
   sections went unnoticed after creating an item. A gold tint plus a heavier
   left rule marks it as GUIDANCE, deliberately distinct from .alert-info
   (blue, standing explanatory panels) and from the success/error alerts, so a
   note about what comes next is never mistaken for something being wrong.
   Colours come from tokens, so both themes follow; --gold is theme-independent
   by design.
   `margin: 0` is preserved on purpose: .form-actions owns the 1.5rem gap below
   this note, so boxing it must not change the form's spacing. */
.form-note {
  margin: 0;
  padding: .7rem .85rem;
  border: 1px solid color-mix(in srgb, var(--gold) 28%, transparent);
  border-left-width: 3px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 8%, transparent);
  font-size: .85rem;
  line-height: 1.5;
  color: var(--text-1b);
}

.menu-item-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .5rem;
  padding: .5rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 8px;
}

.menu-item-avail {
  font-size: .8rem;
  white-space: nowrap;
}

.menu-item-row input, .menu-item-row textarea {
  padding: .35rem .5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: 6px;
  color: var(--text-1);
}

.menu-section-block {
  margin-bottom: 1rem;
}

.menu-item-section-input {
  width: 110px;
}

.menu-item-photo-preview {
  display: inline-flex;
  padding: .2rem;
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: 6px;
}

.menu-item-photo-preview img {
  max-height: 36px;
  max-width: 36px;
  display: block;
  border-radius: 4px;
  object-fit: cover;
}

.menu-item-photo-input {
  width: 150px;
}

.menu-item-photo-remove {
  font-size: .75rem;
  white-space: nowrap;
}

/* Business page gallery — the member editor's thumbnail grid; each image
   pairs with its own delete sub-form, so item lays out as a column. */
.biz-gallery-admin {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin-bottom: .75rem;
}

.biz-gallery-admin-item {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  align-items: center;
}

.biz-gallery-admin-item img {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border-1);
}

.qr-type-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}

/* The card body was left in both select.php views by the first hoist because
   only its :hover state was byte-identical at the time. Whitespace aside, the
   base rule is identical too — same two copies, same drift risk. */
.qr-type-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: 1.25rem;
  min-height: 11rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 12px;
  text-decoration: none;
  color: inherit;
  transition: border-color .15s, transform .15s;
}

a.qr-type-card:hover {
  border-color: var(--gold, #C9A24B);
  transform: translateY(-2px);
}

/* A type the plan does not include. Rendered as a <span>, so the :hover lift
   above (scoped to a.qr-type-card) never applies — the card cannot look
   clickable. Dimmed rather than hidden: the point is to show what an upgrade
   unlocks. */
.qr-type-card--locked {
  opacity: .55;
  cursor: not-allowed;
}

.qr-type-badge {
  position: absolute;   /* .qr-type-card is position:relative */
  top: .6rem;
  right: .6rem;
  padding: .1rem .45rem;
  border-radius: 999px;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--c-amber, #fde047);
  background: rgba(234,179,8,.12);
  border: 1px solid rgba(234,179,8,.28);
}

@media (max-width: 600px) {
  .qr-type-grid { grid-template-columns: 1fr 1fr; }
}

.qr-type-icon {
  font-size: 1.8rem;
  line-height: 1;
}

.qr-type-title {
  font-weight: 700;
  font-size: 1rem;
  color: var(--text-1);
}

.qr-type-desc {
  font-size: .82rem;
  color: var(--text-3);
  line-height: 1.4;
}

.vcard-welcome-logo {
  max-width: 140px;
  max-height: 60px;
  margin: 0 auto 1rem;
  display: block;
}

.vcard-name {
  font-size: 1.3rem;
  font-weight: 700;
  margin: 0 0 .15rem;
  color: var(--text-1);
  word-break: break-word;
  overflow-wrap: anywhere;
}

.vcard-role {
  font-size: .9rem;
  color: var(--text-3);
  margin: 0 0 .75rem;
  word-break: break-word;
  overflow-wrap: anywhere;
}

.vcard-summary {
  font-size: .85rem;
  color: var(--text-2);
  line-height: 1.5;
  margin: 0 0 1.25rem;
  word-break: break-word;
  overflow-wrap: anywhere;
}

.vcard-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  justify-content: center;
  margin-bottom: 1rem;
}

.vcard-btn {
  padding: .55rem 1rem;
  border-radius: 999px;
  font-size: .82rem;
  font-weight: 600;
  text-decoration: none;
  background: var(--surface-2);
  color: var(--text-1);
  border: 1px solid var(--border-1);
  max-width: 100%;
  word-break: break-word;
  overflow-wrap: anywhere;
}

.vcard-btn--primary {
  background: var(--vc-primary, var(--gold, #C9A24B));
  /* Derived from the background's luminance (see on_color() in Helpers.php),
     NOT the owner's secondary colour. Two independently chosen brand colours
     have no reason to contrast: the seeded demo data alone produced amber on
     teal at 2.55:1 and near-black on blue at 2.99:1, both below the 4.5:1
     minimum, and any owner could pick worse. */
  color: var(--vc-on-primary, var(--ink, #0A0F16));
  border-color: transparent;
}

.vcard-socials {
  display: flex;
  gap: .75rem;
  justify-content: center;
  font-size: 1.3rem;
  margin-bottom: 1rem;
}

.vcard-socials a {
  text-decoration: none;
  /* A 1.3rem glyph is a ~21px tap target; 2.75rem clears the 44px minimum for
     WCAG 2.5.8 without changing how big the icon itself looks. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  color: var(--text-1);
}

.vcard-socials a:hover,
.vcard-socials a:focus-visible {
  color: var(--vc-primary, var(--gold, #C9A24B));
  background: color-mix(in srgb, currentColor calc(.1 * 100%), transparent);
}

/* Inline SVG icons size from the surrounding font-size and take its colour, so
   one rule covers every context the emoji used to appear in. */
.icon {
  display: inline-block;
  vertical-align: -0.125em;
  flex-shrink: 0;
}

/* Emoji carry their own visual weight; a stroked outline at the same nominal
   size reads smaller, so the two icon contexts that replaced large glyphs get
   a modest bump to keep the original balance. */
/* Hosted-page cover image. Lived inside business.php's own <style> until WiFi
   gained a cover too; two views needing it makes it a shared component rather
   than page-local styling. */
.biz-cover {
  width: 100%;
  max-height: 180px;
  object-fit: cover;
  border-radius: 10px;
  margin-bottom: 1rem;
  display: block;
}

/* Share / Show-QR row. Secondary to the type's own action buttons above it,
   so it is centred and a little quieter rather than full-width. */
.hosted-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  justify-content: center;
  /* Without this the row's default `stretch` makes the Share button grow to
     the height of the expanded QR panel beside it — a very tall pill. */
  align-items: flex-start;
  margin: .25rem 0 1rem;
}

/* Opened, the QR takes a row of its own so it is centred under both buttons
   rather than squeezed alongside one of them. */
.hosted-qr[open] {
  flex-basis: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.hosted-action {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 2.75rem;      /* 44px tap target */
  cursor: pointer;
  font: inherit;
}

/* <details> renders a disclosure triangle that would sit oddly inside a
   button-shaped summary. */
.hosted-qr > summary { list-style: none; }
.hosted-qr > summary::-webkit-details-marker { display: none; }

.hosted-qr-panel {
  display: flex;
  justify-content: center;
  padding: .85rem 0 .25rem;
}

.hosted-qr-panel img {
  background: #fff;         /* a QR must stay dark-on-light to scan */
  padding: .6rem;
  border-radius: 10px;
  max-width: 100%;
  height: auto;
}

.icon--type  { width: 1.15em; height: 1.15em; color: var(--gold, #C9A24B); }
.icon--chip  { width: 1.2em;  height: 1.2em; }
.icon--social { width: 1.15em; height: 1.15em; }

.vcard-address {
  font-size: .8rem;
  color: var(--text-3);
  word-break: break-word;
  overflow-wrap: anywhere;
}

.vcard-photo {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  margin: 0 auto .75rem;
  display: block;
  border: 2px solid var(--vc-primary, var(--gold, #C9A24B));
}

.vcard-photo--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 2rem;
  font-weight: 700;
}

.qr-capped-wrap {
  max-width: 420px;
  margin: 3rem auto;
  text-align: center;
  padding: 2rem 1.5rem;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: 12px;
}

.qr-capped-icon {
  font-size: 2.4rem;
  margin-bottom: .75rem;
}

.qr-capped-title {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text-1);
  margin: 0 0 .5rem;
}

.qr-capped-text {
  color: var(--text-2);
  font-size: .9rem;
  line-height: 1.5;
  margin: 0;
}


/* ---- State classes ----
   These replace conditional inline styles. Each was a `style="…"` emitted from
   a PHP ternary, which a static utility class could not express and CSP no
   longer allows. A named state reads better than the declaration did anyway. */

/* !important, like the inline styles these replace — and unlike
   .enabled-lang-option above, none of these is toggled by script, so there is
   no classList behaviour for an !important rule to override. Without it they
   would lose to more specific table rules (e.g. `.data-table td { color: … }`)
   that the inline attribute used to beat. */

/* A revoked API key row — dimmed, not hidden. */
.is-revoked { opacity: .5 !important; }

/* An unread hosted-page message. */
.is-unread { font-weight: 600 !important; }

/* Wallet ledger sign. */
.is-credit { color: var(--c-green, #86efac) !important; }
.is-debit  { color: var(--c-red, #fca5a5) !important; }

/* Theme toggle under the 'auto' platform default.
 *
 * Both destinations are in the markup (see _partials/theme_switcher.php) and
 * exactly one is shown, keyed off the [data-theme] the head script resolved
 * from prefers-color-scheme. !important because the wrapper classes
 * (.theme-switcher / .auth-theme) set display themselves.
 *
 * The dark case is the default rather than a [data-theme="dark"] rule so the
 * control still shows something sane in the no-JS case, where data-theme is
 * left at "auto" and the dark palette is what actually renders. */
.theme-toggle--to-dark  { display: none !important; }
.theme-toggle--to-light { display: inline-flex !important; }

:root[data-theme="light"] .theme-toggle--to-dark  { display: inline-flex !important; }
:root[data-theme="light"] .theme-toggle--to-light { display: none !important; }

/* The per-language checkbox rows in the hosted-page locale picker.
 *
 * Deliberately NOT an !important utility: the picker's script hides and shows
 * these by toggling .d-none (which IS !important), and two !important rules
 * would be resolved by source order — components.css loads last, so it would
 * win and .d-none would stop working.
 *
 * Scoped through the grid instead, to outrank `.form-group label { display:
 * block }` in panel.css. A bare `.enabled-lang-option` (0,1,0) loses to that
 * (0,1,1), which the inline style it replaced did not — the options rendered
 * as blocks. */
.enabled-langs-grid .enabled-lang-option {
  display: flex;
  align-items: center;
  gap: .35rem;
  font-weight: normal;
}

/* ---------------------------------------------------------------------------
   QR code with download controls  (_partials/qr_download.php)

   Lives here, not in panel.css, because the partial is used on PUBLIC hosted
   pages as well as in both panels — and public layouts load components.css but
   not panel.css. Styling it there left the controls as unstyled bare text on
   every hosted page.
   --------------------------------------------------------------------------- */
.qr-download          { display:flex; flex-direction:column; align-items:center; gap:.4rem; margin-top:1rem; }

/* The code keeps a white plate of its own. A QR drawn straight onto a dark
   surface has no quiet zone for a scanner to lock onto, and a "transparent"
   background rasterises to whatever is behind it. */
.qr-download-img      { background:#fff; padding:.5rem; border-radius:6px; display:block; }

.qr-download-actions  { display:flex; flex-wrap:wrap; gap:.4rem; align-items:center; justify-content:center; }

/* The buttons themselves: panel.css styles .btn-action, public pages have no
   such rule, so the block carries its own so it looks the same in both. */
.qr-download-actions .btn-action,
.qr-download-actions a.btn-action {
  display:inline-flex; align-items:center; gap:.3rem;
  padding:.3rem .7rem; font-size:.8rem; line-height:1.6;
  border:1px solid var(--border-2, #1e293b); border-radius:6px;
  background:var(--surface-2, #111827); color:var(--text-1, #e2e8f0);
  text-decoration:none; cursor:pointer;
}
.qr-download-actions .btn-action:hover:not([disabled]) { border-color:var(--gold, #d4af37); color:var(--gold-text, #d4af37); }
.qr-download-actions .btn-action[disabled]             { opacity:.55; cursor:not-allowed; }

/* ---------------------------------------------------------------------------
   Plan feature rows  (_partials/plan_feature_rows.php)

   Here, not in landing.css, because the SAME partial renders on the public
   pricing card and inside the member panel's plan accordion — and panel
   layouts load components.css but never landing.css. Moved verbatim so the
   two renderings cannot drift.
   --------------------------------------------------------------------------- */
/* Unified plan-card row groups — every group (classic links, boolean
   platform features, and every content type) uses the SAME icon+label+value
   row shape, so the card reads consistently top to bottom instead of mixing
   a bare checklist with icon-led rows. */
/* ONE grid for the whole card — group titles and rows are siblings inside it.
   Previously each group was its own grid (.plan-group > ul.plan-rows), which
   sized its columns independently and, more importantly, left every card's rows
   in a separate grid: nothing could line row N up ACROSS the three cards. A
   single flat grid per card is what makes shared row tracks possible.
   Rows are subgrids of it, so the value columns still line up DOWN the card. */
/* Column sizing is the whole ballgame here, and the two obvious spellings both
   fail:
     `… 1fr auto auto`        — a bare auto track takes its max-content BEFORE
        1fr gets anything, so a long translated value squeezed the label to
        ~55px against 103px of text and it printed on top of the value.
     `… minmax(0,1fr) …`      — lets the label collapse to ZERO, and with
        overflow-wrap it then breaks one character per line: 129px-tall labels
        and 10,000px-tall cards in Russian.
   So the label gets a FLOOR it cannot be squeezed below, and the value tracks
   get a floor of 0 so they yield to it. Where there is room (English) the
   values still take their natural width and nothing moves; where there is not
   (Russian, German, Greek) the values wrap instead of crushing the label. */
.plan-features {
  /* margin-bottom is the breathing room between the LAST feature row and the
     plan button: the card grid zeroes row-gap, so without it the two touch.
     A margin on the ul contributes to its last (shared) subgrid track, so
     all three cards keep identical row heights and button alignment. */
  list-style: none; margin: 0 0 1.5rem; padding: 0;
  display: grid;
  /* The label gets a share of the CARD (38%), not of its longest word:
     Vietnamese words are short, so a min-content floor left the label ~1
     word wide and every label stacked one word per line. The value
     columns get a min-content floor so a value is never narrower than its
     own longest word — without it "απεριόριστο" printed as "απεριόριστ/ο".
     Measured over 16 languages: word splits 113 -> 27 (the rest are genuine
     over-long compounds), and the cards got SHORTER, not taller.

     FLAT 38%, not minmax(38%, 1fr). As an fr track the label also absorbed
     whatever slack its own card happened to have, so in languages compact
     enough to leave any — English, Hindi, Japanese, Korean, Chinese — the
     three cards ended up with label columns of different widths (Chinese:
     127 / 110 / 154px) and the column edges no longer lined up across the
     table. Row parity was never affected, but the vertical edges were. Flat
     hands that slack to the value tracks instead, which are right-aligned
     and so absorb it invisibly. Cost of pinning it: nothing measurable —
     same wrapped-value count, same three genuinely over-long compounds
     broken (de Unternehmensseite / Weiterleitungsgeschwindigkeit, ru
     Скорость перенаправления), and four FEWER wrapped labels.

     38 is the ceiling, not a preference: at 40% the widest cards start
     overflowing their own width in one language.

     The var makes the flat share ADAPTIVE where that is free: languages
     whose labels need less than the cap (English needs 32%) were paying for
     Vietnamese's 38% with wrapped values ("5,000 views/mo" over two lines
     beside a label using two thirds of its column). app.js measures the
     widest non-spanning label once per load and sets
     --plan-label-col: min(<need>px, 38%) on the pricing grid, so a compact
     language shrinks the column to what it actually uses and the values get
     the difference; a language over the cap clamps to exactly this
     fallback. One measured value styles all three cards, which is what
     keeps the column identical across them. Pure CSS cannot express this —
     fit-content(38%) lets the value columns starve the label to nothing
     (Italian: 10px) because its auto minimum is the item's, not the
     content's. Without JavaScript the fallback below applies and the table
     renders exactly as before. */
  /* WHOLE-CARD SOLVE (second stage, same script): when every card can render
     every cell UNWRAPPED in the width it has, app.js sets explicit pixel
     tracks — the shared label var plus per-card --plan-col-a/--plan-col-b —
     and nothing wraps at all. The fallbacks below ARE the previous layout,
     so with JavaScript off, or whenever any card cannot fit unwrapped, the
     table renders exactly as before (the label-only adaptive still runs as
     its own fallback stage). Value columns are per-card by design; only the
     label column must agree across cards, and it is one shared value. */
  grid-template-columns: 1.1em var(--plan-label-col, 38%) var(--plan-col-a, minmax(min-content, auto)) var(--plan-col-b, minmax(min-content, auto));
  column-gap: .5rem;
}
.plan-group-title {
  grid-column: 1 / -1;           /* a heading spans the whole row */
  font-size: .7rem; text-transform: uppercase; letter-spacing: .07em; font-weight: 700;
  color: var(--text-4, #64748b); margin-bottom: .4rem; padding-bottom: .3rem;
  border-bottom: 1px solid color-mix(in srgb, var(--text-1) calc(.08 * 100%), transparent);
}
/* Separates the groups now that .plan-group's margin is gone. Not applied to
   the first title, which sits directly under the price block. */
.plan-group-title + * ~ .plan-group-title,
.plan-features > .plan-group-title:not(:first-child) { margin-top: 1.1rem; }
.plan-row {
  display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
  align-items: baseline; font-size: .84rem;
  color: var(--text-1b, #cbd5e1); padding: .3rem 0;
}
/* A sub-feature of the row above it — Menu ordering under Menu. Indented and a
   shade quieter so it reads as PART of that QR type rather than as a type of
   its own, which is exactly how it read while it sat at the end of the list.
   The indent is on the icon cell, not a margin on the row: the row is a subgrid
   and a margin would knock its columns out of alignment with every other card. */
.plan-row--sub .plan-row-label {
  padding-left: .9em;
  font-size: .95em;
  color: var(--text-2, #94a3b8);
}
/* The icon is dimmed rather than moved: it sits in a fixed 1.1em subgrid cell,
   so shifting it would push it into the label's column and knock this row out
   of alignment with every other card. The indent goes on the label. */
.plan-row--sub .plan-row-icon { opacity: .75; }

.plan-row-icon { width: 1.1em; text-align: center; font-size: .95em; }
/* break-word, NOT anywhere — the difference decides whether words survive.
   `anywhere` also drops the column's min-content contribution to a single
   CHARACTER, so the track collapses and Greek renders as "ανακατεύθυν / σης":
   78 chopped labels across the 16 languages. `break-word` leaves min-content at
   the longest word, which is what the track floor above is pinned to, so words
   break only when one genuinely cannot fit — measured at zero in 13 of 16
   languages, the exceptions being Japanese and Chinese, which have no spaces
   and are SUPPOSED to wrap between characters. */
.plan-row-label { min-width: 0; overflow-wrap: break-word; }
/* A row with a single value (or a ✓/—) spans both value columns; only the
   content rows split into two. */
.plan-row > :last-child:not(.plan-row-value--b) { grid-column: 3 / -1; }
.plan-row-value--b { grid-column: 4; }
.plan-row-value--wide { grid-column: 3 / -1; }
/* ...except a TICK row, whose value is ~12px of glyph sitting in ~135px of
   track while the label beside it wraps for want of exactly that space. Hand
   the first value column to the label and leave the tick alone in the last
   one: it still right-aligns on the card edge, so nothing moves except the
   label gaining room.

   Keyed off the row carrying a tick rather than a value, which is what keeps
   it from misfiring: a row with a real value keeps both columns, because
   "30-day analytics history" does not fit in the last one alone.

   Measured over the 16 languages at 1358px and 390px: tick labels wrapping
   338 -> 123 (English 16 -> 0), word splits 27 -> 18, and the tallest card
   107px SHORTER, with overlap, row drift and horizontal scroll all still
   zero. Letting the label span all three columns instead wraps fewer labels
   still, but it then contributes its own width to the value tracks and
   pushes the CONTENT rows into wrapping: the English card grows to 1619px,
   taller than leaving it alone.

   :has() introduces no new browser floor here — this layout already requires
   subgrid, which is a year newer. Both rules must stay AFTER the span rule
   above: they match at equal specificity and win on order alone. */
.plan-row:has(> .plan-row-check) .plan-row-label { grid-column: 2 / 4; }
.plan-row > .plan-row-check:last-child { grid-column: 4; }
/* In SOLVED mode (app.js proved every cell fits unwrapped and set explicit
   tracks) a tick-row label may run under the tick's own column too — the
   space in front of a ✓ is empty in that row, and confining a long label to
   columns 2-3 was what made wide-but-wordy languages (Vietnamese) fail the
   fit test while visibly swimming in room. The solver reserves the tick's
   width via --plan-tick-clear, so label text can never reach the glyph.
   Grid items may share a cell; the tick keeps its right-aligned column 4. */
.plan-solved .plan-row:has(> .plan-row-check) .plan-row-label {
  grid-column: 2 / -1;
  padding-right: var(--plan-tick-clear, 2rem);
  grid-row: 1;
}
/* Explicit row so the tick SHARES the line with the spanning label —
   auto-placement would dodge the occupied columns into a second implicit
   row and drop every ✓ under its label. Overlap is intended; the label's
   clearance padding is what keeps the text off the glyph. */
.plan-solved .plan-row > .plan-row-check:last-child { grid-row: 1; }
/* Same reclaim for the handful of rows whose value is always a short TOKEN
   rather than a phrase — location detail, API rate limit, page branding. The
   partial marks those with --tight; it is a property of the string, not of
   the layout, which is why the judgement lives there and not in a selector
   here. Deliberately NOT applied to analytics history or redirect speed:
   "30-day analytics history" needs both columns and would wrap in one.
   Measured over the 16 languages: labels wrapping on non-tick rows 142 -> 90
   and the tallest card 43px shorter, against 16 of these tokens now wrapping
   where a long translation genuinely does not fit one column. */
.plan-row > .plan-row-value--tight:last-child { grid-column: 4; }
.plan-row:has(> .plan-row-value--tight) .plan-row-label { grid-column: 2 / 4; }
.plan-row-value {
  /* Was flex: 0 0 auto + white-space: nowrap — fine for short English
     values ("up to 10", "unlimited"), but a longer translation (e.g. Greek
     "Πλήρες ιστορικό στατιστικών στοιχεία" for "Full analytics history")
     would refuse to wrap and visually overlap the label instead. flex-shrink
     lets the value cell itself narrow and wrap onto 2 lines WITHIN the same
     visual row (icon/label/value stay side by side) rather than the whole
     cell jumping to a new line below — the short ✓/— check column
     (.plan-row-check) is a separate element and is unaffected, so boolean
     rows still render on one tidy line regardless of label length. */
  font-size: .78rem; font-weight: 600; color: var(--text-2, #94a3b8);
  text-align: right; min-width: 0; overflow-wrap: break-word;
}
/* Right-aligned like every other value. Under the old flex layout the check
   was a shrink-to-fit item at the end of the row and landed on the right edge
   by construction; as a grid cell spanning the two value columns it fills the
   span, so without this it sits at the SPAN's left edge and the ticks no
   longer line up with the text values above and below them. */
.plan-row-check { font-weight: 700; color: var(--gold-text, #C9A24B); text-align: right; }
.plan-row--off { color: var(--text-4, #475569); }
.plan-row--off .plan-row-icon { opacity: .4; filter: grayscale(1); }
.plan-row--off .plan-row-value { color: var(--text-4, #475569); }
.plan-row--off .plan-row-check { content: '—'; color: var(--text-4, #334155); }

/* Password reveal (hold-to-show) — app.js wraps each password input in a
   .pw-field span and appends the .pw-reveal button. The input's right padding
   is set inline by the script (so it wins over every per-layout input rule);
   only the wrapper + button are styled here. Loaded by every layout that has a
   password field (auth, panel, install, public, landing). */
.pw-field { position: relative; display: block; }
.pw-reveal {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-4, #64748b);
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;               /* hold-to-reveal must not scroll on touch */
  transition: color 0.15s;
}
.pw-reveal:hover,
.pw-reveal.is-revealing { color: var(--text-2, #94a3b8); }
.pw-reveal:focus-visible { outline: 2px solid var(--gold, #C9A24B); outline-offset: -2px; border-radius: 6px; }
.pw-reveal svg { display: block; width: 1.15rem; height: 1.15rem; }

/* ---------------------------------------------------------------------------
   Shared theme + language controls, and the mobile floating bar.

   Moved here from landing.css when the PANEL needed the same bar: the panel
   does not load landing.css, and a second copy of the geometry (25px, the
   --pad-x insets, the pointer-events pair) is exactly how two surfaces drift
   apart. components.css is loaded by both.

   The bar is fixed to the VIEWPORT, which is only true while its markup sits
   outside any ancestor that establishes a containing block for fixed children
   — the landing header carries backdrop-filter, so the same markup moved
   inside it would pin to the header and look almost right.

   It spans the full width to place its two ends, so it MUST NOT swallow taps
   aimed at the page beneath: pointer-events is disabled on the bar and
   re-enabled on the controls. z-index sits above the sticky header (30) and
   below the cookie notice (40).
   --------------------------------------------------------------------------- */
:root { --ctl-h: 40px; }

/* --pad-x is landing's page inset; the panel does not define it, so the
   fallback IS the panel's value — 5vw, matching the spec. */
.site-theme.theme-toggle-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h); height: var(--ctl-h); border-radius: 8px;
  color: var(--text-2, #94a3b8); text-decoration: none;
  border: 1px solid color-mix(in srgb, var(--text-1) calc(.12 * 100%), transparent);
  background: color-mix(in srgb, var(--text-1) calc(.04 * 100%), transparent);
  transition: color .12s, border-color .12s;
}
.site-theme.theme-toggle-btn:hover { color: var(--gold-text, #C9A24B); border-color: var(--gold, #C9A24B); }
.site-lang { display: inline-flex; }
.site-lang .lang-select {
  appearance: auto;
  height: var(--ctl-h);
  padding: 0 .5rem; border-radius: 8px; font-size: .82rem; font-family: inherit;
  color: var(--text-1, #f1f5f9);
  background: color-mix(in srgb, var(--text-1) calc(.04 * 100%), transparent);
  border: 1px solid color-mix(in srgb, var(--text-1) calc(.12 * 100%), transparent);
  cursor: pointer; max-width: 150px;
}
.site-lang .lang-select:focus { outline: none; border-color: var(--gold, #C9A24B); }

.site-float-switchers {
  position: fixed; z-index: 35;
  left: var(--pad-x, 5vw); right: var(--pad-x, 5vw);
  /* 25px above the home indicator, not 25px above the screen edge. The
     fallback keeps the original offset everywhere the inset is 0 or the
     browser does not support env(). */
  bottom: calc(25px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: space-between;
  pointer-events: none;
}
.site-float-switchers > * { pointer-events: auto; }
@media (min-width: 768px) { .site-float-switchers { display: none; } }

/* The admin use-case list shows an uploaded icon where the emoji would be. */
.usecase-row-img { width: 28px; height: 28px; object-fit: contain; display: block; border-radius: 6px; }

/* ---- Floating status bar ----
   The flash used to render at the top of <main>, which meant it scrolled away
   with the document. After creating an item the app redirects to the editor
   with an #extras fragment, and that jump moved the confirmation off screen —
   so the one message telling members where their new sections are was the one
   message they never saw. Fixed to the VIEWPORT, the document moves underneath
   it and the message stays put.

   Lives here, not panel.css: three layouts render it (panel, auth, install) and
   only one of them loads panel.css — the same reasoning that keeps
   .site-float-switchers in this file, which verify_zip enforces.

   The LEVEL colours are deliberately NOT redefined here. .flash-ok/-err/-warn
   (panel.css) and .msg.ok/.err/.warn (auth.css) still own them, because those
   colours are asserted to live in those exact files. This block is geometry
   and chrome only.

   z-index 45 is chosen, not arbitrary: above .panel-footer (40) and the mobile
   switcher bar (35), below the combobox/user-menu dropdowns (60) so an open
   menu is never covered by a notice. */

/* Making the bar fixed solved half the problem and created the other half: the
   #extras jump lands the card's top edge at the top of the VIEWPORT, which is
   exactly where the bar now floats — so the confirmation stayed visible but
   covered the heading and first control of the very section it points at.
   scroll-margin-top pushes the browser's fragment target down by the bar's
   height, so both are readable at once.

   This must be CSS, not JS: app.js is defer-loaded and the fragment jump has
   already happened by the time it runs — the same constraint that forces the
   bar itself to be server-rendered.

   Sized from the bar, not guessed: .flash-bar sits at top: 1rem, and a
   .flash-bar__item is .8rem padding top and bottom around .875rem/1.5 text —
   2.9rem at one line, 4.2rem at two, 5.5rem at three. 6rem clears the common
   one-and-two-line case; the narrow-screen bump below covers three, because the
   same sentence wraps further at 375px. Values verified by measuring the
   rendered bar and card in a browser rather than by arithmetic alone. */
#extras { scroll-margin-top: 6rem; }

.flash-bar {
  position: fixed;
  top: 1rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 45;
  width: min(560px, calc(100vw - 2rem));
  display: flex;
  flex-direction: column;
  gap: .5rem;
  /* The container spans the width but must not swallow clicks around the
     message; only the bar itself is interactive. */
  pointer-events: none;
}

.flash-bar > * { pointer-events: auto; }

/* OPAQUE, and composed rather than duplicated. The level classes
   (.flash-ok/-err/-warn, .msg.ok/.warn/.err) tint with ~10% alpha, which is
   right for an element sitting inline ON the page background — but this one
   FLOATS over content, and at 10% the page showed straight through it. Mixing
   the level's own colour (currentColor, which those classes set) into an opaque
   surface keeps the exact same hue per level while making the panel solid, and
   still without restating a single colour here.
   Specificity 0,2,0 so it wins against the level classes' plain `background`. */
.flash-bar .flash-bar__item {
  background: color-mix(in srgb, currentColor 12%, var(--surface-1, #0f172a));
}

.flash-bar__item {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: flex-start;
  gap: .75rem;
  padding: .8rem 1rem;
  border-radius: 10px;
  font-size: .875rem;
  line-height: 1.5;
  /* Reads above the page rather than floating on it. */
  box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
  animation: flash-bar-in .18s ease-out;
}

@keyframes flash-bar-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.flash-bar__msg {
  flex: 1 1 auto;
  min-width: 0;
  /* A gateway error or a bank reference must WRAP, never be clipped: for some
     messages the text is the only copy the member will ever get. */
  overflow-wrap: anywhere;
}

.flash-bar__close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin: -.15rem -.35rem 0 0;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 6px;
  color: inherit;
  opacity: .7;
  cursor: pointer;
  transition: opacity .12s, background .12s;
}

.flash-bar__close:hover,
.flash-bar__close:focus-visible {
  opacity: 1;
  background: color-mix(in srgb, currentColor 14%, transparent);
}

/* The auto-dismiss countdown, read as a NUMBER: seconds left, beside the close
   button. The draining line below gives the same information at a glance; this
   gives it exactly, which is what "shown for 30 seconds" actually promises.

   Hidden until app.js adds `is-on`. With JS off nothing auto-dismisses, and a
   number sitting there not moving would be a lie the markup told on its own.

   No colour of its own: currentColor is the LEVEL colour set by .flash-ok /
   .msg.ok, so the badge tints itself per level exactly like the close button's
   hover does, and nothing here restates a level colour.

   A bare integer, never a unit or a word — the same glyphs in all sixteen
   languages, so it needs no translation key. tabular-nums keeps 30 and 9 the
   same width, or the close button would shuffle sideways every tick. */
.flash-bar__count { display: none; }

.flash-bar__count.is-on {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  height: 26px;
  margin-top: -.15rem;
  padding: 0 .3rem;
  border-radius: 6px;
  background: color-mix(in srgb, currentColor 14%, transparent);
  color: inherit;
  opacity: .8;
  font-size: .75rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  white-space: nowrap;
  /* Dragging to select the message must not pick up a number that is about to
     change under the cursor. Prefixed like the other user-select in this file:
     Safari needed it until 16. */
  -webkit-user-select: none;
  user-select: none;
}

/* The auto-dismiss countdown: a line that drains along the bottom edge. Its
   width is driven from JS (see app.js), NOT by a CSS animation — the global
   prefers-reduced-motion rule crushes animation-duration to 0.001ms, which
   would empty the line instantly while the real timeout still ran, so the
   indicator would be lying. aria-hidden in the markup: a per-second update
   inside the live region would make a screen reader re-announce the message
   every tick. */
.flash-bar__timer {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  background: currentColor;
  opacity: .35;
  transform-origin: left center;
}

@media (max-width: 767px) {
  /* Clear the sticky mobile header (z-30) instead of covering the burger. */
  .flash-bar {
    top: calc(var(--nav-ctl-h, 38px) + 1.5rem);
    width: calc(100vw - 1.5rem);
  }

  /* SC 2.4.11, the two failures no scroll property could reach. A fixed bar
     covering a control near the TOP of the document cannot be scrolled clear:
     revealing it would mean scrolling above the start of the page. Measured on
     /app/stats and /app/support at 390px — the page's primary action button sat
     at y=93..125 under a bar occupying y=62..126, with scrollY already 0.
     Errors persist until dismissed here (see layouts/panel.php), so it is not a
     toast that clears itself.

     Sticky instead of fixed makes the bar take its own space in the flow and
     push the content down, which is also the better answer on the screen with
     the least room. Scoped to the PANEL body: the auth and install layouts
     render this bar inside a centred card where sticky has no scroll container
     to work against, and panel.css cannot hold the rule because .flash-bar is
     required to live only in this sheet. */
  body.panel-body .flash-bar {
    position: sticky;
    top: calc(var(--nav-ctl-h, 38px) + .5rem);
    left: auto;
    transform: none;
    width: auto;
    margin: .5rem .75rem 0;
  }
  /* The bar starts lower here (it clears the sticky header) and the same
     sentence wraps to more lines in a narrower column, so the fragment target
     needs to drop further than the 6rem the wide layout uses. Expressed against
     the same --nav-ctl-h the bar itself uses, so the two cannot drift. */
  #extras { scroll-margin-top: calc(var(--nav-ctl-h, 38px) + 7rem); }
  /* Touch target parity with the rest of the mobile UI. */
  .flash-bar__close { width: 44px; height: 44px; margin: -.5rem -.6rem 0 0; }
  /* The count is read, not tapped, so it does NOT grow to the 44px touch
     target — a badge that size would read as a second button. It keeps its
     26px and re-centres against the taller close button instead: that button
     starts .5rem above the row and is 44px tall, so its middle sits at
     22px - .5rem, and a 26px badge centres 13px above that. At 375px the row
     is message + number + button; the number holds its 1.75rem and the
     message takes what is left. */
  .flash-bar__count.is-on { margin-top: calc(22px - .5rem - 13px); }
}

/* A floating overlay has no business on paper. panel.php and public.php load
   print.css, but auth.php and install.php never do — so the rule is repeated
   there; see auth.css. */
@media print {
  .flash-bar { display: none !important; }
}

/* ---- Checkbox rows ----
   Lives here, not in panel.css, because the panel is not the only user: the
   registration consent checkbox (auth layout) and the installer's language
   picker (install layout) both use it, and neither layout loads panel.css.
   They were rendering with the browser's default blue checkbox on the first
   branded screen a customer ever sees. components.css is loaded by all five
   layouts, which is what makes this the shared home — the same reasoning that
   put .site-float-switchers here rather than duplicating it per sheet. */
.check-label { display: flex; align-items: center; gap: .5rem; cursor: pointer; font-size: .875rem; color: var(--text-1b, #cbd5e1); margin-bottom: .35rem; }
/* 24px is the WCAG 2.2 SC 2.5.8 minimum, and this one declaration was 340
   of the 494 failures the AA sweep measured — every .check-label box on
   every surface. The radio selector is here because two of the failures
   were radios (admin/mail/campaigns.php) that this rule never matched,
   leaving them at the 13px UA default.
   Hit-area tricks do not work here: the criterion is measured on the
   input's own border box, so padding or a pseudo-element would still
   fail while looking fixed. */
/* SCOPED TO .check-label AT FIRST, AND THAT WAS THE BUG. The AA sweep measures
   the boxes it can reach, and every failure it found happened to be inside a
   .check-label — so fixing that selector cleared the sweep while leaving 17
   controls at the 13px UA default: the settings pages, checkout, the billing
   plan picker, the links editors, the hosted-page language radios and the
   vCard exchange consent. Sized boxes beside unsized ones on the same screen
   read as a rendering fault rather than a style.
   The selector is now UNSCOPED, so a checkbox is 24px wherever it appears and
   a new one cannot opt out by not knowing about a wrapper class. */
input[type="checkbox"],
input[type="radio"] { width: 24px; height: 24px; accent-color: var(--gold, #C9A24B); flex-shrink: 0; }

/* The allergen picker's sixteen checkboxes. Without a grid every .check-label
   is display:flex and they stack into a sixteen-row column — precisely what
   _partials/menu_allergens.php's docblock says the layout exists to avoid.
   border/padding zeroed because it is a <fieldset>, which carries both by UA
   default. */
/* 170px, not 150px: the checkbox above grew from 15px to 24px, and the
   label text has to keep the room it had in all sixteen languages. */
.menu-allergen-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: .25rem .75rem; border: 0; padding: 0; margin: 0; }
