/* ============================================================================
   Group Operations — prototype stylesheet

   Loaded after app.css. Everything here follows ~/Code/ui-system, which is the
   source of truth for tokens and component shape:

   - Palette is Tailwind's, unchanged, by its real name. app.css ships a custom
     sky ramp, so the ramp is restored to stock at :root below.
   - Controls take an accent at 300 with a black label. Labels (badges) take the
     tint: 100 fill, 900 text, 300 border, 500 icon.
   - Neutrals step one rung from what they sit on. On white that is slate-100,
     hovering to slate-200.
   - Type defaults to 14px. Headings are extrabold. Never all caps.
   ============================================================================ */

:root {
	/* ---- sky, restored to Tailwind stock (app.css ships a brand-custom ramp) */
	--sky-50: #f0f9ff;
	--sky-100: #e0f2fe;
	--sky-200: #bae6fd;
	--sky-300: #7dd3fc;
	--sky-400: #38bdf8;
	--sky-500: #0ea5e9;
	--sky-600: #0284c7;
	--sky-700: #0369a1;
	--sky-800: #075985;
	--sky-900: #0c4a6e;

	/* ---- the four other accents, at the two weights the system uses */
	--teal-100: #ccfbf1;
	--teal-300: #5eead4;
	--teal-400: #2dd4bf;
	--teal-500: #14b8a6;
	--teal-900: #134e4a;
	--amber-100: #fef3c7;
	--amber-300: #fcd34d;
	--amber-400: #fbbf24;
	--amber-500: #f59e0b;
	--amber-900: #78350f;
	--rose-100: #ffe4e6;
	--rose-300: #fda4af;
	--rose-400: #fb7185;
	--rose-500: #f43f5e;
	--rose-900: #881337;
	--violet-100: #ede9fe;
	--violet-300: #c4b5fd;
	--violet-400: #a78bfa;
	--violet-500: #8b5cf6;
	--violet-900: #4c1d95;
	--emerald-400: #34d399;

	/* ---- radii, Tailwind stock. app.css uses its own scale, so these are new
	        names rather than overrides. */
	--ds-r: 0.25rem;
	--ds-r-md: 0.375rem;
	--ds-r-lg: 0.5rem;
	--ds-r-xl: 0.75rem;
	--ds-r-2xl: 1rem;
	--ds-r-full: 9999px;

	--ds-shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
	--ds-shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
	--ds-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
	--ds-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
	--ds-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
	--ds-shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);

	--ds-ease: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Inter Variable is the typeface, vendored locally. Tabular figures, so a
   column of numbers holds still. */
body.proto {
	font-family: 'Inter Variable', Inter, ui-sans-serif, system-ui, sans-serif;
	font-variant-numeric: tabular-nums;
	color: var(--slate-700);
}

/* The surface the app sits on runs the full height of the window, whatever the
   list is doing. `app.css` leaves the canvas at slate-50, so a list too short
   to fill the screen ended in a band of near-white below the page — a seam
   that reads as the layout stopping rather than the content. Both elements
   carry it: the canvas takes html's background when html has one of its own,
   and body's only when it does not. */
html, body.proto { background: var(--slate-200); }

/* The gutter is always there, whether or not the page is long enough to need
   it. Without this, crossing the viewport height takes the scrollbar's width
   out of the page, `sizeColumns` re-fits every column to the narrower box, and
   the whole table twitches sideways — paging a section in, opening one, or
   filtering down to a short list all cross that line. Reserving the space costs
   a strip of background and removes the reflow entirely. */
html { scrollbar-gutter: stable; }

/* ============================================================================
   Button — ui-system Button.vue. A control fills at 300 and labels in black.
   Neutral steps one rung off its surface; hover moves one more.
   ============================================================================ */

.g-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	height: 40px;
	padding: 0 16px;
	border: 1px solid transparent;
	border-radius: var(--ds-r-xl);
	background: var(--slate-100);
	color: #000;
	font: inherit;
	font-size: 12px;
	font-weight: 600;
	white-space: nowrap;
	cursor: pointer;
	text-decoration: none;
	transition: background-color 150ms var(--ds-ease), box-shadow 150ms var(--ds-ease);
}
.g-btn:hover { background: var(--slate-200); color: #000; text-decoration: none; }
.g-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 4px; }
.g-btn:active { transform: translateY(0.5px); }

/* The notification dot: a control carrying one says there is something to read
   inside it, and the thing it refers to is written out in the menu it opens. It
   sits on the corner rather than in the label, so the label never reflows as
   the state comes and goes. */
.g-btn { position: relative; }
.g-notedot {
	position: absolute;
	top: -2px;
	right: -2px;
	width: 10px;
	height: 10px;
	border-radius: 999px;
	background: var(--amber-500);
	box-shadow: 0 0 0 2px var(--slate-200);
}
.g-notedot[hidden] { display: none; }

.g-btn.sm { height: 32px; padding: 0 12px; border-radius: var(--ds-r-lg); gap: 6px; }
.g-btn.xs { height: 24px; padding: 0 8px; border-radius: var(--ds-r-lg); gap: 4px; }
.g-btn.lg { height: 48px; padding: 0 20px; border-radius: var(--ds-r-xl); font-size: 14px; }
.g-btn.block { width: 100%; }

.g-btn.sky { background: var(--sky-300); border-color: var(--sky-300); }
.g-btn.sky:hover { background: var(--sky-400); }
.g-btn.amber { background: var(--amber-300); border-color: var(--amber-300); }
.g-btn.amber:hover { background: var(--amber-400); }
.g-btn.rose { background: var(--rose-300); border-color: var(--rose-300); }
.g-btn.rose:hover { background: var(--rose-400); }
.g-btn.white { background: #fff; border-color: var(--slate-200); }
.g-btn.white:hover { background: var(--slate-50); }
/* `transparent` has no resting fill, then hovers into the surface's slate. */
.g-btn.transparent { background: transparent; border-color: transparent; }
.g-btn.transparent:hover { background: var(--slate-100); }

/* Outline drops the fill and fades it back in at a tenth on hover. Button's own
   slate border on white is slate-100 at 1.1:1, which is no edge at all — and
   outline is the shape the Color doc reaches for when a control has to read as
   one. So the border takes slate-200, the boundary the system gives a form
   control on a panel. The label still carries the contrast. */
.g-btn.outline { background: transparent; border-color: var(--slate-200); }
.g-btn.outline:hover { background: rgb(241 245 249 / 0.1); }

/* On the page background a neutral is slate-300, hovering to slate-400 — one
   rung off slate-200, and the same values as VxButton's `muted-dark`. Accents
   don't care what they sit on. */
.g-on-page .g-btn { background: var(--slate-300); border-color: var(--slate-300); color: #000; }
.g-on-page .g-btn:hover { background: var(--slate-400); }
.g-on-page .g-btn.sky { background: var(--sky-300); border-color: var(--sky-300); }
.g-on-page .g-btn.sky:hover { background: var(--sky-400); }
.g-on-page .g-btn.transparent { background: transparent; border-color: transparent; }
.g-on-page .g-btn.transparent:hover { background: var(--slate-300); }
/* An accent toggle that is on sits at its hover value, so pressed reads as
   engaged without leaving the one weight accents get. */
.g-on-page .g-btn.sky[aria-pressed="true"] { background: var(--sky-400); }

/* A sunken card hands `sunken` down, where a neutral is slate-300 hovering to
   slate-400 — the same rung as the page, one further from white than glass. */
.g-on-sunken .g-btn { background: var(--slate-300); border-color: var(--slate-300); color: #000; }
.g-on-sunken .g-btn:hover { background: var(--slate-400); }
.g-on-sunken .g-btn.sky { background: var(--sky-300); border-color: var(--sky-300); }
.g-on-sunken .g-btn.sky:hover { background: var(--sky-400); }
.g-on-sunken .g-btn.transparent { background: transparent; border-color: transparent; }
.g-on-sunken .g-btn.transparent:hover { background: var(--slate-300); }
.g-on-sunken .g-btn.outline { background: transparent; border-color: var(--slate-300); }
.g-on-sunken .g-btn.outline:hover { background: rgb(203 213 225 / 0.1); }
/* The filter tokens are neutral controls, so they step with everything else. */
.g-on-sunken .g-filter { background: var(--slate-300); }
.g-on-sunken .g-filter:hover { background: var(--slate-400); }

/* On the dark surface a neutral steps lighter, not darker. */
.g-on-dark .g-btn { background: var(--slate-600); color: #fff; border-color: var(--slate-600); }
.g-on-dark .g-btn:hover { background: var(--slate-500); color: #fff; }
.g-on-dark .g-btn.sky { background: var(--sky-300); color: #000; border-color: var(--sky-300); }
.g-on-dark .g-btn.sky:hover { background: var(--sky-400); }
.g-on-dark .g-btn.transparent { background: transparent; border-color: transparent; color: #fff; }
.g-on-dark .g-btn.transparent:hover { background: var(--slate-600); }

.g-btn[disabled], .g-btn.is-disabled { opacity: 0.5; pointer-events: none; }

/* A text button that is not a box. Links inside body copy go sky-700. */
.g-link {
	border: 0;
	background: none;
	padding: 0;
	font: inherit;
	font-size: 12px;
	font-weight: 600;
	color: var(--sky-700);
	cursor: pointer;
	text-decoration: none;
}
.g-link:hover { color: var(--sky-800); text-decoration: underline; }
.g-link.quiet { color: var(--slate-500); }
.g-link.quiet:hover { color: var(--slate-700); }

/* ============================================================================
   Badge — ui-system Badge.vue. A label, never pressable: 100 fill, 900 text,
   300 border, 500 icon, one weight for every hue.
   ============================================================================ */

.g-badge {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: 24px;
	padding: 0 10px;
	border: 1px solid var(--slate-300);
	border-radius: var(--ds-r-full);
	background: var(--slate-100);
	color: var(--slate-900);
	font-size: 12px;
	font-weight: 500;
	line-height: 1.25;
	white-space: nowrap;
}
.g-badge i { font-size: 12px; color: var(--slate-500); margin: 0 -0.2em; }
.g-badge.xs { height: 20px; padding: 0 8px; gap: 4px; font-size: 12px; }
.g-badge.xs i { font-size: 10px; }
.g-badge.md { height: 28px; padding: 0 12px; gap: 8px; }

/* Slate is what a bare badge already is; naming it lets a caller say so. */
.g-badge.slate { background: var(--slate-100); color: var(--slate-900); border-color: var(--slate-300); }
.g-badge.slate i { color: var(--slate-500); }
.g-badge.sky { background: var(--sky-100); color: var(--sky-900); border-color: var(--sky-300); }
.g-badge.sky i { color: var(--sky-500); }
.g-badge.teal { background: var(--teal-100); color: var(--teal-900); border-color: var(--teal-300); }
.g-badge.teal i { color: var(--teal-500); }
.g-badge.amber { background: var(--amber-100); color: var(--amber-900); border-color: var(--amber-300); }
.g-badge.amber i { color: var(--amber-500); }
.g-badge.rose { background: var(--rose-100); color: var(--rose-900); border-color: var(--rose-300); }
.g-badge.rose i { color: var(--rose-500); }
.g-badge.violet { background: var(--violet-100); color: var(--violet-900); border-color: var(--violet-300); }
.g-badge.violet i { color: var(--violet-500); }

/* ============================================================================
   Alert — status messages. No Alert component exists in ui-system yet, so this
   follows the badge's tint weight on a card-sized box. Neutral is the default:
   teal and rose are only reached for when good and bad are unambiguous.
   ============================================================================ */

.g-alert {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	padding: 14px 16px;
	border: 1px solid var(--slate-300);
	border-radius: var(--ds-r-xl);
	background: var(--slate-100);
	color: var(--slate-900);
	font-size: 14px;
	line-height: 1.5;
}
.g-alert > i:first-child { margin-top: 2px; font-size: 14px; color: var(--slate-500); flex-shrink: 0; }
.g-alert .g-alert-body { min-width: 0; flex: 1; }
.g-alert b { font-weight: 800; }
.g-alert p { margin: 4px 0 0; font-size: 13px; color: inherit; opacity: 0.85; }
.g-alert .g-alert-acts { display: flex; gap: 8px; margin-left: auto; flex-shrink: 0; }

.g-alert.sky { background: var(--sky-100); border-color: var(--sky-300); color: var(--sky-900); }
.g-alert.sky > i:first-child { color: var(--sky-500); }
.g-alert.amber { background: var(--amber-100); border-color: var(--amber-300); color: var(--amber-900); }
.g-alert.amber > i:first-child { color: var(--amber-500); }
.g-alert.rose { background: var(--rose-100); border-color: var(--rose-300); color: var(--rose-900); }
.g-alert.rose > i:first-child { color: var(--rose-500); }
.g-alert.teal { background: var(--teal-100); border-color: var(--teal-300); color: var(--teal-900); }
.g-alert.teal > i:first-child { color: var(--teal-500); }

/* A warning names its section in bold before it says anything else. */
.g-alert .g-section { display: block; font-weight: 800; }

/* ============================================================================
   Page head
   ============================================================================ */

/* No `container-type` here or on the table card. A query container is a
   containing block for its descendants, which takes the sticky table header
   with it. Both queries below did what `sizeColumns` already measures, so they
   are media queries now and the header can stick to the page. */
.g-page {}

/* Title and actions on one row, the mode switch on its own beneath it, so the
   switch can take the width of the thing it switches. The margin below is the
   page's one section break: the title and the switch belong together at 12px,
   and everything the switch governs starts after a clear gap. */
.g-pagehead {
	display: grid;
	gap: 12px;
	margin-bottom: 32px;
}
.g-pagehead-top {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}
.g-pagehead .g-spacer { margin-left: auto; }
.g-pagehead h2 {
	margin: 0;
	font-size: 30px;
	font-weight: 800;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--slate-800);
}
.g-pagehead p { margin: 6px 0 0; font-size: 14px; color: var(--slate-500); }
.g-pagehead .g-pagehead-acts { display: flex; gap: 8px; align-items: center; }

/* ============================================================================
   Card
   ============================================================================ */

.g-card {
	background: #fff;
	border-radius: var(--ds-r-2xl);
	box-shadow: var(--ds-shadow-sm);
	padding: 24px;
}
.g-card.glass { background: var(--slate-50); border: 1px solid #fff; }
.g-card.compact { padding: 16px; }

/* ============================================================================
   Select mode.

   The page narrows to the list and the controls acting on it. Everything else
   drops to 5% — the value the group-level-analytics prototype uses when a
   selection is live — and stops taking clicks or focus. `inert` is set in the
   same place, so a dimmed region is out of the tab order too, not just
   unclickable.
   ============================================================================ */

.g-dim { transition: opacity 200ms var(--ds-ease); }
body.is-selecting .g-dim { opacity: 0.05; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
	.g-dim { transition: none; }
}

/* ============================================================================
   Toolbar — search, saved views, filters, table controls
   ============================================================================ */

/* The find row sits on the page, not in a card: the card is spent on the
   filter band below it, which is the thing that holds content. */
#toolRow { margin-bottom: 12px; }
#toolRow #toolbarWrap { flex: 1 1 340px; min-width: 0; }
.g-toolfind { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Only the search field takes the slack. The rest hold their natural width:
   a `.g-selectbox` sets `overflow: hidden`, which resolves its min-content width
   to 0, and that 0 propagates up to the popover anchor wrapping it — so the
   anchor is what collapses, and the anchor is the flex item. */
.g-toolfind > .g-pop-anchor,
.g-toolfind > .g-seg { flex: none; }

/* The actions hold the end of the row through the find controls collapsing
   away beneath them, so they never move while select mode turns on. */
.g-acts { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* The filter band is `Card.vue` at `surface="sunken"`, `padding="compact"`:
   slate-300 at 30%, an inner shadow, and a white bottom border — the one edge
   that sells a recess, lit from above. The filters are a well the page's
   controls sit down in, which is what the surface is for. */
.g-filterbar {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	background: rgb(203 213 225 / 0.3);
	border-bottom: 1px solid #fff;
	border-radius: var(--ds-r-2xl);
	box-shadow: var(--ds-shadow-inner);
	padding: 16px;
}
/* Saved views lead, the tokens run to the end. */
.g-filterbar .g-filters { margin-left: auto; }
#filterWrap { margin-bottom: 16px; }

.g-toolrow {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}
.g-toolrow .g-spacer { margin-left: auto; }

/* Search — vui `Input.vue` at `md`: h-10, rounded-xl, text-sm, 16px of inline
   padding. The icon is the component's `left` slot, which sits 16px in and
   reserves a 16px box, and the value starts 8px past it — 16 + 16 + 8 = 40px.
   Flex here rather than the component's absolute slot, which measures the slot
   and writes the padding back; the arithmetic is the same and it is static. */
.g-search {
	display: flex;
	align-items: center;
	gap: 8px;
	height: 40px;
	min-width: 280px;
	flex: 1 1 320px;
	max-width: 460px;
	padding: 0 16px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	background: #fff;
	transition: box-shadow 150ms var(--ds-ease);
}
.g-search:focus-within { outline: 2px solid #2563eb; outline-offset: 2px; }
/* `useControlBoundary`: on the page background a control has no border to read
   against, so the edge is a shadow and the border only holds its 1px so an
   error state can recolor it without moving the text. On a panel it is the
   other way round. */
.g-on-page .g-search { border-color: transparent; box-shadow: var(--ds-shadow-sm); }
.g-on-page .g-search:hover { box-shadow: var(--ds-shadow-md); }
/* The slot is a fixed 16px box, not the glyph's own width, so the value starts
   at 40px whatever icon is in it. */
.g-search i {
	flex: none;
	width: 16px;
	text-align: center;
	color: var(--slate-500);
	font-size: 14px;
}
/* Group by and Columns — vui `Select.vue` at `md` on the page background, the
   same edge the search field takes there: white fill, `shadow` rising to
   `shadow-md`, a transparent border holding its 1px, `h-10`, `rounded-xl`,
   `text-sm`, and the chevron 14px off the end at 12px in slate-500. They open a
   menu rather than a native list, so the shape is Select's and the behaviour is
   the page's own popover. The one departure: Select absolutely positions its
   chevron over a `pe-10` field, and these are flex rows, so the label sits 8px
   off the chevron rather than the 40px that padding implies. */
.g-selectbox {
	display: inline-flex;
	align-items: stretch;
	/* `overflow: hidden` below resolves the automatic minimum size to 0, so
	   without this the control shrinks to nothing beside a growing search field
	   rather than holding its label. */
	flex: none;
	height: 40px;
	border: 1px solid transparent;
	border-radius: var(--ds-r-xl);
	background: #fff;
	box-shadow: var(--ds-shadow-sm);
	transition: box-shadow 150ms var(--ds-ease);
	overflow: hidden;
}
.g-selectbox:hover { box-shadow: var(--ds-shadow-md); }
.g-selectbox:focus-within { outline: 2px solid #2563eb; outline-offset: 2px; }
.g-selectbox > button {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 0 14px 0 16px;
	border: 0;
	background: transparent;
	font: inherit;
	font-size: 14px;
	line-height: 1.25;
	color: var(--slate-900);
	white-space: nowrap;
	cursor: pointer;
	transition: background-color 150ms var(--ds-ease);
}
.g-selectbox > button:hover { background: var(--slate-50); }
.g-selectbox > button > i { font-size: 12px; color: var(--slate-500); }
.g-selectbox b { font-weight: 700; font-variant-numeric: tabular-nums; }
/* A second segment, divided by a hairline rather than a gap, so the pair still
   reads as one control. */
.g-selectbox > button + button {
	border-left: 1px solid var(--slate-200);
	padding: 0 14px;
}

.g-search input {
	border: 0;
	outline: none;
	background: transparent;
	font: inherit;
	font-size: 14px;
	line-height: 1.25;
	color: var(--slate-900);
	width: 100%;
	min-width: 0;
}

/* ---- Filter tokens. Pressable, so they are neutral controls, not badges, and
        they take Button's `sm` shape: 32px tall, 12px of padding, 8px radius.
        A pill would read as a Badge, which is the one thing here that is not
        pressable. */
.g-filters {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	min-height: 32px;
}
/* A token is Button at `md` that happens to hold three spans: the same 40px
   height, 16px of padding, xl radius, 12px semibold label. It sits on the row
   next to the saved view, which is a real Button, and the two have to measure
   the same. */
.g-filter {
	display: inline-flex;
	align-items: stretch;
	border-radius: var(--ds-r-xl);
	background: var(--slate-100);
	overflow: hidden;
	max-width: 340px;
	transition: background-color 150ms var(--ds-ease), opacity 150ms var(--ds-ease);
}
.g-filter > button {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	height: 40px;
	padding: 0 16px;
	border: 0;
	background: transparent;
	font: inherit;
	font-size: 12px;
	font-weight: 600;
	color: #000;
	cursor: pointer;
	min-width: 0;
	transition: background-color 150ms var(--ds-ease);
}
/* Hover lifts the whole token one rung. `Button.vue` paints its fill on a layer
   covering the control, so hovering anywhere moves all of it; painting only the
   segment under the pointer left the label a rung darker than the close beside
   it, which reads as a patch rather than as one control. */
.g-filter:hover { background: var(--slate-200); }
.g-filter > button:focus-visible { outline: 2px solid #2563eb; outline-offset: -2px; }
.g-filter .g-filter-field { color: var(--slate-600); font-weight: 500; }
.g-filter .g-filter-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-filter .g-filter-x { padding: 0 14px 0 10px; color: var(--slate-600); }
.g-filter .g-filter-x i { font-size: 10px; }
/* Open keeps the token's own colour and drops back to half: the popover under
   it is what the eye is on, and the token is only saying which control the
   popover belongs to. An accent here made the token compete with the popover
   for the same attention. Hover holds the resting fill while it is open —
   the control is already engaged, so a rung of hover on top says nothing. */
.g-filter.is-open { opacity: 0.5; }
.g-filter.is-open:hover { background: var(--slate-100); }
.g-on-sunken .g-filter.is-open:hover { background: var(--slate-300); }

/* Group by is the one token that is not holding a filter value, so nothing in
   it can run long and it takes no cap. */



/* ============================================================================
   Popover — one shape for every menu hung off a control
   ============================================================================ */

.g-pop-anchor { position: relative; }
.g-pop {
	position: absolute;
	z-index: 70;
	top: calc(100% + 6px);
	left: 0;
	width: 300px;
	background: #fff;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	box-shadow: var(--ds-shadow-lg);
	overflow: hidden;
}
.g-pop.right { left: auto; right: 0; }
.g-pop.wide { width: 380px; }
/* A menu hung off a control inside a scroll container, so it lives on the body
   and is placed from the trigger's own rect. */
.g-menu { position: fixed; top: 0; left: 0; width: 230px; }
.g-pop-head {
	padding: 14px 16px 10px;
	border-bottom: 1px solid var(--slate-100);
}
.g-pop-head b { display: block; font-size: 14px; font-weight: 800; color: var(--slate-800); }
.g-pop-head small { display: block; margin-top: 4px; font-size: 12px; line-height: 1.45; color: var(--slate-500); }
.g-pop-body { max-height: 360px; overflow-y: auto; padding: 8px; }
.g-pop-foot {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 12px;
	background: var(--slate-50);
	border-top: 1px solid var(--slate-100);
}
.g-pop-foot small { font-size: 12px; color: var(--slate-500); line-height: 1.4; }
.g-pop-group { display: block; padding: 10px 8px 4px; font-size: 12px; font-weight: 800; color: var(--slate-500); }
.g-pop-item {
	display: flex;
	align-items: center;
	gap: 10px;
	width: 100%;
	padding: 8px 10px;
	border: 0;
	border-radius: var(--ds-r-lg);
	background: transparent;
	font: inherit;
	font-size: 13px;
	color: var(--slate-800);
	text-align: left;
	cursor: pointer;
}
.g-pop-item:hover { background: var(--slate-100); }
.g-pop-item.is-on { background: var(--sky-100); color: var(--sky-900); }
.g-pop-item .g-pop-meta { margin-left: auto; font-size: 11px; color: var(--slate-500); }
.g-pop-item[disabled] { opacity: 0.45; cursor: default; }
.g-pop-item[disabled]:hover { background: transparent; }

/* ---- Column picker: one ordered list. Drag by the grip, or step a row with
        the buttons. Group and Actions are pinned at the ends. */
.g-colrow {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 2px 4px 2px 2px;
	border-radius: var(--ds-r-lg);
}
.g-colrow:hover { background: var(--slate-100); }
.g-colrow.is-dragging { opacity: 0.4; background: var(--sky-50); }
.g-colgrip {
	flex: none;
	width: 14px;
	font-size: 11px;
	color: var(--slate-300);
	cursor: grab;
	text-align: center;
}
.g-colrow:active .g-colgrip { cursor: grabbing; }
.g-colrow.is-pinned { opacity: 0.55; }
.g-colrow.is-pinned .g-colgrip { cursor: default; }
.g-colname {
	display: flex;
	align-items: center;
	gap: 10px;
	flex: 1;
	min-width: 0;
	padding: 7px 4px;
	border: 0;
	background: transparent;
	font: inherit;
	font-size: 13px;
	color: var(--slate-800);
	text-align: left;
	cursor: pointer;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.g-colrow.is-pinned .g-colname { cursor: default; }
.g-colmoves { display: flex; flex: none; gap: 0; }
.g-colmoves .g-btn { width: 22px; padding: 0; color: var(--slate-500); }
.g-colmoves .g-btn:hover { color: #000; }
.g-colmoves .g-btn[disabled] { opacity: 0.25; }
.g-colpin { flex: none; padding-right: 6px; font-size: 10px; color: var(--slate-300); }

/* ============================================================================
   Checkbox / radio — ui-system Checkbox.vue and Radio.vue. Checked fills
   sky-300 with a black mark; a radio is a sky-500 ring with a dot, not a fill.
   ============================================================================ */

/* The table card, full width. Selection is a column inside it now, so there is
   no gutter beside it to make room for. */
.g-tablerow { display: flex; align-items: flex-start; }
.g-tablerow > .g-tablewrap { flex: 1; min-width: 0; }
/* The select column: a fixed, narrow cell with the box centred in it. */
table.g-table th.g-checkcell, table.g-table td.g-checkcell {
	padding-inline: 0;
	text-align: center;
}
.g-checkbtn {
	display: inline-grid;
	place-items: center;
	padding: 6px;
	border: 0;
	background: transparent;
	cursor: pointer;
	border-radius: var(--ds-r-md);
}
.g-checkbtn:focus-visible { outline: 2px solid #2563eb; outline-offset: 0; }
/* The box only fills once it is on, so an unchecked column of 50 rows stays
   quiet: the border carries it at rest and the hover says it is pressable. */
.g-checkbtn:hover .g-check { border-color: var(--slate-400); }

.g-check {
	display: inline-grid;
	place-items: center;
	width: 20px;
	height: 20px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-md);
	background: #fff;
	color: transparent;
	font-size: 12px;
	flex-shrink: 0;
	cursor: pointer;
	transition: background-color 150ms var(--ds-ease), border-color 150ms var(--ds-ease);
}
.g-check.sm { width: 16px; height: 16px; border-radius: var(--ds-r); font-size: 10px; }
.g-check.is-on, .g-check.is-part { background: var(--sky-300); border-color: var(--sky-500); color: #000; }
/* The mark is a real icon element, not a hardcoded codepoint and family: the
   vendored build is Font Awesome 7 Pro, and a literal "\f00c" in "Font Awesome
   6 Free" resolves to nothing but a fallback box. */
.g-check i { font-size: inherit; line-height: 1; }

.g-radio-row {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	cursor: pointer;
	padding: 2px 0;
}
.g-radio {
	display: grid;
	place-items: center;
	width: 16px;
	height: 16px;
	margin-top: 2px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-full);
	background: #fff;
	flex-shrink: 0;
	transition: border-color 150ms var(--ds-ease);
}
.g-radio-row.is-on .g-radio { border: 2px solid var(--sky-500); background: transparent; }
.g-radio-row.is-on .g-radio::after {
	content: "";
	width: 8px;
	height: 8px;
	border-radius: var(--ds-r-full);
	background: var(--sky-500);
}
.g-radio-label { min-width: 0; }
.g-radio-label b { display: block; font-size: 14px; font-weight: 500; line-height: 1.4; color: var(--slate-800); }
.g-radio-label small { display: block; margin-top: 2px; font-size: 12px; line-height: 1.4; color: var(--slate-500); }
.g-radio-group { display: grid; gap: 10px; }

/* A radio row drawn as a card, for options that carry their own description. */
.g-radio-card {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	width: 100%;
	padding: 12px 14px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	background: #fff;
	font: inherit;
	text-align: left;
	cursor: pointer;
	transition: border-color 150ms var(--ds-ease), background-color 150ms var(--ds-ease);
}
.g-radio-card:hover { border-color: var(--slate-300); }
.g-radio-card.is-on { border-color: var(--sky-500); background: var(--sky-50); }
.g-radio-card.is-on .g-radio { border: 2px solid var(--sky-500); background: transparent; }
.g-radio-card.is-on .g-radio::after {
	content: "";
	width: 8px;
	height: 8px;
	border-radius: var(--ds-r-full);
	background: var(--sky-500);
}
.g-radio-card > div { flex: 1; min-width: 0; }
.g-radio-card b { display: block; font-size: 14px; font-weight: 700; color: var(--slate-800); }
.g-radio-card small { display: block; margin-top: 3px; font-size: 12px; line-height: 1.45; color: var(--slate-500); }
.g-radio-card .g-badge { margin-left: auto; flex-shrink: 0; }

.g-field-label {
	display: block;
	font-size: 14px;
	font-weight: 800;
	color: var(--slate-800);
	margin: 0 0 6px;
}
.g-field-label + .g-field-desc { margin: -4px 0 8px; font-size: 12px; color: var(--slate-500); }
.g-field { display: grid; gap: 0; }
.g-field + .g-field { margin-top: 20px; }
.g-input {
	width: 100%;
	box-sizing: border-box;
	height: 40px;
	padding: 0 16px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	background: #fff;
	font: inherit;
	font-size: 14px;
	color: var(--slate-900);
	outline-color: #2563eb;
}
.g-input:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }

/* ============================================================================
   View mode — All groups / Scratch pad.

   A mode of the page, not a consequence of the selection, so it sits at the
   top of the content and is always on screen. Anything that appears only once
   a row is picked pushes the table down at the exact moment someone is aiming
   at it.
   ============================================================================ */

/* Corners take VxTabs' container radius in voxie/frontend-components:
   `rounded-lg`, which on that repo's overridden radius scale (src/tailwind.css)
   is 12px, not Tailwind's stock 8. The segment radius is the nesting rule
   rather than VxTabs' `rounded-md`: an inner corner nests cleanly only at the
   outer radius less the distance between the two edges, which here is the 1px
   border plus 4px of padding. 12 − 5 = 7. Everything else here is its own. */
.g-seg {
	display: inline-flex;
	padding: 4px;
	gap: 2px;
	border: 1px solid #fff;
	border-radius: 0.75rem;
	background: rgb(255 255 255 / 0.6);
}
.g-seg button {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	border: 0;
	background: transparent;
	/* 12px container radius, less its 1px border and 4px padding. */
	border-radius: 7px;
	padding: 7px 16px;
	font: inherit;
	font-size: 13px;
	font-weight: 600;
	color: var(--slate-600);
	cursor: pointer;
	white-space: nowrap;
	transition: background-color 150ms var(--ds-ease), color 150ms var(--ds-ease);
}
/* Full width, for a control that is the whole width of what it sits in. */
.g-seg.block { display: flex; width: 100%; }
.g-seg.block button { flex: 1 1 0; justify-content: center; }
/* On a white panel the track steps to slate-100; the page's glass would vanish. */
.g-seg.panel { background: var(--slate-100); border-color: var(--slate-200); }

/* On the page a neutral steps to slate-300, so a tab bar's track steps with it
   — the same rung the buttons beside it take, scoped the same way. `panel` is
   the white-surface track; used here it would sit lighter than the surface it
   is drawn on, and the white selected segment would have nothing to lift off. */
.g-on-page .g-seg { background: var(--slate-300); border-color: var(--slate-300); }

/* Toolbar size, so the control lines up with the `md` Buttons either side of
   it: 40px overall, less the 1px border and 4px of track padding top and
   bottom, leaves the segment 30px. The label drops to the toolbar's 12px. */
.g-seg.sm button { height: 30px; padding: 0 12px; font-size: 12px; }

/* Icon-only segments are square — `aspect-ratio` against whatever height the
   size class gave them, so the glyph sits centred rather than in a wide slab.
   The glyph steps up a point from the toolbar's label size: it is the whole
   content of the control, not a marker beside a word. */
.g-seg.icon button { padding: 0; aspect-ratio: 1; font-size: 13px; justify-content: center; }

.g-seg button:hover { color: var(--slate-800); }
.g-seg button[aria-pressed="true"] { background: #fff; color: var(--slate-900); box-shadow: var(--ds-shadow-xs); }
.g-seg button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }

/* The count is a label on a control, so it takes the slate tint rather than a
   second weight of the control's own fill. */
.g-seg-n {
	display: inline-grid;
	place-items: center;
	min-width: 20px;
	height: 20px;
	padding: 0 6px;
	border: 1px solid var(--slate-300);
	border-radius: var(--ds-r-full);
	background: var(--slate-100);
	color: var(--slate-900);
	font-size: 12px;
	font-weight: 500;
	font-variant-numeric: tabular-nums;
}

/* ============================================================================
   Table
   ============================================================================ */

.g-tablewrap {
	background: #fff;
	border-radius: var(--ds-r-2xl);
	box-shadow: var(--ds-shadow-sm);
	/* The card owns its corners, so the head and the foot cannot carry a square
	   corner into view when the scroller rubber-bands past its end. `clip`, not
	   `hidden`: it rounds the content off without becoming a scroll container,
	   and a scroll container here would be the one the sticky header answers
	   to — a container that never scrolls is a header that never sticks. Row
	   menus are on the body (`floatMenu`), so nothing inside needs to escape. */
	overflow: clip;
}

/* The horizontal scroller only exists when the columns genuinely cannot fit,
   because turning it on is what takes the sticky header away. `sizeColumns`
   decides. */
.g-tablescroll.is-scrolling { overflow-x: auto; }

table.g-table {
	width: 100%;
	table-layout: fixed;
	border-collapse: collapse;
	/* Density is one variable set; the row, the type and the gutters all read
	   from it, so a change lands everywhere at once. */
	--row-pad-y: 12px;
	--row-pad-x: 12px;
	--row-font: 13px;
	--row-sub: 12px;
	--name-font: 16px;
}
.g-table.density-dense { --row-pad-y: 5px; --row-pad-x: 8px; --row-font: 12px; --row-sub: 11px; --name-font: 13px; }

/* Content-aware density: the narrower each column gets, the tighter the row.
   A container query rather than a media query, so it answers to the table's
   own width and not the window's. */
@media (max-width: 1100px) {
	.g-toolrow .g-spacer { margin-left: 0; }
	.g-search { max-width: none; }
}

table.g-table th {
	position: sticky;
	top: 0;
	z-index: 3;
	background: var(--slate-50);
	border-bottom: 1px solid var(--slate-200);
	text-align: left;
	padding: 10px var(--row-pad-x);
	font-size: 12px;
	font-weight: 700;
	color: var(--slate-600);
	white-space: nowrap;
	cursor: pointer;
	user-select: none;
}
table.g-table th:first-child { padding-left: 16px; }
table.g-table th:last-child { padding-right: 16px; }
table.g-table th .g-th {
	display: flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
}
/* Headers truncate rather than force the column wider than its content needs. */
table.g-table th .g-th span {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
table.g-table th .g-th i { font-size: 9px; color: var(--slate-400); opacity: 0; flex-shrink: 0; }
table.g-table th:hover .g-th i { opacity: 1; }
table.g-table th.is-sorted { color: var(--sky-800); }
table.g-table th.is-sorted .g-th i { opacity: 1; color: var(--sky-600); }
table.g-table th.num .g-th { justify-content: flex-end; }
table.g-table th:focus-visible { outline: 2px solid #2563eb; outline-offset: -2px; }

table.g-table td {
	padding: var(--row-pad-y) var(--row-pad-x);
	font-size: var(--row-font);
	color: var(--slate-700);
	vertical-align: middle;
	border-bottom: 1px solid var(--slate-100);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
table.g-table td:first-child { padding-left: 16px; }
table.g-table td:last-child { padding-right: 16px; }

/* A cell whose content is a chip rather than text. Truncation would measure the
   chip as overflowing and draw an ellipsis beside it; the column is sized to
   fit the widest chip instead. */
table.g-table td.g-fitcell, table.g-table th.g-fitcell {
	overflow: visible;
	text-overflow: clip;
}
table.g-table td .g-sub {
	display: block;
	font-size: var(--row-sub);
	color: var(--slate-500);
	margin-top: 2px;
	overflow: hidden;
	text-overflow: ellipsis;
}
.g-table.density-dense td .g-sub { display: none; }
table.g-table td.num { text-align: right; }

/* Rows step through one neutral ladder: white, slate-50 on hover, slate-100
   when picked. No state is signalled by color alone — picked also carries a
   checked box and the sky rail. */
table.g-table tbody tr { background: #fff; transition: background-color 120ms var(--ds-ease), opacity 150ms var(--ds-ease); }
table.g-table tbody tr:hover { background: var(--slate-50); }
table.g-table tbody tr.is-sel { background: var(--slate-100); }
table.g-table tbody tr.is-sel:hover { background: var(--slate-200); }
table.g-table tbody tr.is-sel td:first-child { box-shadow: inset 3px 0 0 0 var(--sky-500); }
table.g-table tbody tr:focus-visible { outline: 2px solid #2563eb; outline-offset: -2px; }
.g-table.is-selecting tbody tr { cursor: pointer; }

/* With a selection running, everything outside it fades back so the picked
   rows are what the eye lands on. Hover brings a row back to full. */
.g-table.has-sel tbody tr:not(.is-sel):not(.g-grouprow):not(.g-morerow) { opacity: 0.4; }
.g-table.has-sel tbody tr:not(.is-sel):hover { opacity: 1; }

.g-name { font-size: var(--row-font); font-weight: 700; color: var(--slate-800); }

/* Group cell: an ID badge and the name on one baseline, the shape the campaign
   list already uses. The badge is a label, so it takes no hover and no cursor;
   the name is not a link, so it takes neither either. */
.g-groupname { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.g-idbadge {
	flex: none;
	/* A truncated CRM record id says nothing, so it gets only enough room to be
	   recognisable as one. Turn on the External ID column to read them in full. */
	max-width: 72px;
	overflow: hidden;
	text-overflow: ellipsis;
	display: inline-block;
	text-align: center;
	/* A badge is h-5 with px-2, which inline-block has to be told. */
	line-height: 18px;
	padding: 0 8px;
}
.g-namen {
	min-width: 0;
	font-size: var(--name-font);
	font-weight: 700;
	line-height: 1.4;
	color: var(--slate-800);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.g-mono { font-variant-numeric: tabular-nums; }
/* An external ID is a machine's string, not a word: set it in mono so a long
   CRM record number can be read and compared a character at a time. */
.g-ext { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
.g-muted { color: var(--slate-500); }

/* OU reads as a link, matching how an OU is linked everywhere else. */
.g-ou { color: var(--sky-700); text-decoration: none; }
.g-ou:hover { color: var(--sky-800); text-decoration: underline; }

/* ---- Group-by header rows */

/* The header pins under the table head while its own section scrolls past. At
   the sizes this list reaches, a section is longer than the window, and a
   header you have scrolled away from stops answering the only question it is
   there to answer: which OU, or which status, is this row in.

   A cell's sticky container is the whole table, not its section, so each
   header stays pinned once it lands — and the next one, reaching the same
   offset, paints over it, because at equal z-index the later element in the
   document wins. That overlap is the handover; there is no moment with two of
   them on screen. The offset is the head's measured height, published as
   `--gt-head-h` by `sizeColumns`.

   Under the head's own z-index, so the head is never the thing covered. The
   border goes on as a shadow: a collapsed border belongs to the table, not the
   cell, and stops being painted once the cell is stuck. */
tr.g-grouprow td {
	position: sticky;
	top: var(--gt-head-h, 53px);
	z-index: 2;
	background: var(--slate-50);
	box-shadow: inset 0 -1px 0 var(--slate-200);
	padding: 8px 16px;
	font-size: 12px;
	font-weight: 800;
	color: var(--slate-700);
	white-space: nowrap;
	cursor: pointer;
}
tr.g-grouprow .g-grouprow-in { display: flex; align-items: center; gap: 10px; }
tr.g-grouprow i.g-caret { font-size: 10px; color: var(--slate-500); width: 10px; }

/* ---- Opening a section
   A section opens by arriving rather than by appearing: the rows lift the last
   few pixels into place while the caret turns under them. The house curve and
   the toast's 8px, because this is the same interface moving and not a second
   one — a page where every component brings its own timing reads as several
   products stacked up.

   Only a screenful is animated (`WAKE_ROWS`). Opening *Active* renders 1,520
   rows; 1,500 of them would animate below the fold, which is a cost paid for
   motion nobody can see. Dense fits nearly twice as many rows on a screen as
   Comfortable, so the count covers the denser of the two.

   The stagger stops counting at the twelfth row: enough grain that the section
   reads as unfolding rather than blinking, and the rows past it all join on the
   same beat instead of trailing a queue down the page. It puts the last row
   home at 300ms — a tail longer than that stops feeling like a response to the
   press that caused it.

   Closing has no motion at all — see the note on the press in `index.html`.
   `backwards` fill, so a row waiting out its delay holds at opacity 0 rather
   than showing up and then animating in. */
@keyframes g-section-row { from { opacity: 0; transform: translateY(-8px); } }
tr.is-waking {
	animation: g-section-row 180ms var(--ds-ease) backwards;
	animation-delay: calc(min(var(--wake, 0), 12) * 10ms);
}

/* Open is the glyph as drawn; shut is the same glyph a quarter turn round, so
   the pair is one mark at two angles rather than two marks. */
tr.g-grouprow i.g-caret.is-shut { transform: rotate(-90deg); }
@keyframes g-caret-turn { from { transform: rotate(-90deg); } }
tr.g-grouprow.is-turning i.g-caret { animation: g-caret-turn 200ms var(--ds-ease) backwards; }

@media (prefers-reduced-motion: reduce) {
	tr.is-waking, tr.g-grouprow.is-turning i.g-caret { animation: none; }
}
tr.g-grouprow .g-grouprow-n { font-weight: 500; color: var(--slate-500); }
tr.g-grouprow .g-spacer { margin-left: auto; }

/* ---- The end of a section's page
   A section says how much of itself is on screen and extends itself from there.
   It sits inside the table because that is where the section is: a control in
   the table's foot could only ever mean "more of everything", which under a
   group header is not a question anyone is asking. Quiet — it is a seam in the
   list, not a row of it, so it takes the muted button and states the count
   beside it rather than competing with the groups above. */
tr.g-morerow td {
	background: #fff;
	border-bottom: 1px solid var(--slate-100);
	padding: 10px 16px;
}
tr.g-morerow .g-morerow-in { display: flex; align-items: center; gap: 12px; }
tr.g-morerow .g-morerow-n { font-size: 12px; color: var(--slate-500); }

/* ---- The money row: an annotation attached to the row above it */
tr.g-rownote td {
	background: var(--amber-100);
	border-bottom: 1px solid var(--amber-300);
	padding: 10px 16px;
	white-space: normal;
}
tr.g-rownote .g-rownote-in {
	display: flex;
	gap: 10px;
	font-size: 12px;
	line-height: 1.5;
	color: var(--amber-900);
	max-width: 100ch;
}
tr.g-rownote i { margin-top: 2px; color: var(--amber-500); flex-shrink: 0; }
tr.g-rownote b { font-weight: 800; }

/* ---- Sending status. Live is a filled dot and the word; not live is a hollow
        dot and the word. The shape carries it without the color. */
.g-live { display: inline-flex; align-items: center; gap: 7px; font-size: var(--row-font); font-weight: 600; }
.g-live .g-dot {
	width: 8px;
	height: 8px;
	border-radius: var(--ds-r-full);
	flex-shrink: 0;
}
.g-live.on { color: var(--sky-800); }
.g-live.on .g-dot { background: var(--sky-500); }
.g-live.off { color: var(--slate-600); }
.g-live.off .g-dot { border: 1.5px solid var(--slate-400); background: transparent; }

/* ---- T&Cs progress, as a four-node line. Filled nodes are done; hovering the
        cell names the step and its date. */
/* The T&Cs marker: a pill in a rail. The rail is the whole journey, the pill
   is how much of it is done. */
.g-rail {
	display: inline-block;
	vertical-align: middle;
	width: 56px;
	height: 6px;
	margin-right: 8px;
	box-sizing: border-box;
	border-radius: var(--ds-r-full);
	background: var(--slate-100);
	overflow: hidden;
}
/* Nothing sent: an outline, because there is no progress to draw. */
.g-rail.is-empty { background: transparent; border: 1px solid var(--slate-300); }
.g-rail-fill {
	display: block;
	height: 100%;
	border-radius: var(--ds-r-full);
	background: var(--slate-400);
	transition: width 200ms var(--ds-ease), background-color 200ms var(--ds-ease);
}
.g-rail.is-full .g-rail-fill { background: var(--sky-500); }
.g-table.density-dense .g-rail { width: 44px; height: 5px; margin-right: 6px; }

/* The same linear process at full size, with each label under its own node. */
.g-steps { display: flex; }
.g-step { flex: 1; position: relative; text-align: center; }
.g-step + .g-step::before {
	content: "";
	position: absolute;
	top: 6px;
	right: 50%;
	left: -50%;
	height: 2px;
	background: var(--slate-200);
}
.g-step.is-done + .g-step::before, .g-step.is-done::before { background: var(--sky-500); }
.g-step-dot {
	position: relative;
	z-index: 1;
	display: block;
	width: 14px;
	height: 14px;
	margin: 0 auto;
	border: 2px solid var(--slate-300);
	border-radius: var(--ds-r-full);
	background: #fff;
	box-sizing: border-box;
}
.g-step.is-done .g-step-dot { background: var(--sky-500); border-color: var(--sky-500); }
.g-step.is-now .g-step-dot { border-color: var(--sky-500); }
.g-step small { display: block; margin-top: 8px; font-size: 11px; color: var(--slate-500); }
.g-step.is-done small, .g-step.is-now small { color: var(--slate-700); font-weight: 600; }

/* ---- Row overflow menu */
.g-rowmenu { display: flex; justify-content: flex-end; }

/* ---- Table foot */
.g-tablefoot {
	display: flex;
	align-items: center;
	gap: 14px;
	flex-wrap: wrap;
	padding: 12px 16px;
	border-top: 1px solid var(--slate-100);
	background: var(--slate-50);
	font-size: 12px;
	color: var(--slate-500);
}
.g-tablefoot .g-spacer { margin-left: auto; }

.g-empty { padding: 56px 24px; text-align: center; }
.g-empty b { display: block; font-size: 16px; font-weight: 800; color: var(--slate-800); margin-bottom: 6px; }
.g-empty p { margin: 0 auto 16px; max-width: 46ch; font-size: 13px; line-height: 1.5; color: var(--slate-500); }

/* ============================================================================
   Floating actions.

   Matches the edit HUD in ~/Code/prototypes franchise-hub-dashboard
   (FranchiseHub.vue, the panel that holds #floatingActions):

     fixed bottom-4 left-1/2 z-50 flex w-[calc(100vw-32px)] -translate-x-1/2
     flex-col items-stretch gap-3 rounded-2xl border border-solid border-white
     bg-white/90 p-4 shadow-2xl shadow-sky-950/40 backdrop-blur-md
     sm:bottom-8 sm:max-w-5xl sm:p-6

   Width and breakpoints are that prototype's: no cap below Tailwind's `sm`
   (40rem / 640px), where it also picks up the wider offset and padding, and
   `max-w-5xl` (64rem / 1024px) above it. Fixed, not sticky, so it never takes
   part in the page's layout.
   ============================================================================ */

.g-floatbar {
	position: fixed;
	bottom: 16px;
	left: 50%;
	z-index: 50;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	box-sizing: border-box;
	width: calc(100vw - 32px);
	/* The padding sits on the two sections, not here, so the footer's slate
	   fill can run to the bar's edges. `overflow: hidden` keeps it inside the
	   corner radius. */
	border: 1px solid #fff;
	border-radius: var(--ds-r-2xl);
	overflow: hidden;
	background: rgb(255 255 255 / 0.9);
	box-shadow: 0 25px 50px -12px rgb(8 47 73 / 0.4);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	/* Out: down, back and faded, and quicker than it came in. */
	opacity: 0;
	pointer-events: none;
	transform: translateX(-50%) translateY(2rem) scale(0.98);
	transition: opacity 150ms var(--ds-ease), transform 150ms var(--ds-ease);
}
.g-floatbar.is-on {
	opacity: 1;
	pointer-events: auto;
	transform: translateX(-50%) translateY(0) scale(1);
	transition-duration: 300ms;
}
/* Tailwind's `sm`. Everything the reference defers to this breakpoint moves
   together: the offset, the padding and the cap. */
@media (min-width: 40rem) {
	.g-floatbar { bottom: 32px; max-width: 64rem; }
	.g-floatscroll, .g-floatfoot { padding: 24px; }
}
@media (prefers-reduced-motion: reduce) {
	.g-floatbar { transform: translateX(-50%); }
	.g-floatbar.is-on { transform: translateX(-50%); }
}

/* Room under the last row for the bar, which is fixed over the list. */
.g-page.has-floatbar { padding-bottom: 210px; }

/* The actions scroll rather than wrap. Wrapping would grow the bar upward and
   move the buttons out from under the pointer that was aiming at one. */
.g-floatscroll { display: flex; align-items: center; min-width: 0; padding: 16px; }
.g-floatacts {
	display: flex;
	gap: 8px;
	flex: 1;
	min-width: 0;
	overflow-x: auto;
	scroll-behavior: smooth;
	scrollbar-width: none;
}
.g-floatacts::-webkit-scrollbar { display: none; }
.g-floatacts .g-btn { flex: none; }

/* The steppers. Quiet enough to sit beside the actions without competing, and
   disabled at either end rather than hidden — an arrow that vanishes when you
   reach the end takes the affordance with it. */
.g-floatstep {
	display: grid;
	/* Flush to the panel's padding rather than centred in its own hit area: the
	   reference nests these in an inset drawer box, and without that box a
	   centred glyph misses the count and Cancel below it. */
	place-items: center start;
	flex: none;
	width: 1.6rem;
	height: 1.6rem;
	padding: 0;
	border: 0;
	background: transparent;
	border-radius: 0.45rem;
	font-size: 0.7rem;
	color: var(--slate-500);
	cursor: pointer;
	transition: background-color 150ms var(--ds-ease), color 150ms var(--ds-ease);
}
.g-floatstep:first-of-type { margin-right: 0.35rem; }
.g-floatstep:last-of-type { margin-left: 0.35rem; place-items: center end; }
.g-floatstep:hover:not(:disabled) { background: var(--slate-200); color: var(--slate-900); }
.g-floatstep:disabled { color: var(--slate-300); cursor: default; }

/* Count and Cancel hold still while the actions scroll past above them. The
   slate fill separates them from the actions without a rule. */
.g-floatfoot {
	display: flex;
	width: 100%;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 16px;
	padding-left: 20px;
	border-top: 1px solid var(--slate-200);
	background: var(--slate-100);
}
.g-selcount {
	display: flex;
	align-items: baseline;
	gap: 5px;
	min-width: 0;
	font-size: 18px;
	color: var(--slate-700);
	white-space: nowrap;
}
.g-selcount b { font-weight: 800; }
.g-selcount .g-floatsub { color: var(--slate-400); overflow: hidden; text-overflow: ellipsis; }

/* The bar is glass, which hands `glass` down to what sits on it, so a neutral
   control here is slate-200 and hovers to slate-300. Accents do not care what
   they sit on, so they hold their 300 everywhere. */
.g-on-glass .g-btn { background: var(--slate-200); border-color: var(--slate-200); color: #000; }
.g-on-glass .g-btn:hover { background: var(--slate-300); }
.g-on-glass .g-btn.sky { background: var(--sky-300); border-color: var(--sky-300); }
.g-on-glass .g-btn.sky:hover { background: var(--sky-400); }
.g-on-glass .g-btn.amber { background: var(--amber-300); border-color: var(--amber-300); }
.g-on-glass .g-btn.amber:hover { background: var(--amber-400); }
.g-on-glass .g-btn.rose { background: var(--rose-300); border-color: var(--rose-300); }
.g-on-glass .g-btn.rose:hover { background: var(--rose-400); }
.g-on-glass .g-btn.white { background: #fff; border-color: var(--slate-200); }
.g-on-glass .g-btn.white:hover { background: var(--slate-50); }
.g-on-glass .g-btn.transparent { background: transparent; border-color: transparent; }
.g-on-glass .g-btn.transparent:hover { background: var(--slate-200); }
.g-on-glass .g-btn.outline { background: transparent; border-color: var(--slate-200); }
.g-on-glass .g-btn.outline:hover { background: rgb(226 232 240 / 0.1); }

/* ============================================================================
   Modal — ui-system Modal.vue: white head, glass body, white footer, a close
   button floating over the panel rather than sitting in a bar of its own.
   ============================================================================ */

.g-modal {
	position: fixed;
	inset: 0;
	z-index: 120;
	display: flex;
	align-items: flex-end;
	justify-content: center;
	/* Kept in the DOM rather than display-toggled, so opening and closing can
	   transition. Hidden it takes no pointer events. */
	visibility: hidden;
	opacity: 0;
	transition: opacity 200ms ease-in, visibility 0s linear 200ms;
}
.g-modal.is-on {
	visibility: visible;
	opacity: 1;
	transition: opacity 200ms ease-out, visibility 0s;
}
.g-modal::before {
	content: "";
	position: absolute;
	inset: 0;
	background: rgb(203 213 225 / 0.8);
}

.g-modal-panel {
	position: relative;
	display: flex;
	flex-direction: column;
	width: 100%;
	/* A sheet on the bottom edge below sm: full width, square across the bottom,
	   stopping 4rem short of the top so the page behind stays visible. */
	max-height: calc(100dvh - 4rem);
	background: #fff;
	border-radius: var(--ds-r-2xl) var(--ds-r-2xl) 0 0;
	box-shadow: 0 20px 25px -5px rgb(148 163 184 / 0.9), 0 8px 10px -6px rgb(148 163 184 / 0.9);
	overflow: hidden;
	outline: none;
	/* The panel's own type, which the body inherits. */
	font-size: 16px;
	font-weight: 400;
	color: var(--slate-500);
	transform: translateY(100%);
	transition: transform 200ms ease-in;
}
.g-modal.is-on .g-modal-panel {
	transform: translateY(0);
	transition: transform 300ms ease-out;
}
@media (min-width: 40rem) {
	.g-modal { align-items: center; padding: 16px; }
	.g-modal-panel {
		max-width: 36rem;
		max-height: 100%;
		border-radius: var(--ds-r-2xl);
		transform: translateY(4rem);
	}
	.g-modal-panel.lg { max-width: 48rem; }
	.g-modal-panel.xl { max-width: 64rem; }
}
@media (prefers-reduced-motion: reduce) {
	.g-modal-panel, .g-modal.is-on .g-modal-panel { transform: none; transition: none; }
}

.g-modal-x { position: absolute; top: 16px; right: 16px; z-index: 10; }
.g-modal-head {
	padding: 24px;
	padding-inline-end: 64px;
	border-bottom: 1px solid var(--slate-100);
}
.g-modal-head h2 {
	margin: 0;
	font-size: 20px;
	font-weight: 800;
	letter-spacing: -0.005em;
	line-height: 1.3;
	color: var(--slate-700);
}
.g-modal-head p { margin: 8px 0 0; font-size: 14px; line-height: 1.45; color: var(--slate-500); }
.g-modal-body {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	padding: 24px;
	background: var(--slate-50);
}
.g-modal-body > * + * { margin-top: 20px; }
.g-modal-body p { line-height: 1.6; }

/* The action bar is always here. Below sm it stacks, reversed, so the last
   button written is the one on top and closest to the thumb. */
.g-modal-foot {
	display: flex;
	flex-direction: column-reverse;
	gap: 12px;
	padding: 24px;
	background: #fff;
	border-top: 1px solid var(--slate-100);
}
.g-modal-foot .g-btn { width: 100%; }
.g-modal-foot small { font-size: 12px; color: var(--slate-500); }
@media (min-width: 40rem) {
	.g-modal-foot { flex-direction: row; align-items: center; }
	.g-modal-foot .g-btn { width: auto; }
	/* The primary fills what the secondary leaves. */
	.g-modal-foot .g-btn.block { flex: 1 1 auto; }
	.g-modal-foot small { flex: none; }
}

/* Counts inside a modal: plain rows, a number on the right. No color unless
   the number means something that color can carry. */
.g-tally { display: grid; gap: 6px; }
.g-tally > div {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 11px 14px;
	background: #fff;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	font-size: 13px;
	color: var(--slate-700);
}
.g-tally i { width: 16px; font-size: 12px; color: var(--slate-500); flex-shrink: 0; }
.g-tally b { margin-left: auto; font-size: 13px; font-weight: 800; color: var(--slate-800); font-variant-numeric: tabular-nums; }

.g-kv { display: grid; gap: 6px; }
.g-kv > div {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
	gap: 8px;
	padding: 10px 14px;
	background: #fff;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	font-size: 13px;
}
.g-kv b { font-weight: 600; color: var(--slate-700); }
.g-kv span { color: var(--slate-500); }

.g-note {
	display: flex;
	gap: 10px;
	padding: 12px 14px;
	border-radius: var(--ds-r-xl);
	background: #fff;
	border: 1px solid var(--slate-200);
	font-size: 12px;
	line-height: 1.55;
	color: var(--slate-600);
}
.g-note > i { margin-top: 2px; color: var(--slate-500); flex-shrink: 0; }
.g-note b { color: var(--slate-800); font-weight: 700; }

.g-open-q {
	display: flex;
	gap: 10px;
	padding: 12px 14px;
	border-radius: var(--ds-r-xl);
	background: var(--violet-100);
	border: 1px solid var(--violet-300);
	font-size: 12px;
	line-height: 1.55;
	color: var(--violet-900);
}
.g-open-q > i { margin-top: 2px; color: var(--violet-500); flex-shrink: 0; }
.g-open-q b { font-weight: 800; }

/* ---- Fee rows in the billing modal. Plain text by default: no bold, no
        color, no icon, until plain text stops being enough. */
.g-fees { display: grid; gap: 6px; }
.g-fee {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 160px;
	align-items: center;
	gap: 12px;
	padding: 8px 14px;
	background: #fff;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-xl);
	font-size: 13px;
	color: var(--slate-700);
}
.g-fee input {
	height: 32px;
	box-sizing: border-box;
	width: 100%;
	padding: 0 12px;
	border: 1px solid var(--slate-200);
	border-radius: var(--ds-r-lg);
	font: inherit;
	font-size: 12px;
	color: var(--slate-900);
	outline-color: #2563eb;
}

/* ---- Message preview */
.g-preview-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 16px; align-items: start; }
@media (max-width: 900px) { .g-preview-wrap { grid-template-columns: minmax(0, 1fr); } }
.g-preview {
	border-radius: var(--ds-r-xl);
	background: #fff;
	border: 1px solid var(--slate-200);
	overflow: hidden;
	box-shadow: var(--ds-shadow-sm);
}
.g-preview-bar {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 12px;
	background: var(--slate-100);
	border-bottom: 1px solid var(--slate-200);
	font-size: 11px;
	font-weight: 700;
	color: var(--slate-600);
}
.g-preview-bar .g-spacer { margin-left: auto; font-weight: 500; }
.g-preview-brand {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 14px 16px;
	border-bottom: 1px solid var(--slate-100);
}
.g-preview-brand .g-preview-logo {
	width: 28px;
	height: 28px;
	border-radius: var(--ds-r-lg);
	background: var(--sky-300);
	display: grid;
	place-items: center;
	font-size: 12px;
	color: #000;
}
.g-preview-brand b { font-size: 13px; font-weight: 800; color: var(--slate-800); }
.g-preview-brand small { display: block; font-size: 11px; color: var(--slate-500); }
.g-preview-body { padding: 16px; font-size: 13px; line-height: 1.6; color: var(--slate-700); }
.g-preview-body p { margin: 0 0 12px; }
.g-preview-body .g-token { background: var(--sky-100); color: var(--sky-900); border-radius: var(--ds-r); padding: 0 4px; }
.g-preview-cta {
	display: inline-flex;
	align-items: center;
	height: 32px;
	padding: 0 14px;
	border-radius: var(--ds-r-lg);
	background: var(--sky-300);
	color: #000;
	font-size: 12px;
	font-weight: 600;
}
.g-preview-foot {
	padding: 12px 16px;
	border-top: 1px solid var(--slate-100);
	font-size: 11px;
	line-height: 1.5;
	color: var(--slate-500);
}

/* ============================================================================
   Toast — vx-toast in voxie/frontend-components, matched class for class:

     relative flex flex-row items-center gap-3 overflow-hidden rounded-md
     border border-slate-700 bg-slate-700 py-2 ps-3 pe-2 shadow-xl

   Top-centre at 448px, 8px apart, 6s, the countdown drawn as a slate-600 bar
   running out behind the content and pausing while the stack is hovered. The
   offset tracks the navbar and closes to 32px as the page scrolls, the way
   AppToastManager does it.
   ============================================================================ */

.g-toaster {
	position: fixed;
	top: var(--toast-top, 112px);
	left: 50%;
	transform: translateX(-50%);
	z-index: 200;
	display: flex;
	flex-direction: column;
	gap: 8px;
	width: 448px;
	max-width: calc(100vw - 32px);
	margin: 0;
	padding: 0;
	list-style: none;
	pointer-events: none;
	transition: top 300ms var(--ds-ease);
}
.g-toast {
	position: relative;
	display: flex;
	flex-direction: row;
	align-items: center;
	gap: 12px;
	overflow: hidden;
	/* frontend-components' rounded-md is 0.625rem, not Tailwind's stock. */
	border-radius: 0.625rem;
	border: 1px solid var(--slate-700);
	background: var(--slate-700);
	padding: 8px 8px 8px 12px;
	box-shadow: var(--ds-shadow-xl);
	pointer-events: auto;
	animation: g-toast-in 200ms var(--ds-ease);
}
.g-toast.is-leaving { animation: g-toast-out 150ms var(--ds-ease) forwards; }
@keyframes g-toast-in { from { opacity: 0; transform: translateY(-8px) scale(0.98); } }
@keyframes g-toast-out { to { opacity: 0; transform: translateY(-8px) scale(0.98); } }

.g-toast-bar {
	position: absolute;
	inset-block-end: 0;
	inset-inline-start: 0;
	z-index: 0;
	height: 100%;
	width: 100%;
	border-radius: 0.625rem;
	background: var(--slate-600);
	animation: g-toast-countdown var(--duration, 6000ms) linear forwards;
}
.g-toaster:hover .g-toast-bar { animation-play-state: paused; }
@keyframes g-toast-countdown { from { width: 100%; } to { width: 0%; } }

.g-toast-icon { position: relative; z-index: 1; flex: none; font-size: 12px; }
.g-toast[data-type="success"] .g-toast-icon { color: var(--emerald-400); }
.g-toast[data-type="error"] .g-toast-icon { color: var(--rose-400); }
.g-toast[data-type="warning"] .g-toast-icon { color: var(--amber-400); }
.g-toast[data-type="info"] .g-toast-icon { color: var(--slate-400); }

.g-toast-body { position: relative; z-index: 1; display: flex; width: 100%; min-width: 0; flex-direction: column; gap: 4px; }
/* text-xs, and its paired 1rem line height — not a looser one of our own. */
.g-toast-title { margin: 0; font-size: 12px; line-height: 1rem; font-weight: 700; color: #fff; }
.g-toast-desc { margin: 0; font-size: 12px; line-height: 1rem; color: var(--slate-300); }
.g-toast:not(:has(.g-toast-title)) .g-toast-desc { color: #fff; }
.g-toast-acts { position: relative; z-index: 1; display: flex; flex: none; align-items: center; gap: 4px; }

@media (prefers-reduced-motion: reduce) {
	.g-toast, .g-toast.is-leaving { animation: none; }
	.g-toast-bar { animation: none; width: 0; }
}

/* ============================================================================
   Tooltip — ui-system has no Tooltip component yet, so this matches the dark
   card: slate-700, white text, the small end of the type scale.
   ============================================================================ */

.g-tip {
	position: fixed;
	z-index: 200;
	max-width: 320px;
	padding: 8px 10px;
	border-radius: var(--ds-r-lg);
	background: var(--slate-700);
	color: #fff;
	font-size: 12px;
	line-height: 1.45;
	box-shadow: var(--ds-shadow-lg);
	pointer-events: none;
	opacity: 0;
	transition: opacity 120ms var(--ds-ease);
}
.g-tip.is-on { opacity: 1; }
.g-tip b { display: block; font-weight: 800; margin-bottom: 2px; }
.g-tip .g-tip-row { display: flex; gap: 12px; }
.g-tip .g-tip-row span + span { margin-left: auto; color: var(--slate-300); }

/* ============================================================================
   Navbar — ui-system Navbar.vue. Geometry only; the bar's content is the
   prototype's own.

     nav   h-16 @min-[40rem]/navbar:h-20, gap-4, shadow-sm,
           px-4 @min-[40rem]/navbar:px-6 @min-[48rem]/navbar:px-8
     left  flex h-full shrink-0 items-center gap-2 @min-[64rem]/navbar:gap-4
     label text-xs font-medium, slate-800 on glass, icon text-xs slate-400
     marker after:inset-x-0 after:bottom-0 after:h-1.5 after:rounded-t-lg
           after:bg-sky-500
   ============================================================================ */

.g-navbar-c {
	container-type: inline-size;
	container-name: navbar;
	position: relative;
	z-index: 30;
}

.kbar {
	display: flex;
	align-items: center;
	gap: 16px;
	height: 64px;
	padding: 0 16px;
	background: var(--slate-50);
	box-shadow: var(--ds-shadow-sm);
	flex-wrap: nowrap;
}
/* One height: 64px, and 80px once the bar has the room for it. */
@container navbar (min-width: 40rem) { .kbar { height: 80px; padding: 0 24px; } }
@container navbar (min-width: 48rem) { .kbar { padding: 0 32px; } }

.kb-l {
	display: flex;
	height: 100%;
	flex-shrink: 0;
	align-items: center;
	gap: 8px;
	padding: 0;
}
@container navbar (min-width: 64rem) { .kb-l { gap: 16px; } }
.g-mark { display: block; overflow: visible; color: var(--sky-400); flex-shrink: 0; }
.kb-l b { color: var(--slate-800); }

.kb-n {
	display: flex;
	height: 100%;
	min-width: 0;
	flex-grow: 1;
	align-items: center;
	gap: 0;
	overflow-x: auto;
}
.kb-n span {
	position: relative;
	display: flex;
	height: 100%;
	align-items: center;
	gap: 8px;
	/* The one value the spec does not state. Sections carry no padding of their
	   own there, but the marker is inset-x-0, so with none the markers would run
	   into each other. */
	padding: 0 12px;
	border-bottom: 0;
	font-size: 12px;
	font-weight: 500;
	color: var(--slate-800);
	white-space: nowrap;
}
.kb-n span.on::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	height: 6px;
	border-radius: 8px 8px 0 0;
	background: var(--sky-500);
}
.kb-n span.on { border-bottom-color: transparent; }
.kb-n i { font-size: 12px; color: var(--slate-400); }

/* Button sm in the right slot: h-8, rounded-lg, px-3, text-xs, semibold. */
.kb-x {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin-left: auto;
	flex-shrink: 0;
	height: 32px;
	padding: 0 12px;
	border-radius: var(--ds-r-lg);
	background: var(--slate-700);
	color: #fff;
	font-size: 12px;
	font-weight: 600;
}

/* ============================================================================
   Kiosk chrome — a few corrections so the shell reads like the app, not like
   two systems next to each other.
   ============================================================================ */

.kiosk { background: var(--slate-200); }
.kb-x { background: var(--slate-700); border-radius: var(--ds-r-xl); }
.kmain aside span.on { color: var(--sky-700); }
.kt h1 { color: var(--slate-800); }
.ghead h2 { color: var(--slate-800); }
