/* ============================================================
   WHATLABS DESIGN SYSTEM - components.css
   Portable component library. Product-agnostic: every colour here comes
   from a custom property defined by tokens/core.css + tokens/<product>.css.
   Load order: tokens/core.css, tokens/<product>.css, components.css.
   Vanilla CSS, no framework. Behaviour lives in components.js.
   ============================================================ */

/* ---------------- Page chrome ---------------- */
body.wl-body { background: var(--paper); color: var(--ink); }

.wl-chrome-top {
  background: linear-gradient(180deg, var(--brand-deep) 0%, color-mix(in srgb, var(--brand-deep) 82%, black) 100%);
  color: var(--white);
  position: sticky; top: 0; z-index: 40;
  box-shadow: 0 1px 0 rgba(255,255,255,0.06), 0 8px 24px rgba(0,0,0,0.18);
}
.wl-chrome-bar {
  max-width: 1220px; margin: 0 auto; padding: 0.85rem 1.75rem;
  display: flex; align-items: center; gap: var(--space-lg); flex-wrap: wrap;
}
.wl-wordmark {
  font-weight: var(--w-numeral); font-size: 1rem; color: var(--white);
  text-decoration: none; letter-spacing: 0.01em;
  display: flex; align-items: center; gap: 0.55rem;
}
.wl-wordmark .wl-wm-sub { font-weight: var(--w-body); opacity: 0.72; font-size: 0.7rem; letter-spacing: 0.02em; white-space: nowrap; }
.nav-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.wl-chrome-role {
  font-size: 0.76rem; font-weight: var(--w-head);
  background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.16);
  padding: 0.32rem 0.75rem; border-radius: var(--radius-pill); color: rgba(255,255,255,0.92);
  white-space: nowrap; margin-left: auto;
}

/* ---------------- Hero band ---------------- */
.wl-hero-band {
  background: linear-gradient(160deg, var(--brand-deep) 0%, color-mix(in srgb, var(--brand-deep) 70%, var(--brand)) 55%, var(--brand) 130%);
  color: var(--white);
  padding: var(--space-2xl) 0 3.25rem;
  margin-bottom: var(--space-xl);
  border-radius: 0 0 28px 28px;
}
.wl-hero-band .eyebrow { color: rgba(255,255,255,0.75); }
.wl-hero-band .eyebrow::before { background: var(--accent-1, var(--white)); }
.wl-hero-band h1 { color: var(--white); }
.wl-hero-band .lede { color: rgba(255,255,255,0.86); max-width: 62ch; }

.wl-page-head { padding: var(--space-2xl) 0 var(--space-lg); }
.wl-page-head h1 { max-width: 46ch; }
.wl-page-head .lede { max-width: 62ch; margin-top: var(--space-2xs); }

/* ---------------- Cards ---------------- */
.wl-card {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-lg) 1.6rem;
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-md);
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.wl-card h2, .wl-card h3 { margin-top: 0; }
.wl-card.hoverable:hover, a.wl-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--line-strong); }
/* Depth modifier: a tinted, slightly-lifted card for the ONE thing on a
   screen that should read as foreground against the rest -- a narrow
   "in focus" detail panel beside a wide list (a split-main layout's
   second column), never a whole page of these. Added 3rd August 2026
   because every suite app's cards were plain white on pale paper with no
   foreground/background layering at all (measured: 0 non-white .wl-card
   backgrounds across the estate) -- CEC3's own hub mixes tinted and white
   surfaces deliberately so the eye has somewhere to land. Purely additive
   -- every existing `.wl-card` keeps its exact current white background. */
.wl-card.tinted { background: var(--tint); border-color: var(--line-strong); box-shadow: var(--shadow-md); }
/* A mini-stat tile is ALSO --tint by default (the whole point being a
   tinted tile standing out against a white card) -- nested straight
   inside a tinted card the two would be the same colour and the tiles
   would visually disappear. Invert to white here so the layering still
   reads: white tiles as the brighter foreground on the tinted card,
   never two identical washes on top of each other. */
.wl-card.tinted .wl-mini-stat { background: var(--white); }

/* ---------------- KPI tiles + ring gauge + count-up ---------------- */
.wl-kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); margin-bottom: var(--space-lg); }
@media (max-width: 980px) { .wl-kpi-row { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .wl-kpi-row { grid-template-columns: 1fr; } }
/* An app that sets its own column count with an inline style (a tile count
   decided by data at render time -- found in the wild at forms/form_new.html
   and train/home.html, both `style="grid-template-columns: repeat(2, 1fr)"`)
   sits above any selector here regardless of specificity, silently
   overriding the collapse above and forcing that column count all the way
   down to phone width -- a real 390px horizontal-scroll risk. Ported
   verbatim from CEC3's own fix for the identical bug (cec3.css's kpi-row
   comment) -- !important is the only way to out-rank an inline style; kept
   scoped to this one attribute-selector, not a general escape hatch. */
@media (max-width: 980px) {
  .wl-kpi-row[style*="grid-template-columns"] { grid-template-columns: 1fr 1fr !important; }
}
@media (max-width: 560px) {
  .wl-kpi-row[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
}

.wl-kpi-tile {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 1.25rem 1.35rem 1.1rem;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
  position: relative;
  overflow: hidden;
}
.wl-kpi-tile:hover { box-shadow: var(--shadow-md); transform: translateY(-3px); }
/* A CLICKABLE tile is an <a>, so without this every word inside it -- the
   label, the big numeral, the honesty tag, the view hint -- renders
   underlined, which reads as broken rather than as a link. This front
   door's own app.css already had a one-line `a.wl-kpi-tile { text-
   decoration: none; }` workaround (see its comment, flagged there for the
   shared kit); this is the fuller fix ported back from
   packages/suite_ui/suite_ui/static/components.css (4th August 2026) now
   that the kit carries it too, including the descendant reset the app.css
   version didn't have. Kept in sync by hand because web/ does not consume
   suite_ui -- it has its own copy of this stylesheet. */
a.wl-kpi-tile, a.wl-kpi-tile:hover { text-decoration: none; color: inherit; }
a.wl-kpi-tile * { text-decoration: none; }
.wl-kpi-label {
  /* --brand-deep, not --brand: see tokens/core.css .eyebrow comment - this
     is the same small-uppercase-text-on-paper contrast case. */
  text-transform: uppercase; letter-spacing: var(--tracking-label); font-size: 0.68rem; font-weight: var(--w-numeral);
  color: var(--brand-deep); display: flex; align-items: center; justify-content: space-between; gap: var(--space-xs);
  /* flex-wrap: a KPI tile placed at .wl-t-1x1 in the dashboard grid
     (components.css's own DASHBOARD GRID section) is only ~1/2 the page
     width at the 560px collapse -- narrow enough that a longer label plus
     its honesty tag ("Awaiting shortlisting" + "real") no longer both fit
     on one line without the tag clipping against the card edge (found
     4th August 2026, first real use of a 1x1 tile at mobile width -- every
     prior kpi_row.html caller always went full-width at this breakpoint,
     see kpi_row.html's own comment, so this never came up before). Letting
     the tag wrap onto its own line costs nothing at any width where both
     already fit on one line (space-between still holds), and only changes
     the narrow case that was clipping silently. */
  flex-wrap: wrap;
}
/* Reserve two lines for the label so a label whose honesty tag wraps to a
   second line does not shove its numeral down while the shorter labels
   beside it stay put. Ported from the kit's components.css alongside the
   underline fix above -- same measurement (48px at 1280px for a two-line
   label vs 23px for one line, hence 3rem not an `em` value: this element's
   font-size is 0.68rem, so 2.7em would compute to 29px and reserve barely
   one line). */
.wl-kpi-label { min-height: 3rem; align-content: flex-start; }
.wl-kpi-tagrow { display: flex; gap: 0.3rem; }
.wl-kpi-numrow { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-sm); margin-top: 0.55rem; }
.wl-kpi-num {
  font-size: var(--text-numeral); font-weight: var(--w-display); color: var(--brand-deep);
  letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums;
}
.wl-kpi-spark { flex: none; }
.wl-kpi-sub { font-size: var(--text-sm); color: var(--grey); margin-top: 0.4rem; }
.wl-kpi-delta {
  display: inline-flex; align-items: center; gap: 0.2rem;
  font-size: 0.72rem; font-weight: 800; padding: 0.15rem 0.5rem; border-radius: var(--radius-pill);
  white-space: nowrap;
}
.wl-kpi-delta.up { background: #e2f1ee; color: #0d6a52; }
.wl-kpi-delta.down { background: #fdece8; color: #9c3b28; }
.wl-kpi-delta.flat { background: var(--tint); color: var(--grey); }

/* ring gauge (SVG built by components.js: wlRingGaugeSvg) */
.wl-ring-gauge { position: relative; display: inline-flex; align-items: center; justify-content: center; }
.wl-ring-gauge svg { transform: rotate(-90deg); }
.wl-ring-gauge .wl-ring-track { stroke: var(--tint); fill: none; }
.wl-ring-gauge .wl-ring-val { stroke: var(--brand); fill: none; stroke-linecap: round; transition: stroke-dashoffset var(--dur-countup) var(--ease); }
.wl-ring-gauge .wl-ring-label { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-weight: 800; color: var(--brand-deep); font-size: 0.95rem; font-variant-numeric: tabular-nums; }

/* sparkline - decorative shape only, never implies a real time trend on real data */
.wl-spark-svg { display: block; }
.wl-spark-svg .wl-spark-fill { opacity: 0.14; }
.wl-spark-svg .wl-spark-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ---------------- Band-bar meter ---------------- */
.wl-band-bar {
  position: relative; height: 10px; border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--need-low) 0%, var(--need-medium) 35%, var(--need-high) 68%, var(--need-very-high) 100%);
  overflow: visible;
}
.wl-band-bar .wl-marker {
  position: absolute; top: 50%; width: 14px; height: 14px; border-radius: 50%;
  background: var(--white); border: 3px solid var(--brand-deep);
  transform: translate(-50%, -50%); box-shadow: 0 2px 6px rgba(0,0,0,0.3);
  transition: left var(--dur-countup) var(--ease);
}
.wl-band-bar-wrap { display: flex; align-items: center; gap: var(--space-sm); }
.wl-band-bar-wrap .wl-band-bar { flex: 1; }
.wl-band-bar-wrap .wl-band-val { font-weight: 800; color: var(--brand-deep); font-size: 0.9rem; min-width: 2.4em; text-align: right; font-variant-numeric: tabular-nums; }

/* ---------------- Buttons ---------------- */
/* The primary fill gradient runs --brand (top edge, for identity) into
   --brand-deep (dominant, where the label sits) rather than --brand-light
   into --brand. Measured: white text on flat --brand fails AA for 5 of the
   12 product palettes (as low as 1.6:1 for Earthrise:Live's gold); white on
   --brand-deep is AAA (9:1-20:1) for all twelve. Anchoring the fill on the
   deep tone keeps every product's button legible without a per-product
   text-colour override. --on-brand exists for the rare case a product needs
   dark text on a flat brand chip; it defaults to white. */
.wl-btn {
  display: inline-flex; align-items: center; gap: 0.45rem;
  font-family: var(--font); font-weight: 800; font-size: 0.9rem;
  padding: 0.7rem 1.35rem; border-radius: var(--radius-pill);
  border: 1.5px solid var(--brand-deep);
  background: linear-gradient(180deg, var(--brand), var(--brand-deep) 65%);
  color: var(--on-brand, var(--white)); text-decoration: none; cursor: pointer;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), background var(--dur) var(--ease), border-color var(--dur) var(--ease);
  box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 6px 16px var(--brand-20, rgba(0,0,0,0.18));
}
.wl-btn:hover { background: linear-gradient(180deg, var(--brand-deep), var(--brand-deep)); transform: translateY(-1px); }
.wl-btn:active { transform: translateY(0); }
.wl-btn.secondary { background: var(--white); color: var(--brand-deep); border-color: var(--line-strong); box-shadow: var(--shadow-xs); }
.wl-btn.secondary:hover { background: var(--tint); color: var(--brand-deep); border-color: var(--brand-deep); }
.wl-btn.ghost { background: transparent; border-color: transparent; color: var(--grey); box-shadow: none; }
.wl-btn.ghost:hover { color: var(--brand-deep); background: var(--surface-tint); }
.wl-btn[disabled] { opacity: 0.5; cursor: not-allowed; transform: none !important; }
.wl-btn-row { display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; }
/* Ported from the kit's identical fix (packages/suite_ui/suite_ui/static/
   components.css, 4th August 2026): already referenced here (org.html's
   own Approve/Reject/Appoint buttons, this pass's own tile-customiser
   strip) with no rule behind it -- a pre-existing gap, fixed once so
   every ".wl-btn small" gets a real compact size instead of silently
   rendering full-size. Kept in sync by hand, same reason as every other
   "ported from the kit" note in this file (web/ does not consume
   suite_ui). */
.wl-btn.small { padding: 0.45rem 1rem; font-size: 0.82rem; }

/* ---------------- Chips / tags, incl. the honesty tags ---------------- */
.wl-tag {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.68rem; font-weight: var(--w-numeral); letter-spacing: 0.02em;
  padding: 0.2rem 0.6rem; border-radius: var(--radius-pill);
  vertical-align: middle; text-transform: uppercase;
}
/* Honesty tags - first-class component, not an afterthought. Every number
   on a WhatLabs product surface carries one of these four. */
.wl-tag.real { background: #e2f1ee; color: #0d6a52; }
.wl-tag.real::before { content: "\25CF"; font-size: 0.5rem; }
.wl-tag.computed { background: #e8f0fb; color: #1d4f8f; }
.wl-tag.computed::before { content: "\03A3"; font-size: 0.6rem; }
.wl-tag.illustrative { background: #fdece8; color: #9c3b28; }
.wl-tag.illustrative::before { content: "\25C6"; font-size: 0.55rem; }
.wl-tag.proxy { background: #fbf1e0; color: #7a5310; }
.wl-tag.proxy::before { content: "\2248"; font-size: 0.65rem; }

.wl-tag.band-low { background: var(--need-low); color: var(--need-low-ink); border: 1px solid var(--line-strong); }
.wl-tag.band-medium { background: var(--need-medium); color: var(--need-medium-ink); }
.wl-tag.band-high { background: var(--need-high); color: var(--need-high-ink); }
.wl-tag.band-very-high { background: var(--need-very-high); color: var(--need-very-high-ink); }

.wl-pill-note {
  font-size: 0.82rem; color: var(--grey); background: var(--tint);
  border-radius: var(--radius-sm); padding: 0.65rem 0.85rem; display: block;
  border-left: 3px solid var(--brand);
}

/* ---------------- Segmented control (sliding thumb) ---------------- */
.wl-segmented { position: relative; display: inline-flex; background: var(--tint); border-radius: var(--radius-pill); padding: 4px; gap: 2px; }
.wl-segmented .wl-thumb {
  position: absolute; top: 4px; bottom: 4px; left: 4px;
  background: var(--white); border-radius: var(--radius-pill); box-shadow: var(--shadow-sm);
  transition: transform 260ms var(--ease), width 260ms var(--ease); z-index: 0;
}
.wl-segmented button {
  position: relative; z-index: 1; background: transparent; border: none; cursor: pointer;
  font-family: var(--font); font-weight: 800; font-size: 0.82rem; color: var(--grey);
  padding: 0.55rem 1rem; border-radius: var(--radius-pill); white-space: nowrap;
  display: inline-flex; align-items: center; gap: 0.4rem; transition: color var(--dur) var(--ease);
}
.wl-segmented button.active { color: var(--brand-deep); }

/* ---------------- Tabs (sticky, deep-linkable via ?tab=) ---------------- */
.wl-tabs-bar {
  position: sticky; top: 0; z-index: 30;
  background: var(--white); border-bottom: 1px solid var(--line);
}
.wl-tabs-bar .wl-tabs-row { display: flex; gap: var(--space-lg); max-width: 1220px; margin: 0 auto; padding: 0 1.75rem; overflow-x: auto; }
.wl-tab {
  background: none; border: none; cursor: pointer; font-family: var(--font);
  font-weight: 800; font-size: 0.88rem; color: var(--grey);
  padding: 0.95rem 0.1rem; border-bottom: 2.5px solid transparent;
  white-space: nowrap; transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.wl-tab.active { color: var(--brand-deep); border-color: var(--brand); }
.wl-tab:hover:not(.active) { color: var(--brand-deep); }
.wl-tab-panel { display: none; }
.wl-tab-panel.active { display: block; animation: wlFadeUp var(--dur-slow) var(--ease); }

/* ---------------- Data table with chip columns ---------------- */
.wl-table-scroll { overflow-x: auto; border-radius: var(--radius); }
.wl-table { width: 100%; border-collapse: collapse; font-size: 0.89rem; }
/* Give each column a floor so a wide table SCROLLS in its wrapper instead of
   crushing. Found 3rd August 2026: .wl-table is width:100%, so it always
   shrank to its container and the wrapper's overflow-x never fired; combined
   with overflow-wrap:anywhere on cells, recruit's 9-column central record
   collapsed into unreadable one-letter-per-line columns at 390px. It passed
   the no-page-scroll rule and was still useless.
   A per-column minimum scales with the table: three columns still fit a phone,
   nine columns exceed it and the wrapper scrolls, which is the intent. */
.wl-table-scroll > .wl-table th,
.wl-table-scroll > .wl-table td { min-width: 5.5rem; }
.wl-table-scroll > .wl-table th.num,
.wl-table-scroll > .wl-table td.num { min-width: 4rem; }
/* ...and let a column's own content set its floor too, which is the other
   half of the same bug. app.css puts `overflow-wrap: anywhere` on every
   cell so unbroken strings (emails, URNs) cannot overflow. But `anywhere`
   ALSO counts those break points toward the cell's min-content width,
   which collapses a column's intrinsic width to a single character - the
   reason the header text stacked one letter per line. `break-word` still
   breaks a long token when it does not fit the line, so nothing overflows,
   but it does NOT shrink min-content, so a column is at least as wide as
   its longest word and text wraps on spaces like prose. Specificity beats
   app.css's `.wl-table td`, so this wins despite loading first. */
.wl-table-scroll > .wl-table th,
.wl-table-scroll > .wl-table td { overflow-wrap: break-word; }
.wl-table th, .wl-table td { text-align: left; padding: 0.7rem 0.75rem; border-bottom: 1px solid var(--line); }
.wl-table th { color: var(--brand-deep); font-size: 0.68rem; text-transform: uppercase; letter-spacing: var(--tracking-label); font-weight: var(--w-numeral); background: var(--white); }
.wl-table tbody tr { transition: background var(--dur) var(--ease); }
.wl-table tbody tr:hover td { background: var(--surface-tint); }
.wl-table tbody tr:last-child td { border-bottom: none; }
.wl-table td.num, .wl-table th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------------- Side panel ---------------- */
.wl-side-panel {
  border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 1.4rem 1.5rem;
  background: var(--white); min-height: 260px; box-shadow: var(--shadow-sm);
  transform: translateX(12px); opacity: 0;
}
.wl-side-panel.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.6rem;
  color: var(--grey); text-align: center; background: var(--tint); border-style: dashed; box-shadow: none;
  opacity: 1; transform: none;
}
.wl-side-panel.empty svg.wl-empty-glyph { width: 30px; height: 30px; color: var(--brand-light); opacity: 0.9; flex: none; }
.wl-side-panel.filled { opacity: 1; transform: translateX(0); transition: opacity var(--dur-panel) var(--ease), transform var(--dur-panel) var(--ease); }
.wl-side-panel .wl-sp-stagger > * { opacity: 0; transform: translateY(6px); animation: wlSpIn 380ms var(--ease) forwards; }
@keyframes wlSpIn { to { opacity: 1; transform: translateY(0); } }

/* ---------------- Empty state (honest message + SVG glyph, never emoji) ---------------- */
.wl-empty-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-sm);
  text-align: center; padding: var(--space-2xl) var(--space-lg); color: var(--grey);
  background: var(--tint); border: 1px dashed var(--line-strong); border-radius: var(--radius-lg);
}
.wl-empty-state svg { width: 34px; height: 34px; color: var(--brand-light); }
.wl-empty-state strong { color: var(--brand-deep); font-size: 0.95rem; }
.wl-empty-state p { max-width: 42ch; margin: 0; }

/* ---------------- Stepper ---------------- */
.wl-stepper { display: flex; align-items: center; margin-bottom: var(--space-lg); }
/* Wrap at narrow widths. The stepper was a plain non-wrapping flex row with a
   flex:1 connector between each pair, so real step labels overflowed a 390px
   card - attend hit it with four ladder stages and visits with a three-step
   adventurous approval chain, and each patched it in its own stylesheet.
   Two apps independently working around the same gap is the signal to fix it
   centrally, so this is lifted from theirs; both local overrides can now go.
   The connector is hidden once wrapped: a line joining chips on different
   rows reads as broken rather than sequential. */
@media (max-width: 480px) {
  .wl-stepper { flex-wrap: wrap; row-gap: 0.6rem; column-gap: 0.9rem; align-items: flex-start; }
  .wl-step-connector { display: none; }
}
.wl-step-chip { display: flex; align-items: center; gap: 0.55rem; font-size: 0.83rem; font-weight: 700; color: var(--grey); }
.wl-step-chip .wl-n {
  width: 28px; height: 28px; border-radius: 50%; background: var(--white); border: 2px solid var(--line-strong);
  color: var(--grey); display: flex; align-items: center; justify-content: center; font-size: 0.78rem; font-weight: var(--w-numeral);
  transition: all var(--dur) var(--ease);
}
.wl-step-chip.active .wl-n { background: var(--brand); color: var(--white); border-color: var(--brand); box-shadow: 0 0 0 5px var(--brand-14, rgba(0,0,0,0.1)); }
.wl-step-chip.active { color: var(--brand-deep); }
.wl-step-chip.done .wl-n { background: var(--brand-deep); color: var(--white); border-color: var(--brand-deep); font-size: 0; }
.wl-step-chip.done .wl-n::after { content: "\2713"; font-size: 0.85rem; }
.wl-step-connector { flex: 1; height: 2px; background: var(--line-strong); margin: 0 0.4rem; position: relative; top: -14px; min-width: 24px; }
.wl-step-connector.done { background: var(--brand); }
.wl-step-panel { display: none; animation: wlFadeUp var(--dur-slow) var(--ease); }
.wl-step-panel.active { display: block; }
@keyframes wlFadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* ---------------- Timeline (confirmation "what happens next") ---------------- */
.wl-timeline { list-style: none; margin: 1.1rem 0 0; padding: 0; text-align: left; }
.wl-timeline li { position: relative; padding: 0 0 1.25rem 2rem; }
.wl-timeline li::before { content: ""; position: absolute; left: 0; top: 2px; width: 10px; height: 10px; border-radius: 50%; background: var(--brand); box-shadow: 0 0 0 4px var(--brand-14, rgba(0,0,0,0.1)); }
.wl-timeline li::after { content: ""; position: absolute; left: 4.5px; top: 14px; bottom: -2px; width: 1.5px; background: var(--line-strong); }
.wl-timeline li:last-child::after { display: none; }
.wl-timeline li strong { display: block; color: var(--brand-deep); font-size: 0.9rem; margin-bottom: 0.1rem; }
.wl-timeline li span { font-size: 0.85rem; color: var(--grey); }

.wl-confirm-panel {
  text-align: center; padding: var(--space-3xl) 1.75rem;
  background: linear-gradient(180deg, var(--white), var(--tint));
  border-radius: var(--radius-lg); border: 1px solid var(--line); box-shadow: var(--shadow-sm);
}
.wl-confirm-panel .wl-tick {
  width: 68px; height: 68px; border-radius: 50%; background: var(--brand-deep); color: var(--white);
  display: flex; align-items: center; justify-content: center; margin: 0 auto 1.25rem;
  box-shadow: 0 0 0 8px var(--brand-08, rgba(0,0,0,0.06));
}
.wl-confirm-panel .wl-tick svg { width: 30px; height: 30px; }
.wl-confirm-panel .wl-tick path { stroke-dasharray: 24; stroke-dashoffset: 24; animation: wlTickDraw 480ms 180ms var(--ease) forwards; }
@keyframes wlTickDraw { to { stroke-dashoffset: 0; } }
.wl-confirm-panel h2 { margin-bottom: var(--space-xs); }
.wl-confirm-panel p { max-width: 46ch; margin-left: auto; margin-right: auto; }

/* ---------------- Forms ---------------- */
.wl-field label {
  display: block; font-weight: var(--w-numeral); font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: var(--tracking-label); color: var(--brand-deep); margin: 1.1rem 0 0.4rem;
}
.wl-field:first-of-type label { margin-top: 0.2rem; }
/* Opt-OUT, not opt-in. This was a list of four accepted types, so every
   other text-like input rendered as a bare browser box next to properly
   styled siblings. Measured across the estate 4th August 2026: 80
   type=date, 23 type=number, 10 type=time and a type=search were all
   unstyled -- and dates are everywhere in a school product, so this was
   one of the loudest "unfinished" signals in the suite. Spotted by Jason
   on lettings' "Capacity for hire" field, which sat as a small bare box
   beside styled neighbours.
   Excluding the controls that must keep their native appearance is the
   safer shape: a new input type added later is styled by default rather
   than silently missed the way these four were. */
.wl-field input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=submit]):not([type=button]):not([type=range]):not([type=color]),
.wl-field select, .wl-field textarea {
  width: 100%; padding: 0.75rem 0.9rem; border: 1.5px solid var(--line-strong); border-radius: var(--radius-sm);
  font-family: var(--font); font-size: 0.95rem; color: var(--ink); background: var(--white);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.wl-field input:focus, .wl-field select:focus, .wl-field textarea:focus {
  border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-20, rgba(0,0,0,0.12)); outline: none;
}
.wl-field-hint { font-size: 0.78rem; color: var(--grey); margin-top: 0.35rem; }

/* ---------------- Footer with thin accent line ---------------- */
.wl-footer {
  background: var(--footer-grad, linear-gradient(180deg, var(--brand-deep), color-mix(in srgb, var(--brand-deep) 70%, black)));
  color: rgba(255,255,255,0.78); margin-top: var(--space-3xl); padding: 1.6rem 0; font-size: 0.82rem;
  position: relative;
}
.wl-footer::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--accent-1, var(--brand-light)); opacity: 0.7; }
.wl-footer .wrap { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
.wl-footer a { color: rgba(255,255,255,0.92); font-weight: 700; }
.wl-footer .wl-foot-brand { display: flex; align-items: center; gap: var(--space-xs); font-weight: var(--w-numeral); color: var(--white); }
.wl-footer .wl-demo-marker { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.16); border-radius: var(--radius-pill); padding: 0.25rem 0.7rem; font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; }

/* ---------------- Legend ---------------- */
.wl-legend { display: flex; align-items: center; gap: 0.9rem; font-size: 0.8rem; color: var(--grey); flex-wrap: wrap; }
.wl-legend-chip { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; font-weight: 700; color: var(--grey); background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-pill); padding: 0.28rem 0.7rem 0.28rem 0.4rem; }
.wl-legend-chip .wl-swatch { width: 13px; height: 13px; border-radius: 4px; display: inline-block; border: 1px solid rgba(0,0,0,0.06); }

/* ---------------- Scroll-reveal ---------------- */
.wl-reveal { opacity: 0; transform: translateY(14px); transition: opacity 480ms var(--ease), transform 480ms var(--ease); }
.wl-reveal.in { opacity: 1; transform: translateY(0); }

/* ============================================================
   CEC3-GRAMMAR APP SHELL COMPONENTS (added 2026-08-03)
   The shared navigational/vocabulary layer every one of the thirteen
   suite apps composes from -- see packages/README.md section 8 for the
   composition contract and copy-paste examples of each. The rule these
   all follow: an app chooses WHICH of these it shows and what content
   goes in them; it never restyles how one looks. If a screen needs
   something not here, add it here, not in the app's own stylesheet.
   ============================================================ */

/* ---------------- Sub-nav (pill tabs directly under the chrome bar) ----
   CEC3's hub-subnav pattern: a white strip, pill tabs, one filled/tinted
   active state, an optional right-aligned outlined action button. This is
   PAGE-level navigation (which screen of this app) -- distinct from
   .wl-tabs-bar/.wl-tab, which is IN-page tab switching within one screen
   (admin.html's Overview/Orgs/Claims/... panels already use that pattern
   and are untouched by this addition). */
.wl-subnav-bar { background: var(--white); border-bottom: 1px solid var(--line); }
.wl-subnav { display: flex; align-items: center; gap: 0.3rem; padding: 0.65rem 0; flex-wrap: wrap; }
.wl-subnav-pills { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.wl-subnav a.wl-subnav-pill {
  color: var(--grey); text-decoration: none; font-size: 0.82rem; font-weight: 700;
  padding: 0.42rem 0.9rem; border-radius: var(--radius-pill);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.wl-subnav a.wl-subnav-pill:hover { background: var(--surface-tint); color: var(--brand-deep); }
.wl-subnav a.wl-subnav-pill.active { background: var(--brand-08); color: var(--brand-deep); }
.wl-subnav-action { margin-left: auto; }

/* ---------------- Circular icon button (info / expand / etc) ----------
   CEC3's map-icon-btn / btn-icon-circle pattern: a compact, teal-outline
   (here: brand-outline) round button for an icon-only action that doesn't
   need a text label alongside a primary .wl-btn in the same row. Always
   pair with aria-label since there is no visible text. */
.wl-icon-circle-btn {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 2.25rem; height: 2.25rem; border-radius: 50%;
  border: 1.5px solid var(--brand); background: var(--white); color: var(--brand);
  cursor: pointer; text-decoration: none; box-shadow: var(--shadow-xs);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.wl-icon-circle-btn:hover { background: var(--brand); color: var(--white); border-color: var(--brand-deep); transform: translateY(-1px); }
.wl-icon-circle-btn svg { display: block; width: 15px; height: 15px; }
.wl-icon-circle-row { display: flex; gap: 0.4rem; }

/* ---------------- Section card action link ("Open ->") ----------------
   The right-aligned link in a .wl-card's header row (pair with
   .flex-between from tokens/core.css: <div class="flex-between"><h2>...
   </h2><a class="wl-card-action" href="...">Open -></a></div>). Also used
   as the small icon-only variant (.wl-card-action-icon) for a denser
   header where the arrow alone is enough (title already says what it
   opens). */
.wl-card-action {
  font-size: 0.84rem; font-weight: 800; color: var(--brand); text-decoration: none;
  white-space: nowrap; display: inline-flex; align-items: center; gap: 0.3rem;
}
.wl-card-action:hover { color: var(--brand-deep); text-decoration: underline; }
.wl-card-action-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; border-radius: 50%; color: var(--brand);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.wl-card-action-icon:hover { background: var(--brand-08); color: var(--brand-deep); }
.wl-card-action-icon svg { width: 14px; height: 14px; display: block; }

/* ---------------- Tile family ----------------
   FOUR fixed tile shapes, used identically everywhere. An app picks which
   ones it needs and what content goes in them; none of these may be
   restyled per app (packages/README.md section 8). All four share the
   base .wl-kpi-tile card treatment (white, --line border, --radius-lg,
   --shadow-sm, hover lift) so they always feel like one family even when
   mixed in the same grid. */

/* 1. KPI tile: .wl-kpi-tile (already defined above) -- label, numeral,
   optional sub-line. Clickable variant, for a tile that is also a link/
   button into a filtered view (CEC3's "Cold spots" head-stat pattern). */
.wl-kpi-tile.kpi-clickable { cursor: pointer; }
.wl-kpi-tile.kpi-clickable:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
.wl-kpi-tile.kpi-clickable:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--brand-20), 0 0 0 1px var(--brand-deep); }
.wl-kpi-view-hint { display: block; margin-top: 0.5rem; font-size: 0.8rem; color: var(--brand); font-weight: 800; }

/* 2. Sub-card (CEC3's "Focus your week" row): bold title, large numeral,
   muted description, "Open ->" link pinned to the tile foot regardless of
   description length (flex column + margin-top:auto on the link). */
.wl-sub-card-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); margin-bottom: var(--space-lg); }
@media (max-width: 980px) { .wl-sub-card-row { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .wl-sub-card-row { grid-template-columns: 1fr; } }
.wl-sub-card {
  display: flex; flex-direction: column; gap: 0.3rem;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: 1.25rem 1.35rem 1.1rem; box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.wl-sub-card:hover { box-shadow: var(--shadow-md); transform: translateY(-3px); }
.wl-sub-card h3 { margin: 0; font-size: 0.95rem; }
.wl-sub-card .wl-sub-card-num { font-size: var(--text-numeral); font-weight: var(--w-display); color: var(--brand-deep); letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; margin: 0.2rem 0; }
.wl-sub-card p { font-size: 0.82rem; margin: 0; flex: 1; }
.wl-sub-card .wl-card-action { margin-top: 0.6rem; }

/* 3. Stat-strip tile: label, large numeral, an inline sparkline to its
   right, and a link beneath. The sparkline is decorative unless the data
   backing it is real and labelled as such (see WL.sparklineSvg's own
   doctrine in components.js) -- never add one just because a tile has a
   spare corner. */
.wl-stat-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); margin-bottom: var(--space-lg); }
/* Fit the column count to the actual number of tiles, so a strip never
   leaves an orphan on a second row. Four hard-coded columns put the fifth
   KPI alone with three empty slots beside it on the account page -- the
   "random size boxes in random places" reading, in miniature. Counted with
   :has() rather than a template-set inline style on purpose: an inline
   grid-template-columns outranks the media queries below and would pin the
   count all the way down to phone width (the exact bug documented on
   .wl-kpi-row above). Wrapped in a min-width query, NOT left to source
   order: :has() inherits the specificity of its argument, so these score
   higher than the plain .wl-stat-strip breakpoint rules below and would
   otherwise keep a five-column strip at five columns down to 390px --
   a horizontal-scroll bug, which is the thing the breakpoints exist to
   prevent. Confining them to wide viewports keeps the collapse intact. */
@media (min-width: 981px) {
  .wl-stat-strip:has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
  .wl-stat-strip:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, 1fr); }
  .wl-stat-strip:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, 1fr); }
  .wl-stat-strip:has(> :nth-child(6):last-child) { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 980px) { .wl-stat-strip { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .wl-stat-strip { grid-template-columns: 1fr; } }
.wl-stat-tile { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 1.25rem 1.35rem 1.1rem; box-shadow: var(--shadow-sm); }
.wl-stat-tile .wl-kpi-label { margin-bottom: 0; }
.wl-stat-tile .wl-stat-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 0.75rem; margin-top: 0.55rem; }
.wl-stat-tile .wl-stat-num { font-size: var(--text-numeral); font-weight: var(--w-display); color: var(--brand-deep); letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; }
.wl-stat-tile .wl-stat-spark { flex: none; }
.wl-stat-tile .wl-card-action { display: block; margin-top: 0.5rem; }

/* 4. Mini stat tile: for a 2x2 grid inside a side/detail panel -- large
   numeral, small label beneath, tinted background (distinct from the
   white KPI/stat/sub-card tiles, so a mini-stat grid never gets mistaken
   for a second KPI row at a glance). */
.wl-mini-stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin-top: 0.75rem; }
.wl-mini-stat { background: var(--tint); border-radius: var(--radius-sm); padding: 0.7rem 0.85rem; text-align: left; }
.wl-mini-stat .wl-mini-stat-num { font-size: 1.35rem; font-weight: var(--w-display); color: var(--brand-deep); line-height: 1.1; font-variant-numeric: tabular-nums; }
.wl-mini-stat .wl-mini-stat-label { display: block; font-size: 0.68rem; text-transform: uppercase; letter-spacing: var(--tracking-label); color: var(--grey); margin-top: 0.15rem; }

/* ---------------- Page-head + KPI grid split (CEC3's two-column page
   head) -- left: eyebrow/h1/lede (optionally with a status pill inline in
   running text, .wl-inline-status); right: a KPI tile grid. Opt-in via a
   SEPARATE partial (suite/partials/page_head_kpi.html) rather than a
   change to the existing page_head.html, so no app that already includes
   plain page_head.html + its own kpi_row.html underneath is affected. */
/* STACKED, deliberately, since 4th August 2026 -- the side-by-side
   1.3fr:1fr version is retired. Measured on maintain at 1280px: the head
   column was 640px wide holding 145px of text (a mostly empty half-page),
   while four tiles were crushed into a 237px-wide 2x2 stack 423px deep,
   each tile reserving 67px of label space and using 23px. Jason: "look at
   this" -- the empty-left/gappy-right shape read as unfinished on every
   app that used this partial. Stacking gives the head the full wrap width
   (which CEC3's own hub does -- its stat band is a wide strip under the
   heading, not a tall side column) and returns the KPI row to its native
   4-across, where tiles are ~290px wide, labels stop wrapping, and the
   worst-case label reservations below became unnecessary. */
.wl-page-head-split { display: grid; grid-template-columns: 1fr; gap: var(--space-lg); align-items: start; padding: var(--space-2xl) 0 var(--space-lg); }
@media (max-width: 900px) { .wl-page-head-split { padding-top: var(--space-xl); } }
.wl-page-head-split .wl-page-head { padding: 0; }
.wl-page-head-split .wl-kpi-row { margin-bottom: 0; }
/* The old narrow split forced this partial's kpi-row to 2 columns and
   then needed a 4.2rem three-line label reservation to keep numerals
   aligned at ~192px label widths -- both rules deleted with the split
   itself: at full width the row's own 4-across grid and the estate-wide
   two-line reservation on .wl-kpi-label are sufficient. If a THREE-line
   wrap ever reappears, fix the label copy or the tile width, never by
   padding every tile on every page to the worst case again. */
.wl-inline-status { margin: 0 0.2rem; }

/* ---------------- Split card with circular icon buttons (map/chart +
   detail panel) -- the CARD SHELL only (header row with icon buttons,
   body slot); this does NOT include a map/chart implementation. Use where
   a map or chart genuinely earns its place (packages/README.md notes
   which apps are honest candidates) -- most apps will never use this. */
/* flex-wrap + min-width:0 on the heading -- the estate's own recurring
   390px-overflow bug (suite_ui/static/app.css's docstring), applied here
   because this shell is now actually used (a location/cohort name beside
   a status badge, both real, variable-length strings) rather than just
   documented. Added 3rd August 2026 alongside the first real use. */
.wl-split-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.wl-split-card-head h2, .wl-split-card-head h3 { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* Row accents for the shared table partial's "_row_class" hook. Semantic
   tiers only, so a highlighted row means the same thing in every app, and
   always paired with a text label in the row - never colour alone. */
.wl-table tbody tr.wl-row-bad td { background: var(--bad-08); box-shadow: inset 3px 0 0 var(--bad); }
.wl-table tbody tr.wl-row-warn td { background: color-mix(in srgb, var(--need-medium) 16%, white); box-shadow: inset 3px 0 0 var(--need-high); }
.wl-table tbody tr.wl-row-ok td { box-shadow: inset 3px 0 0 var(--need-low); }

/* ================================================================
   DASHBOARD GRID - ported from cec3.css's hub grid, 4th August 2026
   Jason: "remember how we used the grid layout... you have random size
   boxes in random places". Exactly right. Our dashboards were ad-hoc
   cards each choosing its own width and its own numeral size, so nothing
   aligned and gaps appeared wherever a row ran short.

   Nine columns. Every tile declares its span as N columns by M rows, so
   a dashboard's layout is legible in the markup and cannot drift.

   THE RULE THAT MATTERS: size follows span, not the other way round. A
   tile's numeral size is a property of its declared span (see the
   .wl-t-1x1 / .wl-t-3x1 numeral rules below), never hand-set per tile.

   grid-auto-rows is minmax(--cell, auto): a row is AT LEAST one cell
   tall and grows only if its content genuinely needs more, so a 1x1
   tile renders square. Give a content-heavy section enough declared
   rows that it does not force a shared row to stretch, or the square
   tiles beside it stop being square.
   ================================================================ */
.wl-dash-grid {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  grid-auto-rows: minmax(var(--cell), auto);
  gap: var(--grid-gap);
}
/* Grid items default to min-width:auto, which floors the track at the item's
   min-content width - a wide table would blow the whole grid past the
   viewport instead of scrolling inside its own wrapper. */
.wl-dash-grid > * { min-width: 0; }
/* The grid gap owns vertical rhythm here; a card's own bottom margin
   would double it. */
.wl-dash-grid .wl-card { margin-bottom: 0; }
/* Zero ALL of .wl-page-head's own padding here, not just the bottom -- see
   the kit's identical fix and comment in packages/suite_ui/suite_ui/
   static/components.css (4th August 2026): a lingering top padding
   (--space-2xl = 44px) pushed the heading's first line 44px below where
   its neighbouring KPI tiles start in the same grid row, measured live on
   this app's own /apps page. Kept in sync by hand, same reason as the
   GUARD comment just below. */
.wl-dash-grid > .wl-page-head { padding: 0; }

/* GUARD, 4th August 2026 - kept in sync with packages/suite_ui/suite_ui/
   static/components.css's identical rule (web/ keeps its own copy of this
   file rather than importing suite_ui, see web/app/main.py's Jinja2Templates
   call). A .wl-kpi-tile dropped straight onto this grid with no .wl-t-*
   span class at all defaults to a single auto-placed column - at 1280px
   that measured 114px wide (--cell) while the tile's own content pushed
   grid-auto-rows: minmax(--cell, auto) to 154-221px tall on this app's own
   org.html before it was fixed - a ~1:2 sliver, text wrapping mid-phrase.
   Only matches when NO class contains "wl-t-", so every existing, deliberate
   .wl-t-1x1 tile (admin.html, apps.html) is unaffected - this is a floor for
   an omitted span, not a correction of a chosen one. */
.wl-dash-grid > .wl-kpi-tile:not([class*="wl-t-"]) {
  grid-column: span 3;
  grid-row: span 1;
}

/* Span utilities: columns x rows. Reach for one of these. If a genuinely new
   size is needed, add a utility here rather than an inline style, so every
   section's size stays readable as "N by M cells". */
.wl-t-1x1 { grid-column: span 1; grid-row: span 1; }
.wl-t-2x1 { grid-column: span 2; grid-row: span 1; }
.wl-t-3x1 { grid-column: span 3; grid-row: span 1; }
.wl-t-3x2 { grid-column: span 3; grid-row: span 2; }
.wl-t-4x1 { grid-column: span 4; grid-row: span 1; }
.wl-t-5x1 { grid-column: span 5; grid-row: span 1; }
.wl-t-6x1 { grid-column: span 6; grid-row: span 1; }
.wl-t-6x2 { grid-column: span 6; grid-row: span 2; }
.wl-t-6x4 { grid-column: span 6; grid-row: span 4; }
.wl-t-3x4 { grid-column: span 3; grid-row: span 4; }
.wl-t-9x1 { grid-column: span 9; grid-row: span 1; }
.wl-t-9x2 { grid-column: span 9; grid-row: span 2; }

/* Size follows span. */
.wl-dash-grid .wl-t-1x1 .wl-kpi-num,
.wl-dash-grid .wl-t-2x1 .wl-kpi-num { font-size: var(--num-sm); }
.wl-dash-grid .wl-t-3x1 .wl-kpi-num,
.wl-dash-grid .wl-t-4x1 .wl-kpi-num { font-size: var(--num-lg); }

/* A narrow panel hugs its own content rather than stretching to match the
   tall card beside it, so a short record and a long one render alike.

   NAMED .wl-t-hug, NOT .wl-side-panel. My first version of this reused
   .wl-side-panel, which already exists above as a JS-driven inspector that
   sits at opacity:0 until something adds .filled - so every grid tile using
   it was permanently invisible, and a plain dashboard render never adds
   .filled. Caught 4th August 2026 by an agent building the 6x4 + 3x4 split
   in lettings and tickets. Keep this name distinct from any component that
   carries its own visibility behaviour. */
.wl-dash-grid .wl-t-hug { align-self: start; }

/* Collapse sensibly rather than shrinking nine columns onto a phone. */
@media (max-width: 980px) {
  .wl-dash-grid { grid-template-columns: repeat(6, 1fr); }
  .wl-t-9x1, .wl-t-9x2, .wl-t-6x4, .wl-t-6x2, .wl-t-6x1 { grid-column: span 6; }
  .wl-t-3x4, .wl-t-3x2, .wl-t-3x1, .wl-t-4x1, .wl-t-5x1 { grid-column: span 3; }
  .wl-t-1x1, .wl-t-2x1 { grid-column: span 2; }
}
@media (max-width: 560px) {
  .wl-dash-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; }
  .wl-dash-grid > * { grid-column: span 2 !important; grid-row: auto !important; }
  .wl-dash-grid .wl-t-1x1 { grid-column: span 1 !important; }
}

/* ================================================================
   DASHBOARD TILE CUSTOMISER - ported from the kit
   (packages/suite_ui/suite_ui/static/components.css, same section
   heading) for /org, the one page on this front door that uses
   .wl-dash-grid. Kept in sync by hand, same reason as every other
   "ported from the kit" note in this file: web/ does not consume
   suite_ui. See that file's own comment for the full doctrine (why a
   real Move up/Move down button pair replaces CEC3's own role="button"
   tile) and components.js's matching section for the behaviour. */
.wl-dash-toolbar { display: flex; justify-content: flex-end; margin: 0 0 var(--space-sm); }
.wl-dash-toolbar.customising { margin: 0 0 var(--space-md); }
.wl-customise-toggle-btn {
  display: inline-flex; align-items: center; gap: var(--space-2xs);
  background: var(--white); border: 1px solid var(--line-strong);
  color: var(--grey); font-family: var(--font); font-weight: 700; font-size: 0.82rem;
  padding: 0.45rem 0.9rem; border-radius: var(--radius-pill); cursor: pointer;
  box-shadow: var(--shadow-xs);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.wl-customise-toggle-btn:hover { background: var(--tint); color: var(--brand-deep); border-color: var(--brand-deep); }
.wl-customise-toggle-btn[hidden] { display: none; }

.wl-customise-strip {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap;
  width: 100%;
  background: var(--tint); border: 1px solid var(--brand-20, var(--line-strong)); border-radius: var(--radius);
  padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
}
.wl-customise-strip[hidden] { display: none; }
.wl-customise-strip-text { display: inline-flex; align-items: center; gap: var(--space-xs); font-weight: 700; font-size: 0.86rem; color: var(--brand-deep); }
.wl-customise-count {
  background: var(--brand-deep); color: var(--white); font-size: 0.72rem; font-weight: 900;
  padding: 0.15rem 0.55rem; border-radius: var(--radius-pill);
}
.wl-customise-count[hidden] { display: none; }
.wl-customise-strip-actions { display: flex; gap: var(--space-xs); flex-wrap: wrap; }

.wl-dash-grid [data-tile-id] { position: relative; }
.wl-dash-grid.customising [data-tile-id].wl-tile-movable {
  cursor: grab;
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.wl-dash-grid.customising [data-tile-id].wl-tile-movable:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.wl-tile-movable.wl-tile-selected { outline: 2px solid var(--brand-deep); outline-offset: 2px; border-radius: var(--radius); }
.wl-tile-dragging {
  box-shadow: var(--shadow-lg) !important;
  outline: 2px dashed var(--brand-deep) !important; outline-offset: 3px;
  cursor: grabbing;
  z-index: 5;
  opacity: 0.97;
}
.wl-tile-handle {
  display: none; align-items: center; gap: 2px;
  position: absolute; top: var(--space-2xs); right: var(--space-2xs); z-index: 2;
}
.wl-dash-grid.customising .wl-tile-movable .wl-tile-handle { display: inline-flex; }
.wl-tile-handle-grip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; border-radius: 50%;
  color: var(--brand-deep); background: rgba(255,255,255,0.92); border: 1px solid var(--line-strong);
  pointer-events: none;
}
.wl-tile-handle-grip.is-selected { background: var(--brand-deep); color: var(--white); border-color: var(--brand-deep); }
.wl-tile-move-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; border-radius: 50%;
  color: var(--brand-deep); background: rgba(255,255,255,0.92); border: 1px solid var(--line-strong);
  cursor: pointer; padding: 0;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.wl-tile-move-btn:hover:not(:disabled) { background: var(--brand-deep); color: var(--white); border-color: var(--brand-deep); }
.wl-tile-move-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.wl-tile-pinned-badge {
  display: none; align-items: center; gap: 0.3rem;
  position: absolute; top: var(--space-2xs); right: var(--space-2xs); z-index: 2;
  font-size: 0.62rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--grey); background: var(--surface-tint); border: 1px solid var(--line-strong);
  padding: 0.2rem 0.5rem; border-radius: var(--radius-pill);
}
.wl-dash-grid.customising [data-pinned] { opacity: 0.94; }
.wl-dash-grid.customising [data-pinned] .wl-tile-pinned-badge { display: inline-flex; }

/* The tile customiser's live-region announcer uses the existing
   `.visually-hidden` utility (static/css/app.css) -- no new class here. */

@media (max-width: 620px) {
  .wl-dash-toolbar { justify-content: flex-start; }
  .wl-customise-strip { padding: var(--space-sm); }
}
