/* ================================================================
   MARKO REAL ESTATE - COMPONENTS
   Reusable UI shared by every page: cards, buttons, forms, tables,
   badges, alerts, empty states, pagination, modals, galleries, maps.
   ================================================================ */

/* ─── Cards ─────────────────────────────────────────────── */
.card{
  background:var(--surface);border-radius:var(--radius-lg);
  border:1px solid var(--border);box-shadow:var(--shadow-xs);
  transition:box-shadow var(--transition),border-color var(--transition);
}
.card:hover{box-shadow:var(--shadow-sm);border-color:var(--border-strong)}
.card__header{padding:13px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.card__title{font-family:var(--font-display);font-size:.98rem;font-weight:700;color:var(--text);letter-spacing:-.015em;line-height:1.3}
.card__subtitle{font-size:.76rem;color:var(--text-muted);margin-top:2px;line-height:1.35}
.card__body{padding:16px 18px}
.card__footer{padding:11px 18px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:12px}
/* A footer carrying several controls rather than two ends. They wrap on a
   narrow screen instead of being squeezed. */
.card__footer--actions{justify-content:flex-start;flex-wrap:wrap;gap:10px}
.card__footer-note{font-size:var(--fs-2xs);color:var(--text-subtle);margin-left:auto}
@media(max-width:768px){.card__footer-note{margin-left:0;width:100%}}
/* A short single-column form. Left at full width its labels would sit a
   screen-width apart from their controls on a wide monitor, which is a longer
   eye travel than the form is worth. */
.card--narrow{max-width:640px;margin-inline:auto}
/* A column of controls in a card body — the manage panel on a detail page.
   Was a repeated inline flex declaration on every such panel. */
.stack{display:flex;flex-direction:column;gap:var(--space-2);align-items:stretch}


/* ── Document-expiry notice ───────────────────────────────
   Not an .alert: this one carries a list, links and badges, and a wash of
   warning colour over all of it would make the permit titles harder to read
   than the plain rows they link to. The colour is kept to the rail and the
   glyph, and the content stays on white where it is legible. */
/* A banner, not a section: it sits between the page title and the first real
   content, so every pixel it takes is pushed onto everything below it. Kept
   legible but compact — the warning tone and the left rule do the work that
   generous padding was doing. */
.notice{
  display:flex;gap:11px;align-items:flex-start;
  background:var(--surface);border:1px solid var(--border);
  border-left:3px solid var(--warning);
  border-radius:var(--radius-lg);padding:9px 12px;margin-bottom:10px;
  box-shadow:var(--shadow-xs);
}
.notice__icon{
  flex-shrink:0;width:28px;height:28px;border-radius:8px;
  display:grid;place-items:center;font-size:.92rem;
  background:var(--warning-bg);color:var(--warning);
}
.notice__body{flex:1;min-width:0}
.notice__title{font-size:.85rem;font-weight:700;color:var(--text);line-height:1.3}
.notice__list{margin:3px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px}
.notice__item{
  display:flex;align-items:center;gap:7px;flex-wrap:wrap;
  font-size:.8rem;color:var(--text-muted);line-height:1.4;
}
.notice__item a{font-weight:600;color:var(--text);text-decoration:none}
.notice__item a:hover{color:var(--primary);text-decoration:underline}
.notice__link{
  display:inline-flex;align-items:center;gap:6px;margin-top:4px;
  font-size:.8rem;font-weight:600;color:var(--primary);
}
.notice__link i{transition:transform var(--transition)}
.notice__link:hover i{transform:translateX(3px)}
@media(prefers-reduced-motion:reduce){.notice__link i{transition:none}.notice__link:hover i{transform:none}}
/* The glyph goes rather than the padding: on a narrow screen the text needs
   the full width more than the banner needs an ornament. */
@media(max-width:560px){.notice{padding:10px 12px}.notice__icon{display:none}}

/* ─── Buttons ───────────────────────────────────────────── */
/* Min height 44px on --lg and on touch devices keeps tap targets legal
   without inflating dense admin toolbars. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  padding:10px 20px;border-radius:var(--radius-sm);
  font-size:var(--fs-sm);font-weight:600;border:1px solid transparent;
  transition:background var(--transition),border-color var(--transition),
             color var(--transition),box-shadow var(--transition),transform var(--transition);
  line-height:1.3;white-space:nowrap;cursor:pointer;text-decoration:none;
}
.btn:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.btn:active{transform:translateY(1px)}
.btn--primary{background:var(--primary);color:#fff;border-color:var(--primary)}
.btn--primary:hover{background:var(--primary-hover);border-color:var(--primary-hover);color:#fff;box-shadow:0 4px 14px rgba(0,117,192,.28)}
.btn--outline{background:var(--surface);border-color:var(--border-strong);color:var(--text)}
.btn--outline:hover{border-color:var(--primary);background:var(--primary-tint);color:var(--primary)}
.btn--ghost{background:transparent;color:var(--text);border-color:transparent}
.btn--ghost:hover{background:var(--surface-3)}
.btn--danger{background:var(--danger);color:#fff;border-color:var(--danger)}
.btn--danger:hover{background:#a91616;border-color:#a91616}
.btn--success{background:var(--success);color:#fff;border-color:var(--success)}
.btn--success:hover{background:#126b34}
.btn--sm{padding:7px 14px;font-size:var(--fs-xs);gap:6px}
.btn--lg{padding:12px 26px;font-size:var(--fs-body);min-height:44px}
.btn--block{width:100%;white-space:normal}
.btn--icon{width:38px;height:38px;padding:0}
.btn[disabled],.btn.is-disabled{opacity:.55;pointer-events:none}
/* Coarse pointers (touch) get the full 44px target on every button. */
@media (pointer:coarse){
  .btn{min-height:44px}
  .btn--sm{min-height:40px}
  .btn--icon{width:44px;height:44px}
}

/* ─── Forms ─────────────────────────────────────────────── */
.form-group{margin-bottom:18px}
/* Read the scale rather than restating it. These three carried literal rem
   values — .82 / .88 — which meant the type tokens governed everything except
   the controls people actually spend the day in. */
.form-label{display:block;font-size:var(--fs-xs);font-weight:600;color:var(--text);margin-bottom:6px;letter-spacing:-.1px}
.form-control{
  width:100%;padding:10px 13px;
  border:1px solid var(--border-strong);border-radius:var(--radius-sm);
  font-size:var(--fs-sm);color:var(--text);background:var(--surface);
  /* Colour and shadow only. `all` also animated padding and border-width,
     which is what made a focused field appear to twitch. */
  transition:border-color var(--transition),box-shadow var(--transition),background var(--transition);
}
.form-control:hover{border-color:#cbd0d8}
.form-control:focus{outline:none;border-color:var(--primary);box-shadow:var(--ring)}
.form-control::placeholder{color:var(--text-subtle)}
.form-control--error{border-color:var(--danger)}
.form-control--error:focus{box-shadow:0 0 0 3px rgba(220,38,38,.15)}
/* A field whose value is fixed by context — e.g. the single property a tenant
   is allowed to report against. Deliberately not styled like :disabled: the
   value still submits, stays selectable and keeps full text contrast, so only
   the surface recedes and the caret is dropped. */
.form-control[readonly]{background:var(--surface-2);border-color:var(--border);cursor:default}
.form-control[readonly]:hover{border-color:var(--border)}
.form-hint{font-size:.74rem;color:var(--text-subtle);margin-top:4px}
/* Who you are signed in as, under a refusal. Separated by a rule because it
   answers a different question from the one above it — not "why was I
   refused" but "am I even the right person". */
.deny-account{
  border-top:1px solid var(--border);
  margin-top:var(--space-4);padding-top:var(--space-4);
  text-align:center;display:flex;flex-direction:column;gap:6px;
}
.text-center{text-align:center}

/* A password field with a reveal button inside it. The input keeps its own
   padding on the right so the text never runs under the button. */
.input-reveal{position:relative;display:block}
.input-reveal .form-control{padding-right:42px}
.input-reveal__btn{
  position:absolute;right:4px;top:50%;transform:translateY(-50%);
  width:32px;height:32px;display:grid;place-items:center;
  border:0;background:transparent;border-radius:var(--radius-xs);
  color:var(--text-subtle);cursor:pointer;
  transition:color var(--transition),background var(--transition);
}
.input-reveal__btn:hover{color:var(--text);background:var(--surface-3)}
.input-reveal__btn:focus-visible{outline:2px solid var(--primary);outline-offset:1px}

/* A field holding text where the exact characters matter — legal wording with
   its own markup conventions, a template. Monospaced so indentation and
   blank lines are visible while they are being typed. */
.form-control--code{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.85rem;line-height:1.65;tab-size:2;
}
/* The required marker. aria-hidden on the asterisk itself, because the
   control carries `required` and a screen reader announces that already —
   reading "star" after every label is noise. */
.req{color:var(--danger);font-weight:600;margin-left:2px}

/* ── Checkbox / radio rows ─────────────────────────────────
   Replaces the inline flex + font-size that every form wrote by hand. The
   whole row is the target, not just the 15px box: a checkbox is the
   smallest control on any form and the label is the part people aim at. */
/* The submit/cancel bar at the foot of a full-page form. Separated by a rule
   rather than only by space, so on a long form it reads as the end of the
   form rather than another section. */

.check-row{display:flex;flex-wrap:wrap;gap:var(--space-3) var(--space-5);margin-bottom:var(--space-5)}
.check{
  display:inline-flex;align-items:center;gap:8px;cursor:pointer;
  font-size:var(--fs-sm);color:var(--text-body);
  padding:7px 11px;border:1px solid var(--border);border-radius:var(--radius-sm);
  background:var(--surface);transition:border-color var(--transition),background var(--transition);
  min-height:38px;
}
.check:hover{border-color:var(--border-strong);background:var(--surface-2)}
.check input{width:16px;height:16px;accent-color:var(--primary);cursor:pointer;flex-shrink:0}
/* :has is what lets the row show its own checked state. Where it is not
   supported the control still reads correctly — only the tint is lost. */
.check:has(input:checked){border-color:var(--primary-border);background:var(--primary-tint);color:var(--text)}
.check:has(input:focus-visible){outline:2px solid var(--primary);outline-offset:2px}
@media(pointer:coarse){.check{min-height:44px}}
.form-error{font-size:.74rem;color:var(--danger);margin-top:4px;display:flex;align-items:center;gap:4px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
select.form-control{
  appearance:none;cursor:pointer;
  /* Same stroked chevron, weight and colour as the search and filter bars —
     the old one was a filled wedge in an off-palette grey. */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%236b7688' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M3 4.5L6 7.5l3-3'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 12px center;padding-right:32px;
}
/* An optional field left on "— None —" is a placeholder, not a value, and
   should read like one — matching how ::placeholder treats empty inputs.
   Still 5.1:1 on white, so it stays AA. Browsers without :has() simply
   show it at full strength, which is readable either way. */
select.form-control:has(option[value=""]:checked){color:var(--text-subtle)}

/* ─── Tables ────────────────────────────────────────────── */
.table-wrap{overflow-x:auto;border-radius:var(--radius);border:1px solid var(--border);background:var(--surface)}
/* Cell padding comes from the density tokens rather than a literal, which is
   what makes comfortable/compact one switch instead of an edit in forty
   places. Type comes from the scale for the same reason the form does. */
.table{width:100%;border-collapse:collapse;font-size:var(--fs-sm)}
.table th{
  background:var(--surface-2);font-weight:600;font-size:var(--fs-2xs);
  text-transform:uppercase;letter-spacing:.6px;color:var(--text-muted);
  padding:var(--cell-head-y) var(--cell-x);text-align:left;border-bottom:1px solid var(--border);
}
.table td{padding:var(--cell-y) var(--cell-x);border-bottom:1px solid var(--border);vertical-align:middle;color:var(--text)}
.table tr:last-child td{border-bottom:none}
.table tbody tr{transition:background var(--transition)}
.table tbody tr:hover td{background:var(--surface-2)}
.table th:first-child,.table td:first-child{padding-left:18px}
.table th:last-child,.table td:last-child{padding-right:18px}
.table .check-col{width:36px;padding-right:0!important}
.table input[type=checkbox]{width:15px;height:15px;border-radius:4px;accent-color:var(--primary);cursor:pointer}
.table__id{font-family:'SF Mono',ui-monospace,Menlo,Monaco,Consolas,monospace;font-size:.78rem;color:var(--text-muted);letter-spacing:.2px}

/* ─── Status ────────────────────────────────────────────────
   The one status component, rendered by uiStatus(). Every state in the
   system wears this shape: a tinted pill, a hairline of its own tone, and
   a dot.

   The dot is not decoration. Status is the one column people scan by
   colour, and colour alone fails anyone who cannot separate these hues —
   so the label is always spelled out and the dot gives the pill a second,
   non-colour channel. Tone comes from getStatusBadgeClass(), the same map
   the dashboard ring and the charts read.

   All eight tones are defined. The old .status-pill had five, which is why
   a sold property (purple) and a maintenance one (orange) had nowhere to
   go and fell back to a badge instead. */
.status{
  display:inline-flex;align-items:center;gap:6px;
  padding:3px 10px 3px 8px;border-radius:var(--radius-pill);
  font-size:.74rem;font-weight:600;line-height:1.6;
  white-space:nowrap;border:1px solid transparent;
}
.status__dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}
.status--success{background:var(--success-bg);color:var(--success);border-color:rgba(21,128,61,.18)}
.status--warning{background:var(--warning-bg);color:var(--warning);border-color:rgba(180,83,9,.18)}
.status--danger {background:var(--danger-bg); color:var(--danger); border-color:rgba(200,30,30,.18)}
.status--info   {background:var(--info-bg);   color:var(--info);   border-color:rgba(0,95,158,.18)}
.status--primary{background:var(--primary-light);color:var(--primary-ink);border-color:var(--primary-border)}
.status--purple {background:var(--purple-bg); color:var(--purple); border-color:rgba(109,40,217,.18)}
.status--orange {background:var(--orange-bg); color:var(--orange); border-color:rgba(194,65,12,.18)}
.status--muted  {background:var(--surface-3); color:var(--text-muted);border-color:var(--border-strong)}

/* ─── Badges (no dot — keep for legacy) ────────────────── */

/* ─── Alerts ────────────────────────────────────────────── */
.alert{
  display:flex;align-items:center;gap:10px;padding:12px 16px;
  border-radius:var(--radius-sm);font-size:.85rem;margin-bottom:18px;
  border:1px solid transparent;animation:alertIn .3s ease;
}
.alert--success{background:var(--success-bg);color:var(--success);border-color:rgba(22,163,74,.18)}
.alert--danger{background:var(--danger-bg);color:var(--danger);border-color:rgba(220,38,38,.18)}
.alert--warning{background:var(--warning-bg);color:var(--warning);border-color:rgba(217,119,6,.18)}
.alert--info{background:var(--info-bg);color:var(--info);border-color:rgba(37,99,235,.18)}
.alert__close{margin-left:auto;background:none;border:none;color:inherit;opacity:.6;font-size:1rem;cursor:pointer}
.alert__close:hover{opacity:1}
@keyframes alertIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}


/* ─── Empty state ───────────────────────────────────────── */
.empty-state{text-align:center;padding:48px 20px}
.empty-state__icon{
  width:56px;height:56px;border-radius:14px;margin:0 auto 14px;
  background:var(--surface-3);color:var(--text-muted);
  display:flex;align-items:center;justify-content:center;font-size:1.6rem;
}
.empty-state__title{font-size:1rem;font-weight:600;margin-bottom:4px;color:var(--text)}
.empty-state__desc{color:var(--text-muted);font-size:.85rem;max-width:360px;margin:0 auto}
/* Centred as a group and allowed to wrap: a filtered-empty list offers both
   "Clear filters" and "Add …", which is two buttons a narrow column cannot
   keep on one line. */
.empty-state__actions{
  display:flex;justify-content:center;flex-wrap:wrap;
  gap:var(--space-2);margin-top:var(--space-4);
}


/* ─── Pagination ────────────────────────────────────────── */
.pagination{display:flex;align-items:center;gap:4px;justify-content:flex-end;padding:14px 0;flex-wrap:wrap}
.pagination__link{
  min-width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0 9px;
  border-radius:var(--radius-xs);font-size:.78rem;font-weight:600;
  color:var(--text-muted);background:var(--surface);
  border:1px solid var(--border);transition:all var(--transition);
}
.pagination__link:hover{border-color:var(--text-muted);color:var(--text)}
.pagination__link.is-active{background:var(--text);color:#fff;border-color:var(--text)}
.pagination__link.is-disabled{opacity:.4;pointer-events:none}
.pagination__ellipsis{padding:0 6px;color:var(--text-subtle);font-weight:600}

/* ─── Filter bar ────────────────────────────────────────── */

/* ─── Form grids ────────────────────────────────────────── */
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}
.form-grid--2{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
.form-grid--3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.form-grid .full-span{grid-column:1/-1}
@media(max-width:768px){.form-grid--2,.form-grid--3{grid-template-columns:1fr}}
.form-checkbox{display:flex;align-items:center;gap:8px;font-size:.86rem;color:var(--text);padding:8px 0;cursor:pointer}
.form-checkbox input{width:16px;height:16px;accent-color:var(--primary);cursor:pointer}
/* A figure the form derives rather than asks for — the buyer's total, a
   computed balance. It is an outcome, so it does not wear a label and a box
   like something you can type into. */
.form-callout{
  display:flex;align-items:baseline;gap:var(--space-2);flex-wrap:wrap;
  padding:var(--space-3) var(--space-4);margin-bottom:var(--space-4);
  background:var(--primary-tint);border:1px solid var(--primary-border);
  border-radius:var(--radius);
}
.form-callout__label{
  font-size:var(--fs-2xs);font-weight:600;text-transform:uppercase;
  letter-spacing:.5px;color:var(--primary-ink);
}
.form-callout__value{
  font-size:1.15rem;font-weight:700;color:var(--text);
  font-variant-numeric:tabular-nums;letter-spacing:-.2px;
}
.form-callout__note{font-size:var(--fs-2xs);color:var(--text-subtle);margin-left:auto}

/* ─── Galleries / uploads ───────────────────────────────── */
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;margin-bottom:14px}
.gallery__item{position:relative;aspect-ratio:1/1;border-radius:var(--radius-sm);overflow:hidden;border:1px solid var(--border);background:var(--surface-3)}
.gallery__item img{width:100%;height:100%;object-fit:cover}
.gallery__cover-badge{position:absolute;top:6px;left:6px;background:var(--primary);color:#fff;font-size:.64rem;font-weight:700;padding:2px 8px;border-radius:var(--radius-pill);text-transform:uppercase;letter-spacing:.4px}
.gallery__remove{position:absolute;top:6px;right:6px;width:24px;height:24px;border-radius:50%;background:rgba(15,23,42,.65);color:#fff;display:flex;align-items:center;justify-content:center;font-size:.78rem;border:none}
.gallery__remove:hover{background:var(--danger)}

.upload-zone{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:28px;border:1.5px dashed var(--border-strong);border-radius:var(--radius);
  background:var(--surface-2);text-align:center;cursor:pointer;transition:all var(--transition);
}
.upload-zone:hover{border-color:var(--primary);background:var(--primary-tint)}
.upload-zone i{font-size:1.6rem;color:var(--text-subtle);margin-bottom:8px}
.upload-zone__title{font-size:var(--fs-sm);font-weight:600;color:var(--text)}
.upload-zone__hint{font-size:.78rem;color:var(--text-muted);margin-top:4px}
.upload-zone input[type=file]{display:none}
/* A file has been chosen — confirm it without needing a separate line. */
.upload-zone.is-filled{border-style:solid;border-color:var(--primary);background:var(--primary-tint)}
.upload-zone.is-filled i{color:var(--primary)}

/* ─── Property card (grid view) ─────────────────────────── */
.prop-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;transition:all var(--transition)}
.prop-card:hover{box-shadow:var(--shadow-md);transform:translateY(-2px);border-color:var(--border-strong)}
.prop-card__cover{aspect-ratio:16/10;background:var(--surface-3);position:relative;overflow:hidden}
.prop-card__cover img{width:100%;height:100%;object-fit:cover}
.prop-card__cover .status{position:absolute;top:10px;left:10px}
.prop-card__body{padding:14px 16px}
.prop-card__title{font-size:.96rem;font-weight:600;margin-bottom:4px;line-height:1.3;color:var(--text)}
.prop-card__meta{font-size:.76rem;color:var(--text-muted);margin-bottom:10px;display:flex;align-items:center;gap:6px}
.prop-card__price{font-size:1.05rem;font-weight:700;color:var(--text);letter-spacing:-.3px}
.prop-card__specs{display:flex;gap:14px;margin-top:8px;font-size:.76rem;color:var(--text-muted)}
.prop-card__specs span{display:flex;align-items:center;gap:4px}
/* The card's own action strip: the code on the left, the same ⋮ menu the
   table row carries on the right, so an action is in the same place and
   named the same way whichever view someone is working in. */
.prop-card__foot{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);
  padding:9px 12px 9px 16px;border-top:1px solid var(--border);background:var(--surface-2);
}
.prop-card__title a{color:inherit}
.prop-card__title a:hover{color:var(--primary)}
/* A card that is itself one big link — the owner's portfolio grid, where the
   whole tile navigates rather than carrying its own controls. */
.prop-card--link{display:block;text-decoration:none;color:inherit}
.prop-card--link:hover{text-decoration:none}
.prop-card__placeholder{
  display:grid;place-items:center;height:100%;
  color:var(--text-subtle);font-size:1.8rem;
}
.prop-card__per,
/* The same suffix appears in table cells and on detail pages; it belongs
   to a price, not to the card that happened to show one. */
.price-per{font-size:.78rem;font-weight:500;color:var(--text-muted)}
/* The at-a-glance line inside the body: tenant, open faults, paperwork. Not
   .prop-card__foot, which is the action strip and sits outside the body. */
.prop-card__stats{
  display:flex;flex-wrap:wrap;gap:12px;
  border-top:1px solid var(--border);margin-top:12px;padding-top:10px;
  font-size:var(--fs-2xs);color:var(--text-muted);
}
.prop-card__stats span{display:inline-flex;align-items:center;gap:5px}
.prop-card__cta{
  margin-top:12px;font-size:var(--fs-2xs);font-weight:600;color:var(--primary);
  display:inline-flex;align-items:center;gap:5px;
}
.prop-card--link:hover .prop-card__cta i{transform:translateX(2px)}
.prop-card__cta i{transition:transform var(--transition)}
/* The whole card lifts on hover, so the cover must not also scale — two
   things moving at once for one intention reads as fidgeting. */
.prop-card__cover{display:block}

/* ─── Detail page columns ───────────────────────────────────
   The two-column body every detail page uses: the narrative on the left,
   the record's facts on the right. Replaces the inline
   `grid-template-columns:2fr 1fr` that each show page carried its own copy
   of — an inline grid template also beats every stylesheet rule, so those
   pages could not be made to stack on a phone.

   minmax(0,…) on both tracks: grid items default to min-width:auto, so one
   long unbroken string — a code, a URL in a description — would otherwise
   push the column wider than its share and take the page with it. */
.detail-cols{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--space-5);align-items:start}
.detail-cols__main{min-width:0}
/* Sticky so the facts stay in view while a long description scrolls past.
   Offset by the topbar, which is what it would otherwise slide under. */
.detail-cols__side{min-width:0;position:sticky;top:calc(var(--header-height) + var(--space-4))}
@media(max-width:1024px){
  .detail-cols{grid-template-columns:1fr}
  .detail-cols__side{position:static}
}

/* Long-form copy inside a card — a description, a note, stored terms. */
.prose{color:var(--text-body);line-height:var(--lh-body);max-width:72ch;overflow-wrap:anywhere}
.prose p+p{margin-top:.75em}

/* ─── Profile / detail ──────────────────────────────────── */
.profile-grid{display:grid;grid-template-columns:300px 1fr;gap:20px}
@media(max-width:900px){.profile-grid{grid-template-columns:1fr}}
.profile-card{text-align:center;padding:24px}
.profile-card__avatar{width:96px;height:96px;border-radius:50%;margin:0 auto 12px;object-fit:cover;border:3px solid var(--surface);box-shadow:0 0 0 1px var(--border)}
.profile-card__name{font-size:1.15rem;font-weight:700;margin-bottom:2px}
.profile-card__role{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;margin-bottom:14px;font-weight:600}
.profile-meta{text-align:left;margin-top:16px}
.profile-meta__row{display:flex;justify-content:space-between;padding:8px 0;border-top:1px solid var(--border);font-size:.84rem}
.profile-meta__row:first-child{border-top:none}
.profile-meta__label{color:var(--text-muted)}
.profile-meta__value{font-weight:600;color:var(--text);text-align:right}

/* ─── Tabs (with count badges) ──────────────────────────── */
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:20px;overflow-x:auto;padding:0 2px}
/* Inside a flush card body the strip has to reinstate the padding the body
   gave up, or the first tab sits against the card's edge. */
.tabs--inset{padding-left:var(--card-pad-x,20px);padding-right:var(--card-pad-x,20px)}
.tabs__item{
  display:inline-flex;align-items:center;gap:8px;
  padding:11px 4px;margin-right:18px;
  font-size:.88rem;font-weight:500;color:var(--text-muted);
  cursor:pointer;border-bottom:2px solid transparent;
  transition:all var(--transition);white-space:nowrap;background:none;border-top:none;border-left:none;border-right:none;
}
.tabs__item:hover{color:var(--text)}
.tabs__item.is-active{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
.tabs__count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:22px;height:20px;padding:0 6px;border-radius:var(--radius-pill);
  background:var(--surface-3);color:var(--text-muted);
  font-size:.7rem;font-weight:600;
}
.tabs__item.is-active .tabs__count{background:var(--primary-light);color:var(--primary)}
.tab-panel{display:none}
.tab-panel.is-active{display:block}

/* ─── Section title ─────────────────────────────────────── */
.section-title{font-size:.76rem;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:700;margin:16px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)}

/* ─── Grid helpers ──────────────────────────────────────── */
/* Card grid that chooses its own column count. auto-fill rather than a fixed
   three: the content column is a different width on every screen and inside
   every layout, and 260px is the narrowest a property card stays readable
   at. minmax(0,1fr) rather than 1fr so a long unbroken title cannot force a
   column wider than its share. */
.grid-auto{
  display:grid;gap:var(--space-4);
  grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
@media(max-width:768px){.grid-2,.grid-3{grid-template-columns:1fr}}

/* ─── Button group ──────────────────────────────────────── */
.btn-group{display:inline-flex;gap:6px}
.btn-group .btn{padding:6px 10px;font-size:.74rem}

/* ─── Receipt / Print ───────────────────────────────────── */
.receipt{max-width:680px;margin:0 auto;background:#fff;padding:36px;border-radius:var(--radius);border:1px solid var(--border)}
.receipt__head{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid var(--text);padding-bottom:16px;margin-bottom:20px}
.receipt__logo{font-size:1.5rem;font-weight:700;letter-spacing:-.3px}
/* Uploaded mark sits above the company name; height-capped so a large
   upload cannot push the rest of the receipt onto a second page. */
.receipt__logo-img{max-height:56px;max-width:200px;object-fit:contain;margin-bottom:8px;display:block}
.receipt__brand-sub{font-size:.78rem;color:var(--text-muted)}
.receipt__meta{text-align:right;font-size:.84rem}
.receipt__table{width:100%;border-collapse:collapse;margin:16px 0}
.receipt__table td{padding:9px 0;border-bottom:1px dashed var(--border);font-size:.88rem}
.receipt__table tr:last-child td{border-bottom:none;font-weight:700;font-size:1rem;padding-top:14px;border-top:2px solid var(--text)}
@media print{
  .app__sidebar,.app__header,.app__footer,.sidebar-overlay,.no-print{display:none!important}
  .app__main{margin-left:0!important}
  .app__content{padding:0!important;background:#fff}
  .receipt{border:none;max-width:100%}
}


/* ════════════════════════════════════════════════════════════
   MODAL — a dialog over a blurred page
   The page behind is not merely dimmed: the backdrop blurs and
   desaturates it, so the dialog reads as the only live surface
   while the layout underneath stays recognisable.
   ════════════════════════════════════════════════════════════ */
.modal[hidden]{display:none}
.modal{
  position:fixed;inset:0;z-index:300;
  display:flex;align-items:flex-start;justify-content:center;
  padding:var(--space-8) var(--gutter);
  overflow-y:auto;overscroll-behavior:contain;
}
.modal__backdrop{
  position:fixed;inset:0;
  background:rgba(16,34,44,.38);
  backdrop-filter:blur(18px) saturate(135%);
  -webkit-backdrop-filter:blur(18px) saturate(135%);
  opacity:0;transition:opacity var(--transition-slow);
}
.modal.is-open .modal__backdrop{opacity:1}
/* Without backdrop-filter the tint has to carry the separation alone. */
@supports not ((backdrop-filter:blur(2px)) or (-webkit-backdrop-filter:blur(2px))){
  .modal__backdrop{background:rgba(16,34,44,.74)}
}

.modal__dialog{
  position:relative;margin:auto;width:100%;max-width:680px;
  max-height:calc(100vh - var(--space-16));
  display:flex;flex-direction:column;min-height:0;
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-xl);
  opacity:0;transform:translateY(16px) scale(.98);
  transition:opacity var(--transition-slow),transform var(--transition-slow);
}
.modal.is-open .modal__dialog{opacity:1;transform:none}
.modal__dialog--lg{max-width:920px}
/* A prompt rather than a form: narrow enough that the question and its
   two answers read as one thing. */
.modal__dialog--sm{max-width:520px}

.modal__header{
  display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-4);
  padding:var(--space-5) var(--space-6);
  border-bottom:1px solid var(--border);
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  background:linear-gradient(180deg,var(--primary-tint),var(--surface));
}
.modal__title{
  font-family:var(--font-display);font-size:1.15rem;font-weight:700;
  letter-spacing:-.02em;color:var(--text);display:flex;align-items:center;gap:10px;
}
.modal__title i{color:var(--primary)}
.modal__subtitle{font-size:var(--fs-xs);color:var(--text-muted);margin-top:4px}
.modal__close{
  flex-shrink:0;width:34px;height:34px;border-radius:var(--radius-sm);
  display:grid;place-items:center;cursor:pointer;
  border:1px solid transparent;background:transparent;color:var(--text-muted);
  transition:background var(--transition),color var(--transition),border-color var(--transition);
}
.modal__close:hover{background:var(--surface-3);color:var(--text);border-color:var(--border)}
.modal__close:focus-visible{outline:2px solid var(--primary);outline-offset:2px}

/* The form owns the scroll so the header and footer stay put. */
.modal__form{display:flex;flex-direction:column;min-height:0;flex:1}
.modal__body{padding:var(--space-6);overflow-y:auto;flex:1;min-height:0}
.modal__body .form-group:last-child{margin-bottom:0}
.modal__footer{
  display:flex;align-items:center;gap:var(--space-3);
  padding:var(--space-4) var(--space-6);
  border-top:1px solid var(--border);background:var(--surface-2);
  border-radius:0 0 var(--radius-lg) var(--radius-lg);
}
.modal__footer .modal__footer-note{margin-left:auto;font-size:var(--fs-xs);color:var(--text-subtle)}

/* Section captions inside a long form */
.form-section{
  font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.7px;
  color:var(--text-muted);margin:var(--space-6) 0 var(--space-3);
  padding-bottom:6px;border-bottom:1px solid var(--border);
}
.form-section:first-child{margin-top:0}

body.is-modal-open{overflow:hidden}

@media(max-width:768px){
  .modal{padding:0}
  .modal__dialog{max-width:none;min-height:100%;max-height:none;border:none;border-radius:0}
  .modal__header,.modal__footer{border-radius:0}
  .modal__body{padding:var(--space-5)}
  .modal__footer{flex-wrap:wrap}
  .modal__footer .btn{flex:1;min-width:140px}
  .modal__footer .modal__footer-note{margin-left:0;flex-basis:100%}
}

@media(prefers-reduced-motion:reduce){
  .modal__backdrop,.modal__dialog{transition:none}
}

/* ════════════════════════════════════════════════════════════
   GEO FIELD — location text, coordinates, device positioning
   ════════════════════════════════════════════════════════════ */
/* Read-only by intent: the value is generated and offered for copying, so
   it wears the surface of something displayed rather than typed. */
.form-control--readonly{background:var(--surface-2);color:var(--text-muted);cursor:default}
.form-control--readonly:focus{border-color:var(--border);box-shadow:none}

.geo{
  border:1px solid var(--border);border-radius:var(--radius);
  background:var(--surface-2);padding:var(--space-4);margin-bottom:18px;
}
.geo__head{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap;
}
.geo__title{
  font-size:.82rem;font-weight:700;color:var(--text);
  display:flex;align-items:center;gap:6px;
}
.geo__title i{color:var(--primary)}
.geo__hint{font-size:var(--fs-xs);color:var(--text-muted);margin-top:3px;max-width:52ch}
.geo__grid{display:grid;grid-template-columns:1fr 1fr auto;gap:var(--space-3);align-items:end}
.geo .form-group{margin-bottom:var(--space-4)}
.geo__grid .form-group{margin-bottom:0}
.geo__frame{border:0;display:block;width:100%;border-radius:var(--radius-sm)}
.geo__map{
  display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 14px;
  border:1px solid var(--border-strong);border-radius:var(--radius-sm);
  background:var(--surface);color:var(--text-muted);
  font-size:var(--fs-xs);font-weight:600;white-space:nowrap;
  transition:color var(--transition),border-color var(--transition),background var(--transition);
}
.geo__map:hover{color:var(--primary);border-color:var(--primary);background:var(--primary-tint)}
.geo__map.is-disabled{opacity:.5;pointer-events:none}
.geo__status{
  display:none;align-items:flex-start;gap:6px;
  font-size:var(--fs-xs);line-height:1.45;color:var(--text-muted);margin-top:var(--space-3);
}
.geo__status.is-shown{display:flex}
.geo__status--ok{color:var(--success)}
.geo__status--error{color:var(--danger)}
.geo__status i{margin-top:1px}
.spin{display:inline-block;animation:spin 900ms linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.spin{animation:none}}

@media(max-width:768px){
  .geo__grid{grid-template-columns:1fr 1fr}
  .geo__map{grid-column:1 / -1;justify-content:center}
  .geo__head .btn{width:100%;justify-content:center}
}



/* ════════════════════════════════════════════════════════════
   PROPERTY MAP — the pin, framed
   Shared by the admin detail page and the public listing tab.
   ════════════════════════════════════════════════════════════ */
.propmap{display:flex;flex-direction:column;gap:var(--space-3)}
.propmap__frame{
  position:relative;aspect-ratio:16/9;min-height:240px;
  border:1px solid var(--border);border-radius:var(--radius);
  overflow:hidden;background:var(--surface-2);
  box-shadow:var(--shadow-xs);
}
/* Give the tiles a moment's grace: a plain grey box while the frame
   loads reads as "map arriving", not "map broken". */
.propmap__frame::before{
  content:'';position:absolute;inset:0;
  background:
    linear-gradient(var(--border) 1px,transparent 1px) 0 0/32px 32px,
    linear-gradient(90deg,var(--border) 1px,transparent 1px) 0 0/32px 32px,
    var(--surface-2);
  opacity:.5;
}
.propmap__frame iframe{position:relative;display:block;width:100%;height:100%}

.propmap__bar{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--space-3);flex-wrap:wrap;
}
.propmap__coords{
  display:inline-flex;align-items:center;gap:7px;
  font-family:var(--font-mono);font-size:var(--fs-xs);
  color:var(--text-muted);
  background:var(--surface-2);border:1px solid var(--border);
  border-radius:var(--radius-sm);padding:6px 10px;
  user-select:all;   /* one click selects the whole pair for copying */
}
.propmap__coords i{color:var(--primary);font-size:.85rem}
.propmap__actions{display:flex;gap:var(--space-2);flex-wrap:wrap}
/* Specific enough to beat the public page's .dpanel p rules, which set
   a body size and pre-line wrapping that would break the credit across
   three lines. */
.propmap .propmap__credit{
  font-size:.72rem;color:var(--text-subtle);
  line-height:1.5;margin:0;white-space:normal;
}
.propmap .propmap__credit a{color:var(--text-muted);text-decoration:underline}
.propmap .propmap__credit a:hover{color:var(--primary)}

@media(max-width:520px){
  .propmap__frame{aspect-ratio:4/3}
  .propmap__bar{flex-direction:column;align-items:stretch}
  .propmap__actions .btn{flex:1;justify-content:center}
}

/* ════════════════════════════════════════════════════════════
   DATA TABLE — the shared list surface
   Built on .table rather than beside it: twenty-five tables already
   carry that class, so improving it lands everywhere at once instead
   of leaving two table systems to drift apart.
   ════════════════════════════════════════════════════════════ */

/* The card a list sits in. The table meets the card edge-to-edge — a table
   inset inside a padded card draws two nested rectangles and wastes the row
   width that long titles need. */
.table-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.table-card > .table-wrap{border:none;border-radius:0}

/* The strip above a table: the count on the left, bulk or table-level
   controls on the right. */
.table-head{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--border);
  background:var(--surface);flex-wrap:wrap;
}
.table-head__title{font-size:var(--fs-sm);font-weight:600;color:var(--text)}
.table-head__count{color:var(--text-muted);font-weight:400}
.table-head__note{font-size:var(--fs-2xs);color:var(--text-subtle);display:inline-flex;align-items:center;gap:5px}
.table-foot{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-2) var(--space-5);border-top:1px solid var(--border);
  background:var(--surface);flex-wrap:wrap;
}
.table-foot__note{font-size:var(--fs-xs);color:var(--text-subtle)}
.table-foot .pagination{padding:8px 0}
/* The same foot under a card grid, which has no table-card to sit inside and
   so has to draw its own box. */
.table-foot--standalone{
  border:1px solid var(--border);border-radius:var(--radius);
  margin-top:var(--space-4);
}

/* The header stays put while a long page of rows scrolls under it, so the
   column a figure belongs to is still legible at row 40. */
.table thead th{position:sticky;top:0;z-index:2}

/* Figures line up on the decimal only when the digits are the same width.
   Money, dates and counts all get the tabular set; names deliberately do
   not, because proportional text reads better in a name column. */
.table .num,.table .cell-num{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}
.table .cell-date{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--text-muted)}
.table .cell-tight{width:1%;white-space:nowrap}
.table .cell-actions{width:1%;white-space:nowrap;text-align:right}
.table .cell-strong{font-weight:600;color:var(--text)}
/* A cell that must be allowed to shrink and ellipse rather than force the
   table wider than its column. */
.table .cell-clip{max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Sortable column header. The whole cell is the target, not just the label,
   and the glyph is always present on a sortable column — an arrow that only
   appears on hover is an affordance nobody finds. */
.th-sort{padding:0!important}
.th-sort__link{
  display:flex;align-items:center;gap:6px;width:100%;
  padding:11px 16px;color:inherit;font:inherit;
  text-transform:inherit;letter-spacing:inherit;
  transition:color var(--transition),background var(--transition);
}
.table th.th-sort:first-child .th-sort__link{padding-left:18px}
.table th.th-sort:last-child .th-sort__link{padding-right:18px}
.th-sort__link:hover{color:var(--text);background:var(--surface-3)}
.th-sort__link i{font-size:.7rem;opacity:.45;transition:opacity var(--transition)}
.th-sort__link:hover i{opacity:.8}
.th-sort.is-sorted .th-sort__link{color:var(--primary-ink)}
.th-sort.is-sorted .th-sort__link i{opacity:1}

/* Person cell — avatar, name, optional second line. */
.person{display:inline-flex;align-items:center;gap:10px;min-width:0;color:inherit}
.person__avatar{
  width:32px;height:32px;border-radius:50%;object-fit:cover;flex-shrink:0;
  border:1px solid var(--border);background:var(--surface-3);
}
.person__text{display:flex;flex-direction:column;min-width:0;line-height:1.35}
.person__name{font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.person__meta{font-size:var(--fs-2xs);color:var(--text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
a.person:hover .person__name{color:var(--primary)}


/* An applied filter, shown as a removable chip so what is narrowing the
   list is visible without reopening the filter controls. */
.filter-chips{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)}
.filter-chips__label{font-size:var(--fs-xs);color:var(--text-subtle)}
.filter-chip{
  display:inline-flex;align-items:center;gap:6px;
  padding:4px 6px 4px 10px;border-radius:var(--radius-pill);
  background:var(--primary-tint);border:1px solid var(--primary-border);
  color:var(--primary-ink);font-size:var(--fs-2xs);font-weight:600;
}
.filter-chip__key{opacity:.7;font-weight:500}
.filter-chip__x{
  display:grid;place-items:center;width:16px;height:16px;border-radius:50%;
  background:transparent;border:0;color:inherit;cursor:pointer;font-size:.62rem;
  transition:background var(--transition);
}
.filter-chip__x:hover{background:var(--primary-light)}

/* Segmented control — the table/grid view switch. */
.segmented{display:inline-flex;background:var(--surface-3);border-radius:var(--radius-sm);padding:3px;gap:2px}
.segmented__btn{
  display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 11px;
  border:0;background:transparent;border-radius:var(--radius-xs);
  color:var(--text-muted);font-size:var(--fs-xs);font-weight:600;cursor:pointer;
  transition:background var(--transition),color var(--transition),box-shadow var(--transition);
}
.segmented__btn:hover{color:var(--text);text-decoration:none}
.segmented__btn.is-active{background:var(--surface);color:var(--text);box-shadow:var(--shadow-xs)}
/* The control is also used as a row of links (the report period switch), where
   it must not pick up the underline and brand colour a bare <a> would. */
a.segmented__btn{text-decoration:none;color:var(--text-muted)}
a.segmented__btn.is-active{color:var(--text)}

/* A checksum or long token. It has to be able to break anywhere, because it
   has no word boundaries and would otherwise push the card wider than the
   column. */
.hash{
  font-size:.68rem;overflow-wrap:anywhere;word-break:normal;
  color:var(--text-muted);
}

/* A small colour swatch standing in for a chart legend key. */
.dot{
  display:inline-block;width:9px;height:9px;border-radius:50%;
  margin-right:7px;flex-shrink:0;vertical-align:baseline;
}

/* ════════════════════════════════════════════════════════════
   FEED — a list of short messages, not a table

   A notification is a sentence with a time on it. Ruling it into columns
   makes it harder to read, so it gets a stack instead: glyph, prose, time.
   Unread carries a marker down the left edge as well as a tinted ground, so
   it survives being read by someone who cannot separate the two tints.
   ════════════════════════════════════════════════════════════ */
.feed{list-style:none;margin:0;padding:0}
.feed__item{
  display:flex;align-items:flex-start;gap:var(--space-3);
  padding:var(--space-4) var(--space-5);
  border-bottom:1px solid var(--border);
  border-left:3px solid transparent;
  transition:background var(--transition);
}
.feed__item:last-child{border-bottom:0}
.feed__item:hover{background:var(--surface-2)}
.feed__item.is-unread{background:var(--primary-tint);border-left-color:var(--primary)}
.feed__icon{
  display:grid;place-items:center;flex-shrink:0;
  width:34px;height:34px;border-radius:var(--radius-sm);
  background:var(--surface);border:1px solid var(--border);
  font-size:.95rem;
}
.feed__icon--success{color:var(--success)}
.feed__icon--warning{color:var(--warning)}
.feed__icon--danger{color:var(--danger)}
.feed__icon--info{color:var(--info)}
.feed__body{flex:1;min-width:0}
.feed__title{font-size:var(--fs-sm);font-weight:600;color:var(--text);line-height:1.4}
.feed__title a{color:inherit}
.feed__text{font-size:var(--fs-xs);color:var(--text-body);margin:3px 0 0;line-height:1.55}
.feed__meta{font-size:var(--fs-2xs);color:var(--text-subtle);margin-top:6px;font-variant-numeric:tabular-nums}
.feed__action{flex-shrink:0}
@media(max-width:600px){
  .feed__item{padding:var(--space-3) var(--space-4);gap:var(--space-2)}
  .feed__icon{width:28px;height:28px}
}

/* A toolbar carrying a note and an action rather than a search form. */
.toolbar--plain{margin-bottom:var(--space-3)}
.toolbar__note{font-size:var(--fs-xs);color:var(--text-muted);margin:0}

/* ── Permission matrix ─────────────────────────────────────
   A grid of ticks is only readable if the row label stays put while the
   columns scroll, and if a filled cell is distinguishable from an empty one
   by shape as well as colour. */
.table--matrix th[scope="row"]{
  position:sticky;left:0;z-index:1;background:var(--surface);
  text-align:left;font-weight:600;color:var(--text);
  border-right:1px solid var(--border);
}
.table--matrix .cell-center{text-align:center;width:1%;white-space:nowrap}
.table--matrix thead th.cell-center{text-align:center}
.matrix__group th{
  background:var(--surface-3);color:var(--text-muted);
  font-size:var(--fs-2xs);font-weight:700;
  text-transform:uppercase;letter-spacing:.7px;
  padding:8px 18px;position:sticky;left:0;
}
.matrix__row{font-size:var(--fs-xs)}
.matrix__yes{color:var(--success);font-size:1rem}
.matrix__no{color:var(--border-strong);font-size:1rem}
/* The stat strip on a page that has no record header above it. */
.detail-stats--standalone{
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius-lg);padding:var(--space-4) var(--space-5);
  margin-bottom:var(--space-4);
}

/* A star rating in a table row. The public site has three variants of this
   already; this is the one the admin tables use. The count is also stated in
   the aria-label, because five glyphs are not a number to a screen reader. */
.stars{display:inline-flex;gap:2px;color:var(--gold);font-size:.82rem;white-space:nowrap}

/* A warning that is also the repair. Used where a row reports a broken link
   between two records and the button beside it mends the link — so it has to
   read as a problem and behave as a control at once. */
.linkfix{
  display:inline-flex;align-items:center;gap:6px;
  padding:4px 10px;border-radius:var(--radius-pill);
  background:var(--warning-bg);border:1px dashed var(--warning);
  color:var(--warning);font-size:var(--fs-2xs);font-weight:600;
  cursor:pointer;text-align:left;line-height:1.4;
  transition:background var(--transition),color var(--transition);
}
.linkfix:hover{background:var(--warning);color:#fff;border-style:solid}
.linkfix:focus-visible{outline:2px solid var(--warning);outline-offset:2px}

/* A file in a table row: type glyph, name, and what it is underneath.
   The sibling of .person, for the rows that are documents rather than people. */
.filecell{display:flex;align-items:flex-start;gap:10px;min-width:0}
.filecell__icon{font-size:1.3rem;color:var(--text-subtle);line-height:1.15;flex-shrink:0}
.filecell__body{display:flex;flex-direction:column;gap:2px;min-width:0}

/* ════════════════════════════════════════════════════════════
   STATUS TABS — the primary axis of an operations list

   Status is the first question asked of a queue: what is still open, what
   is overdue, what is done. Buried in a <select> it takes three actions to
   answer and shows no quantities; as a row of pills it is one click and the
   counts are readable before anything is clicked.

   Plain links, so this works with scripting off, is bookmarkable, and the
   back button behaves.
   ════════════════════════════════════════════════════════════ */
.status-tabs{
  display:flex;align-items:center;gap:var(--space-1);
  margin-bottom:var(--space-4);padding-bottom:2px;
  overflow-x:auto;scrollbar-width:thin;
}
.status-tabs__item{
  display:inline-flex;align-items:center;gap:7px;
  height:34px;padding:0 var(--space-3);flex-shrink:0;
  border:1px solid transparent;border-radius:var(--radius-pill);
  font-size:var(--fs-xs);font-weight:600;color:var(--text-muted);
  text-decoration:none;white-space:nowrap;
  transition:background var(--transition),color var(--transition),border-color var(--transition);
}
.status-tabs__item:hover{background:var(--surface-3);color:var(--text);text-decoration:none}
.status-tabs__item.is-active{
  background:var(--primary-tint);border-color:var(--primary-border);color:var(--primary-ink);
}
.status-tabs__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex-shrink:0;opacity:.75}
.status-tabs__count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:20px;height:19px;padding:0 6px;border-radius:var(--radius-pill);
  background:var(--surface-3);color:var(--text-muted);
  font-size:var(--fs-2xs);font-weight:700;font-variant-numeric:tabular-nums;
}
.status-tabs__item.is-active .status-tabs__count{background:var(--surface);color:var(--primary-ink)}
/* Tone comes from the same status map the pills use, so "overdue" is the
   same red in the filter as it is in the row it filters to. */
.status-tabs__item--success .status-tabs__dot{color:var(--success)}
.status-tabs__item--warning .status-tabs__dot{color:var(--warning)}
.status-tabs__item--danger  .status-tabs__dot{color:var(--danger)}
.status-tabs__item--info    .status-tabs__dot{color:var(--info)}
.status-tabs__item--primary .status-tabs__dot{color:var(--primary)}
.status-tabs__item--purple  .status-tabs__dot{color:var(--purple)}
.status-tabs__item--orange  .status-tabs__dot{color:var(--orange)}
.status-tabs__item--muted   .status-tabs__dot{color:var(--text-subtle)}

/* ════════════════════════════════════════════════════════════
   LEDGER — money per status, and the filter for it

   The card is the control. Reading "Overdue · 4,800 · 3 payments" and then
   having to find a separate dropdown to see which three is a question asked
   twice; here the answer is one click on the figure itself.
   ════════════════════════════════════════════════════════════ */
.ledger{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));
  gap:var(--space-3);margin-bottom:var(--space-4);
}
.ledger__card{
  display:flex;flex-direction:column;gap:2px;
  padding:var(--space-3) var(--space-4);
  background:var(--surface);border:1px solid var(--border);
  border-left:3px solid var(--border-strong);
  border-radius:var(--radius);text-decoration:none;
  transition:border-color var(--transition),box-shadow var(--transition),transform var(--transition);
}
.ledger__card:hover{box-shadow:var(--shadow-sm);text-decoration:none;transform:translateY(-1px)}
.ledger__card.is-active{border-color:var(--primary);box-shadow:var(--shadow-sm)}
.ledger__label{
  display:flex;align-items:center;gap:6px;
  font-size:var(--fs-2xs);font-weight:600;text-transform:uppercase;letter-spacing:.5px;
  color:var(--text-muted);
}
.ledger__dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex-shrink:0}
.ledger__value{
  font-size:1.22rem;font-weight:700;color:var(--text);
  font-variant-numeric:tabular-nums;letter-spacing:-.2px;line-height:1.25;
}
.ledger__meta{font-size:var(--fs-2xs);color:var(--text-subtle)}
/* The accent is the status tone: the stripe, the dot and the badge in the
   rows below all read from one map. */
.ledger__card--success{border-left-color:var(--success)}
.ledger__card--success .ledger__label{color:var(--success)}
.ledger__card--warning{border-left-color:var(--warning)}
.ledger__card--warning .ledger__label{color:var(--warning)}
.ledger__card--danger{border-left-color:var(--danger)}
.ledger__card--danger .ledger__label{color:var(--danger)}
.ledger__card--info{border-left-color:var(--info)}
.ledger__card--info .ledger__label{color:var(--info)}
.ledger__card--primary{border-left-color:var(--primary)}
.ledger__card--primary .ledger__label{color:var(--primary)}
.ledger__card--purple{border-left-color:var(--purple)}
.ledger__card--purple .ledger__label{color:var(--purple)}
.ledger__card--orange{border-left-color:var(--orange)}
.ledger__card--orange .ledger__label{color:var(--orange)}
.ledger__card--muted{border-left-color:var(--text-subtle)}

/* ════════════════════════════════════════════════════════════
   ROW MENU — the compact ⋮ action list
   ════════════════════════════════════════════════════════════ */
.row-menu{position:relative;display:inline-block}
.row-menu__trigger{
  width:30px;height:30px;display:grid;place-items:center;
  border:1px solid transparent;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-muted);cursor:pointer;
  transition:background var(--transition),color var(--transition),border-color var(--transition);
}
.row-menu__trigger:hover,.row-menu__trigger[aria-expanded="true"]{
  background:var(--surface-3);color:var(--text);border-color:var(--border);
}
/* Fixed, not absolute, and this is the fix for the clipped row actions.
   The menu sits inside two ancestors that clip: .table-wrap carries
   overflow-x:auto so a wide table can scroll, and .table-card carries
   overflow:hidden so its rounded corners cut the table. An absolutely
   positioned menu is clipped by both — sideways in the first case, and at
   the last row in the second — which is why the ⋮ menus were cut off.

   Fixed positioning takes the menu out of that chain entirely. It costs a
   JS-supplied coordinate, which initRowMenu() computes from the trigger's
   bounding rect on open and keeps in step on scroll and resize. No ancestor
   of a table sets transform, filter or perspective, so nothing re-captures
   it into a containing block. */
.row-menu__list{
  position:fixed;top:0;left:0;z-index:200;min-width:196px;max-width:min(280px,calc(100vw - 24px));
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);box-shadow:var(--shadow-lg);
  padding:5px;
}
.row-menu__list[hidden]{display:none}
.row-menu__form{margin:0}
.row-menu__item{
  display:flex;align-items:center;gap:9px;width:100%;
  padding:8px 10px;border:0;border-radius:var(--radius-xs);
  background:transparent;text-align:left;cursor:pointer;
  font-size:var(--fs-xs);font-weight:500;color:var(--text-body);
  font-family:inherit;transition:background var(--transition),color var(--transition);
}
.row-menu__item i{font-size:.88rem;width:16px;text-align:center;color:var(--text-subtle);flex-shrink:0}
.row-menu__item:hover{background:var(--surface-3);color:var(--text)}
.row-menu__item:hover i{color:var(--primary)}
.row-menu__item--danger{color:var(--danger)}
.row-menu__item--danger i{color:var(--danger)}
.row-menu__item--danger:hover{background:var(--danger-bg);color:var(--danger)}
.row-menu__item--danger:hover i{color:var(--danger)}
.row-menu__divider{height:1px;background:var(--border);margin:5px 0}

/* ════════════════════════════════════════════════════════════
   TOASTS — transient confirmation
   Announced politely and never given focus: a toast that steals focus
   interrupts whatever the user was typing when it arrived.
   ════════════════════════════════════════════════════════════ */
.toast-region{
  position:fixed;right:var(--space-5);bottom:var(--space-5);z-index:400;
  display:flex;flex-direction:column;gap:var(--space-2);
  width:min(360px,calc(100vw - var(--space-8)));pointer-events:none;
}
.toast{
  display:flex;align-items:flex-start;gap:10px;
  padding:12px 14px;border-radius:var(--radius);
  background:var(--surface);border:1px solid var(--border);
  box-shadow:var(--shadow-lg);pointer-events:auto;
  opacity:0;transform:translateY(10px);
  transition:opacity var(--transition),transform var(--transition);
}
.toast.is-in{opacity:1;transform:none}
.toast.is-out{opacity:0;transform:translateY(6px)}
.toast__icon{font-size:1rem;line-height:1.3;flex-shrink:0}
.toast__body{flex:1;min-width:0;font-size:var(--fs-xs);color:var(--text-body);line-height:1.5}
.toast__title{display:block;font-weight:600;color:var(--text);margin-bottom:1px}
.toast__close{
  flex-shrink:0;background:none;border:0;color:var(--text-subtle);
  cursor:pointer;font-size:.75rem;padding:2px;border-radius:var(--radius-xs);
}
.toast__close:hover{color:var(--text)}
/* A hairline of the tone down the leading edge — enough to identify the kind
   of message at a glance without tinting the whole card. */
.toast--success{border-left:3px solid var(--success)}
.toast--success .toast__icon{color:var(--success)}
.toast--danger{border-left:3px solid var(--danger)}
.toast--danger .toast__icon{color:var(--danger)}
.toast--warning{border-left:3px solid var(--warning)}
.toast--warning .toast__icon{color:var(--warning)}
.toast--info{border-left:3px solid var(--info)}
.toast--info .toast__icon{color:var(--info)}
@media(max-width:560px){
  .toast-region{left:var(--space-4);right:var(--space-4);bottom:var(--space-4);width:auto}
}

/* ════════════════════════════════════════════════════════════
   CONFIRMATION DIALOG
   Reuses the .modal shell — same backdrop, focus trap and scroll
   lock — so a confirmation behaves like every other dialog.
   ════════════════════════════════════════════════════════════ */
.confirm__dialog{max-width:440px}
.confirm__body{display:flex;gap:var(--space-4);padding:var(--space-6)}
.confirm__icon{
  width:42px;height:42px;border-radius:var(--radius);flex-shrink:0;
  display:grid;place-items:center;font-size:1.15rem;
  background:var(--danger-bg);color:var(--danger);
}
.confirm--warning .confirm__icon{background:var(--warning-bg);color:var(--warning)}
.confirm--info .confirm__icon{background:var(--info-bg);color:var(--info)}
.confirm__title{font-family:var(--font-display);font-size:1.05rem;font-weight:700;color:var(--text);letter-spacing:-.01em}
.confirm__text{font-size:var(--fs-sm);color:var(--text-body);margin-top:6px;line-height:var(--lh-snug)}
/* The record being acted on, quoted back. A confirmation that does not name
   its target is a dialog people learn to dismiss without reading. */
.confirm__record{
  display:block;margin-top:var(--space-3);padding:8px 11px;
  background:var(--surface-2);border:1px solid var(--border);
  border-radius:var(--radius-sm);font-size:var(--fs-xs);
  font-weight:600;color:var(--text);word-break:break-word;
}
.confirm__footer{justify-content:flex-end}

/* ════════════════════════════════════════════════════════════
   LOADING STATES
   ════════════════════════════════════════════════════════════ */
/* A button mid-submit. The label is kept and dimmed rather than swapped for
   a spinner alone: replacing the text makes the button resize and the row
   jump, and the user loses the word that told them what they clicked. */
.btn.is-loading{position:relative;pointer-events:none;opacity:.75}
.btn.is-loading > *{visibility:hidden}
.btn.is-loading::after{
  content:'';position:absolute;inset:0;margin:auto;
  width:15px;height:15px;border-radius:50%;
  border:2px solid currentColor;border-right-color:transparent;
  animation:spin 700ms linear infinite;
}

/* Reduced motion: the spinner keeps turning, slowly, rather than freezing —
   a frozen spinner reads as a hung request rather than a quiet one. */
@media(prefers-reduced-motion:reduce){
  .btn.is-loading::after{animation-duration:1.6s}
}

/* ════════════════════════════════════════════════════════════
   DETAIL HEADER — the record header on every show page
   ════════════════════════════════════════════════════════════ */
.detail-header{
  display:flex;align-items:flex-start;gap:var(--space-5);
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);padding:var(--space-5);
  margin-bottom:var(--space-5);
}
.detail-header__media{
  width:132px;height:96px;flex-shrink:0;border-radius:var(--radius-sm);
  overflow:hidden;background:var(--surface-3);border:1px solid var(--border);
}
.detail-header__media img{width:100%;height:100%;object-fit:cover}
.detail-header__media--avatar{width:72px;height:72px;border-radius:50%}
.detail-header__body{flex:1;min-width:0}
.detail-header__eyebrow{
  font-size:var(--fs-2xs);font-weight:600;letter-spacing:.6px;
  text-transform:uppercase;color:var(--text-subtle);
}
.detail-header__title{
  font-family:var(--font-display);font-size:1.3rem;font-weight:700;
  color:var(--text);letter-spacing:-.02em;margin:3px 0 0;line-height:var(--lh-tight);
}
.detail-header__meta{
  display:flex;align-items:center;gap:var(--space-4);flex-wrap:wrap;
  margin-top:var(--space-3);font-size:var(--fs-xs);color:var(--text-muted);
}
.detail-header__meta span{display:inline-flex;align-items:center;gap:5px}
.detail-header__meta i{color:var(--text-subtle)}
.detail-header__actions{display:flex;align-items:center;gap:var(--space-2);flex-shrink:0;flex-wrap:wrap}
/* One sentence of context under the title — what this record is, rather than
   another key/value pair. */
.detail-header__lede{
  font-size:var(--fs-xs);color:var(--text-body);
  margin:var(--space-2) 0 0;max-width:70ch;line-height:1.55;
}
/* Inside a tab panel the record header is not the top of the page, so it
   carries less weight and less air. */
.detail-header--compact{
  padding:var(--space-4);margin-bottom:var(--space-4);
  box-shadow:none;
}
.detail-header--compact .detail-header__title{font-size:1.1rem}
@media(max-width:768px){
  .detail-header{flex-direction:column;gap:var(--space-4)}
  .detail-header__actions{width:100%}
}
/* ── Data list ─────────────────────────────────────────────
   Key/value rows: the shape every detail page needs and every one of them
   was rebuilding as a <table> with inline cell padding. A description list
   is what this actually is, so screen readers announce "Status, Available"
   as a pair rather than reading two unrelated table cells.

   The label column is sized in ch rather than a percentage so the values
   line up at the same x on every card regardless of how wide the card is. */
.datalist{display:flex;flex-direction:column}
.datalist__row{
  display:grid;grid-template-columns:minmax(9ch,14ch) 1fr;
  gap:var(--space-4);align-items:baseline;
  padding:9px 0;border-bottom:1px solid var(--border);
}
.datalist__row:last-child{border-bottom:none}
.datalist__row:first-child{padding-top:0}
.datalist dt{font-size:var(--fs-xs);color:var(--text-muted);margin:0}
.datalist dd{
  margin:0;font-size:var(--fs-sm);color:var(--text);
  min-width:0;overflow-wrap:anywhere;
}
.datalist dd strong{font-weight:600}
/* Figures in a data list get the tabular set, so a column of money or dates
   down a detail page lines up the way it does in a table. */
.datalist dd.num{font-variant-numeric:tabular-nums}
@media(max-width:480px){
  /* Below this the two columns leave the value about twelve characters —
     stacking keeps the pairing without squeezing either half. */
  .datalist__row{grid-template-columns:1fr;gap:2px}
}

/* A row of figures under the header — rent, deposit, balance. */
.detail-stats{display:flex;gap:var(--space-6);flex-wrap:wrap;margin-top:var(--space-4);padding-top:var(--space-4);border-top:1px solid var(--border)}
.detail-stat__label{font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:.5px;color:var(--text-subtle)}
.detail-stat__value{font-size:1.05rem;font-weight:700;color:var(--text);font-variant-numeric:tabular-nums;margin-top:2px}
@media(max-width:768px){
  .detail-header{flex-direction:column;gap:var(--space-4)}
  .detail-header__media{width:100%;height:170px}
  .detail-header__actions{width:100%}
  .detail-header__actions .btn{flex:1;justify-content:center}
}

/* The hidden attribute has to beat component display rules. Without this a
   `<div class="form-error" hidden>` still shows, because any explicit display
   overrides the user-agent's [hidden]{display:none}. */
[hidden]{display:none!important}

/* ════════════════════════════════════════════════════════════════════
   STEP 2.1 — TOOLBAR, BUTTONS, LOADING
   ════════════════════════════════════════════════════════════════════ */

/* ── Toolbar ────────────────────────────────────────────────────────
   Three groups on one line: search takes the slack, filters collapse
   into a single button, actions pin right. The previous bar laid every
   <select> out inline, which spent a band of the page on controls that
   mostly read "Any type". */
.toolbar{
  display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;
  margin-bottom:var(--space-3);
}
.toolbar__search{position:relative;flex:1 1 280px;min-width:200px}
.toolbar__search > i{
  position:absolute;left:12px;top:50%;transform:translateY(-50%);
  color:var(--text-subtle);font-size:.9rem;pointer-events:none;
}
.toolbar__search .form-control{padding-left:36px;height:var(--control-h)}
.toolbar__inline-filter{height:var(--control-h);min-width:150px;flex:0 0 auto;font-size:var(--fs-xs)}
.toolbar__apply{height:var(--control-h);width:var(--control-h);padding:0;justify-content:center}
.toolbar__actions{display:flex;align-items:center;gap:var(--space-2);margin-left:auto;flex-shrink:0}

/* The filter popover. <details> so it works without JS: closed it is a
   button, open it is a panel, and either way the controls inside submit
   with the form. */
.toolbar__filters{position:relative;flex-shrink:0}
.toolbar__filter-trigger{
  display:inline-flex;align-items:center;gap:8px;height:var(--control-h);
  padding:0 14px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);
  background:var(--surface);color:var(--text);
  font-size:var(--fs-xs);font-weight:600;cursor:pointer;user-select:none;
  list-style:none;white-space:nowrap;
  transition:border-color var(--transition),background var(--transition),box-shadow var(--transition);
}
/* Safari draws its own disclosure triangle on a summary; this removes it
   without removing the element's semantics. */
.toolbar__filter-trigger::-webkit-details-marker{display:none}
.toolbar__filter-trigger::marker{content:""}
.toolbar__filter-trigger:hover{border-color:var(--text-muted)}
.toolbar__filter-trigger:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.toolbar__filters[open] .toolbar__filter-trigger{
  border-color:var(--primary);box-shadow:var(--ring);
}
.toolbar__filter-chev{font-size:.62rem;color:var(--text-subtle);transition:transform var(--dur-base) var(--ease-standard)}
.toolbar__filters[open] .toolbar__filter-chev{transform:rotate(180deg)}
/* The applied count rides on the button, so the bar says what is narrowing
   the list without spending a row on it. */
.toolbar__filter-count{
  min-width:18px;height:18px;padding:0 5px;border-radius:var(--radius-pill);
  background:var(--primary);color:#fff;
  font-size:.65rem;font-weight:700;line-height:18px;text-align:center;
  font-variant-numeric:tabular-nums;
}

.toolbar__popover{
  position:absolute;top:calc(100% + 6px);left:0;z-index:70;
  width:min(420px,calc(100vw - 32px));
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);box-shadow:var(--shadow-lg);
  padding:var(--space-4);
}
.toolbar__popover-grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
  gap:var(--space-3);
}
.toolbar__field{display:flex;flex-direction:column;gap:5px;min-width:0}
.toolbar__field-label{
  font-size:var(--fs-2xs);font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--text-muted);
}
.toolbar__field .form-control{height:var(--control-h);font-size:var(--fs-xs)}
.toolbar__popover-foot{
  display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2);
  margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--border);
}
/* Right-align the panel when the trigger sits near the right edge. */
.toolbar__filters--end .toolbar__popover{left:auto;right:0}

@media (max-width:768px){
  .toolbar__search{flex:1 1 100%}
  .toolbar__actions{margin-left:0;width:100%}
  .toolbar__actions .btn{flex:1;justify-content:center}
  .toolbar__popover{position:static;width:auto;box-shadow:none;border-color:var(--border);margin-top:6px}
}

/* ── Buttons ────────────────────────────────────────────────────────
   One height, one radius, one type ramp. The primary carries a subtle
   brand shadow so it reads as the page's one dominant action; secondary
   and ghost step down through border and ground rather than colour, so
   a bar of three buttons still has an obvious first. */
.btn{
  height:var(--control-h);padding:0 16px;
  font-size:var(--fs-xs);font-weight:600;letter-spacing:.01em;
  border-radius:var(--radius-sm);
  transition:background var(--transition),border-color var(--transition),
             color var(--transition),box-shadow var(--transition),transform var(--dur-fast) var(--ease-standard);
}
.btn--sm{height:var(--control-h-sm);padding:0 12px;font-size:var(--fs-2xs);gap:6px}
.btn--lg{height:var(--control-h-lg);padding:0 22px;font-size:var(--fs-sm)}
.btn--icon{width:var(--control-h);height:var(--control-h);padding:0}
.btn i{font-size:1em;line-height:1}

.btn--primary{box-shadow:0 1px 2px rgba(0,117,192,.24),0 2px 8px -2px rgba(0,117,192,.3)}
.btn--primary:hover{box-shadow:0 2px 4px rgba(0,117,192,.28),0 6px 16px -4px rgba(0,117,192,.38)}
.btn--primary:active{transform:translateY(1px);box-shadow:0 1px 2px rgba(0,117,192,.24)}

/* Quieter than primary and clearly not it: the base declaration above
   already sets the ground and border; this only changes how it answers
   the pointer — away from the brand tint, which read as a second primary. */
.btn--outline:hover{background:var(--surface-2);border-color:var(--text-muted);color:var(--text)}
.btn--ghost{background:transparent;border-color:transparent;color:var(--text-muted)}
.btn--ghost:hover{background:var(--surface-3);color:var(--text)}

/* Danger is the semantic tone, not a louder button. It reads as serious
   without shouting over the primary beside it. */
.btn--danger:hover{box-shadow:0 2px 4px rgba(200,30,30,.22),0 6px 16px -4px rgba(200,30,30,.3)}
.btn--danger:active{transform:translateY(1px)}

.btn:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.btn--danger:focus-visible{outline-color:var(--danger)}
.btn[disabled],.btn.is-disabled{opacity:.5;pointer-events:none;box-shadow:none}
@media (pointer:coarse){
  .btn{min-height:44px}
  .btn--sm{min-height:40px}
}

/* Form action bars: the primary sits last on the right, which is where a
   pointer already is after filling the form. */
.form-actions{
  display:flex;align-items:center;gap:var(--space-2);justify-content:flex-end;
  margin-top:var(--space-6);padding-top:var(--space-4);border-top:1px solid var(--border);
  flex-wrap:wrap;
}
@media (max-width:520px){
  .form-actions{flex-direction:column-reverse;align-items:stretch}
  .form-actions .btn{width:100%}
}

/* ── Button loading ─────────────────────────────────────────────────
   The label stays in the flow at zero opacity rather than being replaced,
   so the button keeps its exact width and the row does not reflow the
   moment someone commits to it. */
.btn.is-loading{position:relative;pointer-events:none;color:transparent!important}
.btn.is-loading > *{opacity:0}
.btn.is-loading::after{
  content:"";position:absolute;top:50%;left:50%;
  width:15px;height:15px;margin:-7.5px 0 0 -7.5px;
  border:2px solid currentColor;border-top-color:transparent;border-radius:50%;
  color:#fff;
  animation:btnSpin 640ms linear infinite;
}
.btn--outline.is-loading::after,
.btn--ghost.is-loading::after{color:var(--primary)}
@keyframes btnSpin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  /* A spinner is a progress report, so it is not removed — only slowed to
     something that does not strobe. */
  .btn.is-loading::after{animation-duration:1.6s}
  .btn:active{transform:none}
}

/* ── Navigation progress ────────────────────────────────────────────
   A 2px bar across the top of the viewport from the moment a navigation
   is committed until the next document paints. It answers "did that
   register?" on a slow query, which is the only unanswered wait left in
   a server-rendered application.

   It cannot stick: leaving the page destroys it, and the only way it
   stays on screen is if navigation was cancelled — which pagehide and
   the safety timeout in the JS both cover. */
.nav-progress{
  position:fixed;top:0;left:0;height:2px;width:0;z-index:9999;
  background:linear-gradient(90deg,var(--primary),#5ac8fa);
  box-shadow:0 0 8px rgba(0,117,192,.5);
  opacity:0;pointer-events:none;
  transition:width 200ms var(--ease-standard),opacity 160ms var(--ease-standard);
}
.nav-progress.is-active{opacity:1}
@media (prefers-reduced-motion:reduce){
  .nav-progress{transition:opacity 160ms linear}
}

/* ════════════════════════════════════════════════════════════════════
   STEP 3 — DATA SURFACES
   One card rhythm, one pill, one table. Refinement of components that
   already work, applied in the shared layer so no page needs an
   override of its own.
   ════════════════════════════════════════════════════════════════════ */

/* ── One pill ───────────────────────────────────────────────────────
   .badge and .status were two pills doing one job: different padding,
   different type sizes, and tone colours written as literal rgba that no
   longer matched the status tokens. Step 3 folded the definitions
   together rather than edit the sixteen call sites; the last of those
   went with the detail pages, so the alias is gone too and uiStatus() is
   now the only way a status is drawn. */
.status{
  display:inline-flex;align-items:center;gap:6px;
  padding:3px 10px;border-radius:var(--radius-pill);
  font-size:var(--fs-2xs);font-weight:600;line-height:1.55;
  white-space:nowrap;border:1px solid transparent;
}
.status:has(.status__dot){padding-left:8px}
.status__dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}

/* One tone map. Every colour reads a token, so a status is the same green
   in a table cell, a chart legend and a detail header. */
.status--success{background:var(--success-bg);color:var(--success);border-color:color-mix(in srgb,var(--success) 20%,transparent)}
.status--warning{background:var(--warning-bg);color:var(--warning);border-color:color-mix(in srgb,var(--warning) 20%,transparent)}
.status--danger {background:var(--danger-bg); color:var(--danger); border-color:color-mix(in srgb,var(--danger) 20%,transparent)}
.status--info   {background:var(--info-bg);   color:var(--info);   border-color:color-mix(in srgb,var(--info) 20%,transparent)}
.status--primary{background:var(--primary-light);color:var(--primary-ink);border-color:var(--primary-border)}
.status--purple {background:var(--purple-bg); color:var(--purple); border-color:color-mix(in srgb,var(--purple) 20%,transparent)}
.status--orange {background:var(--orange-bg); color:var(--orange); border-color:color-mix(in srgb,var(--orange) 20%,transparent)}
.status--muted  {background:var(--surface-3); color:var(--text-muted);border-color:var(--border-strong)}

/* ── One card rhythm ────────────────────────────────────────────────
   Six card shapes had six paddings and two radii: .card at 12px radius
   with 16/18 padding, .table-card and .prop-card at 8px, .mini-stat at
   14/16, .stat-card at 13/15, .ledger__card on the spacing scale. They
   are the same object at different sizes, so they now share one radius
   and one inset, both from tokens. */
.card,
.table-card,
.prop-card,
.mini-stat,
.ledger__card,
.detail-header,
.detail-stats--standalone{
  border-radius:var(--card-radius);
}
.card__header{padding:var(--card-pad-y) var(--card-pad-x)}
.card__body{padding:var(--card-pad-x)}
.card__footer{padding:var(--card-pad-y) var(--card-pad-x)}
.mini-stat{padding:var(--card-pad-y) var(--card-pad-x)}

/* The card heading now reads from the scale rather than a literal .98rem,
   which puts it a clear step below the 28px page title instead of within
   a hair of it. */
.card__title{font-size:var(--fs-title)}
.card__subtitle{font-size:var(--fs-2xs);margin-top:3px}

/* A card body that holds a table has no inset of its own — the table
   draws its own cell padding, and doubling them was leaving a ragged
   white margin inside the border on every register. */
.card__body--flush,
.table-card > .table-wrap{padding:0}

/* ── Table ──────────────────────────────────────────────────────────
   The table was already right: sticky header, hairline rules, tabular
   figures, no zebra. What it lacked was a state for the keyboard and a
   consistent answer for numeric columns. */

/* A focused row. Tabbing through a register moved focus into cells with
   no visible change at all — the only feedback was the link's own ring,
   which on a wide row is easy to lose. The whole row now answers. */
.table tbody tr:focus-within{background:var(--primary-tint)}
.table tbody tr:focus-within td{border-color:var(--primary-border)}
/* :has() lets the row carry the state; where it is unsupported the row
   simply keeps the hover treatment, which is still legible. */
.table tbody tr:has(:focus-visible) td:first-child{box-shadow:inset 3px 0 0 var(--primary)}

/* Numeric and date columns align right and share one figure width, so a
   column of money reads as a column rather than a ragged edge. Declared
   once here rather than per-page. */
.table .cell-num,.table th.cell-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.table .cell-date{font-variant-numeric:tabular-nums;white-space:nowrap}
/* The actions column takes only the width its control needs, and is the
   one column that never wraps. */
.table .cell-actions{width:1%;white-space:nowrap;text-align:right}
/* Long free text truncates rather than pushing the row wider than the
   viewport — the alternative is a page that scrolls sideways because one
   description was verbose. */
.table .cell-clip{max-width:320px;overflow:hidden;text-overflow:ellipsis}

/* The sticky header needs its own ground: the default is translucent
   against the scrolled rows passing under it. */
.table thead th{position:sticky;top:0;z-index:2;background:var(--surface-2)}

/* ── Card grid rhythm ───────────────────────────────────────────────
   .stats and .grid-auto were on different gaps (11-12px vs --space-4),
   so a KPI row and a card grid on the same page sat to different rules. */
.stats{gap:var(--space-3)}
.grid-auto{gap:var(--space-3)}
.mini-stats{gap:var(--space-3);margin-bottom:var(--space-4)}

/* ════════════════════════════════════════════════════════════════════
   STEP 4 — DETAIL PAGES
   The shapes the show pages were each inventing for themselves.
   ════════════════════════════════════════════════════════════════════ */

/* A file at detail size. The register's .filecell with room to breathe and
   an action pinned to the right, so a document looks like the same object
   in a table row and on its own page. */
.filecell--lg{align-items:center;gap:var(--space-4)}
.filecell--lg .filecell__icon{font-size:2.2rem}
.filecell--lg .filecell__body{flex:1}
.filecell--lg .btn{flex-shrink:0}

/* The bar above a printable document. Not .form-actions: nothing here is
   submitting anything, and it must disappear on paper. */
.receipt-actions{
  display:flex;justify-content:flex-end;gap:var(--space-2);
  margin-bottom:var(--space-4);
}

/* ── Receipt ────────────────────────────────────────────────────────
   Was twenty-one inline styles doing what these classes now do. The
   figures column is tabular so the amounts line up as a column, which
   is the whole reason a receipt is laid out as a table. */
.receipt__meta-label{font-weight:700;letter-spacing:.08em;text-transform:uppercase;font-size:var(--fs-2xs)}
.receipt__meta-code{font-family:var(--font-mono);font-size:var(--fs-xs);margin-top:2px}
.receipt__meta-date{color:var(--text-muted);font-size:var(--fs-2xs);margin-top:2px}

/* ── Conversation thread ────────────────────────────────────────────
   An enquiry and its replies. An ordered list because the order is the
   meaning: this is a correspondence, and a screen reader should be told
   how many messages there are and which one it is on. The messages sit
   on the page ground rather than the card's, so each one reads as a
   separate utterance inside a single record. */
.thread{list-style:none;display:flex;flex-direction:column;gap:var(--space-3);margin:0;padding:0}
.thread__msg{
  padding:var(--space-3) var(--space-4);
  background:var(--bg);border-radius:var(--radius-sm);
}
.thread__head{
  display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-3);
  font-size:var(--fs-xs);color:var(--text-muted);
}
.thread__who{font-weight:600;color:var(--text)}
.thread__when{white-space:nowrap;font-variant-numeric:tabular-nums}
.thread__body{margin-top:6px;font-size:var(--fs-sm);color:var(--text-body);overflow-wrap:anywhere}

@media (max-width:480px){
  /* Name over timestamp rather than a squeezed row: the name is what is
     scanned, and it should not be truncated to keep a date on the line. */
  .thread__head{flex-direction:column;align-items:flex-start;gap:2px}
}

.receipt__party{margin-bottom:var(--space-5)}
.receipt__party-label{
  font-size:var(--fs-2xs);color:var(--text-muted);
  text-transform:uppercase;letter-spacing:.06em;font-weight:600;
}
.receipt__party-name{font-size:1.05rem;font-weight:600;margin-top:4px;color:var(--text)}
.receipt__party-meta{font-size:var(--fs-xs);color:var(--text-muted)}

.receipt__amount{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
/* A running subtotal: separated from the lines above it but not the final
   figure, which keeps its own heavier rule. */
.receipt__row--sub td{font-weight:600}
.receipt__notes{margin-top:var(--space-4);font-size:var(--fs-xs);color:var(--text-body)}

.receipt__sign{
  display:grid;grid-template-columns:1fr 1fr;gap:var(--space-8);
  margin-top:var(--space-10);font-size:var(--fs-2xs);color:var(--text-muted);
}
.receipt__sign-block--end{text-align:right}
.receipt__sign-rule{border-top:1px solid var(--text);padding-top:6px}
.receipt__sign-block--end .receipt__sign-rule{display:inline-block;min-width:180px}
.receipt__sign-name{margin-top:4px;color:var(--text)}

@media (max-width:560px){
  .receipt{padding:var(--space-5)}
  .receipt__head{flex-direction:column;gap:var(--space-3)}
  .receipt__meta{text-align:left}
  .receipt__sign{grid-template-columns:1fr;gap:var(--space-6)}
  .receipt__sign-block--end{text-align:left}
}

/* ── Print ──────────────────────────────────────────────────────────
   The existing block already hid the shell. These are the parts it did
   not cover, and they are what separates a page that happens to print
   from a document. */
@media print{
  /* The browser drops backgrounds and borders by default to save ink,
     which takes the receipt's rules with them — and those rules are what
     make it read as a receipt rather than a list. */
  .receipt,.receipt *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  .receipt{padding:0}
  .receipt__head{border-bottom-width:2px}
  /* A signature panel split across two sheets is a signature nobody can
     use, and an amount separated from its label is worse. */
  .receipt__sign,.receipt__table tr{break-inside:avoid;page-break-inside:avoid}
  .receipt__notes{break-inside:avoid}
  /* The page's own margin, so the receipt does not sit against the edge
     of the paper. */
  @page{margin:16mm}
  /* Anything interactive is meaningless on paper. */
  .btn,.row-menu,.toolbar,.page-header__actions,.nav-progress,
  .tabs,.pagination{display:none!important}
  /* Links print their target when the reader cannot click them — but only
     real ones, not every in-page anchor. */
  .receipt a[href^="http"]::after{content:" (" attr(href) ")";font-size:.85em;color:#555}
}

/* ── Step 4 utilities ───────────────────────────────────────────────
   The handful of shapes the detail pages kept re-declaring inline. Each
   one earned a class by appearing in three or more places; a one-off
   stays inline rather than inventing a utility nobody else will use. */

/* A form that is only a wrapper around one button, sitting in a row of
   other buttons. `display:contents` rather than `inline`, so the button
   participates in the parent's flex layout instead of the form becoming
   a stray inline box that breaks the gap. */
.inline-form{display:contents}

/* A record that is switched off — an inactive category, a disabled row.
   Dimmed rather than hidden, because it is still a record. */
.is-muted-row{opacity:.55}
.is-muted-row:hover{opacity:1}

/* A narrow single-column page: a short form, a preview. */
.measure-sm{max-width:520px;margin-inline:auto}
.measure-md{max-width:700px;margin-inline:auto}

/* ════════════════════════════════════════════════════════════════════
   STEP 5 — FORMS, MODALS, FEEDBACK
   ════════════════════════════════════════════════════════════════════ */

/* ── Form rhythm ────────────────────────────────────────────────────
   The pieces of a form were spaced by four different literals — 18px
   between groups, 14px inside a grid, --space-5 under a check row —
   so two forms built from the same partials sat to different rules.
   One scale now, and the gaps step with meaning: fields inside a group
   are closer than groups, groups closer than sections. */
.form-group{margin-bottom:var(--space-4)}
.form-group:last-child{margin-bottom:0}
.form-row,
.form-grid,
.form-grid--2,
.form-grid--3{gap:var(--space-4)}
.check-row{margin-bottom:var(--space-4)}

/* ── Sticky form actions ────────────────────────────────────────────
   Applied by JS, and only to a form genuinely taller than the viewport:
   Save pinned to the foot of a form that already fits is a bar taking
   space for no reason. The class is what makes it opt-in rather than a
   blanket rule.

   It sits inside the card, so the card's own inset is cancelled and
   re-applied — otherwise the bar would float in the middle of the
   padding with the card's white showing beneath it. */
.form-actions--sticky{
  position:sticky;bottom:0;z-index:5;
  margin:var(--space-6) calc(var(--card-pad-x) * -1) calc(var(--card-pad-x) * -1);
  padding:var(--space-3) var(--card-pad-x);
  background:var(--surface);
  border-top:1px solid var(--border);
  /* The card is rounded and does not clip, so a square bar would show its
     corners poking past the card's. It takes the card's own radius on the
     two corners it shares. */
  border-radius:0 0 var(--card-radius) var(--card-radius);
  box-shadow:0 -4px 16px -8px rgba(29,51,64,.18);
  transition:box-shadow var(--dur-base) var(--ease-standard);
}
/* Once the bar reaches the bottom of the form there is nothing below it to
   separate from, so the lift goes away and it settles into the card. */
.form-actions--sticky:not(.is-pinned){box-shadow:none}
@media (max-width:520px){
  /* The narrow-screen rule above stacks form buttons into a column, which is
     right for a bar sitting at the end of a form and wrong for one pinned to
     the screen: stacked, it would hold a sixth of a phone display for as long
     as the form is open. Pinned, the two stay side by side. */
  .form-actions--sticky{flex-direction:row;align-items:center}
  .form-actions--sticky .btn{width:auto;flex:1}
}
/* A modal scrolls its own body, so a sticky bar inside one would pin to
   the wrong box. The modal already has a footer for this. */
.modal .form-actions--sticky{position:static;margin:0;box-shadow:none}

/* ── Modal ──────────────────────────────────────────────────────────
   The dialog grows from where it was opened rather than from the middle
   of the screen. --modal-origin-x/y are written by JS from the trigger's
   position; the fallback is the centre, which is what a modal opened by
   the keyboard or by a redirect should do. */
.modal__dialog{
  transform-origin:var(--modal-origin-x,50%) var(--modal-origin-y,50%);
}

/* ── Toast ──────────────────────────────────────────────────────────
   The bar is the dismiss timer made visible. Without it a toast that
   vanishes reads as something the page did, not something on a clock —
   and there is no way to tell how long is left to read it. */
.toast{position:relative;overflow:hidden}
.toast__progress{
  position:absolute;left:0;bottom:0;height:2px;width:100%;
  background:currentColor;opacity:.35;
  transform-origin:left center;
  animation:toastCountdown linear forwards;
}
@keyframes toastCountdown{from{transform:scaleX(1)}to{transform:scaleX(0)}}
.toast--success .toast__progress{color:var(--success)}
.toast--danger  .toast__progress{color:var(--danger)}
.toast--warning .toast__progress{color:var(--warning)}
.toast--info    .toast__progress{color:var(--info)}
/* Hovering or tabbing into a toast stops the clock — reading it should
   never be a race. */
.toast:hover .toast__progress,
.toast:focus-within .toast__progress{animation-play-state:paused}

@media (prefers-reduced-motion:reduce){
  /* The bar still reports how much time is left — removing it would remove
     the information — but it drops in ten visible steps instead of sweeping
     continuously. step-end on its own would hold at full and jump to empty,
     which reports nothing at all.

     The dialog is not touched here: the rule at the top of this file already
     takes its transition away entirely, and re-declaring anything for it
     from further down would quietly hand the motion back. */
  .toast__progress{animation-timing-function:steps(10,end)}
}

/* ════════════════════════════════════════════════════════════════════
   STEP 6 — TABLE WIDTH AND OVERFLOW
   ════════════════════════════════════════════════════════════════════ */

/* ── Why this block exists ──────────────────────────────────────────
   Measured across the fifteen list tables before writing any of it:
   two scrolled sideways at 1440, seven at 1280, and fourteen at 1024.
   A horizontal scrollbar under every table is not a table problem to be
   solved fifteen times — it is one shared-layer problem, and three
   things were causing it.

   1. Text cells had no upper bound. .person__name is nowrap with an
      ellipsis, but an ellipsis needs a width to trigger against, and a
      table using automatic layout just grows the column to fit the
      longest value on the page. One long email address was setting the
      width of that column for every row under it.
   2. Nothing ever gave way. Every column was equally important, so the
      only thing left that could yield was the viewport.
   3. When scrolling was genuinely unavoidable, it was a raw OS
      scrollbar with no indication that there was anything to scroll to,
      and the identity column slid out of view the moment you used it —
      leaving rows you could read but not identify.
   ─────────────────────────────────────────────────────────────────── */

/* An upper bound on a text column. Auto table layout treats this as the
   column's ceiling, which is what finally lets the ellipsis on a person
   cell do its job. Deliberately not applied to the columns whose width
   is already their content: money, dates, status and the action menu are
   short and fixed, and capping them would only wrap them. */
.table td{max-width:var(--cell-max,220px)}
.table .cell-actions,
.table .cell-tight,
.table .cell-num,
.table .cell-date{max-width:none}

/* Long single words — an email, a reference, a URL someone pasted into a
   note — break rather than push the column past the cap. */
.table td{overflow-wrap:anywhere}
/* …except where the value is a unit that must not be split across lines. */
.table .table__id,
.table .cell-num,
.table .cell-date,
.table .status,
.table .status{overflow-wrap:normal}

/* The person cell fills the capped cell and ellipsises inside it, rather
   than setting the cell's width from its longest name. */
.table .person{max-width:100%}

/* ── Column priority ────────────────────────────────────────────────
   Which column yields first, declared once. The actual hiding lives in
   responsive.css so it wins on source order; these are the semantics.

     .col-lo   Reference codes, IP addresses, "last updated" — data that
               is on the record's own page and is not what anyone scans
               the list by. First to go.
     .col-mid  Secondary identity — the assigned agent, a linked record,
               a category already implied by the row. Goes next.

   Unclassed columns are essential and never hide at any width: who or
   what the row is, the money, the status, and the actions.
 */
/* The same two tiers, decided per table rather than per viewport.

   A viewport breakpoint can only guess at how much room a table has. It
   cannot know that the rail is pinned, or that this is the one table in
   the application that needs 1216px of the 1127 a 1440 window gave it,
   or — the commoner case — that a table it just stripped to four columns
   had room for six. So the media rules above stand as the pre-scripting
   baseline, and the measurement picks the real answer: the most columns
   this table can show in the width it actually has.

   Three states, most columns first. Higher specificity than the media
   rules on purpose, in both directions: this has to be able to hide a
   column the viewport kept and restore one the viewport hid. */
.table-card.is-cols-all .table .col-lo,
.table-card.is-cols-all .table .col-mid,
.table-card.is-cols-mid .table .col-mid{display:table-cell}
.table-card.is-cols-mid .table .col-lo,
.table-card.is-cols-min .table .col-lo,
.table-card.is-cols-min .table .col-mid{display:none}

/* ── Scrolling, when it is genuinely needed ─────────────────────────
   Below the tablet breakpoint a seven-column table cannot be made to fit
   a phone by hiding columns — there is no honest arrangement of that
   much data in 349px. So the wrap scrolls, and the job here is to make
   that read as a considered affordance instead of a raw OS scrollbar. */

/* The same faint-at-rest treatment the sidebar's own scroll uses, so the
   application has one scrollbar idiom rather than two. */
.table-wrap{
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
  /* Momentum on touch, and the sticky column below needs a positioned
     ancestor that is also the scroll container. */
  -webkit-overflow-scrolling:touch;
}
.table-wrap::-webkit-scrollbar{height:8px}
.table-wrap::-webkit-scrollbar-track{background:transparent}
.table-wrap::-webkit-scrollbar-thumb{
  background:var(--border-strong);border-radius:4px;
  border:2px solid var(--surface);
  transition:background var(--dur-base) var(--ease-standard);
}
.table-wrap:hover::-webkit-scrollbar-thumb{background:var(--text-subtle)}

/* Edge shadows: a soft inner edge on whichever side has content out of
   view. This is what tells someone there is more table before they find
   the scrollbar — and it disappears at each end, so it is also the cue
   that they have reached it. Toggled by JS from scrollLeft, because CSS
   cannot ask how far a box is scrolled. */
.table-wrap{position:relative}
.table-card{position:relative}
.table-card::before,
.table-card::after{
  content:'';position:absolute;top:0;bottom:0;width:28px;
  pointer-events:none;z-index:3;opacity:0;
  transition:opacity var(--dur-base) var(--ease-standard);
}
.table-card::before{
  left:0;
  background:linear-gradient(to right,rgba(15,23,42,.13),transparent);
}
.table-card::after{
  right:0;
  background:linear-gradient(to left,rgba(15,23,42,.13),transparent);
}
.table-card.is-scroll-start::before{opacity:1}
.table-card.is-scroll-end::after{opacity:1}

/* The identity column stays put while the rest slides under it. Scrolling
   a table sideways and losing track of whose row you are reading is the
   thing that makes a scrolling table feel broken; this is the fix.
   Applied only while the table actually overflows — pinned on a table
   that fits, the column would carry a divider for no reason. */
.table-card.is-scrollable .table tbody td:first-child,
.table-card.is-scrollable .table thead th:first-child{
  position:sticky;left:0;z-index:1;
  background:var(--surface);
}
.table-card.is-scrollable .table thead th:first-child{
  z-index:4;                      /* over the sticky header and the body */
  background:var(--surface-2);
}
/* A hairline that only shows once the column is actually holding content
   back, so a table scrolled to the far left looks untouched. */
.table-card.is-scrollable.is-scroll-start .table tbody td:first-child,
.table-card.is-scrollable.is-scroll-start .table thead th:first-child{
  box-shadow:1px 0 0 var(--border);
}
/* Row hover has to reach the pinned cell too, or the frozen column stays
   white while the rest of its row highlights. */
.table-card.is-scrollable .table tbody tr:hover td:first-child{background:var(--surface-2)}

@media (prefers-reduced-motion:reduce){
  .table-card::before,.table-card::after{transition:none}
}

/* ── Phone field ────────────────────────────────────────────────────
   A country selector welded to the number box, reading as one control:
   two boxes side by side would look like two questions, and a phone
   number is one. The selector takes only the width its dial code needs
   so the number — the part people actually check — keeps the room.

   The pair is validated against the selected country's own digit
   lengths rather than one hard-coded length, which is the whole reason
   the selector exists. */
.phone-field{display:flex;align-items:stretch;min-width:0}

/* A phone needs more room than a text box: the picker takes a fixed slice
   before the number gets any. In a two-up form grid at tablet widths that
   left the number about eleven characters wide — not enough to read back a
   nine-digit number you had just typed. It takes the whole row instead;
   :has() is the same mechanism the stat cards and the status pill already
   use to react to their own contents. */
.form-grid--2 > .form-group:has(.phone-field),
.form-grid--3 > .form-group:has(.phone-field){grid-column:1 / -1}

/* The number is the field being filled in, so it takes the row and the
   country takes only what it needs. The old control put a select wide
   enough to spell out "Boqortooyada (+44)" in front of it and left the
   number whatever remained, which in a two-column form was not enough to
   see a nine-digit number. */
.phone-field__number{
  flex:1 1 auto;min-width:8ch;
  border-top-left-radius:0;border-bottom-left-radius:0;
}

/* Unenhanced — no scripting — the select is the control, and it is capped
   so it can still never take the row from the number. */
.phone-field__select{
  flex:0 0 auto;width:auto;max-width:9.5rem;min-width:0;
  border-top-right-radius:0;border-bottom-right-radius:0;
  border-right-color:transparent;
  background-color:var(--surface-2);
  font-size:var(--fs-xs);
}
/* Enhanced, the select still posts but the trigger is what is seen. */
.phone-field--enhanced .phone-field__select{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}

.phone-field__trigger{
  flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;
  height:var(--control-h);padding:0 10px;
  border:1px solid var(--border);border-right-color:transparent;
  border-radius:var(--radius-sm) 0 0 var(--radius-sm);
  background:var(--surface-2);color:var(--text);
  font-family:inherit;font-size:var(--fs-xs);font-weight:600;
  white-space:nowrap;cursor:pointer;
  transition:background var(--transition),border-color var(--transition);
}
.phone-field__trigger:hover{background:var(--surface-3)}
/* The flags are 4:3 SVGs, so a fixed pair of dimensions keeps every one of
   them the same size and stops the row from reflowing as the image lands.
   The hairline is what separates a white flag from a white menu. */
.phone-field__flag,
.phone-menu__flag{
  flex-shrink:0;width:20px;height:15px;
  border-radius:2px;object-fit:cover;
  /* Outline rather than an inset shadow: on a replaced element the image
     paints over an inset shadow, so the hairline would never be seen. */
  outline:1px solid rgb(0 0 0 / .1);outline-offset:-1px;
}
.phone-field__dial{font-variant-numeric:tabular-nums}
.phone-field__caret{font-size:.7rem;color:var(--text-subtle)}

/* The seam only reads as one control while both halves agree on their
   border; whichever half has focus owns it. */
.phone-field__trigger:focus-visible,
.phone-field__select:focus,
.phone-field__number:focus{position:relative;z-index:1}
.phone-field--error .phone-field__trigger,
.phone-field--error .phone-field__select{border-color:var(--danger)}

/* ── The country list ────────────────────────────────────────────────
   Fixed, like the row menu, so it escapes a modal body and a card that
   clips its own corners. Wide enough to spell the country out, which is
   the whole reason the trigger does not have to. */
.phone-menu{
  position:fixed;top:0;left:0;z-index:400;
  width:min(320px,calc(100vw - 24px));
  background:var(--surface);border:1px solid var(--border);
  border-radius:var(--radius);box-shadow:var(--shadow-lg);
  padding:6px;
}
.phone-menu[hidden]{display:none}
.phone-menu__search{position:relative;margin-bottom:4px}
.phone-menu__search i{
  position:absolute;left:9px;top:50%;transform:translateY(-50%);
  font-size:.8rem;color:var(--text-subtle);pointer-events:none;
}
.phone-menu__input{
  width:100%;height:34px;padding:0 10px 0 28px;
  border:1px solid var(--border);border-radius:var(--radius-xs);
  background:var(--surface-2);color:var(--text);
  font-family:inherit;font-size:var(--fs-xs);
}
.phone-menu__input:focus{border-color:var(--primary);background:var(--surface)}
.phone-menu__list{
  list-style:none;margin:0;padding:0;
  max-height:min(46vh,290px);overflow-y:auto;
  scrollbar-width:thin;
}
.phone-menu__item{
  display:flex;align-items:center;gap:9px;
  padding:7px 9px;border-radius:var(--radius-xs);
  font-size:var(--fs-xs);color:var(--text-body);cursor:pointer;
}
.phone-menu__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.phone-menu__dial{
  color:var(--text-muted);font-variant-numeric:tabular-nums;flex-shrink:0;
}
.phone-menu__item:hover,
.phone-menu__item.is-cursor{background:var(--surface-3);color:var(--text)}
.phone-menu__item.is-active{color:var(--primary-ink);font-weight:600}
.phone-menu__item.is-active .phone-menu__dial{color:var(--primary)}
.phone-menu__empty{
  margin:0;padding:14px 9px;text-align:center;
  font-size:var(--fs-xs);color:var(--text-muted);
}

@media (max-width:420px){
  /* The trigger is already narrow enough to keep the number readable at
     this width, so the two stay on one line where they used to stack. */
  .phone-menu{width:calc(100vw - 24px)}
}
