/* ══════════════════════════════════════════════════════════════════════════
   LABBIE DASHBOARD — SHARED COMPONENT PRIMITIVES

   The parts of the Labbie dashboard design system that existed only as
   one-off, page-specific rules (a search input here, a modal input there, a
   table inside the Published Sites view) expressed once, as named components,
   so a second Labbie surface can use them instead of inventing its own.

   EVERY VALUE HERE IS THE DASHBOARD'S OWN. Radii come from --radius-md/lg/xl,
   ink from --text/--muted, hairlines from --line, surfaces from --card and
   --token-surface-secondary. Nothing new was designed: .lb-input is
   .proj-search / .wb-modal-input; .lb-table is the Published Sites table;
   .lb-notice is the dashboard's quiet grey note. Monochrome throughout, with
   #b91c1c — the palette's one red, already used by .wb-pass-err and
   .wb-modal-btn-danger — reserved for genuine error state.

   LOAD ORDER MATTERS. This file must come AFTER /assets/glass.css. glass.css
   re-weights .btn-dark / .btn-ghost with !important, so the size and shape
   modifiers below have to be later AND !important or a "make this button
   small" modifier silently loses to the global button DNA.

   Consumers:
     frontend/dashboard.html          the Labbie user dashboard
     coldcalling/public/index.html    Labbie AI Cold Calling

   The dashboard links this file so the system loads uniformly and its own
   views can adopt these names over time. Linking it changes nothing that
   renders on the dashboard today, but that is a property to CHECK rather than
   to assume: a selector here is safe only if it is new (lb-*), or a modifier
   whose class is absent from the dashboard, or scoped to .lb-console. Two
   rules were not — the layout DNA and the :disabled state of .btn-dark /
   .btn-ghost, classes the dashboard already uses — and both are scoped below.
   Before adding a rule for an unprefixed class, grep frontend/ for it.
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════
   BUTTONS — sizes, shapes and the two variants the base sheet lacked

   .btn-dark (primary) and .btn-ghost (secondary) already exist in
   labbie-dashboard-shell.css. These add the layout DNA every button needs
   (icon + label on one baseline) and the variants a data-heavy surface
   needs: small, icon-only, quiet, and destructive.
   ══════════════════════════════════════════════════════════════════════ */

/* The two variants below are NEW classes, so they carry the whole DNA. */
.btn-danger,
.btn-quiet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 400;
}

/* .btn-dark and .btn-ghost already exist and are already on the page in the
   user dashboard, where each instance sets its own layout inline where it
   needs to. Giving them flex centring GLOBALLY would restyle buttons that are
   rendering correctly today, so the icon+label DNA is scoped to a console that
   opts in with .lb-console on <body>. Verified: without this scope the user
   dashboard's .btn-dark/.btn-ghost pick up align-items, justify-content and
   gap they did not have. */
.lb-console .btn-dark,
.lb-console .btn-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
}

/* Destructive. Red as INK and HAIRLINE, never as a filled rectangle — the
   dashboard's own treatment (.wb-pass-err, .account-popover-item-danger). */
.btn-danger {
  background: var(--card, #fff);
  color: #b91c1c;
  border: 1px solid rgba(185, 28, 28, 0.28);
  border-radius: 999px;
  padding: 7px 16px;
  cursor: pointer;
  transition: background 140ms, border-color 140ms;
}
.btn-danger:hover { background: rgba(185, 28, 28, 0.05); border-color: rgba(185, 28, 28, 0.45); }

/* Quiet. A control that must be reachable but must not compete — close
   buttons, "cancel", secondary links inside a card head. */
.btn-quiet {
  background: none;
  border: 0;
  color: var(--muted, #6b7280);
  border-radius: 999px;
  padding: 7px 12px;
  cursor: pointer;
  transition: background 140ms, color 140ms;
}
.btn-quiet:hover { background: rgba(0, 0, 0, 0.05); color: var(--text, #111827); }

/* Sizes and shapes. !important because glass.css sets padding and font-size
   on .btn-dark / .btn-ghost with !important — see the load-order note above. */
.btn-sm   { padding: 6px 13px !important; font-size: 0.78rem !important; }
.btn-wide { width: 100%; }
.btn-icon {
  width: 34px; height: 34px;
  padding: 0 !important;
  border-radius: 999px;
  flex: none;
}

/* Disabled. SCOPED FOR .btn-dark / .btn-ghost, for the same reason their
   layout DNA is: both classes are already on the user dashboard, and both are
   already disabled at runtime there — #btnUpgradePro and #switchPopConfirm go
   disabled while their request is in flight ("Redirecting…", "Confirming…").
   Nothing dims them today, because glass.css has no :disabled rule at all, so
   an unscoped rule here would newly grey out buttons on a dashboard whose
   appearance is approved and must not change. Where the dashboard does want a
   dimmed disabled button it sets the opacity inline (the plan buttons), which
   keeps working either way.

   .btn-danger and .btn-quiet are new classes that exist nowhere but a console,
   so they carry the state unscoped.

   BUSY IS NOT DISABLED. setBusy() sets aria-busy AND the disabled property —
   deliberately, so a second submission cannot be issued while the first is in
   flight — so every rule below has to exclude the busy case. Without the
   :not(), a control that is working normally is drawn at the refusing
   control's opacity, its spinner faded along with it, and the user is told
   the button rejected them when it is in fact carrying out their request. */
.lb-console .btn-dark:disabled:not([aria-busy="true"]),
.lb-console .btn-ghost:disabled:not([aria-busy="true"]),
.btn-danger:disabled:not([aria-busy="true"]),
.btn-quiet:disabled:not([aria-busy="true"]) { opacity: 0.45; cursor: not-allowed; }
.lb-console .btn-dark:disabled:not([aria-busy="true"]):hover,
.lb-console .btn-ghost:disabled:not([aria-busy="true"]):hover,
.btn-danger:disabled:not([aria-busy="true"]):hover,
.btn-quiet:disabled:not([aria-busy="true"]):hover { opacity: 0.45 !important; background: inherit; }
/* A busy control still must not accept a click that the disabled property is
   already refusing — it just says so with the spinner rather than by fading.
   Buttons only: aria-busy is also set on non-interactive regions while they
   load, and those keep the ordinary cursor. */
.lb-console button[aria-busy="true"] { cursor: progress; }

/* Success — a brief confirmation, applied by script and removed on a timer
   (the product's setSuccess helper). SCOPED, like the states above: .btn-dark
   and .btn-ghost are on the user dashboard too, and although nothing there
   sets data-state today, an unscoped rule would be waiting to restyle it.

   Monochrome, because the palette has no green: the confirmation is carried
   by the button's own label changing ("Saved"), and this only settles the
   surface underneath it so the change reads as deliberate rather than as a
   flicker. */
.lb-console [data-state="success"],
.btn-danger[data-state="success"],
.btn-quiet[data-state="success"] {
  border-color: rgba(17, 24, 39, 0.45);
  background: var(--token-surface-secondary, #f9fafb);
  color: var(--text, #111827);
}
/* The upload target confirms the same way, on its own surface. */
.lb-upload[data-state="success"] {
  border-color: rgba(17, 24, 39, 0.45);
  border-style: solid;
  background: var(--card, #fff);
}

/* In-flight. The control is disabled at the same moment (see the product's
   setBusy helper), so this is the visible half of a state that is already
   enforced — never decoration over a control that is still clickable. */
[aria-busy="true"] .lb-spin,
.lb-spin {
  width: 13px; height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: lb-spin 620ms linear infinite;
  flex: none;
}
@keyframes lb-spin { to { transform: rotate(360deg); } }


/* ══════════════════════════════════════════════════════════════════════
   FORM CONTROLS
   Same geometry and focus treatment as .proj-search and .wb-modal-input.
   ══════════════════════════════════════════════════════════════════════ */

.lb-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.lb-fieldrow { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
.lb-label { font-size: 0.8rem; font-weight: 500; color: var(--muted, #6b7280); }
.lb-hint  { font-size: 0.78rem; color: var(--muted, #6b7280); line-height: 1.5; }
.lb-error { font-size: 0.78rem; color: #b91c1c; line-height: 1.5; }

.lb-input,
.lb-select,
.lb-textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text, #111827);
  background: var(--card, #fff);
  border: 1px solid var(--line, #e5e7eb);
  border-radius: var(--radius-md, 10px);
  outline: none;
  transition: border-color 160ms ease;
}
.lb-input:focus,
.lb-select:focus,
.lb-textarea:focus { border-color: rgba(17, 24, 39, 0.42); }
.lb-input::placeholder,
.lb-textarea::placeholder { color: var(--token-text-muted-2, #9ca3af); }
.lb-input:disabled,
.lb-select:disabled,
.lb-textarea:disabled { opacity: 0.55; cursor: not-allowed; }
.lb-input[aria-invalid="true"],
.lb-select[aria-invalid="true"] { border-color: rgba(185, 28, 28, 0.45); }

.lb-textarea { min-height: 96px; resize: vertical; line-height: 1.55; }

/* Native select, restyled rather than replaced: the platform's own listbox is
   better on every device than any div-based imitation. */
.lb-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  cursor: pointer;
}

/* Checkbox. Real <input>, visually hidden, with a monochrome box beside it —
   the tick is the dashboard's ink (#111827), never a system blue. */
/* The whole label is the target, and it carries the platform's 44px minimum:
   the box itself is 18px, which is a miss on a phone for a control that in
   this product gates a recording authorisation. A caller inside a dense table
   row, where 44px per row would double the table's height and the target is
   the row rather than the box, opts out with an inline min-height:auto. */
.lb-check { display: inline-flex; align-items: flex-start; gap: 10px; cursor: pointer; font-size: 0.88rem; color: var(--text, #111827); line-height: 1.55; min-height: 44px; align-content: center; flex-wrap: wrap; }
.lb-check input { position: absolute; opacity: 0; width: 0; height: 0; }
.lb-check__box {
  flex: 0 0 auto;
  width: 18px; height: 18px;
  margin-top: 1px;
  border: 1px solid rgba(17, 24, 39, 0.28);
  border-radius: 5px;
  background: var(--card, #fff);
  display: grid; place-items: center;
  transition: background 140ms, border-color 140ms;
}
.lb-check__box::after {
  content: '';
  width: 9px; height: 5px;
  border-left: 2px solid #fff;
  border-bottom: 2px solid #fff;
  transform: rotate(-45deg) scale(0.6);
  opacity: 0;
  transition: opacity 140ms, transform 140ms;
}
.lb-check:hover .lb-check__box { border-color: rgba(17, 24, 39, 0.5); }
.lb-check input:checked + .lb-check__box { background: var(--accent, #111827); border-color: var(--accent, #111827); }
.lb-check input:checked + .lb-check__box::after { opacity: 1; transform: rotate(-45deg) scale(1); }
.lb-check input:disabled + .lb-check__box { opacity: 0.45; }
.lb-check input:disabled ~ .lb-check__text { opacity: 0.55; }
.lb-check[aria-invalid="true"] .lb-check__box { border-color: rgba(185, 28, 28, 0.55); }

/* Segmented control — a two-or-three-way choice that is not worth a select. */
.lb-seg { display: inline-flex; padding: 3px; gap: 3px; background: var(--token-surface-secondary, #f9fafb); border: 1px solid var(--line, #e5e7eb); border-radius: 999px; }
.lb-seg__btn { padding: 6px 14px; border: 0; border-radius: 999px; background: none; color: var(--muted, #6b7280); font: inherit; font-size: 0.8rem; cursor: pointer; transition: background 140ms, color 140ms; }
.lb-seg__btn:hover { color: var(--text, #111827); }
.lb-seg__btn[aria-pressed="true"] { background: var(--card, #fff); color: var(--text, #111827); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); }


/* ══════════════════════════════════════════════════════════════════════
   NOTICE
   A quiet grey note inside the content flow. Deliberately monochrome: a
   notice that reports a real limitation has to be readable, not loud, and
   the Labbie palette has no amber. Only .lb-notice--error departs, and only
   into the palette's existing red.
   ══════════════════════════════════════════════════════════════════════ */

.lb-notice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: var(--radius-md, 10px);
  background: var(--token-surface-secondary, #f9fafb);
  color: var(--muted, #6b7280);
  font-size: 0.85rem;
  line-height: 1.55;
}
.lb-notice strong { color: var(--text, #111827); font-weight: 500; }
.lb-notice__icon { flex: none; margin-top: 1px; color: var(--token-text-muted-2, #9ca3af); }
.lb-notice--error { border-color: rgba(185, 28, 28, 0.28); background: rgba(185, 28, 28, 0.04); color: #b91c1c; }
.lb-notice--error strong { color: #991b1b; }
.lb-notice--error .lb-notice__icon { color: #b91c1c; }


/* ══════════════════════════════════════════════════════════════════════
   STATUS PILL AND DOT
   Non-interactive. Reports a state; never a control.
   ══════════════════════════════════════════════════════════════════════ */

.lb-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 999px;
  background: var(--card, #fff);
  color: var(--muted, #6b7280);
  font-size: 0.76rem;
  white-space: nowrap;
}
/* FOUR STATES, FOUR LOOKS. The client reports live / pending / offline /
   stale, and a reader must be able to tell them apart — "reconnecting" and
   "no live updates" mean different things, and stale figures must never be
   readable as current ones. Monochrome does the work that colour used to:
   fill weight, a halo, and an outline are three independent axes, so no hue
   is needed and none is used.

   Every modifier the client can emit MUST have a rule here. A missing one
   does not fail loudly — it falls back to the base grey and silently merges
   two states into one look, which is exactly the misreport this indicator
   exists to prevent. coldcalling-dashboard-freshness-check.js asserts that
   every class liveLook() returns is defined in this file. */
.lb-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--token-text-muted-2, #9ca3af); flex: none; }
.lb-dot--live    { background: #111827; box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.10); }
/* Reconnecting: a halo like live, but grey ink and breathing rather than
   settled — activity without the claim that anything is arriving. */
.lb-dot--pending {
  background: var(--muted, #6b7280);
  box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.06);
  animation: lb-dot-pulse 1.5s ease-in-out infinite;
}
@keyframes lb-dot-pulse { 50% { opacity: 0.35; } }
/* PRESENT / ABSENT, which is a different question from live / not live.
   A configured provider is a setting that EXISTS; it is not a connection that
   is working, and nothing here has contacted it. So it gets a definite dot
   without --live's halo, and it must not borrow --pending: that one pulses,
   and a provider has no in-between state to pulse about. Pair it with
   --offline for the absent case. */
.lb-dot--on { background: var(--text, #111827); }
.lb-dot--offline { background: var(--token-text-muted-2, #9ca3af); }
.lb-dot--stale   { background: transparent; border: 1.5px solid var(--token-text-muted-2, #9ca3af); }


/* ══════════════════════════════════════════════════════════════════════
   TABLE
   The Published Sites table, named. Scrolls inside its own box so a wide
   table never makes the PAGE scroll sideways.
   ══════════════════════════════════════════════════════════════════════ */

.lb-tablewrap {
  overflow-x: auto;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: var(--radius-lg, 14px);
  background: var(--card, #fff);
  -webkit-overflow-scrolling: touch;
}
.lb-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.lb-table th {
  text-align: left;
  padding: 11px 14px;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted, #6b7280);
  background: var(--token-surface-secondary, #f9fafb);
  border-bottom: 1px solid var(--line, #e5e7eb);
  white-space: nowrap;
}
.lb-table td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--line, #e5e7eb);
  vertical-align: top;
  color: var(--text, #111827);
}
.lb-table tbody tr:last-child td { border-bottom: 0; }
.lb-table td.lb-td--wrap { white-space: normal; min-width: 180px; }
.lb-table td.lb-td--num { text-align: right; font-variant-numeric: tabular-nums; }


/* ══════════════════════════════════════════════════════════════════════
   UPLOAD AREA
   ══════════════════════════════════════════════════════════════════════ */

.lb-upload {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center;
  padding: 30px 22px;
  border: 1px dashed rgba(17, 24, 39, 0.18);
  border-radius: var(--radius-lg, 14px);
  background: var(--token-surface-secondary, #f9fafb);
  color: var(--muted, #6b7280);
  font-size: 0.85rem;
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease;
}
/* Hover is gated: on a touch device :hover sticks after a tap and leaves the
   target looking permanently drag-active. */
@media (hover: hover) {
  .lb-upload:hover { border-color: rgba(17, 24, 39, 0.4); background: var(--card, #fff); }
}
.lb-upload[data-drag="true"] { border-color: rgba(17, 24, 39, 0.4); background: var(--card, #fff); }

/* In flight. `pointer-events: none` is the load-bearing half: without it a
   second file can be dropped onto a target that is already uploading one, and
   the two responses race to render the same preview. */
.lb-upload[aria-busy="true"] { opacity: 0.6; pointer-events: none; }

/* Refused. The zone also rewrites its own hint text with the server's reason,
   so this is the visual half of a message that is already in words — it never
   marks a target red without saying why. */
.lb-upload[aria-invalid="true"] {
  border-color: rgba(185, 28, 28, 0.45);
  background: rgba(185, 28, 28, 0.04);
}
.lb-upload[aria-invalid="true"] .lb-upload__text { color: #b91c1c; }

.lb-upload__icon { color: var(--token-empty-icon-color, rgba(17, 24, 39, 0.70)); margin-bottom: 2px; }
.lb-upload__title { color: var(--text, #111827); font-size: 0.92rem; font-weight: 500; }
.lb-upload__text { color: var(--muted, #6b7280); font-size: 0.82rem; }


/* ══════════════════════════════════════════════════════════════════════
   BADGE MODIFIERS
   .stat-badge and .badge-blue/green/amber live in the shell sheet, where
   all three resolve to the SAME neutral grey — Labbie carries no status
   hue. These add the distinctions a data table actually needs while
   staying inside that palette: filled, outlined, muted, and the one red
   the system already owns for genuine error.
   ══════════════════════════════════════════════════════════════════════ */

.badge-outline { background: var(--card, #fff); color: var(--text, #111827); box-shadow: inset 0 0 0 1px var(--line, #e5e7eb); }
.badge-quiet   { background: var(--token-surface-secondary, #f9fafb); color: var(--muted, #6b7280); box-shadow: inset 0 0 0 1px var(--line, #e5e7eb); }
.badge-danger  { background: var(--card, #fff); color: #b91c1c; box-shadow: inset 0 0 0 1px rgba(185, 28, 28, 0.30); }


/* ══════════════════════════════════════════════════════════════════════
   KEY / VALUE SUMMARY, ROWS AND TILES
   ══════════════════════════════════════════════════════════════════════ */

.lb-kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px 20px; }
.lb-kv__k { font-size: 0.74rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted, #6b7280); }
.lb-kv__v { font-size: 0.95rem; color: var(--text, #111827); margin-top: 2px; }

.lb-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line, #e5e7eb);
}
.lb-row:last-child { border-bottom: 0; }
.lb-row__title { font-size: 0.92rem; color: var(--text, #111827); }
.lb-row__sub   { font-size: 0.8rem; color: var(--muted, #6b7280); margin-top: 2px; }
.lb-row__end   { flex: none; display: flex; align-items: center; gap: 10px; }

.lb-tile {
  border: 1px solid var(--line, #e5e7eb);
  border-radius: var(--radius-lg, 14px);
  background: var(--card, #fff);
  padding: 14px 16px;
}

.lb-divider { height: 1px; background: var(--line, #e5e7eb); border: 0; margin: 18px 0; }


/* ══════════════════════════════════════════════════════════════════════
   PAGE HEADER
   Title + description on the left, controls on the right, wrapping to two
   rows before anything is allowed to overflow.
   ══════════════════════════════════════════════════════════════════════ */

.lb-page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
.lb-page-head__text { min-width: 0; }
.lb-page-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }


/* ══════════════════════════════════════════════════════════════════════
   DRAWER
   A right-hand panel on a wide screen, a full-height sheet on a narrow one.
   Same surface, border, radius and shadow language as .wb-modal.
   ══════════════════════════════════════════════════════════════════════ */

.lb-drawer-backdrop {
  position: fixed; inset: 0; z-index: 2900;
  background: rgba(10, 10, 11, 0.45);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity 220ms ease;
}
.lb-drawer-backdrop[data-open="true"] { opacity: 1; }

.lb-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 2950;
  width: min(560px, 100vw);
  display: flex; flex-direction: column;
  background: var(--card, #fff);
  border-left: 1px solid var(--line, #e5e7eb);
  box-shadow: -24px 0 64px rgba(0, 0, 0, 0.18);
  transform: translateX(100%);
  transition: transform 260ms cubic-bezier(.32, .72, .36, 1);
}
.lb-drawer[data-open="true"] { transform: translateX(0); }
.lb-drawer__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
  padding: 20px 22px 16px;
  border-bottom: 1px solid var(--line, #e5e7eb);
  flex: none;
}
.lb-drawer__title { font-size: 1.05rem; font-weight: 500; letter-spacing: -0.02em; }
.lb-drawer__sub   { font-size: 0.85rem; color: var(--muted, #6b7280); margin-top: 3px; }
.lb-drawer__body  { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 22px; }
.lb-drawer__foot  { flex: none; display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line, #e5e7eb); background: var(--token-surface-secondary, #f9fafb); }
.lb-drawer__grip  { display: none; }

@media (max-width: 720px) {
  .lb-drawer {
    top: auto; left: 0; right: 0; bottom: 0;
    width: 100%;
    max-height: 92vh; max-height: 92dvh;
    border-left: 0;
    border-top: 1px solid var(--line, #e5e7eb);
    border-radius: 20px 20px 0 0;
    transform: translateY(100%);
    box-shadow: 0 -20px 60px rgba(0, 0, 0, 0.28);
  }
  .lb-drawer[data-open="true"] { transform: translateY(0); }
  .lb-drawer__grip { display: block; width: 38px; height: 4px; border-radius: 999px; background: rgba(17, 24, 39, 0.18); margin: 10px auto 2px; flex: none; }
  /* THE HOME INDICATOR. Once the panel is anchored to the bottom edge, the
     last ~34px of it are underneath the iPhone's gesture bar, and the footer
     is where this component puts its confirm/cancel buttons — so without this
     the primary action of every drawer sits under the indicator and is hard
     to hit. `env()` resolves to 0px on hardware that has no inset and on any
     browser that does not support it, so the desktop and Android cases are
     unchanged. The page opts in with viewport-fit=cover. */
  .lb-drawer__foot { padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }
  .lb-drawer__body { padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px)); }
  /* When the footer is present it sits below the body and owns the inset, so
     the body must not add a second one. Where :has() is unsupported the body
     simply keeps a little extra bottom padding it does not need — the footer
     still clears the indicator, which is the part that matters. */
  .lb-drawer:has(.lb-drawer__foot:not([hidden])) .lb-drawer__body { padding-bottom: 20px; }
}


/* ══════════════════════════════════════════════════════════════════════
   EMPTY STATE (block form)
   .empty-state in labbie-dashboard-shell.css is the centred text form. This
   is the same treatment with the dashboard's round icon plate and room for
   one call to action — the shape #publishedEmpty uses.
   ══════════════════════════════════════════════════════════════════════ */

.lb-empty { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 30px 20px; }
.lb-empty--tall { padding: 46px 24px; }
.lb-empty__icon {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  border-radius: 999px;
  background: var(--token-empty-icon-bg, rgba(17, 24, 39, 0.10));
  border: 1px solid var(--token-empty-icon-border, rgba(17, 24, 39, 0.18));
  color: var(--token-empty-icon-color, rgba(17, 24, 39, 0.70));
  margin-bottom: 12px;
}
.lb-empty__title { font-size: 0.95rem; font-weight: 500; color: var(--text, #111827); margin-bottom: 4px; }
.lb-empty__text  { font-size: 0.86rem; color: var(--muted, #6b7280); line-height: 1.55; max-width: 42ch; }
.lb-empty__action { margin-top: 16px; }


/* ══════════════════════════════════════════════════════════════════════
   ACCESSIBILITY
   ══════════════════════════════════════════════════════════════════════ */

.lb-visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Keyboard focus. SCOPED to a console that opts in with .lb-console on <body>,
   so adding a focus ring to the design system cannot restyle a page that has
   not asked for one. :focus-visible only — a pointer press never draws it. */
.lb-console :where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid rgba(17, 24, 39, 0.55);
  outline-offset: 2px;
  border-radius: var(--radius-md, 10px);
}
.lb-console .lb-check input:focus-visible + .lb-check__box {
  outline: 2px solid rgba(17, 24, 39, 0.55);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .lb-drawer,
  .lb-drawer-backdrop,
  .lb-spin { transition: none !important; animation: none !important; }
  /* The pulse stops, but the halo and the grey fill stay, so "reconnecting"
     is still distinguishable from the other three states without motion. */
  .lb-dot--pending { animation: none !important; }
}
