/* =============================================================================
   recipe-controls.css — every control, assembled from the catalogue build.
   Read recipe-tokens.css first; nothing here works without it.
   ============================================================================= */

/* ---- 01-existing ---------------------------------------------------- */
/* =============================================================================
   THE FOUR CONTROLS THAT SHIPPED FIRST — button, text input, select, checkbox/radio.

   These lived inside recipe-tokens.css until now, because "lock it in" was read as
   "put it in the locked file" and the CSS followed the tokens across. It made the
   token file a token file plus six controls, and made recipe-controls.css claim to
   hold every control while missing four of them.

   Nothing here changed on the way over. The token file is now what its own header
   says it is: reference values, system roles, and the two grounds — the layer a theme
   re-points. A control's own tokens are declared on the control, in the control's
   file, next to the properties that read them.
   ============================================================================= */
/* =============================================================================
   3. COMPONENT — the button.

   These are declared on .al-btn, NOT on :root, for two reasons. The load-bearing
   one is CSS resolution: a var() inside a custom property resolves against the
   element the property is DECLARED on, so --al-button-fill: var(--al-button-intent)
   sitting on :root could never see .al-btn-danger's override. Declaring it on the
   element is what makes emphasis and intent compose at all.

   The second is tidiness: a button's tokens are the button's business, and putting
   them on :root hangs them off every element in the document. Theming still works,
   because theming belongs at the sys tier — .theme-dark { --al-sys-color-surface }
   moves every component that opted in, which is the whole point of the sys tier.
   ============================================================================= */

.al-btn {
	/* -- the composition axis. Everything an intent needs to change is one token,
	      so an intent variant is a single line and can never miss a surface. ---- */
	--al-button-intent: var(--al-sys-color-accent-primary);
	--al-button-intent-line: var(--al-sys-color-accent-primary-line);
	--al-button-intent-strong: var(--al-sys-color-accent-primary-strong);
	--al-button-intent-stronger: var(--al-sys-color-accent-primary-stronger);
	/* the text that sits ON this intent's fill. Part of the intent ladder rather than a
	   single global, because the two intents are not obliged to be equally dark: our
	   emerald and red happen to be, so one role covered both and looked correct, but a
	   theme with a pale accent and a deep danger needs black on one and white on the
	   other. One value cannot be both, and the failure is a label nobody can read. */
	--al-button-intent-text: var(--al-sys-color-text-on-accent);

	--al-button-fill: var(--al-button-intent);
	--al-button-border-color: var(--al-button-intent);
	/* The BUTTON's ring stays on the intent ladder rather than reading the shared focus
	   role, and that is deliberate. A danger button turns red on every surface it has —
	   fill, border, label, hover veil and ring — off one variant line, and pointing the
	   ring at a global focus colour quietly made a focused Void button wear a green
	   halo. A control that carries an intent rings in that intent. Controls with no
	   intent, the input and the affix, read --al-sys-color-focus-ring instead. */
	--al-button-focus-ring-color: var(--al-button-intent-line);
	--al-button-label: var(--al-button-intent-text);
	--al-button-veil-hover: var(--al-sys-color-veil-on-accent);
	--al-button-veil-press: var(--al-sys-color-veil-on-accent);

	/* -- scale: points at the md rung; a size variant re-points these four ----- */
	--al-button-height: var(--al-sys-control-height-md);
	--al-button-padding-x: var(--al-sys-control-padding-x-md);
	--al-button-font-size: var(--al-sys-font-size-300);
	--al-button-icon-size: var(--al-sys-icon-size-md);

	--al-button-radius: var(--al-sys-control-radius);
	--al-button-border-width: var(--al-sys-control-border-width);
	--al-button-gap: var(--al-sys-control-gap);
	--al-button-font-weight: var(--al-sys-font-weight-control);

	--al-button-veil-hover-opacity: var(--al-sys-state-hover-opacity-on-fill);
	--al-button-veil-press-opacity: var(--al-sys-state-press-opacity-on-fill);
	--al-button-press-scale: var(--al-sys-state-press-scale);
	--al-button-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-button-spinner-ring-width: var(--al-sys-spinner-ring-width);
	--al-button-spinner-duration: var(--al-sys-motion-duration-loop);

	--al-button-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-button-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-button-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-button-focus-ring-width: var(--al-sys-focus-ring-width);

	--al-button-motion-duration: var(--al-sys-motion-duration-state);
	--al-button-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-button-* below this line ----------------------- */
	/* The system owns its own box model rather than borrowing one. Without this the
	   size tier is only true on pages that happen to ship *{box-sizing:border-box}:
	   measured on a page without it, a control naming 36px rendered 48. A tier a host
	   page can silently change is not a tier, and a design system that only works
	   inside someone else's reset is not portable. */
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--al-button-gap);

	/* A FLOOR, NOT A FIXED HEIGHT — two separate failures, one shape of fix.
	   flex: none stops a container shrinking it. Put three buttons in a column 120px
	   tall and they were quietly rendering at 19.5px each while the token still said
	   36: half height, no longer matching the field beside them, no longer a
	   comfortable touch target, and nothing anywhere saying so. Now the panel visibly
	   overflows instead, which is a thing someone notices and fixes.
	   min-height instead of height lets it grow when someone turns their text up. Type
	   is relative to the browser's font setting and the tier is pixels, so at 200% the
	   old box held 28px text in a 36px shell. In normal use this is exactly the tier
	   and nothing moves; it only gives way when the text would not otherwise fit. */
	flex: none;
	align-self: center;
	height: auto;
	min-height: var(--al-button-height);
	padding: 0.35em var(--al-button-padding-x);
	border-radius: var(--al-button-radius);
	border: var(--al-button-border-width) solid var(--al-button-border-color);

	font-family: inherit;
	font-size: var(--al-button-font-size);
	font-weight: var(--al-button-font-weight);
	line-height: 1; /* structural: the height token owns the box, flex owns the
	     centring. Not a type decision. */
	white-space: nowrap;
	/* the button is often an <a> — this file's own markup says so — and an anchor
	   arrives decorated. The hover half is stated once for every anchor-capable
	   control, further down. */
	text-decoration: none;

	background: var(--al-button-fill);
	color: var(--al-button-label);

	position: relative;
	isolation: isolate;
	cursor: pointer;
	transition:
		transform var(--al-button-motion-duration) var(--al-button-motion-easing),
		box-shadow var(--al-button-motion-duration) var(--al-button-motion-easing);
}

/* THE LABEL — and it needs no class of its own.

   Any element inside the button truncates, so `<span>Save</span>` is enough. That was
   deliberate: every button has a label and about one in five has an icon, so the
   burden belongs on the rarer case. Only the icon is marked, and only so it keeps its
   size while the label shrinks beside it.

   Wrapping the label in SOMETHING is still required, because text-overflow has no
   effect on a bare text node inside a flex container — measured, not assumed. It also
   gives the busy state something to hide, and tells a leading glyph from a trailing
   one. min-width:0 is what lets a flex item shrink below its content width; without
   it the ellipsis never appears. */
.al-btn > * {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
/* THE ELLIPSIS GUARD ABOVE IS FOR A LABEL, AND IT WAS CLIPPING EVERY ICON IN THE PRODUCT.
   A Material Symbols glyph's ink exceeds its em box by about 2px, so `overflow: hidden` on
   every direct child of .al-btn squared off the bottom of every icon button there is, and
   has since the guard landed. It is not text being truncated, so exempting it weakens
   nothing: a glyph has no label to ellipsise.

   Measured on the running POS by agent-4olt71: content 80x22 inside a 16x16 box with
   overflow hidden. The give-away that it had been met before is
   OrderTypeChangeModal.css:247, an undocumented `.button-visible span { overflow: visible
   !important }` -- somebody's local workaround for this exact rule, deleted in this commit
   now that the root is fixed.

   ONE LINE, AND APPROVED AS ONE LINE. Jeff ruled "1" on ALLM-7419 at 13:44 on 2026-09-20,
   against an option quoting this exact declaration. Do not widen it without asking again. */
.al-btn .material-symbols-outlined {
	overflow: visible;
}
.al-btn-icon {
	flex: none; /* the label gives way, never the glyph */
}

/* THE GLYPH TAKES THE BUTTON'S RUNG. [ALLM-7164]

   .al-back has said this since it was written and it was never generalised, so every
   Material Symbols icon in a button drew at the font's own 24px. Counted across views/
   and public/: 249 icon buttons in 88 files. 151 are al-btn-sm on the 28px rung, where a
   24px glyph does not fit and pushed the button to 35.1px beside 28px siblings. The other
   98 fit, and still carried a glyph half again the size their rung asks for.

   SCOPED UNDER THE CONTROL, for the reason .al-back gives at length: Material Symbols
   ships `.material-symbols-outlined { font-size: 24px }`, which is one class against one
   class, so flat the winner would be whichever file loads last and this file does not get
   to choose. Under .al-btn it is two classes and load order stops mattering.

   Sized twice on purpose, again as .al-back does: the box sizes an SVG and the type sizes
   an icon font, and neither alone covers both.

   NOT .al-btn-icon, deliberately. That class means `flex: none` and the app also puts it
   on BUTTONS as a shape modifier, so sizing it as a glyph would shrink the control. */
.al-btn .material-symbols-outlined {
	box-sizing: border-box;
	flex: none;
	width: var(--al-button-icon-size);
	height: var(--al-button-icon-size);
	font-size: var(--al-button-icon-size);
	line-height: 1;
}

/* One overlay serves hover AND press, by changing colour and direction.

   z-index: -1 puts the veil UNDER the label, which `isolation: isolate` above makes
   safe: the veil stays inside the button's own stacking context, above its
   background and below its text. Without it the veil paints over the label —
   positioned descendants with z-index:auto paint after in-flow content — and a dark
   press veil visibly dims a white label. */
.al-btn::after {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: inherit;
	background: var(--al-button-veil-hover);
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--al-button-motion-duration) var(--al-button-motion-easing);
}
.al-btn:hover::after {
	opacity: var(--al-button-veil-hover-opacity);
}
.al-btn:active::after {
	background: var(--al-button-veil-press);
	opacity: var(--al-button-veil-press-opacity);
}
.al-btn:active {
	transform: scale(var(--al-button-press-scale));
}

.al-btn:focus {
	outline: none;
}
.al-btn:focus-visible {
	/* Windows high-contrast / forced-colors throws box-shadow away, which would leave
	   a keyboard user with no focus indicator at all. A transparent outline is
	   invisible here and is repainted in a system colour there, so the ring survives
	   a mode this design cannot otherwise reach. */
	outline: var(--al-button-focus-ring-width) solid transparent;
	outline-offset: var(--al-button-focus-hairline-width);

	box-shadow:
		0 0 0 var(--al-button-focus-hairline-width) var(--al-button-focus-hairline-light),
		0 0 0 calc(var(--al-button-focus-hairline-width) + var(--al-button-focus-ring-width)) var(--al-button-focus-ring-color),
		0 0 0 calc(var(--al-button-focus-hairline-width) + var(--al-button-focus-ring-width) + var(--al-button-focus-hairline-width))
			var(--al-button-focus-hairline-dark);
}

.al-btn:disabled {
	opacity: var(--al-button-disabled-opacity);
	pointer-events: none;
}

/* -----------------------------------------------------------------------------
   BUSY — the sixth state.

   Keyed off aria-busy, NOT a --loading modifier class, so the visual state and the
   screen-reader announcement cannot disagree: you cannot style it without also
   making it accessible.

   The label is hidden with `color: transparent` rather than by wrapping it in a
   span — the label keeps its width so the button does not resize and the row does
   not reflow, and consumers are not forced into a required inner element.

   The spinner is sized in `em`, so it scales with the size variant for free.
   ----------------------------------------------------------------------------- */
.al-btn[aria-busy='true'] {
	color: transparent;
	pointer-events: none;
}
/* `color: transparent` hides text and nothing else. An icon, an SVG painting from a
   literal fill, or a child with its own background all survive it and collide with the
   spinner. Hiding children by visibility keeps their space, so the button holds size. */
.al-btn[aria-busy='true'] > * {
	visibility: hidden;
}

/* pointer-events stops the MOUSE and nothing else. A focused button still fires on
   Enter and Space, and Chrome and Firefox both set :active during keyboard
   activation — so without these two rules a spinning button visibly presses and
   re-fires under the keyboard. CSS cannot stop the activation itself, only its
   appearance, so the contract is: a busy button also carries aria-disabled="true"
   and its handler returns early while busy. Real `disabled` is the wrong tool here
   — it drops the button out of the tab order mid-action and throws the keyboard
   user's place away. */
.al-btn[aria-busy='true']:active {
	transform: none;
}
.al-btn[aria-busy='true']:active::after {
	opacity: 0;
}
.al-btn[aria-disabled='true'] {
	opacity: var(--al-button-disabled-opacity);
	pointer-events: none;
}
/* aria-disabled dims the control but keeps it focusable, which is the point — a
   keyboard user does not lose their place. It must still not react: without these it
   looks disabled and then lights up under hover and shrinks under a press. */
.al-btn[aria-disabled='true']:hover::after,
.al-btn[aria-disabled='true']:active::after {
	opacity: 0;
}
.al-btn[aria-disabled='true']:active {
	transform: none;
}
.al-btn[aria-busy='true'][aria-disabled='true'] {
	opacity: 1; /* busy already says "not now"; dimming it as well just makes the
	     spinner hard to see */
}

.al-btn[aria-busy='true']::before {
	content: '';
	position: absolute;
	left: 50%;
	top: 50%;
	/* border-box, explicitly. box-sizing is not inherited, and the usual
	   `*{box-sizing:border-box}` reset does not match pseudo-elements — so this box
	   would be content-box, 1em PLUS 2px of ring on each side, and the -0.5em margins
	   would centre only the content. Measured: 2px right and 2px low in all 18 busy
	   cells, worst at --sm where 2px is a large share of a 17px ring. */
	box-sizing: border-box;
	width: 1em;
	height: 1em;
	margin: -0.5em 0 0 -0.5em;
	border: var(--al-button-spinner-ring-width) solid var(--al-button-label);
	border-right-color: transparent;
	border-radius: 50%;
	animation: al-spin var(--al-button-spinner-duration) linear infinite;
}
@keyframes al-spin {
	to {
		transform: rotate(360deg);
	}
}

/* Forced-colors repaints every border side to one system colour, which closes the
   gap in the ring. A rotationally symmetric circle spinning at 360deg shows no
   motion at all, so the busy state would go completely silent in exactly the mode
   the focus ring above is careful to survive. Painting the gap with Canvas keeps the
   asymmetry that makes the rotation visible. */
@media (forced-colors: active) {
	.al-btn[aria-busy='true']::before {
		border-right-color: Canvas;
	}

	/* Forced colours repaint `color: transparent` to a visible system colour, so the
	   busy label comes back whatever we do — rendered and confirmed. Since it cannot
	   be hidden here, the spinner moves out of its way instead and the button makes
	   room. Only this mode is affected. */
	.al-btn[aria-busy='true'] {
		padding-right: calc(var(--al-button-padding-x) + var(--al-button-gap) + 1em);
	}
	.al-btn[aria-busy='true'] > * {
		visibility: visible;
	}
	.al-btn[aria-busy='true']::before {
		left: auto;
		right: var(--al-button-padding-x);
		margin-left: 0;
	}
}

/* =============================================================================
   VARIANTS — two independent axes. Every variant below re-points tokens only, so
   states, focus, scale and disabled all follow without being restated, and the two
   axes compose: .al-btn-outline.al-btn-danger is a red outline button whose
   hover veil, focus ring and label are all red, with nothing written to say so.
   ============================================================================= */

/* -- EMPHASIS: how loud. Default is filled. ------------------------------------
   Named for APPEARANCE, not rank. "Secondary" would name a rank, and rank is not
   an axis this system has — a danger outline button is not "secondary", it is the
   destructive alternative. Mixing the two axes in one name is the thing that makes
   other systems' primary/secondary/tertiary/ghost/danger sets so hard to teach. */

/* Outline and quiet are the two emphases whose accent is a LINE rather than a FILL —
   their edge and their label are ink laid on the ground behind them — so both read
   the line rung. On a light ground that is the same value as the fill and nothing
   changes; on any other ground it is what lets them stay legible without a rule per
   surface being written for them. */
.al-btn-outline {
	--al-button-fill: var(--al-sys-color-surface-control);
	--al-button-border-color: var(--al-button-intent-line);
	--al-button-label: var(--al-button-intent-line);
	--al-button-veil-hover: var(--al-button-intent-line);
	--al-button-veil-hover-opacity: var(--al-sys-state-hover-opacity-on-surface);
	/* PRESS TAKES THE INTENT TOO. It used to be the one state token no variant ever
	   re-pointed, so a pressed outline button fell through to the accent veil: hover was
	   branded and press was grey, on every intent. Worse on a dark panel, where a
	   darkening veil over an already dark canvas produced no visible change at all —
	   the button simply did not respond to being pressed. */
	--al-button-veil-press: var(--al-button-intent-line);
	--al-button-veil-press-opacity: var(--al-sys-state-press-opacity-on-surface);
}

.al-btn-quiet {
	--al-button-fill: transparent;
	--al-button-border-color: transparent;
	--al-button-label: var(--al-button-intent-line);
	--al-button-veil-hover: var(--al-button-intent-line);
	--al-button-veil-hover-opacity: var(--al-sys-state-hover-opacity-on-surface);
	--al-button-veil-press: var(--al-button-intent-line);
	--al-button-veil-press-opacity: var(--al-sys-state-press-opacity-on-surface);
}

/* A state may re-point a token, exactly as a variant does. On a surface-grounded
   button the ground lightens under the label, so the label deepens to match. */
.al-btn-outline:hover,
.al-btn-quiet:hover {
	--al-button-label: var(--al-button-intent-strong);
}
.al-btn-outline:active,
.al-btn-quiet:active {
	--al-button-label: var(--al-button-intent-stronger);
}

/* -- INTENT: what it means. Default is primary. ------------------------------
   One line, because every surface that carries meaning already reads the intent
   token. Composes with all three emphasis levels and all three sizes. */

.al-btn-danger {
	--al-button-intent: var(--al-sys-color-accent-danger);
	--al-button-intent-line: var(--al-sys-color-accent-danger-line);
	--al-button-intent-text: var(--al-sys-color-text-on-accent-danger);
	--al-button-intent-strong: var(--al-sys-color-accent-danger-strong);
	--al-button-intent-stronger: var(--al-sys-color-accent-danger-stronger);
}

/* The utility intent: solid and tappable, and it claims nothing. Use it for the actions
   that are neither the money nor a destruction -- open a panel, edit a detail, navigate.
   It is five lines for the same reason danger is: fill, border, label, both veils, the
   focus ring, all three sizes and all three emphases fall out of the intent axis. */
.al-btn-neutral {
	--al-button-intent: var(--al-sys-color-accent-neutral);
	--al-button-intent-line: var(--al-sys-color-accent-neutral-line);
	--al-button-intent-text: var(--al-sys-color-text-on-accent-neutral);
	--al-button-intent-strong: var(--al-sys-color-accent-neutral-strong);
	--al-button-intent-stronger: var(--al-sys-color-accent-neutral-stronger);
}

/* -- SIZE: default is md. ------------------------------------------------------
   Three re-points each, and because the sys tier pairs height with font size and
   padding, a size variant cannot resize the box without resizing the type. */

.al-btn-sm {
	--al-button-height: var(--al-sys-control-height-sm);
	--al-button-padding-x: var(--al-sys-control-padding-x-sm);
	--al-button-font-size: var(--al-sys-font-size-200);
	--al-button-icon-size: var(--al-sys-icon-size-sm);
}

.al-btn-lg {
	--al-button-height: var(--al-sys-control-height-lg);
	--al-button-padding-x: var(--al-sys-control-padding-x-lg);
	--al-button-font-size: var(--al-sys-font-size-400);
	--al-button-icon-size: var(--al-sys-icon-size-lg);
}

/* =============================================================================
   OUTLINE AND QUIET ARE DARK-CORRECT. This block used to say the opposite — "LIGHT
   GROUNDS ONLY … on a dark ground both come out as light shapes … a dark surface
   treatment is not part of the system" — and that has not been true since dark moved
   to the role layer. The claim outlived the mechanism it described, and it was still
   being read as current and designed around.

   What actually happens now is in recipe-tokens.css .al-on-dark: surface-control goes
   transparent, so an outline button's interior becomes the dark ground itself rather
   than a white fill; accent-primary-line lightens for the edge and the label; and
   -strong/-stronger climb instead of deepening, so hover and press still move. Nothing
   here needs a rule per ground, which is the whole point of reading roles.
   ============================================================================= */

/* =============================================================================
   REDUCED MOTION — keep only what carries meaning.

   Fires only for someone whose operating system is set to reduce animation,
   usually because motion makes them unwell. Everyone else sees the design exactly
   as drawn.

   The press shrink and the veil fade are polish: they go. The spinner stays,
   because it is the only thing telling someone their action is still running —
   removing it would leave a slow save looking like a dead button. It switches to a
   stepped rotation, which still reads as progress without sweeping continuously.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-btn,
	.al-btn::after {
		transition: none;
	}
	.al-btn:active {
		transform: none;
	}
	.al-btn[aria-busy='true']::before {
		animation-timing-function: steps(8, end);
	}
}

/* -----------------------------------------------------------------------------
   BLOCK — fills the width of whatever holds it, instead of being as wide as its
   label. Narrow panels, modal footers, phone-width layouts. Without it, screens
   that need one write their own width override and drift off the system.
   ----------------------------------------------------------------------------- */
.al-btn-block {
	display: flex; /* NOT block: block destroys the flex context, the label stops being
	     a flex item, and overflow:hidden does nothing on an inline box — so a
	     full-width button silently lost its ability to truncate. */
	width: 100%;
}

/* =============================================================================
   THE TEXT INPUT

   A field is three parts that belong together: the name above it, the control, and
   one line underneath that carries a hint before anything is wrong and the error
   message after. The line is always reserved, so a field never changes height when
   an error appears and a form never jumps under someone's cursor.

   The label sits ABOVE. Beside the field is more compact but a long name either
   truncates or breaks the alignment, and it does not survive a narrow screen. Inside
   as a placeholder disappears the moment someone types, which is when they most want
   to check what the field was.

   NO HOVER STATE, deliberately. The button has one because it is a single target you
   aim at. A form is twenty targets you sweep across, and lighting each one in turn is
   noise — the text cursor already says the field is typeable.
   ============================================================================= */

.al-field {
	/* THE LABEL NO LONGER SHOUTS. Jeff's ruling on ALLM-7417, filed as ALLM-7420.
	
	   The micro-caps treatment that was here is kept in this note because the reasoning was
	   sound and the next person will otherwise re-propose it: the label is the smallest rung
	   at weight 700, which separates it from its value by weight rather than by size, and
	   uppercase was doing that separating job too. Weight and size still do it. Caps were the
	   part that carried a cost everywhere else -- design/rules.json already says nothing in
	   this product shouts, and field labels were the one large unstated exception.
	
	   One token, one consumer, 1,609 .al-field instances across 106 files. Nothing re-points
	   it, which is why the flip is the whole change.

	   THE TRACKING GOES WITH THE CAPS. 0.04em is letter-spacing tuned to open up all-caps text;
	   on sentence case at this size it reads loose. Jeff ruled the casing on ALLM-7417 and then
	   ruled the tracking separately after seeing all three states side by side, because the
	   second was not covered by the first and guessing it either way was a change he had not
	   asked for. */
	--al-field-label-color: var(--al-sys-color-text-primary);
	--al-field-label-size: var(--al-sys-font-size-100);
	--al-field-label-weight: var(--al-sys-font-weight-heading);
	--al-field-label-tracking: normal;
	--al-field-label-transform: none;
	--al-field-hint-color: var(--al-sys-color-text-muted);
	--al-field-hint-size: var(--al-sys-font-size-100);
	--al-field-gap: 6px;

	/* ALLM-7415. The POS centres its text on body and html (App.css), and that centring is
	   INHERITED, so it reaches every child of a field that does not declare its own -- the
	   label is merely where it shows first. Declaring it here rather than on the label rule is
	   why the next element in a field to gain visible text does not have to be found again.
	   `left` rather than `start`: this file uses `left` seven times and `start` never, and
	   this is not the change that introduces one. agent-c17w5r's measurement, Jeff's option 2. */
	text-align: left;

	display: block;
}
/* :not(.al-affix) because an affix is also a span. Without it, writing the affix as
   <span> instead of <div> hands it to this rule — display:block at (0,1,1) beats
   .al-affix's display:flex at (0,1,0) — and the whole control collapses, silently and
   totally. Excluding it here is the root fix; restating the affix's own styles at a
   higher specificity would only be a second copy waiting to drift.

   A DIRECT <label> IS DRAWN THE SAME AS A DIRECT <span>, and that is not a convenience.
   A simple field never has one: the field IS the <label>, so clicking the text focuses
   the control and no `for` is needed. A COMPOSITE cannot do that — its control sits in
   .al-field-control, a sibling of the label rather than a descendant — so the only thing
   that names the input to a screen reader is a real <label for>. The composite was added
   for the layout half of that problem and left the naming half open: aria-labelledby on
   the group names the GROUP, and the input inside it keeps no name of its own. Half of
   the first review finding on POS lookup was four controls with no accessible name, and
   two of them were this.
   A <label> is not an affix and never will be, so it takes no guard. */
.al-field > span:not(.al-affix),
.al-field > label {
	display: block;
	margin-bottom: var(--al-field-gap);
	font-size: var(--al-field-label-size);
	font-weight: var(--al-field-label-weight);
	letter-spacing: var(--al-field-label-tracking);
	text-transform: var(--al-field-label-transform);
	color: var(--al-field-label-color);
}
/* the mark on a required field. Required is marked, not optional: what most people
   expect, and it stays quiet when only one or two fields in a form are mandatory. */
.al-field > span .al-required,
.al-field > label .al-required {
	color: var(--al-sys-color-text-muted);
	font-weight: 400;
}
.al-field > small {
	display: block;
	margin-top: var(--al-field-gap);
	font-size: var(--al-field-hint-size);
	line-height: 1.45;
	color: var(--al-field-hint-color);
}

/* -----------------------------------------------------------------------------
   THE COMPOSITE FIELD — the same frame when the control is more than one thing.

   THE FRAME IS A <label> BECAUSE THE FIELD IS ITS LABEL, and that is right up to
   the moment the field holds a group: an input beside a Validate button, a picker
   whose own toggle is a button, a value with a Copy next to it. A <label> hands
   every click inside it to its first form control, so clicking near the Validate
   button focuses the input, and clicking a picker's own label fights the picker's
   toggle. The frame stops being a label and starts being a trap.

   So the composite is a <div class="al-field al-field-composite"> carrying
   role="group" and aria-labelledby, and the label is the same direct <span> with
   an id on it. Nothing about the look changes -- .al-field > span already draws
   the label and does not care what its parent's tag is. What changes is that the
   grouping is stated in a way the browser can act on, instead of being implied by
   a wrapper that would act on it wrongly.

   WHY NOT MAKE EVERY FIELD A GROUP and be done with one pattern. Because a group
   is not a label: a screen reader names a group when focus enters it and names a
   label as the control's own name, and for a single input the label is the better
   of the two. The simple field keeps the better answer; the composite takes the
   one that works.

   .al-field-control IS THE ROW, and it exists so the group's parts line up without
   the page saying how. Anything in it that should take the leftover width says so
   with al-field-grow; everything else keeps the size it asked for. The gap is the
   system's control gap, so an input beside a button sits the same distance apart
   as the two buttons in a form-actions row.
   -------------------------------------------------------------------------- */
.al-field-composite {
	/* nothing to re-point. The frame's tokens are the frame's, and the composite is
	   the same frame wearing a different tag -- a variant that restated them would
	   be a second definition waiting to drift from the first. */
	display: block;
}
.al-field-control {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-sys-control-gap);
	/* a composite may wrap on a narrow page; it may not squash its own controls,
	   and min-width:0 is what lets a grown child shrink instead of overflowing */
	flex-wrap: wrap;
	min-width: 0;
}
/* the part that takes the leftover width. Stated on the child rather than assumed
   by position: in an input-and-button pair the input grows, and in a button-and-
   value pair it is the value, and nothing about the order says which.

   SCOPED UNDER THE ROW, and that is the half that makes it work. Every control in
   this file states `flex: none` and `width: 100%` so it fills whatever box it is
   given, and both of those are (0,1,0) written LATER in the file than this rule --
   so written flat, the input kept flex:none, took the whole row on its width, and
   pushed the button onto a second line. Rendered and confirmed, not reasoned about.
   Under the row it is (0,2,0) and the question stops being about file order.

   flex-basis is 0, not auto, for the same reason: an auto basis reads the control's
   own width:100% as its starting size, which is the whole row before any space is
   shared out. */
.al-field-control > .al-field-grow {
	flex: 1 1 0;
	width: auto;
	min-width: 0;
}

/* =============================================================================
   FOR SCREEN READERS ONLY

   A label that has to exist and must not be seen: the header of a column whose cells hold
   nothing but an icon button, the name of a group of radios that the layout already makes
   obvious, a live region announcing a result. The alternative people reach for is
   `display: none`, which removes it from the accessibility tree as well and so removes the
   only thing it was for.

   `clip-path` rather than the old `clip: rect()`, and `white-space: nowrap` so a long label
   cannot be broken into a column one pixel wide and read out a letter at a time.
   ============================================================================= */
.al-sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: 0;
	padding: 0;
	border: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.al-input {
	--al-input-height: var(--al-sys-control-height-md);
	--al-input-padding-x: var(--al-sys-control-padding-x-md);
	--al-input-font-size: var(--al-sys-font-size-300);
	--al-input-radius: var(--al-sys-control-radius);
	--al-input-border-width: var(--al-sys-control-border-width);
	--al-input-border-color: var(--al-sys-color-border-control);
	--al-input-fill: var(--al-sys-color-surface-control);
	--al-input-text: var(--al-sys-color-text-primary);
	--al-input-placeholder: var(--al-sys-color-text-placeholder);
	--al-input-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-input-line-height: var(--al-sys-line-height-control);

	--al-input-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-input-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-input-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-input-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-input-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box; /* see the button — the system owns its box model */
	display: block;
	width: 100%;
	/* a floor, not a fixed height — same reasoning as the button */
	flex: none;
	height: auto;
	min-height: var(--al-input-height);
	padding: 0.35em var(--al-input-padding-x);
	border: var(--al-input-border-width) solid var(--al-input-border-color);
	border-radius: var(--al-input-radius);
	background: var(--al-input-fill);
	color: var(--al-input-text);
	font: inherit;
	font-size: var(--al-input-font-size);
	/* SET, not inherited. A host page with line-height 1.65 put a 21.45px line box
	   inside the 28px tier and the small field rendered 33px. A size tier that changes
	   with the host page's typography is not a tier. */
	line-height: var(--al-input-line-height);
}
.al-input::placeholder {
	color: var(--al-input-placeholder);
}

/* the same ring as the button, for the same reason: a hairline each side means one of
   them always separates it from whatever is behind.

   The border reads the input's OWN border token rather than the ring colour. That one
   word is the difference between an error surviving focus and being erased by it:
   setting border-color to the ring here was a PROPERTY overriding a token, so the
   instant someone clicked into a wrong field the red edge turned brand-green and the
   field stopped saying anything was wrong. Which token the border points at is
   decided by the two rules underneath — a valid field's edge becomes the accent, an
   invalid one keeps its red and turns the RING red instead. */
.al-input:focus {
	outline: var(--al-input-focus-ring-width) solid transparent;
	outline-offset: var(--al-input-focus-hairline-width);
	border-color: var(--al-input-border-color);
	box-shadow:
		0 0 0 var(--al-input-focus-hairline-width) var(--al-input-focus-hairline-light),
		0 0 0 calc(var(--al-input-focus-hairline-width) + var(--al-input-focus-ring-width)) var(--al-input-focus-ring-color),
		0 0 0 calc(var(--al-input-focus-hairline-width) * 2 + var(--al-input-focus-ring-width)) var(--al-input-focus-hairline-dark);
}
.al-input:not([aria-invalid='true']):focus {
	--al-input-border-color: var(--al-sys-color-accent-primary-line);
}
.al-input[aria-invalid='true']:focus {
	--al-input-focus-ring-color: var(--al-sys-color-status-error);
}

.al-input:disabled {
	opacity: var(--al-input-disabled-opacity);
	pointer-events: none;
}

/* READ-ONLY is not disabled. Disabled means "not available right now, maybe later";
   read-only means "this is a fact you may read and copy but not change". Dropping the
   edge and tinting the ground says the second — the value stays at full strength and
   stays selectable, and it no longer looks like something you can type in. */
.al-input[readonly] {
	--al-input-border-color: transparent;
	--al-input-fill: var(--al-sys-color-surface-sunken);
	padding-left: calc(var(--al-input-padding-x) - var(--al-input-border-width));
}

/* ERROR rides on aria-invalid, so it cannot be styled without also being announced.
   Edge and message only — no tint. Errors arrive in groups, and the version that looks
   better with one is not the one that survives six on a single form. */
.al-input[aria-invalid='true'] {
	--al-input-border-color: var(--al-sys-color-status-error);
}
.al-field:has([aria-invalid='true']) > small {
	--al-field-hint-color: var(--al-sys-color-status-error-text);
}

.al-input-sm {
	--al-input-height: var(--al-sys-control-height-sm);
	--al-input-padding-x: var(--al-sys-control-padding-x-sm);
	--al-input-font-size: var(--al-sys-font-size-200);
}
.al-input-lg {
	--al-input-height: var(--al-sys-control-height-lg);
	--al-input-padding-x: var(--al-sys-control-padding-x-lg);
	--al-input-font-size: var(--al-sys-font-size-400);
}

/* A unit, a currency symbol, a glyph — inside the field's edge so the whole thing
   reads as one control rather than a field with something parked next to it. */
.al-affix {
	--al-affix-height: var(--al-sys-control-height-md);
	--al-affix-padding-x: var(--al-sys-control-padding-x-md);
	--al-affix-font-size: var(--al-sys-font-size-300);
	--al-affix-radius: var(--al-sys-control-radius);
	--al-affix-border-width: var(--al-sys-control-border-width);
	--al-affix-border-color: var(--al-sys-color-border-control);
	--al-affix-fill: var(--al-sys-color-surface-control);
	/* the unit is CONTENT, not a hint. It reads the secondary ink rung, not the muted
	   one — a gram symbol you cannot read is a broken control, not a quiet one. */
	--al-affix-unit-color: var(--al-sys-color-text-secondary);
	--al-affix-line-height: var(--al-sys-line-height-control);

	--al-affix-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-affix-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-affix-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-affix-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-affix-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box; /* see the button — the system owns its box model */
	display: flex;
	align-items: center;
	width: 100%;
	flex: none;
	min-height: var(--al-affix-height);
	border: var(--al-affix-border-width) solid var(--al-affix-border-color);
	border-radius: var(--al-affix-radius);
	background: var(--al-affix-fill);
	/* THE TYPE IS STATED, NOT INHERITED, and this is the reason: line-height above is a
	   unitless multiplier, so it resolves against whatever font-size reaches this element.
	   The size token existed and was only ever applied to the unit INSIDE, so the wrapper
	   took the host page's type: 13px on the config lists, a 15.6px line box against the
	   16.8px this control is supposed to have. Nothing looked wrong because the input
	   states its own size, so only the box around it drifted. Same rule .al-card-title and
	   .al-select already state for themselves: a control whose geometry the host decides is
	   not a control. */
	font-size: var(--al-affix-font-size);
	line-height: var(--al-affix-line-height); /* see the input */
}
.al-affix > span {
	flex: none;
	padding: 0 var(--al-affix-padding-x);
	color: var(--al-affix-unit-color);
}
.al-affix .al-input {
	border: 0;
	height: auto;
	background: transparent;
	box-shadow: none;
	/* the input is the part of an affix that gives way. The base rule sets flex:none so
	   a container cannot shrink a control below its tier, but in here that is exactly
	   backwards: an unshrinkable 100%-wide input pushes the unit symbol out through the
	   affix's own edge, so "$" and "g" ended up floating outside the box they are meant
	   to sit inside. Caught by looking at it, not by the numbers, which were all fine. */
	flex: 1 1 auto;
	width: auto;
	min-width: 0;
	/* the AFFIX owns the height, not the input inside it. Leaving the input's own floor
	   in place stacked it on top of the affix's borders and made every affixed field
	   38px while claiming the 36px rung — two pixels taller than the plain field it sits
	   next to in the same form. */
	min-height: 0;
}

/* STATES re-point the affix's own tokens — rule 8, same as every other control. The
   earlier shape of this fix set border-color as a PROPERTY, which is the exact
   mistake that made four of the five audit bugs, so it is not used here.

   Invalid is written as a :not() on focus rather than relying on source order, so a
   focused wrong field keeps its red edge instead of turning brand-green under the
   cursor. */
/* THE AFFIX GETS THE SAME RING AS A PLAIN FIELD. It had none at all: the inner input's
   ring is deliberately removed so the pair reads as one control, and nothing replaced
   it, so focus was a 1.09:1 border swap. Two controls the file describes as reading
   "as one control" cannot have completely different focus treatments. */
.al-affix:focus-within {
	outline: var(--al-affix-focus-ring-width) solid transparent;
	outline-offset: var(--al-affix-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-affix-focus-hairline-width) var(--al-affix-focus-hairline-light),
		0 0 0 calc(var(--al-affix-focus-hairline-width) + var(--al-affix-focus-ring-width)) var(--al-affix-focus-ring-color),
		0 0 0 calc(var(--al-affix-focus-hairline-width) * 2 + var(--al-affix-focus-ring-width)) var(--al-affix-focus-hairline-dark);
}
.al-affix:not(:has([aria-invalid='true'])):focus-within {
	--al-affix-border-color: var(--al-sys-color-focus-ring);
}
.al-affix:has([aria-invalid='true']):focus-within {
	--al-affix-focus-ring-color: var(--al-sys-color-status-error);
}
.al-affix:has([readonly]) {
	--al-affix-border-color: transparent;
	--al-affix-fill: var(--al-sys-color-surface-sunken);
}
.al-affix:has([aria-invalid='true']) {
	--al-affix-border-color: var(--al-sys-color-status-error);
}

/* SIZE follows the input inside it. Without this the affix hardcoded the md tier, so
   a small or large field wrapped in one silently stayed medium. */
.al-affix:has(.al-input-sm) {
	--al-affix-height: var(--al-sys-control-height-sm);
	--al-affix-padding-x: var(--al-sys-control-padding-x-sm);
	--al-affix-font-size: var(--al-sys-font-size-200);
}
.al-affix:has(.al-input-lg) {
	--al-affix-height: var(--al-sys-control-height-lg);
	--al-affix-padding-x: var(--al-sys-control-padding-x-lg);
	--al-affix-font-size: var(--al-sys-font-size-400);
}

/* The field needs no dark rule. It reads --al-sys-color-surface-control for its fill,
   -text-primary for its value, -border-control for its edge, -text-placeholder and
   -text-muted for its quiet ink, and the dark scope re-points all five. Deleting the
   two blocks that used to live here is what makes read-only and invalid survive on a
   dark panel: there is no longer a rule at (0,2,0) sitting above the state rules. */

/* ============================================================================
   THE SELECT

   SCOPE, and it is deliberately smaller than the last version: the recipe dresses the
   CLOSED control. The open list is drawn by the operating system and is not ours to
   style, and the product's real dropdowns are Kendo's, which already handle the data
   binding, paging and virtualisation that a hand-built listbox would have to
   re-implement. Making those match the recipe is an OVERRIDE LAYER, written later
   alongside the ones for ag-Grid, Bootstrap and the rest. No token here exists to
   serve a third-party control.

   ============================================================================ */
.al-select {
	--al-select-height: var(--al-sys-control-height-md);
	--al-select-padding-x: var(--al-sys-control-padding-x-md);
	--al-select-font-size: var(--al-sys-font-size-300);
	--al-select-radius: var(--al-sys-control-radius);
	--al-select-border-width: var(--al-sys-control-border-width);
	--al-select-border-color: var(--al-sys-color-border-control);
	--al-select-fill: var(--al-sys-color-surface-control);
	--al-select-text: var(--al-sys-color-text-primary);
	--al-select-placeholder: var(--al-sys-color-text-placeholder);
	--al-select-caret: var(--al-sys-color-text-muted);
	--al-select-caret-size: 10px;
	--al-select-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-select-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-select-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-select-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-select-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-select-focus-ring-color: var(--al-sys-color-focus-ring);
	--al-select-hover-border-color: var(--al-sys-color-accent-primary-line);
	/* the option list. It inherited nothing before, which is how a dark ground produced
     a white list carrying white text — the popup kept the platform's white while the
     text took the ground's ink. Both are set now, from ground-aware roles. */
	--al-select-option-fill: var(--al-sys-color-surface-raised);
	--al-select-option-text: var(--al-sys-color-text-primary);
	--al-select-option-selected-fill: var(--al-sys-color-accent-primary);
	--al-select-option-selected-text: var(--al-sys-color-text-on-accent);
	--al-select-option-disabled-text: var(--al-sys-color-text-placeholder);
	--al-select-option-check: var(--al-sys-color-accent-primary-line);
	/* TWO TINTS, ONE LADDER. The chosen row is tinted and carries the check; hovering
     any row tints it harder. They are steps of the same idea rather than two competing
     treatments, and the check is what says "chosen" when the pointer is elsewhere. */
	--al-select-option-selected-fill: var(--al-sys-color-row-tint-selected);
	--al-select-option-hover-fill: var(--al-sys-color-row-tint-active);

	/* appearance:none is the whole trick — it strips the platform's chrome so the box
     underneath is an ordinary one we can dress, while the control stays a real
     <select>: keyboard, typeahead, screen readers and touch all still the browser's. */
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	display: block;
	width: 100%;
	min-height: var(--al-select-height);
	padding: 0.35em calc(var(--al-select-padding-x) * 2 + var(--al-select-caret-size)) 0.35em var(--al-select-padding-x);
	border: var(--al-select-border-width) solid var(--al-select-border-color);
	border-radius: var(--al-select-radius);
	background-color: var(--al-select-fill);
	color: var(--al-select-text);
	font: inherit;
	font-size: var(--al-select-font-size);
	/* line-height is set, not inherited. The page had 1.65 on the body, which put a
     21.45px line box inside a 28px control and made the small tier render 33px — a
     size tier that changes depending on the host page's typography is not a tier.
     Same failure as borrowing box-sizing; 1.2 leaves room for descenders. */
	line-height: 1.2;
	cursor: pointer;
	flex: none;
	align-self: center;

	/* THE CARET IS A CHEVRON, and it was a filled triangle until Jeff accepted this.
     Two gradient halves drew that triangle here, and the ::picker-icon block further down
     drew another one, so a select sat six pixels under a ui-select wearing a chevron and
     the two disagreed. Every other downward mark in this system is the chevron: the
     ui-select's, the app's own native-select override, this control's own popup.

     The mark is MASKED rather than set as a background-image, because the reference token
     bakes a colour into its SVG and a mask takes --al-select-caret instead. Same reason
     the checkbox masks its tick. */
	background-image: none;
	background-repeat: no-repeat;
}
.al-select::-ms-expand {
	display: none;
}

/* THE FALLBACK CARET. Chrome takes the ::picker-icon path below and never reaches this;
   everywhere else this is the mark. A select cannot carry a pseudo-element of its own, so
   it rides on the background of the control and is positioned from the same token the
   padding reserves space with. */
.al-select {
	background-image: var(--al-ref-icon-chevron-down);
	background-position: right var(--al-select-padding-x) center;
	background-size: var(--al-select-caret-size) var(--al-select-caret-size);
	background-repeat: no-repeat;
}

/* THE OPTION LIST. How much of this the engine honours depends on the platform: where
   the popup is drawn by the browser these apply, where it is drawn by the operating
   system they are ignored and the OS decides. Setting them is still right — it is the
   difference between a dark list and a white one wherever we are allowed an opinion,
   and it costs nothing where we are not. */
.al-select option {
	background-color: var(--al-select-option-fill);
	color: var(--al-select-option-text);
}
.al-select option:checked {
	background: var(--al-select-option-selected-fill);
	color: var(--al-select-option-selected-text);
}
.al-select option:disabled {
	color: var(--al-select-option-disabled-text);
}

/* HOVER. It was missing, and a control you click should say so under the pointer. The
   edge takes the accent as a LINE — the same move the focus ring makes, one step
   quieter — rather than a veil, because a veil over a field reads as a disabled or a
   selected state rather than a live one. */
.al-select:hover:not(:disabled):not([data-readonly='true']) {
	--al-select-border-color: var(--al-select-hover-border-color);
}
.al-select:focus-visible {
	outline: var(--al-select-focus-ring-width) solid transparent;
	outline-offset: var(--al-select-focus-hairline-width);
	border-color: var(--al-select-border-color);
	box-shadow:
		0 0 0 var(--al-select-focus-hairline-width) var(--al-select-focus-hairline-light),
		0 0 0 calc(var(--al-select-focus-hairline-width) + var(--al-select-focus-ring-width)) var(--al-select-focus-ring-color),
		0 0 0 calc(var(--al-select-focus-hairline-width) * 2 + var(--al-select-focus-ring-width)) var(--al-select-focus-hairline-dark);
}
.al-select:not([aria-invalid='true']):focus-visible {
	--al-select-border-color: var(--al-sys-color-focus-ring);
}
.al-select[aria-invalid='true'] {
	--al-select-border-color: var(--al-sys-color-status-error);
}
.al-select[aria-invalid='true']:focus-visible {
	--al-select-focus-ring-color: var(--al-sys-color-status-error);
}
.al-select:disabled {
	opacity: var(--al-select-disabled-opacity);
	pointer-events: none;
}
/* a select has no read-only, so the pattern is a disabled control PLUS the read-only
   look, and the value still has to read at full strength */
.al-select[data-readonly='true'] {
	--al-select-border-color: transparent;
	--al-select-fill: var(--al-sys-color-surface-sunken);
	opacity: 1;
	background-image: none;
	cursor: default;
	pointer-events: none;
}
.al-select[data-placeholder='true'] {
	color: var(--al-select-placeholder);
}
.al-select-sm {
	--al-select-height: var(--al-sys-control-height-sm);
	--al-select-padding-x: var(--al-sys-control-padding-x-sm);
	--al-select-font-size: var(--al-sys-font-size-200);
}
.al-select-lg {
	--al-select-height: var(--al-sys-control-height-lg);
	--al-select-padding-x: var(--al-sys-control-padding-x-lg);
	--al-select-font-size: var(--al-sys-font-size-400);
}

/* ============================================================================
   THE POPUP, WHERE THE BROWSER WILL GIVE IT TO US.

   appearance:base-select takes the option list out of the platform's hands and makes
   it a real element in the page's top layer. It is STILL a <select>: keyboard,
   typeahead, screen readers, touch and form submission are all unchanged. What we get
   is the chrome — the ground, the hover, the chosen mark, the caret.

   Everything above this block is the fallback and needs no support for any of it, so a
   browser without base-select shows the platform's own list and nothing breaks. That
   is the whole reason this is written as an @supports block rather than as the
   default: the control degrades to the OS list rather than to a broken one.

   This is also what un-parks --al-sys-elevation-raised: the popup is ours to cast a
   shadow on, so the token finally has a consumer.
   ============================================================================ */
@supports (appearance: base-select) {
	.al-select,
	.al-select::picker(select) {
		appearance: base-select;
	}

	/* THE CARET IS A REAL CHILD NOW, so the control has to be a flex row. Left as
     display:block it laid the icon out as a block and it dropped onto its own line
     underneath the value — which is what put the caret under the text when closed and
     made it appear to jump when the list opened. The right padding also goes: it was
     reserving room for a background image that no longer exists. */
	.al-select:not([size]):not([multiple]) {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 8px;
		padding-right: var(--al-select-padding-x);
	}

	.al-select::picker(select) {
		background: var(--al-select-option-fill);
		border: 1px solid var(--al-sys-color-border);
		border-radius: var(--al-select-radius);
		box-shadow: var(--al-sys-elevation-raised);
		padding: 5px;
		margin-block: 5px 0;
		max-height: 264px;
		overflow-y: auto;
		overscroll-behavior: contain;
	}

	.al-select option {
		display: flex;
		align-items: center;
		gap: 8px;
		min-height: var(--al-select-height);
		padding: 0.3em 10px;
		border-radius: 5px;
		font-size: var(--al-select-font-size);
		background: transparent;
		/* the colour is stated, not inherited: the user agent paints a checked option with
       its own accent pair, and leaving `color` alone left the chosen row's text pale
       on our own hover tint. */
		color: var(--al-select-option-text);
	}
	/* THE GROUND SAYS WHERE YOU ARE, THE CHECK SAYS WHAT IS CHOSEN.
     Two facts, two channels. Filling the chosen row with the accent instead would
     make an option carrying its own colour vanish into it, and it would then be
     competing with hover for the same background. */
	/* the chosen row stays tinted whether or not the pointer is near it */
	.al-select option:checked {
		background: var(--al-select-option-selected-fill);
		color: var(--al-select-option-text);
		font-weight: 600;
	}
	/* hover is one step up the same ladder, and it wins over chosen */
	.al-select option:hover,
	.al-select option:focus,
	.al-select option:active,
	.al-select option:checked:hover,
	.al-select option:checked:active {
		background: var(--al-select-option-hover-fill);
		color: var(--al-select-option-text);
		outline: none;
	}
	.al-select option::checkmark {
		content: '✓';
		color: var(--al-select-option-check);
		font-weight: 700;
		width: 1em;
		order: -1;
	}
	.al-select option:disabled {
		opacity: var(--al-sys-state-disabled-opacity);
	}

	/* the caret becomes a real element, so it can turn over when the list is open.
     The browser's own picker-icon is a filled triangle, and colouring it leaves it a
     triangle: it has to be MASKED OUT and replaced, or the chevron above never appears on
     the one engine most people are using. Masking rather than hiding keeps the element,
     so the rotate-on-open below still has something to turn. */
	.al-select {
		background-image: none;
	}
	.al-select::picker-icon {
		color: transparent;
		width: var(--al-select-caret-size);
		height: var(--al-select-caret-size);
		background-color: var(--al-select-caret);
		mask: var(--al-ref-icon-chevron-down) center / contain no-repeat;
		-webkit-mask: var(--al-ref-icon-chevron-down) center / contain no-repeat;
		transition: transform var(--al-sys-motion-duration-state) var(--al-sys-motion-easing);
	}
	.al-select:open::picker-icon {
		transform: rotate(180deg);
	}
	@media (prefers-reduced-motion: reduce) {
		.al-select::picker-icon {
			transition: none;
		}
	}
}

/* ============================================================================
   THE CHECKBOX AND THE RADIO

   They are ONE control with two marks and two meanings. Everything except the mark
   and the shape of the box is shared, so they are written once — a checkbox that
   drifts from its radio is the kind of inconsistency nobody reports and everybody
   notices.

   THE MARK IS A MASK, not a background image. The shape comes from an SVG and the
   COLOUR comes from a token, so a theme can move it. The select's caret had to bake
   its colour into the image; this does not, and it is the better pattern of the two.
   ============================================================================ */
.al-check {
	--al-check-size: 18px;
	--al-check-radius: 5px;
	--al-check-border-width: var(--al-sys-control-border-width);
	--al-check-border-color: var(--al-sys-color-border-control);
	--al-check-fill: var(--al-sys-color-surface-control);
	--al-check-mark: var(--al-sys-color-text-on-accent);
	--al-check-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.2 4.3 6.5 11 2.8 7.3' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-check-selected-fill: var(--al-sys-color-accent-primary);
	--al-check-selected-border-color: var(--al-sys-color-accent-primary);
	--al-check-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-check-label-color: var(--al-sys-color-text-primary);
	--al-check-label-size: var(--al-sys-font-size-300);
	--al-check-gap: 9px;
	--al-check-row-gap: 9px;
	--al-check-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-check-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-check-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-check-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-check-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-check-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	/* FLEX-START, not centre. Centring looks right on a one-line label and wrong on a
     two-line one: the box drifts to the middle of the paragraph instead of sitting
     beside the thing it marks. */
	align-items: flex-start;
	gap: var(--al-check-gap);
	cursor: pointer;
	font-size: var(--al-check-label-size);
	line-height: 1.45; /* set, not inherited — see the select */
	color: var(--al-check-label-color);
}
.al-check > input {
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	flex: none;
	position: relative;
	width: var(--al-check-size);
	height: var(--al-check-size);
	/* centre the box on the FIRST line of the label, whatever the label does after */
	margin: calc((1.45em - var(--al-check-size)) / 2) 0 0;
	border: var(--al-check-border-width) solid var(--al-check-border-color);
	border-radius: var(--al-check-radius);
	background: var(--al-check-fill);
	cursor: inherit;
}
.al-check > input[type='radio'] {
	border-radius: 50%;
}

/* THE TOUCH TARGET is bigger than the box. An 18px square is the right SIZE and the
   wrong TARGET, so the box grows an invisible one around itself rather than growing
   itself. Nothing moves on the page. */
.al-check > input::before {
	content: '';
	position: absolute;
	inset: calc((var(--al-check-size) - var(--al-check-hit)) / 2);
	--al-check-hit: 24px;
}

.al-check > input::after {
	content: '';
	position: absolute;
	inset: -1px;
	background: var(--al-check-mark);
	-webkit-mask: var(--al-check-mark-image) center / contain no-repeat;
	mask: var(--al-check-mark-image) center / contain no-repeat;
	opacity: 0;
}
.al-check > input:checked::after,
.al-check > input:indeterminate::after {
	opacity: 1;
}

.al-check > input:checked,
.al-check > input:indeterminate {
	--al-check-fill: var(--al-check-selected-fill);
	--al-check-border-color: var(--al-check-selected-border-color);
}
.al-check > input:indeterminate {
	--al-check-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.6 8h8.8' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
}
.al-check > input[type='radio']:checked {
	--al-check-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3.4' fill='%23000'/%3E%3C/svg%3E");
}

.al-check:hover > input:not(:disabled) {
	--al-check-border-color: var(--al-check-hover-border-color);
}

.al-check > input:focus-visible {
	outline: var(--al-check-focus-ring-width) solid transparent;
	outline-offset: var(--al-check-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-check-focus-hairline-width) var(--al-check-focus-hairline-light),
		0 0 0 calc(var(--al-check-focus-hairline-width) + var(--al-check-focus-ring-width)) var(--al-check-focus-ring-color),
		0 0 0 calc(var(--al-check-focus-hairline-width) * 2 + var(--al-check-focus-ring-width)) var(--al-check-focus-hairline-dark);
}
/* DISABLED POINTS not-allowed, AND READONLY DOES NOT. This is the system's policy and it
   is stated here because this is the control that caused it (ALLM-7165): a permission-locked
   location faded to 0.5 and said nothing else, so it read as a box you had not ticked yet
   rather than one you cannot. Opacity says inert; the cursor is what says "not by you".

   Thirteen disabled states carry it. The three [data-readonly='true'] controls and the two
   static crumbs deliberately keep `default`: readonly is not refusal, it is a value that
   happens to be fixed, and not-allowed there would promise a permission problem that does
   not exist. Jeff's call, made recipe-wide rather than on this one control, so a disabled
   switch cannot behave differently from a disabled checkbox.

   The cursor is not the whole answer and was never meant to be -- it is unreachable by
   keyboard and absent on touch. The reason itself lives in the app's [data-tooltip], which
   opens on focus as well as hover. */
.al-check:has(> input:disabled) {
	opacity: var(--al-check-disabled-opacity);
	cursor: not-allowed;
}
.al-check[data-invalid='true'] > input {
	--al-check-border-color: var(--al-sys-color-status-error);
}
.al-check[data-invalid='true'] > input:focus-visible {
	--al-check-focus-ring-color: var(--al-sys-color-status-error);
}

.al-check-sm {
	--al-check-size: 15px;
	--al-check-radius: 4px;
	--al-check-label-size: var(--al-sys-font-size-200);
}

/* A SET. Several of them under one name and one line, which is the field frame doing
   the same job it does for an input — so a group of checkboxes gets a hint, an error
   and a required mark without inventing anything. */
.al-check-set {
	display: flex;
	flex-direction: column;
	gap: var(--al-check-row-gap);
}

/* ---- text-entry ---------------------------------------------------- */
/* =============================================================================
   TEXT ENTRY AND FORM STRUCTURE

   Five things: the textarea, the number field, the search field, the field group
   and the actions row. Four of them are the existing field family extended rather
   than restated. The text input, the field frame and the affix already decided what
   a typeable box looks like, so nothing here re-decides it. Where a rule below looks
   like a copy of one in the recipe, it is a copy on purpose: the same three-layer
   focus ring, the same aria-invalid hook, the same read-only treatment. A second
   idiom for "you may type here" is the failure this avoids.

   NOT ONE RULE IN THIS FILE NAMES A CONTROL ON A DARK GROUND. Every colour comes
   from a sys role that .al-on-dark re-points, so all five are correct on both
   grounds the day they are written. If something here ever looks wrong on a dark
   panel, the bug is a hardcoded value in this file, not a missing dark rule.
   ============================================================================= */

/* =============================================================================
   THE TEXTAREA

   The single-line input truncates; this one wraps. That is the whole difference in
   what it means, and almost the whole difference in how it is built: same edge,
   same fill, same ring, same states, one extra decision about height.

   HEIGHT IS ROWS, NOT A CONTROL TIER. A control tier says how tall a box you click
   is. A textarea's height says how much of the text you can see at once, which is a
   different question and belongs to the content. So `rows` still works, and the size
   variants move the type and the horizontal padding without touching the height.
   ============================================================================= */

.al-textarea {
	--al-textarea-padding-x: var(--al-sys-control-padding-x-md);
	/* vertical padding steps with the tier and comes from the spacing scale. A
	   single-line input can use one em-relative value because its box is a floor;
	   here the padding is real air above the first line of prose, so it is a length. */
	--al-textarea-padding-y: var(--al-sys-space-100);
	--al-textarea-font-size: var(--al-sys-font-size-300);
	/* SET, NOT INHERITED. This is the trap the select block warns about, and it bites
	   hardest here: line-height is inherited, and a host page running 1.65 would make
	   a three-line box render four lines tall while the token still said three. */
	--al-textarea-line-height: var(--al-sys-line-height-body);
	/* THE FLOOR, in lines. Three, because two reads as a stretched input and someone
	   writing a manifest note needs to see the sentence before the one they are
	   typing. Anything shorter than three lines is a text input wearing the wrong
	   control. Above the floor the height is the `rows` attribute, which is the
	   author's own statement of how much of the note should be visible. */
	--al-textarea-rows: 3;
	/* VERTICAL ONLY, and held as a token because two states turn it off. A state that
	   overrode `resize` as a property would be the rule-8 mistake: the value belongs to
	   the control, the state only re-points it. */
	--al-textarea-resize: vertical;
	--al-textarea-radius: var(--al-sys-control-radius);
	--al-textarea-border-width: var(--al-sys-control-border-width);
	--al-textarea-border-color: var(--al-sys-color-border-control);
	--al-textarea-fill: var(--al-sys-color-surface-control);
	--al-textarea-text: var(--al-sys-color-text-primary);
	--al-textarea-placeholder: var(--al-sys-color-text-placeholder);
	--al-textarea-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-textarea-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-textarea-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-textarea-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-textarea-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-textarea-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box; /* see the button: the system owns its box model */
	display: block;
	width: 100%;
	max-width: 100%; /* a textarea's `cols` default is 20 characters and it is an
	     inline-block, so without this a wide value can push a form column open */
	min-height: calc(
		var(--al-textarea-rows) * var(--al-textarea-line-height) * 1em + 2 * var(--al-textarea-padding-y) + 2 * var(--al-textarea-border-width)
	);
	padding: var(--al-textarea-padding-y) var(--al-textarea-padding-x);
	border: var(--al-textarea-border-width) solid var(--al-textarea-border-color);
	border-radius: var(--al-textarea-radius);
	background: var(--al-textarea-fill);
	color: var(--al-textarea-text);
	/* the face is INHERITED, exactly as the input's is. The system names its own
	   family at the sys tier, but a note whose face does not match the field above it
	   is the visible bug, and matching the input beside it is what this family is for.
	   A bare textarea inherits a monospace face from the user agent in some browsers,
	   which is why `font: inherit` is not optional here. */
	font: inherit;
	font-size: var(--al-textarea-font-size);
	line-height: var(--al-textarea-line-height);
	/* Dragging a textarea wider does not widen the column it sits in, so a horizontal
	   resize overlaps whatever is beside it and the form stays broken until the page
	   reloads. Down is the only direction with anywhere to go. */
	resize: var(--al-textarea-resize);
}
.al-textarea::placeholder {
	color: var(--al-textarea-placeholder);
}

/* NO AUTO-GROW, and this one was written, rendered and taken back out.
   `field-sizing: content` makes a textarea grow as you type, which is pleasant and
   which Chromium supports today. It also silently outranks `rows`: a note authored as
   six lines rendered four in Chrome and six in Firefox, because the height stopped
   being a statement by the author and became a fact about how much had been typed so
   far. A height that means one thing in one browser and another in the next is the
   same defect as a size tier that changes with the host page's typography, and it is
   worse here because nothing on the screen says it happened. So the height is `rows`,
   in every browser, and the box scrolls when it runs out.

   The ring, the invalid hook and the read-only treatment are the input's, verbatim,
   reading this control's own tokens. The border deliberately reads
   --al-textarea-border-color rather than the ring colour: that one word is what stops
   focus erasing a red edge and telling someone their note is fine when it is too
   long. */
.al-textarea:focus {
	outline: var(--al-textarea-focus-ring-width) solid transparent;
	outline-offset: var(--al-textarea-focus-hairline-width);
	border-color: var(--al-textarea-border-color);
	box-shadow:
		0 0 0 var(--al-textarea-focus-hairline-width) var(--al-textarea-focus-hairline-light),
		0 0 0 calc(var(--al-textarea-focus-hairline-width) + var(--al-textarea-focus-ring-width)) var(--al-textarea-focus-ring-color),
		0 0 0 calc(var(--al-textarea-focus-hairline-width) * 2 + var(--al-textarea-focus-ring-width)) var(--al-textarea-focus-hairline-dark);
}
.al-textarea:not([aria-invalid='true']):focus {
	--al-textarea-border-color: var(--al-sys-color-accent-primary-line);
}
.al-textarea[aria-invalid='true'] {
	--al-textarea-border-color: var(--al-sys-color-status-error);
}
.al-textarea[aria-invalid='true']:focus {
	--al-textarea-focus-ring-color: var(--al-sys-color-status-error);
}

/* READ-ONLY loses its edge and sinks, same as the input: a fact you may read and
   copy. No padding compensation, because a transparent border still occupies its
   1px, so nothing has moved and adjusting for it would shift the text one pixel out
   of line with every other field in the group. The handle goes too, because there is
   nothing to resize toward: the value is already whatever length it is. */
.al-textarea[readonly] {
	--al-textarea-border-color: transparent;
	--al-textarea-fill: var(--al-sys-color-surface-sunken);
	--al-textarea-resize: none;
}
.al-textarea:disabled {
	--al-textarea-resize: none;
	opacity: var(--al-textarea-disabled-opacity);
	pointer-events: none;
}

/* SIZE re-points type and horizontal padding and leaves the height to `rows`, which
   is the point of the height being rows: a compact textarea holds the same amount of
   text in smaller type rather than showing less of it. */
.al-textarea-sm {
	--al-textarea-padding-x: var(--al-sys-control-padding-x-sm);
	--al-textarea-padding-y: var(--al-sys-space-50);
	--al-textarea-font-size: var(--al-sys-font-size-200);
}
.al-textarea-lg {
	--al-textarea-padding-x: var(--al-sys-control-padding-x-lg);
	--al-textarea-padding-y: var(--al-sys-space-150);
	--al-textarea-font-size: var(--al-sys-font-size-400);
}

/* -----------------------------------------------------------------------------
   THE NOTE LINE, AND THE CHARACTER COUNT

   The field frame already reserves exactly one line under the control and gives it
   to the hint and then to the error. The count rides that SAME line rather than
   adding a second one: a form that grows a line when a counter appears is the jump
   the reserved line exists to prevent.

   The second selector is not decoration. `.al-field > small` is a class plus a type,
   so a bare class loses to it and the note would silently stay display:block with the
   count sitting in the text flow. Naming the parent is what outranks it, and it is
   cheaper than a second element.
   ----------------------------------------------------------------------------- */
.al-fieldnote,
.al-field > .al-fieldnote {
	--al-fieldnote-gap: var(--al-sys-space-150);

	box-sizing: border-box;
	display: flex;
	align-items: baseline; /* the count sits on the hint's first baseline, not on its
	     box, so a hint that wraps to two lines does not drag the count down with it */
	gap: var(--al-fieldnote-gap);
}
.al-charcount {
	--al-charcount-color: var(--al-sys-color-text-muted);
	--al-charcount-size: var(--al-sys-font-size-100);
	--al-charcount-over-color: var(--al-sys-color-status-error-text);

	box-sizing: border-box;
	margin-inline-start: auto; /* to the far end, whether or not there is a hint beside
	     it. A count is scanned, not read, and it is only findable in a fixed place. */
	flex: none;
	font-size: var(--al-charcount-size);
	/* the colour is STATED, not inherited. An invalid field turns its note red, and a
	   count that went red with it would claim the length was the problem when the
	   problem was the licence number. */
	color: var(--al-charcount-color);
	font-variant-numeric: tabular-nums; /* the digits change on every keystroke, and
	     proportional figures make the whole count shuffle sideways as they do */
}
/* Over the limit is the count's own state, and it is reachable only where the limit
   is enforced somewhere other than the browser: with `maxlength` set, typing stops at
   the cap and this never fires. */
.al-charcount[data-over='true'] {
	--al-charcount-color: var(--al-charcount-over-color);
}

/* =============================================================================
   THE NUMBER FIELD

   Not a new control: `.al-input` with the digits made readable and the browser's
   spinner taken away. It mints no tokens of its own, because it re-points nothing.
   Everything it looks like is the input's, and the two things it changes are
   structural facts about numerals rather than values a theme would ever move.

   NO STEPPERS, and that is the decision this control exists to record. The native
   spinner is twelve pixels of unlabelled target that changes the number by an amount
   nobody stated, it appears only on hover so it is discoverable by accident, and it
   puts the scroll wheel in charge of a package weight while someone is scrolling
   past it. Every number in this product — a weight in grams, a unit price, a case
   count of 144 — is typed or scanned, never nudged. Where plus and minus really are
   the interaction, that is a quantity stepper: a control with two real targets, its
   own states and its own tier, and it gets built when a screen needs one.

   Follows `.al-btn-block`'s precedent: a variant that changes structure states
   properties, and says why, rather than pretending to be a token.
   ============================================================================= */
.al-input-number {
	/* tabular figures, so a column of quantities lines up and the caret does not
	   shuffle sideways as 1s become 7s while someone types */
	font-variant-numeric: tabular-nums;
	/* LEFT ALIGNED, like every other field, which is why nothing sets it: start
	   alignment is already the default and restating it would only be a thing to keep
	   in step. The box is as wide as the layout, not as wide as the number, so
	   right-aligning parks the value away from its label and away from the unit
	   sitting on the left edge of an affix. A column of numbers lining up on the
	   decimal point is a table cell's job, where the cell is sized to the number.

	   `appearance: textfield` is what removes the spinner in Firefox; the two
	   pseudo-elements below do it in Chrome and Safari. */
	appearance: textfield;
	-moz-appearance: textfield;
}
.al-input-number::-webkit-outer-spin-button,
.al-input-number::-webkit-inner-spin-button {
	-webkit-appearance: none;
	appearance: none;
	margin: 0;
}

/* =============================================================================
   THE SEARCH FIELD

   A SEARCH FIELD IS AN AFFIX. It carries `al-affix al-search`, and that is
   deliberate: the frame, the edge, the fill, the focus ring, read-only, invalid and
   all three sizes are already decided there, and a search box that drifted from the
   field beside it would be the most obvious inconsistency on any toolbar. What this
   adds is the payload — a glyph in front and a way to empty it.

   The magnifier is a pseudo-element, because it is decoration: nothing should be able
   to select it, tab to it or hear it announced. The clear button is a real <button>,
   because it does something.
   ============================================================================= */
.al-search {
	--al-search-icon-size: var(--al-sys-icon-size-md);
	--al-search-icon-color: var(--al-sys-color-text-muted);
	/* the shape is a mask and the colour is a token, so the glyph follows the ground
	   like everything else. The select's caret has to bake its colour into the image;
	   the checkbox's mark does not, and this follows the checkbox. */
	--al-search-icon-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='6.9' cy='6.9' r='4.4'/%3E%3Cpath d='M10.3 10.3 14 14'/%3E%3C/g%3E%3C/svg%3E");
	/* the gutter is the air between the frame's edge and the glyph or the clear
	   button. It steps with the tier because a 12px gutter inside a 28px box leaves
	   the glyph nearly touching the text. */
	--al-search-gutter: var(--al-sys-space-150);
	--al-search-input-gap: var(--al-sys-space-100);

	--al-search-clear-size: 24px; /* the TARGET, not the mark. Held here rather than
	     taken from the control tiers because the smallest tier is 28px, and a 28px
	     button inside a 28px frame has nowhere to sit. */
	/* the MARK inside that target. Not an icon rung: 14px is the smallest one and a
	   14px cross in a 24px button reads as a close control for the whole frame rather
	   than a way to empty the field. */
	--al-search-clear-glyph-size: 11px;
	--al-search-clear-color: var(--al-sys-color-text-muted);
	--al-search-clear-hover-color: var(--al-sys-color-text-primary);
	--al-search-clear-radius: var(--al-sys-radius-sm);
	--al-search-clear-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 4 12 12M12 4 4 12'/%3E%3C/g%3E%3C/svg%3E");

	--al-search-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-search-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-search-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-search-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-search-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-search-focus-ring-color: var(--al-sys-color-focus-ring);
}
.al-search::before {
	content: '';
	box-sizing: border-box; /* box-sizing is not inherited, and the usual reset does
	     not match a pseudo-element either */
	flex: none;
	width: var(--al-search-icon-size);
	height: var(--al-search-icon-size);
	margin-inline-start: var(--al-search-gutter);
	background: var(--al-search-icon-color);
	-webkit-mask: var(--al-search-icon-image) center / contain no-repeat;
	mask: var(--al-search-icon-image) center / contain no-repeat;
}
/* The affix's own rule already strips the inner input's border and background. Its
   horizontal padding goes too: the gutter and the gap own the spacing now, and
   leaving the input's 16px in place put the text a long way from a glyph that is
   meant to belong to it. */
.al-search .al-input {
	padding-inline: var(--al-search-input-gap);
}
/* the platform draws its own clear cross on a type=search field, in its own place and
   its own colour, and we draw one */
.al-search .al-input::-webkit-search-cancel-button {
	-webkit-appearance: none;
	appearance: none;
}

.al-search-clear {
	box-sizing: border-box;
	flex: none;
	display: flex;
	align-items: center;
	justify-content: center;
	width: var(--al-search-clear-size);
	height: var(--al-search-clear-size);
	margin-inline-end: var(--al-search-gutter);
	padding: 0;
	border: 0;
	border-radius: var(--al-search-clear-radius);
	background: transparent;
	cursor: pointer;
	appearance: none;
	-webkit-appearance: none;
}
/* the glyph is a child, not a mask on the button itself: a mask clips everything the
   element paints, box-shadow included, so masking the button would delete its focus
   ring — which is exactly the state a missing mark is hardest to notice in */
.al-search-clear::before {
	content: '';
	box-sizing: border-box;
	width: var(--al-search-clear-glyph-size);
	height: var(--al-search-clear-glyph-size);
	background: var(--al-search-clear-color);
	-webkit-mask: var(--al-search-clear-image) center / contain no-repeat;
	mask: var(--al-search-clear-image) center / contain no-repeat;
}
.al-search-clear:hover {
	--al-search-clear-color: var(--al-search-clear-hover-color);
}
.al-search-clear:focus {
	outline: none;
}
.al-search-clear:focus-visible {
	outline: var(--al-search-focus-ring-width) solid transparent;
	outline-offset: var(--al-search-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-search-focus-hairline-width) var(--al-search-focus-hairline-light),
		0 0 0 calc(var(--al-search-focus-hairline-width) + var(--al-search-focus-ring-width)) var(--al-search-focus-ring-color),
		0 0 0 calc(var(--al-search-focus-hairline-width) * 2 + var(--al-search-focus-ring-width)) var(--al-search-focus-hairline-dark);
}
/* NOTHING TO CLEAR, SO NO BUTTON — but the space stays. Hidden with visibility rather
   than display, because taking it out of the flow would widen the input and shunt the
   text sideways on the first keystroke of every search anyone ever runs. It is driven
   by :placeholder-shown, so a search field carries a placeholder saying what it
   searches. It should anyway, and where one is missing the button simply stays
   visible, which is harmless. */
.al-search:has(.al-input:placeholder-shown) .al-search-clear {
	visibility: hidden;
}

/* DISABLED DIMS THE FRAME, ONCE. The inner input dims itself, which inside a frame is
   a dim value sitting in a bright box carrying a bright magnifier — three parts of one
   control disagreeing about whether it is available. The frame takes the dimming and
   the input gives it back, because two opacities multiply and 0.25 is not a state, it
   is a control nobody can read. The same shape of fix as the busy-and-disabled button,
   which sets opacity back to 1 for the same reason. */
.al-search:has(.al-input:disabled) {
	opacity: var(--al-search-disabled-opacity);
	pointer-events: none;
}
/* and the input gives it back by RE-POINTING the input's own disabled token, not by
   overriding its opacity property. Same result, and it survives the input ever dimming
   through anything other than opacity. */
.al-search .al-input:disabled {
	--al-input-disabled-opacity: 1;
}

/* the glyph and the target follow the tier, the same way the affix's own padding and
   type do. Without this a small search field carries a medium magnifier. */
.al-search:has(.al-input-sm) {
	--al-search-icon-size: var(--al-sys-icon-size-sm);
	--al-search-gutter: var(--al-sys-space-100);
	--al-search-clear-size: 20px;
	--al-search-clear-glyph-size: 10px;
}
.al-search:has(.al-input-lg) {
	--al-search-icon-size: var(--al-sys-icon-size-lg);
	--al-search-gutter: var(--al-sys-space-200);
	--al-search-clear-size: 28px;
	--al-search-clear-glyph-size: 13px;
}

/* =============================================================================
   THE FIELD GROUP

   Several fields under one name and one rule. It is a real <fieldset> with a real
   <legend>, for the same reason the field frame is a real <label>: the grouping is
   then a fact the browser and a screen reader already know, rather than something
   restated in an aria attribute that drifts.

   The rule belongs to the heading, not to the fields, so it sits on the legend's
   bottom edge and travels with it. A group with no fields in it yet still reads as a
   group, and nothing has to know how many fields follow.
   ============================================================================= */
.al-fieldgroup {
	--al-fieldgroup-heading-color: var(--al-sys-color-text-primary);
	--al-fieldgroup-heading-size: var(--al-sys-font-size-400);
	--al-fieldgroup-heading-weight: var(--al-sys-font-weight-heading);
	--al-fieldgroup-heading-line-height: var(--al-sys-line-height-heading);
	--al-fieldgroup-rule-color: var(--al-sys-color-border);
	/* held here, not shared: this is a rule BETWEEN things, and the system has no
	   width role for one. The control border width is a different job and borrowing it
	   would tie a heading's underline to the edge of every input. The second component
	   that wants a hairline width is what gets one promoted to the sys tier. */
	--al-fieldgroup-rule-width: 1px;
	--al-fieldgroup-rule-gap: var(--al-sys-space-100);
	--al-fieldgroup-body-gap: var(--al-sys-space-200);
	--al-fieldgroup-description-color: var(--al-sys-color-text-secondary);
	--al-fieldgroup-description-size: var(--al-sys-font-size-300);
	--al-fieldgroup-description-line-height: var(--al-sys-line-height-body);
	/* the description is the one thing in a form that is prose, so it stops where
	   prose stops rather than running the width of a two-column grid */
	--al-fieldgroup-description-measure: var(--al-sys-measure);
	/* the narrowest a field may be before it stops being worth a column of its own:
	   under this a two-word label wraps and a licence number truncates, and one column
	   of readable fields beats two columns of broken ones */
	--al-fieldgroup-column-min: 220px;
	--al-fieldgroup-column-gap: var(--al-sys-space-200);
	/* ROWS GET MORE AIR THAN COLUMNS, and it is not symmetry for its own sake: a
	   field's hint line sits 6px under its control, so an equal row gap would put the
	   next field's label nearer that hint than the hint is to its own field. */
	--al-fieldgroup-row-gap: var(--al-sys-space-300);
	--al-fieldgroup-space-between: var(--al-sys-space-400);

	box-sizing: border-box;
	/* a fieldset's user-agent min-inline-size is min-content, which makes it refuse to
	   shrink inside a flex or grid parent. Every form that has ever overflowed
	   sideways in a narrow panel was this one line missing. */
	min-width: 0;
	margin: 0;
	padding: 0;
	border: 0;
}
/* the space between two groups is the group's own, so a form is a stack of these and
   nothing in between has to know about spacing */
.al-fieldgroup + .al-fieldgroup {
	margin-top: var(--al-fieldgroup-space-between);
}
.al-fieldgroup > legend {
	box-sizing: border-box;
	/* a legend is laid out by the user agent as part of the fieldset's border. These
	   three lines take it back: without them it shrink-wraps its text and the rule
	   underneath stops halfway across the group. */
	display: block;
	width: 100%;
	float: none;
	padding: 0 0 var(--al-fieldgroup-rule-gap);
	margin: 0 0 var(--al-fieldgroup-body-gap);
	border-bottom: var(--al-fieldgroup-rule-width) solid var(--al-fieldgroup-rule-color);
	font-size: var(--al-fieldgroup-heading-size);
	font-weight: var(--al-fieldgroup-heading-weight);
	line-height: var(--al-fieldgroup-heading-line-height);
	color: var(--al-fieldgroup-heading-color);
}
/* ONE optional line under the rule. It is deliberately a plain <p> and deliberately
   singular: a group that needs three paragraphs of explanation is a page someone has
   to read before they can fill it in, and that belongs above the form. */
.al-fieldgroup > p {
	box-sizing: border-box; /* the measure is a size, and a host page that pads its
	     paragraphs would otherwise push prose past 68ch */
	margin: 0 0 var(--al-fieldgroup-body-gap);
	max-width: var(--al-fieldgroup-description-measure);
	font-size: var(--al-fieldgroup-description-size);
	line-height: var(--al-fieldgroup-description-line-height);
	color: var(--al-fieldgroup-description-color);
}
.al-fieldgroup-body {
	box-sizing: border-box;
	display: grid;
	/* auto-fit, so the number of columns comes from the width available rather than
	   from a count written into the markup: the same group is two columns on a page
	   and one in a drawer, with nothing said. min() is the guard — without it a column
	   floored at 220px overflows a container narrower than 220px instead of shrinking. */
	/* EVERY ONE OF THESE THREE CARRIES ITS OWN FALLBACK, and that is not belt and braces.
	   The tokens are declared on .al-fieldgroup, so a body used WITHOUT a fieldset above it
	   — inside a card, a dialog, a panel — resolves them to nothing. An invalid var() in
	   grid-template-columns drops the whole declaration and the group falls back to one
	   column; an invalid one in gap resolves to `normal`, which in a grid is ZERO. Measured
	   on the discount record: three fields at 214px each, edge to edge, touching, with
	   nothing anywhere to say why. The fallbacks restore the group's own defaults, and
	   because they are fallbacks rather than declarations on this element, a fieldset that
	   re-points a token still wins by inheritance. */
	grid-template-columns: repeat(auto-fit, minmax(min(var(--al-fieldgroup-column-min, 220px), 100%), 1fr));
	column-gap: var(--al-fieldgroup-column-gap, var(--al-sys-space-200));
	row-gap: var(--al-fieldgroup-row-gap, var(--al-sys-space-300));
	align-items: start; /* a field carrying a hint must not stretch the one beside it */
}
/* A field that needs the full width — a note, an address, anything that reads as
   prose — says so, and it says it by spanning rather than by leaving the group. */
.al-fieldgroup-wide {
	grid-column: 1 / -1;
}
/* A field that needs to span two columns */
.al-fieldgroup-span-2 {
	grid-column: span 2;
}
/* the whole group in one column, for a drawer or a short form where two columns would
   make the eye jump. A variant re-points a token; it does not restate a grid. */
.al-fieldgroup-stack {
	--al-fieldgroup-column-min: 100%;
}

/* =============================================================================
   FORM ACTIONS

   Where Save and Cancel sit. It owns the gap between them, how the row aligns, and
   how far it stands off the last field. It owns nothing about the form above it:
   this is chrome, not layout.

   THE PRIMARY ACTION GOES LAST. The row ends where the reading ends, and the same
   row appears in a modal footer, where the affirmative on the right is not arguable.
   One answer means Save is in the same place whichever kind of form it is in.

   Order is MARKUP, and that is deliberate. CSS `order` would let a screen put Save
   anywhere while leaving it wherever it was in the tab sequence, so a keyboard user
   would tab to Save while looking at Cancel. Nothing here reorders anything.
   ============================================================================= */
.al-formactions {
	/* 12px, not 8. These are the two buttons in a form whose difference matters most,
	   and the gap between them is the only thing standing between "saved" and
	   "discarded" for someone clicking quickly. */
	--al-formactions-gap: var(--al-sys-space-150);
	--al-formactions-align: flex-end;
	/* the row stands off the last field by more than the fields stand off each other,
	   because it is not another field. A Save button hugging the last input is the
	   commonest thing wrong with a form nobody has designed. */
	--al-formactions-space-above: var(--al-sys-space-300);

	box-sizing: border-box;
	display: flex;
	align-items: center; /* a 44px primary and a 36px cancel share a centre line, so a
	     mixed-tier row does not read as a mistake */
	flex-wrap: wrap; /* four actions in a narrow drawer wrap rather than overflow, and
	     one gap covers both directions */
	justify-content: var(--al-formactions-align);
	gap: var(--al-formactions-gap);
	margin-top: var(--al-formactions-space-above);
}
/* A DESTRUCTIVE ACTION DOES NOT STAND NEXT TO SAVE. It goes to the far end of the
   row, on its own, and the auto margin is what puts it there without a second wrapper
   or a second class on the row. One class, on the one button that needs it. */
.al-formactions-aside {
	margin-inline-end: auto;
}
/* a modal footer or a sticky bar already owns its own padding, so the row's standoff
   would be added on top of it */
.al-formactions-flush {
	--al-formactions-space-above: 0;
}

/* ---- toggles ---------------------------------------------------- */
/* =============================================================================
   THE SWITCH, THE SLIDER AND THE FILE UPLOAD

   Three controls that all answer the same question differently: how does someone
   change a value without typing it.

     THE SWITCH is a state, not a value. It takes effect the moment it moves. That
       is the whole difference from a checkbox, and it is the reason the switch is
       not simply a checkbox wearing a pill.
     THE SLIDER is a value inside a RANGE, where the range is the point. If the
       number matters more than where it sits between its ends, the answer is a
       number field, not this.
     THE UPLOAD is a file arriving, which is the only control here with a lifecycle:
       choose, drop, send, attached, wrong.

   ARCHITECTURE. Same as everything else in the recipe. A property reads only its
   own --al-<component>-* token; a sys token appears only on the right-hand side of
   one of those declarations; a state or a variant RE-POINTS TOKENS and never
   overrides a property. Nothing here names a control inside a ground, so all three
   are correct on a dark panel the day they are written.

   ONE THING WORTH KNOWING BEFORE READING ANY OF IT. The fill-versus-line split
   does real work in this file, and it decides differently for the switch and the
   slider. The switch's ON track is a FILL: it is a ground carrying the thumb on top
   of it, so it stays the same emerald on every ground, exactly as a filled button
   does. The slider's filled portion is a LINE: it carries nothing, it is a mark laid
   on a rail that has to separate from the rail and from the page, so it lightens on
   a dark ground. Two controls, one doctrine, opposite answers, and both are correct.
   ============================================================================= */

/* =============================================================================
   THE SWITCH

   A switch is NOT a checkbox. A checkbox is a value you are editing and will save
   when you press Save; a switch is a thing that happens when you move it. Using one
   for the other is the most common mistake in this family, and the cost is real: a
   checkbox that fired immediately loses someone's work, and a switch that waited for
   a Save button leaves a room unlocked because nobody pressed it.

   Because it takes effect immediately it needs a state no other control in the
   recipe has: PENDING. Something has to hold the moment between the flick and the
   server agreeing. Without it a switch either lies (it shows the new state before it
   is true) or freezes (it shows the old state while the request is in flight and
   looks broken). It is the busy state from the button, on the thumb.

   THE MARK IS THE THUMB, and its colour is one token that everything else reads.
   That is the sanctioned composition alias, exactly one per component: the thumb's
   fill and the pending spinner both read --al-switch-mark, so :checked re-points one
   value and cannot miss a surface.
   ============================================================================= */
.al-switch {
	/* -- the composition alias: one colour, two consumers ---------------------- */
	--al-switch-mark: var(--al-sys-color-border-control);

	/* -- scale. The thumb and its travel are DERIVED from the track, so a size
        variant re-points the track and the geometry follows. Deriving rather than
        listing is what stops a size variant shipping a 20px track with a 14px
        thumb still sized for the 24px one. --------------------------------- */
	--al-switch-track-height: 20px;
	--al-switch-track-width: 34px;
	--al-switch-thumb-inset: 3px;
	--al-switch-thumb-size: calc(var(--al-switch-track-height) - var(--al-switch-thumb-inset) * 2);
	--al-switch-travel: calc(var(--al-switch-track-width) - var(--al-switch-thumb-size) - var(--al-switch-thumb-inset) * 2);
	/* the invisible target around a small mark, same idea as the checkbox: the track
     is the right SIZE and the wrong TARGET, so it grows a target rather than
     growing itself. Nothing on the page moves. */
	--al-switch-hit: 24px;

	--al-switch-track-border-width: var(--al-sys-control-border-width);
	--al-switch-track-radius: var(--al-sys-radius-pill);

	/* -- colour. OFF is a recessed empty channel with the same edge every other
        control has, so a switch reads at the same weight as the field beside it.
        ON is the accent as a FILL, because the thumb sits on top of it. ------ */
	--al-switch-track-fill: var(--al-sys-color-surface-sunken);
	--al-switch-track-border-color: var(--al-sys-color-border-control);
	--al-switch-thumb-fill: var(--al-switch-mark);
	/* hover DEEPENS rather than taking the accent line, which is what the checkbox
     does. It has to: once the switch is on, its edge is already the accent line, so
     a hover that pointed there would do nothing on exactly the half of the states
     people hover most. -strong climbs the ladder in the right direction on both
     grounds, darker on light and lighter on dark. */
	--al-switch-hover-border-color: var(--al-sys-color-accent-primary-strong);

	--al-switch-label-color: var(--al-sys-color-text-primary);
	--al-switch-label-size: var(--al-sys-font-size-300);
	--al-switch-hint-color: var(--al-sys-color-text-muted);
	--al-switch-hint-size: var(--al-sys-font-size-100);
	--al-switch-hint-gap: var(--al-sys-space-25);
	--al-switch-gap: 9px; /* mark to label. Matches the checkbox: this is optical
       separation between two marks and it reads correctly at every tier. */
	--al-switch-line-height: 1.45;

	/* LAYOUT IS A TOKEN, not a property a variant overrides. A settings list wants the
     switch at the far right of the row and a form wants it in front of its label,
     and rule 8 says a variant re-points values rather than restating rules, so the
     direction is a value. */
	--al-switch-direction: row;
	--al-switch-label-grow: 0;

	--al-switch-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-switch-spinner-ring-width: var(--al-sys-spinner-ring-width);
	--al-switch-spinner-duration: var(--al-sys-motion-duration-loop);
	--al-switch-spinner-color: var(--al-switch-mark);

	--al-switch-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-switch-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-switch-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-switch-focus-ring-width: var(--al-sys-focus-ring-width);
	/* no intent axis, so it rings in the shared focus role rather than a colour of its
     own. Same call the input and the affix make. */
	--al-switch-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-switch-motion-duration: var(--al-sys-motion-duration-state);
	--al-switch-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-switch-* below this line ------------------------ */
	box-sizing: border-box; /* not inherited. The system owns its own box model. */
	display: flex;
	flex-direction: var(--al-switch-direction);
	/* FLEX-START, not centre, for the same reason as the checkbox: centring looks
     right on a one-line label and drifts to the middle of a three-line one. */
	align-items: flex-start;
	gap: var(--al-switch-gap);
	cursor: pointer;
	font-size: var(--al-switch-label-size);
	line-height: var(
		--al-switch-line-height
	); /* set, not inherited: a host page with
       1.65 on the body moves the track off the first line of its own label. */
	color: var(--al-switch-label-color);
}

/* THE ROLE IS PART OF THE SELECTOR. A checkbox input without role="switch" is
   announced as "checked / not checked", which is wrong for a thing that has already
   happened, so the recipe refuses to dress one. Leave the role off and you get an
   unstyled native checkbox, which is loud, instant and diagnosable. The button does
   the same with aria-busy: you cannot style the state without also announcing it. */
.al-switch > input[role='switch'] {
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	flex: none; /* a narrow column may not squash the track */
	position: relative;
	width: var(--al-switch-track-width);
	height: var(--al-switch-track-height);
	/* centre the track on the FIRST line of the label, whatever the label does after.
     font-size is inherited deliberately: em resolves against the element's own font
     size and a form control does not inherit font, so without this the sum is
     computed against the user agent's 13.33px and the track sits a pixel high. */
	font-size: inherit;
	margin: calc((var(--al-switch-line-height) * 1em - var(--al-switch-track-height)) / 2) 0 0;
	border: var(--al-switch-track-border-width) solid var(--al-switch-track-border-color);
	border-radius: var(--al-switch-track-radius);
	background-color: var(--al-switch-track-fill);
	cursor: inherit;
	transition:
		background-color var(--al-switch-motion-duration) var(--al-switch-motion-easing),
		border-color var(--al-switch-motion-duration) var(--al-switch-motion-easing);
}

/* the target, grown vertically only. The track is already 34px wide, so the pointer
   only ever misses it above and below */
.al-switch > input[role='switch']::before {
	content: '';
	position: absolute;
	inset: calc((var(--al-switch-track-height) - var(--al-switch-hit)) / 2) 0;
}

.al-switch > input[role='switch']::after {
	content: '';
	position: absolute;
	/* explicitly, because box-sizing is not inherited and the usual reset does not
     match pseudo-elements. The pending state puts a border on this box; without
     border-box the thumb would grow by 4px the moment it started spinning. */
	box-sizing: border-box;
	top: 50%;
	/* positioned against the padding box, which starts one border in, so the border
     width comes back out of the inset to leave the thumb 3px from the outer edge */
	left: calc(var(--al-switch-thumb-inset) - var(--al-switch-track-border-width));
	width: var(--al-switch-thumb-size);
	height: var(--al-switch-thumb-size);
	margin-top: calc(var(--al-switch-thumb-size) / -2);
	border-radius: 50%;
	background-color: var(--al-switch-thumb-fill);
	/* the independent `translate` property, not a transform, so the pending spinner's
     rotation can compose with the slide instead of replacing it */
	transition: translate var(--al-switch-motion-duration) var(--al-switch-motion-easing);
}

/* -- STATES. Every one of them re-points tokens on the LABEL, never on the input.
      That is not a style preference: --al-switch-thumb-fill: var(--al-switch-mark)
      is declared on the label, and a var() inside a custom property resolves against
      the element it is DECLARED on. Re-pointing --al-switch-mark on the input would
      change nothing at all, silently. Same trap the button's header describes. --- */

.al-switch:has(> input[role='switch']:checked) {
	--al-switch-track-fill: var(--al-sys-color-accent-primary);
	/* the edge takes the accent as a LINE while the fill stays the accent as a FILL.
     On a light ground they are the same value and the seam is invisible; on a dark
     panel the edge lightens and the pill stays findable against a ground the fill
     alone separates from by only 3.24, the same number a filled button accepts. */
	--al-switch-track-border-color: var(--al-sys-color-accent-primary-line);
	--al-switch-mark: var(--al-sys-color-text-on-accent);
}
.al-switch > input[role='switch']:checked::after {
	translate: var(--al-switch-travel) 0;
}

.al-switch:hover:has(> input[role='switch']:not(:disabled)) {
	--al-switch-track-border-color: var(--al-switch-hover-border-color);
}

.al-switch > input[role='switch']:focus-visible {
	/* forced-colors throws box-shadow away and repaints a transparent outline in a
     system colour, so the ring survives a mode this design cannot otherwise reach */
	outline: var(--al-switch-focus-ring-width) solid transparent;
	outline-offset: var(--al-switch-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-switch-focus-hairline-width) var(--al-switch-focus-hairline-light),
		0 0 0 calc(var(--al-switch-focus-hairline-width) + var(--al-switch-focus-ring-width)) var(--al-switch-focus-ring-color),
		0 0 0 calc(var(--al-switch-focus-hairline-width) * 2 + var(--al-switch-focus-ring-width)) var(--al-switch-focus-hairline-dark);
}

.al-switch:has(> input[role='switch']:disabled) {
	opacity: var(--al-switch-disabled-opacity);
	cursor: not-allowed;
}

/* PENDING. The thumb hollows out and spins where it stands, so the switch keeps
   saying which way it is pointing while it waits. It re-points the fill to
   transparent rather than painting over it, which is why the spinner still finds its
   colour: the ring reads --al-switch-mark through the alias, so it is white on a
   switch that is turning on and slate on one that is turning off.

   THE CONTRACT, and it is the button's: aria-busy AND aria-disabled on the input,
   and the handler returns early while pending. Real `disabled` is the wrong tool:
   it drops the control out of the tab order mid-action and throws the keyboard
   user's place away. pointer-events sits on the LABEL, not the input, because a
   label forwards its clicks and an inert input inside a live label is still live. */
.al-switch:has(> input[role='switch'][aria-busy='true']) {
	--al-switch-thumb-fill: transparent;
	cursor: progress;
	pointer-events: none;
}
.al-switch > input[role='switch'][aria-busy='true']::after {
	border: var(--al-switch-spinner-ring-width) solid var(--al-switch-spinner-color);
	border-right-color: transparent;
	/* al-spin is declared in recipe-tokens.css for the button. One rotation, one
     keyframe set: a second one would be a second thing to keep in step. */
	animation: al-spin var(--al-switch-spinner-duration) linear infinite;
}
/* pending already says "not now". Dimming it as well just makes the spinner hard to
   see. The same call the button makes for busy plus disabled. */
.al-switch:has(> input[role='switch'][aria-busy='true'][aria-disabled='true']) {
	opacity: 1;
}

/* WRONG. A switch is rarely invalid on its own, but a set of them under one field
   can be ("choose at least one terminal"), and the edge is where every other control
   in the recipe says so.

   THE :has() IS HERE FOR SPECIFICITY, NOT FOR MATCHING, and it is load-bearing. As the
   bare attribute this rule scored (0,2,0) and lost to the :checked rule above it at
   (0,3,1), so a switch that was both wrong and ON painted an emerald edge with a red
   focus ring round it — the error disappeared on exactly half the states, which is the
   half a required set is most often in. Rendered and read back, not assumed. Ties with
   :checked and comes after it, so it wins; still loses to :hover, exactly as the
   checkbox's invalid rule does. */
.al-switch[data-invalid='true']:has(> input[role='switch']) {
	--al-switch-track-border-color: var(--al-sys-color-status-error);
	--al-switch-focus-ring-color: var(--al-sys-color-status-error);
}

/* -- the label, and the second line under it ------------------------------- */
.al-switch > span {
	flex: var(--al-switch-label-grow) 1 auto;
	min-width: 0; /* what lets a flex item shrink below its content width */
}
/* A DESCRIPTION LINE, because a switch usually needs one. "Auto-print labels" says
   what it is called; the line under it says what happens, and a setting that fires
   the moment it moves is exactly where that sentence is worth the room. */
.al-switch > span > small {
	display: block;
	margin-top: var(--al-switch-hint-gap);
	font-size: var(--al-switch-hint-size);
	line-height: var(--al-switch-line-height);
	color: var(--al-switch-hint-color);
}

/* -- SIZE. The variants come after the base block, because they tie with it on
      specificity and order is load-bearing. Three re-points, and the thumb and its
      travel follow from them. -------------------------------------------------- */
.al-switch-sm {
	--al-switch-track-height: 16px;
	--al-switch-track-width: 28px;
	--al-switch-thumb-inset: 2px;
	--al-switch-label-size: var(--al-sys-font-size-200);
}
.al-switch-lg {
	--al-switch-track-height: 24px;
	--al-switch-track-width: 42px;
	--al-switch-label-size: var(--al-sys-font-size-400);
}

/* -- SIDE. The switch at the end of the row, label filling the space in front of it:
      a settings list. The DOM order does not change, so the label stays tied to its
      control and the tab order stays the reading order. ------------------------ */
.al-switch-end {
	--al-switch-direction: row-reverse;
	--al-switch-label-grow: 1;
}

/* Forced colours drop every background, which would leave the track outlined and the
   thumb invisible, and the thumb's POSITION is the whole signal. System colour
   keywords are honoured in that mode, so the thumb is painted with one. On and off
   then read from position alone, which is what they read from anyway. */
@media (forced-colors: active) {
	.al-switch > input[role='switch']::after {
		background-color: CanvasText;
	}
	/* a symmetric spinning ring shows no motion once forced colours close its gap */
	.al-switch > input[role='switch'][aria-busy='true']::after {
		background-color: transparent;
		border-right-color: Canvas;
	}
}

/* =============================================================================
   THE SLIDER

   A value in a range, WHERE THE RANGE IS THE POINT. Potency between 0 and 35%, a
   discount between nothing and half. If someone needs to type 12.4 exactly, this is
   the wrong control and a number field is the right one. A slider is for aiming,
   not for entering.

   So it always ships with two things a bare range input does not have: a READOUT,
   because aiming without seeing the number is guessing, and END LABELS, because the
   range is the thing that makes the position mean anything.

   THE RAIL IS A CHANNEL, not a bar. A solid neutral rail cannot work: slate-500 and
   emerald-500 are the same brightness within 7%, so a solid mid-grey rail with an
   emerald fill is two colours a colour-blind eye reads as one. Measured, not
   assumed. A recessed channel with a control-weight edge separates from the page at
   the edge and from the fill in the middle, and it matches the switch's off track,
   which is the same idea doing the same job.
   ============================================================================= */
.al-slider {
	/* -- scale. The BAND is a control tier, so a slider lines up with the field next
        to it and gives a real touch target; the visible rail is a thin thing
        centred inside it. The rail alone would be a 7px hit target. --------- */
	--al-slider-band-height: var(--al-sys-control-height-md);
	--al-slider-track-height: 7px;
	--al-slider-thumb-size: 18px;
	--al-slider-thumb-border-width: 2px;
	--al-slider-track-border-width: var(--al-sys-control-border-width);
	--al-slider-radius: var(--al-sys-radius-pill);

	/* HOW FULL IT IS, as a percentage. There is no cross-engine pseudo-element for the
     filled part of a range input. Firefox has ::-moz-range-progress and Chromium
     has nothing, so the fill is a background the author sizes. One line in the
     input handler, and it is in the docs. Defaults to empty so a slider that is
     never wired renders correctly rather than breaking. */
	--al-slider-percent: 0%;

	--al-slider-track-fill: var(--al-sys-color-surface-sunken);
	--al-slider-track-border-color: var(--al-sys-color-border-control);
	/* the filled part is the accent as a LINE. It carries nothing on top of it, it is
     a mark laid on the rail, and on a dark panel a dark green fill inside a dark
     channel is unreadable, so it lightens with the ground, which is exactly what
     the line rung is for. */
	--al-slider-fill: var(--al-sys-color-accent-primary-line);

	/* the thumb is a raised disc, so it reads the raised ground: white on a light
     page, slate on a dark one. Its accent RING is what makes it findable. On a
     light page the white body vanishes against the empty channel and the ring
     carries it; on a dark one the body vanishes against the emerald fill and the
     ring carries it again. The ring is load-bearing on both grounds, not decoration. */
	--al-slider-thumb-fill: var(--al-sys-color-surface-raised);
	--al-slider-thumb-border-color: var(--al-sys-color-accent-primary-line);
	--al-slider-thumb-hover-border-color: var(--al-sys-color-accent-primary-strong);
	--al-slider-thumb-active-border-color: var(--al-sys-color-accent-primary-stronger);
	/* the STICKY rung, not the raised one. The thumb rides on the rail; it is barely
     off the page and the raised shadow would make a 18px disc look like a menu. */
	--al-slider-thumb-shadow: var(--al-sys-elevation-sticky);

	--al-slider-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-slider-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-slider-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-slider-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-slider-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-slider-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-slider-motion-duration: var(--al-sys-motion-duration-state);
	--al-slider-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-slider-* below this line ------------------------ */
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	display: block;
	width: 100%;
	height: var(--al-slider-band-height);
	/* THE PADDING IS WHAT CENTRES THE RAIL, and it is deliberate rather than trusting
     the engine. The rail is laid out inside the input's CONTENT box, and how a
     browser places a short track inside a tall range input is not something any spec
     pins down. Padding makes the content box exactly rail-height, so there is
     nowhere else for it to go. */
	padding: calc((var(--al-slider-band-height) - var(--al-slider-track-height)) / 2) 0;
	margin: 0;
	background: transparent;
	cursor: pointer;
}

/* THE VENDOR PSEUDO-ELEMENTS CANNOT SHARE A RULE. One unknown selector invalidates
   the whole selector list in every engine, so ::-webkit-* and ::-moz-* are written
   twice on purpose. This is the one place in the file where a value is repeated, and
   it is repeated as a token reference so the two copies cannot hold different
   values, only a different selector. */
.al-slider::-webkit-slider-runnable-track {
	box-sizing: border-box;
	height: var(--al-slider-track-height);
	border: var(--al-slider-track-border-width) solid var(--al-slider-track-border-color);
	border-radius: var(--al-slider-radius);
	background-color: var(--al-slider-track-fill);
	background-image: linear-gradient(to right, var(--al-slider-fill), var(--al-slider-fill));
	background-size: var(--al-slider-percent) 100%;
	background-repeat: no-repeat;
}
.al-slider::-moz-range-track {
	box-sizing: border-box;
	height: var(--al-slider-track-height);
	border: var(--al-slider-track-border-width) solid var(--al-slider-track-border-color);
	border-radius: var(--al-slider-radius);
	background-color: var(--al-slider-track-fill);
	background-image: linear-gradient(to right, var(--al-slider-fill), var(--al-slider-fill));
	background-size: var(--al-slider-percent) 100%;
	background-repeat: no-repeat;
}

.al-slider::-webkit-slider-thumb {
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	width: var(--al-slider-thumb-size);
	height: var(--al-slider-thumb-size);
	/* WebKit hangs the thumb off the top of the rail, so it is pulled back by half the
     difference. Firefox centres it for us, which is why the -moz rule below has no
     margin. The same value in both places would be wrong in one of them. */
	margin-top: calc((var(--al-slider-track-height) - var(--al-slider-thumb-size)) / 2);
	border: var(--al-slider-thumb-border-width) solid var(--al-slider-thumb-border-color);
	border-radius: 50%;
	background-color: var(--al-slider-thumb-fill);
	box-shadow: var(--al-slider-thumb-shadow);
	cursor: inherit;
	transition: border-color var(--al-slider-motion-duration) var(--al-slider-motion-easing);
}
.al-slider::-moz-range-thumb {
	box-sizing: border-box;
	width: var(--al-slider-thumb-size);
	height: var(--al-slider-thumb-size);
	border: var(--al-slider-thumb-border-width) solid var(--al-slider-thumb-border-color);
	border-radius: 50%;
	background-color: var(--al-slider-thumb-fill);
	box-shadow: var(--al-slider-thumb-shadow);
	cursor: inherit;
	transition: border-color var(--al-slider-motion-duration) var(--al-slider-motion-easing);
}

/* -- states. The thumb's ring climbs the emphasis ladder under the pointer, which
      moves darker on a light ground and lighter on a dark one without either being
      written here. Same ladder the outline button walks. --------------------- */
.al-slider:hover:not(:disabled) {
	--al-slider-thumb-border-color: var(--al-slider-thumb-hover-border-color);
}
.al-slider:active:not(:disabled) {
	--al-slider-thumb-border-color: var(--al-slider-thumb-active-border-color);
}

/* THE RING GOES ROUND THE THUMB, not round the whole band. The thumb is the thing
   the arrow keys move, and a ring round a 300px bar tells a keyboard user nothing
   about where they are in it. The band is sized to hold the ring: 18px thumb plus
   4px of ring on each side inside a 36px band. */
.al-slider:focus-visible {
	outline: var(--al-slider-focus-ring-width) solid transparent;
	outline-offset: var(--al-slider-focus-hairline-width);
}
.al-slider:focus-visible::-webkit-slider-thumb {
	box-shadow:
		0 0 0 var(--al-slider-focus-hairline-width) var(--al-slider-focus-hairline-light),
		0 0 0 calc(var(--al-slider-focus-hairline-width) + var(--al-slider-focus-ring-width)) var(--al-slider-focus-ring-color),
		0 0 0 calc(var(--al-slider-focus-hairline-width) * 2 + var(--al-slider-focus-ring-width)) var(--al-slider-focus-hairline-dark),
		var(--al-slider-thumb-shadow);
}
.al-slider:focus-visible::-moz-range-thumb {
	box-shadow:
		0 0 0 var(--al-slider-focus-hairline-width) var(--al-slider-focus-hairline-light),
		0 0 0 calc(var(--al-slider-focus-hairline-width) + var(--al-slider-focus-ring-width)) var(--al-slider-focus-ring-color),
		0 0 0 calc(var(--al-slider-focus-hairline-width) * 2 + var(--al-slider-focus-ring-width)) var(--al-slider-focus-hairline-dark),
		var(--al-slider-thumb-shadow);
}

.al-slider:disabled {
	opacity: var(--al-slider-disabled-opacity);
	cursor: not-allowed;
	pointer-events: none;
}

.al-slider-sm {
	--al-slider-band-height: var(--al-sys-control-height-sm);
	--al-slider-track-height: 5px;
	--al-slider-thumb-size: 14px;
}
.al-slider-lg {
	--al-slider-band-height: var(--al-sys-control-height-lg);
	--al-slider-track-height: 9px;
	--al-slider-thumb-size: 22px;
}

/* -- the row, the readout and the ends.

      Each of these declares its own tokens, because a token is only visible to the
      element it is declared on and that element's descendants. A part that reads a
      token declared on its SIBLING resolves to nothing at all, and it does it
      silently. The property just drops out. ---------------------------------- */
.al-slider-row {
	/* two gaps coexist here, so by rule 4 neither gets to be the unnamed one */
	--al-slider-row-gap-x: var(--al-sys-space-200);
	--al-slider-row-gap-y: var(--al-sys-space-50);

	box-sizing: border-box;
	display: grid;
	grid-template-columns: 1fr auto;
	align-items: center;
	column-gap: var(--al-slider-row-gap-x);
	row-gap: var(--al-slider-row-gap-y);
}
/* THE PLACES ARE NAMED, not left to auto flow, and it is worth the three lines.
   Written as a flex row with the ends outside it, the end labels spanned the readout
   as well and "35%" sat under the number instead of under the end of the rail it
   describes. Rendered, seen, fixed. Auto flow inside the grid has the same fault by a
   different route: with no readout in the markup the ends drop into the second
   column and sit BESIDE the rail. Naming the cells is what makes the layout true for
   every combination of the parts rather than only for the one in the example. */
.al-slider-row > .al-slider {
	grid-area: 1 / 1;
}
.al-slider-row > .al-slider-value {
	grid-area: 1 / 2;
}
.al-slider-row > .al-slider-ends {
	grid-area: 2 / 1;
}

/* THE READOUT IS FIXED WIDTH AND TABULAR. Not for tidiness: a proportional readout
   changes width as the digits change, which moves the slider next to it while
   someone is dragging, so the value runs away from the thumb. A fixed box and
   tabular figures hold both still. */
.al-slider-value {
	--al-slider-value-color: var(--al-sys-color-text-primary);
	--al-slider-value-size: var(--al-sys-font-size-300);
	--al-slider-value-weight: var(--al-sys-font-weight-control);
	--al-slider-value-family: var(--al-sys-font-family-numeric);
	--al-slider-value-line-height: var(--al-sys-line-height-control);
	--al-slider-value-width: 6ch;

	box-sizing: border-box;
	flex: none;
	min-width: var(--al-slider-value-width);
	text-align: right;
	font-family: var(--al-slider-value-family);
	font-size: var(--al-slider-value-size);
	font-weight: var(--al-slider-value-weight);
	line-height: var(--al-slider-value-line-height);
	font-variant-numeric: tabular-nums;
	color: var(--al-slider-value-color);
}

/* THE ENDS ARE THE RANGE, and they are the reason this control is not a number
   field. Without them a thumb two thirds along means nothing. */
.al-slider-ends {
	--al-slider-ends-color: var(--al-sys-color-text-muted);
	--al-slider-ends-size: var(--al-sys-font-size-100);
	--al-slider-ends-gap: var(--al-sys-space-100);

	box-sizing: border-box;
	display: flex;
	justify-content: space-between;
	gap: var(--al-slider-ends-gap);
	font-size: var(--al-slider-ends-size);
	line-height: 1.45;
	color: var(--al-slider-ends-color);
}

/* =============================================================================
   THE FILE UPLOAD

   The only control in the recipe with a LIFECYCLE rather than a set of states.
   Choose, drop, send, attached, wrong. Four of those five can be true of
   different files at the same moment, which is why the zone and the file are two
   separate things and not one control with more states.

   THE ZONE is a container, so it takes the container radius and pads from the
   spacing scale. It has no size tier: a drop target is not a control that lines up
   with a field, it is a region, and giving it 28 / 36 / 44 would be pretending.

   THE FILE is a row, so it takes the ROW tier, the same 44px a table row uses, so a
   list of attachments and a table of them are the same height.
   ============================================================================= */
.al-upload {
	--al-upload-fill: var(--al-sys-color-surface-sunken);
	--al-upload-border-color: var(--al-sys-color-border-control);
	/* 2px, and it is a component decision rather than the control border width. A 1px
     dashed edge reads as a rendering artefact at this size. You see a broken line
     rather than an invitation, and only this control wants it. */
	--al-upload-border-width: 2px;
	--al-upload-border-style: dashed;
	--al-upload-radius: var(--al-sys-radius-container);
	--al-upload-padding-y: var(--al-sys-space-300);
	--al-upload-padding-x: var(--al-sys-space-200);
	--al-upload-gap: var(--al-sys-space-100);
	--al-upload-direction: column;

	/* bigger than the lg icon rung, and deliberately off the scale. That scale is sized
     to sit inside a control tier next to a label; this glyph is the only thing in an
     empty region and at 20px it read as a stray mark rather than as a target. The
     compact variant, which IS a row, drops back onto the rung. */
	--al-upload-icon-size: 26px;
	--al-upload-icon-color: var(--al-sys-color-text-muted);
	/* the mask technique from the checkbox: the SHAPE comes from the SVG and the
     COLOUR comes from a token, so a theme can move it and a state can swap it. The
     select's caret bakes its colour into its image and cannot do either. */
	--al-upload-icon-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M3.5 15v3.5A2.5 2.5 0 0 0 6 21h12a2.5 2.5 0 0 0 2.5-2.5V15' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

	--al-upload-lead-color: var(--al-sys-color-text-secondary);
	--al-upload-lead-size: var(--al-sys-font-size-300);
	/* the word you click. Ink laid on the ground, so it is the accent as a LINE. */
	--al-upload-action-color: var(--al-sys-color-accent-primary-line);
	--al-upload-action-weight: var(--al-sys-font-weight-control);
	--al-upload-hint-color: var(--al-sys-color-text-muted);
	--al-upload-hint-size: var(--al-sys-font-size-100);
	--al-upload-hint-gap: var(--al-sys-space-25);
	--al-upload-line-height: 1.45;

	--al-upload-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-upload-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-upload-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-upload-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-upload-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-upload-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-upload-motion-duration: var(--al-sys-motion-duration-state);
	--al-upload-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-upload-* below this line ------------------------ */
	box-sizing: border-box;
	position: relative; /* the input is parked inside it */
	display: flex;
	flex-direction: var(--al-upload-direction);
	align-items: center;
	justify-content: center;
	gap: var(--al-upload-gap);
	padding: var(--al-upload-padding-y) var(--al-upload-padding-x);
	border: var(--al-upload-border-width) var(--al-upload-border-style) var(--al-upload-border-color);
	border-radius: var(--al-upload-radius);
	background-color: var(--al-upload-fill);
	text-align: center;
	line-height: var(--al-upload-line-height);
	cursor: pointer;
	transition:
		background-color var(--al-upload-motion-duration) var(--al-upload-motion-easing),
		border-color var(--al-upload-motion-duration) var(--al-upload-motion-easing);
}

/* CLIPPED, NOT HIDDEN. display:none and visibility:hidden both take the input out of
   the tab order, which would leave the whole control unreachable from a keyboard.
   Clipped to a pixel it is still focusable, still activated by Enter, and the ring
   below paints on the zone around it. */
.al-upload > input[type='file'] {
	box-sizing: border-box; /* not inherited, and this box carries a size */
	position: absolute;
	width: 1px;
	height: 1px;
	margin: 0;
	padding: 0;
	border: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.al-upload-icon {
	box-sizing: border-box;
	flex: none;
	width: var(--al-upload-icon-size);
	height: var(--al-upload-icon-size);
	background-color: var(--al-upload-icon-color);
	-webkit-mask: var(--al-upload-icon-image) center / contain no-repeat;
	mask: var(--al-upload-icon-image) center / contain no-repeat;
}

.al-upload-lead {
	font-size: var(--al-upload-lead-size);
	color: var(--al-upload-lead-color);
}
.al-upload-lead b {
	color: var(--al-upload-action-color);
	font-weight: var(--al-upload-action-weight);
}
.al-upload > small {
	display: block;
	margin-top: var(--al-upload-hint-gap);
	font-size: var(--al-upload-hint-size);
	line-height: var(--al-upload-line-height);
	color: var(--al-upload-hint-color);
}

/* -- states ---------------------------------------------------------------- */

/* :has(:focus-visible), not :focus-within. A file input takes focus on a mouse click
   as well as a tab, so :focus-within would ring the zone every time someone opened
   the picker. */
.al-upload:has(> input[type='file']:focus-visible) {
	outline: var(--al-upload-focus-ring-width) solid transparent;
	outline-offset: var(--al-upload-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-upload-focus-hairline-width) var(--al-upload-focus-hairline-light),
		0 0 0 calc(var(--al-upload-focus-hairline-width) + var(--al-upload-focus-ring-width)) var(--al-upload-focus-ring-color),
		0 0 0 calc(var(--al-upload-focus-hairline-width) * 2 + var(--al-upload-focus-ring-width)) var(--al-upload-focus-hairline-dark);
}

/* DRAGGING OVER IT. CSS has no selector for a drag hovering a target, so this is a
   data attribute the page sets on dragenter and clears on dragleave and drop. Four
   lines, and they are in the docs. The dashes close up into a solid edge, which is
   the whole message: the target is armed. The tint is the shared row-selected
   tint, the same value a chosen row in a list uses, because it means the same thing. */
.al-upload[data-dragover='true'] {
	--al-upload-border-color: var(--al-sys-color-accent-primary-line);
	--al-upload-border-style: solid;
	--al-upload-fill: var(--al-sys-color-row-tint-selected);
	--al-upload-icon-color: var(--al-sys-color-accent-primary-line);
	--al-upload-lead-color: var(--al-sys-color-text-primary);
}

/* the zone's own error: nothing chosen when something was required. Edge only, no
   tint. Errors arrive in groups and the tinted version does not survive six of them
   on one form. Same call the text input makes. */
.al-upload[aria-invalid='true'] {
	--al-upload-border-color: var(--al-sys-color-status-error);
	--al-upload-focus-ring-color: var(--al-sys-color-status-error);
}

.al-upload:has(> input[type='file']:disabled) {
	opacity: var(--al-upload-disabled-opacity);
	cursor: not-allowed;
}

/* COMPACT. A full drop zone is wrong inside a dense form row, and a form that needs
   one otherwise grows its own width override and drifts off the system. Tokens only:
   the direction, the padding and the icon, and every state above still applies. */
.al-upload-compact {
	--al-upload-direction: row;
	--al-upload-padding-y: var(--al-sys-space-150);
	--al-upload-padding-x: var(--al-sys-space-200);
	--al-upload-gap: var(--al-sys-space-150);
	--al-upload-icon-size: var(--al-sys-icon-size-lg);
	--al-upload-radius: var(--al-sys-control-radius);
}

/* -- THE FILE, once it is attached ----------------------------------------- */
.al-upload-list {
	--al-upload-list-gap: var(--al-sys-space-100);

	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	gap: var(--al-upload-list-gap);
}

.al-upload-file {
	--al-upload-file-height: var(--al-sys-row-height-md);
	--al-upload-file-fill: var(--al-sys-color-surface-sunken);
	--al-upload-file-border-color: var(--al-sys-color-border);
	--al-upload-file-border-width: var(--al-sys-control-border-width);
	/* the control radius, not the container one: this row sits INSIDE the zone's
     container radius, and a container is rounder than the things in it. */
	--al-upload-file-radius: var(--al-sys-control-radius);
	--al-upload-file-padding-x: var(--al-sys-space-150);
	--al-upload-file-padding-y: var(--al-sys-space-100);
	--al-upload-file-gap: var(--al-sys-space-150);

	--al-upload-file-icon-size: var(--al-sys-icon-size-lg);
	--al-upload-file-icon-color: var(--al-sys-color-text-muted);
	--al-upload-file-icon-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M13.5 3H7.5A1.5 1.5 0 0 0 6 4.5v15A1.5 1.5 0 0 0 7.5 21h9a1.5 1.5 0 0 0 1.5-1.5V7.5zm0 0 4.5 4.5h-4.5z' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

	--al-upload-file-name-color: var(--al-sys-color-text-primary);
	--al-upload-file-name-size: var(--al-sys-font-size-300);
	--al-upload-file-name-weight: var(--al-sys-font-weight-medium);
	--al-upload-file-meta-color: var(--al-sys-color-text-muted);
	--al-upload-file-meta-size: var(--al-sys-font-size-100);
	--al-upload-file-line-height: 1.45;

	/* thinner than the slider's rail, because nobody aims at this one — it is read, not
     dragged, so it takes the smallest height that still reads as a bar. */
	--al-upload-progress-height: 4px;
	--al-upload-progress-radius: var(--al-sys-radius-pill);
	--al-upload-progress-rail: var(--al-sys-color-border);
	--al-upload-progress-fill: var(--al-sys-color-accent-primary-line);
	--al-upload-progress-gap: var(--al-sys-space-50);
	--al-upload-progress-percent: 0%;

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-upload-file-gap);
	/* a floor, not a fixed height. The row grows when a progress bar or a second line
     of message needs the room, and it never squashes below the row tier */
	min-height: var(--al-upload-file-height);
	padding: var(--al-upload-file-padding-y) var(--al-upload-file-padding-x);
	border: var(--al-upload-file-border-width) solid var(--al-upload-file-border-color);
	border-radius: var(--al-upload-file-radius);
	background-color: var(--al-upload-file-fill);
	line-height: var(--al-upload-file-line-height);
}

.al-upload-file-icon {
	box-sizing: border-box;
	flex: none;
	width: var(--al-upload-file-icon-size);
	height: var(--al-upload-file-icon-size);
	background-color: var(--al-upload-file-icon-color);
	-webkit-mask: var(--al-upload-file-icon-image) center / contain no-repeat;
	mask: var(--al-upload-file-icon-image) center / contain no-repeat;
}

.al-upload-file-body {
	min-width: 0; /* what lets the name shrink far enough to truncate */
	flex: 1 1 auto;
}

/* the name is wrapped in an element of its own because text-overflow has no effect
   on a bare text node in a flex container. Measured on the button, not assumed. */
.al-upload-file-name {
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-size: var(--al-upload-file-name-size);
	font-weight: var(--al-upload-file-name-weight);
	color: var(--al-upload-file-name-color);
}
.al-upload-file-meta {
	display: block;
	font-size: var(--al-upload-file-meta-size);
	color: var(--al-upload-file-meta-color);
}

/* IN PROGRESS. The same construction as the slider's filled rail, for the same
   reason and with the same one line of script: a background sized by a percentage
   the page sets. A <progress> element would have brought a second set of vendor
   pseudo-elements and a second focus story for no gain. */
.al-upload-progress {
	box-sizing: border-box;
	width: 100%;
	height: var(--al-upload-progress-height);
	margin-top: var(--al-upload-progress-gap);
	border-radius: var(--al-upload-progress-radius);
	background-color: var(--al-upload-progress-rail);
	background-image: linear-gradient(to right, var(--al-upload-progress-fill), var(--al-upload-progress-fill));
	background-size: var(--al-upload-progress-percent) 100%;
	background-repeat: no-repeat;
	overflow: hidden;
}

/* WRONG. Four token re-points and not one property, which is the whole argument for
   rule 8 in one block: the icon changes SHAPE by swapping the mask image, the edge
   and the message change colour, and every other rule about this row is untouched.
   The icon change is what keeps the error readable without colour. A red row and a
   grey row are the same row to a colour-blind eye, a triangle and a document are not. */
.al-upload-file[data-error='true'] {
	--al-upload-file-border-color: var(--al-sys-color-status-error);
	--al-upload-file-icon-color: var(--al-sys-color-status-error);
	--al-upload-file-meta-color: var(--al-sys-color-status-error-text);
	--al-upload-file-icon-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 4.4 2.9 19.9h18.2z' fill='none' stroke='%23000' stroke-width='1.7' stroke-linejoin='round'/%3E%3Cpath d='M12 10.2v3.9' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round'/%3E%3Ccircle cx='12' cy='17' r='1.05' fill='%23000'/%3E%3C/svg%3E");
}

/* =============================================================================
   REDUCED MOTION

   Fires only for someone whose operating system asks for less animation. What goes
   is polish: the thumb slide, the rail and zone colour fades, the thumb's ring fade.
   What stays is the pending spinner, because it is the only thing telling someone
   their switch has not taken effect yet, and a switch that looks settled while the
   server has not answered is the failure this whole state exists to prevent. It
   steps instead of sweeping, exactly as the button's does.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-switch > input[role='switch'],
	.al-switch > input[role='switch']::after,
	.al-slider::-webkit-slider-thumb,
	.al-slider::-moz-range-thumb,
	.al-upload {
		transition: none;
	}
	.al-switch > input[role='switch'][aria-busy='true']::after {
		animation-timing-function: steps(8, end);
	}
}

/* ---- nav ---------------------------------------------------- */
/* =============================================================================
   NAVIGATION — tabs, the segmented control, the breadcrumb, the back control,
   pagination

   Five controls that all answer "where am I and what am I looking at", which is
   why they are written together: the mistakes they make are each other's.

     TABS            swap what is shown in one region. Navigation.
     SEGMENTED       pick one of a few values, all visible. A form control.
     BREADCRUMB      where you are in a hierarchy, and the way back up.
     BACK            the one way out of a record, to the list it belongs to.
     PAGINATION      which page of a long list, how many there are, how big a page is.

   THE LINE BETWEEN TABS AND SEGMENTED, because it is the one everybody blurs.
   Tabs change the CONTENT of a region and the URL usually goes with them; the
   segmented control changes a VALUE and the page around it stays put. So tabs are
   buttons in a tablist wearing an underline, and the segmented control is a real
   radio group wearing chips. Built that way, not just drawn that way — a screen
   reader hears "tab, 2 of 5" for one and "radio button, Adult, 1 of 2" for the
   other, which is the actual difference.

   EVERY RULE HERE READS ONLY ITS OWN --al-<component>-* TOKENS, and every one of
   those points at a sys role. There is not one selector in this file naming a
   control inside .al-on-dark: all five are correct on a dark panel because they
   read the roles the ground re-points, and nothing else.

   Token prefixes are spelled out, class names abbreviate (rule 5):
     --al-tabs-*        .al-tabs   .al-tab   .al-tab-icon   .al-tab-count
     --al-segmented-*   .al-seg    .al-seg-item
     --al-breadcrumb-*  .al-crumbs .al-crumb-fold
     --al-back-*        .al-back   .al-back-icon
     --al-pagination-*  .al-pager  .al-pager-page .al-pager-step .al-pager-gap
                                   .al-pager-count .al-pager-size .al-pager-pages

   .al-seg rather than .al-segmented is not only rule 5. The deprecated
   public/css/bootstrap-style-overrides.css already ships .al-segmented and
   .al-segmented__item with live consumers, and the rollout loads both stylesheets
   at once for a while. Taking that name back would silently repaint screens nobody
   is working on — the exact clash the recipe's namespace note was written about.
   Checked by grep, not assumed: .al-seg, .al-crumbs, .al-pager, .al-tabs and
   .al-back appear nowhere in the product.
   ============================================================================= */

/* =============================================================================
   TABS

   One region, several views, one of them lit. The indicator is an underline
   because it is the only selected-marker that survives a label of any length and
   does not need the tab to change weight or size to say "this one".

   WEIGHT NEVER CHANGES BETWEEN A SELECTED AND AN UNSELECTED TAB. It is the
   obvious way to mark the selected one and it is wrong: a 600 label is wider than
   a 400 label, so selecting a tab re-flows every tab to the right of it and the
   strip visibly twitches under the pointer. Selection is said by ink and by the
   indicator, both of which cost no width.

   THE STRIP SCROLLS. A tab strip is the one control that routinely has more
   content than room, and the answers are: wrap (a second row of tabs with the
   track under only one of them — broken), collapse into a menu (that is a menu,
   and menus are a different family), or scroll. It scrolls, and a tab cut off at
   the edge is the affordance. A colour-fade at the edge was tried and rejected:
   painted in a colour it is wrong on any ground the strip is not sitting on, and
   as an alpha mask that is always on it dims the first label at rest, which is
   worse than no hint at all.

   THE TRACK IS ON THE TABS, NOT ON THE STRIP, and that is a consequence of
   scrolling. A scroll container clips its children at the padding box, focus rings
   included, so a ring drawn around a tab loses its top and bottom strokes. The
   strip therefore reserves the ring's full width as vertical padding — and once it
   has padding, a border-bottom on the strip sits 4px below the tabs with the
   indicator floating in between. So each tab carries the hairline and a flexible
   filler carries it across the rest of the strip.

   THE SAME CLIP HAS A LEFT AND A RIGHT EDGE, which the vertical fix missed. With no
   room reserved at the ends, the first tab sat flush against the padding box and its
   focus ring came out as a U open to the left — rendered, seen, then fixed. Padding
   cannot do this job horizontally: the padding box IS the clip, so padding would move
   the hairline in from both ends and leave the rule short of the strip. A flex spacer
   at each end can, because it carries the hairline across itself. The trailing one is
   the filler, which now refuses to shrink below the same reserve so the last tab is
   safe at the far end of a scroll.
   ============================================================================= */

.al-tabs {
	/* -- scale. A tab is a CONTROL, so it takes the control tiers: a tab strip in a
	      toolbar lines up with the buttons and the search field beside it. */
	--al-tabs-height: var(--al-sys-control-height-md);
	--al-tabs-padding-x: var(--al-sys-control-padding-x-md);
	--al-tabs-font-size: var(--al-sys-font-size-300);

	/* the system names its own face rather than inheriting the host page's, for the
	   same reason it owns its box-sizing and its line-height: a control that changes
	   shape depending on where it is dropped is not a control */
	--al-tabs-font-family: var(--al-sys-font-family);
	--al-tabs-font-weight: var(--al-sys-font-weight-control);
	--al-tabs-line-height: var(--al-sys-line-height-control);

	/* the hairline the whole strip stands on */
	--al-tabs-track-width: var(--al-sys-control-border-width);
	--al-tabs-track-color: var(--al-sys-color-border);

	/* ONE label token and ONE indicator token, re-pointed by each state. Two tokens
	   named -rest and -selected would need the state rules to alias one to the other,
	   which is the same-tier aliasing rule 2 forbids. */
	--al-tabs-label: var(--al-sys-color-text-secondary);
	/* the indicator is INK LAID ON THE GROUND, never a fill, so it reads the accent's
	   line rung — that is what lightens it on a dark panel. Reading the fill rung
	   instead gives a 2px emerald-500 bar on slate-900, which is a bar you have to
	   look for. */
	--al-tabs-indicator: transparent;
	--al-tabs-indicator-thickness: 2px; /* one pixel reads as an artefact next to the
	     1px track it sits on; three starts to look like a heading rule */

	/* THE OPTIONAL LEADING GLYPH, sized by the control so a strip that wants one is
	   not left sizing it itself. It gets no colour token: the glyph inherits the tab's
	   own ink, which four state rules already re-point, and a second token would have
	   to be re-pointed in every one of them to stay in step with the label beside it. */
	--al-tabs-icon-size: var(--al-sys-icon-size-md);

	/* the count that rides in a tab: "Manifests 3" */
	--al-tabs-count-fill: var(--al-sys-color-surface-sunken);
	--al-tabs-count-label: var(--al-sys-color-text-muted);
	--al-tabs-count-font-size: var(--al-sys-font-size-100);
	--al-tabs-count-radius: var(--al-sys-radius-pill);
	--al-tabs-count-padding-x: var(--al-sys-space-100);
	/* medium, not the control weight the tab label uses: a number set at 600 beside a
	   600 label competes with it for the same job. */
	--al-tabs-count-font-weight: var(--al-sys-font-weight-medium);

	--al-tabs-gap: var(
		--al-sys-control-gap
	); /* glyph to label and label to count, the
	     same optical separation a button uses between its glyph and its label */
	--al-tabs-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-tabs-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-tabs-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-tabs-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-tabs-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-tabs-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-tabs-motion-duration: var(--al-sys-motion-duration-state);
	--al-tabs-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-tabs-* below this line ------------------------- */
	box-sizing: border-box; /* not inherited. A strip naming 36px rendered 48 on a
	     page with no reset, and the tier stopped being a tier. */
	display: flex;
	align-items: stretch;
	/* the room the focus ring needs. Without it the ring is sliced off top and bottom
	   by the scroll container, and a keyboard user gets two vertical strokes. */
	padding-block: calc(var(--al-tabs-focus-hairline-width) * 2 + var(--al-tabs-focus-ring-width));
	overflow-x: auto;
	/* the scrollbar goes. A 15px bar under a 36px strip makes the control 51px on
	   Windows and 36px on a Mac, which is the same failure as inheriting a line
	   height. Keyboard users still reach every tab — focus scrolls it into view. */
	scrollbar-width: none;
	scroll-padding-inline: var(--al-tabs-padding-x);
}
.al-tabs::-webkit-scrollbar {
	display: none;
}
/* THE RING'S ROOM AT THE LEADING END, carrying the hairline across itself so the
   track still starts at the strip's own edge. Exactly the ring's outer width, which
   is what the strip already reserves top and bottom. */
.al-tabs::before {
	content: '';
	box-sizing: border-box; /* not inherited, and no reset reaches a pseudo-element */
	flex: none;
	width: calc(var(--al-tabs-focus-hairline-width) * 2 + var(--al-tabs-focus-ring-width));
	border-bottom: var(--al-tabs-track-width) solid var(--al-tabs-track-color);
}
/* THE REST OF THE HAIRLINE, and the ring's room at the trailing end. Grow 1 means it
   takes whatever room the tabs leave, so the track runs the full width of the strip.
   Shrink 0 on a basis of the ring's width is the other half: it used to shrink to
   nothing when the tabs overflowed, which put the last tab flush against the clip and
   cost its ring the right-hand stroke at the far end of a scroll. */
.al-tabs::after {
	content: '';
	box-sizing: border-box;
	flex: 1 0 calc(var(--al-tabs-focus-hairline-width) * 2 + var(--al-tabs-focus-ring-width));
	border-bottom: var(--al-tabs-track-width) solid var(--al-tabs-track-color);
}

.al-tab {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--al-tabs-gap);
	position: relative;
	flex: none; /* a tab never shrinks: the strip scrolls instead. Shrinking is how
	     five tabs become five ellipses and nobody can read any of them. */
	min-height: var(--al-tabs-height);
	padding: 0 var(--al-tabs-padding-x);
	border: 0;
	border-bottom: var(--al-tabs-track-width) solid var(--al-tabs-track-color);
	background: transparent;
	color: var(--al-tabs-label);
	font-family: var(--al-tabs-font-family);
	font-size: var(--al-tabs-font-size);
	font-weight: var(--al-tabs-font-weight);
	line-height: var(--al-tabs-line-height); /* set, not inherited: the host page's
	     1.65 put a 23px line box inside the 28px small tier */
	white-space: nowrap;
	cursor: pointer;
	transition: color var(--al-tabs-motion-duration) var(--al-tabs-motion-easing);
}

/* the label wants an element of its own, because text-overflow does nothing to a
   bare text node in a flex container. Same reason the button asks for one. */
.al-tab > * {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* THE INDICATOR sits over the track rather than above it — bottom: -1px puts its
   lower edge on the tab's border box, so the 2px bar covers the 1px hairline
   instead of stacking on top of it and making the selected tab 1px taller. */
.al-tab::after {
	content: '';
	position: absolute;
	left: 0;
	right: 0;
	bottom: calc(-1 * var(--al-tabs-track-width));
	height: var(--al-tabs-indicator-thickness);
	background: var(--al-tabs-indicator);
	transition: background-color var(--al-tabs-motion-duration) var(--al-tabs-motion-easing);
}

/* STATES re-point tokens, never properties — rule 8. Order is load-bearing: every
   one of these is (0,2,0), so hover comes first, selected outranks it by sitting
   later, and press outranks both. */
.al-tab:hover {
	--al-tabs-label: var(--al-sys-color-text-primary);
	/* a quiet indicator in the CONTROL-EDGE rung, not the accent: it shows where the
	   mark will land without pretending the tab is already selected */
	--al-tabs-indicator: var(--al-sys-color-border-control);
}
.al-tab[aria-selected='true'] {
	--al-tabs-label: var(--al-sys-color-text-primary);
	--al-tabs-indicator: var(--al-sys-color-accent-primary-line);
}
/* press answers immediately, before the app has re-rendered anything. A scale like
   the button's is wrong here — a tab is fixed in a strip and shrinking one leaves a
   gap in the row. */
.al-tab:active {
	--al-tabs-label: var(--al-sys-color-text-primary);
	--al-tabs-indicator: var(--al-sys-color-accent-primary-line);
}

.al-tab:focus {
	outline: none;
}
.al-tab:focus-visible {
	/* the three-layer ring, exactly as the button draws it. The transparent outline
	   is invisible here and repainted by Windows high-contrast, where box-shadow is
	   thrown away and a keyboard user would otherwise have no indicator at all. */
	outline: var(--al-tabs-focus-ring-width) solid transparent;
	outline-offset: var(--al-tabs-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-tabs-focus-hairline-width) var(--al-tabs-focus-hairline-light),
		0 0 0 calc(var(--al-tabs-focus-hairline-width) + var(--al-tabs-focus-ring-width)) var(--al-tabs-focus-ring-color),
		0 0 0 calc(var(--al-tabs-focus-hairline-width) * 2 + var(--al-tabs-focus-ring-width)) var(--al-tabs-focus-hairline-dark);
}

/* DISABLED. aria-disabled is the one to reach for: a tab that drops out of the tab
   order takes the keyboard user's place with it, and a tab they cannot discover is
   a view they do not know exists. Both hooks are styled, because someone will
   reach for the native attribute and it must not look different. */
.al-tab:disabled,
.al-tab[aria-disabled='true'] {
	pointer-events: none;
	cursor: not-allowed;
}
/* THE CONTENTS DIM, NOT THE TAB. Half-strength on the tab takes its share of the
   track's hairline down with it, and the strip's rule comes out with a pale notch in
   it exactly as wide as the disabled label. Rendered, seen, then fixed — the numbers
   were right the whole time. Dimming the children is the same blanket the rest of the
   system uses, applied to the part that is actually unavailable. */
.al-tab:disabled > *,
.al-tab[aria-disabled='true'] > * {
	opacity: var(--al-tabs-disabled-opacity);
}

/* THE ICON, AND IT IS OPTIONAL. Optional has to mean a tab without one is untouched,
   not merely unharmed: this rule reaches nothing that does not carry the class, and a
   custom property nobody reads costs no pixels. A no-icon tab is the same height, the
   same padding and the same label position it was before the slot existed.

   flex:none for the same reason the count has it: the truncation rule above lets every
   child shrink, and the label is the part that gives way.

   The glyph rung is under the tab's own height at every tier, so a tab that takes an
   icon is exactly as tall as a tab that does not — the slot changes what a tab can
   hold, never what a strip measures. */
.al-tab-icon {
	/* not inherited, and no reset reaches a host page's SVG: a glyph carrying padding
	   or a border of its own renders wider than the icon rung says it is */
	box-sizing: border-box;
	flex: none;
	width: var(--al-tabs-icon-size);
	height: var(--al-tabs-icon-size);
	/* A BOX IS NOT A GLYPH. The width and height above size an SVG, and an icon FONT
	   ignores both: it draws at whatever font-size it inherits and spills out of the
	   box. The first strip to use this slot drew four material symbols at 24px inside a
	   16px box and every one of them came out clipped. Setting the type too costs one
	   line and means the slot does not care which kind of icon it is handed, which is
	   better than the recipe having an opinion about that. */
	font-size: var(--al-tabs-icon-size);
	line-height: 1;
}

/* THE COUNT. flex:none after the truncation rule above, so the label gives way and
   the number never does — a count with an ellipsis in it is worse than no count. */
.al-tab-count {
	box-sizing: border-box;
	flex: none;
	padding: 0 var(--al-tabs-count-padding-x);
	border-radius: var(--al-tabs-count-radius);
	background: var(--al-tabs-count-fill);
	color: var(--al-tabs-count-label);
	font-size: var(--al-tabs-count-font-size);
	font-weight: var(--al-tabs-count-font-weight);
	font-variant-numeric: tabular-nums;
	line-height: 1.6; /* structural: this is what gives the pill its height, since it
	     has no height of its own. Not a type decision. */
}
/* the count follows the selection, in the same two channels the tab does: the
   ground tints and the ink takes the accent's line rung */
.al-tab[aria-selected='true'] .al-tab-count {
	--al-tabs-count-fill: var(--al-sys-color-row-tint-selected);
	--al-tabs-count-label: var(--al-sys-color-accent-primary-line);
}
/* A COUNT THAT MEANS MISSING IS A WARNING: amber and bold, so it is seen before Save is pressed.
   The fill stops at 20%, past which the 12px label falls under 4.5:1. Without color-mix there is
   no fill, because the dark tint is amber-700 and the amber-300 label cannot be read on it. */
.al-tab-count-warning,
.al-tab[aria-selected='true'] .al-tab-count-warning {
	--al-tabs-count-fill: transparent;
	--al-tabs-count-label: var(--al-sys-color-status-warning-text);
	font-weight: var(--al-sys-font-weight-heading);
}
@supports (color: color-mix(in srgb, red, blue)) {
	.al-tab-count-warning,
	.al-tab[aria-selected='true'] .al-tab-count-warning {
		--al-tabs-count-fill: color-mix(in srgb, var(--al-sys-color-status-warning) 20%, transparent);
	}
}

/* SIZE. The lg rung is for a tab strip under a page heading, where the strip is the
   main navigation of the screen; md is for one inside a card. */
.al-tabs-lg {
	--al-tabs-height: var(--al-sys-control-height-lg);
	--al-tabs-padding-x: var(--al-sys-control-padding-x-lg);
	--al-tabs-font-size: var(--al-sys-font-size-400);
	--al-tabs-icon-size: var(--al-sys-icon-size-lg);
}
.al-tabs-sm {
	--al-tabs-height: var(--al-sys-control-height-sm);
	--al-tabs-padding-x: var(--al-sys-control-padding-x-sm);
	--al-tabs-font-size: var(--al-sys-font-size-200);
	--al-tabs-icon-size: var(--al-sys-icon-size-sm);
}

/* =============================================================================
   THE SEGMENTED CONTROL

   Two to five options, all of them visible, one chosen. The thing a row of buttons
   is constantly mistaken for.

   IT IS A REAL RADIO GROUP. Not a row of buttons with a class on the active one —
   an <input type="radio"> per segment, hidden and covered by its own label. That
   buys the arrow-key navigation, the form submission, the required/validation
   behaviour and the "radio button, Adult, 1 of 2" announcement for nothing, and it
   makes the control structurally what it looks like. A button row can only ever
   claim to be this.

   AND IT IS DRAWN AS A GROOVE, which is the visual half of the same statement. A
   button stands ON the page; a segment sits IN a track, and the chosen one is a
   plate lifted out of it. One continuous container, no gaps between the items, no
   borders on the items. Nothing about it reads as three separate things to press.

   THE CHIP IS NOT THE ACCENT, and this is the arguable call. A segmented control
   picks a view; the accent is what the product uses for actions and for saying
   where you are in navigation. Two emerald chips in a toolbar out-shout the one
   emerald button in it that actually does something, and the chip does not need to
   shout: every option is on screen and the eye finds the raised one in a single
   pass. Pagination goes the other way and takes the accent, because a pager can
   carry twenty items and the current one has to be findable across a table.

   THE CORNERS ARE CONCENTRIC, not two guesses. The track is the control radius (8),
   its border is 1 and its padding is 2, so the chip is 8 - 3 = 5 — which is exactly
   --al-sys-radius-sm. A chip that repeats the outer radius reads as a sticker on
   top of the track rather than a plate set into it.
   ============================================================================= */

.al-seg {
	/* THE CHIP'S GEOMETRY, declared here and written by js/shared/segmented.js once it can
	   measure the chosen segment. Declared rather than left to the script because an
	   undefined var() unsets the property: a track the script has not reached yet would
	   have no width and no offset at all rather than a harmless zero. has-chip is only
	   added once real numbers are in, so these are a floor, not a look. */
	--al-seg-x: 0px;
	--al-seg-w: 0px;
	--al-seg-h: 0px;

	--al-segmented-height: var(--al-sys-control-height-md);
	--al-segmented-padding-x: var(--al-sys-control-padding-x-md);
	--al-segmented-font-size: var(--al-sys-font-size-300);
	--al-segmented-font-family: var(--al-sys-font-family);
	--al-segmented-font-weight: var(--al-sys-font-weight-control);
	--al-segmented-line-height: var(--al-sys-line-height-control);

	/* the groove */
	--al-segmented-track-fill: var(--al-sys-color-surface-sunken);
	--al-segmented-track-border-color: var(--al-sys-color-border);
	--al-segmented-track-border-width: var(--al-sys-control-border-width);
	--al-segmented-track-radius: var(--al-sys-control-radius);
	--al-segmented-track-padding: var(--al-sys-space-25);

	/* the chip. Its resting fill is nothing at all — an unchosen segment is ink on the
	   track and no box. The chosen fill is set below, and it is the ACCENT, because that
	   is what every chosen thing in this system is. */
	--al-segmented-chip-fill: transparent;
	--al-segmented-chip-radius: var(--al-sys-radius-sm);
	--al-segmented-chip-shadow: none;
	--al-segmented-label: var(--al-sys-color-text-secondary);

	--al-segmented-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-segmented-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-segmented-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-segmented-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-segmented-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-segmented-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-segmented-motion-duration: var(--al-sys-motion-duration-state);
	--al-segmented-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-segmented-* below this line -------------------- */
	box-sizing: border-box;
	display: inline-flex;
	align-items: stretch; /* the chips fill the track's height, so the groove shows as
	     an even margin all the way round rather than a gap top and bottom */
	flex: none;
	align-self: center;
	min-height: var(--al-segmented-height);
	padding: var(--al-segmented-track-padding);
	border: var(--al-segmented-track-border-width) solid var(--al-segmented-track-border-color);
	border-radius: var(--al-segmented-track-radius);
	background: var(--al-segmented-track-fill);
}

.al-seg-item {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	position: relative;
	flex: none;
	min-width: 0;
	padding: 0 var(--al-segmented-padding-x);
	border-radius: var(--al-segmented-chip-radius);
	background: var(--al-segmented-chip-fill);
	box-shadow: var(--al-segmented-chip-shadow);
	color: var(--al-segmented-label);
	font-family: var(--al-segmented-font-family);
	font-size: var(--al-segmented-font-size);
	font-weight: var(--al-segmented-font-weight);
	line-height: var(--al-segmented-line-height);
	white-space: nowrap;
	cursor: pointer;
	transition:
		color var(--al-segmented-motion-duration) var(--al-segmented-motion-easing),
		background-color var(--al-segmented-motion-duration) var(--al-segmented-motion-easing);
}
/* the label needs an element for the same reason the button's does */
.al-seg-item > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
/* the radio covers the whole segment rather than hiding in a corner, so the hit
   area is the chip. display:none would have taken it out of the tab order and out
   of the accessibility tree with it. */
.al-seg-item > input {
	appearance: none;
	-webkit-appearance: none;
	/* border-box, explicitly: box-sizing is not inherited, and a radio carries user
	   agent padding that differs by browser. Left content-box, the 100% hit area is
	   100% PLUS that padding and it reaches past the chip it is meant to cover. */
	box-sizing: border-box;
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	border: 0;
	border-radius: inherit;
	background: transparent;
	cursor: inherit;
}
/* THE RADIO IS STILL A REAL FOCUSABLE ELEMENT and the browser paints its own ring on
   it. Left in place, a keyboard user got a black UA outline traced round the chip
   with the recipe's ring hidden underneath — caught by rendering it, not by reading
   it. The ring belongs on the item, which is the thing that looks like a control. */
.al-seg-item > input:focus {
	outline: none;
}

/* HOVER IS INK AND NOTHING ELSE, and this one was decided by rendering both.

   Painting the chip fill on hover and withholding only the shadow looked better on a
   white card and was plainly wrong on a dark panel: the lift that separated "hovered"
   from "chosen" is a BLACK shadow, a dark ground eats it, and the two states came out
   as the same slate plate side by side. The chip is the one thing this control has to
   say "chosen" with, so nothing else gets to paint one.

   What is left is a smaller move on light (slate-700 to slate-900) than on dark
   (slate-300 to white), and that is the honest trade. The pointer has already changed
   shape, every option is already visible, and a segmented control's hover is the
   lowest-stakes affordance in the family. */
.al-seg-item:hover {
	--al-segmented-label: var(--al-sys-color-text-primary);
}
/* CHOSEN — THE ACCENT, LIKE EVERY OTHER CHOSEN THING IN THIS SYSTEM.

   A checked checkbox is accent-primary. A checked radio is accent-primary. A switch
   that is on is accent-primary. A selected tab's indicator is accent-primary-line, a
   selected row is the accent at 10%, and the pager's current page is accent-primary
   with an on-accent label — which is the identical sentence to this one: a filled chip
   in a row of chips saying "this is the one you are on". This rule is that rule.

   Two earlier answers were built and thrown away here, and it is worth saying why,
   because both were reasoned about this control on its own:

     A raised white plate. Measured 1.06:1 against its own track on a card and 1.26:1
     on a dark panel. Everything saying "chosen" was the label ink and a hairline
     shadow, and at a glance neither reads.

     A near-black ink plate. Loud enough — 13.6:1 and 12.1:1 — and the only control in
     the product that answered "which one is chosen" in a colour the product does not
     use for that. Consistency is the whole reason a design system exists, so a control
     that is legible and inconsistent has not solved the problem.

   The worry that justified both — that emerald chips out-shout the one emerald button
   that actually does something — is answered by the pager, which has been doing exactly
   this on every table screen without anyone minding.

   Also rejected, and worth not re-deriving: deepening the track and leaving the chip
   pale. It works on a card and inverts on a dark panel, where deepening moves the track
   TOWARD the chip; at 15% they met at 1.02:1 and the chosen segment vanished.

   Last, so it outranks hover — hovering the chosen segment must not undo the chip.
   There is deliberately no press state: clicking a radio checks it instantly and the
   chip moves, which is the feedback. A veil under a chip that is about to be replaced
   is a flicker, not a response.

   The shadow stays a real value rather than `none` because the focus rule below
   composes it into a box-shadow LIST, and `none` is not legal inside one. */
.al-seg-item:has(> input:checked) {
	--al-segmented-chip-fill: var(--al-sys-color-accent-primary);
	--al-segmented-chip-shadow: var(--al-sys-elevation-sticky);
	--al-segmented-label: var(--al-sys-color-text-on-accent);
}

/* -------------------------------------------------------------------------
   THE CHIP TRAVELS, and this is the half that was missing.

   js/shared/segmented.js has always measured the chosen segment and written
   --al-seg-x / -w / -h onto the track, and nothing read them, so the chip was
   painted on the item and swapped. A swap makes the eye re-find the selection;
   a chip that slides is followed.

   It is a pseudo-element on the TRACK rather than a background on the item,
   because only one element can move between two positions. ::before, not
   ::after, so it paints under the segments: generated content takes its place
   in the child order and the items come after it. They are position:relative
   already, which is what keeps their labels above it.

   position:relative on the track is not decoration. The script reads
   item.offsetLeft, which is measured from the nearest positioned ancestor, and
   its own comment says "the track is position:relative" -- which was never
   true. Without it the chip was placed from whatever outer element happened to
   be positioned.

   THE TRANSITION IS GATED ON .seg-armed, which the script adds two frames after
   the first placement. Ungated, every track slides its chip in from the left on
   page load, which reads as a loading state rather than a selection.

   width is animated as well as transform because the segments are content-width
   -- Cannabis and Plant nutrient are not the same size -- so a chip that only
   translated would be the wrong width for most of its journey. */
.al-seg {
	position: relative;
}
.al-seg.has-chip::before {
	content: '';
	position: absolute;
	top: var(--al-segmented-track-padding);
	left: var(--al-segmented-track-padding);
	width: var(--al-seg-w);
	height: var(--al-seg-h);
	border-radius: var(--al-segmented-chip-radius);
	background: var(--al-sys-color-accent-primary);
	box-shadow: var(--al-sys-elevation-sticky);
	transform: translateX(calc(var(--al-seg-x) - var(--al-segmented-track-padding)));
	pointer-events: none;
}
.al-seg.has-chip.seg-armed::before {
	transition:
		transform var(--al-segmented-motion-duration) var(--al-segmented-motion-easing),
		width var(--al-segmented-motion-duration) var(--al-segmented-motion-easing);
}
/* the item stops painting its own chip once the track paints a travelling one, and
   keeps only the ink change, which is what makes the two look like one control. */
.al-seg.has-chip .al-seg-item:has(> input:checked) {
	--al-segmented-chip-fill: transparent;
	--al-segmented-chip-shadow: none;
}
/* the focus ring still belongs to the segment, and it composes with no chip shadow
   now, so it states the list itself rather than restating a shadow that is gone. */
.al-seg.has-chip .al-seg-item:has(> input:checked):has(> input:focus-visible) {
	box-shadow:
		0 0 0 var(--al-segmented-focus-hairline-width) var(--al-segmented-focus-hairline-light),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-ring-color),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) * 2 + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-hairline-dark);
}
.al-seg-item:has(> input:focus-visible) {
	outline: var(--al-segmented-focus-ring-width) solid transparent;
	outline-offset: var(--al-segmented-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-segmented-focus-hairline-width) var(--al-segmented-focus-hairline-light),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-ring-color),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) * 2 + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-hairline-dark);
}
/* a chosen segment that is also focused keeps its lift AND gets the ring, which
   means restating the chip's shadow here — box-shadow is one property and the ring
   would otherwise replace it. This is the one place a state has to name a property
   twice, and the alternative (dropping the lift while focused) makes the chosen
   segment look like it moved. */
.al-seg-item:has(> input:checked):has(> input:focus-visible) {
	box-shadow:
		var(--al-segmented-chip-shadow),
		0 0 0 var(--al-segmented-focus-hairline-width) var(--al-segmented-focus-hairline-light),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-ring-color),
		0 0 0 calc(var(--al-segmented-focus-hairline-width) * 2 + var(--al-segmented-focus-ring-width)) var(--al-segmented-focus-hairline-dark);
}
/* disabled is PER SEGMENT — "Medical is not licensed at this location" is a real
   thing to say, and disabling the whole control is just every segment disabled */
.al-seg-item:has(> input:disabled) {
	opacity: var(--al-segmented-disabled-opacity);
	pointer-events: none;
	cursor: not-allowed;
}

/* FILL — the segments share the width of whatever holds them instead of each being
   as wide as its label. Narrow panels, a POS sale-type picker, a modal. Same job
   .al-btn-block does, and it exists for the same reason: without it every screen
   that needs one writes its own width override and drifts off the system. */
.al-seg-fill {
	display: flex;
	width: 100%;
}
.al-seg-fill .al-seg-item {
	flex: 1 1 0;
}

.al-seg-sm {
	--al-segmented-height: var(--al-sys-control-height-sm);
	--al-segmented-padding-x: var(--al-sys-control-padding-x-sm);
	--al-segmented-font-size: var(--al-sys-font-size-200);
}
.al-seg-lg {
	--al-segmented-height: var(--al-sys-control-height-lg);
	--al-segmented-padding-x: var(--al-sys-control-padding-x-lg);
	--al-segmented-font-size: var(--al-sys-font-size-400);
}

/* =============================================================================
   THE BREADCRUMB

   Where you are, and the way back. It is a trail, not navigation you browse — so
   it is quiet, it is one line, and it never wraps.

   TWO KINDS OF TOO LONG, and they need different answers.

     ONE NAME IS ENORMOUS — "Line 3 · Blue Dream 3.5g Preroll 10pk". That is
     handled here, in CSS: every crumb can ellipsize, and the ancestors give way
     four times faster than the page you are actually on, so the thing you most
     need to read is the last to be cut.

     THE PATH IS DEEP — six levels of hierarchy. That is NOT handled in CSS, and
     pretending otherwise is the trap. Folding the middle correctly needs to know
     the available width, which is a measurement CSS cannot make; a media query
     folding at a fixed breakpoint is wrong at every width except the one it was
     tuned at. So the rule is depth, not width, and the server applies it: past
     four levels, render first / fold / last two, and mark the fold with
     .al-crumb-fold carrying a title listing what it swallowed.

   THE SEPARATOR IS A MASKED CHEVRON, not a character. A "›" depends on the glyph
   existing in whatever face the page resolved to and it inherits the label's
   weight, so it comes out spindly in one place and heavy in another. A mask takes
   its shape from the SVG and its colour from a token, which is the pattern the
   checkbox's mark already established as the better of the two.
   ============================================================================= */

.al-crumbs {
	--al-breadcrumb-font-size: var(
		--al-sys-font-size-200
	); /* a notch below body: the
	     trail is orientation, and it sits above a page title that should win */
	--al-breadcrumb-font-family: var(--al-sys-font-family);
	--al-breadcrumb-line-height: var(--al-sys-line-height-control);
	--al-breadcrumb-gap: var(--al-sys-space-100);
	/* A CLICKABLE CRUMB IS THE ACCENT, and that is not decoration. This trail has THREE
	   kinds of thing in it, not two: a link, a module that is a grouping container with no
	   page of its own, and the crumb you are standing on. If links were muted ink they
	   would sit one shade from the containers, and a person cannot tell which is which by
	   looking — nor by convention, because nobody knows which nav entries are containers.
	   The accent already means "you can act on this" everywhere else in the system, and a
	   link is something you act on. */
	--al-breadcrumb-label: var(--al-sys-color-accent-primary-line);
	--al-breadcrumb-static-label: var(--al-sys-color-text-muted);
	--al-breadcrumb-current-label: var(--al-sys-color-text-primary);
	--al-breadcrumb-current-font-weight: var(--al-sys-font-weight-medium);
	--al-breadcrumb-separator: var(--al-sys-color-text-muted);
	--al-breadcrumb-separator-size: 10px; /* optically matched to a 13px label — a
	     14px chevron between two 13px words reads as a third word */
	/* the shape only. The colour in the SVG is discarded: it is used as a mask, so
	   only the alpha survives and --al-breadcrumb-separator paints it. */
	--al-breadcrumb-separator-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m6 3 5 5-5 5' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-breadcrumb-item-max-width: 24ch; /* a ceiling on ONE crumb so a single long
	     name cannot push the rest of the trail off the row before flex gets a say */
	/* AND A FLOOR, which is the half everyone forgets. Without one, flex happily
	   shrinks an ancestor to zero and the trail renders as a chevron, a blank, a
	   chevron — rendered, not theorised. These are on the ROW, not on the link: the
	   row also holds the separator, and `min-width: min-content` was the first attempt
	   and does nothing at all, because the min-content size of nowrap text is the
	   whole string. Also rendered, also not theorised. */
	--al-breadcrumb-radius: var(--al-sys-radius-sm);

	--al-breadcrumb-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-breadcrumb-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-breadcrumb-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-breadcrumb-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-breadcrumb-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-breadcrumb-motion-duration: var(--al-sys-motion-duration-state);
	--al-breadcrumb-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-breadcrumb-* below this line ------------------- */
	box-sizing: border-box;
	display: block;
	min-width: 0;
	font-family: var(--al-breadcrumb-font-family);
	font-size: var(--al-breadcrumb-font-size);
	line-height: var(--al-breadcrumb-line-height);
}
.al-crumbs ol {
	display: flex;
	align-items: center;
	flex-wrap: nowrap;
	min-width: 0;
	margin: 0;
	padding: 0;
	list-style: none;
}
.al-crumbs li {
	display: flex;
	align-items: center;
	gap: var(--al-breadcrumb-gap);
	min-width: 0;
	/* the ancestors give way first. Flex shrinks in proportion to the factor, so at 4
	   an ancestor loses four characters for every one the current page loses — which
	   is the right order, because the crumb you are standing on is the one you were
	   reading.

	   Once every ancestor has hit its floor they freeze and the remaining shrink falls
	   on the current crumb, which has a floor of its own. Past that the row overflows
	   on purpose — the same call the button makes with flex:none, because a panel that
	   visibly overflows gets noticed and fixed, and a trail that silently shrank to
	   nothing does not. */
	flex: 0 4 auto;
	/* NO WIDTH FLOOR. There was one -- 8ch per crumb, 14ch on the current one -- meant as
	   the point a squeezed crumb stops shrinking. min-width does not work that way: it
	   applies always, so a short label got padded out to the floor and left dead space
	   before its separator. Measured on a real trail: "Home" is 36px of text in a 65px
	   box, and the current crumb 57px of text in 113px. The ellipsis and the fold are
	   what stop a trail collapsing; a floor was solving a problem they already had. */
}
.al-crumbs li + li {
	margin-left: var(--al-breadcrumb-gap);
}
.al-crumbs li + li::before {
	content: '';
	box-sizing: border-box; /* not inherited, and the usual reset does not reach a
	     pseudo-element — the same guard the pager's chevron carries */
	flex: none; /* the chevron never shrinks: a half-drawn separator reads as damage */
	width: var(--al-breadcrumb-separator-size);
	height: var(--al-breadcrumb-separator-size);
	background: var(--al-breadcrumb-separator);
	-webkit-mask: var(--al-breadcrumb-separator-image) center / contain no-repeat;
	mask: var(--al-breadcrumb-separator-image) center / contain no-repeat;
}

/* THE TRUNCATION LIVES ON THE LINK, NOT ON THE LI, and that is not a style choice.
   text-overflow needs a block box, and overflow:hidden on the li would clip the
   link's focus ring — an element's own box-shadow is not clipped by its own
   overflow, but it is very much clipped by its parent's. */
.al-crumbs a,
.al-crumbs .al-crumb-static,
.al-crumbs [aria-current='page'] {
	display: block;
	max-width: var(--al-breadcrumb-item-max-width);
	min-width: 0; /* the floor is on the row; inside it the label is free to ellipsize
	     all the way down, which is what an ellipsis is for */
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	border-radius: var(--al-breadcrumb-radius);
	color: var(--al-breadcrumb-label);
	text-decoration: none;
	transition: color var(--al-breadcrumb-motion-duration) var(--al-breadcrumb-motion-easing);
}
.al-crumbs a:hover {
	--al-breadcrumb-label: var(--al-sys-color-text-primary);
	text-decoration: underline;
	text-underline-offset: 2px;
}
.al-crumbs a:active {
	--al-breadcrumb-label: var(--al-sys-color-accent-primary-line);
}
.al-crumbs a:focus {
	outline: none;
}
.al-crumbs a:focus-visible {
	outline: var(--al-breadcrumb-focus-ring-width) solid transparent;
	outline-offset: var(--al-breadcrumb-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-breadcrumb-focus-hairline-width) var(--al-breadcrumb-focus-hairline-light),
		0 0 0 calc(var(--al-breadcrumb-focus-hairline-width) + var(--al-breadcrumb-focus-ring-width)) var(--al-breadcrumb-focus-ring-color),
		0 0 0 calc(var(--al-breadcrumb-focus-hairline-width) * 2 + var(--al-breadcrumb-focus-ring-width)) var(--al-breadcrumb-focus-hairline-dark);
}

/* WHERE YOU ARE. Not a link, because it goes nowhere — a link that reloads the page
   you are on is a promise the product does not keep. It takes the primary ink and
   the medium weight, which is the only weight change in this control and it is safe
   because the current crumb is last: nothing re-flows behind it. */
/* THE THIRD STATE — a module that is a grouping container with no page of its own.
   "Inventory" in Home / Inventory / Items is a place in the tree, not a destination. It
   is neither a link nor where you are, so it gets neither treatment: muted ink, body
   weight, and no pointer. Every grouped navigation has these, which is why it lives here
   rather than in one product's stylesheet. */
.al-crumbs .al-crumb-static {
	--al-breadcrumb-label: var(--al-breadcrumb-static-label);
	cursor: default;
}

.al-crumbs [aria-current='page'] {
	--al-breadcrumb-label: var(--al-breadcrumb-current-label);
	/* the ceiling is LIFTED by re-pointing the token, not by writing max-width again —
	   rule 8. A second max-width here would mean the rule above and this one both own
	   the property, and the day the cap moves only one of them would learn about it. */
	--al-breadcrumb-item-max-width: none;
	font-weight: var(--al-breadcrumb-current-font-weight);
}
.al-crumbs li:last-child {
	flex: 0 1 auto;
}

/* THE FOLD. What the server renders in place of the levels it dropped. Not a
   button: opening a menu from it is the overlay family's job, and a title listing
   what was swallowed answers the question for the cost of an attribute. */
/* the floor is DROPPED by re-pointing the row's token, not by writing min-width a
   second time — rule 8, and it is why this needs no specificity trick to outrank the
   row rule above. A fold is three dots and has no business reserving eight characters
   of room. Still scoped under .al-crumbs, because the class means nothing outside one. */
.al-crumbs .al-crumb-fold {
	/* the fold had to opt out of the width floor; there is no floor now */
	flex: none;
	color: var(--al-breadcrumb-label);
	cursor: default;
}

/* =============================================================================
   THE BACK CONTROL

   The way out of a record. One line above the record's name: an arrow, then the
   name of the place it returns to.

   IT IS NOT THE BREADCRUMB, AND IT IS NOT THE BROWSER'S BACK BUTTON. Three
   different sentences, and a record page in this product says two of them at once:
   the trail in the chrome says where the screen sits in the tree and offers every
   level of it, and this says one thing — "the list this record came from" — in the
   content, where the eye lands before it reads the title. History is the third
   again, and it is the only one of the three that can send someone somewhere the
   product did not choose.

   IT STATES ITS OWN CHROME RATHER THAN INHERITING IT, and that is the rule the
   control exists to keep. Hosts write it as an <a> where leaving is a route and as
   a <button> where something has to run on the way out, and a <button> arrives
   wearing the browser's 2px outset border, its grey fill, its padding and its own
   font family. A control that changes shape depending on the element it lands on is
   not a control, so every one of those is stated here. It is the same call
   .al-card-title makes about heading elements, for the same reason.

   THE SIZE IS THE TRAIL'S, NOT A CONTROL'S. Everything else you can click takes the
   300 rung; this takes 200, the same as the breadcrumb, on the breadcrumb's own
   grounds: it sits directly above a record name at the 500 rung, its job is
   orientation rather than action, and the name has to win the line. The glyph does
   NOT step down with the type — it stays on the md rung, because it is the part
   that says "this goes somewhere" and a 14px arrow beside 13px text reads as
   punctuation.

   THE GLYPH IS REQUIRED, AND THE SLOT SIZES EITHER KIND. Required, unlike the tab's
   optional one: without the arrow this is a short line of muted text with nothing
   saying it leaves the page. What it shares with the tab is the lesson that slot
   learned by being rendered — a width and a height size an SVG, and an icon FONT
   ignores both, drawing at whatever font-size it inherits and spilling out of the
   box. Both are set, so the slot does not care which kind it is handed, and the
   recipe does not have to have an opinion about which the product ships.

   NO UNDERLINE, IN ANY STATE. The breadcrumb underlines on hover because a trail of
   quiet words needs one thing saying which of them can be followed. Here the arrow
   is already that thing, and a rule running under a glyph and a label reads as a
   link inside a sentence rather than the way out of a page.

   THERE IS NO DISABLED STATE. A way out is never unavailable. A record that cannot
   be left is a trap, and the control that would say so should not exist.
   ============================================================================= */

.al-back {
	/* -- scale. The trail's rung, not a control's — see above. ---------------- */
	--al-back-font-size: var(--al-sys-font-size-200);
	--al-back-icon-size: var(--al-sys-icon-size-md);
	--al-back-gap: var(--al-sys-control-gap);

	/* the system names its own face rather than inheriting the host page's, and here
	   it is also half of the <a>-and-<button> rule: a button does not inherit the
	   page's font at all, so leaving this out is what makes one host render the
	   control in Public Sans and the next in whatever the browser's form font is */
	--al-back-font-family: var(--al-sys-font-family);
	--al-back-font-weight: var(--al-sys-font-weight-control);
	--al-back-line-height: var(--al-sys-line-height-control);

	/* ONE ink token, re-pointed by each state. Two tokens named -rest and -hover
	   would need the state rules to alias one to the other, which is the same-tier
	   aliasing rule 2 forbids. The glyph gets none of its own: it takes the label's
	   ink, so it cannot fall out of step with the words beside it. */
	--al-back-label: var(--al-sys-color-text-muted);

	--al-back-radius: var(--al-sys-radius-sm); /* the focus ring's corners. The
	     control has no ground of its own for this to round. */

	--al-back-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-back-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-back-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-back-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-back-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-back-motion-duration: var(--al-sys-motion-duration-state);
	--al-back-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-back-* below this line ------------------------- */
	/* the chrome, stated rather than inherited. appearance comes first because it is
	   what stops the button drawing a native widget at all. */
	appearance: none;
	-webkit-appearance: none;
	box-sizing: border-box;
	padding: 0;
	border: 0;
	border-radius: var(--al-back-radius);
	background: none;

	display: inline-flex;
	align-items: center;
	gap: var(--al-back-gap);
	color: var(--al-back-label);
	font-family: var(--al-back-font-family);
	font-size: var(--al-back-font-size);
	font-weight: var(--al-back-font-weight);
	line-height: var(--al-back-line-height);
	/* the last three of the <a>-and-<button> rule, and the only ones left that a host
	   page can still split. A button does not inherit the page's tracking, casing or
	   word spacing; an anchor does. Measured on a host setting all three, the anchor
	   came out 166px and UPPERCASE beside a 125px sentence-case button wearing the same
	   class. Same call .al-card-title makes about a heading, for the same reason. */
	text-transform: none;
	letter-spacing: normal;
	word-spacing: normal;
	white-space: nowrap;
	text-decoration: none;
	cursor: pointer;
	transition: color var(--al-back-motion-duration) var(--al-back-motion-easing);
}

/* THE GLYPH. Sized twice on purpose: the box sizes an SVG and the type sizes an icon
   font, and neither one alone covers both. flex:none because it is the part that must
   never give way — a half-width arrow reads as damage.

   SCOPED UNDER THE CONTROL, and that is not tidiness — it is what makes the type half
   of the sizing actually hold. An icon font ships a class of its own, and Material
   Symbols' own stylesheet sets .material-symbols-outlined { font-size: 24px }. That is
   one class against one class, so the winner is whichever file loads last, and this
   file does not get to choose. Rendered flat, the arrow drew at 24px inside its 16px
   box, spilled over the label and ate the gap between them. Under .al-back it is two
   classes and the question stops being about load order. Same reason .al-crumb-static
   is scoped: the class means nothing outside its control anyway. */
.al-back .al-back-icon {
	box-sizing: border-box; /* not inherited, and no reset reaches a host page's SVG:
	     a glyph carrying padding of its own renders wider than the rung says it is */
	flex: none;
	width: var(--al-back-icon-size);
	height: var(--al-back-icon-size);
	font-size: var(--al-back-icon-size);
	line-height: 1;
}

/* STATES re-point tokens, never properties — rule 8. Both moves are real on both
   grounds: slate-500 to slate-900 on a card, slate-300 to white on a dark panel. */
.al-back:hover {
	--al-back-label: var(--al-sys-color-text-primary);
}
/* press takes the accent's LINE rung, the same answer the crumb gives, so the control
   has answered before the next page has begun to load */
.al-back:active {
	--al-back-label: var(--al-sys-color-accent-primary-line);
}
.al-back:focus {
	outline: none;
}
.al-back:focus-visible {
	/* the three-layer ring, exactly as the tab and the crumb draw it. The transparent
	   outline is invisible here and repainted by Windows high-contrast, where
	   box-shadow is thrown away and a keyboard user would otherwise have nothing. */
	outline: var(--al-back-focus-ring-width) solid transparent;
	outline-offset: var(--al-back-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-back-focus-hairline-width) var(--al-back-focus-hairline-light),
		0 0 0 calc(var(--al-back-focus-hairline-width) + var(--al-back-focus-ring-width)) var(--al-back-focus-ring-color),
		0 0 0 calc(var(--al-back-focus-hairline-width) * 2 + var(--al-back-focus-ring-width)) var(--al-back-focus-hairline-dark);
}

/* =============================================================================
   PAGINATION

   Three facts and they are not interchangeable: WHICH page you are on, HOW MANY
   there are, and HOW BIG a page is. A pager that shows only the numbers makes
   someone click to the end to find out how much there is.

   READING ORDER IS THE LAYOUT. What you are looking at ("1–25 of 412 batches"),
   then how much of it at a time, then the way to move — left to right, with the
   controls that move you at the end of the row where the eye finishes.

   THE PER-PAGE PICKER IS AN .al-select. It is a short known list and the recipe
   already has the control for that. Minting a pager-flavoured dropdown would be a
   second answer to a question already answered, and it would drift.

   THE CURRENT PAGE TAKES THE ACCENT AS A FILL, which is the opposite of the call
   the segmented control makes twenty lines up, on purpose. A pager can carry twenty
   items of near-identical shape and the current one has to be findable from across
   a table in one glance; a segmented control has three items and all of them are
   already in the same saccade. Different job, different loudness.

   THERE ARE NO FIRST/LAST JUMPS. "1" is the first page and the last number is the
   last page, both already on the row — a double chevron would be a fourth control
   doing what two existing ones do.
   ============================================================================= */

.al-pager {
	/* The small rung. A pager is chrome under a table, not a control you aim at: at the md
	   rung it competes with the table's own rows for weight. Every pager in the app already
	   renders at sm. */
	--al-pagination-item-size: var(--al-sys-control-height-sm);
	--al-pagination-item-padding-x: var(--al-sys-space-100);
	--al-pagination-item-radius: var(--al-sys-control-radius);
	--al-pagination-item-border-width: var(--al-sys-control-border-width);
	--al-pagination-font-size: var(--al-sys-font-size-200);
	--al-pagination-font-family: var(--al-sys-font-family);
	--al-pagination-font-weight: var(--al-sys-font-weight-control);
	--al-pagination-line-height: var(--al-sys-line-height-control);

	/* REST CARRIES AN EDGE, and this reverses an earlier call. The argument for an edgeless
	   rest state was that "a row of twenty of them is a wall of boxes" — true, and no pager in
	   this product shows twenty. They show four or five beside a page-size select and a count,
	   and at that length the edge is what separates a page you can press from the words around
	   it. This is also what every real pager in the app already renders; the recipe was the
	   odd one out, describing a control only the lab page used.
	   Transparent is still available: a variant can re-point these two lines. */
	--al-pagination-item-fill: var(--al-sys-color-surface-control);
	--al-pagination-item-border-color: var(--al-sys-color-border);
	--al-pagination-item-label: var(--al-sys-color-text-primary);

	/* the veil is the accent as a LINE over the page's own ground, the same move the
	   quiet button makes. The neutral on-accent veil is wrong here: over a light card
	   it is a grey smudge, and over a dark panel a darkening veil on an already dark
	   ground produces no visible change at all. */
	--al-pagination-veil: var(--al-sys-color-accent-primary-line);
	--al-pagination-veil-hover-opacity: var(--al-sys-state-hover-opacity-on-surface);
	--al-pagination-veil-press-opacity: var(--al-sys-state-press-opacity-on-surface);

	/* the step chevron: one image, mirrored for "previous". Two images would be two
	   things to keep in step for no gain. */
	--al-pagination-step-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m6 3 5 5-5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-pagination-step-icon-size: var(--al-sys-icon-size-md);
	--al-pagination-step-flip: 1;

	--al-pagination-gap: var(
		--al-sys-space-50
	); /* between page buttons: close enough
	     to read as one control, far enough that the current fill has a margin */
	--al-pagination-label-gap: var(--al-sys-space-100); /* "Per page" to its select */
	--al-pagination-group-gap: var(--al-sys-space-300);
	--al-pagination-meta-label: var(--al-sys-color-text-muted);
	--al-pagination-meta-font-size: var(--al-sys-font-size-200);
	--al-pagination-disabled-opacity: var(--al-sys-state-disabled-opacity);

	--al-pagination-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-pagination-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-pagination-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-pagination-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-pagination-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-pagination-motion-duration: var(--al-sys-motion-duration-state);
	--al-pagination-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-pagination-* below this line -------------------
	      EVERY PART READS THESE, so every part lives inside .al-pager. Same contract
	      as .al-check's input and .al-seg's items: the family root owns the tokens so
	      a size variant re-points them in one place and cannot half-apply. A page
	      list lifted out on its own collapses to bare numbers, visibly — which is the
	      failure mode to prefer, because it is the one someone notices immediately. */
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-pagination-group-gap);
	flex-wrap: wrap; /* a pager is the widest thing in a card footer. It wraps rather
	     than scrolls, because unlike a tab strip its groups are independent. */
	font-family: var(--al-pagination-font-family);
}

.al-pager-count {
	margin: 0;
	color: var(--al-pagination-meta-label);
	font-size: var(--al-pagination-meta-font-size);
	line-height: var(--al-pagination-line-height);
	font-variant-numeric: tabular-nums; /* "1–25 of 412" must not change width as the
	     digits change, or the row twitches every time you page */
}

.al-pager-size {
	display: flex;
	align-items: center;
	gap: var(--al-pagination-label-gap);
	color: var(--al-pagination-meta-label);
	font-size: var(--al-pagination-meta-font-size);
	line-height: var(--al-pagination-line-height);
	white-space: nowrap;
}
/* the select inside is full-width by default, which is right in a form and wrong in
   a row. The container adjusts the nested control's layout only — its colours,
   states and focus are still entirely the select's own. */
.al-pager-size .al-select {
	width: auto;
}

/* the left group: what you are looking at, and how much of it at a time */
.al-pager-meta {
	display: flex;
	align-items: center;
	gap: var(--al-pagination-group-gap);
	flex-wrap: wrap;
}

.al-pager-pages {
	display: flex;
	align-items: center;
	gap: var(--al-pagination-gap);
	margin: 0;
	padding: 0;
	list-style: none;
}

.al-pager-page,
.al-pager-step {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	position: relative;
	isolation: isolate; /* keeps the veil inside the button's own stacking context, so
	     z-index:-1 puts it above the fill and below the number rather than over it */
	flex: none;
	min-width: var(--al-pagination-item-size);
	min-height: var(--al-pagination-item-size);
	padding: 0 var(--al-pagination-item-padding-x); /* so page 1024 grows rather than
	     spilling out of a square sized for page 9 */
	border: var(--al-pagination-item-border-width) solid var(--al-pagination-item-border-color);
	border-radius: var(--al-pagination-item-radius);
	background: var(--al-pagination-item-fill);
	color: var(--al-pagination-item-label);
	font-family: inherit;
	font-size: var(--al-pagination-font-size);
	font-weight: var(--al-pagination-font-weight);
	line-height: var(--al-pagination-line-height);
	font-variant-numeric: tabular-nums;
	cursor: pointer;
	transition: color var(--al-pagination-motion-duration) var(--al-pagination-motion-easing);
}
.al-pager-page::after,
.al-pager-step::after {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: inherit;
	background: var(--al-pagination-veil);
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--al-pagination-motion-duration) var(--al-pagination-motion-easing);
}
.al-pager-page:hover::after,
.al-pager-step:hover::after {
	opacity: var(--al-pagination-veil-hover-opacity);
}
.al-pager-page:active::after,
.al-pager-step:active::after {
	opacity: var(--al-pagination-veil-press-opacity);
}
.al-pager-page:hover,
.al-pager-step:hover {
	--al-pagination-item-label: var(--al-sys-color-text-primary);
}

/* WHERE YOU ARE. The veil flips with it: over an accent fill a veil must darken,
   because the fill is a known colour carrying a white label and lightening it eats
   the label's contrast. This is the same pair of roles the button uses, and getting
   it wrong is how a pressed current page washes out its own number. */
.al-pager-page[aria-current='page'] {
	--al-pagination-item-fill: var(--al-sys-color-accent-primary);
	--al-pagination-item-border-color: var(--al-sys-color-accent-primary);
	--al-pagination-item-label: var(--al-sys-color-text-on-accent);
	--al-pagination-veil: var(--al-sys-color-veil-on-accent);
	--al-pagination-veil-hover-opacity: var(--al-sys-state-hover-opacity-on-fill);
	--al-pagination-veil-press-opacity: var(--al-sys-state-press-opacity-on-fill);
}
.al-pager-page[aria-current='page']:hover {
	--al-pagination-item-label: var(--al-sys-color-text-on-accent);
}

.al-pager-page:focus,
.al-pager-step:focus {
	outline: none;
}
.al-pager-page:focus-visible,
.al-pager-step:focus-visible {
	outline: var(--al-pagination-focus-ring-width) solid transparent;
	outline-offset: var(--al-pagination-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-pagination-focus-hairline-width) var(--al-pagination-focus-hairline-light),
		0 0 0 calc(var(--al-pagination-focus-hairline-width) + var(--al-pagination-focus-ring-width)) var(--al-pagination-focus-ring-color),
		0 0 0 calc(var(--al-pagination-focus-hairline-width) * 2 + var(--al-pagination-focus-ring-width)) var(--al-pagination-focus-hairline-dark);
}

/* THE STEP. Its chevron is a mask so the colour comes from the label token and it
   follows every state the number does — an SVG with a baked fill would stay dark
   grey on a dark panel while the numbers beside it went white. */
.al-pager-step::before {
	content: '';
	box-sizing: border-box; /* not inherited, and the usual reset does not reach
	     pseudo-elements. Measured on the button's spinner: 2px off centre. */
	width: var(--al-pagination-step-icon-size);
	height: var(--al-pagination-step-icon-size);
	background: var(--al-pagination-item-label);
	-webkit-mask: var(--al-pagination-step-image) center / contain no-repeat;
	mask: var(--al-pagination-step-image) center / contain no-repeat;
	transform: scaleX(var(--al-pagination-step-flip));
}
/* the variant re-points a token; it does not restate the transform — rule 8 */
.al-pager-prev {
	--al-pagination-step-flip: -1;
}

/* DISABLED. aria-disabled is the one to reach for on a pager: paging to the first
   page must not silently delete "previous" from the tab order under the keyboard
   user's fingers. :disabled is styled identically for whoever reaches for the
   native attribute anyway. */
.al-pager-page:disabled,
.al-pager-step:disabled,
.al-pager-page[aria-disabled='true'],
.al-pager-step[aria-disabled='true'] {
	opacity: var(--al-pagination-disabled-opacity);
	pointer-events: none;
	cursor: not-allowed;
}

/* THE GAP — the run of pages nobody rendered. Not a button, not focusable, and it
   holds the width of a page button so the row does not jump when the gap moves
   from one side of the current page to the other. */
.al-pager-gap {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	min-width: var(--al-pagination-item-size);
	min-height: var(--al-pagination-item-size);
	color: var(--al-pagination-meta-label);
	font-size: var(--al-pagination-font-size);
	line-height: var(--al-pagination-line-height);
	user-select: none;
}

/* SM — a grid footer, where the pager sits under a table of 28px rows and a 36px
   one would be the tallest thing in the region. */
.al-pager-sm {
	--al-pagination-item-size: var(--al-sys-control-height-sm);
	--al-pagination-font-size: var(--al-sys-font-size-200);
	--al-pagination-step-icon-size: var(--al-sys-icon-size-sm);
	--al-pagination-group-gap: var(--al-sys-space-200);
}

/* =============================================================================
   REDUCED MOTION

   Everything these five controls animate is polish — a colour easing into place.
   None of it carries information the resting state does not already carry, so all
   of it goes for someone whose operating system asks for less motion. Nothing is
   lost: every state here is legible as a still frame, which is the test.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-tab,
	.al-tab::after,
	.al-seg-item,
	.al-crumbs a,
	.al-back,
	.al-pager-page,
	.al-pager-step,
	.al-pager-page::after,
	.al-pager-step::after {
		transition: none;
	}
}

/* ---- chrome ---------------------------------------------------- */
/* =============================================================================
   CHROME — THE SIDE NAVIGATION, THE TOP BAR, AND THE MENU

   These three are the frame. Everything else in the system stands inside them, so
   they are the parts most likely to be seen on a ground the author did not choose:
   a nav can be a white column or a dark rail, a bar can sit on either, and a menu
   floats over whatever happens to be underneath. Not one rule in this file names a
   control inside .al-on-dark. Every colour comes from a sys role, so all three are
   correct on both grounds the day they are written, and a theme reaches every pixel
   by re-pointing the same roles.

   THREE MEASUREMENTS DECIDED THINGS HERE, and they are worth stating once because
   they explain values that otherwise look arbitrary:

     1. A MENU IS A RAISED SURFACE, AND ON A DARK GROUND RAISED MEANS LIGHTER.
        --al-sys-color-surface-raised is slate-700 there, not slate-900. The -line
        rungs were measured against slate-900: red-300 on slate-700 is 3.56, under
        the 4.5 floor. So a destructive menu item reads the -strong rung at rest
        instead, which moves AWAY from the ground on both sides — red-600 on white
        is 6.40, red-200 on slate-700 is 5.42.

     2. ON A HOVERED ROW, ONLY THE PRIMARY INK SURVIVES ON A DARK GROUND. The active
        row tint is 40% emerald-300 there, which lands lighter than the panel under
        it, and secondary ink on that measures 3.27. So hover promotes every mark in
        the row at once — label, glyph, count, shortcut — to the primary rung, which
        measures 5.01. A row that commits all at once also reads better than one
        whose glyph stays quiet while its label lights up.

     3. A DESTRUCTIVE ITEM'S HOVER IS A SOLID FILL, NOT A TINT. Any translucent red
        moves the ground toward the red label and walks it under the floor: 20% over
        white measured 3.77. The accent FILL role holds still between grounds, so
        white on it is 5.01 on both, and nobody has to wonder what the row does.
   ============================================================================= */

/* =============================================================================
   1. THE SIDE NAVIGATION

   One column, two widths. Expanded it carries a glyph, a name and a count per row;
   collapsed to a rail it carries the glyph alone.

   THE RAIL RE-POINTS TOKENS AND NOTHING ELSE — there is no property in
   .al-sidenav-rail. That is rule 8 doing the work it exists for: the rail cannot
   forget the chosen row, the focus ring, the hover tint or the marker, because it
   does not own any of those rules. It owns seven values the base rules already read.
   ============================================================================= */

.al-sidenav {
	/* -- the column -------------------------------------------------------- */
	--al-sidenav-width: 248px; /* holds "Transfers & manifests" at the 300 rung
	     without truncating, which is the longest name in the product's own nav */
	--al-sidenav-fill: var(--al-sys-color-surface);
	--al-sidenav-border-color: var(--al-sys-color-border);
	--al-sidenav-border-width: var(--al-sys-control-border-width);
	--al-sidenav-padding-x: var(--al-sys-space-100);
	--al-sidenav-padding-y: var(--al-sys-space-150);
	--al-sidenav-group-gap: var(--al-sys-space-300);
	--al-sidenav-switch-margin: var(--al-sys-space-100) var(--al-sidenav-padding-x);
	--al-sidenav-action-size: var(--al-sys-control-height-sm);
	--al-sidenav-action-opacity-rest: 0;
	--al-sidenav-action-opacity-shown: 1;
	--al-sidenav-mark-size: var(--al-sys-icon-size-sm);
	--al-sidenav-mark-rotation: 0deg;
	--al-sidenav-mark-image: var(--al-ref-icon-chevron-down);
	--al-sidenav-foot-border-width: var(--al-sidenav-rule-width);
	--al-sidenav-font-family: var(--al-sys-font-family);
	/* set, not inherited: a host page's 1.65 puts a 24px line box inside a 44px row,
	   and a tier the host page can change is not a tier. Same failure as borrowing
	   box-sizing, one property along. */
	--al-sidenav-line-height: var(--al-sys-line-height-control);

	/* -- the head: the workspace mark and its name -------------------------- */
	/* the same height as the top bar, derived the same way, so the two meet in one
	   straight line across the top of the screen instead of missing by 4px */
	--al-sidenav-head-height: calc(var(--al-sys-control-height-md) + var(--al-sys-space-300));
	--al-sidenav-head-color: var(--al-sys-color-text-primary);
	--al-sidenav-head-size: var(--al-sys-font-size-400);
	--al-sidenav-head-weight: var(--al-sys-font-weight-heading);
	--al-sidenav-mark-size: var(--al-sys-control-height-sm);
	--al-sidenav-mark-font-size: var(--al-sys-font-size-300);
	--al-sidenav-mark-fill: var(--al-sys-color-accent-primary);
	--al-sidenav-mark-color: var(--al-sys-color-text-on-accent);
	--al-sidenav-mark-radius: var(--al-sys-control-radius);

	/* -- the section heading ------------------------------------------------ */
	/* It sits at the 100 rung — the smallest the one type scale has — and takes its
	   emphasis from case and letter-spacing rather than from a size invented for it.
	   A heading off the ladder is how two screens end up disagreeing about 11px. */
	--al-sidenav-label-color: var(--al-sys-color-text-muted);
	--al-sidenav-label-size: var(--al-sys-font-size-100);
	--al-sidenav-label-weight: var(--al-sys-font-weight-control);
	--al-sidenav-label-tracking: 0.07em;
	/* asymmetric on purpose: a heading sits closer to the rows it names than to the
	   group above it, so proximity does the grouping and no divider has to */
	--al-sidenav-label-padding-top: var(--al-sys-space-50);
	--al-sidenav-label-padding-bottom: var(--al-sys-space-100);
	/* THE RULE BETWEEN GROUPS, off at both ends here. In the expanded nav the section
	   headings and the air between them already say where one group stops, so there is
	   nothing left for a line to do. The rail, which has no headings, turns it on by
	   re-pointing these two. Holding it as a width of 0 rather than as a rule the rail
	   owns is what keeps the base in charge of the seam. */
	--al-sidenav-rule-width: 0;
	--al-sidenav-rule-space: 0;

	/* -- a row --------------------------------------------------------------- */
	/* a nav item is a list ROW, not a control, so it takes the row tier */
	--al-sidenav-item-height: var(--al-sys-row-height-md);
	--al-sidenav-item-padding-x: var(--al-sys-space-150);
	--al-sidenav-item-radius: var(--al-sys-radius-sm);
	--al-sidenav-item-gap: var(--al-sys-space-150);
	--al-sidenav-item-font-size: var(--al-sys-font-size-300);
	--al-sidenav-item-font-weight: var(--al-sys-font-weight-medium);
	--al-sidenav-item-color: var(--al-sys-color-text-secondary);
	--al-sidenav-item-fill: transparent;
	--al-sidenav-item-marker: transparent; /* only the chosen row grows one */
	--al-sidenav-item-marker-width: 3px;
	--al-sidenav-item-marker-inset: var(--al-sys-space-100);
	--al-sidenav-item-marker-radius: var(--al-sys-radius-pill);
	--al-sidenav-icon-size: var(--al-sys-icon-size-lg);
	--al-sidenav-icon-color: var(--al-sys-color-text-muted);

	/* -- a nested child ------------------------------------------------------ */
	/* the guide line runs down the CENTRE of the parent's glyph column, so children
	   hang off the row they belong to rather than off the panel's edge */
	--al-sidenav-sub-display: block;
	--al-sidenav-sub-gutter: var(--al-sys-space-150);

	/* -- the count ----------------------------------------------------------- */

	/* -- what the rail moves, and the only things it moves ------------------- */
	--al-sidenav-text-display: block;
	--al-sidenav-justify: flex-start;

	--al-sidenav-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-sidenav-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-sidenav-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-sidenav-focus-ring-width: var(--al-sys-focus-ring-width);
	/* a nav row carries no intent, so it rings in the shared focus role — the same
	   split the input and the affix make against the button */
	--al-sidenav-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-sidenav-motion-duration: var(--al-sys-motion-duration-state);
	--al-sidenav-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-sidenav-* below this line --------------------- */
	box-sizing: border-box; /* the system owns its box model — see the button */
	display: flex;
	flex-direction: column;
	gap: var(--al-sidenav-group-gap);
	/* a floor, not a guess. Without it a flex parent squeezes the column down to its
	   content and the rail width stops meaning anything. */
	flex: none;
	width: var(--al-sidenav-width);
	height: 100%;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: 0 var(--al-sidenav-padding-x) var(--al-sidenav-padding-y);
	background: var(--al-sidenav-fill);
	border-right: var(--al-sidenav-border-width) solid var(--al-sidenav-border-color);
	font-family: var(--al-sidenav-font-family);
	line-height: var(--al-sidenav-line-height);
	transition: width var(--al-sidenav-motion-duration) var(--al-sidenav-motion-easing);
}

.al-sidenav-head {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: var(--al-sidenav-justify);
	gap: var(--al-sidenav-item-gap);
	/* the mark is wider than a row's glyph, so it is pulled back by half the
	   difference and the two share one centre line down the panel */
	padding: 0 calc(var(--al-sidenav-item-padding-x) + (var(--al-sidenav-icon-size) - var(--al-sidenav-mark-size)) / 2);
	flex: none;
	min-height: var(--al-sidenav-head-height);
	/* set, not inherited from the column. This box can GROW, and the promise it makes
	   is that it is exactly as tall as the top bar beside it: a line box it did not
	   choose is a promise it cannot keep. */
	line-height: var(--al-sidenav-line-height);
	color: var(--al-sidenav-head-color);
	font-size: var(--al-sidenav-head-size);
	font-weight: var(--al-sidenav-head-weight);
}
.al-sidenav-mark {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: var(--al-sidenav-mark-size);
	height: var(--al-sidenav-mark-size);
	border-radius: var(--al-sidenav-mark-radius);
	background: var(--al-sidenav-mark-fill);
	color: var(--al-sidenav-mark-color);
	font-size: var(--al-sidenav-mark-font-size);
	font-weight: var(--al-sidenav-head-weight);
}

.al-sidenav-group {
	box-sizing: border-box; /* it carries the seam's border and its padding in the rule
	     below, and box-sizing is not inherited. The system owns its box model rather
	     than borrowing whatever reset the host page happens to ship. */
	display: flex;
	flex-direction: column;
}
.al-sidenav-label {
	box-sizing: border-box;
	display: var(--al-sidenav-text-display);
	padding: var(--al-sidenav-label-padding-top) var(--al-sidenav-item-padding-x) var(--al-sidenav-label-padding-bottom);
	color: var(--al-sidenav-label-color);
	font-size: var(--al-sidenav-label-size);
	font-weight: var(--al-sidenav-label-weight);
	letter-spacing: var(--al-sidenav-label-tracking);
	text-transform: uppercase;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
/* WITH THE HEADINGS GONE, THE RAIL STILL HAS TO SHOW ITS SEAMS. A rule where a
   heading was is what stops seven glyphs reading as one undifferentiated stack.

   THE SEAM IS DECLARED HERE, IN THE BASE, and the rail only turns it on. Written the
   other way round, as .al-sidenav-rail .al-sidenav-group + .al-sidenav-group, the
   variant would own a rule of its own, and that is the one thing rule 8 forbids: the
   next thing this seam learns to do would reach the expanded nav and silently miss
   the rail, which is the same shape of bug as a variant forgetting a state. */
.al-sidenav-group + .al-sidenav-group {
	margin-top: var(--al-sidenav-rule-space);
	padding-top: var(--al-sidenav-rule-space);
	border-top: var(--al-sidenav-rule-width) solid var(--al-sidenav-border-color);
}

/* THE LOCATION SWITCH — a slot, not a control. What sits here is a recipe button: the
   current workspace as its label, a menu behind it. The nav owns only where it goes and
   that it spans the column, because a control that changes what the whole product is
   pointed at belongs above the navigation rather than inside it. */
.al-sidenav-switch {
	box-sizing: border-box;
	display: var(--al-sidenav-text-display);
	width: auto;
	margin: var(--al-sidenav-switch-margin);
}

/* A TRAILING ACTION ON A ROW — the pin, and whatever else a product hangs off a row.
   Also a slot: what goes in it is an ordinary quiet icon button. It is invisible at rest
   and appears on hover or focus, EXCEPT when it is switched on, because a pin that hides
   itself once pinned has thrown away the only signal it existed to give.

   Keyed off aria-pressed, so the pinned look and the pinned announcement cannot drift
   apart -- the same bargain the checkbox and the busy button strike. */
.al-sidenav-action {
	flex: none;
	width: var(--al-sidenav-action-size);
	height: var(--al-sidenav-action-size);
	opacity: var(--al-sidenav-action-opacity-rest);
	transition: opacity var(--al-sidenav-motion-duration) var(--al-sidenav-motion-easing);
}
.al-sidenav-item:hover .al-sidenav-action,
.al-sidenav-item:focus-within .al-sidenav-action,
.al-sidenav-action[aria-pressed='true'] {
	--al-sidenav-action-opacity-rest: var(--al-sidenav-action-opacity-shown);
}

/* A GROUP THAT OPENS AND SHUTS. The heading becomes a <button aria-expanded>, which is
   the whole change: a nav with nine groups needs it, and aria-expanded is what makes the
   mark's direction and the announcement one fact instead of two.

   The mark is a mask, so its colour comes from a token and a theme can move it. It turns
   by re-pointing a rotation token rather than by a second transform rule, so the day it
   learns to do something else there is one place to teach. */
.al-sidenav-label[aria-expanded] {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-sidenav-item-gap);
	width: 100%;
	border: 0;
	background: none;
	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	text-align: left;
	cursor: pointer;
}
.al-sidenav-label[aria-expanded]::after {
	content: '';
	flex: none;
	width: var(--al-sidenav-mark-size);
	height: var(--al-sidenav-mark-size);
	rotate: var(--al-sidenav-mark-rotation);
	background: currentColor;
	mask: var(--al-sidenav-mark-image) center / contain no-repeat;
	transition: rotate var(--al-sidenav-motion-duration) var(--al-sidenav-motion-easing);
}
.al-sidenav-label[aria-expanded='false'] ~ .al-sidenav-sub {
	display: none;
}
.al-sidenav-label[aria-expanded='true'] {
	--al-sidenav-mark-rotation: 180deg;
}

/* THE FOOT — an account block, a build number, whatever a product pins to the bottom.
   It does not scroll with the list, which is the only reason it needs naming: the nav
   above it scrolls, and a footer that scrolls away is not a footer. */
.al-sidenav-foot {
	box-sizing: border-box;
	flex: none;
	margin-top: auto;
	padding-top: var(--al-sidenav-padding-y);
	border-top: var(--al-sidenav-foot-border-width) solid var(--al-sidenav-border-color);
}

.al-sidenav-item {
	box-sizing: border-box;
	position: relative;
	display: flex;
	align-items: center;
	justify-content: var(--al-sidenav-justify);
	gap: var(--al-sidenav-item-gap);
	flex: none; /* a row keeps its tier when the column runs long — see the button */
	min-height: var(--al-sidenav-item-height);
	padding: 0 var(--al-sidenav-item-padding-x);
	border-radius: var(--al-sidenav-item-radius);
	background: var(--al-sidenav-item-fill);
	color: var(--al-sidenav-item-color);
	font-size: var(--al-sidenav-item-font-size);
	font-weight: var(--al-sidenav-item-font-weight);
	line-height: var(--al-sidenav-line-height);
	text-decoration: none;
	cursor: pointer;
	transition:
		background var(--al-sidenav-motion-duration) var(--al-sidenav-motion-easing),
		color var(--al-sidenav-motion-duration) var(--al-sidenav-motion-easing);
}
/* THE MARKER IS A SECOND CHANNEL, not decoration. Colour is never the only thing
   saying which page you are on: the chosen row carries a bar, a heavier weight and
   a tint, so it survives a colour-blind reader, a poor screen and a printout. */
.al-sidenav-item::before {
	content: '';
	position: absolute;
	left: 0;
	top: var(--al-sidenav-item-marker-inset);
	bottom: var(--al-sidenav-item-marker-inset);
	width: var(--al-sidenav-item-marker-width);
	border-radius: var(--al-sidenav-item-marker-radius);
	background: var(--al-sidenav-item-marker);
}
.al-sidenav-icon {
	flex: none; /* the label gives way, never the glyph */
	width: var(--al-sidenav-icon-size);
	height: var(--al-sidenav-icon-size);
	color: var(--al-sidenav-icon-color);
}
/* text-overflow has no effect on a bare text node inside a flex container —
   measured, not assumed — so the label is wrapped in something. min-width:0 is what
   lets a flex item shrink below its content width; without it no ellipsis appears. */
.al-sidenav-text {
	display: var(--al-sidenav-text-display);
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* THE NESTED LIST. Child rows need no class of their own — being inside the sub
   list is what makes them children — and this rule re-points three tokens rather
   than restating a row. One less thing for an author to remember and get wrong. */
.al-sidenav-sub {
	box-sizing: border-box; /* it carries the guide line and the gutter beside it */
	display: var(--al-sidenav-sub-display);
	/* DERIVED IN THE PROPERTY, not held as a token of its own: the indent has no value
	   worth naming — it is wherever the middle of the glyph column happens to be. Written
	   as a number it would come unstuck the first time the padding or the icon tier
	   moved, and nothing re-points it, so a token would only be a second place to look. */
	margin-left: calc(var(--al-sidenav-item-padding-x) + var(--al-sidenav-icon-size) / 2);
	padding-left: var(--al-sidenav-sub-gutter);
	/* No guide line down the nested list. It was drawn here, and against a real nav it
	   read as an extra edge next to the selected row rather than as structure: the
	   indent and the smaller row tier already say these are children. */
}
.al-sidenav-sub .al-sidenav-item {
	--al-sidenav-item-height: var(--al-sys-row-height-sm);
	--al-sidenav-item-font-size: var(--al-sys-font-size-200);
	--al-sidenav-item-padding-x: var(--al-sys-space-100);
}

/* -- STATES. Each one re-points tokens; not one of them sets a property. --------
   Hover promotes EVERY mark in the row at once. On a dark ground the active tint
   lands lighter than the panel under it and secondary ink on it measures 3.27 —
   only the primary rung clears the floor there, at 7.00. */
.al-sidenav-item:hover {
	--al-sidenav-item-fill: var(--al-sys-color-row-tint-active);
	--al-sidenav-item-color: var(--al-sys-color-text-primary);
	--al-sidenav-icon-color: var(--al-sys-color-text-primary);
}

/* WHERE YOU ARE rides on aria-current, not a class, for the same reason the button
   rides on aria-busy: you cannot style it without also announcing it. It ties with
   :hover on specificity and the order settles it, which is what lets the chosen row
   keep its accent glyph while the pointer is over it. */
.al-sidenav-item[aria-current='page'] {
	--al-sidenav-item-fill: var(--al-sys-color-row-tint-selected);
	--al-sidenav-item-color: var(--al-sys-color-text-primary);
	--al-sidenav-item-font-weight: var(--al-sys-font-weight-control);
	--al-sidenav-icon-color: var(--al-sys-color-accent-primary-line);
	--al-sidenav-item-marker: var(--al-sys-color-accent-primary-line);
}
.al-sidenav-item[aria-current='page']:hover {
	--al-sidenav-item-fill: var(--al-sys-color-row-tint-active);
}

/* the same three-layer ring as every other control: a light hairline, the ring, a
   dark hairline. One of the two always separates it from what is behind, so the
   indicator cannot vanish — including on a nav the colour of the ring itself. */
.al-sidenav-item:focus {
	outline: none;
}
.al-sidenav-item:focus-visible {
	/* forced-colors throws box-shadow away; a transparent outline is invisible here
	   and repainted in a system colour there, so the ring survives that mode */
	outline: var(--al-sidenav-focus-ring-width) solid transparent;
	outline-offset: var(--al-sidenav-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-sidenav-focus-hairline-width) var(--al-sidenav-focus-hairline-light),
		0 0 0 calc(var(--al-sidenav-focus-hairline-width) + var(--al-sidenav-focus-ring-width)) var(--al-sidenav-focus-ring-color),
		0 0 0 calc(var(--al-sidenav-focus-hairline-width) * 2 + var(--al-sidenav-focus-ring-width)) var(--al-sidenav-focus-hairline-dark);
}

/* -- THE RAIL. Seven re-points, no properties. ---------------------------------
   The counts go with the text rather than shrinking to a dot: 142 does not fit in a
   64px column, and a dot that cannot say the number is a different fact from the one
   the expanded nav is showing. A rail row carries title="" instead, which is honest,
   costs nothing and works from the keyboard.
   The child lists go too — a nested row with no glyph has nothing left to be. */
.al-sidenav-rail {
	--al-sidenav-width: 64px;
	--al-sidenav-text-display: none;
	--al-sidenav-sub-display: none;
	--al-sidenav-justify: center;
	/* the seam the base leaves off. With the headings gone something has to say where
	   one section ends, and this is the rail turning on a rule it does not own. */
	--al-sidenav-rule-width: var(--al-sys-control-border-width);
	--al-sidenav-rule-space: var(--al-sys-space-100);
}

/* =============================================================================
   2. THE TOP BAR

   The band across the top of a screen: what the page is, and the handful of controls
   that act on the whole of it. It is sticky by default — that is the entire reason it
   is a component rather than a div — and it reads --al-sys-layer-sticky so it never
   needs a z-index invented for it in a page file.

   HOW IT KNOWS CONTENT IS UNDER IT. At rest it casts no shadow; the hairline under it
   is enough while the page is at the top. Once the page has scrolled it lifts. Two
   ways in, and they do not fight:

     - Where the browser has scroll-driven animations, the bar does it itself and the
       app writes no JavaScript at all. That matters here: this is a reskin, and a
       chrome change that needs a controller change is a chrome change that does not
       get made.
     - Anywhere else, or wherever the app would rather own it, set data-scrolled to
       "true" or "false". Writing the attribute AT ALL stands the CSS animation down,
       so whichever one is in charge is the only one in charge.
   ============================================================================= */

.al-topbar {
	/* Derived, not picked: one medium control plus 12px of air above and below. A bar
	   sized off the thing it holds cannot drift away from it, which is the same
	   reasoning that gave the row heights their numbers. */
	--al-topbar-height: calc(var(--al-sys-control-height-md) + var(--al-sys-space-300));
	--al-topbar-fill: var(--al-sys-color-surface);
	--al-topbar-border-color: var(--al-sys-color-border);
	--al-topbar-border-width: var(--al-sys-control-border-width);
	--al-topbar-padding-x: var(--al-sys-space-300);
	--al-topbar-gap: var(--al-sys-space-200);
	--al-topbar-layer: var(--al-sys-layer-sticky);
	--al-topbar-shadow: none; /* nothing has passed under it yet */
	/* the same sticky rung, held as a second token because a keyframe is a PROPERTY
	   context and a property may only read this component's own tokens */
	--al-topbar-shadow-lifted: var(--al-sys-elevation-sticky);
	--al-topbar-lift-distance: var(--al-sys-space-300);
	--al-topbar-title-color: var(--al-sys-color-text-primary);
	--al-topbar-title-size: var(--al-sys-font-size-500);
	--al-topbar-title-weight: var(--al-sys-font-weight-heading);
	--al-topbar-search-width: 320px;
	--al-topbar-search-min-width: 160px; /* the floor it stops giving way at */
	--al-topbar-title-max-width: 45%; /* past this a long title is eating the bar */
	--al-topbar-font-family: var(--al-sys-font-family);
	--al-topbar-line-height: var(--al-sys-line-height-control);
	--al-topbar-motion-duration: var(--al-sys-motion-duration-state);
	--al-topbar-motion-easing: var(--al-sys-motion-easing);

	/* -- properties: only --al-topbar-* below this line ---------------------- */
	box-sizing: border-box;
	position: sticky;
	top: 0;
	z-index: var(--al-topbar-layer);
	display: flex;
	align-items: center;
	gap: var(--al-topbar-gap);
	flex: none;
	min-height: var(--al-topbar-height);
	padding: 0 var(--al-topbar-padding-x);
	background: var(--al-topbar-fill);
	border-bottom: var(--al-topbar-border-width) solid var(--al-topbar-border-color);
	box-shadow: var(--al-topbar-shadow);
	font-family: var(--al-topbar-font-family);
	line-height: var(--al-topbar-line-height);
	transition: box-shadow var(--al-topbar-motion-duration) var(--al-topbar-motion-easing);
}
.al-topbar[data-scrolled='true'] {
	--al-topbar-shadow: var(--al-sys-elevation-sticky);
}
/* :not([data-scrolled]) is the whole of the handshake. An app that writes the
   attribute — either value — is saying it owns this, and the animation stands down
   rather than the two of them arguing over one shadow. */
@supports (animation-timeline: scroll()) {
	.al-topbar:not([data-scrolled]) {
		animation: al-topbar-lift linear both;
		animation-timeline: scroll(nearest block);
		animation-range: 0 var(--al-topbar-lift-distance);
	}
}
@keyframes al-topbar-lift {
	from {
		box-shadow: none;
	}
	to {
		box-shadow: var(--al-topbar-shadow-lifted);
	}
}

/* WHAT GIVES WAY, AND IN WHAT ORDER. Three items share one line and they cannot all
   be flexible, so the priority is stated rather than left to flexbox's proportional
   shrink — which is what produced "Ba…" in a bar with 70px to spare, because the title
   took its share of the shortfall alongside a field ten times its width.

   The title does not shrink at all; it is capped instead, so a title long enough to eat
   the bar is the only one that ever truncates. The field gives way down to its floor.
   The actions never move: losing half of "Submit to BioTrack" is worse than losing half
   a heading. */
.al-topbar-title {
	flex: 0 0 auto;
	min-width: 0;
	max-width: var(--al-topbar-title-max-width);
	margin: 0; /* it is usually an h1, and an h1 arrives carrying margins */
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--al-topbar-title-color);
	font-size: var(--al-topbar-title-size);
	font-weight: var(--al-topbar-title-weight);
	line-height: var(--al-topbar-line-height);
}
/* the search slot exists so that every page does not invent its own width for the one
   field that lives in the bar. It is the only thing on this line that gives way, and it
   stops at a floor rather than at zero — a search box narrower than a search term is
   not a smaller control, it is a broken one. */
.al-topbar-search {
	flex: 0 1 var(--al-topbar-search-width);
	min-width: var(--al-topbar-search-min-width);
}
/* everything after this hugs the right edge. margin-left:auto rather than a spacer
   element, so the markup stays a list of the things that are actually there. */
.al-topbar-actions {
	display: flex;
	align-items: center;
	gap: var(--al-topbar-gap);
	flex: none;
	margin-left: auto;
}

/* =============================================================================
   3. THE MENU

   A list of actions opened by a control. It is a RAISED surface — it sits ON the page
   rather than in it — so it reads --al-sys-color-surface-raised and
   --al-sys-elevation-raised, both of which the grounds already re-point, and it goes
   in the top layer via popover so no ancestor with overflow:hidden can clip it. That
   last one is not a nicety: a menu opened from a toolbar inside a scroll pane is
   exactly where a positioned div gets cut in half.

   NO role="menu" HERE, DELIBERATELY. The ARIA menu pattern promises arrow-key roving
   focus, and announcing a menu whose arrow keys do nothing is worse than announcing
   nothing at all. Plain buttons in a popover are already keyboard-operable: Tab
   moves, Enter fires, Escape closes, and focus returns to the trigger. An app that
   does implement roving focus adds the roles on top of this markup and nothing in the
   CSS has to change.

   The surface works with or without the popover attribute. With it, it is a menu.
   Without it, it is the same list of actions laid inline — which is how the specimen
   in the docs can be a real one rather than a picture of one.
   ============================================================================= */

.al-menu {
	--al-menu-fill: var(--al-sys-color-surface-raised);
	--al-menu-shadow: var(--al-sys-elevation-raised);
	/* THE HAIRLINE IS THE INK, FADED, and it is the one value in this file that does not
	   read a colour role straight. --al-sys-color-border is measured against the page
	   surface, and a menu is the only thing in the system standing on the RAISED one —
	   which on a dark ground is slate-700, the same value the border role is. Rendered
	   before this line existed: the menu's edge and its separator both disappeared into
	   its own fill. Deriving the hairline from the ink instead follows the ground by
	   construction, because the ink always contrasts with what it is written on. One
	   token, both jobs: the edge around the menu and the rule between its groups. */
	--al-menu-hairline-color: color-mix(in srgb, var(--al-sys-color-text-primary) 12%, transparent);
	--al-menu-border-width: var(--al-sys-control-border-width);
	/* a container is rounder than the rows inside it — 12 outside, 5 in — which is
	   what stops a menu reading as one very large button */
	--al-menu-radius: var(--al-sys-radius-container);
	--al-menu-padding: var(--al-sys-space-100);
	--al-menu-min-width: 220px;
	--al-menu-max-width: 340px;
	--al-menu-max-height: 320px;
	--al-menu-offset: var(--al-sys-space-50); /* the air between trigger and menu */
	--al-menu-layer: var(--al-sys-layer-popover);
	--al-menu-label-color: var(--al-sys-color-text-muted);
	--al-menu-label-size: var(--al-sys-font-size-100);
	--al-menu-label-weight: var(--al-sys-font-weight-control);
	--al-menu-label-tracking: 0.07em;
	/* same asymmetry as the nav's heading, and for the same reason */
	--al-menu-label-padding-top: var(--al-sys-space-50);
	--al-menu-label-padding-x: var(--al-sys-space-100);
	--al-menu-label-padding-bottom: var(--al-sys-space-100);
	--al-menu-sep-width: var(--al-sys-control-border-width);
	--al-menu-sep-space: var(--al-sys-space-50);
	--al-menu-font-family: var(--al-sys-font-family);
	--al-menu-line-height: var(--al-sys-line-height-control);
	--al-menu-motion-duration-enter: var(--al-sys-motion-duration-enter);
	--al-menu-motion-duration-exit: var(--al-sys-motion-duration-exit);
	--al-menu-motion-easing-enter: var(--al-sys-motion-easing-enter);
	--al-menu-motion-easing-exit: var(--al-sys-motion-easing-exit);
	--al-menu-enter-scale: 0.97;

	/* -- properties: only --al-menu-* below this line ------------------------ */
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	width: max-content;
	min-width: var(--al-menu-min-width);
	max-width: var(--al-menu-max-width);
	max-height: var(--al-menu-max-height);
	overflow-y: auto;
	overscroll-behavior: contain; /* a long menu does not scroll the page behind it */
	padding: var(--al-menu-padding);
	background: var(--al-menu-fill);
	border: var(--al-menu-border-width) solid var(--al-menu-hairline-color);
	border-radius: var(--al-menu-radius);
	box-shadow: var(--al-menu-shadow);
	font-family: var(--al-menu-font-family);
	line-height: var(--al-menu-line-height);
}

/* THE POPOVER FORM. The UA stylesheet hands [popover] a border, a padding, a fill and
   inset:0 with auto margins, which would centre the menu on the screen; all of it is
   undone here. position:fixed with inset:auto falls back to the element's static
   position, so a menu written next to its trigger still lands under it in a browser
   without anchor positioning. */
.al-menu[popover] {
	position: fixed;
	inset: auto;
	margin: 0;
	z-index: var(--al-menu-layer); /* moot in the top layer, and correct the moment
	     someone renders this list without the popover attribute */
	opacity: 1;
	transform: none;
	/* allow-discrete is what lets display and overlay animate, which is the
	   difference between a menu that fades out and one that blinks out */
	transition:
		opacity var(--al-menu-motion-duration-exit) var(--al-menu-motion-easing-exit),
		transform var(--al-menu-motion-duration-exit) var(--al-menu-motion-easing-exit),
		overlay var(--al-menu-motion-duration-exit) allow-discrete,
		display var(--al-menu-motion-duration-exit) allow-discrete;
}
/* ANCHORING, WHERE IT EXISTS. A popover opened through popovertarget gets its invoker
   as its implicit anchor, so nothing has to be named on either side — which is what
   stops two menus on one page colliding over a shared anchor-name. The fallbacks are
   what keep a menu near the bottom of the window on screen. */
@supports (anchor-name: --al-menu) {
	.al-menu[popover] {
		position-area: block-end span-inline-end;
		position-try-fallbacks: flip-block, flip-inline;
		margin: var(--al-menu-offset);
	}
}
.al-menu[popover]:popover-open {
	transition:
		opacity var(--al-menu-motion-duration-enter) var(--al-menu-motion-easing-enter),
		transform var(--al-menu-motion-duration-enter) var(--al-menu-motion-easing-enter),
		overlay var(--al-menu-motion-duration-enter) allow-discrete,
		display var(--al-menu-motion-duration-enter) allow-discrete;
}
/* A CLOSED POPOVER IS NOT ON THE PAGE. The UA stylesheet sets display:none for
   [popover]:not(:popover-open), and .al-menu's own display:flex is an author rule, so it
   won that fight everywhere: a shut menu stayed laid out, position:fixed and transparent,
   and ate the clicks of whatever sat under it. Measured on the batch list, where a
   213x17 invisible box covered Print and Clear selection.

   display belongs in THIS rule rather than as pointer-events elsewhere, because the
   transition above already lists `display ... allow-discrete`: the browser holds the
   display change until the fade finishes, so the menu still leaves rather than blinks. */
.al-menu[popover]:not(:popover-open) {
	display: none;
	opacity: 0;
	transform: scale(var(--al-menu-enter-scale));
}
@starting-style {
	.al-menu[popover]:popover-open {
		opacity: 0;
		transform: scale(var(--al-menu-enter-scale));
	}
}

/* the same shape as the nav's section heading, and for the same reason: the 100 rung
   with case and tracking doing the emphasis, not a size off the ladder */
.al-menu-label {
	box-sizing: border-box;
	padding: var(--al-menu-label-padding-top) var(--al-menu-label-padding-x) var(--al-menu-label-padding-bottom);
	color: var(--al-menu-label-color);
	font-size: var(--al-menu-label-size);
	font-weight: var(--al-menu-label-weight);
	letter-spacing: var(--al-menu-label-tracking);
	text-transform: uppercase;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
/* an <hr>, so a screen reader gets the break too. Its own border and margins have to
   go first, or the browser's inset 3D rule shows through underneath. */
.al-menu-sep {
	box-sizing: border-box; /* a height AND a border, which is exactly the pair that
	     measures differently on a page with no reset */
	flex: none;
	height: 0;
	margin: var(--al-menu-sep-space) 0;
	border: 0;
	border-top: var(--al-menu-sep-width) solid var(--al-menu-hairline-color);
}

.al-menu-item {
	/* declared on the ITEM rather than on .al-menu because the destructive variant
	   sits on the item, and a variant can only re-point tokens it can reach */
	--al-menu-item-height: var(--al-sys-row-height-sm);
	--al-menu-item-padding-x: var(--al-sys-space-100);
	--al-menu-item-radius: var(--al-sys-radius-sm);
	--al-menu-item-gap: var(--al-sys-space-150);
	--al-menu-item-font-size: var(--al-sys-font-size-300);
	--al-menu-item-font-weight: var(--al-sys-font-weight-medium);
	/* the PRIMARY ink rung, not secondary. A menu is a raised surface, which on a dark
	   ground is lighter than the panel behind it, and secondary ink on the hover tint
	   there measures 3.27 against a 4.5 floor. */
	--al-menu-item-color: var(--al-sys-color-text-primary);
	--al-menu-item-fill: transparent;
	--al-menu-icon-size: var(--al-sys-icon-size-md);
	--al-menu-icon-color: var(--al-sys-color-text-muted);
	--al-menu-key-color: var(--al-sys-color-text-muted);
	--al-menu-key-size: var(--al-sys-font-size-200);
	--al-menu-item-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-menu-item-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-menu-item-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-menu-item-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-menu-item-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-menu-item-focus-ring-color: var(--al-sys-color-focus-ring);
	--al-menu-item-motion-duration: var(--al-sys-motion-duration-state);
	--al-menu-item-motion-easing: var(--al-sys-motion-easing);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-menu-item-gap);
	flex: none;
	width: 100%;
	min-height: var(--al-menu-item-height);
	padding: 0 var(--al-menu-item-padding-x);
	border: 0;
	border-radius: var(--al-menu-item-radius);
	background: var(--al-menu-item-fill);
	color: var(--al-menu-item-color);
	font-family: inherit;
	font-size: var(--al-menu-item-font-size);
	font-weight: var(--al-menu-item-font-weight);
	line-height: var(--al-menu-line-height);
	text-align: left;
	text-decoration: none;
	cursor: pointer;
	appearance: none;
	-webkit-appearance: none;
	transition:
		background var(--al-menu-item-motion-duration) var(--al-menu-item-motion-easing),
		color var(--al-menu-item-motion-duration) var(--al-menu-item-motion-easing);
}
.al-menu-icon {
	flex: none;
	width: var(--al-menu-icon-size);
	height: var(--al-menu-icon-size);
	font-size: var(--al-menu-icon-size);
	line-height: 1;
	color: var(--al-menu-icon-color);
}
.al-menu-text {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
/* the shortcut is a HINT, so it never grows and it never wins an argument about
   width — the name of the action is the thing being read. */
.al-menu-key {
	box-sizing: border-box; /* it has padding, and box-sizing is not inherited */
	flex: none;
	padding-left: var(--al-menu-item-gap);
	color: var(--al-menu-key-color);
	font-size: var(--al-menu-key-size);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* HOVER AND KEYBOARD FOCUS ARE ONE STATE HERE. A menu has no chosen row, so there is
   nothing for a second tint to say, and the pointer and the keyboard should not put a
   person in two different-looking places. Every mark in the row promotes together —
   see the measurement at the top of this file. */
.al-menu-item:hover:not(:disabled),
.al-menu-item:focus-visible:not(:disabled) {
	--al-menu-item-fill: var(--al-sys-color-row-tint-active);
	--al-menu-item-color: var(--al-sys-color-text-primary);
	--al-menu-icon-color: var(--al-sys-color-text-primary);
	--al-menu-key-color: var(--al-sys-color-text-primary);
}
.al-menu-item:focus {
	outline: none;
}
.al-menu-item:focus-visible {
	outline: var(--al-menu-item-focus-ring-width) solid transparent;
	outline-offset: var(--al-menu-item-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-menu-item-focus-hairline-width) var(--al-menu-item-focus-hairline-light),
		0 0 0 calc(var(--al-menu-item-focus-hairline-width) + var(--al-menu-item-focus-ring-width)) var(--al-menu-item-focus-ring-color),
		0 0 0 calc(var(--al-menu-item-focus-hairline-width) * 2 + var(--al-menu-item-focus-ring-width)) var(--al-menu-item-focus-hairline-dark);
}
.al-menu-item:disabled {
	opacity: var(--al-menu-item-disabled-opacity);
	cursor: not-allowed;
	pointer-events: none;
}

/* -- DESTRUCTIVE. One variant, tokens only. ------------------------------------
   At rest it reads the -strong rung, not -line: a menu floats on the raised surface,
   which is lighter than the page on a dark ground, and -line was measured against
   slate-900. -strong moves away from the ground on both sides — 6.40 on white, 5.42
   on slate-700.

   Its hover is a SOLID fill rather than a tint, because a translucent red under a red
   label moves the ground toward the label: 20% over white measured 3.77. The accent
   FILL role holds still between grounds, so white on it is 5.01 on both. */
.al-menu-item-danger {
	--al-menu-item-color: var(--al-sys-color-accent-danger-strong);
	--al-menu-icon-color: var(--al-sys-color-accent-danger-strong);
	--al-menu-item-focus-ring-color: var(--al-sys-color-accent-danger-line);
}
.al-menu-item-danger:hover:not(:disabled),
.al-menu-item-danger:focus-visible:not(:disabled) {
	--al-menu-item-fill: var(--al-sys-color-accent-danger);
	--al-menu-item-color: var(--al-sys-color-text-on-accent-danger);
	--al-menu-icon-color: var(--al-sys-color-text-on-accent-danger);
	--al-menu-key-color: var(--al-sys-color-text-on-accent-danger);
}

/* =============================================================================
   REDUCED MOTION — keep only what carries meaning.

   Nothing that moves in this file is telling anyone anything: the rail's width tween,
   the bar's shadow fade and the menu's arrival are all polish, so they go. The menu
   still opens and closes; it just does it at once.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-sidenav,
	.al-sidenav-item,
	.al-topbar,
	.al-menu-item {
		transition: none;
	}
	.al-menu[popover],
	.al-menu[popover]:popover-open {
		transition: none;
	}
	.al-menu[popover]:not(:popover-open) {
		transform: none;
	}
}

/* ---- containers ---------------------------------------------------- */
/* =============================================================================
   CONTAINERS: the page ground, the card, the panel, the divider, the accordion.

   These are the first things in the system that HOLD other things, so they are the
   first consumers of --al-sys-radius-container and --al-sys-color-surface-page.
   A container is ROUNDER than the controls inside it: 12px around an 8px button is
   what stops a card reading as one very large button.

   CARD OR PANEL. The question a screen asks before it asks anything else.

     A PANEL is part of the page: a heading, a rule under it, and the content below.
     It paints nothing and draws no edge. It says "this region of the screen is about
     transfers" and then gets out of the way.

     A CARD is a thing ON the page: it paints a ground, draws an edge, rounds its
     corners and can carry its own actions and its own footer. It holds ONE object you
     could point at: a batch, an order, a terminal, a licence.

     The test: if you cannot name the single thing inside it, it is a panel. Reaching
     for a card because the content "needs to look grouped" is how a screen ends up as
     nine boxes with no hierarchy, which reads as clutter rather than as structure.

   NOTHING HERE IS CLICKABLE AS A WHOLE, and that is a decision rather than an
   omission. A card that is one big target hides where the real action is, gives a
   keyboard user one stop for six pieces of content, and forces every button inside it
   to stop the click from bubbling. A card carries a link or a button; it is not one.

   NO ELEVATION AT REST. The system's three shadows all mean "off the page": sticky
   (content passes under me), raised (a menu floating over the page) and overlay (a
   modal). A card is not off the page, it is on it, so it is drawn with a ground and a
   hairline and no shadow at all. That also means a card reads on a dark ground, where
   a shadow does almost nothing, without a single dark rule being written.

   GROUNDS. Not one selector below names a control inside .al-on-dark or .al-on-light.
   Every colour here reads a shared sys role, so a card, a panel, a divider and an
   accordion are correct on a white page and on a dark bar with no work of their own.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   THE PAGE GROUND

   Somebody has to paint the thing a card sits on, and until now nothing did, which
   is why --al-sys-color-surface-page has sat in the system with no consumer. A white
   card on a white body is a rectangle you can only find by its hairline.

   It carries padding as well as a fill, because a ground with content jammed against
   its edge is not a ground anyone would ship, and the alternative is every screen
   inventing its own number for the same gap.

   It does NOT name a ground of its own. The page fill is a ground-aware role, so this
   paints slate-50 on a light screen and the darker page ink inside .al-on-dark. To
   build a dark screen, put .al-on-dark on this same element.
   ----------------------------------------------------------------------------- */
.al-page {
	--al-page-fill: var(--al-sys-color-surface-page);
	--al-page-padding: var(--al-sys-space-300);
	/* THE GROUND STATES THE INK, and this is not decoration. Colour inherits as a
	   COMPUTED value, so text that inherited slate ink from an outer element keeps that
	   exact colour inside .al-on-dark no matter how many roles the ground re-points.
	   Nothing can reach it except an element that sets `color` again, and the element
	   that paints the ground is the one in a position to. Measured on a dark strip:
	   bare copy stayed dark-on-dark until this line existed. The strongest ink is the
	   right default here, because everything below can quieten itself and a card and a
	   panel both do. */
	--al-page-text-color: var(--al-sys-color-text-primary);

	box-sizing: border-box;
	background: var(--al-page-fill);
	padding: var(--al-page-padding);
	color: var(--al-page-text-color);
}

/* =============================================================================
   THE CARD
   ============================================================================= */
.al-card {
	--al-card-fill: var(--al-sys-color-surface);
	/* the card's edge is a hairline BETWEEN things, not the edge of a control. The
	   control rung is deliberately much darker because it has to be findable, and a
	   card wearing it looks like a giant input. */
	--al-card-border-color: var(--al-sys-color-border);
	/* 1px, held here rather than pointed at --al-sys-control-border-width. Same number,
	   different meaning: that token is the edge of a control, and a card is not one.
	   Borrowing it would tie a card's edge to a change made for buttons. */
	--al-card-border-width: 1px;
	--al-card-radius: var(--al-sys-radius-container);

	/* The same inset for all three parts, so the title, the content and the footer
	   buttons stand on the same left edge. That alignment is most of what makes a card
	   look built rather than assembled.

	   The body's inset is a SEPARATE token holding the same value, not an alias of the
	   one above. Two reasons, and the second is the load-bearing one: aliasing inside a
	   tier is the thing rule 2 forbids, and the flush variant has to move the body's
	   inset without moving the heading's. Written as one token, flush zeroed all three
	   and the heading walked to the edge with the table. */
	--al-card-padding-x: var(--al-sys-space-300);
	--al-card-body-padding-x: var(--al-sys-space-300);
	--al-card-body-padding-y: var(--al-sys-space-300);
	/* The header and footer are rows of controls, not prose, so they sit tighter than
	   the body. 12px around a 36px button gives a 60px bar; 24px gave 84 and made every
	   card look like it was mostly chrome. */
	--al-card-header-padding-y: var(--al-sys-space-150);
	--al-card-footer-padding-y: var(--al-sys-space-150);

	--al-card-divider-color: var(--al-sys-color-border);
	--al-card-divider-width: 1px;

	--al-card-gap: var(--al-sys-space-200); /* title block to actions */
	--al-card-actions-gap: var(--al-sys-space-100); /* button to button */
	--al-card-spacing: var(--al-sys-space-300); /* card to card, when they stack */

	--al-card-title-color: var(--al-sys-color-text-primary);
	--al-card-title-size: var(--al-sys-font-size-400);
	--al-card-title-weight: var(--al-sys-font-weight-heading);
	--al-card-title-line-height: var(--al-sys-line-height-heading);

	/* one quiet-text decision, used twice: the line under the title, and the note beside
	   the footer buttons. Two tokens under two names would let them drift apart, and
	   nobody has ever wanted them to. */
	--al-card-meta-color: var(--al-sys-color-text-muted);
	--al-card-meta-size: var(--al-sys-font-size-200);

	--al-card-text-color: var(--al-sys-color-text-secondary);
	--al-card-font-size: var(--al-sys-font-size-300);
	--al-card-font-family: var(--al-sys-font-family);
	/* set, not inherited. A host page at 1.65 makes the body of a card taller than the
	   same card on a page at 1.4. That is the same failure as borrowing box-sizing. */
	--al-card-line-height: var(--al-sys-line-height-body);

	/* visible by default, so a focus ring on a control near the card's edge is not
	   clipped by the card's own rounded corner. The flush variant flips it, and the
	   comment there says what that buys. */
	--al-card-overflow: visible;

	/* -- properties: only --al-card-* below this line ------------------------- */
	box-sizing: border-box;
	/* A COLUMN, not a block. Put four cards in a row of a grid and their bodies are
	   different lengths; as a column the body takes the slack, so every footer in the
	   row lands on the same line. As blocks they stagger. */
	display: flex;
	flex-direction: column;
	background: var(--al-card-fill);
	border: var(--al-card-border-width) solid var(--al-card-border-color);
	border-radius: var(--al-card-radius);
	overflow: var(--al-card-overflow);
	color: var(--al-card-text-color);
	font-family: var(--al-card-font-family);
	font-size: var(--al-card-font-size);
	line-height: var(--al-card-line-height);
}

/* Cards stack with a gap of their own rather than relying on whatever holds them. The
   adjacent-sibling form is what keeps the last card from pushing a margin out through
   the bottom of the page ground. */
.al-card + .al-card {
	margin-top: var(--al-card-spacing);
}

.al-card-header {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-card-gap);
	padding: var(--al-card-header-padding-y) var(--al-card-padding-x);
	border-bottom: var(--al-card-divider-width) solid var(--al-card-divider-color);
}

/* The title and its meta line need a wrapper around the pair; a bare title does not.
   Same trade the button makes with its label: the burden goes on the rarer case, and
   the actions find their side with an auto margin either way. */
/* A TITLE OWNS ITS WHOLE APPEARANCE, not just its colour and size. A card title is
   almost always a real heading element, and headings are the most heavily decorated
   things on any page: rendered inside this documentation, whose own h3 rule is small,
   green and uppercase, every card title came out shouting. So margin, padding, border,
   case, tracking and the typeface are all stated. Same rule as box-sizing and line-height:
   a control that changes shape depending on the page it lands on is not a control. */
.al-card-title {
	box-sizing: border-box; /* it states a border, so it states its box model too */
	margin: 0;
	padding: 0;
	border: 0;
	text-transform: none;
	letter-spacing: normal;
	color: var(--al-card-title-color);
	font-family: var(--al-card-font-family);
	font-size: var(--al-card-title-size);
	font-weight: var(--al-card-title-weight);
	line-height: var(--al-card-title-line-height);
}

.al-card-meta {
	margin: 0;
	color: var(--al-card-meta-color);
	font-size: var(--al-card-meta-size);
	line-height: var(--al-card-line-height);
}

/* an auto margin rather than justify-content:space-between, so the actions still sit
   right when the header holds only a title, and still sit right when it holds a title,
   a meta line and a badge. */
.al-card-actions {
	flex: none;
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--al-card-actions-gap);
}

.al-card-body {
	box-sizing: border-box;
	/* takes the slack in a row of equal-height cards. See the column note above */
	flex: 1 1 auto;
	padding: var(--al-card-body-padding-y) var(--al-card-body-padding-x);
}

.al-card-footer {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: flex-end; /* actions sit right; a note takes the left with an auto
	     margin, so the common case needs no extra element */
	gap: var(--al-card-actions-gap);
	padding: var(--al-card-footer-padding-y) var(--al-card-padding-x);
	border-top: var(--al-card-divider-width) solid var(--al-card-divider-color);
}

.al-card-footer .al-card-meta {
	margin-right: auto;
}

/* -- VARIANTS. Token re-points only, so the parts, the rules and the alignment all
      follow without being restated. Order is load-bearing: base first. ---------- */

/* NO CHROME. For a card inside a card, or a card inside a modal whose body is already
   the card colour. Both of those otherwise draw a box inside a box, the single most
   common way a dense screen turns to soup. The header and footer rules stay, because
   with the box gone they are the only thing left saying these parts are one group.
   The width goes to zero as well as the colour: a transparent 1px border would still
   indent the content by a pixel and break alignment with the cards beside it. */
.al-card-flat {
	--al-card-fill: transparent;
	--al-card-border-color: transparent;
	--al-card-border-width: 0px;
	/* the side inset goes with the box. Rendered with it kept, a flat card inside a
	   card body sat 48px in from the edge: its own 24px on top of its parent's, so the
	   nested titles lined up with nothing. Flush against the parent's content they read
	   as parts of it, which is the whole point of taking the box away. The vertical
	   padding stays, because that is the rhythm between the parts. Both insets move
	   here, unlike the flush variant, which moves only the body's. */
	--al-card-padding-x: 0px;
	--al-card-body-padding-x: 0px;
}

/* CONTENT TO THE EDGE. A card whose body is a table or a list of rows. The padding
   goes and the card starts clipping, because content painted to the edge has to be cut
   by the card's own radius or it pokes square corners through a rounded box. The cost
   of the clip is that a focus ring on something at the very edge of a flush body gets
   trimmed; that bites only when a control sits hard against the boundary, and a square
   corner on every card in a grid bites always. */
/* Only the BODY's inset moves. The header and footer keep theirs, so a table runs
   edge to edge under a heading that still lines up with the rest of the screen. That
   is why the two insets are separate tokens: written as one, this variant could not
   say "the content goes flush" without also saying "the heading does". */
.al-card-flush {
	--al-card-body-padding-y: 0px;
	--al-card-body-padding-x: 0px;
	--al-card-overflow: hidden;
}

/* =============================================================================
   THE PANEL. A region of the page, not an object on it.

   No fill, no border, no radius, and no padding down the sides: a panel is part of the
   page, so it stands on the page's own ground and lines up with everything else there.
   All it owns is a heading, one rule, and the space around them.
   ============================================================================= */
.al-panel {
	--al-panel-title-color: var(--al-sys-color-text-primary);
	/* one rung above the card's title. A panel divides a screen and cards sit inside
	   that division, so the panel has to read as the larger thing. At the same size the
	   hierarchy would be carried by position alone. */
	--al-panel-title-size: var(--al-sys-font-size-500);
	--al-panel-title-weight: var(--al-sys-font-weight-heading);
	--al-panel-title-line-height: var(--al-sys-line-height-heading);

	/* the sentence under a section heading. Body size, not caption size: it is
	   explaining the region, and an explanation you have to squint at is decoration. */
	--al-panel-meta-color: var(--al-sys-color-text-muted);
	--al-panel-meta-size: var(--al-sys-font-size-300);

	--al-panel-rule-color: var(--al-sys-color-border);
	--al-panel-rule-width: 1px;

	--al-panel-gap: var(--al-sys-space-200); /* title block to actions */
	--al-panel-actions-gap: var(--al-sys-space-100);
	--al-panel-title-gap: var(--al-sys-space-50); /* title to its meta line */
	--al-panel-header-padding-bottom: var(--al-sys-space-150);
	--al-panel-body-padding-top: var(--al-sys-space-200);
	/* between sections of a page. Twice the gap between two cards, because a panel break
	   is a bigger break than a card break, and even spacing reads as one long list
	   rather than as regions. */
	--al-panel-spacing: var(--al-sys-space-600);

	--al-panel-text-color: var(--al-sys-color-text-secondary);
	--al-panel-font-size: var(--al-sys-font-size-300);
	--al-panel-font-family: var(--al-sys-font-family);
	--al-panel-line-height: var(--al-sys-line-height-body);

	box-sizing: border-box;
	display: block;
	color: var(--al-panel-text-color);
	font-family: var(--al-panel-font-family);
	font-size: var(--al-panel-font-size);
	line-height: var(--al-panel-line-height);
}

.al-panel + .al-panel {
	margin-top: var(--al-panel-spacing);
}

.al-panel-header {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-panel-gap);
	padding-bottom: var(--al-panel-header-padding-bottom);
	border-bottom: var(--al-panel-rule-width) solid var(--al-panel-rule-color);
}

/* the same reset as the card's title, and it was found the same way: this page styles
   h2 with a 2px rule above it, so every panel heading arrived wearing a black bar and
   the header's actions sat 22px above the words they belong to */
.al-panel-title {
	box-sizing: border-box; /* it states a border, so it states its box model too */
	margin: 0;
	padding: 0;
	border: 0;
	text-transform: none;
	letter-spacing: normal;
	color: var(--al-panel-title-color);
	font-family: var(--al-panel-font-family);
	font-size: var(--al-panel-title-size);
	font-weight: var(--al-panel-title-weight);
	line-height: var(--al-panel-title-line-height);
}

/* it sits ABOVE the rule, with the title. Put in the body it would land under the rule
   and read as the first line of the content instead of as the section's description. */
.al-panel-meta {
	margin: var(--al-panel-title-gap) 0 0;
	color: var(--al-panel-meta-color);
	font-size: var(--al-panel-meta-size);
	line-height: var(--al-panel-line-height);
}

.al-panel-actions {
	flex: none;
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--al-panel-actions-gap);
}

.al-panel-body {
	box-sizing: border-box;
	padding-top: var(--al-panel-body-padding-top);
}

/* =============================================================================
   THE DIVIDER

   A LAST RESORT, and the docs say so out loud: space separates two groups without
   adding a mark, and most rules that get drawn are there because the space was too
   small. A divider earns its place where space alone is ambiguous: two lists of the
   same shape in one column, or two groups of buttons in one toolbar.

   One class, two orientations, and the orientation is four token re-points rather than
   a second rule, so the colour, the thickness and the gap are decided once.
   ============================================================================= */
.al-divider {
	--al-divider-color: var(--al-sys-color-border);
	/* THE ONE LADDER this component gets, and it is spent on the thickness: both
	   orientations read it, so a hairline cannot be 1px lying down and 2px standing up.
	   The gap is NOT a second ladder. It reads the sys step directly in each of the two
	   places that need it, because rule 2 allows one ladder per component and a second
	   alias would be the exact thing it forbids. */
	--al-divider-thickness: 1px;

	--al-divider-width: 100%;
	--al-divider-height: var(--al-divider-thickness);
	--al-divider-margin: var(--al-sys-space-200) 0;

	box-sizing: border-box;
	/* painted as a background rather than as a border, so one set of tokens draws both
	   orientations and an <hr> comes out identical to a <div role="separator"> */
	border: 0;
	background: var(--al-divider-color);
	width: var(--al-divider-width);
	height: var(--al-divider-height);
	margin: var(--al-divider-margin);
	/* a flex container would otherwise crush a 1px rule to nothing, and the failure is
	   silent: the rule is still in the DOM and still has its colour */
	flex: none;
	/* in a flex ROW this is what gives a vertical divider the row's full height */
	align-self: stretch;
}

.al-divider-vertical {
	--al-divider-width: var(--al-divider-thickness);
	/* auto, so align-self:stretch above can take the height of the row. Outside a flex
	   row there is nothing to stretch to and a vertical divider has no length, which is
	   the honest answer, since it only means anything inside a row. */
	--al-divider-height: auto;
	--al-divider-margin: 0 var(--al-sys-space-200);
}

/* =============================================================================
   THE ACCORDION

   Built on <details> and <summary>, so it works with no script at all: the keyboard,
   screen readers, find-in-page and the open state itself all come from the browser,
   and there is no aria-expanded for a component to get out of step with.

   ONE AT A TIME is the name attribute. A <details name="manifest"> makes a group
   exclusive, natively. A browser that does not support it lets several stand open,
   which is a degradation nobody notices rather than a broken control.

   WHEN IT EARNS ITS PLACE: content most people do not need most of the time. Hiding
   something everyone needs behind a click is this control's failure mode, and an
   accordion whose sections are all open on arrival should have been a page.
   ============================================================================= */
.al-accordion {
	--al-accordion-fill: var(--al-sys-color-surface);
	--al-accordion-border-color: var(--al-sys-color-border);
	--al-accordion-border-width: 1px;
	--al-accordion-radius: var(--al-sys-radius-container);
	--al-accordion-divider-color: var(--al-sys-color-border);
	--al-accordion-divider-width: 1px;

	/* a summary is a ROW you click, so it takes a row height rather than a control
	   height. 44px is the rung a list row uses, which is what lets an accordion and a
	   table sit in one column without the rhythm changing */
	--al-accordion-summary-min-height: var(--al-sys-row-height-md);
	--al-accordion-summary-padding-y: var(--al-sys-space-100);
	--al-accordion-padding-x: var(--al-sys-space-300);
	--al-accordion-body-padding-bottom: var(--al-sys-space-300);
	--al-accordion-gap: var(--al-sys-space-150);

	--al-accordion-summary-fill: transparent;
	/* hover takes the SUNKEN ground, not an accent tint. Sunken is the one ground-aware
	   role meaning "the same surface, one step away". It sinks on a light ground and
	   lifts on a dark one, which is exactly what hover wants on each. The row tints were
	   the alternative and they are the wrong meaning: they say "chosen", so every
	   collapsed section would look selected as the pointer swept over it. */
	--al-accordion-summary-hover-fill: var(--al-sys-color-surface-sunken);
	--al-accordion-summary-radius: 0px;

	--al-accordion-title-color: var(--al-sys-color-text-primary);
	--al-accordion-title-size: var(--al-sys-font-size-300);
	/* medium, not the heading weight. A stack of seven bold rows is a wall; medium still
	   separates the summary from the content it hides. */
	--al-accordion-title-weight: var(--al-sys-font-weight-medium);
	--al-accordion-meta-color: var(--al-sys-color-text-muted);
	--al-accordion-meta-size: var(--al-sys-font-size-200);

	--al-accordion-text-color: var(--al-sys-color-text-secondary);
	--al-accordion-font-size: var(--al-sys-font-size-300);
	--al-accordion-font-family: var(--al-sys-font-family);
	--al-accordion-line-height: var(--al-sys-line-height-body);

	/* the mark is a MASK, so its shape comes from the SVG and its colour from a token, which is
	   the pattern the checkbox settled on, rather than the select's baked-in caret */
	--al-accordion-marker-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6.25 8 10.25 12 6.25' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-accordion-marker-color: var(--al-sys-color-text-muted);
	--al-accordion-marker-size: var(--al-sys-icon-size-md);
	--al-accordion-marker-rotation: 0deg;

	--al-accordion-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-accordion-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-accordion-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-accordion-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-accordion-focus-ring-color: var(--al-sys-color-focus-ring);

	--al-accordion-motion-duration: var(--al-sys-motion-duration-state);
	--al-accordion-motion-easing: var(--al-sys-motion-easing);
	--al-accordion-open-duration: var(--al-sys-motion-duration-enter);
	--al-accordion-open-easing: var(--al-sys-motion-easing-enter);

	box-sizing: border-box;
	display: block;
	background: var(--al-accordion-fill);
	border: var(--al-accordion-border-width) solid var(--al-accordion-border-color);
	border-radius: var(--al-accordion-radius);
	color: var(--al-accordion-text-color);
	font-family: var(--al-accordion-font-family);
	font-size: var(--al-accordion-font-size);
	line-height: var(--al-accordion-line-height);
}

/* The item states its own box rather than accepting whatever the host page gives a
   <details>. This is not defensive tidying: the documentation page itself styles bare
   <details> with a fill, an edge and a bottom margin, and without these three lines
   every section of every accordion drew a second box inside the container and pushed
   its neighbours apart. Any app page with a disclosure style of its own does the same.
   Same principle as owning box-sizing and line-height, one element up. */
.al-accordion-item {
	box-sizing: border-box;
	display: block;
	margin: 0;
	background: transparent;
	border: 0;
	border-radius: 0px;
}

/* the hairline goes BETWEEN items, so a one-item accordion has no stray rule and the
   last item never draws a line onto the container's own bottom edge */
.al-accordion-item + .al-accordion-item {
	border-top: var(--al-accordion-divider-width) solid var(--al-accordion-divider-color);
}

.al-accordion-summary {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-accordion-gap);
	min-height: var(--al-accordion-summary-min-height);
	padding: var(--al-accordion-summary-padding-y) var(--al-accordion-padding-x);
	border-radius: var(--al-accordion-summary-radius);
	background: var(--al-accordion-summary-fill);
	color: var(--al-accordion-title-color);
	font-size: var(--al-accordion-title-size);
	font-weight: var(--al-accordion-title-weight);
	/* set, not inherited: a summary is a row with a floor of 44px, and a host page at
	   1.65 turns that floor into the height */
	line-height: var(--al-accordion-line-height);
	cursor: pointer;
	/* both are needed: Firefox and Safari read list-style, older WebKit reads the
	   pseudo-element, and either one left behind puts a second triangle in the row */
	list-style: none;
	position: relative; /* so the focus ring below can lift over its neighbours */
	transition: background-color var(--al-accordion-motion-duration) var(--al-accordion-motion-easing);
}
/* A summary can be given a marker three different ways, and the control draws its own,
   so all three are cleared: list-style covers Firefox and current WebKit, the
   -webkit- pseudo covers older Safari, and ::before covers a host page that has
   decorated its own disclosures. The documentation page does exactly that, which is
   how a stray triangle turned up in front of every row. */
.al-accordion-summary::-webkit-details-marker {
	display: none;
}
.al-accordion-summary::before {
	content: none;
}

.al-accordion-summary:hover {
	--al-accordion-summary-fill: var(--al-accordion-summary-hover-fill);
}

/* the quiet half of a summary: a count, a date, a status. It sits with the title
   rather than against the far edge, because pinned right it lines up with nothing and
   reads as a second column that does not exist. */
.al-accordion-meta {
	color: var(--al-accordion-meta-color);
	font-size: var(--al-accordion-meta-size);
}

.al-accordion-summary::after {
	content: '';
	flex: none;
	/* the auto margin is what pushes the mark to the far edge, so the title and its meta
	   stay together on the left however long either of them is */
	margin-left: auto;
	box-sizing: border-box;
	width: var(--al-accordion-marker-size);
	height: var(--al-accordion-marker-size);
	background: var(--al-accordion-marker-color);
	-webkit-mask: var(--al-accordion-marker-image) center / contain no-repeat;
	mask: var(--al-accordion-marker-image) center / contain no-repeat;
	transform: rotate(var(--al-accordion-marker-rotation));
	transition: transform var(--al-accordion-motion-duration) var(--al-accordion-motion-easing);
}

/* open is a state, and it re-points a token exactly as a variant would */
.al-accordion-item[open] > .al-accordion-summary {
	--al-accordion-marker-rotation: 180deg;
}

.al-accordion-summary:focus {
	outline: none;
}
/* the same three layers as every other control: a light hairline, the ring, a dark
   hairline, so one of the two always separates the ring from whatever is behind it.
   The transparent outline is what survives forced-colors, where box-shadow is thrown
   away and a keyboard user would otherwise have no indicator at all. */
.al-accordion-summary:focus-visible {
	outline: var(--al-accordion-focus-ring-width) solid transparent;
	outline-offset: var(--al-accordion-focus-hairline-width);
	z-index: 1; /* the next item's hairline paints after this row, so without it the ring
	     is crossed by a border line */
	box-shadow:
		0 0 0 var(--al-accordion-focus-hairline-width) var(--al-accordion-focus-hairline-light),
		0 0 0 calc(var(--al-accordion-focus-hairline-width) + var(--al-accordion-focus-ring-width)) var(--al-accordion-focus-ring-color),
		0 0 0 calc(var(--al-accordion-focus-hairline-width) + var(--al-accordion-focus-ring-width) + var(--al-accordion-focus-hairline-width))
			var(--al-accordion-focus-hairline-dark);
}

.al-accordion-body {
	box-sizing: border-box;
	/* no top padding: the summary's own padding already separates them, and more air
	   makes the content look like it belongs to nothing */
	padding: 0 var(--al-accordion-padding-x) var(--al-accordion-body-padding-bottom);
}

/* THE CORNERS. The container is rounded and the first and last summaries paint a hover
   fill right into those corners, so their own radius has to match or the fill shows
   square where the box is round. Written as token re-points, and derived: the inner
   radius of a rounded box is the outer radius less the border it sits inside, so this
   stays correct if either one moves. The last row only rounds while it is CLOSED:
   open, the body is the bottom of the box. */
.al-accordion-item:first-child > .al-accordion-summary {
	--al-accordion-summary-radius: calc(var(--al-accordion-radius) - var(--al-accordion-border-width))
		calc(var(--al-accordion-radius) - var(--al-accordion-border-width)) 0px 0px;
}
.al-accordion-item:last-child:not([open]) > .al-accordion-summary {
	--al-accordion-summary-radius: 0px 0px calc(var(--al-accordion-radius) - var(--al-accordion-border-width))
		calc(var(--al-accordion-radius) - var(--al-accordion-border-width));
}
.al-accordion-item:first-child:last-child:not([open]) > .al-accordion-summary {
	--al-accordion-summary-radius: calc(var(--al-accordion-radius) - var(--al-accordion-border-width));
}

/* NO CHROME, for an accordion inside a card or a panel that already draws the box. The
   hairlines between items stay. They are the accordion, the box was only ever the
   container. Zeroing the radius makes the corner re-points above collapse to square on
   their own, which is why they were written as calc rather than as a second set of
   values. */
.al-accordion-flat {
	--al-accordion-fill: transparent;
	--al-accordion-border-color: transparent;
	--al-accordion-border-width: 0px;
	--al-accordion-radius: 0px;
}

/* THE OPENING ANIMATION, where the browser will give it to us.

   A <details> cannot animate to its content's height without script unless
   interpolate-size is supported, so this is an @supports block and everything outside
   it is the fallback: the section simply appears, which is what <details> has always
   done. Nothing in here is required for the control to work.

   The clip that makes the animation possible cuts at the body's outer edge, and the
   body carries 24px of padding, so a focus ring on a control inside it is nowhere near
   the boundary. */
@supports (interpolate-size: allow-keywords) {
	.al-accordion-item {
		interpolate-size: allow-keywords;
	}
	.al-accordion-item::details-content {
		block-size: 0;
		overflow: hidden;
		transition:
			block-size var(--al-accordion-open-duration) var(--al-accordion-open-easing),
			content-visibility var(--al-accordion-open-duration) allow-discrete;
	}
	.al-accordion-item[open]::details-content {
		block-size: auto;
	}
}

/* REDUCED MOTION. The section still opens and the mark still turns, they just stop
   moving. Nothing here carries meaning on its own, so all of it can go. */
@media (prefers-reduced-motion: reduce) {
	.al-accordion-summary,
	.al-accordion-summary::after {
		transition: none;
	}
	.al-accordion-item::details-content {
		transition: none;
	}
}

/* ---- overlays ---------------------------------------------------- */
/* =============================================================================
   OVERLAYS — the modal, the drawer, the tooltip and the popover.

   Four things that sit ON the page rather than IN it. They differ on exactly one
   question, and every other difference falls out of the answer: what is the page
   still allowed to do while this is up?

     modal     nothing. It blocks until it is answered.
     drawer    everything except the strip it covers.
     popover   everything, and the popover itself can be used.
     tooltip   everything. It is a label, and it can never be aimed at.

   That ordering IS the layer scale, and the recipe already decided it:
   drawer 300 < modal 500 < popover 600 < tooltip 800. Nothing here invents a
   number, and nothing here needed a new system token.

   THE BROWSER OWNS THE BEHAVIOUR, NOT US. The modal and the drawer are <dialog>;
   the tooltip and the popover carry the popover attribute. Focus trapping, the
   Escape key, light dismiss, the top layer, making the page behind a modal inert
   and returning focus on close are all the platform's. Every one of those is a
   thing a hand-rolled overlay gets wrong eventually, usually the focus return,
   and usually only for keyboard users who never file a bug about it. What is
   written below is the chrome and nothing else.

   THE TRAP THAT BIT EVERY DRAFT OF THIS FILE. Author styles beat user-agent
   styles whatever the specificity says, so a bare `.al-modal { display: flex }`
   overrides the UA's `dialog:not([open]) { display: none }` and a CLOSED dialog
   renders. Same for `[popover]`. So EVERY display declaration here is qualified
   — `[open]` for the two dialogs, `:popover-open` or `:not([popover])` for the
   other two. If you add one, qualify it. Layout properties that are inert until
   an element is a flex container (flex-direction, gap) stay on the base rule,
   which is why the qualified rules are one line each.

   GROUNDS. Not one selector below names a control inside .al-on-dark, and none
   needs to. Every colour here arrives through a component token pointing at a
   shared role, so a panel is white on a light ground and slate-700 on a dark one
   with nothing written to say so. Worth knowing about the modal in particular:
   showModal() moves a dialog to the TOP LAYER but not out of the DOM, and custom
   properties inherit down the DOM. A modal declared inside .al-on-dark is a dark
   modal even though it covers the whole window.
   ============================================================================= */

/* =============================================================================
   THE SHEET — the head, the body and the foot that a modal and a drawer share.

   Written once for both, for the reason the checkbox and the radio are written
   once: a drawer footer that drifts from a modal footer is the inconsistency
   nobody reports and everybody notices. What differs between the two is the
   PANEL — one is centred and blocks, the other is pinned to an edge and does not
   — and that is exactly what --al-modal-* and --al-drawer-* own.

   There is no close button here. A close control is a button, so it is .al-btn,
   and it gets the recipe's focus ring, its press, its disabled and its busy for
   free. A second thing in the system that looks like a button and is not is how
   two focus treatments end up in one product.
   ============================================================================= */

.al-sheet-head {
	--al-sheet-head-padding-x: var(--al-sys-space-300);
	--al-sheet-head-padding-y: var(--al-sys-space-200);
	--al-sheet-head-gap: var(--al-sys-space-200);
	--al-sheet-head-rule-width: var(--al-sys-control-border-width);
	--al-sheet-head-rule-color: var(--al-sys-color-border);

	box-sizing: border-box; /* the system owns its box model — see the button */
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-sheet-head-gap);
	/* flex: none so a long body cannot squeeze the head. The head and the foot are
	   the two fixed ends; only the body gives way. */
	flex: none;
	/* THE BOX IS RESET BEFORE IT IS BUILT, for the same reason .al-sheet-title is.
	   The head lands on a <header> and the foot lands on a <footer> or a <form>, and a
	   host page is free to have styled all three. Measured on a page that gives header
	   a 1.5em margin and form a 2px dashed border: the head opened a 24px band inside
	   the panel, and the foot drew the host's border down three of its sides. The title
	   already carries this reset. Missing it here was the same defect one tag along. */
	margin: 0;
	border: 0;
	padding: var(--al-sheet-head-padding-y) var(--al-sheet-head-padding-x);
	border-bottom: var(--al-sheet-head-rule-width) solid var(--al-sheet-head-rule-color);
}

/* A FORM BETWEEN THE MODAL AND ITS PARTS IS TRANSPARENT TO THE LAYOUT.

   .al-modal is a flex column and everything below assumes al-sheet-head / -body / -foot are
   its direct children: that is what lets the body take the slack and scroll while the foot
   stays put. A dialog that submits wraps all three in a <form>, so the modal has exactly one
   flex child, that child is display:block, and nothing inside it is in a flex context at all.
   Measured on the conversion dialog at an 800px viewport: a 785px form inside a modal capped
   at 752px, so the footer ran to 810px and overflow:hidden took Submit and Cancel with it.

   Two dialogs in the product had already worked around this in their own page CSS.

   INHERIT, NOT 100% — and this one is load-bearing in Safari. A percentage max-height resolves
   against the containing block's height, and .al-modal has no height, only a max-height. Blink
   resolves the percentage against that cap anyway; WEBKIT RESOLVES IT TO ZERO, so the form
   computed height: 0px and every dialog wearing this rule was left as its own two 1px borders:
   a 2px hairline on a scrim, on iPad and on desktop Safari alike. ALLM-7244, and 39 view files
   including login.njk. `inherit` says the same thing as a length WebKit can resolve. */
.al-modal > form {
	display: flex;
	flex-direction: column;
	min-height: 0;
	max-height: inherit;
}

.al-sheet-title {
	--al-sheet-title-color: var(--al-sys-color-text-primary);
	--al-sheet-title-size: var(--al-sys-font-size-400);
	--al-sheet-title-weight: var(--al-sys-font-weight-heading);
	--al-sheet-title-line-height: var(--al-sys-line-height-heading);
	--al-sheet-title-font-family: var(--al-sys-font-family);

	/* THE BOX IS RESET, IN FULL, ON PURPOSE. This lands on whatever heading level
	   the surrounding document called for — h2 in one place, h3 in another — and a
	   host page is free to have decided that an h2 carries a rule above it and 44px
	   of margin. This system's own documentation page does exactly that. A title
	   that changes shape depending on the tag someone reached for is not a title.

	   IN FULL means the two nobody remembers, and they were both missing from the
	   first draft: the docs style h3 as uppercase small-caps with letter-spacing, so
	   a title written as an h3 rendered "VOID MANIFEST MAN-40118?" while the same
	   title written as an h2 did not. Caught by looking at it — the font size and
	   the weight were already correct, which is what made it easy to miss. */
	box-sizing: border-box;
	margin: 0;
	padding: 0;
	border: 0;
	text-transform: none;
	letter-spacing: normal;
	/* the title takes the room and the close button takes what it needs. min-width
	   0 is what lets a flex item shrink below its content, without which a long
	   manifest number pushes the close button off the panel instead of truncating. */
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;

	font-family: var(--al-sheet-title-font-family);
	font-size: var(--al-sheet-title-size);
	font-weight: var(--al-sheet-title-weight);
	/* stated, never inherited: line-height IS inherited, and a host page's 1.65
	   makes this line box taller than the head's whole padding budget */
	line-height: var(--al-sheet-title-line-height);
	color: var(--al-sheet-title-color);
}

.al-sheet-body {
	--al-sheet-body-padding-x: var(--al-sys-space-300);
	--al-sheet-body-padding-y: var(--al-sys-space-300);
	--al-sheet-body-gap: var(--al-sys-space-200);
	--al-sheet-body-color: var(--al-sys-color-text-secondary);
	--al-sheet-body-size: var(--al-sys-font-size-300);
	--al-sheet-body-line-height: var(--al-sys-line-height-body);

	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	gap: var(--al-sheet-body-gap);
	/* THE BODY IS THE ONLY PART THAT SCROLLS, and min-height: 0 is what makes that
	   true. A flex item's floor is its own content, so without it the body refuses
	   to shrink, the panel grows past its max-height, and the foot with the confirm
	   button in it goes off the bottom of the window — the failure where somebody
	   cannot reach the one button the dialog exists for. */
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	/* the page behind must not start scrolling when the body runs out */
	overscroll-behavior: contain;
	padding: var(--al-sheet-body-padding-y) var(--al-sheet-body-padding-x);

	font-size: var(--al-sheet-body-size);
	line-height: var(--al-sheet-body-line-height);
	color: var(--al-sheet-body-color);
}
/* the gap owns the rhythm, so a paragraph's own margin would double it. Stated
   because a host page's margin on <p> is otherwise inherited into the panel. */
.al-sheet-body > p {
	margin: 0;
}

.al-sheet-foot {
	--al-sheet-foot-padding-x: var(--al-sys-space-300);
	--al-sheet-foot-padding-y: var(--al-sys-space-200);
	--al-sheet-foot-gap: var(--al-sys-space-150);
	--al-sheet-foot-fill: var(--al-sys-color-surface-sunken);
	--al-sheet-foot-rule-width: var(--al-sys-control-border-width);
	--al-sheet-foot-rule-color: var(--al-sys-color-border);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: flex-end;
	/* wrap rather than overflow: three buttons in a narrow drawer stack instead of
	   pushing the first one out through the panel's edge */
	flex-wrap: wrap;
	gap: var(--al-sheet-foot-gap);
	flex: none;
	/* reset in full before the rule is drawn, and this one matters most: the documented
	   markup puts the foot on a <form method="dialog">, which is the element a host page
	   is likeliest to have given a margin and a border of its own. See the head. */
	margin: 0;
	border: 0;
	padding: var(--al-sheet-foot-padding-y) var(--al-sheet-foot-padding-x);
	border-top: var(--al-sheet-foot-rule-width) solid var(--al-sheet-foot-rule-color);
	/* the foot paints a background, which normally means it has to name its ground.
	   It does not, and this is the exception worth understanding: the sunken role is
	   a recess relative to whatever it sits on, so it is slate-50 over white and a
	   white lift over slate-700. It never crosses from light to dark, so the ground
	   it inherits stays the right one. */
	background: var(--al-sheet-foot-fill);
}

/* =============================================================================
   THE MODAL

   A <dialog>, always, opened with showModal(). That single call is what buys the
   focus trap, the Escape key, the inert page behind it, the top layer above every
   z-index in the product, and focus returning to whatever opened it. None of
   those is written here, because none of them should be ours to maintain.

   USE IT WHEN THE ANSWER IS REQUIRED. A modal is the rudest control in the
   system: it takes the whole page hostage over one question. If the page can
   carry on without an answer, this is a drawer or a popover.
   ============================================================================= */

.al-modal {
	/* three widths exist but only one is ever in force, so by rule 4 the token is
	   unsuffixed and a size variant re-points it. Held as pixels rather than
	   pointed at a shared scale because no other component wants these numbers, and
	   minting a system rung for a single consumer is the speculative token rule 1
	   forbids. */
	--al-modal-width: 520px;
	--al-modal-margin: var(--al-sys-space-300);
	--al-modal-fill: var(--al-sys-color-surface-raised);
	--al-modal-text: var(--al-sys-color-text-secondary);
	--al-modal-font-family: var(--al-sys-font-family);
	--al-modal-radius: var(--al-sys-radius-container);
	--al-modal-border-width: var(--al-sys-control-border-width);
	--al-modal-border-color: var(--al-sys-color-border);
	--al-modal-shadow: var(--al-sys-elevation-overlay);
	--al-modal-scrim: var(--al-sys-color-scrim);
	--al-modal-layer: var(--al-sys-layer-modal);
	/* the edge along the top. Transparent on an ordinary dialog, which costs
	   nothing, and re-pointed by the destructive variant — so "this one destroys
	   something" is readable before a word of the title has been. */
	--al-modal-edge: transparent;
	--al-modal-edge-width: 3px;

	--al-modal-motion-duration-enter: var(--al-sys-motion-duration-enter);
	--al-modal-motion-duration-exit: var(--al-sys-motion-duration-exit);
	--al-modal-motion-easing-enter: var(--al-sys-motion-easing-enter);
	--al-modal-motion-easing-exit: var(--al-sys-motion-easing-exit);
	/* it grows the last two per cent into place rather than arriving from nowhere.
	   Anything larger reads as a pop, and draws the eye to the motion instead of to
	   the question. */
	--al-modal-enter-scale: 0.98;

	box-sizing: border-box;
	/* POSITION IS THE ONLY THING :modal CHANGES. The panel is the same box whether
	   it is in the top layer or standing in the flow of a page, and it has to be: a
	   <dialog open> that was never showModal()'d is how a page shows a permanently
	   open detail panel, and how every example in the docs below is drawn. */
	position: static;
	inset: 0;
	z-index: var(--al-modal-layer);
	margin: auto;
	flex-direction: column;

	width: var(--al-modal-width);
	max-width: calc(100% - var(--al-modal-margin) * 2);
	/* dvh, not vh: on a phone the browser's own chrome eats the difference, and a
	   dialog sized to the large viewport hides its footer under the address bar —
	   which is where the confirm button lives. */
	/* vh first as a fallback: dvh is Chrome 108 and the POS terminals run 101, where this
	   declaration is dropped and the dialog loses its cap entirely. The browser own dialog
	   default then caps it instead, with overflow:hidden still applied, so a tall modal is
	   truncated with no scrollbar. Unlike a custom property, a normal declaration DOES fall
	   back to the one before it. */
	max-height: calc(100vh - var(--al-modal-margin) * 2);
	max-height: calc(100dvh - var(--al-modal-margin) * 2);
	padding: 0;
	border: var(--al-modal-border-width) solid var(--al-modal-border-color);
	border-radius: var(--al-modal-radius);
	background: var(--al-modal-fill);
	color: var(--al-modal-text);
	font-family: var(--al-modal-font-family);
	/* the head and the foot run to the panel's edge, so the radius has to clip them */
	overflow: hidden;
	/* the top edge is an inset shadow rather than a border or a bar: an inset shadow
	   is clipped by the border radius for free, needs no pseudo-element to fight the
	   flex layout, and composes into the same declaration as the elevation — so a
	   variant cannot re-point one and silently lose the other. */
	box-shadow:
		inset 0 var(--al-modal-edge-width) 0 var(--al-modal-edge),
		var(--al-modal-shadow);

	opacity: 0;
	transform: scale(var(--al-modal-enter-scale));
	transition:
		opacity var(--al-modal-motion-duration-exit) var(--al-modal-motion-easing-exit),
		transform var(--al-modal-motion-duration-exit) var(--al-modal-motion-easing-exit),
		overlay var(--al-modal-motion-duration-exit) allow-discrete,
		display var(--al-modal-motion-duration-exit) allow-discrete;
}
.al-modal:modal {
	position: fixed;
}
/* QUALIFIED — see the header. An unqualified display here renders every closed
   dialog in the document. */
.al-modal[open] {
	display: flex;
	opacity: 1;
	transform: none;
	transition:
		opacity var(--al-modal-motion-duration-enter) var(--al-modal-motion-easing-enter),
		transform var(--al-modal-motion-duration-enter) var(--al-modal-motion-easing-enter),
		overlay var(--al-modal-motion-duration-enter) allow-discrete,
		display var(--al-modal-motion-duration-enter) allow-discrete;
}
@starting-style {
	.al-modal[open] {
		opacity: 0;
		transform: scale(var(--al-modal-enter-scale));
	}
}

/* THE SCRIM. ::backdrop inherits from the element it belongs to, which is what
   lets it read the modal's own token — and what makes a modal declared inside a
   dark panel dim the page with the dark ground's deeper scrim rather than the
   light one, with nothing written to say so. */
.al-modal::backdrop {
	background: var(--al-modal-scrim);
	opacity: 0;
	transition:
		opacity var(--al-modal-motion-duration-exit) var(--al-modal-motion-easing-exit),
		overlay var(--al-modal-motion-duration-exit) allow-discrete,
		display var(--al-modal-motion-duration-exit) allow-discrete;
}
.al-modal[open]::backdrop {
	opacity: 1;
	transition:
		opacity var(--al-modal-motion-duration-enter) var(--al-modal-motion-easing-enter),
		overlay var(--al-modal-motion-duration-enter) allow-discrete,
		display var(--al-modal-motion-duration-enter) allow-discrete;
}
@starting-style {
	.al-modal[open]::backdrop {
		opacity: 0;
	}
}

/* THE PAGE BEHIND MUST NOT SCROLL, and showModal() does not stop it — the page is
   inert to clicks and to the keyboard, but a wheel still scrolls it under the
   scrim. Done with :has() rather than a class the app adds and removes, because
   the script version fails the same way every time: one early return between the
   open and the close and the whole page is frozen for the rest of the session.
   This cannot get out of step, because there is no state to keep.

   A host page that does not want the scrollbar's width to shift its layout sets
   scrollbar-gutter: stable on its own root. That is the page's call to make. */
html:has(.al-modal:modal) {
	overflow: hidden;
}

/* -- SIZE. One token each, and the same three names the rest of the system uses.
      380 is a confirm: one sentence and two buttons, narrow enough that the
      question reads in a single glance. 520 is the default. 760 holds a form or a
      short table without the lines running past a comfortable measure. -------- */
.al-modal-sm {
	--al-modal-width: 380px;
}
.al-modal-lg {
	--al-modal-width: 760px;
}
.al-modal-xlg {
	--al-modal-width: 960px;
}

/* -- DESTRUCTIVE. One line, because the panel already reads its edge from a token.
      It deliberately does NOT turn the title red: red ink is harder to read than
      ink, and it says nothing the words do not, whereas the edge is legible before
      anything has been read. The other half of the signal is the confirm button,
      which is .al-btn-danger and carries it already. ------------------------- */
.al-modal-danger {
	--al-modal-edge: var(--al-sys-color-status-error);
}

/* =============================================================================
   THE DRAWER

   Also a <dialog>, but opened with show() rather than showModal(). That is the
   whole difference and it is exactly the right one: show() does not trap focus,
   does not make the page inert, and draws no backdrop, so the page carries on
   underneath — which is the definition of a drawer. It still brings the open
   attribute, close(), and a form method="dialog" that closes it without a line of
   script.

   NO SCRIM, deliberately. A scrim says "answer me first". A drawer is a side
   panel you consult while you work, so dimming the thing you are working on would
   be the control contradicting itself. If it needs a scrim, it is a modal.
   ============================================================================= */

.al-drawer {
	--al-drawer-width: 400px;
	/* a strip of page always stays showing, so a drawer never reads as a full
	   screen takeover on a narrow window — that is a modal, and this is not one.
	   It is also the target you aim at to dismiss it. */
	--al-drawer-min-page-visible: var(--al-sys-space-600);
	--al-drawer-fill: var(--al-sys-color-surface-raised);
	--al-drawer-text: var(--al-sys-color-text-secondary);
	--al-drawer-font-family: var(--al-sys-font-family);
	--al-drawer-border-color: var(--al-sys-color-border);
	--al-drawer-shadow: var(--al-sys-elevation-overlay);
	--al-drawer-layer: var(--al-sys-layer-drawer);

	/* WHICH EDGE, as five tokens, so the side variant re-points values and never
	   overrides properties — rule 8. The insets are PHYSICAL rather than logical,
	   and that is a decision: the classes are named for physical sides, a slide
	   transform is physical and has no logical form, and a logical placement
	   driving a physical slide is the pair that comes apart in a right-to-left
	   document. Physical throughout is at least honest about what it promises. */
	--al-drawer-inset-left: auto;
	--al-drawer-inset-right: 0;
	--al-drawer-border-left-width: var(--al-sys-control-border-width);
	--al-drawer-border-right-width: 0px;
	--al-drawer-slide-from: 100%;

	--al-drawer-motion-duration-enter: var(--al-sys-motion-duration-enter);
	--al-drawer-motion-duration-exit: var(--al-sys-motion-duration-exit);
	--al-drawer-motion-easing-enter: var(--al-sys-motion-easing-enter);
	--al-drawer-motion-easing-exit: var(--al-sys-motion-easing-exit);

	box-sizing: border-box;
	position: fixed;
	top: 0;
	bottom: 0;
	left: var(--al-drawer-inset-left);
	right: var(--al-drawer-inset-right);
	z-index: var(--al-drawer-layer);
	flex-direction: column;
	margin: 0;

	width: var(--al-drawer-width);
	max-width: calc(100% - var(--al-drawer-min-page-visible));
	/* auto, stated. The user agent gives a dialog height: fit-content, which does
	   NOT stretch between two insets — so a full-height drawer came out the height
	   of its own contents, floating in the middle of the right-hand edge. */
	height: auto;
	padding: 0;
	/* no radius. A drawer is attached to the edge of the window, and rounding the
	   outer corners makes it look like a card that missed. The inner edge carries a
	   hairline instead, because that is the only edge anyone has to see. */
	border: 0;
	border-left: var(--al-drawer-border-left-width) solid var(--al-drawer-border-color);
	border-right: var(--al-drawer-border-right-width) solid var(--al-drawer-border-color);
	background: var(--al-drawer-fill);
	color: var(--al-drawer-text);
	font-family: var(--al-drawer-font-family);
	overflow: hidden;
	box-shadow: var(--al-drawer-shadow);

	transform: translateX(var(--al-drawer-slide-from));
	transition:
		transform var(--al-drawer-motion-duration-exit) var(--al-drawer-motion-easing-exit),
		overlay var(--al-drawer-motion-duration-exit) allow-discrete,
		display var(--al-drawer-motion-duration-exit) allow-discrete;
}
.al-drawer[open] {
	display: flex;
	transform: none;
	transition:
		transform var(--al-drawer-motion-duration-enter) var(--al-drawer-motion-easing-enter),
		overlay var(--al-drawer-motion-duration-enter) allow-discrete,
		display var(--al-drawer-motion-duration-enter) allow-discrete;
}
@starting-style {
	.al-drawer[open] {
		transform: translateX(var(--al-drawer-slide-from));
	}
}

/* a drawer draws no scrim. Stated rather than assumed, so that opening one with
   showModal() by mistake dims nothing and the mistake is visible as a focus trap
   rather than as a page that went dark for no reason. */
.al-drawer::backdrop {
	background: transparent;
}

/* -- SIDE. Five re-points each, no properties. ------------------------------- */
.al-drawer-right {
	--al-drawer-inset-left: auto;
	--al-drawer-inset-right: 0;
	--al-drawer-border-left-width: var(--al-sys-control-border-width);
	--al-drawer-border-right-width: 0px;
	--al-drawer-slide-from: 100%;
}
.al-drawer-left {
	--al-drawer-inset-left: 0;
	--al-drawer-inset-right: auto;
	--al-drawer-border-left-width: 0px;
	--al-drawer-border-right-width: var(--al-sys-control-border-width);
	--al-drawer-slide-from: -100%;
}

/* -- SIZE. 320 is a list of filters. 400 is the default and holds one record's
      detail. 560 takes a form without the labels and the values fighting. ---- */
.al-drawer-sm {
	--al-drawer-width: 320px;
}
.al-drawer-lg {
	--al-drawer-width: 560px;
}

/* =============================================================================
   THE TOOLTIP

   A label, and only ever a label. Everything a tooltip says must also exist
   somewhere a person who never hovers can find it, because a tooltip is
   unreachable on a touch screen, invisible on a printout, and gone the moment the
   pointer moves. If it is the only place a fact lives, that fact is lost.

   It carries popover="hint", which puts it in the top layer — so it cannot be
   clipped by a scrolling table cell or buried under a sticky header, and opening
   it does not close the ordinary popover somebody already has open. WHAT OPENS IT
   is not ours: hover and focus with a short delay, and dismissal on Escape and on
   blur, are a few lines the app owns. The recipe dresses the bubble.
   ============================================================================= */

.al-tooltip {
	--al-tooltip-fill: var(--al-sys-color-surface-raised);
	--al-tooltip-text: var(--al-sys-color-text-primary);
	--al-tooltip-border-width: var(--al-sys-control-border-width);
	--al-tooltip-border-color: var(--al-sys-color-border);
	--al-tooltip-radius: var(--al-sys-radius-sm);
	--al-tooltip-padding-x: var(--al-sys-space-100);
	--al-tooltip-padding-y: var(--al-sys-space-50);
	--al-tooltip-font-family: var(--al-sys-font-family);
	--al-tooltip-font-size: var(--al-sys-font-size-200);
	--al-tooltip-line-height: var(--al-sys-line-height-body);
	/* past this it is prose, and prose belongs in a popover you can read at your
	   own pace rather than in a bubble that vanishes when you look away */
	--al-tooltip-max-width: 260px;
	--al-tooltip-shadow: var(--al-sys-elevation-raised);
	--al-tooltip-layer: var(--al-sys-layer-tooltip);
	--al-tooltip-offset: var(--al-sys-space-100);
	--al-tooltip-motion-duration-enter: var(--al-sys-motion-duration-enter);
	--al-tooltip-motion-duration-exit: var(--al-sys-motion-duration-exit);
	--al-tooltip-motion-easing-enter: var(--al-sys-motion-easing-enter);
	--al-tooltip-motion-easing-exit: var(--al-sys-motion-easing-exit);

	box-sizing: border-box;
	/* THE BUBBLE IS THE RAISED SURFACE, NOT AN INVERTED SLAB. The convention is a
	   dark chip on a light page, and it is declined here for one reason: an
	   inverted bubble needs an inverted colour pair, and this system recorded a
	   deliberate decision not to mirror every colour role. Without that mirror an
	   inverted tooltip is a hardcoded dark box no theme can reach, and it turns
	   invisible the moment it opens over a dark panel. Reading the raised surface
	   makes it the same object as the menu and the popover, correct on both
	   grounds, and themeable. */
	width: max-content;
	max-width: var(--al-tooltip-max-width);
	margin: 0;
	padding: var(--al-tooltip-padding-y) var(--al-tooltip-padding-x);
	border: var(--al-tooltip-border-width) solid var(--al-tooltip-border-color);
	border-radius: var(--al-tooltip-radius);
	background: var(--al-tooltip-fill);
	color: var(--al-tooltip-text);
	box-shadow: var(--al-tooltip-shadow);
	font-family: var(--al-tooltip-font-family);
	font-size: var(--al-tooltip-font-size);
	line-height: var(--al-tooltip-line-height);
}
/* the bubble without the attribute, for a page that wants the same chrome inline
   — and for every example in the documentation, which has to render standing
   still rather than only under a pointer */
.al-tooltip:not([popover]) {
	display: inline-block;
}
.al-tooltip[popover] {
	z-index: var(--al-tooltip-layer);
	/* A TOOLTIP IS NEVER A TARGET. Without this it sits under the pointer that
	   summoned it, the pointer is now over the tooltip rather than the trigger, the
	   trigger loses hover, the tooltip closes, the pointer is over the trigger
	   again — and the bubble flickers on and off until the mouse moves away. */
	pointer-events: none;
	opacity: 0;
	transition:
		opacity var(--al-tooltip-motion-duration-exit) var(--al-tooltip-motion-easing-exit),
		overlay var(--al-tooltip-motion-duration-exit) allow-discrete,
		display var(--al-tooltip-motion-duration-exit) allow-discrete;
}
.al-tooltip:popover-open {
	display: block;
	opacity: 1;
	transition:
		opacity var(--al-tooltip-motion-duration-enter) var(--al-tooltip-motion-easing-enter),
		overlay var(--al-tooltip-motion-duration-enter) allow-discrete,
		display var(--al-tooltip-motion-duration-enter) allow-discrete;
}
@starting-style {
	.al-tooltip:popover-open {
		opacity: 0;
	}
}
/* PLACEMENT, only where the browser can do it. An invoker is its popover's
   implicit anchor, so nothing has to be named on either element. Guarded by
   @supports because the fallback matters: without anchor positioning the user
   agent centres a popover in the window, which is odd but readable and
   dismissible, whereas overriding inset outside the guard would strand it in the
   top left corner of the screen. */
@supports (position-area: block-start) {
	.al-tooltip[popover] {
		inset: auto;
		position-area: block-start;
		/* BOTH AXES, and the inline half is not optional. A centred bubble over a
		   trigger near the left edge of the window measured at x = -18: half of a
		   260px bubble hanging off the screen, with the first two words unreadable.
		   flip-block alone only ever moves it up or down. The span- fallbacks stop
		   centring and pin the bubble to one edge of the trigger instead, which is
		   what brings it back on screen. Order is preference: centred above,
		   centred below, then pinned left, then pinned right. */
		position-try-fallbacks:
			flip-block,
			block-start span-inline-end,
			block-end span-inline-end,
			block-start span-inline-start,
			block-end span-inline-start;
		margin: var(--al-tooltip-offset);
	}
}

/* =============================================================================
   THE POPOVER

   What a tooltip is not allowed to be: it can hold controls, it can be read at
   leisure, and you can click inside it. It carries the plain popover attribute,
   which is popover="auto" — so the browser closes it when you click away or press
   Escape, closes any other auto popover when it opens, and returns focus. Light
   dismiss is the whole reason to use the attribute rather than a div: it is the
   behaviour every hand-rolled dropdown gets wrong on the second one.

   NO ARROW, and that is a decision rather than an omission. An arrow has to flip
   when the bubble flips off the edge of a window, and an arrow left pointing at
   nothing is worse than no arrow at all. The offset gap and the shadow already
   tie a bubble to what opened it.
   ============================================================================= */

.al-popover {
	--al-popover-fill: var(--al-sys-color-surface-raised);
	--al-popover-text: var(--al-sys-color-text-secondary);
	--al-popover-border-width: var(--al-sys-control-border-width);
	--al-popover-border-color: var(--al-sys-color-border);
	--al-popover-radius: var(--al-sys-radius-container);
	--al-popover-padding: var(--al-sys-space-200);
	--al-popover-gap: var(--al-sys-space-150);
	--al-popover-font-family: var(--al-sys-font-family);
	--al-popover-font-size: var(--al-sys-font-size-300);
	--al-popover-line-height: var(--al-sys-line-height-body);
	--al-popover-max-width: 340px;
	/* it scrolls rather than growing past the window. A popover taller than this is
	   a drawer that has not admitted it yet. */
	--al-popover-max-height: 60dvh;
	--al-popover-shadow: var(--al-sys-elevation-raised);
	--al-popover-layer: var(--al-sys-layer-popover);
	--al-popover-offset: var(--al-sys-space-100);
	--al-popover-rise: 4px;
	--al-popover-motion-duration-enter: var(--al-sys-motion-duration-enter);
	--al-popover-motion-duration-exit: var(--al-sys-motion-duration-exit);
	--al-popover-motion-easing-enter: var(--al-sys-motion-easing-enter);
	--al-popover-motion-easing-exit: var(--al-sys-motion-easing-exit);

	box-sizing: border-box;
	flex-direction: column;
	gap: var(--al-popover-gap);
	width: max-content;
	max-width: var(--al-popover-max-width);
	margin: 0;
	padding: var(--al-popover-padding);
	border: var(--al-popover-border-width) solid var(--al-popover-border-color);
	border-radius: var(--al-popover-radius);
	background: var(--al-popover-fill);
	color: var(--al-popover-text);
	box-shadow: var(--al-popover-shadow);
	font-family: var(--al-popover-font-family);
	font-size: var(--al-popover-font-size);
	line-height: var(--al-popover-line-height);
}
.al-popover:not([popover]) {
	display: flex;
}
.al-popover[popover] {
	z-index: var(--al-popover-layer);
	max-height: var(--al-popover-max-height);
	overflow-y: auto;
	overscroll-behavior: contain;
	opacity: 0;
	transform: translateY(calc(var(--al-popover-rise) * -1));
	transition:
		opacity var(--al-popover-motion-duration-exit) var(--al-popover-motion-easing-exit),
		transform var(--al-popover-motion-duration-exit) var(--al-popover-motion-easing-exit),
		overlay var(--al-popover-motion-duration-exit) allow-discrete,
		display var(--al-popover-motion-duration-exit) allow-discrete;
}
.al-popover:popover-open {
	display: flex;
	opacity: 1;
	transform: none;
	transition:
		opacity var(--al-popover-motion-duration-enter) var(--al-popover-motion-easing-enter),
		transform var(--al-popover-motion-duration-enter) var(--al-popover-motion-easing-enter),
		overlay var(--al-popover-motion-duration-enter) allow-discrete,
		display var(--al-popover-motion-duration-enter) allow-discrete;
}
@starting-style {
	.al-popover:popover-open {
		opacity: 0;
		transform: translateY(calc(var(--al-popover-rise) * -1));
	}
}
@supports (position-area: block-end) {
	.al-popover[popover] {
		inset: auto;
		/* below the trigger by default, which is where a menu belongs. It flips up
		   near the bottom of the window, and pins to one edge of the trigger rather
		   than staying centred when centring would hang it off the side — see the
		   tooltip, where that was measured going 18px off the left of the window. */
		position-area: block-end;
		position-try-fallbacks:
			flip-block,
			block-end span-inline-end,
			block-start span-inline-end,
			block-end span-inline-start,
			block-start span-inline-start;
		margin: var(--al-popover-offset);
	}
}

.al-popover-title {
	--al-popover-title-color: var(--al-sys-color-text-primary);
	--al-popover-title-size: var(--al-sys-font-size-300);
	--al-popover-title-weight: var(--al-sys-font-weight-heading);
	--al-popover-title-line-height: var(--al-sys-line-height-heading);

	/* reset in full, for the same reason .al-sheet-title is: this lands on whichever
	   heading level the surrounding document called for, and text-transform and
	   letter-spacing are the two that survive a partial reset unnoticed */
	box-sizing: border-box;
	margin: 0;
	padding: 0;
	border: 0;
	text-transform: none;
	letter-spacing: normal;
	font-family: inherit;
	font-size: var(--al-popover-title-size);
	font-weight: var(--al-popover-title-weight);
	line-height: var(--al-popover-title-line-height);
	color: var(--al-popover-title-color);
}
/* the popover's gap owns the rhythm — see the sheet body */
.al-popover > p {
	margin: 0;
}
.al-popover-actions {
	--al-popover-actions-gap: var(--al-sys-space-100);

	box-sizing: border-box;
	display: flex;
	justify-content: flex-end;
	flex-wrap: wrap;
	gap: var(--al-popover-actions-gap);
}

/* =============================================================================
   REDUCED MOTION — keep only what carries meaning.

   Every animation in this file is polish. Nothing here is telling somebody that
   work is still running, the way the button's spinner is, so all of it goes: the
   modal appears, the drawer is simply there, the bubbles blink on. What survives
   is what was always doing the work — a scrim behind a modal, a panel pinned to
   an edge, a bubble beside its trigger.

   The discrete display transition goes with it, which is what stops a closing
   overlay lingering for a beat with nothing left to animate.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-modal,
	.al-modal[open],
	.al-modal::backdrop,
	.al-modal[open]::backdrop,
	.al-drawer,
	.al-drawer[open],
	.al-tooltip[popover],
	.al-tooltip:popover-open,
	.al-popover[popover],
	.al-popover:popover-open {
		transition: none;
	}
	.al-modal {
		transform: none;
	}
	.al-drawer {
		/* display: none is already keeping a closed drawer off the screen, so the
		   slide offset has nothing left to do — and a transform with no transition
		   only risks painting a half-slid panel on the frame it opens */
		transform: none;
	}
	.al-popover[popover] {
		transform: none;
	}
}

/* ---- data ---------------------------------------------------- */
/* =============================================================================
   DATA — THE TABLE, THE LIST, THE PAIR, AND THE EMPTY STATE

   Four controls answering the same question in four shapes: here is what the
   system knows. The table is for facts that line up in columns. The list is for
   facts that do not. The pair is for one record read rather than scanned. The
   empty state is what all three become when there is nothing to show, and it is
   the one people forget to design until a customer meets it.

   NOT ONE RULE HERE NAMES A GROUND. Every colour comes from a sys role, so all
   four are correct on a card and on a dark panel the day they are written. The
   two row tints were already decided — --al-sys-color-row-tint-selected and
   -active — and they are steps of one ladder: chosen is quiet, where-you-are is
   louder, so "what I picked" and "where I am" never fight over one background.

   NOTHING HERE ANIMATES, deliberately. A tint that fades in makes a table of two
   hundred rows feel laggy under a moving pointer, and the tint has to be there
   the instant the pointer arrives or it reads as lag rather than feedback. That
   also means this file needs no reduced-motion block: there is no motion in it.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   A NUMBER.

   Its own two-line control because getting it wrong is easy and very visible. A
   column of amounts that is left aligned cannot be compared down the column, and
   proportional digits make 1,111 narrower than 8,888 so the decimal points
   wander. Right alignment and tabular figures fix both, and it is the same fix
   in a table cell, a list row and a detail pair — so it is written once rather
   than three times.
   ----------------------------------------------------------------------------- */
.al-num {
	--al-num-align: right;
	--al-num-font-family: var(--al-sys-font-family-numeric);

	text-align: var(--al-num-align);
	font-family: var(--al-num-font-family);
	font-variant-numeric: tabular-nums;
}

/* =============================================================================
   THE TABLE

   THE HEADER IS ALWAYS STICKY. There is no non-sticky variant, because a header
   that scrolls away is never what anyone wanted — the moment the column names
   leave the screen the numbers under them stop meaning anything. Making it a
   variant would also have meant a variant changing `position`, which is a
   property, and rule 8 says variants re-point tokens. Sticky by default costs
   nothing when the table is short and saves the table when it is long.

   The offset is a token because a page with its own sticky bar needs the header
   to stop below it, and that is the page's fact, not the table's.

   NO ZEBRA STRIPING. Stripes are a crutch for a table with no vertical rhythm,
   and they fight the selected tint — a chosen even row and a chosen odd row come
   out as two different colours, which reads as a bug. A hairline between rows
   and a strong hover tint do the job stripes were hired for.
   ============================================================================= */

/* The viewport. Only needed when the table has to scroll inside a bounded region
   rather than with the page. It is deliberately chromeless: a table is content,
   and the card or drawer around it owns the border and the radius. Giving this
   one a radius would clip the sticky header against it. */
.al-table-scroll {
	/* 60vh because a table that fills more than about two thirds of the window
	   leaves nothing on screen to orient by — no page heading, no filters, no
	   idea what you are looking at. */
	--al-table-viewport-max-height: 60vh;

	box-sizing: border-box;
	max-height: var(--al-table-viewport-max-height);
	overflow: auto;
}

.al-table {
	/* -- density. Three tiers, each re-pointing four tokens, the same shape the
	      button uses. The row height comes from --al-sys-row-height-*, which is a
	      control tier plus 4px of air, so a row with a button in it is the same
	      height as a row without one. --------------------------------------- */
	--al-table-row-height: var(--al-sys-row-height-md);
	--al-table-cell-padding-x: var(--al-sys-control-padding-x-md);
	--al-table-font-size: var(--al-sys-font-size-300);
	--al-table-header-font-size: var(--al-sys-font-size-200);

	/* Vertical padding is NOT tiered and is deliberately small. The row height
	   owns the row; this exists only to keep a cell that wraps onto two lines off
	   the hairlines above and below it. Tiering it as well would mean the row
	   height stopped deciding anything at the small tier — 8px top and bottom
	   plus one 20px line is exactly 36px, so the tier would be doing no work. */
	--al-table-cell-padding-y: var(--al-sys-space-50);

	/* Line height is SET, not inherited: a host page with 1.65 on the body would
	   otherwise decide how tall our rows are. The body rung rather than the
	   control rung, because cells hold real prose — a strain name, a delivery
	   address — and 1.2 reads cramped when one wraps. It costs nothing here: even
	   at the large tier one line is 26px inside a 52px row, so the tier holds. */
	--al-table-line-height: var(--al-sys-line-height-body);
	--al-table-font-family: var(--al-sys-font-family);

	/* -- ink. The DATA is the loudest thing in a table and the column names are
	      labels, so the cells take the primary rung and the header takes the
	      secondary one. That is the opposite of the instinct to make headings
	      dark, and it is right: you read the header once and the data all day. */
	--al-table-text: var(--al-sys-color-text-primary);
	--al-table-header-text: var(--al-sys-color-text-secondary);
	--al-table-header-font-weight: var(--al-sys-font-weight-control);

	/* The header fill is the SURFACE, not the sunken rung, and that is a
	   correctness matter rather than a taste one. Sticky means rows pass
	   underneath it, so it has to be opaque — and on a dark ground the sunken
	   role is a translucent white lift, which would let the rows show straight
	   through. Opaque on both grounds is the only value that works on both. */
	--al-table-header-fill: var(--al-sys-color-surface);
	--al-table-header-rule-width: var(--al-sys-control-border-width);
	--al-table-header-rule-color: var(--al-sys-color-border);
	/* THE ELEVATION BELONGS TO THE HEADER GROUP, NOT TO EACH CELL, and that is
	   worth spelling out because the first version put it on the <th>. A blurred
	   shadow bleeds sideways as well as down, so every cell cast one against its
	   neighbour and the header grew a set of faint vertical seams that looked
	   like column dividers nobody asked for. Caught by looking at it at 4x, not
	   by any number. One box, one shadow. */
	--al-table-header-elevation: var(--al-sys-elevation-sticky);
	--al-table-header-offset: 0px;
	--al-table-header-layer: var(--al-sys-layer-sticky);

	/* -- rows ------------------------------------------------------------- */
	--al-table-fill: transparent; /* the table paints nothing; it sits on the
	     ground it is given, and the card around it owns that */
	--al-table-row-fill: transparent; /* the state token — a row state re-points
	     THIS, and the cell reads it. Custom properties inherit, so a re-point on
	     the <tr> reaches every <td> inside it without the state rule ever having
	     to name a cell. */
	--al-table-row-fill-selected: var(--al-sys-color-row-tint-selected);
	--al-table-row-fill-active: var(--al-sys-color-row-tint-active);
	--al-table-row-rule-width: var(--al-sys-control-border-width);
	--al-table-row-rule-color: var(--al-sys-color-border);

	/* -- the total row. It separates by WEIGHT and a rule above it, the way a
	      total has been drawn on paper for two hundred years, not by a grey band.
	      Same opacity reasoning as the header: it is sticky to the bottom, so its
	      fill has to be opaque on both grounds. */
	--al-table-foot-fill: var(--al-sys-color-surface);
	--al-table-foot-text: var(--al-sys-color-text-primary);
	--al-table-foot-font-weight: var(--al-sys-font-weight-heading);
	--al-table-foot-rule-width: var(--al-sys-control-border-width);
	--al-table-foot-rule-color: var(--al-sys-color-border);
	--al-table-foot-offset: 0px;

	/* the gap between two quiet buttons in a row-actions cell. Small: their own
	   padding is already separating the labels, and 8px pushed them apart enough
	   to read as two groups rather than one set. */
	--al-table-actions-gap: var(--al-sys-space-50);

	/* THE HEADER'S ALIGNMENT IS A TOKEN, and it has to be. The browser centres a
	   <th>, so the table must state left — and a blanket `text-align: left` on
	   .al-table th outranks .al-num, which is a plain single-class utility. The
	   symptom was a numeric column whose name sat 23px to the left of the digits
	   it named, on every table in the product. Rule 8 again: do not set the
	   property, set the token, and let the more specific rule re-point it. */
	--al-table-header-align: left;

	--al-table-caption-text: var(--al-sys-color-text-muted);
	--al-table-caption-font-size: var(--al-sys-font-size-200);

	/* -- properties: only --al-table-* below this line -------------------- */
	/* border-collapse is SEPARATE, not collapse, and that is what makes the
	   sticky header work. With collapse the borders belong to the table rather
	   than to the cell, so the header's bottom rule stays behind and scrolls away
	   while the header itself sticks — a header with a line hanging under it in
	   the wrong place. Separate with zero spacing looks identical and does not
	   have that failure. */
	/* box-sizing is not inherited and it does not reach cells on its own, so all
	   three parts state it. Without it a column given a width in the markup
	   measures that width PLUS its padding and the columns stop adding up. */
	box-sizing: border-box;
	border-collapse: separate;
	border-spacing: 0;
	width: 100%;
	background: var(--al-table-fill);
	color: var(--al-table-text);
	font-family: var(--al-table-font-family);
	font-size: var(--al-table-font-size);
	line-height: var(--al-table-line-height);
}

.al-table caption {
	/* left, not the browser's centre. A caption is the accessible name of the
	   table and it reads as a heading, and a centred heading over a left-aligned
	   grid looks like a mistake in every table that has one. */
	text-align: left;
	padding: 0 var(--al-table-cell-padding-x) var(--al-table-cell-padding-y);
	color: var(--al-table-caption-text);
	font-size: var(--al-table-caption-font-size);
}

/* thead AND th are both sticky. thead is the one that carries the shadow, so it
   has to travel with it; th is the safety net, because a sticky row group is the
   younger of the two behaviours and a browser that ignores it still pins the
   cells. Belt and braces costs one declaration. */
.al-table thead {
	position: sticky;
	top: var(--al-table-header-offset);
	z-index: var(--al-table-header-layer);
	box-shadow: var(--al-table-header-elevation);
}

/* thead, NOT bare th. A <th scope="row"> in the body is still a th, so this rule --
   written for a COLUMN header -- was claiming row headers too and giving them sticky
   positioning, the header fill, nowrap, and the HEADER rule colour. That last one is
   what broke the bottom edge: `tr:last-child` drops the row rule by re-pointing
   --al-table-row-rule-color, which only td consumes, so the final row kept a visible
   line under its label and lost it under its value -- a rule that stops halfway across
   the table. nowrap did the other half, forcing a table 30px wider than its container
   and out through the card's rounded edge (ALLM-7205, measured on the Grow rail).

   .al-grid already learned this and says so at "A ROW HEADER IS A CELL"; this is the
   same fix on the same reasoning, applied to the control that never got it. */
.al-table thead th {
	box-sizing: border-box; /* not inherited, and a cell owns a width */
	position: sticky;
	top: var(--al-table-header-offset);
	z-index: var(--al-table-header-layer);
	height: var(--al-table-row-height);
	padding: var(--al-table-cell-padding-y) var(--al-table-cell-padding-x);
	text-align: var(--al-table-header-align);
	vertical-align: middle;
	white-space: nowrap; /* a column name that wraps changes the header's height
	     and drags every other column's name off the baseline with it */
	background: var(--al-table-header-fill);
	color: var(--al-table-header-text);
	font-size: var(--al-table-header-font-size);
	font-weight: var(--al-table-header-font-weight);
	border-bottom: var(--al-table-header-rule-width) solid var(--al-table-header-rule-color);
}

.al-table td,
.al-table tbody th {
	box-sizing: border-box; /* not inherited, and a cell owns a width */
	/* `height` on a cell is a MINIMUM in table layout, which is exactly what is
	   wanted: the tier is a floor, and a cell holding two lines grows past it
	   rather than clipping. Same reasoning as the button's min-height. */
	height: var(--al-table-row-height);
	padding: var(--al-table-cell-padding-y) var(--al-table-cell-padding-x);
	vertical-align: middle;
	background: var(--al-table-row-fill);
	border-bottom: var(--al-table-row-rule-width) solid var(--al-table-row-rule-color);
}

/* The UA centres a th and bolds it. Undone in its own rule rather than in the cell rule
   above, because that one covers td as well and a text-align there would outrank
   .al-num on specificity -- which is exactly how .al-grid lost its numeric alignment. */
.al-table tbody th {
	text-align: left;
	font-weight: inherit;
}

/* The last row keeps its rule at full WIDTH and drops its colour, rather than
   dropping the border. A table inside a card would otherwise draw a hairline
   right on top of the card's own edge, and removing the border instead would
   move every row up by a pixel when a row was deleted. */
.al-table tbody tr:last-child {
	--al-table-row-rule-color: transparent;
}

/* -- ROW STATES. Each one re-points --al-table-row-fill and nothing else, so the
      cell rule above never has to know a state exists. ---------------------- */

/* Selected is the QUIET step. It is a lasting fact about the row — you ticked
   it, and it stays ticked while you go and do something else. */
.al-table tbody tr[aria-selected='true'] {
	--al-table-row-fill: var(--al-table-row-fill-selected);
}

/* Where you are is the LOUD step, and it comes second on purpose: it ties with
   selected on specificity, so source order decides, and the pointer winning is
   correct. Hovering a chosen row should still light up, or the table stops
   responding to you exactly where you were working.

   :focus-within is what makes the keyboard and the pointer agree. A row is not
   focusable in this system — you reach it through the controls inside it, its
   tick box and its actions — so when one of those takes focus, the row it
   belongs to lights up the same way the pointer lights it. That is also why
   there is no row focus RING: the ring belongs to the control that has focus,
   and a second ring around the whole row would be a competing indicator. */
.al-table tbody tr:hover,
.al-table tbody tr:focus-within {
	--al-table-row-fill: var(--al-table-row-fill-active);
}

/* -- the total row ------------------------------------------------------- */
.al-table tfoot td {
	background: var(--al-table-foot-fill);
	color: var(--al-table-foot-text);
	font-weight: var(--al-table-foot-font-weight);
	border-bottom: 0;
	border-top: var(--al-table-foot-rule-width) solid var(--al-table-foot-rule-color);
}
/* THE TOTAL PINS ONLY INSIDE A BOUNDED VIEWPORT, and the header's rule does not
   need the same qualification. Both directions of sticky clamp to the scrollport
   edge, but they fail differently when the scrollport is the whole page. A header
   pinned to the top of the window is the behaviour you wanted anyway. A footer
   pinned to the BOTTOM of the window lands wherever the fold happens to be, so a
   long table on a scrolling page drew its total across the middle of its own
   rows — seen in a full-page render, and it is the real behaviour, not an
   artefact of the capture. Inside .al-table-scroll the scrollport is the table's
   own region and the total sits on its floor, which is the case worth having. */
.al-table-scroll .al-table tfoot td {
	position: sticky;
	bottom: var(--al-table-foot-offset);
	z-index: var(--al-table-header-layer);
}

/* the total row is not one of the rows you are picking through, so it does not
   light up under the pointer */
.al-table tfoot tr:hover,
.al-table tfoot tr:focus-within {
	--al-table-row-fill: transparent;
}

/* -- two column shapes ---------------------------------------------------- */

/* A NUMERIC COLUMN'S NAME SITS OVER ITS NUMBERS. The table spots the class and
   re-points its OWN header-alignment token, the same move .al-affix makes when it
   spots .al-input-sm. It does not read the number's token: that would be a
   component reaching into another component, and the header's alignment is the
   table's fact, not the cell's. */
.al-table th.al-num {
	--al-table-header-align: right;
}

/* As narrow as its content. A tick box column, an actions column, a status
   column: all of them should take what they need and give the rest to the
   columns carrying names. 1% rather than 0 because a table distributes leftover
   width proportionally, and 0 is a share of nothing in some engines. */
.al-table .al-fit {
	width: 1%;
	white-space: nowrap;
}

/* A row's buttons. A div inside the cell rather than the cell itself, because
   display:flex on a <td> stops it being a table cell and the column alignment
   goes with it.

   THE BUTTONS ARE ALWAYS VISIBLE, never revealed on hover. Hover-reveal is
   invisible on a touch screen, invisible to anyone scanning with the keyboard,
   and it makes the table twitch as the pointer crosses it. Quiet buttons that
   are always there cost a little ink and are always findable. */
.al-table-actions {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: var(--al-table-actions-gap);
}

/* -- density ------------------------------------------------------------- */
/* Four re-points each, and the type moves with the box: the header is always one
   rung below the body, so the two can never drift apart. The large tier takes
   the 17px rung on purpose — large is the touch rung in this system, which is a
   register screen, and a 52px row carrying 14px text looks broken. */
.al-table-sm {
	--al-table-row-height: var(--al-sys-row-height-sm);
	--al-table-cell-padding-x: var(--al-sys-control-padding-x-sm);
	--al-table-font-size: var(--al-sys-font-size-200);
	--al-table-header-font-size: var(--al-sys-font-size-100);
}
.al-table-lg {
	--al-table-row-height: var(--al-sys-row-height-lg);
	--al-table-cell-padding-x: var(--al-sys-control-padding-x-lg);
	--al-table-font-size: var(--al-sys-font-size-400);
	--al-table-header-font-size: var(--al-sys-font-size-300);
}

/* =============================================================================
   THE SORTABLE HEADER

   A real <button> inside the <th>, and the aria-sort lives on the <th>. That is
   the shape assistive technology expects, and keying the appearance off aria-sort
   means the same discipline the button's busy state uses: you cannot style the
   sorted column without also announcing it.

   It inherits the header's type entirely rather than owning size tokens of its
   own, which is how it follows the table's density for free. A control that had
   to be re-pointed by every density tier is a control that will be forgotten by
   the fourth one.
   ============================================================================= */
.al-sort {
	--al-sort-text: inherit;
	--al-sort-gap: var(--al-sys-control-gap);
	--al-sort-radius: var(--al-sys-radius-sm);
	/* WHICH SIDE THE MARK SITS ON, and how wide the button is, are TOKENS because a
	   numeric column needs both changed and rule 8 says a variant re-points rather
	   than overrides. Note what is NOT here: justify-content. In row-reverse the
	   main axis starts on the RIGHT, so the default packing already puts the label
	   flush against the column's edge. Stating flex-end instead packed the whole
	   header 53px to the LEFT of the digits it names, measured. */
	--al-sort-direction: row;
	--al-sort-width: auto;
	--al-sort-mark: var(--al-sys-color-text-muted);
	--al-sort-mark-size: var(--al-sys-icon-size-sm);
	/* the unsorted mark is present but faint. Faint rather than absent, because a
	   mark that only appears on hover cannot tell a touch user or someone
	   scanning the header that the column sorts at all. */
	--al-sort-mark-opacity: 0.55;
	/* THE MARK IS A MASK, so the shape comes from the SVG and the colour comes
	   from a token — the pattern the checkbox uses, and the reason the select's
	   caret had to bake its colour in. Two adjacent triangles: sortable, in
	   neither direction yet. */
	--al-sort-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 2.2 12.2 7H3.8z'/%3E%3Cpath d='M8 13.8 3.8 9h8.4z'/%3E%3C/svg%3E");

	--al-sort-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-sort-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-sort-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-sort-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-sort-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: inline-flex;
	flex-direction: var(--al-sort-direction);
	align-items: center;
	gap: var(--al-sort-gap);
	width: var(--al-sort-width);
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: var(--al-sort-radius);
	background: none;
	color: var(--al-sort-text);
	font: inherit; /* the header's size, weight and family, whatever the tier */
	cursor: pointer;
}

.al-sort::after {
	content: '';
	flex: none;
	box-sizing: border-box;
	width: var(--al-sort-mark-size);
	height: var(--al-sort-mark-size);
	background: var(--al-sort-mark);
	opacity: var(--al-sort-mark-opacity);
	-webkit-mask: var(--al-sort-mark-image) center / contain no-repeat;
	mask: var(--al-sort-mark-image) center / contain no-repeat;
}

/* Hover darkens the label and brings the mark up to full strength. No background
   tint: a tinted header cell reads as a SELECTED column, which is a different
   thing that this table does not have. */
.al-sort:hover {
	--al-sort-text: var(--al-sys-color-text-primary);
	--al-sort-mark-opacity: 1;
}

.al-sort:focus {
	outline: none;
}
/* the three-layer ring, copied from the button: a light hairline, the ring, a
   dark hairline. One of the two hairlines always has contrast, so the indicator
   cannot vanish — including against a header the same colour as the ring. */
.al-sort:focus-visible {
	outline: var(--al-sort-focus-ring-width) solid transparent;
	outline-offset: var(--al-sort-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-sort-focus-hairline-width) var(--al-sort-focus-hairline-light),
		0 0 0 calc(var(--al-sort-focus-hairline-width) + var(--al-sort-focus-ring-width)) var(--al-sort-focus-ring-color),
		0 0 0 calc(var(--al-sort-focus-hairline-width) * 2 + var(--al-sort-focus-ring-width)) var(--al-sort-focus-hairline-dark);
}

/* THE SORTED COLUMN TAKES THE ACCENT. Which column the table is sorted by is the
   one fact about its current state that has to read at a glance, and the accent
   is how this system says "this is live". It reads the LINE rung, so it lightens
   on a dark ground with everything else. */
.al-table th[aria-sort='ascending'] .al-sort,
.al-table th[aria-sort='descending'] .al-sort {
	--al-sort-text: var(--al-sys-color-text-primary);
	--al-sort-mark: var(--al-sys-color-accent-primary-line);
	--al-sort-mark-opacity: 1;
}
.al-table th[aria-sort='ascending'] .al-sort {
	--al-sort-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 3.6 13.4 11H2.6z'/%3E%3C/svg%3E");
}
.al-table th[aria-sort='descending'] .al-sort {
	--al-sort-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 12.4 2.6 5h10.8z'/%3E%3C/svg%3E");
}

/* A NUMERIC COLUMN SORTS FROM THE OTHER END. Reversing the row puts the mark on
   the left of the label, which keeps the label itself flush with the column's
   right edge — so the header sits directly over the digits it names instead of
   being pushed 20px off them by its own mark. The full width is what gives the
   reversed row something to pack against; the packing itself is the default. */
.al-table th.al-num .al-sort {
	--al-sort-direction: row-reverse;
	--al-sort-width: 100%;
}

/* =============================================================================
   THE LIST

   Rows that are not a table because there are no columns to align. A batch and
   its room and its plant count are three facts about one thing, not three
   columns you compare down the page — so they get a title line and a meta line
   instead of cells, and the row is free to be as tall as it needs.

   The tier is a FLOOR here too. A one-line row IS the tier exactly; add a meta
   line and it grows past it. That is the same promise the button makes and it is
   the honest one: the alternative is clipping the second line.
   ============================================================================= */
.al-list {
	--al-list-padding: 0;
	--al-list-font-family: var(--al-sys-font-family);

	--al-list-row-height: var(--al-sys-row-height-md);
	--al-list-row-padding-x: var(--al-sys-control-padding-x-md);
	/* 4px, not 8, and it is the row height that decides. At 8px a one-line row
	   measured 38.7px, so the SMALL rung of the density axis never took effect
	   at all: .al-list-sm and .al-list-rail both asked for 36 and both got 39.
	   A tier rung that can never be reached is not a tier. Same reasoning and
	   the same value as the table's cell padding: this exists only to keep a
	   two-line row off the hairlines above and below it. */
	--al-list-row-padding-y: var(--al-sys-space-50);
	--al-list-row-gap: var(--al-sys-space-150);
	--al-list-row-radius: 0;
	--al-list-line-height: var(--al-sys-line-height-body);

	--al-list-row-fill: transparent;
	/* transparent by default is what makes .al-list-nav a pure token re-point:
	   the hover rule below always exists, it just resolves to nothing until a
	   list says its rows are targets. A plain list of facts should not light up
	   under a pointer that is only passing over it. */
	--al-list-row-fill-hover: transparent;
	--al-list-row-fill-selected: var(--al-sys-color-row-tint-selected);
	--al-list-row-fill-active: var(--al-sys-color-row-tint-active);
	--al-list-row-rule-width: var(--al-sys-control-border-width);
	--al-list-row-rule-color: var(--al-sys-color-border);

	/* the row is usually an <a>, which makes it a focusable control, and every
	   focusable control in this system rings the same way */
	--al-list-row-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-list-row-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-list-row-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-list-row-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-list-row-focus-ring-color: var(--al-sys-color-focus-ring);

	/* the bar down the leading edge of the chosen row. Transparent until
	   something is chosen, so it is a re-point rather than a second rule. Unlike
	   a table row — which is one of a grid disambiguated by a tick box — a list
	   row often has to say "you are here" in a rail four inches wide, and shape
	   survives a colour-blind reader and a bad screen where a tint may not. */
	--al-list-row-marker: transparent;
	--al-list-row-marker-width: 3px;

	--al-list-text: var(--al-sys-color-text-primary);
	--al-list-title-size: var(--al-sys-font-size-300);
	--al-list-title-weight: var(--al-sys-font-weight-medium);
	--al-list-meta-size: var(--al-sys-font-size-100);
	--al-list-meta-color: var(--al-sys-color-text-muted);
	--al-list-body-gap: var(--al-sys-space-25);
	--al-list-side-size: var(--al-sys-font-size-300);
	--al-list-side-color: var(--al-sys-color-text-secondary);

	/* 32px is not a control tier and should not be: this is a badge, not
	   something you press. It is the size two initials need to sit comfortably,
	   and it leaves a 44px row looking like a row rather than a strip of
	   avatars. */
	--al-list-lead-size: 32px;
	--al-list-lead-fill: var(--al-sys-color-surface-sunken);
	--al-list-lead-text: var(--al-sys-color-text-secondary);
	--al-list-lead-size-text: var(--al-sys-font-size-100);
	--al-list-lead-weight: var(--al-sys-font-weight-control);
	--al-list-lead-radius: var(--al-sys-radius-pill); /* a badge, so the pill rung
	     rather than the control rung — it is not something you press */

	box-sizing: border-box;
	list-style: none;
	margin: 0;
	/* the system owns its own box, the same argument as box-sizing one tier up. A
	   list is usually a <ul>, and a host page that caps ul at a reading measure
	   silently stops the rows short of the panel they are in — measured on a doc
	   page with `ul { max-width: 86ch }`, where the tint ended in the middle of
	   the card. A control that changes shape depending on the page it is dropped
	   into is not a control. */
	max-width: none;
	/* and min-width:0, so a flex parent can squeeze it. A flex item refuses to
	   shrink below its content width by default, and this control's content is a
	   nowrap title — so dropped into a flex row it would push itself wider than
	   the panel and the truncation it promises would never fire. Block layout
	   gets this right on its own; flex does not, and half the panels in the
	   product are flex. */
	min-width: 0;
	padding: var(--al-list-padding);
	font-family: var(--al-list-font-family);
}

.al-list-row {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-list-row-gap);
	min-height: var(--al-list-row-height);
	padding: var(--al-list-row-padding-y) var(--al-list-row-padding-x);
	border-radius: var(--al-list-row-radius);
	border-bottom: var(--al-list-row-rule-width) solid var(--al-list-row-rule-color);
	background: var(--al-list-row-fill);
	box-shadow: inset var(--al-list-row-marker-width) 0 0 var(--al-list-row-marker);
	color: var(--al-list-text);
	line-height: var(--al-list-line-height); /* set, not inherited — see the table */
	text-decoration: none; /* a row is often an <a>, and it is a row, not a link */
}

.al-list > *:last-child .al-list-row,
.al-list > .al-list-row:last-child {
	--al-list-row-rule-color: transparent;
}

/* the leading badge. What goes in it — initials, an icon, a status dot — is the
   author's; the slot only promises it will not shrink. */
.al-list-lead {
	box-sizing: border-box;
	flex: none;
	display: flex;
	align-items: center;
	justify-content: center;
	width: var(--al-list-lead-size);
	height: var(--al-list-lead-size);
	border-radius: var(--al-list-lead-radius);
	background: var(--al-list-lead-fill);
	color: var(--al-list-lead-text);
	font-size: var(--al-list-lead-size-text);
	font-weight: var(--al-list-lead-weight);
}

/* min-width:0 is what lets the middle give way. Without it a flex item refuses to
   shrink below its content width, the row overflows, and the ellipsis that both
   lines below ask for never appears. */
.al-list-body {
	flex: 1 1 auto;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: var(--al-list-body-gap);
}

.al-list-title,
.al-list-meta {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.al-list-title {
	font-size: var(--al-list-title-size);
	font-weight: var(--al-list-title-weight);
}
.al-list-meta {
	font-size: var(--al-list-meta-size);
	color: var(--al-list-meta-color);
}

.al-list-side {
	flex: none;
	color: var(--al-list-side-color);
	font-size: var(--al-list-side-size);
}

/* -- states. All four re-point --al-list-row-fill or the marker, never a
      property, so the base row rule never learns a state exists. ----------- */
.al-list-row:hover {
	--al-list-row-fill: var(--al-list-row-fill-hover);
}
.al-list-row[aria-current],
.al-list-row[aria-selected='true'] {
	--al-list-row-fill: var(--al-list-row-fill-selected);
	--al-list-row-marker: var(--al-sys-color-accent-primary-line);
}
/* both hooks, because a list does two jobs and each has its own correct markup:
   aria-current="page" for a navigation rail, aria-selected for a row you picked
   out of a set. Styling only one of them would have quietly pushed authors into
   the wrong one. */
.al-list-row[aria-current]:hover,
.al-list-row[aria-selected='true']:hover,
.al-list-row:focus-within {
	--al-list-row-fill: var(--al-list-row-fill-active);
}

/* THE ROW IS A CONTROL WHEN IT IS A LINK, so it gets the system's ring rather
   than the browser's. Measured before this existed: a focused row wore Chrome's
   own `outline: auto` in the platform's near-black. That is an indicator no
   theme can reach, and the only control in the recipe not wearing the
   three-layer ring. A tint alone will not do either: it is the same tint that
   hover already paints.

   The inset marker is restated first because box-shadow is one property, not a
   list a second rule can add to: a ring that did not carry the marker would
   erase the chosen row's leading bar the moment the keyboard reached it. */
.al-list-row:focus {
	outline: none;
}
.al-list-row:focus-visible {
	outline: var(--al-list-row-focus-ring-width) solid transparent;
	outline-offset: var(--al-list-row-focus-hairline-width);
	box-shadow:
		inset var(--al-list-row-marker-width) 0 0 var(--al-list-row-marker),
		0 0 0 var(--al-list-row-focus-hairline-width) var(--al-list-row-focus-hairline-light),
		0 0 0 calc(var(--al-list-row-focus-hairline-width) + var(--al-list-row-focus-ring-width)) var(--al-list-row-focus-ring-color),
		0 0 0 calc(var(--al-list-row-focus-hairline-width) * 2 + var(--al-list-row-focus-ring-width)) var(--al-list-row-focus-hairline-dark);
}

/* -- variants ------------------------------------------------------------- */

/* EVERY ROW HERE IS A TARGET. One line to say so, and the hover rule that was
   already written starts resolving to something. */
.al-list-nav {
	--al-list-row-fill-hover: var(--al-sys-color-row-tint-active);
}

/* A NAVIGATION RAIL. No rules between items — a rail is a set of destinations,
   not a ledger — and rounded rows so the chosen tint reads as a chip rather than
   a band running out of both sides of the panel. */
.al-list-rail {
	--al-list-padding: var(--al-sys-space-50);
	--al-list-row-fill-hover: var(--al-sys-color-row-tint-active);
	--al-list-row-rule-color: transparent;
	--al-list-row-radius: var(--al-sys-radius-sm);
	--al-list-row-height: var(--al-sys-row-height-sm);
}

.al-list-sm {
	--al-list-row-height: var(--al-sys-row-height-sm);
	--al-list-row-padding-x: var(--al-sys-control-padding-x-sm);
	--al-list-title-size: var(--al-sys-font-size-200);
	--al-list-side-size: var(--al-sys-font-size-200);
	--al-list-lead-size: 26px;
}
.al-list-lg {
	--al-list-row-height: var(--al-sys-row-height-lg);
	--al-list-row-padding-x: var(--al-sys-control-padding-x-lg);
	--al-list-title-size: var(--al-sys-font-size-400);
	--al-list-side-size: var(--al-sys-font-size-400);
	--al-list-meta-size: var(--al-sys-font-size-200);
	--al-list-lead-size: 40px;
}

/* =============================================================================
   THE PAIR — a label and a fact, repeated.

   This is the control people build out of disabled inputs, and it is worth being
   blunt about why that is wrong. A disabled input says "you could change this,
   but not now". A shipped manifest's departure time is not something you could
   change now or later — it is a fact you are being shown. Dressing it as a
   control that has been switched off makes every read-only screen look broken,
   costs a tab stop each, and dims the very text someone came to read to half
   strength.

   It is a <dl>, with each pair wrapped in a <div>. That is the correct markup for
   a description list, it is valid HTML, and it means the parts need no classes —
   the same instinct as .al-field > span.

   LAYOUT IS A TOKEN, not a property, so the inline variant is a re-point like
   every other variant in the system: --al-kv-direction flips column to row and
   the rest follows.
   ============================================================================= */
.al-kv {
	--al-kv-columns: 1;
	--al-kv-direction: column;
	--al-kv-gap: var(--al-sys-space-25); /* between a label and its own fact */
	--al-kv-row-gap: var(--al-sys-space-200); /* between one pair and the next */
	--al-kv-column-gap: var(--al-sys-space-400);
	--al-kv-pair-padding-y: 0;
	--al-kv-label-width: auto;
	--al-kv-align: stretch;
	--al-kv-font-family: var(--al-sys-font-family);

	/* the label is the quiet one and the fact is the loud one. Weight-medium on
	   the label is the role's stated job — "a label that must separate from its
	   value" — with the ink rung and the size doing most of the separating. */
	--al-kv-label-size: var(--al-sys-font-size-200);
	--al-kv-label-weight: var(--al-sys-font-weight-medium);
	--al-kv-label-color: var(--al-sys-color-text-muted);
	--al-kv-value-size: var(--al-sys-font-size-300);
	--al-kv-value-weight: var(--al-sys-font-weight-body);
	--al-kv-value-color: var(--al-sys-color-text-primary);
	--al-kv-value-line-height: var(--al-sys-line-height-body);

	--al-kv-rule-width: var(--al-sys-control-border-width);
	--al-kv-rule-color: transparent;

	box-sizing: border-box;
	margin: 0;
	min-width: 0; /* same reason as the list: a flex parent must be able to
	     squeeze it, or a long fact widens the panel instead of wrapping */
	display: grid;
	/* a custom property as the repeat count works because custom properties are
	   substituted before the value is parsed. It is what lets .al-kv-2 be one
	   line instead of a second grid declaration. */
	grid-template-columns: repeat(var(--al-kv-columns), minmax(0, 1fr));
	gap: var(--al-kv-row-gap) var(--al-kv-column-gap);
	font-family: var(--al-kv-font-family);
}

.al-kv > div {
	box-sizing: border-box;
	display: flex;
	flex-direction: var(--al-kv-direction);
	align-items: var(--al-kv-align);
	gap: var(--al-kv-gap);
	padding-bottom: var(--al-kv-pair-padding-y);
	border-bottom: var(--al-kv-rule-width) solid var(--al-kv-rule-color);
}
.al-kv > div:last-child {
	--al-kv-rule-color: transparent;
}

.al-kv dt {
	flex: none;
	/* box-sizing is not inherited, and this is the one part of the pair that owns a
	   width. A host page that gives its dt any padding would otherwise push the
	   label column past the 40% the inline variant asked for. */
	box-sizing: border-box;
	width: var(--al-kv-label-width);
	font-size: var(--al-kv-label-size);
	font-weight: var(--al-kv-label-weight);
	color: var(--al-kv-label-color);
	line-height: var(--al-kv-value-line-height);
}

.al-kv dd {
	flex: 1 1 auto;
	min-width: 0;
	margin: 0;
	font-size: var(--al-kv-value-size);
	font-weight: var(--al-kv-value-weight);
	color: var(--al-kv-value-color);
	line-height: var(--al-kv-value-line-height);
}

/* A FACT THAT DOES NOT EXIST still occupies its slot. Leaving the <dd> empty
   would make a two-column panel go ragged and would leave the reader unsure
   whether the value is missing or the page is. The mark is a typographic
   convention for "no value", not a sentence, and it takes the quiet ink so it
   never reads as data. */
.al-kv dd:empty::before {
	content: '\2014';
	color: var(--al-kv-label-color);
}

/* -- variants ------------------------------------------------------------- */

/* ONE LINE PER FACT. A ledger, a totals panel, a summary in a narrow drawer. The
   rule under each pair is what carries the eye from the label across to the
   number, which is the whole reason this layout exists. */
.al-kv-inline {
	--al-kv-direction: row;
	--al-kv-gap: var(--al-sys-space-200);
	--al-kv-row-gap: var(--al-sys-space-100);
	--al-kv-pair-padding-y: var(--al-sys-space-100);
	--al-kv-align: baseline;
	--al-kv-rule-color: var(--al-sys-color-border);
	/* a percentage, not a rem width: the same panel is a 300px drawer on one
	   screen and half a page on another, and a fixed label column is wrong on one
	   of them. */
	--al-kv-label-width: 40%;
}

.al-kv-2 {
	--al-kv-columns: 2;
}
.al-kv-3 {
	--al-kv-columns: 3;
}

/* =============================================================================
   THE EMPTY STATE

   TWO STATES, NOT ONE, and the difference is the whole point. "Nothing here yet"
   is a beginning: the reader has done nothing wrong and the way forward is to
   make the first one, so it carries a filled button. "Nothing matched your
   filter" is a dead end: there IS data, the reader just cannot see it, and the
   only useful action is to undo what they did — so it carries a quiet button
   that widens the net, and no invitation to create anything.

   Shipping one component with one piece of copy for both is the common failure,
   and it produces "Create your first package" on a screen where the customer has
   four thousand packages and a bad date filter.

   The CSS difference between them is small and deliberate: a different mark, and
   a quieter mark colour, because a dead end should not look like an opportunity.
   The COPY is the author's, and the doc spells out what to write.
   ============================================================================= */
.al-empty {
	--al-empty-padding-y: var(--al-sys-space-600);
	--al-empty-padding-x: var(--al-sys-space-300);
	--al-empty-gap: var(--al-sys-space-100);

	/* 48px is a decorative badge, not a control, so it takes none of the control
	   tiers. Large enough to read as a deliberate mark and small enough that it
	   does not become the subject of the screen. */
	--al-empty-mark-size: 48px;
	--al-empty-mark-inset: 13px;
	--al-empty-mark-radius: var(--al-sys-radius-pill);
	--al-empty-mark-fill: var(--al-sys-color-surface-sunken);
	/* the accent, because "nothing here yet" is where your work is going to go
	   and the accent is how this system points at an action */
	--al-empty-mark-color: var(--al-sys-color-accent-primary-line);
	--al-empty-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M3 7.8 12 3.4l9 4.4v8.4L12 20.6 3 16.2z'/%3E%3Cpath d='M3 7.8 12 12.2l9-4.4'/%3E%3Cpath d='M12 12.2v8.4'/%3E%3C/g%3E%3C/svg%3E");

	--al-empty-title-size: var(--al-sys-font-size-400);
	--al-empty-title-weight: var(--al-sys-font-weight-heading);
	--al-empty-title-color: var(--al-sys-color-text-primary);
	--al-empty-title-line-height: var(--al-sys-line-height-heading);
	--al-empty-body-size: var(--al-sys-font-size-300);
	--al-empty-body-weight: var(--al-sys-font-weight-body);
	--al-empty-body-color: var(--al-sys-color-text-secondary);
	--al-empty-body-line-height: var(--al-sys-line-height-body);
	/* not --al-sys-measure. 68ch is a measure for PROSE; an empty state that runs
	   to 68 characters a line becomes a paragraph, and nobody reads a paragraph
	   on a screen that says there is nothing to look at. Two short lines. */
	--al-empty-measure: 42ch;
	--al-empty-actions-gap: var(--al-sys-space-100);
	--al-empty-font-family: var(--al-sys-font-family);

	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--al-empty-gap);
	padding: var(--al-empty-padding-y) var(--al-empty-padding-x);
	text-align: center;
	font-family: var(--al-empty-font-family);
}

.al-empty-mark {
	box-sizing: border-box;
	flex: none;
	width: var(--al-empty-mark-size);
	height: var(--al-empty-mark-size);
	border-radius: var(--al-empty-mark-radius);
	background: var(--al-empty-mark-fill);
	margin-bottom: var(--al-empty-gap);
}
/* the glyph is a mask over the badge, so its colour is a token and a theme can
   move it — the same pattern as the checkbox mark and the sort caret */
.al-empty-mark::before {
	content: '';
	display: block;
	height: 100%;
	background: var(--al-empty-mark-color);
	-webkit-mask: var(--al-empty-mark-image) center / calc(100% - var(--al-empty-mark-inset) * 2) no-repeat;
	mask: var(--al-empty-mark-image) center / calc(100% - var(--al-empty-mark-inset) * 2) no-repeat;
}

/* THE TITLE IS USUALLY AN <h3>, AND THAT IS WHERE IT GOT BITTEN. The host page
   styled its own headings — uppercase, letter-spaced, brand-coloured — and the
   empty state inherited every bit of it, so "No manifests yet" rendered as
   "NO MANIFESTS YET" in a spaced-out micro-caps that belongs to the page, not to
   the control. Same class of failure as borrowing box-sizing or line-height: a
   control that changes shape depending on the page it is dropped into is not a
   control. So it states its own type completely, including the two properties
   nobody thinks to state. */
.al-empty-title {
	margin: 0;
	font-family: var(--al-empty-font-family);
	font-size: var(--al-empty-title-size);
	font-weight: var(--al-empty-title-weight);
	color: var(--al-empty-title-color);
	line-height: var(--al-empty-title-line-height);
	letter-spacing: normal;
	text-transform: none;
}

.al-empty-body {
	margin: 0;
	max-width: var(--al-empty-measure);
	font-family: var(--al-empty-font-family);
	font-size: var(--al-empty-body-size);
	font-weight: var(--al-empty-body-weight);
	color: var(--al-empty-body-color);
	line-height: var(--al-empty-body-line-height);
	letter-spacing: normal;
	text-transform: none;
}

.al-empty-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: var(--al-empty-actions-gap);
	margin-top: var(--al-empty-gap);
}

/* NOTHING MATCHED. A magnifier instead of a package, and the muted ink instead
   of the accent: this is not an opportunity, it is a dead end, and colouring it
   like an invitation would be lying about which one the reader is in. */
.al-empty-filtered {
	--al-empty-mark-color: var(--al-sys-color-text-muted);
	--al-empty-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='10.2' cy='10.2' r='6.3'/%3E%3Cpath d='M14.9 14.9 20.4 20.4'/%3E%3C/g%3E%3C/svg%3E");
}

/* A SMALL REGION — a card, a side panel, a widget. The full-size empty state in
   a 200px-tall card pushes its own button out of the box. */
.al-empty-sm {
	--al-empty-padding-y: var(--al-sys-space-300);
	--al-empty-mark-size: 34px;
	--al-empty-mark-inset: 9px;
	--al-empty-title-size: var(--al-sys-font-size-300);
	--al-empty-body-size: var(--al-sys-font-size-100);
	--al-empty-measure: 34ch;
}

/* ---- marks ---------------------------------------------------- */
/* =============================================================================
   MARKS — the avatar, the badge, the tag, the progress indicator, the skeleton.

   Five things a screen puts on a record rather than next to it. Four of them state
   a fact and take no action; the fifth is the shape of a record that has not
   arrived yet. They are written together because they share one failure mode: a
   mark that carries its meaning in colour alone means nothing on a black-and-white
   picking sheet, on a sun-washed warehouse screen, or to the one man in twelve who
   cannot separate the red from the green. So every mark here carries words, and
   every coloured one carries a glyph as well. The hue is the third channel, never
   the first.

   WHERE THE TOKENS ARE DECLARED, and why it is not :root. A var() inside a custom
   property resolves against the element the property is DECLARED on, so a ladder
   sitting on :root can never see a variant's override. The button already learned
   this; these follow it.

   WHAT READS WHAT. Every property below reads only its own --al-<component>-*
   token. Every sys role appears exactly once, on the right of a component token —
   that is the opt-in. Nothing in this file names a control inside a ground, which
   is what makes all five correct on a dark panel without a line of dark CSS.
   ============================================================================= */

/* =============================================================================
   THE AVATAR

   A person, at the size of a control, as a picture or as their initials. It is a
   MARK, not a control: there is no hover, no focus, no press. If the person is
   clickable, the row or the name is the target and the disc rides along — an 36px
   disc is a poor target and a second tab stop nobody wanted.

   IT TAKES THE CONTROL TIERS, not the row tiers. An avatar's whole job is to sit
   in a line with a name, a button and a field, and the control ladder is what
   those three are already on. On the row ladder it would stand 8px proud of every
   button beside it.
   ============================================================================= */
.al-avatar {
	--al-avatar-size: var(--al-sys-control-height-md);
	--al-avatar-radius: var(--al-sys-radius-pill);
	--al-avatar-fill: var(--al-sys-color-surface-sunken);
	--al-avatar-text: var(--al-sys-color-text-secondary);
	/* The face is NAMED, not inherited. The sys tier is explicit about this: a
	   control that changes shape depending on the page it is dropped into is not a
	   control, and initials are a shape before they are words. */
	--al-avatar-font-family: var(--al-sys-font-family);
	--al-avatar-font-size: var(--al-sys-font-size-300);
	--al-avatar-font-weight: var(--al-sys-font-weight-control);
	/* A photograph with a pale edge stops being a disc on a white card. The hairline
	   is what keeps the shape when the picture will not. */
	--al-avatar-hairline-width: var(--al-sys-control-border-width);
	--al-avatar-hairline-color: var(--al-sys-color-border);
	/* Zero by default, so a lone avatar paints no ring at all. The group re-points
	   it — that is the only place a ring has a job to do. */
	--al-avatar-ring-width: 0;
	--al-avatar-ring-color: var(--al-sys-color-surface);
	/* A RATIO, not a length, so a group of small discs laps the same SHARE a group of
	   large ones does. Held unitless and multiplied by the size in the rule that uses
	   it, rather than as calc(size * 0.16) here — that would make one component token
	   read another outside the sanctioned intent ladder, which is the aliasing rule 2
	   forbids. The arithmetic is the same; where it happens is what keeps the token
	   block flat.

	   A GROUPED AVATAR CARRIES ONE INITIAL, and that is the decision the number
	   depends on. Measured on the render, with the ring counted as part of what the
	   disc in front covers: TWO initials have 2.9px of clear space beside them at the
	   small tier and the lap plus the ring takes 7.6px, so DW MO PR comes out DV MC PR.
	   It fails at every tier, not only the small one: md is short by 3.1px and lg by
	   3.3px. No lap survives two letters, so there is no ratio that fixes it and the
	   content rule is the fix.

	   0.16 is then set by the widest SINGLE letter. The small tier binds, because its
	   type is floored at 13px rather than scaled down, so it carries the widest letter
	   relative to its disc. Swept A to Z there: W is the worst and clears by 1.19px at
	   0.16 and by 0.06px at a fifth, which is a rounding error away from touching. An
	   overlap that clears the small tier clears the other two. */
	--al-avatar-group-overlap-ratio: 0.16;

	/* -- properties: only --al-avatar-* below this line --------------------- */
	box-sizing: border-box; /* not inherited, and a disc that measures 38 because the
	     host page has no reset is not a tier */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none; /* a flex parent may not squash a face into an oval */
	position: relative;
	width: var(--al-avatar-size);
	height: var(--al-avatar-size);
	border-radius: var(--al-avatar-radius);
	background: var(--al-avatar-fill);
	color: var(--al-avatar-text);
	font-family: var(--al-avatar-font-family);
	font-size: var(--al-avatar-font-size);
	font-weight: var(--al-avatar-font-weight);
	line-height: 1; /* structural: the disc owns the height and flex owns the
	     centring. Inherit a page's 1.65 here and the initials sit low. */
	letter-spacing: 0.01em;
	overflow: hidden; /* clips the picture to the disc. box-shadow is not clipped by
	     overflow, so the group ring still paints outside it. */
	user-select: none;
	box-shadow: 0 0 0 var(--al-avatar-ring-width) var(--al-avatar-ring-color);
}

/* The picture fills the disc and is cropped to it, never squashed — a portrait and
   a landscape photo have to come out the same shape. */
.al-avatar > img {
	box-sizing: border-box; /* not inherited, and a host page that puts a border on img
	     would otherwise push the picture past the disc it is cropped to */
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: inherit;
}

/* The hairline is a pseudo-element, not an inset box-shadow on the disc itself: an
   inset shadow paints under the children, so a photograph would cover it. */
.al-avatar::after {
	content: '';
	position: absolute;
	inset: 0;
	border-radius: inherit;
	box-shadow: inset 0 0 0 var(--al-avatar-hairline-width) var(--al-avatar-hairline-color);
	pointer-events: none;
}

/* Size re-points the tier and the type together, exactly as the button does, so a
   small avatar cannot end up wearing large initials. */
.al-avatar-sm {
	--al-avatar-size: var(--al-sys-control-height-sm);
	--al-avatar-font-size: var(--al-sys-font-size-200);
}
.al-avatar-lg {
	--al-avatar-size: var(--al-sys-control-height-lg);
	--al-avatar-font-size: var(--al-sys-font-size-400);
}

/* ONE avatar may be the accent: the signed-in user, or the person a row is
   assigned to. Not a decoration — if every avatar is accented the accent says
   nothing. The hairline goes because a filled disc is already its own shape. */
.al-avatar-accent {
	--al-avatar-fill: var(--al-sys-color-accent-primary);
	--al-avatar-text: var(--al-sys-color-text-on-accent);
	--al-avatar-hairline-color: transparent;
}

/* -----------------------------------------------------------------------------
   A GROUP — several people on one line, overlapping.

   The ring is the GROUND showing through between two discs, which is why it reads
   --al-sys-color-surface and why it is correct on a dark panel without being told.
   Its width comes off the spacing scale's hairline rung rather than a number
   invented here.

   DOM ORDER IS PAINT ORDER, so each disc laps the one before it and the overflow
   count lands on top at the end of the stack. That reads as a queue with a total
   on the end, which is what it is.
   ----------------------------------------------------------------------------- */
.al-avatar-group {
	display: inline-flex;
	align-items: center;
}
.al-avatar-group > .al-avatar {
	--al-avatar-ring-width: var(--al-sys-space-25);
}
.al-avatar-group > .al-avatar + .al-avatar {
	margin-left: calc(var(--al-avatar-size) * var(--al-avatar-group-overlap-ratio) * -1);
}

/* =============================================================================
   THE BADGE

   A static label that says what a thing IS: fulfilled, expiring, failed, in
   transit, draft. It is NOT a button even when it looks like one — no hover, no
   press, no focus, no cursor change. A badge you can click is a badge someone will
   click, and then nothing happens.

   THE FOUR STATUS INTENTS PLUS A NEUTRAL, and all five are built the same way, out
   of the three parts the sys tier already names a status by: the MARK (the edge and
   the glyph), the TINT it sits on, and the TEXT on that tint.

   TWO PLACES THIS DEPARTS FROM THE OBVIOUS READING, both measured:

   1. THE TINT IS MIXED, not the --tint role. --al-sys-color-status-*-tint is a
      fixed near-white and no ground re-points it, so on a dark panel five status
      badges would be five bright holes. Mixing the ground-aware mark colour into
      whatever is behind is what the row tints already do and for the same reason:
      a percentage that reads on white disappears on slate, and a fixed tint that
      reads on white glares on slate. Where color-mix is unsupported the whole
      declaration is invalid, the fill computes to nothing, and the badge degrades
      to an outlined chip with its edge, its glyph and its words intact.

   2. THE LABEL IS INK, not the --text role. Only status-error-text moves on a dark
      ground; warning, success and info keep their light-ground values, so amber-700
      words would sit on a dark chip and vanish. Ink follows the ground everywhere
      and measures 15:1 on the tint at either end. Nothing is lost: the hue is
      carried by the edge and the glyph, and the meaning by the words.
   ============================================================================= */
.al-badge {
	/* -- the composition axis: three rungs of one idea, and an intent variant is a
	      two-line re-point that cannot miss a surface. ---------------------- */
	--al-badge-intent: var(--al-sys-color-text-muted);
	--al-badge-intent-tint: color-mix(in srgb, var(--al-sys-color-text-muted) 14%, transparent);
	--al-badge-intent-text: var(--al-sys-color-text-primary);

	--al-badge-fill: var(--al-badge-intent-tint);
	--al-badge-border-color: var(--al-badge-intent);
	--al-badge-label: var(--al-badge-intent-text);
	--al-badge-icon-color: var(--al-badge-intent);

	--al-badge-font-family: var(--al-sys-font-family);
	--al-badge-font-size: var(--al-sys-font-size-100);
	/* MEDIUM, not the control weight. 600 is reserved for things you can click or
	   type into, and a badge is neither. 500 is the rung for a label that has to
	   separate from the value beside it, which is exactly this job. */
	--al-badge-font-weight: var(--al-sys-font-weight-medium);
	--al-badge-line-height: var(--al-sys-line-height-control);
	--al-badge-padding-x: var(--al-sys-space-100);
	--al-badge-padding-y: var(--al-sys-space-25);
	--al-badge-gap: var(--al-sys-space-50);
	--al-badge-radius: var(--al-sys-radius-pill);
	--al-badge-border-width: var(--al-sys-control-border-width);
	--al-badge-icon-size: var(--al-sys-icon-size-sm);

	/* -- properties: only --al-badge-* below this line ---------------------- */
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	gap: var(--al-badge-gap);
	max-width: 100%;
	padding: var(--al-badge-padding-y) var(--al-badge-padding-x);
	border: var(--al-badge-border-width) solid var(--al-badge-border-color);
	border-radius: var(--al-badge-radius);
	background: var(--al-badge-fill);
	color: var(--al-badge-label);
	font-family: var(--al-badge-font-family);
	font-size: var(--al-badge-font-size);
	font-weight: var(--al-badge-font-weight);
	/* set, not inherited: a badge in a table cell and a badge in a paragraph are the
	   same height, and the host page's leading does not get a vote */
	line-height: var(--al-badge-line-height);
	white-space: nowrap;
	/* NO text-transform. Uppercase micro-labels read as a warning sticker; sentence
	   case reads as a fact, which is what a badge states. */
}

/* The label needs no class of its own, same bargain the button struck: any element
   inside truncates. Wrapping it in SOMETHING is still required, because
   text-overflow has no effect on a bare text node in a flex container. */
.al-badge > * {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The glyph keeps its size while the words give way, and it takes the intent
   rather than the ink so the hue reads at a glance from across a room. */
.al-badge-icon {
	/* the same reason the badge itself states it: box-sizing is not inherited, so on a
	   host page with no reset a glyph carrying any padding or border of its own renders
	   wider than the icon rung says it is, and the badge grows with it */
	box-sizing: border-box;
	flex: none;
	width: var(--al-badge-icon-size);
	height: var(--al-badge-icon-size);
	color: var(--al-badge-icon-color);
}

/* -- INTENT: what the thing IS. Two lines each, because the tint is mixed from the
      same role the mark uses and so cannot drift away from it. ------------- */
.al-badge-success {
	--al-badge-intent: var(--al-sys-color-status-success);
	--al-badge-intent-tint: color-mix(in srgb, var(--al-sys-color-status-success) 14%, transparent);
}
.al-badge-warning {
	--al-badge-intent: var(--al-sys-color-status-warning);
	--al-badge-intent-tint: color-mix(in srgb, var(--al-sys-color-status-warning) 14%, transparent);
}
.al-badge-error {
	--al-badge-intent: var(--al-sys-color-status-error);
	--al-badge-intent-tint: color-mix(in srgb, var(--al-sys-color-status-error) 14%, transparent);
}
.al-badge-info {
	--al-badge-intent: var(--al-sys-color-status-info);
	--al-badge-intent-tint: color-mix(in srgb, var(--al-sys-color-status-info) 14%, transparent);
}

/* -- SIZE: two rungs, and the second one earns its place at the top of a record.
      A 12px badge beside a 25px page heading reads as a footnote about the
      heading rather than a fact about the order. ------------------------- */
.al-badge-lg {
	--al-badge-font-size: var(--al-sys-font-size-200);
	--al-badge-padding-x: var(--al-sys-space-150);
	--al-badge-padding-y: var(--al-sys-space-50);
	--al-badge-icon-size: var(--al-sys-icon-size-md);
}

/* =============================================================================
   THE TAG

   A badge you can take off. That one difference makes it a control: it needs a
   focus ring, a hit target big enough to hit, and a disabled state for the filter
   you are not allowed to drop.

   ONE TAB STOP PER TAG, and it is the remove button, not the tag. The words are
   not an action — nothing happens when you press them — so giving the shell a
   tabindex would double the length of a filter bar in the tab order to reach
   nothing. The label is read out by the button's own accessible name.

   IT DEFAULTS TO THE SMALL RUNG. Tags arrive in tens: ten 36px pills is a wall,
   ten 28px pills is a filter bar. .al-tag-md exists for the single tag that has to
   line up with a field or a button beside it.

   NO INTENT VARIANTS, deliberately. A tag is a value someone chose, not a verdict
   on a record. Colouring it by strain or category would be data-driven colour, and
   the system has no opinion on data — the badge is where meaning goes.
   ============================================================================= */
/* =============================================================================
   LINK

   Somebody has to draw a link that is just a link, and until now nothing did. The
   system had three: a crumb, a menu item, a sidenav item -- each scoped to the
   container it lives in, each drawn for that container. A name in a table cell, a
   record referenced in a sentence, an id beside a value: none of them had an answer,
   so the POS wrote its own the first time it needed one. That is how 287 app .al-*
   classes came to exist that this file did not have, and the cheapest moment to stop
   it is the first time.

   INK AT REST, NOT BLUE. A queue of twenty rows where every name is a link is twenty
   coloured strings, and colour that common stops meaning anything. It inherits, so a
   link reads as the text it sits in and the row keeps one ink. The affordance arrives
   on hover and on focus, where it is asked for.

   THE UNDERLINE IS THE MARK, not the colour. Colour alone fails anyone who cannot see
   the difference, and it is also what makes a list look busy at rest.

   .al-link-external CARRIES THE GLYPH, and it is a separate class because the promise
   is different: it says a new tab is about to open. A link that makes that promise and
   navigates in place is worse than one that never made it.
   ============================================================================= */
.al-link {
	--al-link-color: inherit;
	--al-link-color-hover: var(--al-sys-color-accent-primary);
	--al-link-underline-offset: 2px;
	--al-link-glyph-color: var(--al-sys-color-text-muted);
	--al-link-glyph-size: var(--al-sys-icon-size-sm);
	--al-link-gap: var(--al-sys-space-100);

	color: var(--al-link-color);
	text-decoration: none;
	border-radius: var(--al-sys-radius-sm);
}
.al-link:hover,
.al-link:focus-visible {
	color: var(--al-link-color-hover);
	text-decoration: underline;
	text-underline-offset: var(--al-link-underline-offset);
}
.al-link:focus-visible {
	outline: var(--al-sys-focus-ring-width) solid var(--al-sys-color-focus-ring);
	outline-offset: 2px;
}
/* The glyph sits with the text rather than after it, so a wrapped link does not leave
   its mark stranded on a line of its own. */
.al-link-external {
	display: inline-flex;
	align-items: center;
	gap: var(--al-link-gap);
}
.al-link-external > .al-link-icon {
	flex: none;
	font-size: var(--al-link-glyph-size);
	line-height: 1;
	color: var(--al-link-glyph-color);
}
.al-link-external:hover > .al-link-icon,
.al-link-external:focus-visible > .al-link-icon {
	color: var(--al-link-color-hover);
}

/* =============================================================================
   AN ANCHOR IS NOT A LINK JUST BECAUSE IT IS AN ANCHOR

   bootstrap.min.css carries `a:hover { text-decoration: underline }`. That is (0,1,1),
   so it beats every control that states `text-decoration: none` at rest and stops
   there — those are (0,1,0), and the underline arrives the moment the pointer does.
   Measured on the running app: a.al-btn and a.al-back both computed `none` at rest and
   `underline` on hover, and the decoration propagated to every inline descendant, so
   the back control's arrow got one too.

   WHY IT IS WRONG AND NOT MERELY UNTIDY. An underline is a TEXT affordance: it says
   where a target starts and stops inside a line of prose. These four are regions. The
   whole box is the target, so underlining the words in it makes the feedback and the
   hit area disagree — the box says "all of me", the underline says "this word". A
   region answers with the region, which is what their hover states already do.

   .al-back shows why it is worth a block. Its own rules reset appearance, border,
   background, padding, font-family, tracking, casing and word-spacing so the control
   renders identically on an <a> and a <button>. Decoration is the ninth item on that
   list and the one that was missed, so the anchor form underlined and the button form
   could not. "A control that changes shape depending on the element it lands on is not
   a control" is this file's own sentence about exactly that.

   ONE BLOCK RATHER THAN A LINE IN EACH, because it is one decision and the four differ
   in nothing but their name. It had already been answered twice, and both answers were
   in the wrong file: the override layer carried a.al-list-row:hover, and _sidebar.css
   covers .al-sidenav-item by accident through `.app-sidebar a` — which means a sidenav
   item outside that shell underlines. Both are retired by this.

   NOT LISTED, DELIBERATELY. .al-link and .al-crumbs a ARE text, and the underline is
   their hover state rather than a leak. The tab and the menu item are <button> in this
   system and have nothing to decorate.

   :focus is carried alongside :hover so the retirement of the override-layer patch is
   like for like; that patch covered both, and dropping one quietly is how a fix
   becomes a regression.
   ============================================================================= */
.al-btn:hover,
.al-btn:focus,
.al-back:hover,
.al-back:focus,
.al-list-row:hover,
.al-list-row:focus,
.al-sidenav-item:hover,
.al-sidenav-item:focus {
	text-decoration: none;
}

.al-tag {
	--al-tag-height: var(--al-sys-control-height-sm);
	--al-tag-radius: var(--al-sys-radius-pill);
	--al-tag-fill: color-mix(in srgb, var(--al-sys-color-text-muted) 12%, transparent);
	--al-tag-label: var(--al-sys-color-text-primary);
	--al-tag-border-width: var(--al-sys-control-border-width);
	/* the CONTROL edge, not the hairline. A tag is something you operate, and 1.4.11
	   asks 3:1 for the boundary of a control — the hairline rung manages 1.29. */
	--al-tag-border-color: var(--al-sys-color-border-control);
	--al-tag-font-family: var(--al-sys-font-family);
	--al-tag-font-size: var(--al-sys-font-size-200);
	--al-tag-font-weight: var(--al-sys-font-weight-medium);
	--al-tag-line-height: var(--al-sys-line-height-control);
	/* asymmetric on purpose: the words need air on the left, the round remove button
	   brings its own on the right */
	--al-tag-padding-left: var(--al-sys-space-150);
	--al-tag-padding-right: var(--al-sys-space-50);
	--al-tag-gap: var(--al-sys-space-50);

	--al-tag-remove-size: 18px;
	/* THE SAME 24px THE CHECKBOX USES, and for the same reason: 18px is the right
	   size for the mark and the wrong size for the target, so the target grows
	   invisibly around it instead and nothing on the page moves. */
	--al-tag-remove-hit: 24px;
	--al-tag-remove-radius: var(--al-sys-radius-pill);
	--al-tag-remove-glyph: var(--al-sys-color-text-muted);
	--al-tag-remove-fill: transparent;
	/* the cross is a MASK, so its colour comes from a token and a theme can move it.
	   The checkbox settled this: a baked-in image colour is a colour no theme can
	   reach. */
	--al-tag-remove-glyph-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5 5 11 11M11 5 5 11' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");

	--al-tag-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-tag-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-tag-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-tag-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-tag-focus-ring-width: var(--al-sys-focus-ring-width);
	/* no intent to ring in, so it takes the shared focus role — the input and the
	   affix made the same call */
	--al-tag-focus-ring-color: var(--al-sys-color-focus-ring);

	/* -- properties: only --al-tag-* below this line ------------------------ */
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	gap: var(--al-tag-gap);
	flex: none; /* a filter bar may wrap; it may not squash a tag */
	max-width: 100%;
	/* a floor, not a fixed height: at 200% text the pill grows instead of clipping */
	min-height: var(--al-tag-height);
	padding: 0.2em var(--al-tag-padding-right) 0.2em var(--al-tag-padding-left);
	border: var(--al-tag-border-width) solid var(--al-tag-border-color);
	border-radius: var(--al-tag-radius);
	background: var(--al-tag-fill);
	color: var(--al-tag-label);
	font-family: var(--al-tag-font-family);
	font-size: var(--al-tag-font-size);
	font-weight: var(--al-tag-font-weight);
	line-height: var(--al-tag-line-height);
	white-space: nowrap;
}

/* THE LABEL ONLY, and the selector says so for a reason. `.al-tag > *` would put
   overflow:hidden on the remove button as well, which clips the invisible box that
   IS its hit target — the target would silently shrink back to 18px and nothing
   would look wrong. */
.al-tag > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.al-tag-remove {
	box-sizing: border-box;
	position: relative;
	flex: none;
	width: var(--al-tag-remove-size);
	height: var(--al-tag-remove-size);
	padding: 0;
	border: 0;
	border-radius: var(--al-tag-remove-radius);
	background: var(--al-tag-remove-fill);
	cursor: pointer;
}

/* the hit target: invisible, centred, and larger than the mark it surrounds */
.al-tag-remove::before {
	content: '';
	position: absolute;
	inset: calc((var(--al-tag-remove-size) - var(--al-tag-remove-hit)) / 2);
}

.al-tag-remove::after {
	content: '';
	position: absolute;
	inset: 0;
	background: var(--al-tag-remove-glyph);
	-webkit-mask: var(--al-tag-remove-glyph-image) center / contain no-repeat;
	mask: var(--al-tag-remove-glyph-image) center / contain no-repeat;
}

/* States re-point tokens; they do not set properties. The veil is a mix of the ink
   rather than a veil element, so there is no third pseudo-element to keep in step —
   and where color-mix is unsupported the glyph still darkens, which is the half of
   the signal that matters. */
.al-tag-remove:hover {
	--al-tag-remove-glyph: var(--al-sys-color-text-primary);
	--al-tag-remove-fill: color-mix(in srgb, var(--al-sys-color-text-primary) 12%, transparent);
}
/* PRESS RESTATES THE GLYPH, and that is not redundancy. A mouse press always arrives
   with a hover under it, so leaving the glyph to the rule above looked correct — but
   Enter and Space on a focused button set :active with no pointer anywhere near it,
   and the veil deepened while the cross stayed muted. A state may not depend on
   another state having fired first. */
.al-tag-remove:active {
	--al-tag-remove-glyph: var(--al-sys-color-text-primary);
	--al-tag-remove-fill: color-mix(in srgb, var(--al-sys-color-text-primary) 20%, transparent);
}

.al-tag-remove:focus {
	outline: none;
}
/* the three-layer ring, copied from the button verbatim with the tag's own tokens.
   The transparent outline is invisible here and is repainted by forced-colors,
   where box-shadow is thrown away and a keyboard user would otherwise be left with
   no indicator at all. */
.al-tag-remove:focus-visible {
	outline: var(--al-tag-focus-ring-width) solid transparent;
	outline-offset: var(--al-tag-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-tag-focus-hairline-width) var(--al-tag-focus-hairline-light),
		0 0 0 calc(var(--al-tag-focus-hairline-width) + var(--al-tag-focus-ring-width)) var(--al-tag-focus-ring-color),
		0 0 0 calc(var(--al-tag-focus-hairline-width) * 2 + var(--al-tag-focus-ring-width)) var(--al-tag-focus-hairline-dark);
}

/* A TAG NOBODY CAN REMOVE IS A LABEL, and it should not keep the space for a button
   that is not there. The padding above is asymmetric on purpose -- the words take the
   air on the left and the round remove button brings its own on the right -- so a tag
   rendered WITHOUT that button wears 12px on one side and 4px on the other, and the
   label reads as shoved off-centre in its own pill. Six of the fourteen tags in the app
   are this shape, across admin config, the documentation topic page, the lab and the
   POS (ALLM-7183). Matching the right padding to the left is the whole fix; the button
   variant is untouched, because :has() only claims the tags that lack one. */
.al-tag:not(:has(> .al-tag-remove)) {
	--al-tag-padding-right: var(--al-tag-padding-left);
}

/* A tag whose value is locked: the filter your role pins on every query. The whole
   pill dims, not just the button, because a full-strength tag with a dead button
   looks broken rather than fixed. */
.al-tag:has(> .al-tag-remove:disabled) {
	opacity: var(--al-tag-disabled-opacity);
}
.al-tag-remove:disabled {
	cursor: not-allowed;
	pointer-events: none;
}

/* SIZE. Three re-points, and the remove glyph is deliberately not one of them: a
   target that is already comfortable does not get better by getting bigger, and a
   larger cross would start to compete with the word beside it. */
.al-tag-md {
	--al-tag-height: var(--al-sys-control-height-md);
	--al-tag-font-size: var(--al-sys-font-size-300);
	--al-tag-padding-left: var(--al-sys-space-200);
}

/* =============================================================================
   PROGRESS

   Three shapes of one idea: a bar with a value, a bar without one, and a small
   circle for the places a bar will not fit.

   THE VALUE LIVES IN A TOKEN. The consumer writes style="--al-progress-value:62%"
   and aria-valuenow="62" — the token draws it, the attribute announces it, and the
   contract is that they say the same number. CSS cannot read an attribute's value,
   so nothing can enforce that; what CAN be enforced is the other half, and it is:

   INDETERMINATE IS THE ABSENCE OF aria-valuenow. Not a class, not a data attribute.
   You cannot make it look like it is thinking without also telling a screen reader
   it is thinking. Same bargain the button's aria-busy strikes.

   THE FILL READS THE ACCENT AS A LINE, NOT AS A FILL, and that is measured. Nothing
   sits on top of a progress bar, so it is ink laid on the ground rather than a
   ground of its own — and on a dark panel the fill rung against the track manages
   2.0:1 where the line rung manages 5.45:1.

   THE TRACK IS THE BORDER ROLE. The unfilled part of the bar is a line, not a
   surface. The sunken surface would have been the tidy-looking answer and it
   measures 1.06:1 against a white card: a bar with no visible total.
   ============================================================================= */
.al-progress {
	--al-progress-value: 0%;
	--al-progress-height: var(--al-sys-space-100);
	--al-progress-radius: var(--al-sys-radius-pill);
	--al-progress-track: var(--al-sys-color-border);
	--al-progress-fill: var(--al-sys-color-accent-primary-line);
	--al-progress-motion-duration: var(--al-sys-motion-duration-state);
	--al-progress-motion-easing: var(--al-sys-motion-easing);
	/* A LOOP OF ITS OWN LENGTH, deliberately opting out of the sys rung. 0.6s is one
	   revolution of a 16px ring, where it reads as working; the same 0.6s dragging a
	   band across 400px of bar reads as a strobe. */
	--al-progress-loop-duration: 1.2s;
	--al-progress-loop-easing: ease-in-out;
	/* the indeterminate state re-points this from `none` — so the state is two token
	   re-points and not one property override */
	--al-progress-animation-name: none;

	/* -- properties: only --al-progress-* below this line ------------------- */
	box-sizing: border-box;
	display: block;
	width: 100%;
	height: var(--al-progress-height);
	border-radius: var(--al-progress-radius);
	background: var(--al-progress-track);
	overflow: hidden;
}

.al-progress-bar {
	box-sizing: border-box;
	display: block;
	width: var(--al-progress-value);
	height: 100%;
	border-radius: inherit;
	background: var(--al-progress-fill);
	transition: width var(--al-progress-motion-duration) var(--al-progress-motion-easing);
	animation: var(--al-progress-animation-name) var(--al-progress-loop-duration) var(--al-progress-loop-easing) infinite;
}

/* NO VALUE, NO NUMBER. The bar becomes a fixed-width shuttle and the width token
   carries how long it is, so the property above never changes. */
.al-progress:not([aria-valuenow]) {
	--al-progress-value: 40%;
	--al-progress-animation-name: al-progress-slide;
}
/* -100% is the bar's own width, so it starts fully off the left; 250% is 100% of a
   track divided by the bar's 40%, so it ends fully off the right. */
@keyframes al-progress-slide {
	from {
		transform: translateX(-100%);
	}
	to {
		transform: translateX(250%);
	}
}

/* -----------------------------------------------------------------------------
   THE CIRCLE — for a table cell, a toolbar, the inside of a row: anywhere a bar
   has no width to be a bar in.

   ONE SIZE. It takes the large icon rung because it stands where an icon stands.
   There is no small or large variant: a circle is what you use when there is no
   room, and if there is room for a big one there is room for a bar, which is
   easier to read at every value.

   It declares its own tokens rather than sharing the bar's, because it is a
   different shape of the same idea and not a variant of the bar — the two are
   never worn together.
   ----------------------------------------------------------------------------- */
.al-progress-circle {
	--al-progress-value: 0%;
	--al-progress-circle-size: var(--al-sys-icon-size-lg);
	--al-progress-circle-radius: var(--al-sys-radius-pill);
	/* the same 2px the button's spinner uses: 1px reads as an artefact once it moves */
	--al-progress-circle-ring-width: var(--al-sys-spinner-ring-width);
	--al-progress-track: var(--al-sys-color-border);
	--al-progress-fill: var(--al-sys-color-accent-primary-line);
	--al-progress-loop-duration: var(--al-sys-motion-duration-loop);
	--al-progress-loop-easing: linear;
	--al-progress-animation-name: none;

	/* -- properties: only --al-progress-* below this line ------------------- */
	box-sizing: border-box;
	display: inline-block;
	flex: none;
	vertical-align: middle;
	width: var(--al-progress-circle-size);
	height: var(--al-progress-circle-size);
	border-radius: var(--al-progress-circle-radius);
	/* the ring is stated, not inherited, and it is set for one reason: the mask below
	   uses currentColor so that no literal enters this file, and a colour that is
	   never transparent is what stops the mask erasing the whole control. Only the
	   alpha of a mask image is read, so the hue is irrelevant. */
	color: var(--al-progress-fill);
	background: conic-gradient(var(--al-progress-fill) var(--al-progress-value), var(--al-progress-track) 0);
	/* punch the middle out so the disc becomes a ring. closest-side is the radius, so
	   the hole ends exactly one ring-width in, and the 0 stop clamps to the one
	   before it, which is what makes the edge hard rather than a gradient. */
	-webkit-mask: radial-gradient(closest-side, transparent calc(100% - var(--al-progress-circle-ring-width)), currentColor 0);
	mask: radial-gradient(closest-side, transparent calc(100% - var(--al-progress-circle-ring-width)), currentColor 0);
	animation: var(--al-progress-animation-name) var(--al-progress-loop-duration) var(--al-progress-loop-easing) infinite;
}
/* Same hook as the bar. The arc is a quarter turn — enough to read as a shape going
   round, small enough that nobody mistakes it for a value. al-spin is the button's
   own keyframe, reused rather than re-declared: two identical @keyframes blocks is
   how a spinner and a progress ring end up spinning at different speeds. */
.al-progress-circle:not([aria-valuenow]) {
	--al-progress-value: 25%;
	--al-progress-animation-name: al-spin;
}

/* =============================================================================
   THE SKELETON

   The shape of a record that has not arrived. It is not a spinner: a spinner says
   "something is happening", a skeleton says "a customer row is coming and it will
   be this tall", so the page does not jump when it lands. That only holds if the
   skeleton is the size of the real thing, which is why every variant here is
   pinned to the rung the real control uses rather than to a shape that looks
   about right.

   IT IS INVISIBLE TO A SCREEN READER. aria-hidden on every block, aria-busy on the
   region that holds them — otherwise a page mid-load reads out as a dozen blank
   groups.

   THE GREY IS THE BORDER ROLE, the same choice the progress track makes and for the
   same measurement: the sunken surface is 1.06:1 on a white card, so a skeleton
   built from it is an invisible placeholder, which is no placeholder at all.
   ============================================================================= */
.al-skeleton {
	--al-skeleton-fill: var(--al-sys-color-border);
	/* The sweep is the GROUND at 60%, so it is a pale band on a light card and a dark
	   band on a dark panel. Either way it is a band travelling across the block,
	   which is the whole of what the motion has to say — it does not have to be a
	   highlight, it has to be a difference. */
	--al-skeleton-sheen: color-mix(in srgb, var(--al-sys-color-surface) 60%, transparent);
	--al-skeleton-radius: var(--al-sys-radius-sm);
	--al-skeleton-width: 100%;
	--al-skeleton-height: var(--al-sys-space-200);
	--al-skeleton-sweep-width: 60%;
	/* its own duration, for the reason the indeterminate bar gives: a band crossing a
	   wide block at the sys loop rung reads as a strobe */
	--al-skeleton-sweep-duration: 1.5s;
	--al-skeleton-sweep-easing: ease-in-out;
	--al-skeleton-animation-name: al-skeleton-sweep;

	/* -- properties: only --al-skeleton-* below this line ------------------- */
	box-sizing: border-box;
	display: block;
	width: var(--al-skeleton-width);
	height: var(--al-skeleton-height);
	border-radius: var(--al-skeleton-radius);
	background-color: var(--al-skeleton-fill);
	background-image: linear-gradient(90deg, transparent, var(--al-skeleton-sheen), transparent);
	background-size: var(--al-skeleton-sweep-width) 100%;
	background-repeat: no-repeat;
	animation: var(--al-skeleton-animation-name) var(--al-skeleton-sweep-duration) var(--al-skeleton-sweep-easing) infinite;
}
/* the overshoot at each end is what gives the sweep a pause between passes rather
   than an unbroken conveyor */
@keyframes al-skeleton-sweep {
	from {
		background-position: -40% 0;
	}
	to {
		background-position: 140% 0;
	}
}

/* A LINE OF COPY. Its height is the body type rung, so it is the height of the text
   it is standing in for and not a bar that happens to look thin. */
.al-skeleton-text {
	--al-skeleton-height: var(--al-sys-font-size-300);
}

/* A FACE. The same rung the medium avatar takes, because that is what is coming. */
.al-skeleton-circle {
	--al-skeleton-radius: var(--al-sys-radius-pill);
	--al-skeleton-width: var(--al-sys-control-height-md);
	--al-skeleton-height: var(--al-sys-control-height-md);
}

/* A PARAGRAPH. 14px of line plus 8px of gap is 22px, which is 14px of body copy at
   the 1.55 body leading — so a block of skeleton lines occupies the same space the
   sentences will, to the pixel. */
.al-skeleton-lines {
	/* declared HERE and not on .al-skeleton, which was the first version and rendered
	   four lines as one slab: the stack is the parent, it does not carry the block's
	   class, so the token it was reading resolved to nothing and `gap` fell back to
	   zero. An undefined custom property fails silently — this is the shape of bug it
	   makes. */
	--al-skeleton-line-gap: var(--al-sys-space-100);

	display: flex;
	flex-direction: column;
	gap: var(--al-skeleton-line-gap);
}
/* The last line stops short, the way a real last line does. It re-points the width
   token rather than setting the property, so a consumer can still override it. */
.al-skeleton-lines > .al-skeleton:last-child {
	--al-skeleton-width: 62%;
}

/* =============================================================================
   REDUCED MOTION — keep only what carries meaning.

   THE PROGRESS INDICATORS KEEP THEIR MOTION, stepped rather than swept. They are
   the only thing on screen saying an upload is still running; take that away and a
   slow manifest looks like a dead page. This is the same call the button's spinner
   made, and the same steps() treatment.

   THE SKELETON LOSES ITS SWEEP ENTIRELY, and that is the difference between the
   two. A skeleton's meaning is its SHAPE — content is coming and this is how much
   of it there is — and the shimmer adds nothing to that. So the sheen goes to
   transparent as well as the animation stopping, otherwise the band would freeze
   mid-block and read as a stripe in the design.

   The value transition goes to zero rather than being switched off as a property,
   so the rule stays a token re-point like every other state in this file.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-progress {
		--al-progress-motion-duration: 0s;
		--al-progress-loop-easing: steps(6, end);
	}
	.al-progress-circle {
		--al-progress-loop-easing: steps(8, end);
	}
	.al-skeleton {
		--al-skeleton-animation-name: none;
		--al-skeleton-sheen: transparent;
	}
}

/* =============================================================================
   FORCED COLOURS — Windows high contrast repaints every background to one colour
   and throws shadows away.

   Borders survive and are repainted for us, so the badge and the tag keep their
   shape with nothing written here. The bar and the skeleton do not: both are
   nothing BUT a background, so a progress bar would lose the difference between
   filled and unfilled and a skeleton would vanish. Both are fixed by re-pointing
   their own colour tokens at system colours — a re-point, not a property override,
   so this block cannot forget a state any more than a variant can.

   The avatar degrades to bare initials here: its disc is a background and its
   hairline is a shadow, and neither survives. That is a legible outcome — the
   letters still say who it is — so nothing is spent chasing it.
   ============================================================================= */
@media (forced-colors: active) {
	.al-progress {
		--al-progress-track: GrayText;
		--al-progress-fill: Highlight;
	}
	.al-progress-circle {
		--al-progress-track: GrayText;
		--al-progress-fill: Highlight;
	}
	.al-skeleton {
		--al-skeleton-fill: GrayText;
		--al-skeleton-sheen: transparent;
	}
}

/* ---- messages ---------------------------------------------------- */
/* =============================================================================
   TELLING SOMEONE SOMETHING — the inline message, the toast, the banner.

   Three controls. What separates them is not how loud they are, it is how long
   the thing lasts and whether it needs an answer.

     .al-msg      sits in the page or the form, beside what it is about, and
                  stays as long as the condition does.
     .al-toast    floats over the page, says one sentence about something that
                  has just happened, and goes away. It blocks nothing and it
                  never holds the only copy of a fact someone needs.
     .al-banner   spans the page and stays. Something is ongoing, and there is
                  normally something to do about it.

   COLOUR IS NEVER THE ONLY SIGNAL. This is the component where that stops being
   a line in the token file and becomes something you can see. Each of the three
   carries the status three separate ways at once:

     the SHAPE of the glyph — only the warning is a triangle, and the four marks
       differ from one another before any colour is applied,
     the COLOUR of that glyph and, on the toast and the banner, of the edge,
     the WORDS, which name the condition in the first few of them. "Manifest
       00418-2 was rejected", not "Something went wrong".

   Any one of the three, read alone, still says which status it is. That is what
   makes these survive colour blindness, a bad monitor, a printout and a screen
   reader. It is also why the glyph is part of the control and not something a
   consumer chooses: pick the class, get the right mark.

   WHAT THEY READ FROM THE STATUS FAMILY, AND WHAT THEY DELIBERATELY DO NOT.
   Each status has three rungs: the mark, the tint it sits on, and the text on
   that tint. Only the MARK is re-pointed by a ground, so only the mark is safe
   for a control that has to be right on both. The tint rungs are light-ground
   values — red-50 on a dark panel is a near-white box — and three of the four
   text rungs are the same. A control reading those is a light-only control, and
   the grounds rule says a control that reads the system's roles is correct on
   both the day it is written. So these three take the mark and derive the rest:

     the GROUND is the mark at 14% over whatever is behind. A translucent wash is
       correct over every ground for exactly the reason --al-ref-white-translucent
       gives for being a lift rather than a rung: a fixed value would be right on
       one canvas and wrong on the next.
     the WORDS read the ground's own ink roles, not the status text rung. Words
       have to be legible before they are coloured, and this way an error and an
       info message read at identical contrast.
     the GLYPH is the status colour leaned 30% toward the ground's primary ink.

   THE 30% LEAN is worth explaining, because it looks like cleverness and is not.
   On a LIGHT ground the ink is slate-900, so the mix deepens the hue — and it
   lands within a couple of points of the rungs this system already decided:
   red-700 is #8a322d and the mix gives #8d3937; blue-700 is #294c7a and the mix
   gives #295185. It is not a new language, it reproduces the decided one. On a
   DARK ground the same expression leans toward white instead, and supplies the
   values the dark block never had a rung for. And it is what carries the two
   marks that fail at full strength: an amber mark on its own light tint measures
   2.83 against the 3:1 bar for a meaningful graphic, and 4.57 after; a blue mark
   on a dark toast measures 1.91, and 4.15 after.

   COLOR-MIX IS GUARDED. Both derivations sit in an @supports block at the foot of
   this file. Without the guard, an engine that cannot parse color-mix computes
   the custom property to the guaranteed-invalid value, `background` falls to
   unset, and the glyph disappears — the mark going silent is the one failure
   this component cannot have. Guarded, the fallback is a plain surface with a
   full-strength mark, which is a coherent design rather than a broken one.
   ============================================================================= */

/* The icon ref shapes and the sys status-icon roles this family minted now live
   in recipe-tokens.css, at the tiers they belong to. */

/* =============================================================================
   THE INLINE MESSAGE

   It sits in the flow. A form's submit failed, a page-level condition applies to
   what you are looking at, a step succeeded and you want to say so where the
   person's eyes already are.

   NO DISMISS, deliberately. A message describes a condition; when the condition
   goes, the code that knows it has gone removes the message. A close button on an
   inline message means someone can hide the reason their save failed and then
   wonder why it keeps failing.

   ONE SIZE. A message is words, and words do not have a control tier. The size
   variants elsewhere exist because a box has to line up with the box beside it;
   nothing lines up with a paragraph.
   ============================================================================= */

.al-msg {
	/* -- the composition ladder: one rung, and it is the status. Everything the four
	      statuses change hangs off it, so a status variant is two lines — the colour
	      and the shape — and cannot miss a surface. The shape is separate on purpose:
	      it is a second, independent signal, not a decoration on the first. */
	--al-message-status: var(--al-sys-color-status-info);
	--al-message-icon-image: var(--al-sys-icon-status-info);

	--al-message-mark: var(--al-message-status); /* upgraded in the @supports block */
	--al-message-fill: var(--al-sys-color-surface); /* likewise */
	/* a NEUTRAL hairline, not a coloured one. Errors arrive in groups — the same
	   argument the input's error state makes — and six saturated outlines on one form
	   is a page nobody can read. The tint and the mark carry the status; the edge
	   only has to say where the box ends. */
	--al-message-border-color: var(--al-sys-color-border);
	--al-message-border-width: var(--al-sys-control-border-width);
	--al-message-radius: var(--al-sys-radius-container);

	--al-message-padding-y: var(--al-sys-space-150);
	--al-message-padding-x: var(--al-sys-space-200);
	--al-message-gap: var(--al-sys-space-150); /* mark to words */
	--al-message-title-gap: var(--al-sys-space-50); /* headline to copy */
	--al-message-actions-space: var(--al-sys-space-150);
	--al-message-actions-gap: var(--al-sys-space-100);

	--al-message-icon-size: var(--al-sys-icon-size-lg);
	--al-message-font-size: var(--al-sys-font-size-300);
	--al-message-line-height: var(--al-sys-line-height-body);
	--al-message-title-color: var(--al-sys-color-text-primary);
	--al-message-title-weight: var(--al-sys-font-weight-control);
	--al-message-text-color: var(--al-sys-color-text-secondary);

	/* -- properties: only --al-message-* below this line -------------------- */
	/* the system owns its box model. box-sizing does not inherit, and a host page
	   without a reset would otherwise put the padding outside the width. */
	box-sizing: border-box;
	display: flex;
	align-items: flex-start; /* the mark sits beside the FIRST line, not in the
	     middle of a three-line paragraph */
	gap: var(--al-message-gap);
	padding: var(--al-message-padding-y) var(--al-message-padding-x);
	border: var(--al-message-border-width) solid var(--al-message-border-color);
	border-radius: var(--al-message-radius);
	background: var(--al-message-fill);
	color: var(--al-message-text-color);
	font-family: inherit;
	font-size: var(--al-message-font-size);
	/* set, not inherited: the icon is centred on the first line with a calc that
	   reads this value, so a host page's 1.65 would silently move the mark. */
	line-height: var(--al-message-line-height);
	/* the tint and the glyph are backgrounds, and a browser strips backgrounds when
	   it prints. Without this a printed error message is words in a grey box and the
	   status is gone — which is the one thing the icon exists to prevent. */
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}

.al-msg-icon {
	box-sizing: border-box;
	display: block; /* a span is inline, and width and height do nothing to it */
	flex: none; /* the words give way, never the mark */
	width: var(--al-message-icon-size);
	height: var(--al-message-icon-size);
	/* centre the mark on the first line of the copy, whatever the copy does after.
	   Same move the checkbox makes to sit beside a two-line label. */
	margin-top: calc((var(--al-message-line-height) * 1em - var(--al-message-icon-size)) / 2);
	background: var(--al-message-mark);
	/* THE MARK IS A MASK. The shape comes from the image and the colour from a
	   token, so the ground can move it — the pattern the checkbox settled on, and
	   the reason the select's baked-in caret is the worse of the two. */
	-webkit-mask: var(--al-message-icon-image) center / contain no-repeat;
	mask: var(--al-message-icon-image) center / contain no-repeat;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}

/* min-width:0 is what lets a flex item shrink below its content width — without it
   a long unbroken tag like PKG-0000-4471-9920-3311 pushes the message wider than
   its container instead of wrapping. */
.al-msg-body {
	flex: 1 1 auto;
	min-width: 0;
}
/* THE HEADLINE NEEDS NO CLASS. Every message has copy and about half have a
   headline, so the burden goes on the rarer case: <strong> is the headline. Same
   instinct as the button, where the label is any element and only the icon is
   marked. */
.al-msg-body > strong {
	display: block;
	color: var(--al-message-title-color);
	font-weight: var(--al-message-title-weight);
}
.al-msg-body > p {
	margin: 0;
}
.al-msg-body > strong + p {
	margin-top: var(--al-message-title-gap);
}

/* The actions row hosts ORDINARY BUTTONS, and that is a decision rather than a
   shortcut. The status colour describes the CONDITION; the accent describes the
   ACTION. They are separate axes in this system already — status-error and
   accent-danger resolve to the same red and mean different things — so a green
   Retry inside a red message is correct, not a mismatch. */
.al-msg-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--al-message-actions-gap);
	margin-top: var(--al-message-actions-space);
}

/* -- STATUS: two lines each, the colour and the shape. Info is the default,
      because a message with nothing else said about it is information. --------- */
.al-msg-error {
	--al-message-status: var(--al-sys-color-status-error);
	--al-message-icon-image: var(--al-sys-icon-status-error);
}
.al-msg-warning {
	--al-message-status: var(--al-sys-color-status-warning);
	--al-message-icon-image: var(--al-sys-icon-status-warning);
}
.al-msg-success {
	--al-message-status: var(--al-sys-color-status-success);
	--al-message-icon-image: var(--al-sys-icon-status-success);
}
/* stated for symmetry and for the reader — it re-points to what the base already
   holds, so writing it changes nothing and omitting it changes nothing. Having it
   means the four statuses are one greppable set. */
.al-msg-info {
	--al-message-status: var(--al-sys-color-status-info);
	--al-message-icon-image: var(--al-sys-icon-status-info);
}

/* =============================================================================
   THE TOAST, AND THE STACK IT LIVES IN

   Brief, floating, self-removing. It reports something that has already happened
   and it never blocks anything.

   THE GROUND IS OPAQUE, not tinted. A toast sits ON the page rather than in it,
   over content nobody chose — a translucent wash would let a table's rules and a
   photo of a product show through the words. So it takes the raised surface, and
   the status shows as the mark and a rule down the leading edge. It also happens
   to be the more expensive-looking of the two: a clean card with a coloured edge
   reads as considered where a colour-washed box reads as an alert.

   NO ACTION BUTTON. A control that removes itself in four seconds is not a place
   to put something someone has to click — for anyone reading slowly it is a
   target that moves away. A notification that needs a decision is a message or a
   banner. The dismiss is not an exception to that: it removes the toast, it does
   not act on anything.

   CSS DOES NOT DISMISS IT. The code that showed the toast removes it, because the
   node has to leave the accessibility tree as well as the screen. What the system
   gives you is the two halves of that: it arrives with the enter animation, and
   adding .al-toast-leaving plays the exit, after which you remove the node on
   animationend. An auto-dismiss written as a CSS animation would leave a toast
   that is invisible, still focusable, and still announced.
   ============================================================================= */

.al-toast-stack {
	--al-toast-stack-width: 380px;
	--al-toast-stack-gap: var(--al-sys-space-150);
	--al-toast-stack-gutter: var(--al-sys-space-300);
	--al-toast-stack-layer: var(--al-sys-layer-toast);
	/* the four insets are tokens rather than a fixed corner so an anchor variant can
	   re-point them instead of overriding a property. Bottom-right by default: the
	   Alleaves header carries the location switcher and the search, and a toast in
	   the top-right corner covers the thing someone is on their way to click. */
	--al-toast-stack-inset-top: auto;
	--al-toast-stack-inset-right: var(--al-toast-stack-gutter);
	--al-toast-stack-inset-bottom: var(--al-toast-stack-gutter);
	--al-toast-stack-inset-left: auto;
	/* column-reverse, and it is not a detail. The newest toast belongs nearest the
	   corner; with plain `column` and a bottom anchor, appending one shoves every
	   older toast upward and whatever someone was half way through reading moves. */
	--al-toast-stack-direction: column-reverse;

	box-sizing: border-box;
	position: fixed;
	top: var(--al-toast-stack-inset-top);
	right: var(--al-toast-stack-inset-right);
	bottom: var(--al-toast-stack-inset-bottom);
	left: var(--al-toast-stack-inset-left);
	z-index: var(--al-toast-stack-layer);
	display: flex;
	flex-direction: var(--al-toast-stack-direction);
	gap: var(--al-toast-stack-gap);
	width: var(--al-toast-stack-width);
	max-width: calc(100% - var(--al-toast-stack-gutter) * 2);
	/* the stack covers a corner of the page whether or not it holds anything. Without
	   this it is an invisible 380px column swallowing clicks on whatever is under it
	   — and it is fixed, so it does that on every screen at once. The toasts
	   themselves take pointer events back. */
	pointer-events: none;
}
.al-toast-stack-top {
	--al-toast-stack-inset-top: var(--al-toast-stack-gutter);
	--al-toast-stack-inset-bottom: auto;
	/* anchored at the top, plain column is the one that does not move older toasts:
	   the first one stays against the top edge and new ones arrive below it. */
	--al-toast-stack-direction: column;
}

.al-toast {
	--al-toast-status: var(--al-sys-color-status-info);
	--al-toast-icon-image: var(--al-sys-icon-status-info);

	--al-toast-mark: var(--al-toast-status); /* upgraded in the @supports block */
	--al-toast-fill: var(--al-sys-color-surface-raised);
	--al-toast-border-color: var(--al-sys-color-border);
	--al-toast-border-width: var(--al-sys-control-border-width);
	/* the leading rule, and it reads the same mark the glyph does. Two reds in one
	   control reads as a mistake — the token file says so about status and accent,
	   and it is just as true inside a single box. */
	--al-toast-edge-width: 3px;
	--al-toast-radius: var(--al-sys-radius-container);
	--al-toast-elevation: var(--al-sys-elevation-overlay);

	--al-toast-padding-y: var(--al-sys-space-150);
	--al-toast-padding-x: var(--al-sys-space-200);
	--al-toast-gap: var(--al-sys-space-150);
	--al-toast-title-gap: var(--al-sys-space-50);

	--al-toast-icon-size: var(--al-sys-icon-size-lg);
	--al-toast-font-size: var(--al-sys-font-size-300);
	--al-toast-line-height: var(--al-sys-line-height-body);
	--al-toast-title-color: var(--al-sys-color-text-primary);
	--al-toast-title-weight: var(--al-sys-font-weight-control);
	--al-toast-text-color: var(--al-sys-color-text-secondary);

	/* a live toast takes pointer events back from the stack, which gave them up so it
	   would stop swallowing clicks on the page under it. It is a TOKEN rather than a
	   plain property for the same reason the animation below is: .al-toast-leaving has
	   to be able to turn it off, and a variant that owns a property is how a state gets
	   forgotten. */
	--al-toast-pointer-events: auto;

	/* which animation runs is a TOKEN, so .al-toast-leaving re-points three values
	   rather than overriding the animation property — a variant that owns properties
	   is how a state gets forgotten. */
	--al-toast-animation-name: al-toast-in;
	--al-toast-animation-duration: var(--al-sys-motion-duration-enter);
	--al-toast-animation-easing: var(--al-sys-motion-easing-enter);
	--al-toast-motion-scale: 0.96; /* it grows into place and shrinks away. No
	     translate, so the same animation is right in any corner. */

	--al-toast-dismiss-size: var(--al-sys-control-height-sm);
	--al-toast-dismiss-icon-size: var(--al-sys-icon-size-md);
	--al-toast-dismiss-icon-image: var(--al-sys-icon-dismiss);
	--al-toast-dismiss-color: var(--al-sys-color-text-muted);
	--al-toast-dismiss-hover-color: var(--al-sys-color-text-primary);
	--al-toast-dismiss-fill: transparent;
	--al-toast-dismiss-radius: var(--al-sys-radius-sm);
	--al-toast-dismiss-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-toast-dismiss-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-toast-dismiss-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-toast-dismiss-focus-ring-width: var(--al-sys-focus-ring-width);
	/* the dismiss carries no intent, so it rings in the shared focus colour — the
	   rule the input and the affix follow, not the button's. */
	--al-toast-dismiss-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: flex-start;
	gap: var(--al-toast-gap);
	padding: var(--al-toast-padding-y) var(--al-toast-padding-x);
	border: var(--al-toast-border-width) solid var(--al-toast-border-color);
	/* logical, not border-left: the rule belongs at the start of the line, which is
	   the right-hand side in an RTL locale. Declared after the shorthand so it wins. */
	border-inline-start: var(--al-toast-edge-width) solid var(--al-toast-mark);
	border-radius: var(--al-toast-radius);
	background: var(--al-toast-fill);
	box-shadow: var(--al-toast-elevation);
	color: var(--al-toast-text-color);
	font-family: inherit;
	font-size: var(--al-toast-font-size);
	line-height: var(--al-toast-line-height);
	pointer-events: var(--al-toast-pointer-events);
	animation-name: var(--al-toast-animation-name);
	animation-duration: var(--al-toast-animation-duration);
	animation-timing-function: var(--al-toast-animation-easing);
	animation-fill-mode: both;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}

.al-toast-icon {
	box-sizing: border-box;
	display: block;
	flex: none;
	width: var(--al-toast-icon-size);
	height: var(--al-toast-icon-size);
	margin-top: calc((var(--al-toast-line-height) * 1em - var(--al-toast-icon-size)) / 2);
	background: var(--al-toast-mark);
	-webkit-mask: var(--al-toast-icon-image) center / contain no-repeat;
	mask: var(--al-toast-icon-image) center / contain no-repeat;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}
.al-toast-body {
	flex: 1 1 auto;
	min-width: 0;
}
.al-toast-body > strong {
	display: block;
	color: var(--al-toast-title-color);
	font-weight: var(--al-toast-title-weight);
}
.al-toast-body > p {
	margin: 0;
}
.al-toast-body > strong + p {
	margin-top: var(--al-toast-title-gap);
}

/* THE DISMISS IS ITS OWN CONTROL, not a .al-btn wearing a cross. A quiet button
   takes its label colour from the intent ladder, which would put a green cross on
   a red toast, and the only way out would be re-pointing another component's
   tokens from this file. It needs a target, a mark, a hover and a ring — six
   tokens — and the focus ring is duplicated here exactly as it is duplicated on
   the input, the affix, the select and the checkbox. That duplication is the
   established shape in this system. */
.al-toast-close {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	/* the target is the small control tier, 28px, while the mark inside it is 16.
	   An icon the size of its own hit area is a mark you have to aim at. */
	width: var(--al-toast-dismiss-size);
	height: var(--al-toast-dismiss-size);
	/* pull the target back level with the first line: it is 28px against a 21.7px
	   line, so without this the whole row grows to fit the button. */
	margin-top: calc((var(--al-toast-line-height) * 1em - var(--al-toast-dismiss-size)) / 2);
	padding: 0;
	border: 0;
	border-radius: var(--al-toast-dismiss-radius);
	/* A BUTTON DOES NOT INHERIT ITS FONT. The browser hands it its own face and size,
	   and the margin above measures in `em`, so without these three the dismiss was
	   positioned by whatever the browser happened to default to: measured at -3.67px in
	   Chrome where the toast's own line box asks for -3.15px, and a different number in
	   the next engine. Same failure the token file names for box-sizing and
	   line-height, one control down. */
	font-family: inherit;
	font-size: var(--al-toast-font-size);
	line-height: var(--al-toast-line-height);
	background: var(--al-toast-dismiss-fill);
	color: var(--al-toast-dismiss-color);
	cursor: pointer;
}
.al-toast-close::before {
	content: '';
	/* box-sizing explicitly: the usual reset does not match pseudo-elements, and a
	   content-box mark with a border would centre only its content. */
	box-sizing: border-box;
	display: block;
	width: var(--al-toast-dismiss-icon-size);
	height: var(--al-toast-dismiss-icon-size);
	background: currentColor;
	-webkit-mask: var(--al-toast-dismiss-icon-image) center / contain no-repeat;
	mask: var(--al-toast-dismiss-icon-image) center / contain no-repeat;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}
/* hover moves the INK, not a background. A hover fill would have to be a colour
   that works over an opaque raised surface and over a tinted banner ground, and
   the sunken role is a grey patch on a pink one. The ink step from muted to
   primary is unambiguous and correct on every ground. */
.al-toast-close:hover {
	--al-toast-dismiss-color: var(--al-toast-dismiss-hover-color);
}
.al-toast-close:focus {
	outline: none;
}
.al-toast-close:focus-visible {
	/* the three-layer ring, verbatim: a transparent outline that forced-colors
	   repaints, then a light hairline, the ring, and a dark hairline. One of the two
	   hairlines always separates the ring from whatever is behind it. */
	outline: var(--al-toast-dismiss-focus-ring-width) solid transparent;
	outline-offset: var(--al-toast-dismiss-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-toast-dismiss-focus-hairline-width) var(--al-toast-dismiss-focus-hairline-light),
		0 0 0 calc(var(--al-toast-dismiss-focus-hairline-width) + var(--al-toast-dismiss-focus-ring-width)) var(--al-toast-dismiss-focus-ring-color),
		0 0 0 calc(var(--al-toast-dismiss-focus-hairline-width) * 2 + var(--al-toast-dismiss-focus-ring-width))
			var(--al-toast-dismiss-focus-hairline-dark);
}

@keyframes al-toast-in {
	from {
		opacity: 0;
		transform: scale(var(--al-toast-motion-scale));
	}
}
@keyframes al-toast-out {
	to {
		opacity: 0;
		transform: scale(var(--al-toast-motion-scale));
	}
}

/* LEAVING re-points four tokens and writes no property. Three are the animation.
   The fourth stops the clicks, because a toast fading out is still a 380px target
   sitting over the page and clicking a dismiss on a node that is already going away
   fires the handler twice. */
.al-toast-leaving {
	--al-toast-animation-name: al-toast-out;
	--al-toast-animation-duration: var(--al-sys-motion-duration-exit);
	--al-toast-animation-easing: var(--al-sys-motion-easing-exit);
	--al-toast-pointer-events: none;
}

.al-toast-error {
	--al-toast-status: var(--al-sys-color-status-error);
	--al-toast-icon-image: var(--al-sys-icon-status-error);
}
.al-toast-warning {
	--al-toast-status: var(--al-sys-color-status-warning);
	--al-toast-icon-image: var(--al-sys-icon-status-warning);
}
.al-toast-success {
	--al-toast-status: var(--al-sys-color-status-success);
	--al-toast-icon-image: var(--al-sys-icon-status-success);
}
.al-toast-info {
	--al-toast-status: var(--al-sys-color-status-info);
	--al-toast-icon-image: var(--al-sys-icon-status-info);
}

/* =============================================================================
   THE BANNER

   Page-wide and persistent. Something is ongoing — a sync is down, a licence is
   about to lapse, a count has locked a room — and it stays on screen until that
   stops being true. It carries an action, because a condition someone can do
   nothing about is a condition that should not be interrupting them.

   IT IS LOUDER THAN A MESSAGE, and that is why it has both the tint and a rule
   down its leading edge. A message is one thing among many on a form; a banner
   speaks for the whole page, so it gets the extra mark. Same tint, same glyph,
   one more signal.

   THE LAYOUT IS A GRID, not a wrapping flex row. Three columns — mark, words,
   dismiss — with the actions on a second row under the words. Flex wrapping put
   the dismiss below the buttons at narrow widths, which is where nobody looks for
   it; the grid puts it in the top corner at every width without a media or
   container query, and an action sitting under the sentence it belongs to reads
   better than one squeezed beside it.
   ============================================================================= */

.al-banner {
	--al-banner-status: var(--al-sys-color-status-info);
	--al-banner-icon-image: var(--al-sys-icon-status-info);

	--al-banner-mark: var(--al-banner-status); /* upgraded in the @supports block */
	--al-banner-fill: var(--al-sys-color-surface); /* likewise */
	--al-banner-border-color: var(--al-sys-color-border);
	--al-banner-border-width: var(--al-sys-control-border-width);
	--al-banner-edge-width: 3px;
	--al-banner-radius: var(--al-sys-radius-container);

	--al-banner-padding-y: var(--al-sys-space-150);
	--al-banner-padding-x: var(--al-sys-space-300);
	--al-banner-column-gap: var(--al-sys-space-150);
	--al-banner-row-gap: var(--al-sys-space-150);
	--al-banner-title-gap: var(--al-sys-space-50);
	--al-banner-actions-gap: var(--al-sys-space-100);

	--al-banner-icon-size: var(--al-sys-icon-size-lg);
	--al-banner-font-size: var(--al-sys-font-size-300);
	--al-banner-line-height: var(--al-sys-line-height-body);
	--al-banner-title-color: var(--al-sys-color-text-primary);
	--al-banner-title-weight: var(--al-sys-font-weight-control);
	--al-banner-text-color: var(--al-sys-color-text-secondary);

	--al-banner-dismiss-size: var(--al-sys-control-height-sm);
	--al-banner-dismiss-icon-size: var(--al-sys-icon-size-md);
	--al-banner-dismiss-icon-image: var(--al-sys-icon-dismiss);
	--al-banner-dismiss-color: var(--al-sys-color-text-muted);
	--al-banner-dismiss-hover-color: var(--al-sys-color-text-primary);
	--al-banner-dismiss-fill: transparent;
	--al-banner-dismiss-radius: var(--al-sys-radius-sm);
	--al-banner-dismiss-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-banner-dismiss-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-banner-dismiss-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-banner-dismiss-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-banner-dismiss-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: grid;
	/* minmax(0,1fr), not 1fr: a bare 1fr has an auto minimum, so one long unbroken
	   manifest number stops the column shrinking and pushes the dismiss off the end. */
	grid-template-columns: auto minmax(0, 1fr) auto;
	column-gap: var(--al-banner-column-gap);
	row-gap: var(--al-banner-row-gap);
	align-items: start;
	padding: var(--al-banner-padding-y) var(--al-banner-padding-x);
	border: var(--al-banner-border-width) solid var(--al-banner-border-color);
	border-inline-start: var(--al-banner-edge-width) solid var(--al-banner-mark);
	border-radius: var(--al-banner-radius);
	background: var(--al-banner-fill);
	color: var(--al-banner-text-color);
	font-family: inherit;
	font-size: var(--al-banner-font-size);
	line-height: var(--al-banner-line-height);
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;

	/* THE ONE COMPONENT IN THIS FILE THAT CARRIES ITS OWN BOTTOM MARGIN. [ALLM-7439]
	   Everything else leaves rhythm to its container -- "the gap owns the rhythm" is said
	   twice above, at the sheet body and the popover -- so this is a deliberate exception
	   rather than an oversight being corrected. Jeff ruled it on the ticket, option 1 of
	   three, on a block that carried the diff.

	   WHY THE EXCEPTION IS RIGHT HERE: a banner is never the last thing on a page. It exists
	   to precede the content it is about, so unlike a card or a panel there is no arrangement
	   in which a flush bottom edge is correct.

	   THE SCOPED ALTERNATIVE WAS MEASURED AND REJECTED. A child selector under .al-cfg reaches
	   about three pages: of 251 banners in the app, 219 are in views/*.njk outside that
	   container entirely, and even among the 32 config ones they are not all direct children --
	   fees nests one inside a card. It would have looked like a fix for every page and been a
	   fix for almost none, against a ticket that said this is not the only page.

	   NO KNOB ON PURPOSE. A --al-banner-margin-bottom token, so a page could re-point this to
	   0, was offered as a separate option and not taken. Nothing in the app currently wants a
	   flush banner, and a component with nineteen tokens does not need a twentieth for a case
	   that does not exist yet.

	   Nothing else sets a margin on .al-banner: grepped before adding, and the three margin
	   declarations nearby are on the icon, the title and the dismiss, inside the box rather
	   than around it. */
	margin-bottom: var(--al-sys-space-300);
}

.al-banner-icon {
	box-sizing: border-box;
	display: block;
	grid-area: 1 / 1 / 2 / 2;
	width: var(--al-banner-icon-size);
	height: var(--al-banner-icon-size);
	margin-top: calc((var(--al-banner-line-height) * 1em - var(--al-banner-icon-size)) / 2);
	background: var(--al-banner-mark);
	-webkit-mask: var(--al-banner-icon-image) center / contain no-repeat;
	mask: var(--al-banner-icon-image) center / contain no-repeat;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}
.al-banner-body {
	grid-area: 1 / 2 / 2 / 3;
	min-width: 0;
}
.al-banner-body > strong {
	display: block;
	color: var(--al-banner-title-color);
	font-weight: var(--al-banner-title-weight);
}
.al-banner-body > p {
	margin: 0;
}
.al-banner-body > strong + p {
	margin-top: var(--al-banner-title-gap);
}
/* row two, starting under the words and running to the outer edge. The buttons are
   ordinary .al-btn — the banner's colour says what the condition is, the button's
   says what the action does, and those are two axes this system already separates. */
.al-banner-actions {
	grid-area: 2 / 2 / 3 / 4;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--al-banner-actions-gap);
}
.al-banner-close {
	box-sizing: border-box;
	grid-area: 1 / 3 / 2 / 4;
	display: flex;
	align-items: center;
	justify-content: center;
	width: var(--al-banner-dismiss-size);
	height: var(--al-banner-dismiss-size);
	margin-top: calc((var(--al-banner-line-height) * 1em - var(--al-banner-dismiss-size)) / 2);
	padding: 0;
	border: 0;
	border-radius: var(--al-banner-dismiss-radius);
	/* see the toast's dismiss: a button inherits no font, and the margin above
	   measures in `em`. */
	font-family: inherit;
	font-size: var(--al-banner-font-size);
	line-height: var(--al-banner-line-height);
	background: var(--al-banner-dismiss-fill);
	color: var(--al-banner-dismiss-color);
	cursor: pointer;
}
.al-banner-close::before {
	content: '';
	box-sizing: border-box;
	display: block;
	width: var(--al-banner-dismiss-icon-size);
	height: var(--al-banner-dismiss-icon-size);
	background: currentColor;
	-webkit-mask: var(--al-banner-dismiss-icon-image) center / contain no-repeat;
	mask: var(--al-banner-dismiss-icon-image) center / contain no-repeat;
	print-color-adjust: exact;
	-webkit-print-color-adjust: exact;
}
.al-banner-close:hover {
	--al-banner-dismiss-color: var(--al-banner-dismiss-hover-color);
}
.al-banner-close:focus {
	outline: none;
}
.al-banner-close:focus-visible {
	outline: var(--al-banner-dismiss-focus-ring-width) solid transparent;
	outline-offset: var(--al-banner-dismiss-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-banner-dismiss-focus-hairline-width) var(--al-banner-dismiss-focus-hairline-light),
		0 0 0 calc(var(--al-banner-dismiss-focus-hairline-width) + var(--al-banner-dismiss-focus-ring-width))
			var(--al-banner-dismiss-focus-ring-color),
		0 0 0 calc(var(--al-banner-dismiss-focus-hairline-width) * 2 + var(--al-banner-dismiss-focus-ring-width))
			var(--al-banner-dismiss-focus-hairline-dark);
}

.al-banner-error {
	--al-banner-status: var(--al-sys-color-status-error);
	--al-banner-icon-image: var(--al-sys-icon-status-error);
}
.al-banner-warning {
	--al-banner-status: var(--al-sys-color-status-warning);
	--al-banner-icon-image: var(--al-sys-icon-status-warning);
}
.al-banner-success {
	--al-banner-status: var(--al-sys-color-status-success);
	--al-banner-icon-image: var(--al-sys-icon-status-success);
}
.al-banner-info {
	--al-banner-status: var(--al-sys-color-status-info);
	--al-banner-icon-image: var(--al-sys-icon-status-info);
}

/* FLUSH — the banner sits directly under the app header and runs edge to edge, so
   the rounded corners have nothing to be rounded against. One re-point, because a
   variant re-points tokens; the leading rule and the hairline stay, and at full
   bleed the side hairlines land at the screen edge where they cost nothing. */
.al-banner-flush {
	--al-banner-radius: 0;
}

/* =============================================================================
   THE TWO DERIVED VALUES.

   Everything above works without this block — the ground falls back to the plain
   surface and the mark to the status colour at full strength, which is a coherent
   design. This block is the better version of it where the engine can compute it,
   and it is guarded rather than written inline because a color-mix an engine
   cannot parse computes to the guaranteed-invalid value, which would take
   `background` to unset and delete the glyph entirely.

   14%, one number for both grounds. The row tints needed 10 on light and 16 on
   dark because a percentage that reads over white disappears over slate-900, and
   a ground can re-point a sys role to say so. A component token cannot, so this
   is the single value that is soft enough on white and present enough on the dark
   panel, and the hairline and the mark do the rest of the work of finding the box.
   ============================================================================= */
@supports (color: color-mix(in srgb, currentColor 50%, currentColor)) {
	.al-msg {
		--al-message-fill: color-mix(in srgb, var(--al-message-status) 14%, transparent);
		--al-message-mark: color-mix(in srgb, var(--al-message-status) 70%, var(--al-sys-color-text-primary));
	}
	.al-toast {
		/* the toast keeps its opaque ground; only the mark is derived */
		--al-toast-mark: color-mix(in srgb, var(--al-toast-status) 70%, var(--al-sys-color-text-primary));
	}
	.al-banner {
		--al-banner-fill: color-mix(in srgb, var(--al-banner-status) 14%, transparent);
		--al-banner-mark: color-mix(in srgb, var(--al-banner-status) 70%, var(--al-sys-color-text-primary));
	}
}

/* =============================================================================
   REDUCED MOTION — keep only what carries meaning.

   The toast's arrival IS the message: something happened, and here is what. So the
   fade stays, because with it gone a toast simply materialises and someone who has
   turned motion down loses the cue that it is new. The scale is polish and goes,
   and it goes by re-pointing the token the keyframes read rather than by writing a
   second set of keyframes.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-toast {
		--al-toast-motion-scale: 1;
	}
}

/* ---- thirdparty ---------------------------------------------------- */
/* =============================================================================
   THIRD-PARTY CONTROLS — A SPECIFICATION, NOT AN IMPLEMENTATION

   Six controls the product does not build and is not going to: a data grid, a date
   picker, a time picker, a combobox, a multi-select and an autocomplete. Each one
   carries data binding, paging, virtualisation, keyboard handling and screen-reader
   wiring, and every hour spent re-implementing that is an hour not spent on the
   product. So they stay third-party.

   WHO IMPLEMENTS THEM TODAY — counted in the running product, not assumed
     data grid      Kendo Grid (43 admin files, 13 React). ag-Grid + ag-Grid Enterprise
                    on 9 admin screens, loaded on every admin page. TWO implementations
                    of ONE appearance; ag-Grid is not going anywhere on a known date and
                    is spec'd here accordingly. An earlier version of this file said it
                    was being retired and used that to skip the work. It was wrong.
     date picker    Kendo DatePicker, angular-datepicker (4), react-datepicker (7).
                    Kendo DateRangePicker is this spec twice in one frame, not a spec.
     time picker    Kendo TimePicker, and DateTimePicker, which is two panels.
     combobox       Kendo ComboBox + DropDownList (119), and ui-select (51 admin files,
                    currently wearing theme="bootstrap" in 34 of them).
     multi-select   Kendo MultiSelect (45). ui-select in multiple mode, one place.
     autocomplete   Kendo AutoComplete, and ui-select where its search hits the server.

   WHAT IS NOT HERE, AND WHY THAT IS THE POINT
     Kendo Button (70), Switch (49), Tooltip (24), NumericTextBox (12), TextArea (11),
     TextBox (11), Upload (8), Window / React Dialog (93), Loader (15), Labels and Form
     (36), Layout (23). Bootstrap's modal (88). react-toastify, react-loading-skeleton,
     react-pro-sidebar. Every one of those is a control THE RECIPE ALREADY BUILDS, so
     it gets a MAPPING in recipe.html, not a second spec here. Writing an --al-kendo-*
     answer for a switch the recipe has already answered is the exact duplication this
     architecture exists to prevent. There is no --al-kendo-* and there will not be.

   TWO VERSION SPLITS THAT CONSTRAIN THE OVERRIDE LAYER
     Bootstrap 4.6.1 in the admin, 5.2 in Go Console: two mapping files, shared tokens,
     different selectors. Kendo theme 5.x in POS, 7.0 in Go Console: Kendo renamed its
     own theme variables between them, so one set of recipe tokens needs two bridges.
     Recorded, not solved. Unifying majors is a real project and wants a real reason.

   WHAT THIS FILE IS
     The appearance, written as tokens, plus enough CSS to render a STATIC SPECIMEN
     of each so a person can hold Kendo's output against it and see the difference.
     The specimens have no behaviour on purpose. Nothing here binds data, opens a
     popup, moves a selection or handles a key. Where a specimen shows an open popup,
     the popup is drawn in the flow of the page — placement is the library's job.

   WHAT IT IS FOR
     Two things. Kendo's own theming is pointed at these tokens, so one change to the
     recipe moves the library with it. And if Kendo is retired, the spec survives the
     library: the next control gets pointed at the same names.

   NO BEHAVIOUR, NO JAVASCRIPT, NO KEYBOARD HANDLING. If you find yourself wanting to
   write any of the three here, you are rebuilding the control, which is the thing
   this file exists to avoid.

   THE RULES IT FOLLOWS — the same eight as the recipe
     Every colour, size and duration below reads a --al-sys-* role on the right-hand
     side of a component token, and the CSS properties read only component tokens. Not
     one rule in this file names a control inside .al-on-dark: a control that reads the
     shared roles is correct on both grounds the day it is written, and thirteen of the
     holes an earlier audit found were controls that had reached for a value directly.

   GLYPHS ARE MASKS, NOT IMAGES. The shape comes from an SVG and the colour comes from
   a token, so a theme can move it. The stroke colour inside each data URI is ignored —
   a mask reads alpha. This is the checkbox's pattern, and it is the better of the two
   the recipe has.

   BUTTONS ARE THE RECIPE'S BUTTONS. A calendar footer, a grid pager, a popup's Apply:
   all .al-btn. None of them gets a token here, and the override layer maps the
   library's own buttons onto .al-btn rather than restyling them twice.
   ============================================================================= */

/* =============================================================================
   THE DATA GRID — .al-grid
   Kendo Grid, and ag-Grid on nine admin screens. Two implementations, one appearance:
   ag-theme-alpine is variable-driven, so its variables get pointed at --al-datagrid-*
   and the values stay here. Row height has to be told to ag-Grid in its JS config as
   well as its CSS, and it must be the SAME number, or virtualisation mispositions rows.

   A grid is a container that holds controls, so it takes the container radius, and
   the chrome lives on a wrapper rather than on the <table>: border-collapse squares
   off a rounded corner, and a sticky header loses its border under it. The wrapper
   owns the edge and clips; the table inside is bare.
   ============================================================================= */

.al-grid {
	/* -- the container -------------------------------------------------------- */
	--al-datagrid-fill: var(--al-sys-color-surface);
	--al-datagrid-border-width: var(--al-sys-control-border-width);
	--al-datagrid-border-color: var(--al-sys-color-border);
	--al-datagrid-radius: var(--al-sys-radius-container);
	--al-datagrid-font-size: var(--al-sys-font-size-300);
	/* set, not inherited. The host page's 1.65 puts a 23px line box inside a 36px row
	   and the density tier stops being a tier. Same failure as borrowing box-sizing. */
	--al-datagrid-line-height: var(--al-sys-line-height-control);
	--al-datagrid-cell-padding-x: var(--al-sys-space-150);
	--al-datagrid-gap: var(--al-sys-control-gap); /* a label to the mark beside it */
	/* an editable number in a numeric cell is as wide as the number, not as wide as the
	   column. Eight characters holds 99,999.99 and every price this product prints. */
	--al-datagrid-numeric-input-width: 8ch;

	/* -- the header ----------------------------------------------------------- */
	--al-datagrid-header-height: var(--al-sys-row-height-sm);
	/* the recessed ground, which is a LIFT on a dark panel and a tint on a light one.
	   Either way the header separates from the rows without a second border. */
	--al-datagrid-header-fill: var(--al-sys-color-surface-sunken);
	--al-datagrid-header-text: var(--al-sys-color-text-secondary);
	--al-datagrid-header-font-size: var(--al-sys-font-size-200);
	--al-datagrid-header-weight: var(--al-sys-font-weight-control);
	--al-datagrid-header-border-color: var(--al-sys-color-border);
	/* the vertical rule between header cells. It is also where the resize grip lives,
	   so it is a hairline BETWEEN things rather than the edge of a control. */
	--al-datagrid-header-divider-color: var(--al-sys-color-border);
	/* the header is sticky in every real grid, so it needs the rung that says
	   "content is passing under me" and a layer that keeps it there. */
	--al-datagrid-header-elevation: var(--al-sys-elevation-sticky);
	--al-datagrid-header-layer: var(--al-sys-layer-sticky);

	/* -- sorting -------------------------------------------------------------- */
	--al-datagrid-sort-color: var(--al-sys-color-accent-primary-line);
	--al-datagrid-sort-size: var(--al-sys-icon-chevron-size); /* smaller than the icon tier
	     on purpose: this mark sits inside a 13px label and the 14px rung crowds it */
	--al-datagrid-sort-glyph: var(--al-sys-icon-chevron);
	/* the little 2 on a second sort column. Quiet: it is a fact about the sort, not a
	   thing you act on, and without it a two-column sort looks like a bug. */
	--al-datagrid-sort-index-color: var(--al-sys-color-text-muted);
	--al-datagrid-sort-index-font-size: var(--al-sys-font-size-100);

	/* -- the rows ------------------------------------------------------------- */
	--al-datagrid-row-height: var(--al-sys-row-height-data);
	/* Its own token rather than sharing the row's: a column edge and a row edge are the same
	   colour today and are not the same decision, and a density variant may want one and not
	   the other. */
	--al-datagrid-column-border-color: var(--al-sys-color-border);
	--al-datagrid-row-text: var(--al-sys-color-text-secondary);
	/* the identifying column — the tag, the order number — is the one you read down.
	   Primary ink there and body ink everywhere else is what makes a grid scannable. */
	--al-datagrid-row-key-text: var(--al-sys-color-text-primary);
	--al-datagrid-row-key-weight: var(--al-sys-font-weight-medium);
	--al-datagrid-row-border-color: var(--al-sys-color-border);
	/* two steps of one ladder, from the shared roles: chosen is tinted, where-you-are
	   is tinted harder. No zebra striping — it spends the same channel on nothing. */
	--al-datagrid-row-selected-fill: var(--al-sys-color-row-tint-selected);
	--al-datagrid-row-active-fill: var(--al-sys-color-row-tint-active);
	/* colour is never the only signal, so a chosen row also carries a bar. */
	--al-datagrid-row-selected-marker-color: var(--al-sys-color-accent-primary-line);
	--al-datagrid-row-selected-marker-width: 3px;

	/* numbers line up under each other, which is the only reason a numeric column is
	   readable at all. The face is the system's, not a mono novelty. */
	--al-datagrid-numeric-font-family: var(--al-sys-font-family-numeric);

	/* -- a group header row, and a totals row --------------------------------- */
	--al-datagrid-group-height: var(--al-sys-row-height-sm);
	--al-datagrid-group-fill: var(--al-sys-color-surface-sunken);
	--al-datagrid-group-text: var(--al-sys-color-text-primary);
	--al-datagrid-group-weight: var(--al-sys-font-weight-control);
	--al-datagrid-group-count-color: var(--al-sys-color-text-muted);
	--al-datagrid-group-count-weight: var(--al-sys-font-weight-body);
	--al-datagrid-group-caret-color: var(--al-sys-color-text-muted);
	--al-datagrid-group-caret-size: var(--al-sys-icon-size-sm);
	--al-datagrid-summary-fill: var(--al-sys-color-surface-sunken);
	--al-datagrid-summary-text: var(--al-sys-color-text-primary);
	--al-datagrid-summary-weight: var(--al-sys-font-weight-control);

	/* -- a cell being edited --------------------------------------------------- */
	--al-datagrid-edit-height: var(--al-sys-control-height-sm);
	--al-datagrid-edit-padding-x: var(--al-sys-space-100);
	--al-datagrid-edit-fill: var(--al-sys-color-surface-control);
	--al-datagrid-edit-text: var(--al-sys-color-text-primary);
	--al-datagrid-edit-font-size: var(--al-sys-font-size-200);
	--al-datagrid-edit-border-width: var(--al-sys-control-border-width);
	--al-datagrid-edit-border-color: var(--al-sys-color-border-control);
	--al-datagrid-edit-radius: var(--al-sys-control-radius);
	--al-datagrid-edit-invalid-border-color: var(--al-sys-color-status-error);
	--al-datagrid-edit-focus-ring-color: var(--al-sys-color-focus-ring);

	/* -- focus: the three-layer ring, on the cell and on the editor ------------
	   The same sandwich the button wears — a light hairline, the ring, a dark
	   hairline — so one of the two always separates it from whatever is behind. On a
	   CELL it is drawn inset, because a cell has neighbours on every side and an
	   outset ring would be clipped by the one next to it. Same three layers, same
	   order, read inward. */
	--al-datagrid-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-datagrid-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-datagrid-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-datagrid-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-datagrid-focus-ring-color: var(--al-sys-color-focus-ring);

	/* -- the footer, and nothing in a grid with no rows ------------------------ */
	--al-datagrid-footer-height: var(--al-sys-row-height-md);
	--al-datagrid-footer-fill: var(--al-sys-color-surface-sunken);
	--al-datagrid-footer-text: var(--al-sys-color-text-muted);
	--al-datagrid-footer-font-size: var(--al-sys-font-size-200);
	--al-datagrid-footer-border-color: var(--al-sys-color-border);
	--al-datagrid-footer-gap: var(--al-sys-space-150);
	--al-datagrid-footer-padding-y: var(--al-sys-space-50);
	--al-datagrid-pager-gap: var(--al-sys-space-50);
	--al-datagrid-empty-text: var(--al-sys-color-text-muted);
	--al-datagrid-empty-padding-y: var(--al-sys-space-600);

	/* -- properties: only --al-datagrid-* below this line ---------------------- */
	box-sizing: border-box; /* the system owns its box model — see the button */
	/* A FRAME THAT IS SHORTER THAN ITS ROWS HANDS THE HEIGHT TO ITS SCROLLPORT, rather
	   than clipping it. Measured in the action dialog with ten batches: the frame was
	   flex-shrunk to 341px, the scrollport rendered its full 397 and overflow:hidden cut
	   the last row in half -- and because nothing constrained the scrollport, no scrollbar
	   ever appeared. A column layout plus min-height:0 below is what lets the shrink reach
	   the child that can actually scroll. */
	display: flex;
	flex-direction: column;
	background: var(--al-datagrid-fill);
	border: var(--al-datagrid-border-width) solid var(--al-datagrid-border-color);
	border-radius: var(--al-datagrid-radius);
	overflow: hidden; /* what makes the corner radius reach the header and the footer */
	font-size: var(--al-datagrid-font-size);
	line-height: var(--al-datagrid-line-height);
	color: var(--al-datagrid-row-text);
}

/* The scrollport. It exists so the header has something to stick to; give it a height
   and the header stays while the rows pass under it.

   min-height:0 is the whole fix: a flex item's floor is its content, so without it the
   scrollport refuses to shrink, the frame clips it, and the scrollbar it exists to
   provide never appears. */
.al-grid-scroll {
	box-sizing: border-box;
	min-block-size: 0;
	overflow: auto;
}

/* border-collapse: SEPARATE, deliberately. Under `collapse` a sticky header's border
   is owned by the table rather than the cell and slides away with the rows, which is
   the classic "the line under the header disappears when you scroll" bug. Separate
   with zero spacing looks identical and each cell keeps its own edge. */
.al-grid table {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
	font-size: inherit;
}

.al-grid thead th {
	box-sizing: border-box;
	position: sticky;
	top: 0;
	z-index: var(--al-datagrid-header-layer);
	height: var(--al-datagrid-header-height);
	padding: 0 var(--al-datagrid-cell-padding-x);
	text-align: left;
	white-space: nowrap;
	/* OPAQUE, AND STILL THE COLOUR IT ALWAYS WAS. The header is sticky, so rows pass
	   underneath it -- and on a dark ground the sunken role is a translucent white lift at
	   9%, which let the rows read straight through the column names. Measured on the POS
	   Visitor queue: a row's ink scored 8.65:1 against the header it was sliding under, so
	   the data was more legible than the labels it was passing.

	   The tint is painted as a solid background-image OVER the grid's own opaque ground
	   rather than straight onto the cell, so it composites against that ground instead of
	   against whatever happens to be scrolling behind. Same pixels on both grounds --
	   #f7f8f9 light, #2b3038 dark, exactly what it rendered before -- and nothing shows
	   through. Re-pointing the token instead would have moved the colour; this does not.

	   .al-table already made this call for the same reason (see its --al-table-header-fill
	   note); this is .al-grid catching up with it, without giving up its header band. */
	background-color: var(--al-datagrid-fill);
	background-image: linear-gradient(var(--al-datagrid-header-fill), var(--al-datagrid-header-fill));
	color: var(--al-datagrid-header-text);
	font-size: var(--al-datagrid-header-font-size);
	font-weight: var(--al-datagrid-header-weight);
	line-height: var(--al-datagrid-line-height);
	border-bottom: var(--al-datagrid-border-width) solid var(--al-datagrid-header-border-color);
	border-right: var(--al-datagrid-border-width) solid var(--al-datagrid-header-divider-color);
	box-shadow: var(--al-datagrid-header-elevation);
}
.al-grid thead th:last-child {
	border-right: 0;
}
/* A NUMERIC COLUMN'S HEADER SITS OVER ITS NUMBERS. `.al-grid thead th` sets text-align:
   left at (0,1,2) and beat `.al-grid-num` at (0,1,0), so every numeric column in the app
   carried a left-aligned label above a right-aligned column and the two did not line up.
   Scoped the same way the row rules are, which is the only way to win it. */
.al-grid thead th.al-grid-num {
	text-align: right;
}
/* A SPANNING HEADER, over the columns of one group: a market, a period, a site. The group
   is named once above its columns instead of being repeated inside each of their names,
   which is the difference between `Medicinal | Out the door` twice and `Medicinal` once.
   It sits in the first of two header rows; the columns it spans sit in the second.

   Centred and ruled on both edges, because a label over two columns has to say which two. */
.al-grid thead th.al-grid-colgroup {
	text-align: center;
	border-inline: var(--al-datagrid-border-width) solid var(--al-datagrid-border-color);
}
/* the label is wrapped so it can truncate: text-overflow does nothing to a bare text
   node in a flex container, and a header cell that carries a sort mark is one. */
.al-grid-hd {
	display: flex;
	align-items: center;
	gap: var(--al-datagrid-gap);
}
.al-grid-hd > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.al-grid-hd.al-grid-num {
	justify-content: flex-end;
}

/* THE SORT MARK. Present only on a column that IS sorted — a mark on every sortable
   column is twelve marks saying nothing. Descending is the resting shape and ascending
   is the same glyph turned over, so there is one shape to recognise, not two. */
.al-grid-sort {
	flex: none;
	width: var(--al-datagrid-sort-size);
	height: var(--al-datagrid-sort-size);
	background: var(--al-datagrid-sort-color);
	-webkit-mask: var(--al-datagrid-sort-glyph) center / contain no-repeat;
	mask: var(--al-datagrid-sort-glyph) center / contain no-repeat;
}
.al-grid-sort[data-dir='asc'] {
	transform: rotate(180deg);
}
.al-grid-sort-index {
	flex: none;
	color: var(--al-datagrid-sort-index-color);
	font-size: var(--al-datagrid-sort-index-font-size);
	font-variant-numeric: tabular-nums;
}

/* VERTICAL RULES, and this reverses an earlier call. The component said horizontal
   hairlines only, on the argument that column edges are a wall of lines the eye has to
   cross. Shown the two side by side over real data, Jeff kept the rules: a grid is a
   table of VALUES and the edge is what tells you which column a number belongs to when
   the column beside it is also numbers. It is also what every Kendo grid in the product
   already renders, so the recipe was describing something no screen showed.

   Left, not right, and never on the first cell, so the outer edge stays the container's. */
/* A ROW HEADER IS A CELL. Written for td alone, this left every <th scope="row"> with the
   browser's own 1px padding, no row height, no borders and the UA's centred bold -- so the
   first column of every grid that names its rows was a different cell from the four beside
   it. Measured in the action manifest: 1px against 8px. */
.al-grid tbody tr > td,
.al-grid tbody tr > th {
	box-sizing: border-box;
	height: var(--al-datagrid-row-height);
	padding: 0 var(--al-datagrid-cell-padding-x);
	vertical-align: middle;
	line-height: var(--al-datagrid-line-height);
	border-bottom: var(--al-datagrid-border-width) solid var(--al-datagrid-row-border-color);
	border-left: var(--al-datagrid-border-width) solid var(--al-datagrid-column-border-color);
	background: transparent; /* stated, so the row tint behind it shows through */
}
.al-grid tbody tr > td:first-child,
.al-grid tbody tr > th:first-child,
.al-grid thead th:first-child {
	border-left: 0;
}
.al-grid tbody tr:last-child > td,
.al-grid tbody tr:last-child > th {
	border-bottom: 0;
}
/* the UA centres a th and bolds it. Undone HERE and not in the rule above, because that
   one covers td as well and text-align there would outrank .al-grid-num on specificity --
   which silently left-aligned every number and every row action. */
.al-grid tbody tr > th {
	text-align: left;
	font-weight: inherit;
}
/* the key IS the row's name, so it carries the weight whether or not its text is wrapped
   in a span -- .al-grid-key > span styles the wrapped case and this one the bare text */
.al-grid tbody tr > th.al-grid-key {
	color: var(--al-datagrid-row-key-text);
	font-weight: var(--al-datagrid-row-key-weight);
}
/* the cell's content is wrapped for the same reason the header's is.

   :not() ON THE TWO SHAPES THAT ARE ALSO SPANS, for the same reason .al-field > span
   excludes .al-affix. A status column is a badge in a cell and a tag list is tags in a
   cell; both are spans, and display:block at (0,1,2) beat their own inline-flex at
   (0,1,0), so a pill stretched the whole column. Measured: a 110px "open" badge in a
   118px cell. Truncation is for a cell of TEXT; a badge is already as wide as its word. */
.al-grid td > span:not(.al-badge):not(.al-tag) {
	display: block;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.al-grid-key > span {
	color: var(--al-datagrid-row-key-text);
	font-weight: var(--al-datagrid-row-key-weight);
}
.al-grid-num {
	text-align: right;
	font-family: var(--al-datagrid-numeric-font-family);
	font-variant-numeric: tabular-nums;
}
/* AN EDITABLE NUMERIC CELL SIZES AND ALIGNS ITS OWN CONTROL, which is exactly what
   `.al-input-number` says a table cell is for: "the box is as wide as the layout, not as
   wide as the number ... a column of numbers lining up on the decimal point is a table
   cell's job, where the cell is sized to the number." This is that job.

   No class on the input. A number in a numeric cell is already declared numeric by the
   cell it is in, and a second class on every one of forty boxes is a second thing to
   forget. */
.al-grid td.al-grid-num > .al-input {
	display: block;
	inline-size: var(--al-datagrid-numeric-input-width);
	margin-inline-start: auto;
	text-align: right;
}

/* WHERE YOU ARE, AND WHAT IS CHOSEN. Two facts, two steps of one ladder. The pointer
   and the keyboard get the same treatment because they are the same fact. */
.al-grid tbody tr:hover td,
.al-grid tbody tr[data-active='true'] td {
	background: var(--al-datagrid-row-active-fill);
}
.al-grid tbody tr[aria-selected='true'] td {
	background: var(--al-datagrid-row-selected-fill);
}
.al-grid tbody tr[aria-selected='true']:hover td,
.al-grid tbody tr[aria-selected='true'][data-active='true'] td {
	background: var(--al-datagrid-row-active-fill);
}
/* the bar rides on the first cell. A box-shadow on a <tr> is not painted reliably, and
   this is the one place that costs a rule rather than a token. */
.al-grid tbody tr[aria-selected='true'] td:first-child {
	box-shadow: inset var(--al-datagrid-row-selected-marker-width) 0 0 0 var(--al-datagrid-row-selected-marker-color);
}

/* THE FOCUSED CELL — the keyboard's position inside the grid, which is a different
   fact from which row is chosen and needs its own mark. Inset, so the neighbours do
   not clip it. */
.al-grid td[data-focus='true'],
.al-grid td:focus-visible {
	outline: none;
	box-shadow:
		inset 0 0 0 var(--al-datagrid-focus-hairline-width) var(--al-datagrid-focus-hairline-light),
		inset 0 0 0 calc(var(--al-datagrid-focus-hairline-width) + var(--al-datagrid-focus-ring-width)) var(--al-datagrid-focus-ring-color),
		inset 0 0 0 calc(var(--al-datagrid-focus-hairline-width) * 2 + var(--al-datagrid-focus-ring-width)) var(--al-datagrid-focus-hairline-dark);
}

/* A CELL BEING EDITED. The editor is a control inside a row, so it takes the compact
   control tier — 28 inside 44 — and leaves air above and below rather than filling the
   row edge to edge, which would make the grid look like it had lost a line. */
.al-grid-edit {
	box-sizing: border-box;
	display: block;
	width: 100%;
	min-height: var(--al-datagrid-edit-height);
	padding: 0 var(--al-datagrid-edit-padding-x);
	border: var(--al-datagrid-edit-border-width) solid var(--al-datagrid-edit-border-color);
	border-radius: var(--al-datagrid-edit-radius);
	background: var(--al-datagrid-edit-fill);
	color: var(--al-datagrid-edit-text);
	font-family: inherit;
	font-size: var(--al-datagrid-edit-font-size);
	line-height: var(--al-datagrid-line-height);
}
.al-grid-edit.al-grid-num {
	text-align: right;
}
.al-grid-edit:focus,
.al-grid-edit[data-focus='true'] {
	outline: var(--al-datagrid-focus-ring-width) solid transparent;
	outline-offset: var(--al-datagrid-focus-hairline-width);
	border-color: var(--al-datagrid-edit-border-color);
	box-shadow:
		0 0 0 var(--al-datagrid-focus-hairline-width) var(--al-datagrid-focus-hairline-light),
		0 0 0 calc(var(--al-datagrid-focus-hairline-width) + var(--al-datagrid-focus-ring-width)) var(--al-datagrid-edit-focus-ring-color),
		0 0 0 calc(var(--al-datagrid-focus-hairline-width) * 2 + var(--al-datagrid-focus-ring-width)) var(--al-datagrid-focus-hairline-dark);
}
/* a value the server rejected keeps its red edge under the cursor and turns the RING
   red instead — the input's lesson, and the reason it is a token re-point, not a
   border-color property on a state rule */
.al-grid-edit[aria-invalid='true'] {
	--al-datagrid-edit-border-color: var(--al-datagrid-edit-invalid-border-color);
}
.al-grid-edit[aria-invalid='true']:focus,
.al-grid-edit[aria-invalid='true'][data-focus='true'] {
	--al-datagrid-edit-focus-ring-color: var(--al-sys-color-status-error);
}

/* A GROUP ROW. It spans the table, so it reads the recessed ground rather than a row
   tint — a tint here would be competing with the two that already mean something.

   SCOPED THE SAME WAY THE ROW RULE IS, and that is not decoration. Written as
   `.al-grid-group td` this lost to `.al-grid tbody tr td` on specificity (0,1,1 against
   0,1,3): the group row rendered at the 44px body rung while its own token said 36, and
   the hover tint painted over its ground as well. Measured, not reasoned about. Beat
   competing CSS by scoping under a real ancestor, never !important. */
.al-grid tbody tr.al-grid-group td {
	height: var(--al-datagrid-group-height);
	background: var(--al-datagrid-group-fill);
	color: var(--al-datagrid-group-text);
	font-weight: var(--al-datagrid-group-weight);
}
.al-grid-group td > span {
	display: flex;
	align-items: center;
	gap: var(--al-datagrid-gap);
}
.al-grid-group-caret {
	flex: none;
	width: var(--al-datagrid-group-caret-size);
	height: var(--al-datagrid-group-caret-size);
	background: var(--al-datagrid-group-caret-color);
	-webkit-mask: var(--al-datagrid-sort-glyph) center / contain no-repeat;
	mask: var(--al-datagrid-sort-glyph) center / contain no-repeat;
}
.al-grid-group-caret[data-collapsed='true'] {
	transform: rotate(-90deg);
}
.al-grid-group-count {
	color: var(--al-datagrid-group-count-color);
	font-weight: var(--al-datagrid-group-count-weight);
}

.al-grid tfoot td {
	box-sizing: border-box;
	height: var(--al-datagrid-group-height);
	padding: 0 var(--al-datagrid-cell-padding-x);
	vertical-align: middle;
	line-height: var(--al-datagrid-line-height);
	background: var(--al-datagrid-summary-fill);
	color: var(--al-datagrid-summary-text);
	font-weight: var(--al-datagrid-summary-weight);
	border-top: var(--al-datagrid-border-width) solid var(--al-datagrid-border-color);
}

.al-grid-foot {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-datagrid-footer-gap);
	flex-wrap: wrap;
	min-height: var(--al-datagrid-footer-height);
	padding: var(--al-datagrid-footer-padding-y) var(--al-datagrid-cell-padding-x);
	background: var(--al-datagrid-footer-fill);
	color: var(--al-datagrid-footer-text);
	font-size: var(--al-datagrid-footer-font-size);
	line-height: var(--al-datagrid-line-height);
	border-top: var(--al-datagrid-border-width) solid var(--al-datagrid-footer-border-color);
}
.al-grid-pager {
	display: flex;
	align-items: center;
	gap: var(--al-datagrid-pager-gap);
}

/* NOTHING TO SHOW is a state of the grid, not a missing one. The header stays, so the
   columns still say what would be here and the filters above still make sense. */
.al-grid-empty {
	padding: var(--al-datagrid-empty-padding-y) var(--al-datagrid-cell-padding-x);
	text-align: center;
	color: var(--al-datagrid-empty-text);
}

/* DENSITY. A grid changes rung far more often than a button does — a picking screen
   wants every row it can get, a settings table wants air — so both variants earn
   their place. Header and body move together; a 52px row under a 36px header reads
   as a mistake. */
.al-grid-sm {
	--al-datagrid-row-height: var(--al-sys-row-height-sm);
	--al-datagrid-font-size: var(--al-sys-font-size-200);
	--al-datagrid-cell-padding-x: var(--al-sys-space-100);
	--al-datagrid-header-font-size: var(--al-sys-font-size-100);
	/* THE CORNER MOVES WITH THE DENSITY, for the reason stated above: the container
	   radius is measured for a card, a panel and a modal, and a dense grid inside one of
	   those is not a container of that size. At 12px on a two-row manifest inside a dialog
	   the corners were most of what the table said. */
	--al-datagrid-radius: var(--al-sys-radius-sm);
}
.al-grid-lg {
	--al-datagrid-row-height: var(--al-sys-row-height-lg);
	--al-datagrid-header-height: var(--al-sys-row-height-md);
	/* the group row is a header for its block, so it moves with the column header and
	   not with the body. Left behind it would be the one row in a large grid still on
	   the compact rung, which is the same "reads as a mistake" this variant exists to
	   avoid. */
	--al-datagrid-group-height: var(--al-sys-row-height-md);
	--al-datagrid-cell-padding-x: var(--al-sys-space-200);
}

/* =============================================================================
   THE DATE PICKER — .al-datepick
   Kendo DatePicker today.

   Two parts that are usually themed apart and should not be: a field that has to line
   up with every other field in the form, and a calendar panel that hangs off it.
   ============================================================================= */

.al-datepick {
	--al-datepicker-height: var(--al-sys-control-height-md);
	--al-datepicker-padding-x: var(--al-sys-control-padding-x-md);
	--al-datepicker-font-size: var(--al-sys-font-size-300);
	/* set, not inherited: the host page's line-height would decide this control's
	   height and the size tier would stop being one */
	--al-datepicker-line-height: var(--al-sys-line-height-control);
	--al-datepicker-font-weight: var(--al-sys-font-weight-body);
	--al-datepicker-gap: var(--al-sys-control-gap);
	--al-datepicker-radius: var(--al-sys-control-radius);
	--al-datepicker-border-width: var(--al-sys-control-border-width);
	--al-datepicker-border-color: var(--al-sys-color-border-control);
	--al-datepicker-fill: var(--al-sys-color-surface-control);
	--al-datepicker-text: var(--al-sys-color-text-primary);
	--al-datepicker-placeholder: var(--al-sys-color-text-placeholder);
	--al-datepicker-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-datepicker-disabled-opacity: var(--al-sys-state-disabled-opacity);
	/* the trigger is a mark, not a button: it opens the thing the whole field opens,
	   so giving it its own hit target would put two targets on one control */
	--al-datepicker-trigger-color: var(--al-sys-color-text-muted);
	--al-datepicker-trigger-size: var(--al-sys-icon-size-md);
	--al-datepicker-trigger-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='2' y='3.2' width='12' height='11' rx='1.6' fill='none' stroke='%23000' stroke-width='1.5'/%3E%3Cpath d='M2 6.6h12M5.4 1.6v3M10.6 1.6v3' stroke='%23000' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");

	--al-datepicker-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-datepicker-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-datepicker-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-datepicker-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-datepicker-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-datepicker-gap);
	width: 100%;
	flex: none;
	min-height: var(--al-datepicker-height);
	padding: 0 var(--al-datepicker-padding-x);
	border: var(--al-datepicker-border-width) solid var(--al-datepicker-border-color);
	border-radius: var(--al-datepicker-radius);
	background: var(--al-datepicker-fill);
	color: var(--al-datepicker-text);
	font-size: var(--al-datepicker-font-size);
	font-weight: var(--al-datepicker-font-weight);
	line-height: var(--al-datepicker-line-height);
	cursor: pointer;
}
/* the value is a real input so the field focuses like one. It gives up its own edge,
   its own ground and its own height: the wrapper owns all three, exactly as the affix
   owns them for the input inside it. */
.al-datepick > input {
	box-sizing: border-box;
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
	border: 0;
	padding: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: inherit;
	cursor: inherit;
}
.al-datepick > input:focus {
	outline: none;
}
.al-datepick > input::placeholder {
	color: var(--al-datepicker-placeholder);
}
.al-datepick-trigger {
	flex: none;
	width: var(--al-datepicker-trigger-size);
	height: var(--al-datepicker-trigger-size);
	background: var(--al-datepicker-trigger-color);
	-webkit-mask: var(--al-datepicker-trigger-glyph) center / contain no-repeat;
	mask: var(--al-datepicker-trigger-glyph) center / contain no-repeat;
}

.al-datepick:hover:not([aria-disabled='true']):not([data-readonly='true']) {
	--al-datepicker-border-color: var(--al-datepicker-hover-border-color);
}
.al-datepick:focus-within,
.al-datepick[data-focus='true'] {
	outline: var(--al-datepicker-focus-ring-width) solid transparent;
	outline-offset: var(--al-datepicker-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-datepicker-focus-hairline-width) var(--al-datepicker-focus-hairline-light),
		0 0 0 calc(var(--al-datepicker-focus-hairline-width) + var(--al-datepicker-focus-ring-width)) var(--al-datepicker-focus-ring-color),
		0 0 0 calc(var(--al-datepicker-focus-hairline-width) * 2 + var(--al-datepicker-focus-ring-width)) var(--al-datepicker-focus-hairline-dark);
}
.al-datepick:not([aria-invalid='true']):focus-within,
.al-datepick:not([aria-invalid='true'])[data-focus='true'] {
	--al-datepicker-border-color: var(--al-sys-color-focus-ring);
}
.al-datepick[aria-invalid='true'] {
	--al-datepicker-border-color: var(--al-sys-color-status-error);
}
.al-datepick[aria-invalid='true']:focus-within,
.al-datepick[aria-invalid='true'][data-focus='true'] {
	--al-datepicker-focus-ring-color: var(--al-sys-color-status-error);
}
.al-datepick[aria-disabled='true'] {
	opacity: var(--al-datepicker-disabled-opacity);
	pointer-events: none;
}
/* read-only is not disabled: a date set when the manifest was created is a fact you
   may read and copy, so it keeps full strength and loses the edge and the trigger */
.al-datepick[data-readonly='true'] {
	--al-datepicker-border-color: transparent;
	--al-datepicker-fill: var(--al-sys-color-surface-sunken);
	cursor: default;
}
.al-datepick[data-readonly='true'] .al-datepick-trigger {
	display: none;
}

.al-datepick-sm {
	--al-datepicker-height: var(--al-sys-control-height-sm);
	--al-datepicker-padding-x: var(--al-sys-control-padding-x-sm);
	--al-datepicker-font-size: var(--al-sys-font-size-200);
}
.al-datepick-lg {
	--al-datepicker-height: var(--al-sys-control-height-lg);
	--al-datepicker-padding-x: var(--al-sys-control-padding-x-lg);
	--al-datepicker-font-size: var(--al-sys-font-size-400);
}

/* -----------------------------------------------------------------------------
   THE CALENDAR PANEL. Every popup in this file takes the CONTROL radius rather than
   the container one: a popup is chrome hanging off a control and it should agree with
   that control's corner, not with a card's.
   ----------------------------------------------------------------------------- */
.al-datepick-cal {
	--al-datepicker-popup-fill: var(--al-sys-color-surface-raised);
	--al-datepicker-popup-border-color: var(--al-sys-color-border);
	--al-datepicker-popup-border-width: var(--al-sys-control-border-width);
	--al-datepicker-popup-radius: var(--al-sys-control-radius);
	--al-datepicker-popup-padding: var(--al-sys-space-100);
	--al-datepicker-line-height: var(--al-sys-line-height-control);
	--al-datepicker-popup-elevation: var(--al-sys-elevation-raised);
	--al-datepicker-popup-layer: var(--al-sys-layer-popover);
	--al-datepicker-popup-text: var(--al-sys-color-text-primary);

	--al-datepicker-nav-size: var(--al-sys-control-height-sm);
	--al-datepicker-nav-color: var(--al-sys-color-text-muted);
	--al-datepicker-nav-icon-size: var(--al-sys-icon-size-sm);
	--al-datepicker-nav-radius: var(--al-sys-control-radius);
	--al-datepicker-nav-hover-fill: var(--al-sys-color-row-tint-active);
	--al-datepicker-nav-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10 3L5 8l5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-datepicker-title-font-size: var(--al-sys-font-size-300);
	--al-datepicker-title-weight: var(--al-sys-font-weight-control);
	--al-datepicker-head-gap: var(--al-sys-space-100);
	--al-datepicker-head-padding: var(--al-sys-space-50);

	--al-datepicker-weekday-height: var(--al-sys-control-height-sm);
	--al-datepicker-weekday-color: var(--al-sys-color-text-muted);
	--al-datepicker-weekday-font-size: var(--al-sys-font-size-100);
	--al-datepicker-weekday-weight: var(--al-sys-font-weight-medium);

	/* a day is a target you click, so it takes a control tier rather than a row one */
	--al-datepicker-day-size: var(--al-sys-control-height-md);
	--al-datepicker-day-font-size: var(--al-sys-font-size-200);
	--al-datepicker-day-text: var(--al-sys-color-text-secondary);
	/* THE DAY'S GROUND IS A TOKEN, so every state RE-POINTS it instead of overriding
	   background. It was written the other way and hover — one specificity step above
	   the chosen day — replaced the accent fill while the white number stayed, which
	   measured 1.6:1 the moment the pointer crossed the chosen day. This is the whole
	   reason rule 8 exists. */
	--al-datepicker-day-fill: transparent;
	--al-datepicker-day-hover-fill: var(--al-sys-color-row-tint-active);
	/* the chosen day takes the accent as a FILL. One cell in thirty-five has to be
	   findable at a glance, and a tint is not enough at that size. */
	--al-datepicker-day-selected-fill: var(--al-sys-color-accent-primary);
	--al-datepicker-day-selected-text: var(--al-sys-color-text-on-accent);
	--al-datepicker-day-selected-weight: var(--al-sys-font-weight-control);
	/* today is a dot under the number, not a border, so it survives being chosen as
	   well — a ring plus a fill is two marks fighting inside 36px */
	--al-datepicker-day-today-color: var(--al-sys-color-accent-primary-line);
	--al-datepicker-day-today-size: 4px;
	--al-datepicker-day-today-radius: var(--al-sys-radius-pill);
	--al-datepicker-day-disabled-opacity: var(--al-sys-state-disabled-opacity);
	/* THE RANGE LADDER — the one composition alias this component gets. The base rule
	   builds border-radius out of the two rungs, so the middle of a range squares off
	   and the ends keep their corner by RE-POINTING A TOKEN rather than by a variant
	   overriding border-radius. */
	--al-datepicker-day-radius: var(--al-sys-control-radius);
	--al-datepicker-day-radius-start: var(--al-datepicker-day-radius);
	--al-datepicker-day-radius-end: var(--al-datepicker-day-radius);
	--al-datepicker-day-range-fill: var(--al-sys-color-row-tint-selected);

	--al-datepicker-footer-border-color: var(--al-sys-color-border);
	--al-datepicker-footer-padding: var(--al-sys-space-100);
	--al-datepicker-footer-gap: var(--al-sys-space-100);

	box-sizing: border-box;
	position: relative; /* what makes the layer token real; placement is the library's */
	z-index: var(--al-datepicker-popup-layer);
	display: inline-block;
	padding: var(--al-datepicker-popup-padding);
	background: var(--al-datepicker-popup-fill);
	border: var(--al-datepicker-popup-border-width) solid var(--al-datepicker-popup-border-color);
	border-radius: var(--al-datepicker-popup-radius);
	box-shadow: var(--al-datepicker-popup-elevation);
	color: var(--al-datepicker-popup-text);
	line-height: var(--al-datepicker-line-height);
}
.al-datepick-cal-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-datepicker-head-gap);
	padding: var(--al-datepicker-head-padding) var(--al-datepicker-head-padding) var(--al-datepicker-head-gap);
}
.al-datepick-title {
	font-size: var(--al-datepicker-title-font-size);
	font-weight: var(--al-datepicker-title-weight);
}
.al-datepick-nav {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: var(--al-datepicker-nav-size);
	height: var(--al-datepicker-nav-size);
	border: 0;
	border-radius: var(--al-datepicker-nav-radius);
	background: transparent;
	cursor: pointer;
}
.al-datepick-nav::before {
	content: '';
	width: var(--al-datepicker-nav-icon-size);
	height: var(--al-datepicker-nav-icon-size);
	background: var(--al-datepicker-nav-color);
	-webkit-mask: var(--al-datepicker-nav-glyph) center / contain no-repeat;
	mask: var(--al-datepicker-nav-glyph) center / contain no-repeat;
}
.al-datepick-nav[data-dir='next']::before {
	transform: rotate(180deg);
}
.al-datepick-nav:hover {
	background: var(--al-datepicker-nav-hover-fill);
}

.al-datepick-week,
.al-datepick-days {
	display: grid;
	grid-template-columns: repeat(7, var(--al-datepicker-day-size));
}
.al-datepick-week > span {
	display: flex;
	align-items: center;
	justify-content: center;
	height: var(--al-datepicker-weekday-height);
	color: var(--al-datepicker-weekday-color);
	font-size: var(--al-datepicker-weekday-font-size);
	font-weight: var(--al-datepicker-weekday-weight);
}

/* A DAY. Days from the neighbouring month are left BLANK rather than drawn quietly:
   the two quiet ink roles are the same value on a dark ground, so "quieter than a
   normal day" is a thing this system cannot say there — and a dim day you can click
   moves you to another month without saying so. A blank cell says nothing and means
   nothing, which is correct. */
.al-datepick-day {
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 1px;
	width: var(--al-datepicker-day-size);
	height: var(--al-datepicker-day-size);
	border: 0;
	border-radius: var(--al-datepicker-day-radius-start) var(--al-datepicker-day-radius-end) var(--al-datepicker-day-radius-end)
		var(--al-datepicker-day-radius-start);
	background: var(--al-datepicker-day-fill);
	color: var(--al-datepicker-day-text);
	font-family: inherit;
	font-size: var(--al-datepicker-day-font-size);
	font-variant-numeric: tabular-nums;
	line-height: var(--al-datepicker-line-height);
	cursor: pointer;
}
.al-datepick-day::after {
	content: '';
	box-sizing: border-box;
	width: var(--al-datepicker-day-today-size);
	height: var(--al-datepicker-day-today-size);
	border-radius: var(--al-datepicker-day-today-radius);
	background: transparent;
}
.al-datepick-day[data-today='true']::after {
	background: var(--al-datepicker-day-today-color);
}
.al-datepick-day:hover:not([aria-disabled='true']) {
	--al-datepicker-day-fill: var(--al-datepicker-day-hover-fill);
}
.al-datepick-day[data-range='true'] {
	--al-datepicker-day-radius-start: 0;
	--al-datepicker-day-radius-end: 0;
	--al-datepicker-day-fill: var(--al-datepicker-day-range-fill);
}
.al-datepick-day[data-range-start='true'] {
	--al-datepicker-day-radius-start: var(--al-datepicker-day-radius);
}
.al-datepick-day[data-range-end='true'] {
	--al-datepicker-day-radius-end: var(--al-datepicker-day-radius);
}
.al-datepick-day[aria-selected='true'] {
	--al-datepicker-day-fill: var(--al-datepicker-day-selected-fill);
	/* HOVER HOLDS THE FILL on the chosen day rather than trading it for the row tint.
	   The chosen day is already the loudest mark in the panel and the pointer is on it,
	   so there is nothing hover needs to add — and every rung that would deepen the
	   accent instead is a LIGHTER colour on a dark ground, which would put a white
	   number on a pale green cell there. Holding is correct on both grounds. */
	--al-datepicker-day-hover-fill: var(--al-datepicker-day-selected-fill);
	color: var(--al-datepicker-day-selected-text);
	font-weight: var(--al-datepicker-day-selected-weight);
	/* today's dot flips to the ink that reads on the accent, so the two marks can be
	   true at once — a re-point, not a second rule about dots */
	--al-datepicker-day-today-color: var(--al-sys-color-text-on-accent);
}
.al-datepick-day[aria-disabled='true'] {
	opacity: var(--al-datepicker-day-disabled-opacity);
	cursor: not-allowed;
}
.al-datepick-day-blank {
	width: var(--al-datepicker-day-size);
	height: var(--al-datepicker-day-size);
}
.al-datepick-cal-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-datepicker-footer-gap);
	margin-top: var(--al-datepicker-footer-padding);
	padding-top: var(--al-datepicker-footer-padding);
	border-top: var(--al-datepicker-popup-border-width) solid var(--al-datepicker-footer-border-color);
}

/* =============================================================================
   THE TIME PICKER — .al-timepick
   Kendo TimePicker today.

   The same field, a different popup: three columns of a list rather than a grid.
   ============================================================================= */

.al-timepick {
	--al-timepicker-height: var(--al-sys-control-height-md);
	--al-timepicker-padding-x: var(--al-sys-control-padding-x-md);
	--al-timepicker-font-size: var(--al-sys-font-size-300);
	--al-timepicker-line-height: var(--al-sys-line-height-control);
	--al-timepicker-font-weight: var(--al-sys-font-weight-body);
	--al-timepicker-gap: var(--al-sys-control-gap);
	--al-timepicker-radius: var(--al-sys-control-radius);
	--al-timepicker-border-width: var(--al-sys-control-border-width);
	--al-timepicker-border-color: var(--al-sys-color-border-control);
	--al-timepicker-fill: var(--al-sys-color-surface-control);
	--al-timepicker-text: var(--al-sys-color-text-primary);
	--al-timepicker-placeholder: var(--al-sys-color-text-placeholder);
	--al-timepicker-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-timepicker-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-timepicker-trigger-color: var(--al-sys-color-text-muted);
	--al-timepicker-trigger-size: var(--al-sys-icon-size-md);
	--al-timepicker-trigger-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='none' stroke='%23000' stroke-width='1.5'/%3E%3Cpath d='M8 4.4V8.2l2.6 1.6' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

	--al-timepicker-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-timepicker-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-timepicker-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-timepicker-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-timepicker-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-timepicker-gap);
	width: 100%;
	flex: none;
	min-height: var(--al-timepicker-height);
	padding: 0 var(--al-timepicker-padding-x);
	border: var(--al-timepicker-border-width) solid var(--al-timepicker-border-color);
	border-radius: var(--al-timepicker-radius);
	background: var(--al-timepicker-fill);
	color: var(--al-timepicker-text);
	font-size: var(--al-timepicker-font-size);
	font-weight: var(--al-timepicker-font-weight);
	line-height: var(--al-timepicker-line-height);
	cursor: pointer;
}
.al-timepick > input {
	box-sizing: border-box;
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
	border: 0;
	padding: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: inherit;
	cursor: inherit;
}
.al-timepick > input:focus {
	outline: none;
}
.al-timepick > input::placeholder {
	color: var(--al-timepicker-placeholder);
}
.al-timepick-trigger {
	flex: none;
	width: var(--al-timepicker-trigger-size);
	height: var(--al-timepicker-trigger-size);
	background: var(--al-timepicker-trigger-color);
	-webkit-mask: var(--al-timepicker-trigger-glyph) center / contain no-repeat;
	mask: var(--al-timepicker-trigger-glyph) center / contain no-repeat;
}
.al-timepick:hover:not([aria-disabled='true']):not([data-readonly='true']) {
	--al-timepicker-border-color: var(--al-timepicker-hover-border-color);
}
.al-timepick:focus-within,
.al-timepick[data-focus='true'] {
	outline: var(--al-timepicker-focus-ring-width) solid transparent;
	outline-offset: var(--al-timepicker-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-timepicker-focus-hairline-width) var(--al-timepicker-focus-hairline-light),
		0 0 0 calc(var(--al-timepicker-focus-hairline-width) + var(--al-timepicker-focus-ring-width)) var(--al-timepicker-focus-ring-color),
		0 0 0 calc(var(--al-timepicker-focus-hairline-width) * 2 + var(--al-timepicker-focus-ring-width)) var(--al-timepicker-focus-hairline-dark);
}
.al-timepick:not([aria-invalid='true']):focus-within,
.al-timepick:not([aria-invalid='true'])[data-focus='true'] {
	--al-timepicker-border-color: var(--al-sys-color-focus-ring);
}
.al-timepick[aria-invalid='true'] {
	--al-timepicker-border-color: var(--al-sys-color-status-error);
}
.al-timepick[aria-invalid='true']:focus-within,
.al-timepick[aria-invalid='true'][data-focus='true'] {
	--al-timepicker-focus-ring-color: var(--al-sys-color-status-error);
}
.al-timepick[aria-disabled='true'] {
	opacity: var(--al-timepicker-disabled-opacity);
	pointer-events: none;
}
.al-timepick[data-readonly='true'] {
	--al-timepicker-border-color: transparent;
	--al-timepicker-fill: var(--al-sys-color-surface-sunken);
	cursor: default;
}
.al-timepick[data-readonly='true'] .al-timepick-trigger {
	display: none;
}
.al-timepick-sm {
	--al-timepicker-height: var(--al-sys-control-height-sm);
	--al-timepicker-padding-x: var(--al-sys-control-padding-x-sm);
	--al-timepicker-font-size: var(--al-sys-font-size-200);
}
.al-timepick-lg {
	--al-timepicker-height: var(--al-sys-control-height-lg);
	--al-timepicker-padding-x: var(--al-sys-control-padding-x-lg);
	--al-timepicker-font-size: var(--al-sys-font-size-400);
}

/* -----------------------------------------------------------------------------
   THE TIME PANEL. The chosen time is a TINT plus weight, not the accent fill the
   calendar's chosen day gets — in a column of sixty rows an accent block is a bar of
   colour, and the row is already the width of the column, so it does not need finding
   the way one cell in thirty-five does.
   ----------------------------------------------------------------------------- */
.al-timepick-panel {
	--al-timepicker-popup-fill: var(--al-sys-color-surface-raised);
	--al-timepicker-popup-border-color: var(--al-sys-color-border);
	--al-timepicker-popup-border-width: var(--al-sys-control-border-width);
	--al-timepicker-popup-radius: var(--al-sys-control-radius);
	--al-timepicker-popup-padding: var(--al-sys-space-100);
	--al-timepicker-popup-elevation: var(--al-sys-elevation-raised);
	--al-timepicker-popup-layer: var(--al-sys-layer-popover);
	--al-timepicker-line-height: var(--al-sys-line-height-control);

	--al-timepicker-column-width: 64px; /* four glyphs and air. No sys rung fits: this
	     is the width of "12" at the control size, not a control height. */
	--al-timepicker-column-divider-color: var(--al-sys-color-border);
	--al-timepicker-column-padding-x: var(--al-sys-space-50);
	--al-timepicker-column-label-color: var(--al-sys-color-text-muted);
	--al-timepicker-column-label-font-size: var(--al-sys-font-size-100);
	--al-timepicker-column-label-weight: var(--al-sys-font-weight-medium);
	--al-timepicker-column-label-padding-y: var(--al-sys-space-50);
	--al-timepicker-column-label-padding-x: var(--al-sys-space-100);
	--al-timepicker-list-max-height: 176px; /* four rows and part of a fifth, so the
	     list plainly has more in it than fits */

	--al-timepicker-item-height: var(--al-sys-row-height-sm);
	--al-timepicker-item-font-size: var(--al-sys-font-size-300);
	--al-timepicker-item-text: var(--al-sys-color-text-secondary);
	--al-timepicker-item-radius: var(--al-sys-radius-sm);
	--al-timepicker-item-hover-fill: var(--al-sys-color-row-tint-active);
	--al-timepicker-item-selected-fill: var(--al-sys-color-row-tint-selected);
	--al-timepicker-item-selected-text: var(--al-sys-color-text-primary);
	--al-timepicker-item-selected-weight: var(--al-sys-font-weight-control);
	--al-timepicker-item-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-timepicker-footer-border-color: var(--al-sys-color-border);
	--al-timepicker-footer-gap: var(--al-sys-space-100);

	box-sizing: border-box;
	position: relative;
	z-index: var(--al-timepicker-popup-layer);
	display: inline-block;
	padding: var(--al-timepicker-popup-padding);
	background: var(--al-timepicker-popup-fill);
	border: var(--al-timepicker-popup-border-width) solid var(--al-timepicker-popup-border-color);
	border-radius: var(--al-timepicker-popup-radius);
	box-shadow: var(--al-timepicker-popup-elevation);
	line-height: var(--al-timepicker-line-height);
}
.al-timepick-cols {
	display: flex;
}
.al-timepick-col {
	box-sizing: border-box;
	width: var(--al-timepicker-column-width);
	border-right: var(--al-timepicker-popup-border-width) solid var(--al-timepicker-column-divider-color);
	padding: 0 var(--al-timepicker-column-padding-x);
}
.al-timepick-col:last-child {
	border-right: 0;
}
.al-timepick-col > h6 {
	margin: 0;
	padding: var(--al-timepicker-column-label-padding-y) var(--al-timepicker-column-label-padding-x) var(--al-timepicker-column-label-padding-x);
	text-align: center;
	color: var(--al-timepicker-column-label-color);
	font-size: var(--al-timepicker-column-label-font-size);
	font-weight: var(--al-timepicker-column-label-weight);
	letter-spacing: 0.06em;
	text-transform: uppercase;
}
.al-timepick-list {
	max-height: var(--al-timepicker-list-max-height);
	overflow-y: auto;
	overscroll-behavior: contain;
}
.al-timepick-item {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	min-height: var(--al-timepicker-item-height);
	border: 0;
	border-radius: var(--al-timepicker-item-radius);
	background: transparent;
	color: var(--al-timepicker-item-text);
	font-family: inherit;
	font-size: var(--al-timepicker-item-font-size);
	font-variant-numeric: tabular-nums;
	line-height: var(--al-timepicker-line-height);
	cursor: pointer;
}
.al-timepick-item:hover:not([aria-disabled='true']) {
	background: var(--al-timepicker-item-hover-fill);
}
.al-timepick-item[aria-selected='true'] {
	background: var(--al-timepicker-item-selected-fill);
	color: var(--al-timepicker-item-selected-text);
	font-weight: var(--al-timepicker-item-selected-weight);
}
.al-timepick-item[aria-disabled='true'] {
	opacity: var(--al-timepicker-item-disabled-opacity);
	cursor: not-allowed;
}
.al-timepick-foot {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: var(--al-timepicker-footer-gap);
	margin-top: var(--al-timepicker-footer-gap);
	padding-top: var(--al-timepicker-footer-gap);
	border-top: var(--al-timepicker-popup-border-width) solid var(--al-timepicker-footer-border-color);
}

/* =============================================================================
   THE COMBOBOX — .al-combo
   Kendo ComboBox and DropDownList today.

   One value out of a list too long, too remote or too searchable for a <select>. The
   recipe's own select covers the short, known, local case and should keep it.
   ============================================================================= */

.al-combo {
	--al-combobox-height: var(--al-sys-control-height-md);
	--al-combobox-padding-x: var(--al-sys-control-padding-x-md);
	--al-combobox-font-size: var(--al-sys-font-size-300);
	--al-combobox-line-height: var(--al-sys-line-height-control);
	--al-combobox-font-weight: var(--al-sys-font-weight-body);
	--al-combobox-gap: var(--al-sys-control-gap);
	--al-combobox-radius: var(--al-sys-control-radius);
	--al-combobox-border-width: var(--al-sys-control-border-width);
	--al-combobox-border-color: var(--al-sys-color-border-control);
	--al-combobox-fill: var(--al-sys-color-surface-control);
	--al-combobox-text: var(--al-sys-color-text-primary);
	--al-combobox-placeholder: var(--al-sys-color-text-placeholder);
	--al-combobox-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-combobox-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-combobox-caret-color: var(--al-sys-color-text-muted);
	--al-combobox-caret-size: var(--al-sys-icon-size-sm);
	--al-combobox-caret-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	/* clearing a chosen value is not destruction, so the x strengthens on hover rather
	   than turning red. A filter bar full of red x's is alarm nobody reads. */
	--al-combobox-clear-color: var(--al-sys-color-text-muted);
	--al-combobox-clear-hover-color: var(--al-sys-color-text-primary);
	--al-combobox-clear-size: var(--al-sys-icon-size-sm);
	--al-combobox-clear-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' stroke='%23000' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
	/* a remote list is fetching. The ring replaces the caret in place, so the field
	   does not change width while it thinks. */
	--al-combobox-spinner-color: var(--al-sys-color-text-muted);
	--al-combobox-spinner-ring-width: var(--al-sys-spinner-ring-width);
	--al-combobox-spinner-duration: var(--al-sys-motion-duration-loop);
	--al-combobox-spinner-radius: var(--al-sys-radius-pill);

	--al-combobox-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-combobox-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-combobox-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-combobox-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-combobox-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-combobox-gap);
	width: 100%;
	flex: none;
	min-height: var(--al-combobox-height);
	padding: 0 var(--al-combobox-padding-x);
	border: var(--al-combobox-border-width) solid var(--al-combobox-border-color);
	border-radius: var(--al-combobox-radius);
	background: var(--al-combobox-fill);
	color: var(--al-combobox-text);
	font-size: var(--al-combobox-font-size);
	font-weight: var(--al-combobox-font-weight);
	line-height: var(--al-combobox-line-height);
	cursor: pointer;
}
.al-combo > input {
	box-sizing: border-box;
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
	border: 0;
	padding: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: inherit;
	text-overflow: ellipsis;
	cursor: inherit;
}
.al-combo > input:focus {
	outline: none;
}
.al-combo > input::placeholder {
	color: var(--al-combobox-placeholder);
}
.al-combo-caret {
	flex: none;
	width: var(--al-combobox-caret-size);
	height: var(--al-combobox-caret-size);
	background: var(--al-combobox-caret-color);
	-webkit-mask: var(--al-combobox-caret-glyph) center / contain no-repeat;
	mask: var(--al-combobox-caret-glyph) center / contain no-repeat;
}
.al-combo[data-open='true'] .al-combo-caret {
	transform: rotate(180deg);
}
.al-combo-clear {
	flex: none;
	width: var(--al-combobox-clear-size);
	height: var(--al-combobox-clear-size);
	background: var(--al-combobox-clear-color);
	-webkit-mask: var(--al-combobox-clear-glyph) center / contain no-repeat;
	mask: var(--al-combobox-clear-glyph) center / contain no-repeat;
	cursor: pointer;
}
.al-combo-clear:hover {
	background: var(--al-combobox-clear-hover-color);
}
/* the busy ring. box-sizing stated because it is not inherited and a 2px ring on a
   content-box would render the mark 4px wider than the caret it replaced. */
.al-combo-busy {
	box-sizing: border-box;
	flex: none;
	width: var(--al-combobox-caret-size);
	height: var(--al-combobox-caret-size);
	border: var(--al-combobox-spinner-ring-width) solid var(--al-combobox-spinner-color);
	border-right-color: transparent;
	border-radius: var(--al-combobox-spinner-radius);
	animation: al-spin var(--al-combobox-spinner-duration) linear infinite;
}

.al-combo:hover:not([aria-disabled='true']) {
	--al-combobox-border-color: var(--al-combobox-hover-border-color);
}
.al-combo:focus-within,
.al-combo[data-focus='true'] {
	outline: var(--al-combobox-focus-ring-width) solid transparent;
	outline-offset: var(--al-combobox-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-combobox-focus-hairline-width) var(--al-combobox-focus-hairline-light),
		0 0 0 calc(var(--al-combobox-focus-hairline-width) + var(--al-combobox-focus-ring-width)) var(--al-combobox-focus-ring-color),
		0 0 0 calc(var(--al-combobox-focus-hairline-width) * 2 + var(--al-combobox-focus-ring-width)) var(--al-combobox-focus-hairline-dark);
}
.al-combo:not([aria-invalid='true']):focus-within,
.al-combo:not([aria-invalid='true'])[data-focus='true'] {
	--al-combobox-border-color: var(--al-sys-color-focus-ring);
}
.al-combo[aria-invalid='true'] {
	--al-combobox-border-color: var(--al-sys-color-status-error);
}
.al-combo[aria-invalid='true']:focus-within,
.al-combo[aria-invalid='true'][data-focus='true'] {
	--al-combobox-focus-ring-color: var(--al-sys-color-status-error);
}
.al-combo[aria-disabled='true'] {
	opacity: var(--al-combobox-disabled-opacity);
	pointer-events: none;
}
.al-combo-sm {
	--al-combobox-height: var(--al-sys-control-height-sm);
	--al-combobox-padding-x: var(--al-sys-control-padding-x-sm);
	--al-combobox-font-size: var(--al-sys-font-size-200);
}
.al-combo-lg {
	--al-combobox-height: var(--al-sys-control-height-lg);
	--al-combobox-padding-x: var(--al-sys-control-padding-x-lg);
	--al-combobox-font-size: var(--al-sys-font-size-400);
}

/* -----------------------------------------------------------------------------
   THE LIST. The ground says where you are, the check says what is chosen — two facts,
   two channels. Filling the chosen row with the accent instead would swallow any row
   carrying its own colour and would then be competing with hover for the same
   background. That is the select's decision, kept, because they are the same list.
   ----------------------------------------------------------------------------- */
.al-combo-list {
	--al-combobox-popup-fill: var(--al-sys-color-surface-raised);
	--al-combobox-popup-border-color: var(--al-sys-color-border);
	--al-combobox-popup-border-width: var(--al-sys-control-border-width);
	--al-combobox-popup-radius: var(--al-sys-control-radius);
	--al-combobox-popup-padding: var(--al-sys-space-50);
	--al-combobox-line-height: var(--al-sys-line-height-control);
	--al-combobox-popup-elevation: var(--al-sys-elevation-raised);
	--al-combobox-popup-layer: var(--al-sys-layer-popover);
	--al-combobox-popup-max-height: 264px; /* the select's figure, deliberately: two
	     lists that open the same way should not stop at different heights */

	--al-combobox-option-height: var(--al-sys-row-height-sm);
	--al-combobox-option-padding-x: var(--al-sys-space-100);
	--al-combobox-option-font-size: var(--al-sys-font-size-300);
	--al-combobox-option-gap: var(--al-sys-control-gap);
	--al-combobox-option-text: var(--al-sys-color-text-primary);
	/* SECONDARY, not muted. The meta sits on a row that may be tinted, and the muted
	   rung measured 4.03 against the active tint at 12px — under the 4.5 floor. The
	   quiet rung that is safe on a page is not automatically safe on a tinted row. */
	--al-combobox-option-meta-color: var(--al-sys-color-text-secondary);
	--al-combobox-option-meta-font-size: var(--al-sys-font-size-100);
	--al-combobox-option-radius: var(--al-sys-radius-sm);
	--al-combobox-option-hover-fill: var(--al-sys-color-row-tint-active);
	--al-combobox-option-selected-fill: var(--al-sys-color-row-tint-selected);
	--al-combobox-option-selected-weight: var(--al-sys-font-weight-control);
	--al-combobox-option-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-combobox-check-color: var(--al-sys-color-accent-primary-line);
	--al-combobox-check-size: var(--al-sys-icon-size-md);
	--al-combobox-check-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.2 4.3 6.5 11 2.8 7.3' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
	--al-combobox-group-color: var(--al-sys-color-text-muted);
	--al-combobox-group-font-size: var(--al-sys-font-size-100);
	--al-combobox-group-weight: var(--al-sys-font-weight-control);
	--al-combobox-group-padding-top: var(--al-sys-space-150);
	--al-combobox-group-padding-bottom: var(--al-sys-space-50);
	--al-combobox-empty-color: var(--al-sys-color-text-muted);
	--al-combobox-empty-padding-y: var(--al-sys-space-300);

	box-sizing: border-box;
	position: relative;
	z-index: var(--al-combobox-popup-layer);
	display: block;
	max-height: var(--al-combobox-popup-max-height);
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: var(--al-combobox-popup-padding);
	background: var(--al-combobox-popup-fill);
	border: var(--al-combobox-popup-border-width) solid var(--al-combobox-popup-border-color);
	border-radius: var(--al-combobox-popup-radius);
	box-shadow: var(--al-combobox-popup-elevation);
	line-height: var(--al-combobox-line-height);
}
.al-combo-opt {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-combobox-option-gap);
	min-height: var(--al-combobox-option-height);
	padding: 0 var(--al-combobox-option-padding-x);
	border-radius: var(--al-combobox-option-radius);
	color: var(--al-combobox-option-text);
	font-size: var(--al-combobox-option-font-size);
	line-height: var(--al-combobox-line-height);
	cursor: pointer;
}
.al-combo-opt > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.al-combo-opt-meta {
	flex: none;
	margin-left: auto;
	color: var(--al-combobox-option-meta-color);
	font-size: var(--al-combobox-option-meta-font-size);
	font-variant-numeric: tabular-nums;
}
/* the check occupies its space whether or not it is showing, so the labels down the
   list stay on one left edge instead of stepping in and out */
.al-combo-check {
	flex: none;
	width: var(--al-combobox-check-size);
	height: var(--al-combobox-check-size);
	background: transparent;
	-webkit-mask: var(--al-combobox-check-glyph) center / contain no-repeat;
	mask: var(--al-combobox-check-glyph) center / contain no-repeat;
}
.al-combo-opt[aria-selected='true'] {
	background: var(--al-combobox-option-selected-fill);
	font-weight: var(--al-combobox-option-selected-weight);
}
.al-combo-opt[aria-selected='true'] .al-combo-check {
	background: var(--al-combobox-check-color);
}
.al-combo-opt:hover:not([aria-disabled='true']),
.al-combo-opt[data-active='true'] {
	background: var(--al-combobox-option-hover-fill);
}
.al-combo-opt[aria-disabled='true'] {
	opacity: var(--al-combobox-option-disabled-opacity);
	cursor: not-allowed;
}
.al-combo-group {
	padding: var(--al-combobox-group-padding-top) var(--al-combobox-option-padding-x) var(--al-combobox-group-padding-bottom);
	color: var(--al-combobox-group-color);
	font-size: var(--al-combobox-group-font-size);
	font-weight: var(--al-combobox-group-weight);
	letter-spacing: 0.06em;
	text-transform: uppercase;
}
.al-combo-empty {
	padding: var(--al-combobox-empty-padding-y) var(--al-combobox-option-padding-x);
	text-align: center;
	color: var(--al-combobox-empty-color);
	font-size: var(--al-combobox-option-font-size);
}

/* =============================================================================
   THE MULTI-SELECT — .al-multi
   Kendo MultiSelect today.

   The combobox that keeps its answers. The field grows down as chips wrap rather than
   scrolling sideways: a filter you cannot read is a filter you will set twice.
   ============================================================================= */

.al-multi {
	--al-multiselect-height: var(--al-sys-control-height-md);
	--al-multiselect-padding-x: var(--al-sys-space-100);
	--al-multiselect-padding-y: var(
		--al-sys-space-25
	); /* 2, not 4. The field is a
	     FLOOR like every other control, so the chip plus its air has to come in UNDER
	     the tier or the box stops being 36 and stops lining up with the input beside
	     it. Measured: at 4 the medium field rendered 38 and the large 46. */
	--al-multiselect-font-size: var(--al-sys-font-size-300);
	--al-multiselect-line-height: var(--al-sys-line-height-control);
	--al-multiselect-font-weight: var(--al-sys-font-weight-body);
	--al-multiselect-radius: var(--al-sys-control-radius);
	--al-multiselect-border-width: var(--al-sys-control-border-width);
	--al-multiselect-border-color: var(--al-sys-color-border-control);
	--al-multiselect-fill: var(--al-sys-color-surface-control);
	--al-multiselect-text: var(--al-sys-color-text-primary);
	--al-multiselect-placeholder: var(--al-sys-color-text-placeholder);
	--al-multiselect-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-multiselect-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-multiselect-gap: var(--al-sys-space-50);
	--al-multiselect-caret-color: var(--al-sys-color-text-muted);
	--al-multiselect-caret-size: var(--al-sys-icon-size-sm);
	--al-multiselect-caret-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

	/* THE CHIP. It reads the chosen-row tint, which is the same fact in a different
	   shape and follows the ground for free. Radius sm, not pill: a pill inside an
	   8px-cornered field reads as a badge floating in a box rather than as a value
	   sitting in one. */
	--al-multiselect-chip-height: var(--al-sys-control-height-sm);
	--al-multiselect-chip-fill: var(--al-sys-color-row-tint-selected);
	--al-multiselect-chip-text: var(--al-sys-color-text-primary);
	--al-multiselect-chip-font-size: var(--al-sys-font-size-200);
	--al-multiselect-chip-radius: var(--al-sys-radius-sm);
	--al-multiselect-chip-padding-x: var(--al-sys-space-100);
	--al-multiselect-chip-gap: var(--al-sys-space-50);
	--al-multiselect-chip-max-width: 180px; /* a long strain name truncates rather than
	     pushing every other chip onto its own line */
	--al-multiselect-chip-remove-color: var(--al-sys-color-text-muted);
	--al-multiselect-chip-remove-hover-color: var(--al-sys-color-text-primary);
	--al-multiselect-chip-remove-size: var(--al-sys-icon-size-sm);
	--al-multiselect-chip-remove-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' stroke='%23000' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");

	--al-multiselect-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-multiselect-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-multiselect-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-multiselect-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-multiselect-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--al-multiselect-gap);
	width: 100%;
	min-height: var(--al-multiselect-height);
	padding: var(--al-multiselect-padding-y) var(--al-multiselect-padding-x);
	border: var(--al-multiselect-border-width) solid var(--al-multiselect-border-color);
	border-radius: var(--al-multiselect-radius);
	background: var(--al-multiselect-fill);
	color: var(--al-multiselect-text);
	font-size: var(--al-multiselect-font-size);
	font-weight: var(--al-multiselect-font-weight);
	line-height: var(--al-multiselect-line-height);
	cursor: text;
}
.al-multi > input {
	box-sizing: border-box;
	flex: 1 1 60px;
	min-width: 0;
	border: 0;
	padding: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: inherit;
}
.al-multi > input:focus {
	outline: none;
}
.al-multi > input::placeholder {
	color: var(--al-multiselect-placeholder);
}
.al-multi-caret {
	flex: none;
	margin-left: auto;
	width: var(--al-multiselect-caret-size);
	height: var(--al-multiselect-caret-size);
	background: var(--al-multiselect-caret-color);
	-webkit-mask: var(--al-multiselect-caret-glyph) center / contain no-repeat;
	mask: var(--al-multiselect-caret-glyph) center / contain no-repeat;
	align-self: center;
}
.al-multi-chip {
	box-sizing: border-box;
	display: inline-flex;
	align-items: center;
	gap: var(--al-multiselect-chip-gap);
	flex: none;
	max-width: var(--al-multiselect-chip-max-width);
	min-height: var(--al-multiselect-chip-height);
	padding: 0 var(--al-multiselect-chip-padding-x);
	border-radius: var(--al-multiselect-chip-radius);
	background: var(--al-multiselect-chip-fill);
	color: var(--al-multiselect-chip-text);
	font-size: var(--al-multiselect-chip-font-size);
	line-height: var(--al-multiselect-line-height);
}
.al-multi-chip > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.al-multi-chip-x {
	flex: none;
	width: var(--al-multiselect-chip-remove-size);
	height: var(--al-multiselect-chip-remove-size);
	background: var(--al-multiselect-chip-remove-color);
	-webkit-mask: var(--al-multiselect-chip-remove-glyph) center / contain no-repeat;
	mask: var(--al-multiselect-chip-remove-glyph) center / contain no-repeat;
	cursor: pointer;
}
.al-multi-chip-x:hover {
	background: var(--al-multiselect-chip-remove-hover-color);
}
/* THE OVERFLOW CHIP re-points the chip's own tokens rather than being a second kind of
   chip: it is the same shape carrying a count instead of a value, and it must not read
   as one more chosen thing. */
.al-multi-chip-more {
	--al-multiselect-chip-fill: var(--al-sys-color-surface-sunken);
	--al-multiselect-chip-text: var(--al-sys-color-text-muted);
}

.al-multi:hover:not([aria-disabled='true']) {
	--al-multiselect-border-color: var(--al-multiselect-hover-border-color);
}
.al-multi:focus-within,
.al-multi[data-focus='true'] {
	outline: var(--al-multiselect-focus-ring-width) solid transparent;
	outline-offset: var(--al-multiselect-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-multiselect-focus-hairline-width) var(--al-multiselect-focus-hairline-light),
		0 0 0 calc(var(--al-multiselect-focus-hairline-width) + var(--al-multiselect-focus-ring-width)) var(--al-multiselect-focus-ring-color),
		0 0 0 calc(var(--al-multiselect-focus-hairline-width) * 2 + var(--al-multiselect-focus-ring-width)) var(--al-multiselect-focus-hairline-dark);
}
.al-multi:not([aria-invalid='true']):focus-within,
.al-multi:not([aria-invalid='true'])[data-focus='true'] {
	--al-multiselect-border-color: var(--al-sys-color-focus-ring);
}
.al-multi[aria-invalid='true'] {
	--al-multiselect-border-color: var(--al-sys-color-status-error);
}
.al-multi[aria-invalid='true']:focus-within,
.al-multi[aria-invalid='true'][data-focus='true'] {
	--al-multiselect-focus-ring-color: var(--al-sys-color-status-error);
}
.al-multi[aria-disabled='true'] {
	opacity: var(--al-multiselect-disabled-opacity);
	pointer-events: none;
}
.al-multi-sm {
	--al-multiselect-height: var(--al-sys-control-height-sm);
	--al-multiselect-font-size: var(--al-sys-font-size-200);
	--al-multiselect-chip-height: 22px; /* the tier below sm does not exist and must
	     not: a 28px chip inside a 28px field leaves no air at all */
	--al-multiselect-chip-font-size: var(--al-sys-font-size-100);
}
.al-multi-lg {
	--al-multiselect-height: var(--al-sys-control-height-lg);
	--al-multiselect-font-size: var(--al-sys-font-size-400);
	--al-multiselect-chip-height: var(--al-sys-control-height-md);
	--al-multiselect-chip-font-size: var(--al-sys-font-size-300);
}

/* THE LIST. Rows carry the recipe's own checkbox rather than a check mark on the
   right: a multi-select's row is a thing you toggle, and the box says so before you
   click it. It is .al-check, not a copy of it. */
.al-multi-list {
	--al-multiselect-popup-fill: var(--al-sys-color-surface-raised);
	--al-multiselect-popup-border-color: var(--al-sys-color-border);
	--al-multiselect-popup-border-width: var(--al-sys-control-border-width);
	--al-multiselect-popup-radius: var(--al-sys-control-radius);
	--al-multiselect-popup-padding: var(--al-sys-space-50);
	--al-multiselect-line-height: var(--al-sys-line-height-control);
	--al-multiselect-popup-elevation: var(--al-sys-elevation-raised);
	--al-multiselect-popup-layer: var(--al-sys-layer-popover);
	--al-multiselect-popup-max-height: 264px;
	--al-multiselect-option-height: var(--al-sys-row-height-sm);
	--al-multiselect-option-padding-x: var(--al-sys-space-100);
	--al-multiselect-option-radius: var(--al-sys-radius-sm);
	--al-multiselect-option-hover-fill: var(--al-sys-color-row-tint-active);
	--al-multiselect-option-selected-fill: var(--al-sys-color-row-tint-selected);
	--al-multiselect-option-disabled-opacity: var(--al-sys-state-disabled-opacity);
	--al-multiselect-toolbar-border-color: var(--al-sys-color-border);
	--al-multiselect-bar-gap: var(--al-sys-space-100);
	--al-multiselect-bar-padding: var(--al-sys-space-50);

	box-sizing: border-box;
	position: relative;
	z-index: var(--al-multiselect-popup-layer);
	display: block;
	max-height: var(--al-multiselect-popup-max-height);
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: var(--al-multiselect-popup-padding);
	background: var(--al-multiselect-popup-fill);
	border: var(--al-multiselect-popup-border-width) solid var(--al-multiselect-popup-border-color);
	border-radius: var(--al-multiselect-popup-radius);
	box-shadow: var(--al-multiselect-popup-elevation);
	line-height: var(--al-multiselect-line-height);
}
.al-multi-opt {
	box-sizing: border-box;
	display: flex;
	align-items: center;
	min-height: var(--al-multiselect-option-height);
	padding: 0 var(--al-multiselect-option-padding-x);
	line-height: var(--al-multiselect-line-height);
	border-radius: var(--al-multiselect-option-radius);
	cursor: pointer;
}
.al-multi-opt[aria-selected='true'] {
	background: var(--al-multiselect-option-selected-fill);
}
.al-multi-opt:hover:not([aria-disabled='true']),
.al-multi-opt[data-active='true'] {
	background: var(--al-multiselect-option-hover-fill);
}
.al-multi-opt[aria-disabled='true'] {
	opacity: var(--al-multiselect-option-disabled-opacity);
	cursor: not-allowed;
}
.al-multi-bar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-multiselect-bar-gap);
	margin-top: var(--al-multiselect-bar-padding);
	padding-top: var(--al-multiselect-bar-padding);
	border-top: var(--al-multiselect-popup-border-width) solid var(--al-multiselect-toolbar-border-color);
}

/* =============================================================================
   THE AUTOCOMPLETE — .al-autocomplete
   Kendo AutoComplete today.

   Not a combobox. A combobox has a list and you pick from it; an autocomplete has no
   list until you type, and the answer may be something the list never held. Customer
   lookup, package tag lookup, strain search.
   ============================================================================= */

.al-autocomplete {
	--al-autocomplete-height: var(--al-sys-control-height-md);
	--al-autocomplete-padding-x: var(--al-sys-control-padding-x-md);
	--al-autocomplete-font-size: var(--al-sys-font-size-300);
	--al-autocomplete-line-height: var(--al-sys-line-height-control);
	--al-autocomplete-font-weight: var(--al-sys-font-weight-body);
	--al-autocomplete-gap: var(--al-sys-control-gap);
	--al-autocomplete-radius: var(--al-sys-control-radius);
	--al-autocomplete-border-width: var(--al-sys-control-border-width);
	--al-autocomplete-border-color: var(--al-sys-color-border-control);
	--al-autocomplete-fill: var(--al-sys-color-surface-control);
	--al-autocomplete-text: var(--al-sys-color-text-primary);
	--al-autocomplete-placeholder: var(--al-sys-color-text-placeholder);
	--al-autocomplete-hover-border-color: var(--al-sys-color-accent-primary-line);
	--al-autocomplete-disabled-opacity: var(--al-sys-state-disabled-opacity);
	/* the magnifier leads rather than trails. It is the only thing saying this field
	   searches instead of storing what you type, and it has to be read before typing,
	   not after. */
	--al-autocomplete-search-color: var(--al-sys-color-text-muted);
	--al-autocomplete-search-size: var(--al-sys-icon-size-md);
	--al-autocomplete-search-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='7' cy='7' r='4.4' fill='none' stroke='%23000' stroke-width='1.7'/%3E%3Cpath d='M10.4 10.4L14 14' stroke='%23000' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
	--al-autocomplete-clear-color: var(--al-sys-color-text-muted);
	--al-autocomplete-clear-hover-color: var(--al-sys-color-text-primary);
	--al-autocomplete-clear-size: var(--al-sys-icon-size-sm);
	--al-autocomplete-clear-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' stroke='%23000' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
	--al-autocomplete-spinner-color: var(--al-sys-color-text-muted);
	--al-autocomplete-spinner-ring-width: var(--al-sys-spinner-ring-width);
	--al-autocomplete-spinner-duration: var(--al-sys-motion-duration-loop);
	--al-autocomplete-spinner-radius: var(--al-sys-radius-pill);

	--al-autocomplete-focus-hairline-width: var(--al-sys-focus-hairline-width);
	--al-autocomplete-focus-hairline-light: var(--al-sys-color-focus-hairline-light);
	--al-autocomplete-focus-hairline-dark: var(--al-sys-color-focus-hairline-dark);
	--al-autocomplete-focus-ring-width: var(--al-sys-focus-ring-width);
	--al-autocomplete-focus-ring-color: var(--al-sys-color-focus-ring);

	box-sizing: border-box;
	display: flex;
	align-items: center;
	gap: var(--al-autocomplete-gap);
	width: 100%;
	flex: none;
	min-height: var(--al-autocomplete-height);
	padding: 0 var(--al-autocomplete-padding-x);
	border: var(--al-autocomplete-border-width) solid var(--al-autocomplete-border-color);
	border-radius: var(--al-autocomplete-radius);
	background: var(--al-autocomplete-fill);
	color: var(--al-autocomplete-text);
	font-size: var(--al-autocomplete-font-size);
	font-weight: var(--al-autocomplete-font-weight);
	line-height: var(--al-autocomplete-line-height);
}
.al-autocomplete > input {
	box-sizing: border-box;
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
	border: 0;
	padding: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: inherit;
	text-overflow: ellipsis;
}
.al-autocomplete > input:focus {
	outline: none;
}
.al-autocomplete > input::placeholder {
	color: var(--al-autocomplete-placeholder);
}
.al-autocomplete-search {
	flex: none;
	width: var(--al-autocomplete-search-size);
	height: var(--al-autocomplete-search-size);
	background: var(--al-autocomplete-search-color);
	-webkit-mask: var(--al-autocomplete-search-glyph) center / contain no-repeat;
	mask: var(--al-autocomplete-search-glyph) center / contain no-repeat;
}
.al-autocomplete-clear {
	flex: none;
	width: var(--al-autocomplete-clear-size);
	height: var(--al-autocomplete-clear-size);
	background: var(--al-autocomplete-clear-color);
	-webkit-mask: var(--al-autocomplete-clear-glyph) center / contain no-repeat;
	mask: var(--al-autocomplete-clear-glyph) center / contain no-repeat;
	cursor: pointer;
}
.al-autocomplete-clear:hover {
	background: var(--al-autocomplete-clear-hover-color);
}
.al-autocomplete-busy {
	box-sizing: border-box;
	flex: none;
	width: var(--al-autocomplete-clear-size);
	height: var(--al-autocomplete-clear-size);
	border: var(--al-autocomplete-spinner-ring-width) solid var(--al-autocomplete-spinner-color);
	border-right-color: transparent;
	border-radius: var(--al-autocomplete-spinner-radius);
	animation: al-spin var(--al-autocomplete-spinner-duration) linear infinite;
}
.al-autocomplete:hover:not([aria-disabled='true']) {
	--al-autocomplete-border-color: var(--al-autocomplete-hover-border-color);
}
.al-autocomplete:focus-within,
.al-autocomplete[data-focus='true'] {
	outline: var(--al-autocomplete-focus-ring-width) solid transparent;
	outline-offset: var(--al-autocomplete-focus-hairline-width);
	box-shadow:
		0 0 0 var(--al-autocomplete-focus-hairline-width) var(--al-autocomplete-focus-hairline-light),
		0 0 0 calc(var(--al-autocomplete-focus-hairline-width) + var(--al-autocomplete-focus-ring-width)) var(--al-autocomplete-focus-ring-color),
		0 0 0 calc(var(--al-autocomplete-focus-hairline-width) * 2 + var(--al-autocomplete-focus-ring-width))
			var(--al-autocomplete-focus-hairline-dark);
}
.al-autocomplete:not([aria-invalid='true']):focus-within,
.al-autocomplete:not([aria-invalid='true'])[data-focus='true'] {
	--al-autocomplete-border-color: var(--al-sys-color-focus-ring);
}
.al-autocomplete[aria-invalid='true'] {
	--al-autocomplete-border-color: var(--al-sys-color-status-error);
}
.al-autocomplete[aria-invalid='true']:focus-within,
.al-autocomplete[aria-invalid='true'][data-focus='true'] {
	--al-autocomplete-focus-ring-color: var(--al-sys-color-status-error);
}
.al-autocomplete[aria-disabled='true'] {
	opacity: var(--al-autocomplete-disabled-opacity);
	pointer-events: none;
}
.al-autocomplete-sm {
	--al-autocomplete-height: var(--al-sys-control-height-sm);
	--al-autocomplete-padding-x: var(--al-sys-control-padding-x-sm);
	--al-autocomplete-font-size: var(--al-sys-font-size-200);
}
.al-autocomplete-lg {
	--al-autocomplete-height: var(--al-sys-control-height-lg);
	--al-autocomplete-padding-x: var(--al-sys-control-padding-x-lg);
	--al-autocomplete-font-size: var(--al-sys-font-size-400);
}

/* -----------------------------------------------------------------------------
   THE SUGGESTIONS.

   THE MATCH IS MARKED BY WEIGHT, NOT BY A HIGHLIGHT FILL AND NOT BY COLOUR. A fill
   would be a third background in a list that already spends two on "where you are" and
   "what is chosen", and on the active row it would be a paler hole inside a darker tint.
   Colour fails for a related reason: the row tints are made OF the accent, so an accent
   mark on a tinted row is green on green, and it measured under the contrast floor at
   two rungs. Weight is the channel nothing else in this list is using.
   ----------------------------------------------------------------------------- */
.al-autocomplete-list {
	--al-autocomplete-popup-fill: var(--al-sys-color-surface-raised);
	--al-autocomplete-popup-border-color: var(--al-sys-color-border);
	--al-autocomplete-popup-border-width: var(--al-sys-control-border-width);
	--al-autocomplete-popup-radius: var(--al-sys-control-radius);
	--al-autocomplete-popup-padding: var(--al-sys-space-50);
	--al-autocomplete-line-height: var(--al-sys-line-height-control);
	--al-autocomplete-popup-elevation: var(--al-sys-elevation-raised);
	--al-autocomplete-popup-layer: var(--al-sys-layer-popover);
	--al-autocomplete-popup-max-height: 264px;

	--al-autocomplete-item-height: var(--al-sys-row-height-md);
	--al-autocomplete-item-padding-x: var(--al-sys-space-100);
	--al-autocomplete-item-padding-y: var(--al-sys-space-50);
	--al-autocomplete-item-radius: var(--al-sys-radius-sm);
	--al-autocomplete-item-title-color: var(--al-sys-color-text-primary);
	--al-autocomplete-item-title-font-size: var(--al-sys-font-size-300);
	/* secondary for the same reason as the combobox's meta: measured 3.15 against the
	   active row tint on the muted rung */
	--al-autocomplete-item-meta-color: var(--al-sys-color-text-secondary);
	--al-autocomplete-item-meta-font-size: var(--al-sys-font-size-100);
	--al-autocomplete-item-active-fill: var(--al-sys-color-row-tint-active);
	/* THE MARK IS WEIGHT, AND THE TITLE'S OWN INK. It wanted to be the accent, and the
	   accent is exactly what it cannot be: the row tints ARE the accent, so a green mark
	   on a green tint is green on green. Measured on the active row — 3.32 at the line
	   rung, 4.23 one rung deeper, both under the 4.5 floor, and deepening it further just
	   turns the mark black. Weight is the one channel the row tints do not already own,
	   and it is correct on every ground and under both tints. The colour stays a token so
	   a theme whose tints are not the accent can put colour back. */
	--al-autocomplete-match-color: var(--al-sys-color-text-primary);
	--al-autocomplete-match-weight: var(--al-sys-font-weight-heading);
	--al-autocomplete-empty-color: var(--al-sys-color-text-muted);
	--al-autocomplete-empty-padding-y: var(--al-sys-space-300);
	--al-autocomplete-foot-color: var(--al-sys-color-text-muted);
	--al-autocomplete-foot-font-size: var(--al-sys-font-size-100);
	--al-autocomplete-foot-border-color: var(--al-sys-color-border);
	--al-autocomplete-foot-padding-y: var(--al-sys-space-100);
	--al-autocomplete-foot-margin-top: var(--al-sys-space-50);

	box-sizing: border-box;
	position: relative;
	z-index: var(--al-autocomplete-popup-layer);
	display: block;
	max-height: var(--al-autocomplete-popup-max-height);
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: var(--al-autocomplete-popup-padding);
	background: var(--al-autocomplete-popup-fill);
	border: var(--al-autocomplete-popup-border-width) solid var(--al-autocomplete-popup-border-color);
	border-radius: var(--al-autocomplete-popup-radius);
	box-shadow: var(--al-autocomplete-popup-elevation);
	line-height: var(--al-autocomplete-line-height);
}
.al-autocomplete-item {
	box-sizing: border-box;
	display: block;
	min-height: var(--al-autocomplete-item-height);
	padding: var(--al-autocomplete-item-padding-y) var(--al-autocomplete-item-padding-x);
	line-height: var(--al-autocomplete-line-height);
	border-radius: var(--al-autocomplete-item-radius);
	cursor: pointer;
}
.al-autocomplete-item > span {
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--al-autocomplete-item-title-color);
	font-size: var(--al-autocomplete-item-title-font-size);
	line-height: 1.4;
}
.al-autocomplete-item > small {
	display: block;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--al-autocomplete-item-meta-color);
	font-size: var(--al-autocomplete-item-meta-font-size);
	line-height: 1.4;
}
.al-autocomplete-item:hover,
.al-autocomplete-item[data-active='true'] {
	background: var(--al-autocomplete-item-active-fill);
}
.al-autocomplete-match {
	color: var(--al-autocomplete-match-color);
	font-weight: var(--al-autocomplete-match-weight);
}
.al-autocomplete-empty {
	padding: var(--al-autocomplete-empty-padding-y) var(--al-autocomplete-item-padding-x);
	text-align: center;
	color: var(--al-autocomplete-empty-color);
	font-size: var(--al-autocomplete-item-title-font-size);
}
.al-autocomplete-foot {
	padding: var(--al-autocomplete-foot-padding-y) var(--al-autocomplete-item-padding-x);
	margin-top: var(--al-autocomplete-foot-margin-top);
	border-top: var(--al-autocomplete-popup-border-width) solid var(--al-autocomplete-foot-border-color);
	color: var(--al-autocomplete-foot-color);
	font-size: var(--al-autocomplete-foot-font-size);
}

/* =============================================================================
   THE FIELD FRAME CLAIMS A DIRECT <span> CHILD AS ITS LABEL — and every control in
   this file is a wrapper.

   .al-field > span:not(.al-affix) is the recipe's label rule. It sets display: block,
   the label's size, weight and colour, and a bottom gap, at (0,2,1) — which outranks a
   control's own (0,1,0) base rule. Drop one of these controls straight into a field
   frame and it goes from a flex row to a block: the trigger and the caret are inline
   elements with a width and a height that no longer apply, so they collapse to nothing
   and the control's value renders in the label's semibold. Rendered and confirmed, not
   reasoned about — the calendar mark was simply missing from every dated field.

   The recipe already met this once and answered it by naming the affix in the :not().
   A third-party wrapper it has never heard of gets no such exclusion, and Kendo emits
   exactly this shape: <span class="k-datepicker"> inside whatever the form gives it. So
   the answer here is the project's usual one — beat it by scoping under a real
   ancestor, never !important. These rules restore only what the label rule took, and
   every value they restore is the control's own token.

   :has(> input) is not decoration. Tying on specificity would leave the fix depending
   on which stylesheet loaded last, and the two files are separate; :has takes the
   weight of its argument, so this lands one step above the label rule and wins on
   merit. It is also exactly true of the thing being fixed: a wrapper with an input in
   it, which is what a third-party control is and what a label is not.
   ============================================================================= */
.al-field > span.al-datepick:has(> input) {
	display: flex;
	margin-bottom: 0;
	font-size: var(--al-datepicker-font-size);
	font-weight: var(--al-datepicker-font-weight);
	color: var(--al-datepicker-text);
}
.al-field > span.al-timepick:has(> input) {
	display: flex;
	margin-bottom: 0;
	font-size: var(--al-timepicker-font-size);
	font-weight: var(--al-timepicker-font-weight);
	color: var(--al-timepicker-text);
}
.al-field > span.al-combo:has(> input) {
	display: flex;
	margin-bottom: 0;
	font-size: var(--al-combobox-font-size);
	font-weight: var(--al-combobox-font-weight);
	color: var(--al-combobox-text);
}
.al-field > span.al-multi:has(> input) {
	display: flex;
	margin-bottom: 0;
	font-size: var(--al-multiselect-font-size);
	font-weight: var(--al-multiselect-font-weight);
	color: var(--al-multiselect-text);
}
.al-field > span.al-autocomplete:has(> input) {
	display: flex;
	margin-bottom: 0;
	font-size: var(--al-autocomplete-font-size);
	font-weight: var(--al-autocomplete-font-weight);
	color: var(--al-autocomplete-text);
}

/* =============================================================================
   REDUCED MOTION

   The two spinners stay, because they are the only thing telling someone a remote
   list is still coming — take them away and a slow lookup looks like a dead field.
   They step instead of sweeping, which still reads as progress. The caret's turn and
   every hover change are polish and go.

   `al-spin` is the recipe's own keyframes, reused rather than redeclared: a second
   @keyframes of the same name silently replaces the first.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
	.al-combo-busy,
	.al-autocomplete-busy {
		animation-timing-function: steps(8, end);
	}
	.al-combo[data-open='true'] .al-combo-caret {
		transition: none;
	}
}

/* =============================================================================
   SAVE BAR
   A fixed bar at the foot of the content column: a status on the left saying the
   record is dirty, the actions that resolve it on the right. The host gates it —
   it is absent until there is something to save, and absence is the resting state.

   It reports, it does not judge. No validation, no counting, no disabling Save:
   the bar's job is to say "there are unsaved changes" and offer the two ways out.

   IT DOES NOT KNOW ABOUT A SIDEBAR. A bar pinned to the viewport would underlap a
   rail, so it takes an inset instead and lets the shell say how wide its own
   furniture is. The recipe cannot know that number and must not import it.
   ========================================================================== */
.al-savebar {
	--al-savebar-inset-start: 0; /* the shell sets this; 0 spans the viewport */
	--al-savebar-fill: var(--al-sys-color-surface);
	--al-savebar-border-color: var(--al-sys-color-border);
	--al-savebar-border-width: var(--al-sys-control-border-width);
	--al-savebar-layer: var(--al-sys-layer-sticky);
	--al-savebar-padding-x: var(--al-sys-space-300);
	--al-savebar-padding-y: 13px; /* measured off the shipped bar, not picked */
	--al-savebar-gap: var(--al-sys-space-200);
	/* the sticky rung, cast upward: this is the one bar whose content is above it */
	--al-savebar-shadow:
		0 -4px 16px rgb(from var(--al-sys-color-shadow-ink) r g b / 0.08);

	position: fixed;
	left: var(--al-savebar-inset-start);
	right: 0;
	bottom: 0;
	margin: 0; /* zero the reboot <footer> margin so it sits flush to the viewport */
	z-index: var(--al-savebar-layer);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-savebar-gap);
	padding: var(--al-savebar-padding-y) var(--al-savebar-padding-x);
	background: var(--al-savebar-fill);
	border-top: var(--al-savebar-border-width) solid var(--al-savebar-border-color);
	box-shadow: var(--al-savebar-shadow);
}

.al-savebar-status {
	display: flex;
	align-items: center;
	gap: var(--al-sys-space-100);
}

/* AMBER, NOT EMERALD. Unsaved is a caution, and emerald is spoken for by the
   primary action sitting two inches to its right. A dot the same colour as the
   Save button would read as "saved". */
.al-savebar-dot {
	width: var(--al-sys-space-100);
	height: var(--al-sys-space-100);
	border-radius: 50%;
	background: var(--al-sys-color-status-warning);
	flex: none;
}

.al-savebar-label {
	font-size: var(--al-sys-font-size-200);
	font-weight: var(--al-sys-font-weight-control);
	color: var(--al-sys-color-text-primary);
}

.al-savebar-actions {
	display: flex;
	gap: 10px; /* measured; between the 8px and 12px rungs and deliberately so */
	flex: none;
}

/* THE BAR IS FIXED, so the last field of the form would sit under it. The host
   drops this in ahead of the bar and takes it away with it. */
.al-savebar-spacer {
	height: 64px;
	flex: none;
}

/* ARRIVING, SAVED, LEAVING. The bar rises into place when a form goes dirty and drops
   away faster than it came, on the same enter/exit tokens the toast and dialog use.
   A keyframe, not @starting-style: hosts insert the bar with ng-if or show it with
   display, a keyframe plays on both, and it works in the terminals' Chrome 101.
   Saved and leaving go on a copy of the bar that the app's save-bar script puts up
   after the host removes the real one, because a bar removed by ng-if can't animate out. */
.al-savebar {
	animation: al-savebar-in var(--al-sys-motion-duration-enter) var(--al-sys-motion-easing-enter) both;
}
.al-savebar-leaving {
	animation: al-savebar-out var(--al-sys-motion-duration-exit) var(--al-sys-motion-easing-exit) both;
	pointer-events: none;
}
@keyframes al-savebar-in {
	from {
		transform: translateY(100%);
		opacity: 0;
	}
}
@keyframes al-savebar-out {
	to {
		transform: translateY(100%);
		opacity: 0;
	}
}

/* SAVED. The amber dot turns success-green and the label says Saved, held for a beat
   before the bar leaves. The buttons go: the save already happened. */
.al-savebar-saved .al-savebar-dot {
	background: var(--al-sys-color-status-success);
}
.al-savebar-saved .al-savebar-label {
	color: var(--al-sys-color-status-success-text);
}
.al-savebar-saved .al-savebar-actions {
	visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
	.al-savebar,
	.al-savebar-leaving {
		animation-duration: 0.01ms;
	}
}

/* =============================================================================
   ALERT AND CONFIRM
   The two message dialogs. An alert states something and offers one way out; a
   confirm asks something and offers two, one of which is usually destructive.
   Both live inside .al-modal — they are not containers, they are the fixed
   anatomy a message dialog always has and a modal window deliberately does not
   prescribe, because a modal window holds a form whose shape is unknown.

   THEY ARE THE THIRD AND FOURTH CONTROLS ON A RAIL THAT ALREADY EXISTS. Toast
   and banner already read the status tones; these read the same ones, so a
   warning is one colour across every way the app can raise one.

   ERROR OR DANGER, AND WHY BOTH ARE RIGHT HERE. An alert reports a state that
   has already happened, which is an error. A confirm asks about an action that
   has not happened yet, which is a danger. Same tint, different word, because
   they are answering different questions. It is the same split that already
   separates al-banner-error from al-btn-danger.
   ========================================================================== */
.al-alert,
.al-confirm {
	--al-dialog-tone: var(--al-sys-color-status-info);
	--al-dialog-tone-tint: var(--al-sys-color-status-info-tint);
	--al-dialog-icon-size: 46px;
	--al-dialog-icon-gap: 14px;
	--al-dialog-title-size: var(--al-sys-font-size-300);
	--al-dialog-title-weight: var(--al-sys-font-weight-heading);
	--al-dialog-title-color: var(--al-sys-color-text-primary);
	--al-dialog-text-size: var(--al-sys-font-size-200);
	--al-dialog-text-color: var(--al-sys-color-text-secondary);
	--al-dialog-text-leading: 1.55;
	--al-dialog-actions-gap: var(--al-sys-space-150);
}

/* THE ROOT SETS TONE AND NOTHING ELSE, and that is not tidiness — it is what makes
   the control safe to adopt. Forty-six admin dialogs already wear `al-confirm` on a
   Bootstrap modal wrapper that is `position: fixed; width: 100%`. A `max-width` here
   capped every one of them at 400px. Width belongs to the modal (al-modal-sm), never
   to the message inside it, and a root that declares only custom properties cannot
   reach markup that does not consume them.

   The anatomy lives on the parts below, so an existing dialog gets the tone the day
   its parts are renamed and not one moment before. */
.al-alert-icon,
.al-alert-title,
.al-alert-text,
.al-alert-actions,
.al-confirm-icon,
.al-confirm-title,
.al-confirm-text,
.al-confirm-actions {
	text-align: center;
}

/* THE DISC IS THE ONLY PLACE THE TONE IS SEEN before the words are read, which is
   why it is a tinted field rather than a bare glyph. */
.al-alert-icon,
.al-confirm-icon {
	width: var(--al-dialog-icon-size);
	height: var(--al-dialog-icon-size);
	margin: 0 auto var(--al-dialog-icon-gap);
	border-radius: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: var(--al-dialog-tone-tint);
	color: var(--al-dialog-tone);
}

.al-alert-title,
.al-confirm-title {
	font-size: var(--al-dialog-title-size);
	font-weight: var(--al-dialog-title-weight);
	color: var(--al-dialog-title-color);
	margin-bottom: var(--al-sys-space-50);
}

.al-alert-text,
.al-confirm-text {
	font-size: var(--al-dialog-text-size);
	line-height: var(--al-dialog-text-leading);
	color: var(--al-dialog-text-color);
}
/* the one emphasis these ever carry: the number that makes the warning worth reading */
.al-alert-text b,
.al-confirm-text b {
	color: var(--al-sys-color-text-primary);
	font-weight: var(--al-sys-font-weight-control);
}

.al-alert-actions,
.al-confirm-actions {
	display: flex;
	gap: var(--al-dialog-actions-gap);
	justify-content: center;
	margin-top: var(--al-sys-space-300);
}
/* AN ALERT HAS ONE WAY OUT, so its single button fills the width it is given
   rather than floating in the middle of a wide dialog. */
.al-alert-actions > :only-child {
	min-width: 120px;
}

/* --- tone: a state that happened --------------------------------------------- */
.al-alert-info {
	--al-dialog-tone: var(--al-sys-color-status-info);
	--al-dialog-tone-tint: var(--al-sys-color-status-info-tint);
}
.al-alert-warning {
	--al-dialog-tone: var(--al-sys-color-status-warning);
	--al-dialog-tone-tint: var(--al-sys-color-status-warning-tint);
}
.al-alert-error {
	--al-dialog-tone: var(--al-sys-color-status-error);
	--al-dialog-tone-tint: var(--al-sys-color-status-error-tint);
}
/* SUCCESS IS THE FOURTH TONE, and it is here because toast and banner both carry it
   and an alert saying "Payment Complete" has nowhere else to go. Confirm has no
   success: you do not ask someone to approve a thing that already went well. */
.al-alert-success {
	--al-dialog-tone: var(--al-sys-color-status-success);
	--al-dialog-tone-tint: var(--al-sys-color-status-success-tint);
}

/* --- tone: an action about to happen ----------------------------------------- */
.al-confirm-info {
	--al-dialog-tone: var(--al-sys-color-status-info);
	--al-dialog-tone-tint: var(--al-sys-color-status-info-tint);
}
.al-confirm-warning {
	--al-dialog-tone: var(--al-sys-color-status-warning);
	--al-dialog-tone-tint: var(--al-sys-color-status-warning-tint);
}
.al-confirm-danger {
	--al-dialog-tone: var(--al-sys-color-status-error);
	--al-dialog-tone-tint: var(--al-sys-color-status-error-tint);
}

/* THE DESTRUCTIVE BUTTON TAKES THE DIALOG'S TONE, so a danger confirm does not
   need the caller to remember to pass al-btn-danger as well. A confirm whose
   tone is info or warning keeps the ordinary primary button, because only the
   danger tone describes something that cannot be undone.

   THIS RULE IS CORRECT AND CURRENTLY OUTRANKED, which is worth stating so nobody
   debugs it twice. `.al-confirm .modal-footer .al-confirm-action` in
   bootstrap-style-overrides.css is (0,3,0) against this rule's (0,2,0) and loads
   later, so it paints every confirm action emerald whatever the tone says.
   Measured on the delete-role dialog: --al-button-intent resolved to #c0463f
   while the button still rendered rgb(21,122,82). It takes effect when the modal
   rebuild retires that rule; until then a danger confirm must still say
   al-btn-danger out loud. Raising the specificity here would win the pixel and
   start an arms race with the layer this control is meant to replace. */
.al-confirm-danger .al-confirm-action {
	--al-button-intent: var(--al-sys-color-status-error);
	--al-button-intent-line: var(--al-sys-color-status-error);
}

/* =============================================================================
   RECORD HEADER
   The head of a record's own page: a way back, the record's name, and the actions
   that apply to the whole record. It is what sits above the cards on a detail
   screen, and it is not al-panel-header — a panel header lives inside a container
   and draws a rule under itself to separate its title from its body. This one has
   no body to separate from. The page is its container.

   THE WAY BACK IS al-back, WHICH ALREADY EXISTS. The app grew its own
   al-record-back doing the same job by hand, down to re-stating appearance and
   border because a host writes it as an <a> on some records and a <button> on
   others. al-back solves exactly that and is fully tokenised, so this control does
   not restate it.
   ========================================================================== */
.al-record-header {
	--al-record-gap: var(--al-sys-space-200);
	--al-record-actions-gap: 10px; /* measured; between the 8 and 12 rungs, deliberately */
	--al-record-titlebar-space: 14px; /* measured off the shipped header */
	--al-record-title-size: var(--al-sys-font-size-500);
	--al-record-title-weight: var(--al-sys-font-weight-heading);
	--al-record-title-color: var(--al-sys-color-text-primary);

	margin-top: var(--al-sys-space-300);
}

/* WRAPS ON PURPOSE. A record name is user data and can be long, and the actions
   must not be pushed off the edge by a customer with a long business name. */
.al-record-titlebar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--al-record-gap);
	flex-wrap: wrap;
	margin-bottom: var(--al-record-titlebar-space);
}

.al-record-title {
	font-size: var(--al-record-title-size);
	font-weight: var(--al-record-title-weight);
	color: var(--al-record-title-color);
	/* it is the page's h1 on a detail screen, so the browser's heading margin goes */
	margin: 0;
}

.al-record-actions {
	display: flex;
	align-items: center;
	gap: var(--al-record-actions-gap);
}

/* THE HEADER SPACES ITS OWN BACK LINK. al-back is a control and owns its type and
   ink; where it sits relative to the title below it is this header's business, which
   is why the spacing is here and not there. */
.al-record-header .al-back {
	margin-bottom: var(--al-sys-space-150);
}

/* =============================================================================
   RECORD LAYOUT
   The body of a record page that is edited AND read: the form down the left, and
   a panel down the right that reads the record back. It is the shape every record
   editor worth using has, and the reason it is here rather than on one page is
   that the alternative — a single full-width column of stacked groups — spends its
   whole right-hand side on nothing while running twice the height.

   THE SIDE IS A FIXED WIDTH, and that is the decision. Everything a summary holds
   is a short label and a short value; given a fraction it would grow with the
   window and end up a second form. Given a fixed width it stays a rail, and the
   form takes whatever is left.

   IT DOES NOT REFLOW PER RECORD. A layout that rearranges itself when the data
   changes is a layout nobody can learn, which is a lesson this one cost two
   rebuilds to arrive at. One breakpoint, and it is about the WINDOW.
   ========================================================================== */
.al-record-layout {
	--al-record-side-width: 300px;
	--al-record-layout-gap: var(--al-sys-space-400);
	/* the width below which the rail would squeeze the form's three-across fields to
	   two, measured on the widest form this holds rather than picked off a device */
	--al-record-layout-stack-at: 1100px;

	box-sizing: border-box;
	display: grid;
	grid-template-columns: minmax(0, 1fr) var(--al-record-side-width);
	gap: var(--al-record-layout-gap);
	align-items: start;
}

/* THE SIDE FOLLOWS YOU DOWN THE FORM, because its whole job is to be glanced at
   while you are editing something else. Sticky, not fixed: it stops at the end of
   the layout instead of hanging over what comes after it. */
.al-record-side {
	position: sticky;
	top: var(--al-sys-space-200);
}

@media (max-width: 1100px) {
	.al-record-layout {
		grid-template-columns: minmax(0, 1fr);
	}
	.al-record-side {
		position: static;
	}
}

/* =============================================================================
   THE READING
   One line saying what a record DOES, derived from the record every time it is
   drawn. It is not a description somebody typed, and that is the whole point: a
   discount named "10% off flower" can be five percent off everything, and the two
   halves of that screen will sit there agreeing with each other forever. A derived
   line cannot. Anywhere a record's meaning is assembled out of a dozen fields —
   a discount, a sale limit, a price rule, a permission set — this is where the
   answer goes.

   IT LEADS ITS PANEL AND IT IS NOT A HEADING. Set as a heading it rewrites itself
   as the user types, which is exactly as unsettling as it sounds; the record's NAME
   is the heading, and this is what the record turns out to mean.
   ========================================================================== */
.al-reading {
	--al-reading-color: var(--al-sys-color-text-primary);
	--al-reading-muted-color: var(--al-sys-color-text-muted);
	--al-reading-size: var(--al-sys-font-size-300);
	--al-reading-line-height: var(--al-sys-line-height-body);
	--al-reading-gap: var(--al-sys-space-200);
	--al-reading-rule-color: var(--al-sys-color-border);
	--al-reading-rule-width: 1px;

	box-sizing: border-box;
	margin: 0 0 var(--al-reading-gap);
	padding-bottom: var(--al-reading-gap);
	border-bottom: var(--al-reading-rule-width) solid var(--al-reading-rule-color);
	font-size: var(--al-reading-size);
	line-height: var(--al-reading-line-height);
	color: var(--al-reading-color);
}
/* Nothing to read yet, because the record has not been told enough to mean anything.
   It keeps its place and goes quiet, the same call a-missing-number-is-quiet makes. */
.al-reading-empty {
	--al-reading-color: var(--al-reading-muted-color);
}

/* =============================================================================
   PAGE TITLE
   The name of the thing the page is about, on the page rather than inside
   something on it. Every other title this system owns belongs to a container —
   a card, a panel, a popover, a sheet, a topbar — and none of them was right
   for a heading whose container IS the page. The app had grown two of these
   under its own names before this existed.

   IT DECLARES ITS OWN TOKENS AND STANDS ALONE, which is the whole point of it.
   al-card-title, al-panel-title and al-record-title read tokens declared on
   their roots, so they collapse to body type when used without one. Measured:
   13px regular instead of the heading they look like in place. This one behaves
   like al-sheet-title, al-popover-title and al-confirm-title, which are the
   three that already got this right.

   FLEX, BECAUSE A PAGE TITLE IS RARELY ALONE. It sits beside a count, a status
   or a badge far more often than not, and a heading that forces its host to
   re-declare display and a gap every time is not carrying its weight.
   ========================================================================== */
.al-page-title {
	--al-page-title-color: var(--al-sys-color-text-primary);
	--al-page-title-size: var(--al-sys-font-size-500);
	--al-page-title-weight: var(--al-sys-font-weight-heading);
	--al-page-title-line-height: var(--al-sys-line-height-heading);
	--al-page-title-font-family: var(--al-sys-font-family);
	--al-page-title-gap: var(--al-sys-space-150);

	/* the box is reset in full, for the reason al-sheet-title states at length:
	   this lands on whatever heading level the surrounding document called for,
	   and a title that changes shape with the tag someone reached for is not one */
	box-sizing: border-box;
	margin: 0;
	padding: 0;
	border: 0;
	text-transform: none;
	letter-spacing: normal;

	display: flex;
	align-items: baseline;
	gap: var(--al-page-title-gap);
	font-family: var(--al-page-title-font-family);
	font-size: var(--al-page-title-size);
	font-weight: var(--al-page-title-weight);
	line-height: var(--al-page-title-line-height);
	color: var(--al-page-title-color);
}

/* =============================================================================
   THE SAME FIVE WRAPPERS FOR AN ENGINE WITHOUT :has().

   :has() is Chrome 105. On the POS terminals (Chrome 101) each rule above is
   discarded WHOLE, so these wrappers lose display:flex and their typography and a
   date field stops laying out. 16 .al-datepick uses in react/pos alone.

   The header above explains that :has(> input) is carrying SPECIFICITY, to land one
   step above the label rule. Without it the selector is a step lighter, so the class
   is repeated to buy that step back rather than reaching for !important.

   @supports not selector(:has(*)) fires only where :has() is missing. Verified: on
   Chromium CSS.supports("selector(:has(*))") is true, so none of this reaches a
   modern browser and the precise rules above keep their exclusion of label wrappers.
   ============================================================================= */
@supports not selector(:has(*)) {
	.al-field > span.al-datepick.al-datepick {
		display: flex;
		margin-bottom: 0;
		font-size: var(--al-datepicker-font-size);
		font-weight: var(--al-datepicker-font-weight);
		color: var(--al-datepicker-text);
	}
	.al-field > span.al-timepick.al-timepick {
		display: flex;
		margin-bottom: 0;
		font-size: var(--al-timepicker-font-size);
		font-weight: var(--al-timepicker-font-weight);
		color: var(--al-timepicker-text);
	}
	.al-field > span.al-combo.al-combo {
		display: flex;
		margin-bottom: 0;
		font-size: var(--al-combobox-font-size);
		font-weight: var(--al-combobox-font-weight);
		color: var(--al-combobox-text);
	}
	.al-field > span.al-multi.al-multi {
		display: flex;
		margin-bottom: 0;
		font-size: var(--al-multiselect-font-size);
		font-weight: var(--al-multiselect-font-weight);
		color: var(--al-multiselect-text);
	}
	.al-field > span.al-autocomplete.al-autocomplete {
		display: flex;
		margin-bottom: 0;
		font-size: var(--al-autocomplete-font-size);
		font-weight: var(--al-autocomplete-font-weight);
		color: var(--al-autocomplete-text);
	}
}

/* =============================================================================
   THE THREE :has() RULES THAT ARE NOT POLISH, FOR AN ENGINE WITHOUT :has().

   Most of this file own :has() usage is state that degrades acceptably on the POS
   terminals (Chrome 101): a disabled control that is not dimmed, an affix that does
   not redden, a size tier that stays medium. Those are left alone deliberately --
   they are listed in scripts/engine-baseline.txt with that reasoning.

   These three are different, because without them a control cannot be READ:
     - a switch that never shows its on state
     - a segmented control with no visible selection
     - a page that scrolls behind an open modal

   A parent cannot be styled from a child state without :has(), so the classes below
   are put on by public/js/shared/has-shim.js, which returns immediately on any engine
   that has :has() and is therefore inert everywhere else. It re-derives the truth
   from the DOM rather than toggling on open/close, so it cannot get out of step --
   which is the failure the scroll-lock comment above rightly warns about.

   Values are copied from the three rules above. If those move, these move with them.
   ============================================================================= */
@supports not selector(:has(*)) {
	.al-switch.is-checked {
		--al-switch-track-fill: var(--al-sys-color-accent-primary);
		--al-switch-track-border-color: var(--al-sys-color-accent-primary-line);
		--al-switch-mark: var(--al-sys-color-text-on-accent);
	}
	.al-seg-item.is-checked {
		--al-segmented-chip-fill: var(--al-sys-color-accent-primary);
		--al-segmented-chip-shadow: var(--al-sys-elevation-sticky);
		--al-segmented-label: var(--al-sys-color-text-on-accent);
	}
	html.has-open-modal {
		overflow: hidden;
	}
}

/* =============================================================================
   CHANGED
   A value that just moved, on its own or because of an act, gets a short tint
   behind it that fades out, so the eye catches it without anything shifting. On an
   overlay, not the element's background: cells and tiles often paint their own
   ground, some with !important, and the flash has to show over them. The class is
   put on for one change and taken off on animationend; deciding WHEN a value really
   changed (not on first load, not on a refresh that wrote the same number) is the
   caller's job.
   ============================================================================= */
.al-changed {
	--al-changed-tint: var(--al-sys-color-row-tint-selected);
	--al-changed-duration: 0.6s;
	position: relative;
	/* the tint goes UNDER the text and over the element's own ground: isolation makes the
	   element its own stacking context, so the -1 below stops at its background rather
	   than falling behind the page. An opaque tint over the digits hid them mid-flash. */
	isolation: isolate;
}
.al-changed::after {
	content: '';
	position: absolute;
	inset: 0;
	border-radius: inherit;
	background: var(--al-changed-tint);
	pointer-events: none;
	z-index: -1;
	animation: al-changed-fade var(--al-changed-duration) var(--al-sys-motion-easing-exit) both;
}
.al-changed-warning {
	--al-changed-tint: var(--al-sys-color-status-warning-tint);
}
@keyframes al-changed-fade {
	from {
		opacity: 1;
	}
	to {
		opacity: 0;
	}
}
@media (prefers-reduced-motion: reduce) {
	.al-changed::after {
		animation-timing-function: steps(1, end);
	}
}
