/* Darkless portal.
 *
 * This is "The Whole Panel" (design artifact, 2026-08-25) rather than the
 * earlier "Darkless Portal Design" the file used to carry. The two disagreed
 * about nearly every primitive -- ground, accent, radius, display face -- so
 * they could not be blended; a stylesheet holding two design systems is a
 * stylesheet with none.
 *
 * The file is four blocks and the order carries weight: tokens, then the
 * portal's own components, then the design system, then the bridge. The
 * components sit ABOVE the system because the two share names -- .btn, .card,
 * .row, .pill -- and document order settles a tie. Put them below and the old
 * rules win every collision, which is exactly how a port comes out looking
 * almost right.
 *
 * The discipline from the previous file survives and is the reason it stayed
 * coherent: every length here comes from a token. Adding a page means picking
 * from the scale, not inventing 13px because 12 looked tight.
 */

/* --- Type ---------------------------------------------------------------
   IBM Plex, self-hosted (OFL, static/fonts). Sans for everything a customer
   reads, mono for identifiers only. Latin and Latin Extended as separate
   subsets so an English page downloads about 70KB of face and a name with
   an accent still sets in Plex. Block, not swap: the shell preloads the
   four faces a page is made of and the URL carries a version so they are
   cached for a year, and a page that paints in Helvetica and then re-sets
   itself in Plex a moment later looks like a page loading twice. The stack
   behind it is what the portal set in before: a page with the fonts
   blocked is the old page, not a broken one. */
@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 400;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-400-normal.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 400;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-ext-400-normal.woff2?v=1) format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 500;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-500-normal.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 500;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-ext-500-normal.woff2?v=1) format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 600;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-600-normal.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: normal;
	font-weight: 600;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-ext-600-normal.woff2?v=1) format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
	font-family: "IBM Plex Sans";
	font-style: italic;
	font-weight: 400;
	font-display: block;
	src: url(fonts/ibm-plex-sans-latin-400-italic.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "IBM Plex Mono";
	font-style: normal;
	font-weight: 400;
	font-display: block;
	src: url(fonts/ibm-plex-mono-latin-400-normal.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "IBM Plex Mono";
	font-style: normal;
	font-weight: 500;
	font-display: block;
	src: url(fonts/ibm-plex-mono-latin-500-normal.woff2?v=1) format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
	/* --- Colour -----------------------------------------------------------
	   Light is stated on bare :root and dark is the override, which is the
	   only arrangement where a viewer who has expressed no preference still
	   gets a complete palette. Neither theme is the identity: the choice is
	   the customer's operating system until they say otherwise. */
	--ground: #f8f7f5;
	--ground-2: #f1efec;
	--surface: #fff;
	--raised: #fff;
	--sunk: #f2f0ed;

	--ink: #15151a;
	--body: #5a5860;
	--faint: #6f6b68;
	--line: #e5e1dc;
	--hair: #eeebe6;

	/* Accent. Two of them, and they are always a ramp rather than a flat
	   fill: the gradient is what separates a control you act on from a
	   surface you read. It is wayfinding and action -- the current rail item,
	   the primary button, the focus ring, the tab underline. Nothing that
	   reports what a machine is DOING may use it, because a customer who has
	   learnt that blue means "go here" must never meet a blue that means
	   "running". */
	--accent: #009eff;
	--accent-2: #6e5bff;
	--accent-ink: #0060a8;
	/* The accent as a FILL under white text. --accent itself is too light
	   for that (white on #009eff is 2.86:1); these hold 4.5:1 in both
	   themes, so a chosen chip or the primary button is readable. */
	--accent-fill: #0072c6;
	--accent-fill-2: #5b47e6;
	--accent-soft: #e8f4ff;
	--accent-glow: rgba(0, 158, 255, 0.22);
	/* The shop's brand wash and tints (David, 2026-11-06: the checkout
	   "feels so much just one color"): the two fills, faint, behind the
	   top of a shop page and under the things worth a second look. */
	--wash-a: rgba(91, 71, 230, 0.09);
	--wash-b: rgba(0, 114, 198, 0.08);
	--tint: rgba(91, 71, 230, 0.06);
	--tint-line: rgba(91, 71, 230, 0.28);

	/* Status. Amber wants you, red failed. There is deliberately no green:
	   "working correctly" is the condition of almost every row on every page,
	   and painting it makes the two rows that need reading harder to find. */
	--attn: #a35d00;
	--attn-soft: #fdf1e0;
	--bad: #c22d22;
	--bad-soft: #fdeae8;
	/* The one green (portal review, October 2026), and it means one thing: a
	   server that is running. Not paid, not done, not healthy -- those stay
	   plain, because painting every good row green hides the one that is
	   not. A customer opening a server page needs to see at a glance that
	   it is on, and a grey pill beside the name did not say it loudly
	   enough. */
	--run: #15803d;
	--run-soft: #e3f5ea;
	--run-glow: rgba(21, 128, 61, 0.2);

	/* The console and the build log keep their own dark ground in both
	   themes. A terminal that turns white is a terminal nobody recognises. */
	--panel: #101014;
	--panel-2: #17171d;
	--panel-ink: #f2f1ef;
	--panel-line: #26262e;

	--sh-1: 0 1px 2px rgba(21, 21, 26, 0.06);
	--sh-2: 0 2px 4px rgba(21, 21, 26, 0.05), 0 6px 16px -6px rgba(21, 21, 26, 0.1);
	--sh-3: 0 12px 32px -12px rgba(21, 21, 26, 0.22), 0 2px 6px rgba(21, 21, 26, 0.06);

	--r-lg: 12px;
	--r: 8px;
	--r-sm: 5px;
	--r-xs: 3px;

	--sans: "IBM Plex Sans", "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
	--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

	/* Four durations, three curves. Motion here is confirmation that
	   something happened, never decoration: --t-micro is a button admitting
	   it was pressed, --t-enter is a view arriving. */
	--t-micro: 110ms;
	--t-fast: 180ms;
	--t: 260ms;
	--t-enter: 420ms;
	--e-out: cubic-bezier(0.16, 1, 0.3, 1);
	--e-io: cubic-bezier(0.4, 0, 0.2, 1);
	--e-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

	--side: 240px;
	/* The canvas gutter. Wider than any spacing step because it is the one
	   space that frames a whole page rather than a part of one. */
	--gutter: 40px;

	/* --- Legacy aliases ---------------------------------------------------
	   The un-ported pages below still name the old tokens. Pointing them at
	   the new palette means those pages inherit the new colours the moment
	   this file lands, instead of sitting in the old scheme looking broken
	   next to the ported ones. Each alias dies with the last rule that uses
	   it; when this block is empty the port is finished. */
	--surface-raised: var(--raised);
	--border: var(--line);
	--border-strong: var(--faint);
	--text: var(--ink);
	--text-muted: var(--body);
	--text-faint: var(--faint);

	--ok: var(--accent-ink);
	--working: var(--accent);
	--ok-dim: var(--accent-soft);
	--working-dim: var(--accent-soft);
	--attn-dim: var(--attn-soft);
	--bad-dim: var(--bad-soft);
	--ok-line: var(--line);
	--working-line: var(--line);
	--attn-line: var(--line);
	--bad-line: var(--line);
	--attn-ground: var(--attn-soft);

	--accent-strong: var(--accent-2);
	--accent-dim: var(--accent-soft);
	--accent-line: var(--accent-glow);

	--s-1: 2px;
	--s-2: 4px;
	--s-3: 6px;
	--s-4: 8px;
	--s-5: 10px;
	--s-6: 12px;
	--s-7: 14px;
	--s-8: 16px;
	--s-9: 20px;
	--s-10: 24px;
	--s-11: 32px;
	--s-12: 48px;

	--radius: var(--r-xs);
	--radius-control: var(--r-sm);
	--radius-shell: var(--r);
	--radius-pill: 999px;

	/* Seven sizes (portal review, October 2026). Nothing a customer reads is
	   under 13px; 12px is for labels and column heads, which are set
	   semibold and tracked so they read as furniture, not as small text.
	   xs label · sm note · md cell and control · base body · lg section
	   title · xl big number · 2xl page title. */
	--t-xs: 12px;
	--t-sm: 13px;
	--t-base: 15px;
	--t-md: 14.5px;
	--t-lg: 18px;
	--t-xl: 26px;
	--t-2xl: 30px;
	--w-normal: 400;
	--w-medium: 500;
	--w-semi: 600;

	--lift: var(--sh-1);
	--lift-strong: var(--sh-2);
	--halo: 0 0 0 3px var(--accent-glow);
	/* Fluid. Tables and lists run to the window's edge; only prose and a
	   form stop, at --prose, because a sentence 1800px long is a sentence
	   nobody finishes. */
	--measure: none;
	--measure-app: none;
	--prose: 720px;
	--bar: 53px;
	--ui: var(--sans);
}

/* --- Dark -----------------------------------------------------------------
   Stated twice on purpose. The media query serves the viewer who has never
   touched the toggle, guarded so an explicit choice of light survives their
   operating system being dark; the attribute serves the viewer who has, and
   has to be able to win in both directions.

   Only the tokens are restated, never a rule, so nothing below this point
   knows two themes exist. The relationships hold: ground under surface under
   raised, faint under body under ink. The accent brightens because on a dark
   ground the light-theme blue is a bruise rather than a signal. */
@media (prefers-color-scheme: dark) {
	html:not([data-theme="light"]) {
		--ground: #0b0b0e;
		--ground-2: #101014;
		--surface: #141419;
		--raised: #1a1a21;
		--sunk: #101014;

		--ink: #f4f3f1;
		--body: #a3a0a8;
		--faint: #8c8990;
		--line: #26262e;
		--hair: #1e1e25;

		--accent: #38b0ff;
		--accent-2: #8b7bff;
		--accent-ink: #8ed2ff;
		--accent-fill: #0072c6;
		--accent-fill-2: #5b47e6;
		--accent-soft: #0d2436;
		--accent-glow: rgba(56, 176, 255, 0.3);
		--wash-a: rgba(91, 71, 230, 0.16);
		--wash-b: rgba(0, 114, 198, 0.14);
		--tint: rgba(91, 71, 230, 0.1);
		--tint-line: rgba(124, 108, 255, 0.35);

		--attn: #dfa03c;
		--attn-soft: #2a2013;
		--bad: #f0685c;
		--bad-soft: #2e1614;
		--run: #4cc38a;
		--run-soft: #13261c;
		--run-glow: rgba(76, 195, 138, 0.22);

		--panel: #08080a;
		--panel-2: #101014;
		--panel-line: #22222a;

		--sh-1: 0 1px 2px rgba(0, 0, 0, 0.5);
		--sh-2: 0 2px 6px rgba(0, 0, 0, 0.45), 0 10px 24px -10px rgba(0, 0, 0, 0.6);
		--sh-3: 0 16px 40px -16px rgba(0, 0, 0, 0.8), 0 2px 8px rgba(0, 0, 0, 0.5);
	}
}

html[data-theme="dark"] {
	--ground: #0b0b0e;
	--ground-2: #101014;
	--surface: #141419;
	--raised: #1a1a21;
	--sunk: #101014;

	--ink: #f4f3f1;
	--body: #a3a0a8;
	--faint: #8c8990;
	--line: #26262e;
	--hair: #1e1e25;

	--accent: #38b0ff;
	--accent-2: #8b7bff;
	--accent-ink: #8ed2ff;
	--accent-fill: #0072c6;
	--accent-fill-2: #5b47e6;
	--accent-soft: #0d2436;
	--accent-glow: rgba(56, 176, 255, 0.3);
	--wash-a: rgba(91, 71, 230, 0.16);
	--wash-b: rgba(0, 114, 198, 0.14);
	--tint: rgba(91, 71, 230, 0.1);
	--tint-line: rgba(124, 108, 255, 0.35);

	--attn: #dfa03c;
	--attn-soft: #2a2013;
	--bad: #f0685c;
	--bad-soft: #2e1614;
	--run: #4cc38a;
	--run-soft: #13261c;
	--run-glow: rgba(76, 195, 138, 0.22);

	--panel: #08080a;
	--panel-2: #101014;
	--panel-line: #22222a;

	--sh-1: 0 1px 2px rgba(0, 0, 0, 0.5);
	--sh-2: 0 2px 6px rgba(0, 0, 0, 0.45), 0 10px 24px -10px rgba(0, 0, 0, 0.6);
	--sh-3: 0 16px 40px -16px rgba(0, 0, 0, 0.8), 0 2px 8px rgba(0, 0, 0, 0.5);
}

/* Ordering note: the legacy block sits ABOVE the new primitives on purpose.
   The two systems share names -- .btn, .card, .row, .pill -- and CSS settles
   a tie by document order, so with legacy last the old rules would quietly win
   and the port would look like it had not happened. Last one wins, and the new
   system has to be the one that does. */

/* ===========================================================================
   PORTAL COMPONENTS -- the parts the drawing does not cover.
   ===========================================================================
   This block started out as the previous design system, kept alive so that
   un-ported pages still rendered. Every page has since been rebuilt on the
   primitives below, and what is left here is not the old design at all: it is
   the components this product has and The Whole Panel never drew.

   The invoice document. The SVG charts, with their gap markers, crosshair and
   read-as-a-table fallback -- the drawing has a CSS bar chart where we have a
   real plot. The OS picker. The masked-address reveal. The shop's cart lines.
   The modal. The marketing bar. The not-built-yet markers.

   They still read the old token names, which the alias block at the top points
   at the new palette, so they take the new colours without being rewritten.
   Retokenising them is worth doing; it is not worth doing under the heading
   "legacy", because that word says they are on their way out and they are not.
   =========================================================================== */

/* The gradient button re-pitched for a light room: the dark theme's pale top
   edge under white ink would wash out, so the whole ramp sits on the darker
   accent the light theme already reads text in. */
html[data-theme="light"] .btn.primary {
	/* The fill tokens, not --accent: white on #009eff is 2.86:1, and this
	   rule out-specifies the accessible ramp below it. */
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.25),
		0 2px 8px rgba(107, 61, 232, 0.3);
}

html[data-theme="light"] .btn.primary:hover {
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	filter: brightness(1.08);
}

/* Amber's ink flips with the ground: near-black on the bright dark-theme
   amber, white on the deeper light-theme one. */
html[data-theme="light"] .btn.pay {
	color: #ffffff;
}

* {
	box-sizing: border-box;
}

/* The skip link: off-screen until it is focused, then a real, visible control
   at the top-left. Not display:none and not visibility:hidden -- both remove it
   from the tab order, which is the one place it has to be. */
.skip {
	position: absolute;
	left: -9999px;
	top: 0;
	z-index: 200;
	background: var(--surface-raised);
	color: var(--text);
	border: 1px solid var(--accent);
	border-radius: var(--radius-control);
	padding: var(--s-4) var(--s-7);
	font-size: var(--t-md);
	text-decoration: none;
}

.skip:focus {
	left: var(--s-6);
	top: var(--s-6);
}

/* The staff-view band: the one state where the person at the keyboard is not
   the customer, worn above the whole shell on every page. Amber is right by
   the system's own rule -- it is a condition that needs reading -- and there
   is no dismiss control because the condition does not dismiss. */
.staffband {
	background: var(--attn-dim);
	color: var(--attn);
	border-bottom: 1px solid var(--attn-line);
	padding: var(--s-3) var(--s-7);
	font-size: var(--t-sm);
	text-align: center;
}

/* The band's own exit. Inline with the sentence that names the state, because
   the sentence tells staff to sign out when done and a promise the band makes
   is the band's to keep. */
.bandout {
	display: inline-block;
	margin-left: var(--s-5);
}

/* The person's way out, under their chip. Present for everyone, not only the
   staff view: a shared machine is the ordinary case this exists for. */
.signout-foot {
	margin-top: var(--s-3);
}

/* [hidden] has to win. The browser's own rule for it is `display: none` at
   author-origin strength, so ANY component rule that sets display -- .btn is
   inline-flex, .pill is inline-flex, rows are flex -- silently beats it, and
   an element we asked to be hidden stays on screen taking up its full height.
   Hiding by attribute is the one mechanism that works identically with and
   without JavaScript, so it must not depend on which class an element carries. */
[hidden] {
	display: none !important;
}

/* The safety net under the per-element focus rules: nothing interactive is
   ever focusable without a visible ring. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

/* The page header sits closer to its own content than panels sit to each
   other: the gap under an H1 is a caption gap, not a section gap. */
.app-main > .page-head {
	margin-bottom: calc(-1 * var(--s-4));
}

/* The ground is lit, not flat. Two glows in the accent's own family -- violet
   into indigo, never a status hue -- fixed to the viewport so scrolling moves
   the page through the light rather than dragging the light along. This is
   atmosphere, and it is allowed to be: the rule that colour must mean
   something binds data, and a room is not data.

   (Amended 2026-08-23: the original system was greyscale-pure by decree.
   David overruled it -- the austerity read as unfinished, not disciplined.) */
body {
	margin: 0;
	background:
		radial-gradient(1100px 520px at 82% -8%, rgba(178, 138, 255, 0.075), transparent 62%),
		radial-gradient(900px 620px at -12% 40%, rgba(109, 94, 255, 0.05), transparent 60%),
		var(--ground);
	background-attachment: fixed;
	color: var(--text);
	font-family: var(--ui);
	font-size: var(--t-base);
	line-height: 1.6;
	-webkit-font-smoothing: antialiased;
}

/* The drawing's link treatment, which is quieter than the one this replaced:
   links inherit their surroundings and never underline on hover. Hover is
   shown the way every other control shows it -- a colour step -- because an
   underline appearing under half the words on a panel of controls makes the
   page flicker as the pointer crosses it. Prose links are the exception and
   get the accent ink below, so a link in a sentence is still findable without
   sweeping the mouse over it. */
a {
	color: inherit;
	text-decoration: none;
}

a:hover {
	text-decoration: none;
}

/* A link living inside prose: a sentence's word that goes somewhere. These
   carry the accent ink at rest -- the one place colour is the affordance --
   and step brighter on hover instead of underlining. */
.rd a,
.rt a,
.lede a,
.card-lede a,
.hint a,
.inline a:not(.btn),
.ed a,
.foot a,
p a:not(.btn) {
	color: var(--accent-ink);
}

.rd a:hover,
.rt a:hover,
.lede a:hover,
.card-lede a:hover,
.hint a:hover,
.inline a:hover,
.ed a:hover,
.foot a:hover,
p a:hover {
	color: var(--accent);
}

/* Links that are navigation rather than prose stay in ink: a bar of coloured
   words is not a bar somebody can scan. The accent marks the one that is
   current instead. */
.marketing nav a,
.crumbs a,
.tabs a,
.rail a,
.alt a {
	color: var(--text-muted);
}

h1 {
	font-size: var(--t-2xl);
	line-height: 1.15;
	margin: 0;
	letter-spacing: -0.02em;
	font-weight: var(--w-semi);
	text-wrap: balance;
}

h2 {
	font-size: var(--t-lg);
	margin: 0;
	font-weight: var(--w-semi);
	letter-spacing: -0.01em;
}

/* --- Chrome: the shop -----------------------------------------------------
   The bar a prospect sees. It navigates the shop it is attached to: linking
   each product line back to darkless.cloud meant the nav on our own store
   took people off the store. */
.marketing {
	display: flex;
	align-items: center;
	gap: var(--s-8);
	flex-wrap: wrap;
	padding: var(--s-6) var(--s-10);
	border-bottom: 1px solid var(--border);
	background: var(--surface);
	position: sticky;
	top: 0;
	z-index: 10;
	min-height: var(--bar);
}

/* Frosted, where the platform can. The bar floats over content scrolling
   beneath it -- the single cheapest way a page stops feeling like 2014 -- and
   browsers without backdrop-filter keep the solid bar above. */
@supports (backdrop-filter: blur(1px)) {
	.marketing,
	.app-top {
		background: color-mix(in srgb, var(--surface) 72%, transparent);
		backdrop-filter: blur(14px) saturate(1.4);
		-webkit-backdrop-filter: blur(14px) saturate(1.4);
	}
}

.wordmark {
	font-size: var(--t-sm);
	letter-spacing: 0.2em;
	color: var(--text);
	text-decoration: none;
	white-space: nowrap;
}

/* The one piece of brand colour on the page. A wordmark in the accent would
   compete with the nav; a full stop in it does not. */
.wordmark::after {
	content: ".";
	color: var(--accent);
	letter-spacing: 0;
}

.marketing nav {
	display: flex;
	gap: var(--s-8);
	flex-wrap: wrap;
}

.marketing nav a {
	font-size: var(--t-md);
	text-decoration: none;
	padding-bottom: var(--s-1);
	border-bottom: 1px solid transparent;
}

.marketing nav a:hover {
	color: var(--text);
}

.marketing nav a.on {
	color: var(--text);
	border-bottom-color: var(--accent);
}

/* The shop bar's plain links, in the nav's ink rather than the prose
   accent. */
.mk-link {
	font-size: var(--t-md);
	color: var(--text-muted);
	text-decoration: none;
	white-space: nowrap;
}

.mk-link:hover {
	color: var(--text);
}

/* A phone keeps the bar to one row: Support is the one that goes, since
   the footer's Contact is the same door. */
@media (max-width: 560px) {
	.mk-support {
		display: none;
	}
}

.marketing .right,
.app-top .right {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--s-5);
}

.app-nav .btn {
	width: 100%;
}

.rail {
	display: flex;
	flex-direction: column;
	gap: var(--s-1);
}

.rail a {
	position: relative;
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--s-4);
	padding: var(--s-5) var(--s-6);
	border-radius: var(--radius-control);
	text-decoration: none;
	font-size: var(--t-md);
	border: 1px solid transparent;
}

.rail a:hover {
	background: var(--surface-raised);
	color: var(--text);
}

/* Where you are is the one thing the rail has to say without being read.
   Colour and a bar, not weight alone -- bold grey against grey is a
   difference you have to go looking for. */
.rail a.on {
	background: var(--accent-dim);
	color: var(--text);
	border-color: var(--accent-line);
	font-weight: var(--w-medium);
}

.rail a.on::before {
	content: "";
	position: absolute;
	left: -1px;
	top: var(--s-3);
	bottom: var(--s-3);
	width: 2px;
	border-radius: 0 2px 2px 0;
	background: var(--accent);
}

/* The count is mono and faint: it is a number you scan, not a word you read.
   Amber when something in it needs paying -- the rail is the only place a
   customer reliably looks. */
.rail em {
	font-style: normal;
	font-size: var(--t-xs);
	color: var(--text-faint);
	font-variant-numeric: tabular-nums;
}

.rail em.due {
	color: var(--attn);
}

@media (max-width: 760px) {
	.app-body {
		grid-template-columns: 1fr;
	}

	.app-body {
		background: none;
	}

	/* A phone has no column to fix: the nav becomes a band and flows. */
	.app-nav {
		border-right: none;
		border-bottom: 1px solid var(--border);
		position: static;
		height: auto;
		overflow-y: visible;
	}

	.rail {
		flex-direction: row;
		flex-wrap: wrap;
	}

	/* On a phone the nav is a band ABOVE the content, so anything left in it
	   is something the customer must scroll past before reaching what they
	   came for. The foot of the sidebar -- an upsell card and a profile chip
	   -- was costing about 200px of that, which on a 390px screen put "Pay
	   now" and the fleet below the fold.

	   The upsell goes: it is the least urgent thing on the page and the shop
	   is one tap away in nothing-yet states and the rail alike. The profile
	   chip stays, because signing out has to be reachable, but it collapses
	   to a single quiet row rather than a card. */
	.nav-foot {
		margin-top: var(--s-6);
	}

}

/* --- Page furniture ------------------------------------------------------- */
.page {
	max-width: var(--measure);
	margin: 0 auto;
	/* The same side gutter as the signed-in pages: the checkout and the
	   404 sat 16px nearer the edge than every page around them. */
	padding: var(--s-11) var(--gutter) var(--s-12);
	display: grid;
	gap: var(--s-11);
	align-content: start;
}

.page-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--s-8);
	flex-wrap: wrap;
}

.page-head h1 {
	margin: 0;
}

/* The line under a heading that is made of identifiers -- a host and a date,
   a plan and a hostname. Mono wherever it appears, because it is one. */
.head-meta {
	font-size: var(--t-sm);
	color: var(--text-faint);
	letter-spacing: 0;
}

.page-head .sub,
.page-head p {
	margin: var(--s-3) 0 0;
	font-size: var(--t-base);
	color: var(--text-muted);
	max-width: var(--prose);
}

/* Crumbs belong to the title under them, not to the section above. The page's
   own gap is set for the distance between sections, which is too far here. */
.crumbs + .page-head,
.crumbs + form .page-head {
	margin-top: calc(var(--s-9) * -1);
}

/* A section is announced by a label and a rule across the rest of the line,
   not by drawing a box round everything under it. Boxes inside boxes is how a
   page of panels turns into a page of blocks. */
.section-head {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	margin: 0 0 var(--s-7);
	font-size: var(--t-xs);
	color: var(--text-faint);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	white-space: nowrap;
}

.section-head::after {
	content: "";
	flex: 1;
	height: 1px;
	min-width: var(--s-9);
	background: var(--border);
}

.crumbs {
	font-size: var(--t-sm);
	color: var(--text-faint);
	display: flex;
	gap: var(--s-3);
	flex-wrap: wrap;
}

.crumbs a {
	color: var(--text-muted);
	text-decoration: none;
}

.crumbs a:hover {
	color: var(--text);
}

.crumbs span {
	color: var(--border-strong);
}

/* --- Controls ------------------------------------------------------------- */
.btn {
	font: inherit;
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	padding: 9px var(--s-8);
	border-radius: var(--radius-control);
	cursor: pointer;
	background: transparent;
	color: var(--text);
	border: 1px solid var(--border-strong);
	text-decoration: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--s-4);
	white-space: nowrap;
	line-height: 20px;
}

.btn:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

/* The way on. It is the accent rather than a white slab because a white slab
   is the brightest thing on a dark page whether or not it is the thing you
   came to press. */
.btn.primary {
	background: linear-gradient(180deg, #c5a4ff, var(--accent) 55%, var(--accent-strong));
	color: var(--accent-ink);
	border-color: var(--accent-strong);
	font-weight: var(--w-semi);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.28),
		0 2px 8px rgba(154, 108, 245, 0.35);
}

.btn.primary:hover {
	background: linear-gradient(180deg, #d2b8ff, #bd97ff 55%, var(--accent));
	border-color: var(--accent);
	color: var(--accent-ink);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.32),
		0 4px 16px rgba(154, 108, 245, 0.45);
}

.btn.primary:active {
	transform: translateY(1px);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.2),
		0 1px 4px rgba(154, 108, 245, 0.3);
}

/* A quiet button that still reads as the action in its group. */
.btn.ghost {
	border-color: transparent;
	color: var(--accent);
	padding-left: var(--s-5);
	padding-right: var(--s-5);
}

.btn.ghost:hover {
	background: var(--accent-dim);
	border-color: var(--accent-line);
	color: var(--accent);
}

.btn.pay {
	background: var(--attn);
	border-color: var(--attn);
	color: #241a00;
	font-weight: var(--w-semi);
}

.btn.danger {
	color: var(--bad);
}

.btn.danger:hover {
	border-color: var(--bad);
	background: var(--bad-dim);
}

.btn.wide {
	width: 100%;
}

.btn[disabled] {
	opacity: 0.4;
	cursor: not-allowed;
}

.actions {
	display: flex;
	gap: var(--s-4);
	flex-wrap: wrap;
}

@media (prefers-reduced-motion: no-preference) {
	.btn,
	.rail a,
	a.card,
	.tile,
	.plan,
	.order-row,
	.term-opt,
	.opt,
	.os-fam,
	.tabs a,
	.windows a,
	.table .row-act,
	.sub-machine {
		transition:
			background 140ms ease,
			border-color 140ms ease,
			color 140ms ease,
			box-shadow 180ms ease,
			transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2);
	}

	dialog.modal[open] {
		animation: rise 200ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
	}
}

@keyframes rise {
	from {
		opacity: 0;
		transform: translateY(7px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* --- Pill -----------------------------------------------------------------
   The dot is the point. Colour alone is not a signal everybody can see, so
   state carries a shape as well: filled dot for a colour, hollow dot and a
   dashed edge for "we cannot tell". */
.pill {
	display: inline-flex;
	align-items: center;
	gap: var(--s-3);
	font-size: 12.5px;
	font-weight: var(--w-semi);
	padding: var(--s-2) var(--s-5);
	border-radius: var(--radius-pill);
	border: 1px solid var(--border-strong);
	color: var(--text-muted);
	white-space: nowrap;
	letter-spacing: 0;
	text-transform: none;
}

.pill::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: currentColor;
	flex: none;
}

.pill.ok {
	color: var(--ok);
	background: var(--ok-dim);
	border-color: var(--ok-line);
}

.pill.working {
	color: var(--working);
	background: var(--working-dim);
	border-color: var(--working-line);
}

.pill.attn {
	color: var(--attn);
	background: var(--attn-dim);
	border-color: var(--attn-line);
}

.pill.bad {
	color: var(--bad);
	background: var(--bad-dim);
	border-color: var(--bad-line);
}

/* Off is a decision, not an alarm and not an absence. Red would say something
   went wrong and dashed-grey would say we cannot see it; a machine somebody
   switched off themselves is neither, so it gets ink -- a solid dot on the
   quiet surface, which reads as deliberate. */
.pill.off {
	color: var(--text-muted);
	background: var(--surface);
	border-color: var(--border-strong);
}

.pill.off::before {
	background: var(--text-faint);
}

/* Not a state: "this is you" on the team page is a fact about the reader, and
   green there would claim the row is healthy. */
.pill.you {
	color: var(--text-muted);
	background: var(--surface);
}

.pill.you::before {
	background: var(--accent);
}

/* Unknown gets no colour at all. Green, amber and red each assert something,
   and "we cannot see it" is a fourth thing, not a shade of one of them. */
.pill.unknown {
	color: var(--text-faint);
	border-style: dashed;
}

/* A tag names a KIND, not a state -- an SSH key's algorithm, a plan's
   family. No dot: the dot is the pill saying "this is how something is
   doing", and a kind is not doing anything. */
.pill.tag {
	color: var(--text-muted);
}

.pill.tag::before {
	display: none;
}

.pill.unknown::before {
	background: none;
	border: 1.5px solid currentColor;
}

/* --- Notice --------------------------------------------------------------- */
.notice {
	display: flex;
	gap: var(--s-6);
	align-items: flex-start;
	border: 1px solid var(--border-strong);
	border-left-width: 3px;
	border-radius: var(--radius-control);
	padding: var(--s-6) var(--s-7);
	font-size: var(--t-md);
}

.notice p {
	margin: 0;
	color: var(--text-muted);
}

.notice.ok {
	border-left-color: var(--ok);
	background: var(--ok-dim);
}

.notice.attn {
	border-left-color: var(--attn);
	background: var(--attn-dim);
}

.notice.working {
	border-left-color: var(--working);
	background: var(--working-dim);
}

.notice.bad {
	border-left-color: var(--bad);
	background: var(--bad-dim);
}

/* A statement of fact with nothing to act on. The four kinds above all assert
   a severity; this one deliberately asserts none -- no tint, and the same grey
   rule the unknown pill uses. It exists because the alternative was dressing
   "there is nothing to measure here" in amber, which tells somebody to do
   something about a thing that needs nothing done. */
.notice.quiet {
	border-left-color: var(--border-strong);
	border-left-style: dashed;
	background: transparent;
}

/* --- Card ----------------------------------------------------------------- */
.card {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface-raised);
	padding: var(--s-9);
	box-shadow: var(--lift);
}

.card-detail {
	margin: var(--s-6) 0 0;
	font-size: var(--t-md);
	color: var(--text-muted);
}

a.card {
	display: block;
	text-decoration: none;
	color: inherit;
}

a.card:hover {
	border-color: var(--accent-line);
	background: var(--surface-raised);
	box-shadow: var(--lift-strong);
	transform: translateY(-2px);
}

a.card:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.specs {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: var(--s-3);
}

/* What a plan gives you, read across rather than down. A four-row list of
   label-and-number is the same shape as every other list on the page; four
   cells with the figure set large is the one thing on a plan page somebody
   compares. The 1px gap over a border-coloured ground draws the dividers, so
   the cells can wrap without a stranded edge. */
.specs.boxed {
	grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
	gap: 1px;
	background: var(--border);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
}

.specs.boxed li {
	flex-direction: column;
	align-items: flex-start;
	justify-content: flex-start;
	gap: var(--s-2);
	padding: var(--s-7) var(--s-8);
	background: var(--surface);
}

.specs.boxed span {
	font-size: var(--t-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

.specs.boxed b {
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	color: var(--text);
}

.specs li {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	font-size: var(--t-md);
}

.specs span {
	color: var(--text-faint);
}

.specs b {
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
}

@media (max-width: 620px) {
	.needs-item {
		grid-template-columns: 1fr;
		justify-items: start;
	}

	/* Stacked, the grid would stretch a pill and a button to the full width
	   of the band. A status the width of the screen is not a status. */
	.needs-item .btn {
		justify-self: start;
	}
}

.needs-item .btn {
	justify-self: end;
}

.quiet {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	padding: var(--s-7) var(--s-8);
	font-size: var(--t-md);
	color: var(--text-muted);
}

.quiet b {
	color: var(--text);
}

.tabs {
	display: flex;
	gap: var(--s-1);
	border-bottom: 1px solid var(--border);
	flex-wrap: wrap;
}

/* On a phone six tabs wrapped to two rows, and the strip's rule ran under
   the first row, so the underline of the open tab sat under a tab in the
   other row. One row that scrolls keeps the underline under its word. */
@media (max-width: 600px) {
	.tabs {
		flex-wrap: nowrap;
		overflow-x: auto;
		scrollbar-width: thin;
	}

	.tabs a,
	.tabs button {
		white-space: nowrap;
		flex: 0 0 auto;
	}
}

/* The old tab bar's rules lived here. The new system owns .tabs, and this one
   had a `.tabs span` arm that reached the sliding indicator -- padding it, and
   with border-box turning its 2px height into 18px, so the underline sat over
   the word instead of under it. */

/* A button carries a whole browser's worth of default chrome that an anchor
   does not -- Access and Console are buttons because they have no page of
   their own to link to, and they need to read as the same tab regardless. */
.tabs button {
	background: none;
	border-left: none;
	border-right: none;
	border-top: none;
	font-family: inherit;
	cursor: pointer;
}

.tabs a:hover,
.tabs button:hover {
	color: var(--text);
}

.tabs a.on,
.tabs button.on {
	color: var(--text);
	border-bottom-color: var(--accent);
	font-weight: var(--w-medium);
}

/* A tab with nothing behind it yet says so and cannot be clicked. Hiding it
   would be tidier and would also mean the page silently changes shape the day
   the feature lands. */
.tabs .soon {
	color: var(--text-faint);
	cursor: not-allowed;
}

.tabs .soon::after {
	content: " soon";
	font-size: var(--t-xs);
	opacity: 0.7;
}

.two {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 320px;
	gap: var(--s-9);
	align-items: start;
}

.two > div,
.two > aside {
	display: grid;
	gap: var(--s-7);
	min-width: 0;
	align-content: start;
}

/* Rows inside the rail stack instead of splitting. The two-column row shape
   assumes a panel's full width; in a 320px rail it leaves the label a sliver
   and the prose wraps one word per line. Stacked, the control sits under its
   explanation, left-aligned, which is the shape a narrow card wants. */
.two > aside .row {
	grid-template-columns: 1fr;
	gap: var(--s-5);
}

.two > aside /* A legacy row whose control is the width of the row -- a textarea, a long
   field -- rather than a button parked on the right. The two-column grid is
   right for almost every settings row and wrong for these, and getting it
   wrong pins a textarea into the narrow column. */
.row.full {
	grid-template-columns: 1fr;
	gap: var(--s-4);
}

.row.full .row-control {
	justify-content: stretch;
	display: block;
}

.row.full textarea,
.row.full .input {
	width: 100%;
}

@media (max-width: 860px) {
	.two {
		grid-template-columns: 1fr;
	}
}

.power {
	display: flex;
	gap: var(--s-4);
	flex-wrap: wrap;
	align-items: center;
}

/* The console's place among the buttons: the button, then the word that the
   panel is making the screen, then the link to it. The states swap in the
   one spot, so the row keeps its height and its neighbours hold still. */
.console {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	flex-wrap: wrap;
}

.console-note {
	font-size: var(--t-sm);
	color: var(--faint);
}

.console.opening .console-note {
	animation: console-pulse 1.6s ease-in-out infinite;
}

.console.refused .console-note {
	color: var(--ink);
}

/* A link that is good for a few minutes: the button wears the accent
   while it is, since it is the one thing on the row to press. */
.console.ready .btn {
	border-color: var(--accent-line);
	color: var(--accent-ink);
}

@keyframes console-pulse {
	50% {
		opacity: 0.45;
	}
}

/* --- Activity log ---------------------------------------------------------
   Three columns: what happened, when, and how long it took. Duration is the
   column that answers "is it stuck", which is the question somebody has when
   they open this. */
.log {
	display: grid;
}

.log-row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: var(--s-6);
	align-items: center;
	padding: var(--s-5) 0;
	border-bottom: 1px solid var(--border);
	font-size: var(--t-md);
}

.log-row:last-child {
	border-bottom: none;
}

/* A task that has not finished. The marker is a rule down the left rather than
   a colour on the row, because the pill already carries the colour and two
   colours for one fact is how a page stops having a foreground. */
.log-row.live {
	position: relative;
	padding-left: var(--s-7);
}

.log-row.live::before {
	content: "";
	position: absolute;
	left: 0;
	top: var(--s-3);
	bottom: var(--s-3);
	width: 2px;
	border-radius: 2px;
	background: var(--working);
}

.log-row .what {
	display: flex;
	align-items: baseline;
	gap: var(--s-6);
	flex-wrap: wrap;
	min-width: 0;
}

.log-row .what .pill {
	align-self: center;
}

.log-row time,
.log-row .dur {
	font-size: var(--t-sm);
	color: var(--text-faint);
	font-variant-numeric: tabular-nums;
}

.log-error {
	grid-column: 1 / -1;
	margin: var(--s-2) 0 0;
	font-size: var(--t-sm);
	color: var(--bad);
}

@media (max-width: 860px) {
	.order-row {
		grid-template-columns: 130px minmax(0, 1fr) auto;
	}

	.order-what {
		display: none;
	}
}

@media (max-width: 560px) {
	.log-row,
	.order-row {
		grid-template-columns: 1fr auto;
	}

	.order-when {
		display: none;
	}
}

/* --- Empty states --------------------------------------------------------- */
.empty {
	border: 1px dashed var(--border-strong);
	border-radius: var(--radius);
	background: var(--surface);
	padding: var(--s-12) var(--s-10);
}

.empty-lead {
	margin: 0 0 var(--s-5);
	font-size: var(--t-xl);
	letter-spacing: -0.02em;
	color: var(--text);
	font-weight: var(--w-semi);
}

.empty-body {
	margin: 0 0 var(--s-9);
	color: var(--text-muted);
	font-size: var(--t-md);
	max-width: 54ch;
}

/* --- Catalogue: categories ------------------------------------------------ */
.tiles {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
	gap: var(--s-6);
}

.tile {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface-raised);
	padding: var(--s-9);
	text-decoration: none;
	color: inherit;
	display: flex;
	flex-direction: column;
	gap: var(--s-5);
}

.tile {
	box-shadow: var(--lift);
}

.tile:hover {
	border-color: var(--accent-line);
	box-shadow: var(--lift-strong);
	transform: translateY(-2px);
}

/* The tile is a link, and the price is the thing it is answering. */
.tile:hover .tile-from b {
	color: var(--accent);
}

/* A shelf of plans is a shelf of equal choices; three filled buttons in a row
   is three of them shouting. The card lights up on hover and its button comes
   with it, which is where a choice is actually being made. .accent is exempt
   alongside .primary: the configurator shows ONE card at a time, and its
   button is the page's action, not a shelf voice to hush. */
.plan .btn:not(.primary):not(.accent) {
	color: var(--text-muted);
}

.plan:hover .btn:not(.primary):not(.accent) {
	border-color: var(--accent);
	background: var(--accent-dim);
	color: var(--accent);
}

.tile:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.tile h2 {
	margin: 0;
	font-size: var(--t-lg);
}

.tile-blurb {
	margin: 0;
	font-size: var(--t-md);
	color: var(--text-muted);
}

/* Pinned to the bottom, so the price sits on one line across the row whether
   or not a category has a blurb. */
.tile-foot {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--s-6);
	margin-top: auto;
	padding-top: var(--s-7);
}

.tile-count {
	font-size: var(--t-xs);
	color: var(--text-faint);
}

.tile-from {
	font-size: var(--t-md);
	color: var(--text-muted);
}

.tile-from b {
	color: var(--text);
	font-variant-numeric: tabular-nums;
	font-weight: var(--w-semi);
}

/* --- Catalogue: plans ----------------------------------------------------- */
.plans {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(270px, 100%), 1fr));
	gap: var(--s-6);
}

.plan {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface-raised);
	padding: var(--s-8);
	display: flex;
	flex-direction: column;
	gap: var(--s-6);
	text-decoration: none;
	color: inherit;
}

.plan {
	box-shadow: var(--lift);
}

.plan:hover {
	border-color: var(--accent-line);
	box-shadow: var(--lift-strong);
	transform: translateY(-2px);
}

.plan:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.plan h2 {
	margin: 0;
	font-size: var(--t-lg);
	letter-spacing: -0.02em;
}

.plan-desc {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
	line-height: 1.45;
}

/* margin-top:auto belongs on the price, not on the word "from" inside it --
   aimed at the span, it pinned nothing, and the price and the button landed
   at a different height in every card of the row. */
.plan .price {
	margin: auto 0 0;
	padding-top: var(--s-7);
	border-top: 1px solid var(--border);
	display: flex;
	align-items: baseline;
	gap: var(--s-3);
}

.plan .price .from {
	font-size: var(--t-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-faint);
	font-weight: var(--w-normal);
}

.plan .price b {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	color: var(--text);
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}

.plan .price > span:last-child {
	font-size: var(--t-sm);
	color: var(--text-faint);
	font-weight: var(--w-normal);
}

.plan .specs {
	gap: var(--s-4);
}


/* --- The shop's configure page -------------------------------------------
   One page per kind of thing we sell: the lines across the top, the plans
   in the chosen line as sizes or stock rows, then what is left to decide
   beside a summary that re-prices as the choices change and carries the
   one button. Tiles are the portal's own surfaces and lines; a chosen tile
   wears the accent as a ring, a chosen chip wears it as a fill. */
/* The form takes the width a form reads well at and no more: 980px holds
   five size tiles in a row, and past that a pair of line cards or a strip
   of four facts only spread out. The summary sits beside it rather than
   at the far edge of a wide screen, so the two read as one thing. */
.cfg {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 340px;
	gap: var(--s-10);
	align-items: start;
}
/* The whole page stops at 1440px and sits in the middle of anything
   wider, so its title, its form and its summary move together; the form
   alone capped left a hole beside the summary. */
.canvas:has(> .cfg) {
	max-width: 1440px;
	margin-inline: auto;
}
.cfg-main {
	min-width: 0;
}
.cfg-q {
	margin: var(--s-11) 0 var(--s-6);
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.015em;
	color: var(--ink);
}
.cfg-main > .cfg-q:first-child {
	margin-top: 0;
}
.cfg-lines + .cfg-q,
.cfg-line-blurb + .cfg-q {
	margin-top: var(--s-11);
}
.cfg-q small {
	font-size: var(--t-md);
	font-weight: var(--w-normal);
	color: var(--faint);
	margin-left: var(--s-5);
}
/* The product lines are a toggle: a row of pills, the name and its lowest
   price, with the chosen one's sentence under the row. */
.cfg-lines {
	display: inline-flex;
	flex-wrap: wrap;
	gap: var(--s-2);
	padding: var(--s-2);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--sunk);
	max-width: 100%;
}
.cfg-line-blurb {
	margin-top: var(--s-4);
	font-size: var(--t-md);
	color: var(--body);
}
.cfg-lines .cfg-line {
	flex-direction: row;
	align-items: baseline;
	gap: var(--s-4);
	padding: var(--s-3) var(--s-7);
	border-color: transparent;
	background: none;
	border-radius: var(--r-sm);
}
.cfg-lines .cfg-line.on {
	background: var(--surface);
	border-color: var(--accent);
	box-shadow: none;
}
.cfg-line,
.cfg-size {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	padding: var(--s-7) var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	color: var(--ink);
	text-decoration: none;
	cursor: pointer;
	position: relative;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}
.cfg-line:hover,
.cfg-size:hover {
	border-color: var(--border-strong);
	background: var(--raised);
}
.cfg-line.on,
.cfg-size.on {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}
/* A size billing counted nothing behind: drawn faint, named sold out, and
   never the accent, so the eye lands on a size that can be bought. */
.cfg-size.sold,
.cfg-size.sold b,
.cfg-size.sold span {
	color: var(--faint);
}
.cfg-size.sold:hover {
	border-color: var(--line);
}
.cfg-sold {
	display: block;
	font-size: var(--t-sm);
	text-transform: uppercase;
	letter-spacing: 0.04em;
}
.cfg-line:focus-visible,
.cfg-size:focus-visible,
.cfg-size:focus-within,
.cfg-term:focus-within,
.cfg-osf:focus-within {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}
.cfg-line b {
	font-size: var(--t-base);
	font-weight: var(--w-medium);
}
.cfg-line em {
	font-style: normal;
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}
.cfg-sizes {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(184px, 100%), 1fr));
	gap: var(--s-4);
}
.cfg-size b {
	font-size: var(--t-lg);
	font-weight: var(--w-medium);
	letter-spacing: -0.02em;
}
/* The second line, "4 vCPU · £10.00/mo", is the fact people compare the
   tiles by, so it is set at the control size in the text colour rather
   than as a faint note; the tile is wide enough for it on one line. */
.cfg-size span {
	font-size: var(--t-md);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}
/* The chosen size's facts: a strip as wide as the tiles above it, the
   facts packed to its left at a reading distance from each other rather
   than spread to its corners, which on a wide screen put a hand's width
   between a label and the next. */
.cfg-facts {
	display: grid;
	/* auto-fit, not auto-fill: a row of five facts in a width that takes
	   six drew a sixth, empty box at the end (David, 2026-11-02); fit
	   collapses the track nothing fills and the five share the width. */
	grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
	margin-top: var(--s-4);
	background: var(--sunk);
	border: 1px solid var(--line);
	border-radius: var(--r);
	overflow: hidden;
}
/* Each cell draws the hairline to its right and below it; the box clips
   the outer ones, so a short last row leaves sunk surface, not a hole. */
.cfg-facts div {
	padding: var(--s-7) var(--s-8);
	box-shadow: 1px 1px 0 0 var(--line);
}
.cfg-facts span {
	display: block;
	font-size: var(--t-sm);
	color: var(--faint);
	margin-bottom: var(--s-2);
}
.cfg-facts b {
	font-size: var(--t-base);
	font-weight: var(--w-medium);
}
/* Beside the summary the strip only repeats it, so it shows where the
   summary has gone to the foot of the page (David, 2026-11-03). */
@media (min-width: 901px) {
	.cfg-facts {
		display: none;
	}
}
.cfg-terms {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
	gap: var(--s-4);
}
.cfg-term {
	display: flex;
	flex-direction: column;
	gap: var(--s-3);
	padding: var(--s-7) var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	cursor: pointer;
	position: relative;
}
.cfg-term:hover {
	border-color: var(--border-strong);
}
.cfg-term.on {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}
.cfg-term .l {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
}
.cfg-term .p {
	font-size: 21px;
	font-weight: var(--w-medium);
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}
.cfg-term .p small {
	font-size: var(--t-sm);
	font-weight: var(--w-normal);
	color: var(--faint);
	margin-left: var(--s-2);
	letter-spacing: 0;
}
.cfg-term .s {
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
	line-height: 1.5;
}
.cfg-term .s i {
	font-style: normal;
	display: inline-block;
	margin-left: var(--s-3);
	padding: 1px var(--s-4);
	border-radius: var(--radius-pill);
	background: var(--accent-soft);
	color: var(--accent-ink);
	font-weight: var(--w-medium);
}
.cfg-term.on .s i {
	background: var(--accent-fill);
	color: #fff;
}
.cfg-field {
	display: grid;
	gap: var(--s-4);
	max-width: 520px;
}
.cfg-stock {
	overflow-x: auto;
}
.cfg-stock table {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0 var(--s-3);
}
.cfg-stock th {
	text-align: left;
	font-family: inherit;
	font-size: var(--t-xs);
	font-weight: var(--w-medium);
	letter-spacing: 0;
	text-transform: none;
	color: var(--faint);
	padding: 0 var(--s-7) var(--s-2);
	white-space: nowrap;
}
.cfg-stock .pill {
	font-family: inherit;
	font-size: 12px;
	letter-spacing: 0;
	text-transform: none;
	padding: 2px var(--s-5);
}
.cfg-stock td {
	padding: var(--s-7);
	font-size: var(--t-md);
	color: var(--body);
	background: var(--surface);
	border-top: 1px solid var(--line);
	border-bottom: 1px solid var(--line);
	white-space: nowrap;
}
.cfg-stock td:first-child {
	border-left: 1px solid var(--line);
	border-radius: var(--r) 0 0 var(--r);
}
.cfg-stock td:last-child {
	border-right: 1px solid var(--line);
	border-radius: 0 var(--r) var(--r) 0;
}
.cfg-stock td.cpu a {
	color: var(--ink);
	font-weight: var(--w-medium);
	text-decoration: none;
}
.cfg-stock td.cpu a::after {
	content: "";
	position: absolute;
	inset: 0;
}
.cfg-stock tr {
	position: relative;
}
.cfg-stock tr:hover td {
	border-color: var(--border-strong);
}
.cfg-stock tr.on td {
	border-color: var(--accent);
	background: var(--accent-soft);
}
/* A sold-out row stays in the table, so the line still says what it is
   made of, but it is quieter and nothing about it answers a pointer. */
.cfg-stock tr.sold td,
.cfg-stock tr.sold td.cpu b,
.cfg-stock tr.sold td.num b {
	color: var(--faint);
}
.cfg-stock tr.sold:hover td {
	border-color: var(--line);
}
.cfg-stock td.num,
.cfg-stock th.num {
	text-align: right;
	font-variant-numeric: tabular-nums;
}
.cfg-stock td.num {
	font-family: inherit;
	font-size: var(--t-md);
}
.cfg-stock td.num b {
	color: var(--ink);
	font-size: var(--t-base);
	font-weight: var(--w-medium);
}
/* On a phone a stock row is a card: the processor and the price on the
   first line, the facts wrapping under them, the badge last. The row is a
   wrapping flex line and its ::before is the line break after the price,
   so the table's cells land in order without a wrapper. */
@media (max-width: 900px) {
	.cfg-stock table,
	.cfg-stock tbody {
		display: block;
		min-width: 0;
	}
	.cfg-stock thead {
		display: none;
	}
	.cfg-stock tr {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		column-gap: var(--s-4);
		row-gap: var(--s-2);
		margin-bottom: var(--s-3);
		padding: var(--s-7) var(--s-7) var(--s-6);
		border: 1px solid var(--line);
		border-radius: var(--r);
		background: var(--surface);
	}
	.cfg-stock tr::before {
		content: "";
		flex-basis: 100%;
		height: 0;
		order: 1;
	}
	.cfg-stock tr.on {
		border-color: var(--accent);
		background: var(--accent-soft);
	}
	.cfg-stock td,
	.cfg-stock td:first-child,
	.cfg-stock td:last-child {
		display: block;
		min-width: 0;
		padding: 0;
		border: 0;
		border-radius: 0;
		background: transparent;
		white-space: normal;
	}
	.cfg-stock td.cpu {
		flex: 1 1 auto;
	font-size: var(--t-base);
	}
	.cfg-stock td.num {
		margin-left: auto;
	}
	.cfg-stock td.mem,
	.cfg-stock td.sto,
	.cfg-stock td.loc {
		order: 2;
	}
	.cfg-stock td.sto::before,
	.cfg-stock td.loc::before {
		content: "·";
		color: var(--faint);
		margin-right: var(--s-4);
	}
	.cfg-stock td.avail {
		order: 3;
		flex-basis: 100%;
		margin-top: var(--s-2);
	}
	.cfg-stock td.avail:empty {
		display: none;
	}
}
.cfg-sum {
	position: sticky;
	top: 70px;
}
/* The same card where the sign-in detour comes back: a summary, not a
   sidebar, so it neither sticks nor stretches to the page's width. */
.continue-card {
	position: static;
	max-width: 520px;
}
.continue-card .btnrow {
	margin-bottom: var(--s-4);
}
.cfg-name {
	margin: 0 0 var(--s-1);
	font-size: var(--t-lg);
	font-weight: var(--w-medium);
	letter-spacing: -0.02em;
}
.cfg-where {
	margin: 0 0 var(--s-7);
	font-size: var(--t-md);
	color: var(--faint);
}
.cfg-rows .sr {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	font-size: var(--t-md);
	padding: 7px 0;
	border-bottom: 1px solid var(--hair);
}
.cfg-rows .sr span {
	color: var(--faint);
}
.cfg-rows .sr b {
	color: var(--ink);
	font-weight: var(--w-medium);
	text-align: right;
}
.cfg-sum .tot {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	padding-top: var(--s-7);
	margin: var(--s-3) 0 var(--s-7);
}
.cfg-sum .tot .v {
	font-size: 26px;
	color: var(--ink);
	letter-spacing: -0.03em;
	font-variant-numeric: tabular-nums;
	font-weight: var(--w-medium);
}
.cfg-sum .tot .v small {
	font-size: var(--t-sm);
	color: var(--faint);
	font-weight: var(--w-normal);
	letter-spacing: 0;
	margin-left: var(--s-3);
}
.ask-avail {
	display: flex;
}
.cfg-bar {
	display: none;
}
@media (max-width: 900px) {
	.cfg {
		grid-template-columns: minmax(0, 1fr);
	}
	.cfg-sum {
		position: static;
	}
	.canvas:has(.cfg-bar) {
		padding-bottom: 120px;
	}
	.cfg-bar {
		display: flex;
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 40;
		align-items: center;
		gap: var(--s-7);
		padding: var(--s-6) var(--s-8) calc(var(--s-6) + env(safe-area-inset-bottom, 0px));
		background: var(--surface);
		border-top: 1px solid var(--line);
		box-shadow: var(--sh-3);
	}
	.cfg-bar .v {
		font-size: 19px;
		font-weight: var(--w-medium);
		letter-spacing: -0.02em;
		font-variant-numeric: tabular-nums;
		min-width: 0;
	}
	.cfg-bar .v small {
		display: block;
	font-size: var(--t-sm);
		color: var(--faint);
		font-weight: var(--w-normal);
		letter-spacing: 0;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}
	.cfg-bar .btn {
		margin-left: auto;
		flex: none;
	}
	.cfg-sizes {
		grid-template-columns: repeat(auto-fill, minmax(min(110px, 100%), 1fr));
	}
}

/* --- Configure page ------------------------------------------------------- */
.configure {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 320px;
	gap: var(--s-11);
	align-items: start;
}

/* The left column stacks a header and several sections; without this they sit
   flush against one another and read as one long box. */
.configure > div {
	display: grid;
	gap: var(--s-11);
	align-content: start;
	min-width: 0;
}

@media (max-width: 860px) {
	.configure {
		grid-template-columns: 1fr;
	}
}

.field {
	display: grid;
	gap: var(--s-4);
	margin-bottom: var(--s-9);
}

.field > label,
.field > .section-head {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--text);
	margin: 0;
	font-family: var(--ui);
}

/* An input with no type is a text input to the browser and must be one to the
   page too: matching only the spelled-out type left every code box that
   forgot it as the platform's bare white strip. */
.field input:not([type]),
.field input[type="text"] {
	font: inherit;
	font-size: var(--t-md);
	width: 100%;
	padding: 11px var(--s-7);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-control);
	background: var(--surface);
	color: var(--text);
}

.field input:not([type]):hover,
.field input[type="text"]:hover {
	border-color: var(--text-faint);
}

.field input:not([type]):focus-visible,
.field input[type="text"]:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

.hint {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
}

/* --- Term selector -------------------------------------------------------- */
/* One grouped control rather than four floating boxes: the options are a
   single question, and four separate cards is the shape that made this page
   read as a stack of blocks. Borders collapse into shared hairlines. */
.term-list,
.opt-list,
.os-versions {
	display: grid;
	gap: 0;
}

.term-opt,
.opt {
	border-radius: 0;
	margin-top: -1px;
}

.term-opt:first-child,
.opt:first-child {
	margin-top: 0;
	border-radius: var(--radius) var(--radius) 0 0;
}

.term-opt:last-child,
.opt:last-child {
	border-radius: 0 0 var(--radius) var(--radius);
}

/* A selected row owns its whole outline, including the edges it shares. */
.term-opt.on,
.opt.on,
.term-opt:hover,
.opt:hover {
	position: relative;
	z-index: 1;
}

/* The radio is hidden, so the row has to draw one. Without it the only sign
   of which term is selected is a border, and a border is what hover does. */
.term-opt::before,
.opt::before {
	content: "";
	grid-row: 1 / -1;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	border: 1px solid var(--border-strong);
	background: var(--ground);
	flex: none;
}

.term-opt:hover,
.opt:hover {
	border-color: var(--border-strong);
	background: var(--surface-raised);
}

.term-opt.on,
.opt.on {
	border-color: var(--accent-line);
	background: var(--accent-dim);
}

.term-opt.on::before,
.opt.on::before {
	border-color: var(--accent);
	background: var(--accent);
	box-shadow: inset 0 0 0 3px var(--accent-dim);
}

.term-opt input,
.opt input,
.os-ver input {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

/* Focus has to remain visible with the control itself hidden, or the form
   becomes unusable by keyboard. */
.term-opt:focus-within,
.opt:focus-within,
.os-ver:focus-within {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.term-opt.on .term-price {
	color: var(--accent);
}

/* --- OS selector ---------------------------------------------------------- */
.os-families {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(116px, 100%), 1fr));
	gap: var(--s-4);
}

.os-fam {
	font: inherit;
	cursor: pointer;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--s-4);
	padding: var(--s-7) var(--s-4) var(--s-6);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius);
	background: var(--surface);
	color: var(--text);
}

.os-fam:hover {
	background: var(--surface-raised);
	border-color: var(--text-faint);
}

.os-fam:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.os-fam.on {
	border-color: var(--accent);
	background: var(--accent-dim);
	box-shadow: inset 0 0 0 1px var(--accent-line);
}

.os-fam.on b {
	color: var(--accent);
}

.os-fam b {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
}

.os-fam em {
	font-style: normal;
	font-size: var(--t-xs);
	color: var(--text-faint);
}

.oslogo {
	width: 32px;
	height: 32px;
	display: block;
	object-fit: contain;
}

.os-versions {
	margin-top: var(--s-7);
}

.opt {
	display: flex;
	align-items: center;
	gap: var(--s-7);
	cursor: pointer;
	border: 1px solid var(--border);
	padding: var(--s-6) var(--s-8);
	background: var(--surface);
	font-size: var(--t-base);
}

/* --- Checkout ------------------------------------------------------------- */
.lines {
	display: grid;
	gap: var(--s-5);
}

.line {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: var(--s-9);
	background: var(--surface-raised);
	display: grid;
	gap: var(--s-7);
	box-shadow: var(--lift);
}

.line-head {
	display: flex;
	justify-content: space-between;
	align-items: flex-start;
	gap: var(--s-8);
}

.line-head h2 {
	margin: 0;
	font-size: var(--t-lg);
	letter-spacing: -0.02em;
}

.line-term {
	margin: var(--s-1) 0 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
}

.line-price {
	margin: 0;
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* A sold-out line's price is still in billing's total but is not being
   bought: struck, and quiet, so the column is not read as the order. */
.line-price.gone {
	text-decoration: line-through;
	color: var(--text-faint);
}

.line-actions {
	display: flex;
	gap: var(--s-9);
	align-items: baseline;
	padding-top: var(--s-6);
	border-top: 1px solid var(--border);
}

/* The no-key choice at the till: the box sits on the first line of its
   sentence, and the sentence wraps under itself, not under the box. */
.key-check {
	display: flex;
	gap: var(--s-4);
	align-items: flex-start;
	font-size: var(--t-sm);
	color: var(--body);
}
.key-check input {
	margin-top: 2px;
	flex: none;
}

.line-edit,
.line-remove {
	font: inherit;
	font-size: var(--t-md);
	/* The ink shade: plain --accent is 2.86:1 on the light ground. */
	color: var(--accent-ink);
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	text-decoration: none;
}

.line-remove {
	color: var(--text-muted);
}

.line-edit:hover,
.line-remove:hover {
	color: var(--accent-strong);
}

.summary {
	border: 1px solid var(--border-strong);
	border-top: 2px solid var(--accent);
	border-radius: var(--radius);
	padding: var(--s-9);
	background: var(--surface-raised);
	display: grid;
	gap: var(--s-6);
	box-shadow: var(--lift-strong);
	position: sticky;
	top: calc(var(--bar) + var(--s-9));
}

.summary h2 {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	margin: 0;
	font-size: var(--t-xs);
	color: var(--text-faint);
	font-weight: var(--w-medium);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	white-space: nowrap;
}

.summary h2::after {
	content: "";
	flex: 1;
	height: 1px;
	background: var(--border);
}

.sum-total {
	margin: 0;
	display: flex;
	align-items: baseline;
	gap: var(--s-3);
}

.sum-total b {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	letter-spacing: -0.03em;
	font-variant-numeric: tabular-nums;
}

.sum-total span {
	font-size: var(--t-sm);
	color: var(--text-faint);
}

/* The coupon row in the summary: one line, box-and-button when empty, the
   promise-and-remove when a code is on. Muted like the fineprint -- it is a
   fact about the bill, not a call to action. */
.coupon-row {
	display: flex;
	align-items: center;
	gap: var(--s-4);
}

.coupon-box {
	flex: 1;
	min-width: 0;
	padding: var(--s-3) var(--s-4);
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
	color: var(--ink);
	font: inherit;
	font-size: var(--t-sm);
}

.coupon-box:focus {
	outline: none;
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.coupon-on {
	flex: 1;
	font-size: var(--t-sm);
	color: var(--body);
}

.coupon-on code {
	font-family: var(--mono);
	color: var(--ink);
}

/* The till's key box: a public key is a long word, so the row stacks
   where the coupon's stays inline. */
.key-row {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--s-3);
}

.key-note {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--body);
}

.key-box {
	width: 100%;
	padding: var(--s-3) var(--s-4);
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
	color: var(--ink);
	font-family: var(--mono);
	font-size: var(--t-sm);
	resize: vertical;
}

/* A refused key, said beside the box rather than over the page. */
.key-why {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--bad);
}

.key-box[aria-invalid="true"] {
	border-color: var(--bad);
}

.key-box:focus {
	outline: none;
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.sum-renew {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-muted);
}

.fineprint {
	margin: 0;
	font-size: var(--t-xs);
	color: var(--text-faint);
	line-height: 1.5;
}

/* The two doors to PayPal (ADR 0073), stacked with the summary's own rhythm
   rather than touching. */
.pay-doors {
	display: grid;
	gap: var(--s-4);
}

/* The card fields (ADR 0073): PayPal's own inputs, drawn in iframes inside
   these boxes. The boxes are only places; PayPal's iframes carry the look,
   passed to them from these same tokens by cardfields.js.

   The live half shows from the first paint, its boxes drawn as the fields
   they are about to be, so the page does not open as one button and then
   rebuild itself as a form when the SDK arrives. It goes, and the fallback
   -- PayPal's own card form -- takes its place, the moment anything says
   the fields are not coming: no SDK, no eligibility, a refusal (cf-off,
   from cardfields.js), or no script at all (a noscript rule in the
   markup). A page of boxes nobody can type into is still worse than one
   button that works; it is just no longer the page's first guess. */
.cardpay {
	display: grid;
	gap: var(--s-6);
}

/* In the invoice's summary the fields are one more section of it, ruled
   off like the rest. */
.summary .cardpay {
	padding-top: var(--s-6);
	border-top: 1px solid var(--border);
}

/* On a page of their own they keep a form's width, the one the sign-in
   and the settings forms keep: a card number box as wide as a 2560 screen
   reads as a text area. */
.card .cardpay {
	max-width: 520px;
}

.cf-title {
	margin: 0;
	font-size: var(--t-sm);
	font-weight: var(--w-semi);
	color: var(--ink);
}

.cardpay .cf-live {
	display: grid;
	gap: var(--s-6);
}

.cardpay.cf-off .cf-live {
	display: none;
}

.cf-fallback {
	display: none;
	gap: var(--s-4);
}

.cardpay.cf-off .cf-fallback {
	display: grid;
}

/* Once the fields are on (cf-ready, from cardfields.js, or the preview's
   stand-in) the fallback is not coming back, whatever else is said. */
.cardpay.cf-ready .cf-fallback {
	display: none;
}

.cardpay.cf-busy .cf-fields {
	opacity: 0.6;
}

.cf-fields {
	display: grid;
	gap: var(--s-5);
}

.cf-field {
	display: grid;
	gap: var(--s-2);
	min-width: 0;
}

.cf-label {
	font-size: var(--t-xs);
	font-weight: var(--w-medium);
	color: var(--text-muted);
}

/* Expiry and code side by side: two short values, and a phone has room
   for both at 390. */
.cf-pair {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	gap: var(--s-5);
}

/* The box is the field's place while PayPal is still drawing it. The SDK's
   own placeholder is turned off (cardfields.js): a white pulse in colours of
   PayPal's choosing, which on a dark page is a white box. This one is drawn
   from the page's tokens, under the iframe, the shape the input will take,
   and goes the moment the field is on. The box's height is the iframe's
   settled height, so the page does not jump while the frame grows from the
   SDK's 30px opening size. */
/* The box one of PayPal's fields is drawn into. Its frame insets the
   input six pixels on every side and lands 58px tall, so the box pulls
   itself out by those six and the input sits exactly on the box's own
   rect, level with the button under it. Flex, because the SDK's wrapper is
   an inline-block: left on a line of its own it carried the line's
   descender under it, and every row grew six pixels as its field arrived,
   which is the "second load" the page used to do. */
.cf-box {
	position: relative;
	isolation: isolate;
	display: flex;
	min-width: 0;
	min-height: 58px;
	margin: -6px;
}
/* The input, drawn by the stylesheet at the exact geometry and in the exact
   colours PayPal's will have, and kept there under it: the iframe fades in
   over this and nothing is swapped, so there is no moment the box is empty
   and no moment it changes shape. */
.cf-box.cf-under {
	cursor: text;
}
/* While the field cannot take a press (cardfields.js, cf-wait), the box
   says so: a skeleton bar where the words will be, the pointer reading
   "busy", and the frame catching no press, so one falls through to the
   box, which keeps it and hands it to the field as focus once drawn. A
   field drawn ready a second early took presses and typing that went
   nowhere, which is worse than a second of honest waiting. */
.cf-box.cf-wait {
	cursor: progress;
}
.cf-box.cf-wait iframe {
	pointer-events: none;
}
.cf-box.cf-under::before {
	content: "";
	position: absolute;
	z-index: -1;
	top: 6px;
	left: 6px;
	right: 6px;
	height: 46px;
	box-sizing: border-box;
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
}
.cf-box.cf-wait::after {
	content: "";
	position: absolute;
	z-index: -1;
	top: 22px;
	left: 19px;
	width: 42%;
	max-width: 180px;
	height: 14px;
	border-radius: 7px;
	background: linear-gradient(90deg,
		color-mix(in srgb, var(--ink) 8%, transparent) 0%,
		color-mix(in srgb, var(--ink) 15%, transparent) 50%,
		color-mix(in srgb, var(--ink) 8%, transparent) 100%);
	background-size: 200% 100%;
	animation: cf-sheen 1.4s ease-in-out infinite;
	pointer-events: none;
}
@keyframes cf-sheen {
	to {
		background-position: -200% 0;
	}
}
@media (prefers-reduced-motion: reduce) {
	.cf-box.cf-wait::after {
		animation: none;
	}
}
.cf-standin {
	display: flex;
	flex: 1 1 auto;
	align-items: center;
	height: 46px;
	margin: 6px;
	padding: 0 var(--s-6);
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
	color: var(--faint);
	font-family: var(--mono);
	font-size: var(--t-sm);
	white-space: nowrap;
	overflow: hidden;
}

.cf-save span {
	font-size: var(--t-sm);
}

/* --- Auth ----------------------------------------------------------------- */
.centre {
	display: grid;
	place-items: center;
	padding: var(--s-12) var(--s-10);
}

/* The sign-in card (portal review, October 2026): 410 wide, body at 15,
   inputs 44 tall, 12px radius and 20px padding like every other card. */
.auth {
	width: 100%;
	max-width: 410px;
	border: 1px solid var(--border);
	border-radius: var(--r-lg);
	background: var(--surface-raised);
	padding: var(--s-9);
	display: grid;
	gap: var(--s-6);
}

.auth h1 {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.01em;
}

.auth .sub {
	margin: 0;
	font-size: var(--t-base);
	color: var(--text-muted);
}

.auth label {
	display: block;
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	margin-bottom: var(--s-3);
}

.auth input {
	font: inherit;
	font-size: var(--t-md);
	line-height: var(--s-9);
	width: 100%;
	padding: 11px var(--s-6);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-control);
	background: var(--surface);
	color: var(--text);
}

.auth input:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

.alt {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
	text-align: center;
}

.alt a {
	color: var(--text-muted);
}

.right {
	display: flex;
	align-items: center;
	gap: var(--s-5);
}

.sub {
	color: var(--text-muted);
}

.price {
	font-variant-numeric: tabular-nums;
	font-weight: var(--w-semi);
}

/* A labelled divider between two ways of doing the same thing. */
.or {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	color: var(--text-faint);
	font-size: var(--t-xs);
}

.or::before,
.or::after {
	content: "";
	height: 1px;
	background: var(--border);
	flex: 1;
}

.card .line-sub:first-child {
	margin-top: 0;
}

/* Prose in a card is nearly always followed by the control it is explaining,
   and the two need air between them or the label reads as the last line of
   the paragraph. */
.card > p + form,
.card > p + .actions,
.card > form + .actions {
	margin-top: var(--s-8);
}

.stack .field {
	margin-bottom: 0;
}

/* Controls that sit on the same line as the row they act on. */
.inline {
	display: flex;
	gap: var(--s-4);
	align-items: center;
	flex-wrap: wrap;
	font-size: var(--t-sm);
}

/* --- Form controls not previously styled ---------------------------------- */
.field label {
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	color: var(--text);
}

.field input[type="email"],
.field input[type="password"],
.field textarea,
.field select,
select {
	font: inherit;
	font-size: var(--t-md);
	width: 100%;
	padding: var(--s-4) var(--s-6);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-control);
	/* background-color, not the background shorthand: the shorthand resets
	   background-image, and this selector is more specific than the one below
	   that draws the select's arrow -- so every select inside a field lost it
	   and looked like a text box that could not be opened. */
	background-color: var(--surface);
	color: var(--text);
}

/* The platform's own select is a light-mode control with a light-mode arrow,
   and it is the one thing on these pages that gives away that nobody drew it.
   The options list still belongs to the browser; only the closed control is
   ours. */
select {
	appearance: none;
	background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='%2399a1af' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.6 6 6.4l5-4.8'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right var(--s-6) center;
	padding-right: var(--s-11);
	cursor: pointer;
}

select:hover {
	border-color: var(--text-faint);
}

.inline select {
	width: auto;
	min-width: 140px;
}

.field textarea {
	font-family: var(--mono);
	font-size: var(--t-sm);
	line-height: 1.5;
	resize: vertical;
	min-height: 96px;
}

.field input[type="email"]:focus-visible,
.field input[type="password"]:focus-visible,
.field textarea:focus-visible,
select:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* --- Charts ---------------------------------------------------------------
   Data is drawn in ink. Every line here is --text or --text-muted whatever it
   says; the single exception is a disk over 90%, and that alarm is repeated as
   a sentence in the caption so colour is never the only carrier.

   Gridlines are solid hairlines. A dashed gridline reads as a threshold or a
   projection when it is neither -- the one dashed thing on these plots is the
   marker under a stretch with no data, where "dotted" means "nothing here",
   which is what it looks like. */
.charts {
	display: grid;
	gap: var(--s-9);
}

.windows a.on {
	position: relative;
	z-index: 1;
	background: var(--accent-dim);
	border-color: var(--accent-line);
	color: var(--accent);
}

.chart-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr));
	gap: var(--s-6);
	/* Charts size to their own content. Stretching them to the tallest in the
	   row leaves a panel of empty ground under the shortest, which reads as a
	   chart that failed to draw. */
	align-items: start;
}

/* Alpine has not started yet. Without this, x-show cannot hide anything whose
   display the stylesheet does not set. */
[x-cloak] {
	display: none !important;
}

.chart {
	margin: 0;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	padding: var(--s-8);
	box-shadow: var(--lift);
	min-width: 0;
}

.chart-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--s-6);
	flex-wrap: wrap;
	margin-bottom: var(--s-7);
}

.chart-head b {
	font-size: var(--t-md);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-faint);
	font-weight: var(--w-medium);
}

/* The two numbers somebody came for, in text. A reader who never sees the line
   loses nothing. */
.chart-now {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}

.chart-now em {
	font-style: normal;
	font-size: var(--t-sm);
	font-weight: var(--w-normal);
	color: var(--text-faint);
	margin-left: var(--s-5);
}

.chart.alarm .chart-now {
	color: var(--attn);
}

.chart-empty {
	margin: 0;
	font-size: var(--t-md);
	color: var(--text-muted);
}

.chart-plot {
	position: relative;
}

.chart-plot svg {
	display: block;
	width: 100%;
	height: auto;
	overflow: visible;
}

.chart-rule {
	stroke: var(--border);
	stroke-width: 1;
}

.tick {
	font-size: 12px;
	fill: var(--text-faint);
	font-variant-numeric: tabular-nums;
}

/* Nothing was measured here. Dotted, and along the floor, so that "no line"
   cannot be read as "the chart ends". */
.gap {
	stroke: var(--border-strong);
	stroke-width: 2;
	stroke-dasharray: 2 3;
	stroke-linecap: round;
}

.ink {
	fill: none;
	stroke: var(--text);
	stroke-width: 1.8;
	stroke-linejoin: round;
	stroke-linecap: round;
}

/* The second series has no colour to be told apart by, so it is dashed, and
   labelled at its end, and in the legend. Never the dash alone. */
.ink.dashed {
	stroke: var(--text-muted);
	stroke-width: 1.5;
	stroke-dasharray: 5 3;
}

.ink-dot {
	fill: var(--text);
	stroke: none;
}

.chart.alarm .ink {
	stroke: var(--attn);
}

.chart.alarm .ink-dot {
	fill: var(--attn);
}

.series-label {
	font-size: 12px;
	fill: var(--text-faint);
}

.crosshair {
	stroke: var(--border-strong);
	stroke-width: 1;
	pointer-events: none;
}

.chart-tip {
	position: absolute;
	top: 0;
	transform: translateX(-50%);
	pointer-events: none;
	white-space: nowrap;
	font-size: var(--t-xs);
	color: var(--text);
	background: var(--surface-raised);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius);
	padding: var(--s-2) var(--s-5);
	box-shadow: var(--lift-strong);
}

.chart-note {
	margin: var(--s-6) 0 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
	line-height: 1.5;
}

.chart.alarm .chart-note {
	color: var(--text-muted);
}

.chart-legend {
	list-style: none;
	margin: var(--s-5) 0 0;
	padding: 0;
	display: flex;
	gap: var(--s-8);
	font-size: var(--t-xs);
	color: var(--text-faint);
}

.chart-legend li {
	display: flex;
	align-items: center;
	gap: var(--s-4);
}

.legend-mark {
	width: 18px;
	height: 0;
	border-top: 2px solid var(--text);
}

.legend-mark.dashed {
	border-top-style: dashed;
	border-color: var(--text-muted);
}

@media (max-width: 860px) {
	.sub-row {
		grid-template-columns: 1fr;
		justify-items: start;
	}
}

/* --- Payment attempts -----------------------------------------------------
   What was tried and did not work. On the invoice rather than in an email,
   because the customer looking at an unpaid invoice is the one wondering
   whether they imagined paying it. */
/* A swap target that is empty must take no room. It exists so an action
   elsewhere on the page has somewhere to put its answer, and an empty div
   still earning a grid gap leaves a hole nobody can see the cause of. */
#machine-notice:empty {
	display: none;
}

.attempts {
	margin-top: var(--s-9);
}

.attempt {
	display: flex;
	align-items: center;
	gap: var(--s-7);
	padding: var(--s-5) 0;
	border-bottom: 1px solid var(--border);
	font-size: var(--t-md);
	color: var(--text-muted);
}

.attempt-who {
	margin-left: auto;
	font-size: var(--t-sm);
	color: var(--text-faint);
}

.attempts .hint {
	margin-top: var(--s-6);
}

/* A table is not a form. It has as many columns as it has facts, and cramping
   them into a form's measure is what makes a table look like a list that went
   wrong. */
.panel.wide {
	max-width: none;
}

/* The one place a colour names a section rather than a state. It is the
   heading and the button that go red, not a red box round everything -- a
   panel that shouts before you have chosen anything is a panel people learn to
   scroll past. */
.panel.danger .panel-head h2 {
	color: var(--bad);
}

/* One setting: what it is and what it does on the left, the control on the
   right, capped so a text field does not stretch to the width of a monitor. */
.row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
	align-items: start;
	gap: var(--s-6) var(--s-11);
	padding: var(--s-7) var(--s-8);
}

.row-label {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--text);
}

.row-help {
	margin: var(--s-3) 0 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
	font-weight: var(--w-normal);
	line-height: 1.5;
	/* A measure, because a full-width panel would otherwise run a sentence to
	   1100px and nobody's eye finds the next line. */
	max-width: 68ch;
}

.row-control .field {
	margin-bottom: 0;
}

/* A row that is only prose -- a setting we have not built, said plainly rather
   than left out. */
/* A row carrying a warning rather than a failure. The pill says which; the
   ground says it is worth stopping on. */
.row.warn {
	background: var(--attn-soft);
}

.row.said {
	grid-template-columns: 1fr;
}

/* A control too wide for the right-hand column: a pasted key, a script, an
   address list. Label and help on top, control across the full width. */
.row.full {
	grid-template-columns: 1fr;
	gap: var(--s-6);
}

.row.full .row-control {
	justify-self: stretch;
	max-width: none;
}

/* Inline code inside help text: a filename, a path, a command. */
.row-help code {
	font-family: var(--mono);
	font-size: var(--t-sm);
	color: var(--text-muted);
	background: var(--ground);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0 var(--s-3);
}

/* The password-reset gate: the words you type sit right beside the button
   they unlock, so there is nothing to scan for -- see it, type it, press it. */
.confirm-type {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	gap: var(--s-4);
}

.confirm-type .field {
	margin-bottom: 0;
}

.confirm-type input {
	width: 220px;
}

@media (max-width: 720px) {
	.row {
		grid-template-columns: 1fr;
	}

	.row-control {
		justify-self: stretch;
	}
}

/* --- Touch targets --------------------------------------------------------
   A 44px hit area on coarse pointers, drawn nowhere and only where a finger
   might miss. Lifted from Forge, which hangs one off every control in its
   interface; the `pointer: fine` guard is why it costs a mouse nothing. */
.btn,
.rail a,
.windows a,
.tabs a {
	position: relative;
}

.btn::after,
.act-btn::after,
.rail a::after,
.windows a::after,
.tabs a::after {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: max(100%, 2.75rem);
	height: max(100%, 2.75rem);
}

@media (pointer: fine) {
	.btn::after,
	.act-btn::after,
	.rail a::after,
	.windows a::after,
	.tabs a::after {
		display: none;
	}
}

/* --- Tables ---------------------------------------------------------------
   Every class here is namespaced -- col-, row-, cell- -- and that is not
   tidiness. The first cut of this component used `.right`, `.who`, `.num` and
   `.dim`, and two of those already existed in this file: `.right` is the
   header bar's flex group, and `display:flex` landing on a `<td>` takes the
   cell out of table layout, so its bottom border painted eighteen pixels above
   everyone else's. `.quiet` did the same and drew a box round the word
   "Active".

   TestEveryClassHasCSS cannot catch this. It proves every class the markup
   emits HAS a rule; it cannot prove the rule is the one you meant. A generic
   name in a single flat stylesheet is a collision waiting for the next page.

   A list of things that all have the same fields is a table. The row pattern
   this replaces was a flex row per item, which meant every row worked out its
   own column positions and none of them agreed -- the reason the team page
   read as a jumble rather than a list.

   Headers are mono and quiet: they are read once, and then never again. */
.table-wrap {
	overflow-x: auto;
	/* Positioned, so the absolutely-positioned .sr-only label in the actions
	   header is contained by the scroll box rather than by the page. Without
	   it that 1px span lands at the table's scrolled-out x position and the
	   whole document gains a horizontal scrollbar on a phone -- from an
	   element nobody can see. */
	position: relative;
}

/* A grid child's min-width is auto, which means it is sized by its content and
   will happily push past its container -- so an overflow-x on something inside
   it never gets the chance to scroll, and the PAGE scrolls sideways instead.
   Both page shells are grids, so both need this. */
.app-main > *,
.page > *,
.panel,
.panel-body {
	min-width: 0;
}

/* The min-width is what makes the wrapper scroll rather than crush. Without
   it the table shrinks to the phone and the columns fight: an email address
   breaks after every third character and the row is four lines tall. Wide
   content scrolls inside its own container; the page never scrolls sideways. */
.table {
	width: 100%;
	min-width: 560px;
	border-collapse: collapse;
	font-size: var(--t-md);
}

.table th {
	text-align: left;
	padding: var(--s-6) var(--s-8);
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	font-weight: var(--w-semi);
	color: var(--text-muted);
	white-space: nowrap;
	/* The header is a strip, not a row: a shade below the card with a firm
	   rule under it, so the table reads as a layered instrument rather than
	   lines on a flat card. */
	background: var(--surface);
	border-bottom: 1px solid var(--border-strong);
}

.table td {
	padding: var(--s-6) var(--s-8);
	border-bottom: 1px solid var(--border);
	vertical-align: middle;
}

.table tbody tr:last-child td {
	border-bottom: none;
}

/* Hover lifts the row toward the reader; the old hover darkened it, which
   read as the row sinking INTO the page -- the opposite of depth. The mix
   leans on --text so it brightens on the dark theme and deepens on the
   light one, both of which are "toward the ink". */
.table tbody tr {
	transition: background-color 0.1s ease;
}

.table tbody tr:hover {
	background: color-mix(in srgb, var(--surface-raised) 95%, var(--text) 5%);
}

/* The row that is somebody's move -- an unpaid invoice, a ticket waiting on
   them -- carries the amber ground the needs-band uses, so the one row that
   matters most sits on its own layer before a single word is read. */
.table tbody tr.row-due td {
	background: var(--attn-ground);
}

.table tbody tr.row-due:hover td {
	background: color-mix(in srgb, var(--attn-ground) 88%, var(--attn) 12%);
}

.table tbody tr.row-due td:first-child {
	box-shadow: inset 2px 0 0 var(--attn);
}

/* One link treatment inside a table: accent ink, underline only on hover --
   a stacked column of underlined ids is a noisy vertical band. */
/* Links in a table are text, so they take the accent's text shade: the
   accent itself is #009eff in the light theme, 2.9:1 on white. */
.table a {
	color: var(--accent-ink);
	text-decoration: none;
}

.table a {
	text-decoration: none;
}

/* Numbers line up or they are not worth putting in a column. */
.table .col-num {
	text-align: right;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* Money in a body row carries weight. The whole table exists for this
   column; it should not weigh the same as a date. */
.table tbody .col-num {
	font-weight: var(--w-semi);
	color: var(--text);
}

.table .col-end,
.tw .col-end {
	text-align: right;
	white-space: nowrap;
}

/* A subscription's last day, where its renewal date would be: amber, because
   it can still be taken back, and that is a decision waiting. */
.cell-ends {
	color: var(--attn);
	white-space: nowrap;
}

/* What not paying will do to an overdue subscription, and when, in the same
   column: amber like a last day, because paying still undoes it. Sentences
   rather than a date, so they wrap -- held to a measure, or one row's
   warning would widen the whole table. */
.cell-due {
	display: block;
	max-width: 26ch;
	color: var(--attn);
	font-size: var(--t-xs);
	line-height: 1.45;
}

.cell-due + .cell-due {
	margin-top: var(--s-2);
}

/* Not .quiet: that name is already the dashboard's "nothing needs you" panel,
   which is a bordered box, and reusing it here drew a border round the word
   "Active" in every row. */
.table .col-dim {
	color: var(--text-faint);
}

/* A date column. Mono and tabular so "1 Jun 2026" and "12 Aug 2026" line up on
   the day rather than starting a character apart down the whole column. */
.table .col-when {
	font-size: var(--t-sm);
	font-variant-numeric: tabular-nums;
	color: var(--text-faint);
	white-space: nowrap;
}

/* The identifier a row is about: mono, because it is one. */
/* The identifier a row is about: mono, because it is one, and never broken
   mid-word. An email split across three lines is not an email anybody can
   read; the column scrolls instead. */
.table .col-id {
	font-family: var(--mono);
	letter-spacing: -0.01em;
	color: var(--text);
	white-space: nowrap;
}

/* Actions in a table are text, not buttons. A row of bordered buttons in every
   row is a wall of edges, and the row itself is already the boundary. */
.table .row-act,
.tw .row-act,
.row-control .row-act {
	font: inherit;
	font-size: var(--t-md);
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	color: var(--accent);
	text-decoration: none;
}

.table .row-act:hover,
.tw .row-act:hover,
.row-control .row-act:hover {
	color: var(--accent-strong);
}

.table .row-act.danger,
.tw .row-act.danger,
.row-control .row-act.danger {
	color: var(--text-muted);
}

.table .row-act.danger:hover,
.tw .row-act.danger:hover,
.row-control .row-act.danger:hover {
	color: var(--bad);
}

.table .row-acts {
	display: flex;
	gap: var(--s-8);
	align-items: center;
	justify-content: flex-end;
}

.table select {
	min-width: 150px;
}

/* A small mono marker inside a cell -- "you", "default", "owner". Not a pill:
   a pill in a table row competes with the status column, which is the one
   place in the row that is allowed to signal. */
.cell-tag {
	display: inline-block;
	margin-left: var(--s-5);
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--text-faint);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius);
	padding: 1px var(--s-4);
	vertical-align: middle;
}

/* Present to a screen reader, absent to everything else. A table column of
   actions still needs a header; sighted readers do not need to be told twice. */
.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* A second line inside a table cell: the hostname under a plan, the interval
   under a date, what kind of figure an amount is. Mono and quiet, because it
   qualifies the line above rather than competing with it. */
.cell-sub {
	display: block;
	margin-top: var(--s-2);
	font-size: var(--t-xs);
	color: var(--text-faint);
	letter-spacing: -0.01em;
}

a.cell-sub {
	color: var(--accent-ink);
	text-decoration: none;
}

a.cell-sub:hover {
	color: var(--accent-strong);
}

/* An invoice's lines on a phone: the term goes under the item and the
   table fits the screen, so the amounts are never behind a scroll. */
.table .doc-term-under {
	display: none;
}

@media (max-width: 600px) {
	.table.doc-lines {
		min-width: 0;
	}

	.table.doc-lines .doc-term {
		display: none;
	}

	.table.doc-lines .doc-term-under {
		display: block;
	}

	/* A quote's four columns at the document's wide padding pushed the
	   totals off a phone. The unit price goes, the quantity and the line
	   total stay, and the cells keep a table's padding rather than a
	   document's. */
	.table.doc-lines .col-each {
		display: none;
	}

	.doc .table.doc-lines th,
	.doc .table.doc-lines td {
		padding-left: var(--s-6);
		padding-right: var(--s-6);
	}
}

/* Ending is a decision somebody made, not a fault. */
.table .ending {
	color: var(--text-muted);
	font-weight: var(--w-medium);
}

/* What happens when the date arrives. A list, because they are separate facts
   and the reader has to weigh each one. */
.consequences {
	margin: var(--s-7) 0 var(--s-7);
	padding-left: var(--s-9);
	display: grid;
	gap: var(--s-5);
	max-width: 68ch;
}

.consequences li {
	font-size: var(--t-md);
	color: var(--text-muted);
}

.consequences b {
	color: var(--text);
	font-weight: var(--w-semi);
}

/* --- Modal ----------------------------------------------------------------
   A native dialog, opened with showModal so Esc and focus trapping are the
   platform's own. With JavaScript absent the link that would have opened it
   is an ordinary link to the same question as a page -- the one screen that
   must never fail to appear has two ways to appear. */
dialog.modal {
	margin: auto;
	width: min(560px, calc(100vw - 2 * var(--s-9)));
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-control);
	background: var(--surface-raised);
	color: var(--text);
	padding: 0;
	box-shadow: var(--lift-strong);
}

dialog.modal::backdrop {
	background: rgba(4, 5, 7, 0.7);
}

.modal-head {
	padding: var(--s-9) var(--s-9) 0;
}

.modal-head h2 {
	margin: 0;
	font-size: var(--t-lg);
	letter-spacing: -0.02em;
	/* No pulled-in word-spacing: that was the mono era's, and in the sans
	   face it rendered every dialog title as one word ("Resetthepassword?",
	   found 2026-11-03). */
	font-weight: var(--w-semi);
}

.modal-head .head-meta {
	margin-top: var(--s-4);
}

dialog.modal .row.said {
	padding: var(--s-7) var(--s-9);
}

/* The secret dialog: wider, so a password fits on one line, with the two
   secrets side by side and a copy the size of the fact beside each. */
dialog.modal.wide {
	width: min(720px, calc(100vw - 2 * var(--s-9)));
}

.modal-body {
	padding: var(--s-7) var(--s-9) var(--s-3);
}

.secrets {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
	gap: var(--s-5);
}

@media (max-width: 560px) {
	.secrets {
		grid-template-columns: 1fr;
	}
}

.secret {
	border: 1px solid var(--border);
	border-radius: var(--radius-control);
	padding: var(--s-5) var(--s-6);
	background: var(--sunk);
	min-width: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-rows: auto auto;
	gap: var(--s-2) var(--s-5);
	align-items: center;
}

.secret .lbl {
	font-size: var(--t-xs);
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--faint);
	grid-column: 1;
}

.secret .val {
	font-family: var(--mono);
	font-size: var(--t-md);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	color: var(--ink);
	grid-column: 1;
}

.secret .val.pw {
	letter-spacing: 0.02em;
}

.secret .cp {
	grid-column: 2;
	grid-row: 1 / span 2;
	font: inherit;
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	border: 1px solid var(--accent);
	background: var(--accent);
	color: #fff;
	border-radius: var(--radius-control);
	padding: var(--s-4) var(--s-7);
	cursor: pointer;
	white-space: nowrap;
}

.secret .cp.copied {
	background: var(--run);
	border-color: var(--run);
}

.ssh {
	margin-top: var(--s-6);
	font-size: var(--t-sm);
	color: var(--body);
	display: flex;
	align-items: center;
	gap: var(--s-4);
	flex-wrap: wrap;
}

.ssh code {
	font-family: var(--mono);
	background: var(--sunk);
	padding: var(--s-2) var(--s-4);
	border-radius: var(--s-3);
	color: var(--ink);
}

.modal-note {
	display: flex;
	gap: var(--s-4);
	align-items: flex-start;
	margin-top: var(--s-6);
	padding: var(--s-4) var(--s-5);
	border-radius: var(--s-3);
	background: var(--attn-soft);
	color: var(--attn);
	font-size: var(--t-sm);
}

.modal-foot {
	display: flex;
	justify-content: flex-end;
	/* The two forms are the items: Never mind on its own, and the confirm
	   form with its typed field above its button. Bottom-aligned, so the two
	   buttons sit on one line instead of Never mind floating at the field's
	   height (David, 2026-11-03). */
	align-items: flex-end;
	gap: var(--s-4);
	padding: var(--s-7) var(--s-9);
	border-top: 1px solid var(--border);
	background: var(--surface);
	border-radius: 0 0 var(--radius-control) var(--radius-control);
}

/* The row the whole table was adding up to. */
.table tfoot .total td {
	border-top: 1px solid var(--border-strong);
	border-bottom: none;
	font-weight: var(--w-semi);
	color: var(--text);
}

.table tfoot .total .col-num {
	font-size: var(--t-base);
}

/* --- The invoice as a document --------------------------------------------
   Branded on screen and identically in print: the same markup renders both,
   with the chrome stripped by the print block below, so what lands in the PDF
   is what was on the page. */
.doc {
	border: 1px solid var(--border);
	border-radius: var(--radius-control);
	background: var(--surface-raised);
	box-shadow: var(--lift-strong);
	overflow: hidden;
}

.doc-head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--s-8);
	flex-wrap: wrap;
	padding: var(--s-9);
	/* The one gradient in the product that is on a surface rather than the
	   room: a document's masthead earns a little ceremony. */
	background: linear-gradient(135deg, rgba(178, 138, 255, 0.12), transparent 55%);
	border-bottom: 2px solid var(--accent);
}

.doc-brand {
	display: flex;
	align-items: baseline;
	gap: var(--s-6);
}

.doc-kind {
	font-size: var(--t-xs);
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--accent);
	border: 1px solid var(--accent-line);
	border-radius: var(--radius);
	padding: 1px var(--s-4);
}

.doc-ref {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	flex-wrap: wrap;
}

.doc-ref b {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: 0.01em;
	font-variant-numeric: tabular-nums;
}

.doc-ref span {
	font-size: var(--t-sm);
	color: var(--text-faint);
}

.doc-parties {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
	gap: var(--s-8);
	padding: var(--s-8) var(--s-9);
	border-bottom: 1px solid var(--border);
}

.doc-label {
	margin: 0 0 var(--s-3);
	font-size: var(--t-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-faint);
	font-weight: var(--w-medium);
}

.doc-label::after {
	display: none;
}

.doc-parties p {
	margin: 0;
	font-size: var(--t-md);
	color: var(--text-muted);
	line-height: 1.55;
}

.doc-fine {
	margin: 0;
	padding: var(--s-6) var(--s-9);
	font-size: var(--t-xs);
	color: var(--text-faint);
	border-top: 1px solid var(--border);
	overflow-wrap: anywhere;
}

/* --- Theme toggle ---------------------------------------------------------
   A plain form whose button shows where you are: a sun on the light page, a
   moon on the dark one. Both icons ship in the markup and these rules choose,
   resolving follow-the-machine through the same media query as the palette --
   so when script flips data-theme in place, the icon turns with the page. */
.theme-form {
	display: inline-flex;
}

/* Scoped under .theme-form so the hide outranks .theme-icon's display:block
   below -- equal specificity would let source order show both icons at once. */
.theme-form .theme-sun,
.theme-form .theme-moon {
	display: none;
}

html[data-theme="light"] .theme-sun {
	display: block;
}

html[data-theme="dark"] .theme-moon {
	display: block;
}

@media (prefers-color-scheme: dark) {
	html:not([data-theme="light"]):not([data-theme="dark"]) .theme-moon {
		display: block;
	}
}

@media (prefers-color-scheme: light) {
	html:not([data-theme="light"]):not([data-theme="dark"]) .theme-sun {
		display: block;
	}
}

.theme-icon {
	width: 14px;
	height: 14px;
	stroke: currentColor;
	fill: none;
	stroke-width: 1.4;
	stroke-linecap: round;
	stroke-linejoin: round;
	display: block;
}

/* --- Rail iconography and grouping ----------------------------------------
   The rail is scanned by shape faster than it is read, so each item gets one:
   thin, stroke-only, currentColor, never filled and never the accent --
   wayfinding stays typography-first, and an icon that lit up in colour would
   be decoration pretending to be state. */
.rail .lab {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	min-width: 0;
}

.rail a.on .rail-icon {
	opacity: 1;
}

/* The account group: rare-use setup below a divider, so it never sits
   visually equal to a daily destination. */
.rail.sub {
	margin-top: var(--s-6);
	padding-top: var(--s-6);
	border-top: 1px solid var(--border);
}

.nav-profile.on {
	background: var(--accent-dim);
	border-color: var(--accent-line);
}

.ordercard .btn {
	width: 100%;
	margin-bottom: 0;
	text-align: center;
}

@media (max-width: 760px) {
	.rail.sub {
		margin-top: 0;
		padding-top: 0;
		border-top: none;
	}

	/* The nav collapses behind its Menu button. Keyed on nav-closed, which
	   only Alpine ever adds, so script-off leaves the nav open and usable. */
	.nav-toggle {
		display: inline-flex;
	}

	.app-nav.nav-closed .nav-groups {
		display: none;
	}

	/* Hidden on a phone, and it has to be declared HERE rather than in the
	   earlier mobile block: a media query adds no specificity, so a rule of
	   equal weight later in the file wins regardless of the query. The base
	   .ordercard sits below that block, which is why the first attempt at
	   this silently did nothing. */
	.ordercard {
		margin-top: var(--s-6);
		display: none;
	}

	/* The profile chip stays -- signing out must stay reachable -- but drops
	   its second line, so identity costs one row instead of a card. */
	.nav-profile small {
		display: none;
	}
}

/* A notice inside a panel body sits on the panel's own text grid. */
.notice.inset {
	margin: var(--s-6) var(--s-8) 0;
}

/* The per-interval suffix on a price: quieter than the figure it follows. */
.per {
	font-size: var(--t-xs);
	color: var(--text-muted);
}

.row-menu-item.danger {
	color: var(--bad);
}

/* A short mono reference standing in for a name. */
.mono-ref {
	font-family: var(--mono);
}

/* A row-scale button: secondary by construction, so the header's one true
   call to action stays the only filled one. */
.btn.slim {
	padding: var(--s-2) var(--s-6);
	font-size: var(--t-sm);
}

/* The invoice tabs: the overview of what exists in each state, doubling as
   the filter -- the boring convention every billing page shares. The counts
   never filter with the list; they summarise the whole account. */
.inv-tabs {
	margin: var(--s-2) var(--s-8) 0;
}

.tab-count {
	font-style: normal;
	font-size: var(--t-xs);
	color: var(--text-faint);
	margin-left: var(--s-2);
}

.tab-count.due {
	color: var(--attn);
}

.col-host .cell-sub {
	display: block;
	margin-top: var(--s-2);
	white-space: normal;
}

/* Hover and keyboard selection are the SAME state deliberately. Two different
   highlights -- one following the mouse, one following the arrows -- means two
   things look chosen and Enter only honours one of them. */
.palette-list a:hover,
.palette-list a.on {
	background: var(--accent-dim);
}

/* The selected row carries a spine as well as a tint, so which one Enter takes
   survives being read on a dim screen or by someone who cannot separate the
   tint from the ground. */
.palette-list a.on {
	box-shadow: inset 2px 0 0 var(--accent);
}

.act-btn.go {
	color: var(--ok);
	background: var(--ok-dim);
	border-color: var(--ok-line);
}

/* --- The overview's tiles --------------------------------------------------
   The account's shape in three numbers: what runs, what is owed, what
   recurs. The owed tile is the one thing on the row allowed amber. */
.tiles {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
	gap: var(--s-6);
}

.tile {
	display: block;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface-raised);
	box-shadow: var(--lift);
	padding: var(--s-7) var(--s-8);
	text-decoration: none;
	transition:
		border-color 0.15s ease,
		background 0.15s ease;
}

/* A tile is a place, not a phrase: hovering one lifts it -- never underlines
   it, which reads as a sentence about to be clicked. */
.tile:hover {
	text-decoration: none;
	border-color: var(--accent);
	background: color-mix(in srgb, var(--surface-raised) 94%, var(--accent));
}

.tile.warm:hover {
	border-color: var(--attn);
	background: color-mix(in srgb, var(--attn-ground) 92%, var(--attn));
}

.tile.warm {
	border-color: var(--attn-line);
	background: var(--attn-ground);
}

.tile.warm .tile-num {
	color: var(--attn);
}

/* --- Tickets ----------------------------------------------------------------
   One rule of colour: amber when it is the customer's move, blue when it is
   ours, and closed is quiet text because nothing is waited on. */
.cell-link {
	color: var(--text);
	text-decoration: none;
	font-weight: var(--w-medium);
}

.cell-link:hover {
	color: var(--accent);
}

@media (max-width: 1000px) {
	.hub {
		grid-template-columns: 1fr;
	}
}

.side-card .section-head {
	margin-bottom: var(--s-5);
}

.side-card .btn {
	width: 100%;
	text-align: center;
}

.side-head .section-head {
	flex: 1;
	margin-bottom: 0;
}

/* The one row that is about the future for real: a raised, unpaid invoice. */
.t-amt.due {
	color: var(--attn);
}

/* --- Blocked-but-drawn ----------------------------------------------------
   The Console-tab convention, generalised: a feature the backend cannot honour
   yet keeps its real shape on the page, greyed, instead of being hidden. The
   page shows the whole product; the grey says which parts of it are still
   promises. */

/* A panel body (or any region) that is drawn but cannot work yet. */
.locked {
	opacity: 0.45;
	pointer-events: none;
	user-select: none;
}

/* The chip beside a heading that names the state, in the tab strip's voice. */
.soon-chip {
	font-size: 12px;
	font-weight: var(--w-normal);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--text-faint);
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 1px var(--s-4);
	vertical-align: 2px;
	margin-left: var(--s-3);
}

/* An address masked until deliberately shown. A native <details>, so the
   toggle works with JavaScript off and with a keyboard; the two states live
   in one summary and CSS swaps them, which is what lets Hide exist after
   Reveal without a script.

   The swap is by VISIBILITY over a stacked grid, not by display. display:none
   removes the hidden state from layout, so the cell resized to whichever
   string was showing and the whole line shifted on every toggle -- the dots
   and the digits are different widths, and so are "Reveal" and "Hide". With
   both states occupying the same grid cell, the cell is always as wide as the
   wider one, and revealing moves nothing. */
.reveal {
	display: inline-grid;
	min-width: 0;
}

.reveal summary {
	display: inline-grid;
	grid-template-columns: minmax(0, auto) auto;
	align-items: center;
	gap: var(--s-4);
	min-width: 0;
	cursor: pointer;
	list-style: none;
}

.reveal summary::-webkit-details-marker {
	display: none;
}

/* Both codes share the first cell, both buttons the second. */
.reveal summary > code {
	grid-row: 1;
	grid-column: 1;
	justify-self: start;
}

.reveal summary > .btn {
	grid-row: 1;
	grid-column: 2;
	justify-self: center;
}

.reveal:not([open]) .when-open {
	visibility: hidden;
}

.reveal[open] .when-closed {
	visibility: hidden;
}

/* A revealed pair of addresses can outgrow the rail; scroll the line rather
   than the page. */
.reveal[open] code.when-open {
	overflow-x: auto;
	max-width: 100%;
}

/* --- How-to ----------------------------------------------------------------
   The chart-table convention, borrowed: a quiet summary line in the accent,
   costing nothing until asked, answering the question where it happens
   instead of linking to a doc site. */
.howto {
	margin-top: var(--s-7);
	border-top: 1px solid var(--border);
	padding-top: var(--s-6);
}

.howto summary {
	font-size: var(--t-sm);
	color: var(--accent);
	cursor: pointer;
}

.howto-body {
	margin-top: var(--s-6);
	display: grid;
	gap: var(--s-5);
	max-width: 68ch;
}

.howto-body p {
	margin: 0;
	font-size: var(--t-md);
	color: var(--text-muted);
}

/* The command sits left, unlike a connect row's right-aligned control. */
.howto-line {
	justify-content: flex-start;
}

/* Print lives at the END of this file on purpose. Screen rules for the same
   selectors sit at equal specificity, and anything appended after this block
   would beat it inside the print media too -- which is exactly how the first
   PDF came out dark. */
/* --- Print ----------------------------------------------------------------
   The invoice on paper, which is also the PDF: every browser's print dialog
   carries "Save as PDF", and this stylesheet is what it renders. Ink on
   white, because a page of near-black background is a printer complaint; the
   chrome, the actions and the attempts go, because a document is not a page.
   Times, not our stacks: a printed invoice is read as a record, and the
   system-ui of whoever printed it is an accident. */
@media print {
	/* No motion on paper -- and not only for taste: the entry animation's
	   fill-mode holds its first frame, which is opacity zero, and a print
	   context never advances an animation. Without this line the PDF is a
	   blank page. */
	*,
	*::before,
	*::after {
		animation: none !important;
		transition: none !important;
	}

	body {
		background: #fff;
		color: #000;
		font-family: var(--ui);
	}

	.marketing,
	.app-top,
	.app-nav,
	.crumbs,
	.no-print,
	.notice {
		display: none !important;
	}

	/* The app shell is a grid with a rail column; on paper there is no rail,
	   and a grid printing into a removed column prints nothing. */
	.app-shell,
	.app-body {
		display: block;
	}

	.app-main {
		max-width: none;
		margin: 0;
		padding: 0;
		display: block;
	}

	.page {
		max-width: none;
		padding: 0;
	}

	/* The crumbs are gone, but their sibling rule pulls the header up by the
	   page gap to sit beside them -- and with nothing above it, that pull
	   drags the title off the top of the sheet. */
	.crumbs + .page-head,
	.crumbs + form .page-head {
		margin-top: 0;
	}

	@page {
		margin: 18mm 15mm;
	}

	.configure {
		display: block;
	}

	.panel {
		background: none;
		padding: 0;
		max-width: none;
	}

	.panel-body {
		background: none;
		border: 1px solid #999;
		box-shadow: none;
	}

	/* The document keeps its identity on paper: the wordmark, the accent
	   rule under the masthead, the violet reference. Colour printers exist,
	   and a grey ghost of the brand is worse than either. */
	.doc {
		border: none;
		border-radius: 0;
		background: none;
		box-shadow: none;
		overflow: visible;
	}

	.doc-head {
		background: none;
		border-bottom: 2px solid #9a6cf5;
		padding: 0 0 14pt;
	}

	.doc-parties {
		padding: 12pt 0;
		border-color: #ddd;
	}

	.doc-parties p {
		color: #333;
	}

	.doc-kind {
		color: #9a6cf5;
		border-color: #9a6cf5;
	}

	.doc-ref b {
		color: #000;
	}

	.doc-fine {
		padding: 10pt 0 0;
		border-color: #ddd;
		color: #666;
	}

	.wordmark {
		color: #000;
	}

	.wordmark::after {
		color: #9a6cf5;
	}

	h1,
	.head-meta,
	.table th,
	.table td,
	.cell-sub,
	.col-dim,
	.col-num,
	.col-when {
		color: #000 !important;
	}

	.cell-sub,
	.col-dim,
	.head-meta,
	.table th {
		color: #444 !important;
	}

	.table th,
	.table td {
		border-color: #ccc;
	}

	.table tfoot .total td {
		border-top-color: #000;
	}

	.pill {
		/* !important because .pill.attn outranks .pill and would keep its
		   amber border onto paper. */
		border: 1px solid #000 !important;
		background: none !important;
		color: #000 !important;
	font-size: 12.5px;
	font-weight: var(--w-semi);
	letter-spacing: 0;
	text-transform: none;
	}

	.pill::before {
		background: #000;
	}
}

/* The verbs reuse the table's text-action treatment: a card with three
   bordered buttons in it is a wall of edges. */
.fleet-acts .row-act {
	font: inherit;
	font-size: var(--t-md);
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	color: var(--accent);
	text-decoration: none;
}

.fleet-acts .row-act:hover {
	color: var(--accent-strong);
}

.fleet-acts .row-act.danger {
	color: var(--text-muted);
}

.fleet-acts .row-act.danger:hover {
	color: var(--bad);
}

/* ---- not built yet -------------------------------------------------------
   Notes to ourselves, drawn only when PORTAL_SHOW_PENDING=1. Red on purpose:
   these borrow the alarm colour so that a page carrying three of them looks
   as unfinished as it is. See pending.go. */
.pending {
	border: 1px dashed var(--bad-line);
	border-left: 3px solid var(--bad);
	border-radius: var(--radius-control);
	background: var(--bad-dim);
	padding: var(--s-6) var(--s-7);
	margin: var(--s-6) 0;
}

.pending-tag {
	display: inline-block;
	font-size: var(--t-xs);
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--bad);
	margin-bottom: var(--s-4);
}

.pending-what {
	margin: 0;
	color: var(--text);
	font-size: var(--t-md);
}

.pending-lead {
	margin: var(--s-5) 0 var(--s-2);
	color: var(--text-faint);
	font-size: var(--t-sm);
}

.pending-needs {
	margin: 0;
	padding-left: var(--s-7);
	color: var(--text-muted);
	font-size: var(--t-sm);
}

.pending-needs li {
	margin-bottom: var(--s-2);
}

/* --- Base ---------------------------------------------------------------- */

*,
*::before,
*::after {
	box-sizing: border-box;
}

html,
body {
	margin: 0;
	padding: 0;
}

body {
	background: var(--ground);
	color: var(--body);
	font-family: var(--sans);
	font-size: var(--t-base);
	line-height: 1.55;
	-webkit-font-smoothing: antialiased;
	overflow-x: hidden;
}

/* Headings are semibold Plex at three sizes: 30 for the page, 18 for a
   section, 15 for a card. The weight is what carries the hierarchy now
   that nothing is set in mono; the tracking is small, because Plex is
   already tight at these sizes.

   Stated in full rather than as a colour and a weight, because the previous
   system set headings in the mono face and pulled their word-spacing in to
   pay for it. Leave either behind and "Standard 4" renders as "Standard4". */
h1,
h2,
h3,
h4 {
	font-family: var(--sans);
	color: var(--ink);
	font-weight: var(--w-semi);
	margin: 0;
	word-spacing: normal;
	text-wrap: balance;
}

h1 {
	font-size: var(--t-2xl);
	letter-spacing: -0.02em;
	line-height: 1.15;
}

h2 {
	font-size: var(--t-lg);
	letter-spacing: -0.012em;
}

h3 {
	font-size: var(--t-base);
	letter-spacing: -0.01em;
}

h4 {
	font-size: var(--s-7);
	letter-spacing: -0.016em;
}

p {
	margin: 0 0 var(--s-7);
	text-wrap: pretty;
}

strong {
	color: var(--ink);
	font-weight: var(--w-medium);
}

a {
	color: inherit;
}

code {
	font-family: var(--mono);
	font-size: 0.86em;
	background: var(--sunk);
	padding: 0.1em 0.38em;
	border-radius: var(--r-xs);
	color: var(--ink);
}

button {
	font-family: inherit;
}

/* Somebody who has asked their machine to stop animating has asked us too.
   The durations go to nearly-zero rather than to zero so transitionend still
   fires and nothing that waits on it hangs. */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.001ms !important;
		/* One pass, not a pulse at a thousand frames a second: an infinite
		   animation shortened is still infinite. */
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
	}
}

/* --- Shell ----------------------------------------------------------------
   A fixed rail and a scrolling canvas. The rail is sticky rather than fixed
   so it can scroll internally when the nav outgrows a laptop screen, which it
   does the moment an account has more than a handful of sections. */

.shell {
	display: grid;
	grid-template-columns: var(--side) 1fr;
	min-height: 100vh;
}

.side {
	background: var(--ground-2);
	border-right: 1px solid var(--line);
	display: flex;
	flex-direction: column;
	position: sticky;
	top: 0;
	height: 100vh;
	overflow-y: auto;
}

.side-top {
	padding: var(--s-8) var(--s-7) var(--s-5);
	display: flex;
	align-items: center;
	gap: 9px;
}

/* The wordmark (2026-11-03): the brand kit's logo inline, in the text
   colour, so it is white on dark and ink on light. Its own aspect, 141:33. */
.logo {
	height: 22px;
	aspect-ratio: 141 / 33;
	flex: none;
	display: block;
	background: var(--ink);
	-webkit-mask: url(logo.svg) center / contain no-repeat;
	mask: url(logo.svg) center / contain no-repeat;
}

/* The wordmark, rail and shop header both: the mark and the name as one
   link, so the whole thing is the target and not just the word. */
.brand {
	display: inline-flex;
	align-items: center;
	gap: 9px;
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	letter-spacing: -0.032em;
	color: var(--ink);
	text-decoration: none;
	white-space: nowrap;
}

.deploy {
	margin: var(--s-4) var(--s-6) var(--s-7);
}

.navgrp {
	padding: 0 var(--s-4);
}

/* The group labels are mono and tiny on purpose: they are furniture somebody
   reads once to learn the shape of the rail and never reads again. */
.navgrp .lbl {
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
	padding: var(--s-8) var(--s-5) var(--s-3);
	display: block;
	font-weight: var(--w-semi);
}

/* The product lines under Servers. Indented to the width of the parent's icon
   plus its gap, so the labels line up with the parent's label rather than with
   its icon -- a child that starts where the icon does reads as another
   top-level item that has lost its picture. */
.navkids {
	display: flex;
	flex-direction: column;
	gap: 1px;
	margin: 1px 0 var(--s-3);
}

.navkids .kid {
	padding-left: 34px;
	font-size: var(--t-md);
	color: var(--text-faint);
}

.navkids .kid.on {
	color: var(--text);
}

.navlink {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	padding: 8px var(--s-5);
	border-radius: var(--r-sm);
	font-size: var(--t-base);
	color: var(--body);
	cursor: pointer;
	letter-spacing: -0.012em;
	text-decoration: none;
	transition: background var(--t-fast) var(--e-io), color var(--t-fast) var(--e-io);
	position: relative;
}

.navlink:hover {
	background: var(--sunk);
	color: var(--ink);
}

/* The current item is a raised card with an accent edge rather than a fill.
   A filled rail item competes with the primary button directly above it, and
   only one thing on the rail should look like the thing to press. */
.navlink.on {
	background: var(--surface);
	color: var(--ink);
	font-weight: var(--w-medium);
	box-shadow: var(--sh-1);
}

.navlink.on::before {
	content: "";
	position: absolute;
	left: -8px;
	top: var(--s-4);
	bottom: var(--s-4);
	width: 2px;
	border-radius: 2px;
	background: linear-gradient(180deg, var(--accent), var(--accent-2));
}

.navlink .ic {
	width: 15px;
	color: var(--faint);
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

/* The rail's icons are our own inline SVGs -- one grid, one stroke, one
   hand -- because no font renders eleven Unicode symbols as one family. */
.ic > svg {
	width: 15px;
	height: 15px;
	display: block;
}

.navlink.on .ic {
	color: var(--accent-ink);
}

/* A rail badge is amber only when the number means "your move". A count that
   is merely informative stays quiet, or the amber stops meaning anything. */
.navlink .badge {
	margin-left: auto;
	font-size: var(--t-xs);
	background: var(--attn);
	/* Ink, not white: white on the dark theme's amber is 2.27:1. */
	color: var(--ground);
	border-radius: var(--radius-pill);
	padding: 0 7px;
	font-weight: var(--w-semi);
}

/* The quiet count -- how many servers, how many keys -- is information, not
   an instruction, so it never takes the amber. It still has to be READABLE
   though: on the rail's own ground, --sunk against --faint is two greys a
   pixel apart, and the number may as well not be there. */
.navlink .badge.q {
	background: var(--line);
	color: var(--body);
}

/* A section drawn but not built. Greyed and chipped rather than hidden:
   somebody who cannot find a feature assumes we do not have it, and assumes
   wrong more often than they assume we are still building it. */
.navlink.soon {
	opacity: 0.55;
	cursor: default;
}

.navlink.soon:hover {
	background: none;
	color: var(--body);
}

/* Drawn but not built, wherever it is said. Dashed rather than filled: a
   solid chip reads as a state the thing is IN, and this is a state the thing
   is not in yet. */
.chip {
	display: inline-block;
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	border: 1px dashed var(--line);
	border-radius: var(--radius-pill);
	padding: 1px var(--s-3);
	color: var(--faint);
	font-weight: var(--w-semi);
	vertical-align: 1px;
}

/* In the rail it is pushed to the far edge, where the counts sit. */
.navlink .chip {
	margin-left: auto;
}

.side-foot {
	margin-top: auto;
	padding: var(--s-6);
	border-top: 1px solid var(--line);
}

/* The team switcher: a details popover that opens UPWARD from the rail foot,
   in the palette's clothes -- same raised surface, same item shape -- because
   it answers the same kind of question: "take me somewhere else". */
.ctx {
	position: relative;
	margin-bottom: var(--s-3);
}

.ctx summary {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	padding: 7px var(--s-4);
	border-radius: var(--r-sm);
	font-size: var(--t-md);
	color: var(--body);
	cursor: pointer;
	list-style: none;
}

.ctx summary::-webkit-details-marker {
	display: none;
}

.ctx summary:hover {
	background: var(--sunk);
	color: var(--ink);
}

.ctx summary .ic {
	width: 15px;
	color: var(--faint);
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

.ctx summary .chip {
	margin-left: auto;
}

.ctx-menu {
	position: absolute;
	bottom: calc(100% + var(--s-2));
	left: 0;
	right: 0;
	background: var(--raised);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-3);
	padding: var(--s-2);
	z-index: 40;
	animation: palin 200ms var(--e-out) both;
}

.ctx-item {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	width: 100%;
	padding: var(--s-4) var(--s-5);
	border: 0;
	border-radius: var(--r-sm);
	background: none;
	font-size: var(--t-md);
	color: var(--body);
	cursor: pointer;
	text-align: left;
}

.ctx-item:hover {
	background: var(--sunk);
	color: var(--ink);
}

.ctx-item .nm {
	color: var(--ink);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.ctx-item .hint {
	margin-left: auto;
	font-size: var(--t-sm);
	color: var(--faint);
	flex: none;
}

/* The team you are already in: marked, and inert. Switching to where you
   are is not an action, so the row says "you are here" instead of offering
   to take you there. */
.ctx-item.on {
	background: var(--accent-soft);
	cursor: default;
}

.ctx-item.on .nm {
	color: var(--accent-ink);
}

.userchip {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 7px var(--s-4);
	border-radius: var(--r-sm);
	cursor: pointer;
	text-decoration: none;
	transition: background var(--t-fast) var(--e-io);
}

.userchip:hover {
	background: var(--sunk);
}

.av {
	width: var(--s-10);
	height: var(--s-10);
	border-radius: 50%;
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	color: #fff;
	font-size: 12px;
	display: grid;
	place-items: center;
	flex: none;
	font-weight: var(--w-medium);
}

.av.staff {
	background: linear-gradient(135deg, var(--ink), #3a3a44);
}

.userchip .who {
	font-size: var(--t-md);
	color: var(--ink);
	line-height: 1.25;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.userchip .who small {
	display: block;
	font-size: var(--t-sm);
	color: var(--faint);
}

/* --- Topbar --------------------------------------------------------------- */

/* The person, at the bar's far end, as a details popover: who you are, your
   account, the teams you can act for, and the way out. It replaced a block
   at the foot of the rail that said the same things in 12px on every page;
   here it costs one avatar until it is asked. Alpine closes it on a click
   elsewhere; without Alpine it closes on its own summary, which is how a
   details element has always worked. */
.me {
	position: relative;
}

.me > summary {
	list-style: none;
	cursor: pointer;
	display: grid;
	place-items: center;
	width: 32px;
	height: 32px;
	border-radius: 50%;
}

.me > summary::-webkit-details-marker {
	display: none;
}

.me > summary:hover .av,
.me[open] > summary .av {
	box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--line);
}

.me-menu {
	position: absolute;
	right: 0;
	top: calc(100% + var(--s-3));
	min-width: 260px;
	max-width: 320px;
	background: var(--raised);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-3);
	padding: var(--s-2);
	z-index: 40;
	animation: palin 200ms var(--e-out) both;
}

.me-who {
	padding: var(--s-4) var(--s-5) var(--s-5);
	border-bottom: 1px solid var(--hair);
	margin-bottom: var(--s-2);
	font-size: var(--t-sm);
	color: var(--faint);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.me-who b {
	display: block;
	color: var(--ink);
	font-weight: var(--w-medium);
	font-size: var(--t-md);
}

.me-grp {
	display: block;
	padding: var(--s-5) var(--s-5) var(--s-2);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}

.me-menu form {
	margin: 0;
}

.me-menu .ctx-item {
	text-decoration: none;
}

.main {
	min-width: 0;
	display: flex;
	flex-direction: column;
}

.top {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	padding: 11px var(--s-10);
	border-bottom: 1px solid var(--line);
	background: color-mix(in srgb, var(--ground) 82%, transparent);
	backdrop-filter: blur(12px);
	position: sticky;
	top: 0;
	z-index: 20;
	flex-wrap: wrap;
}

.crumbs {
	font-size: var(--t-base);
	color: var(--faint);
	display: flex;
	gap: 7px;
	align-items: center;
	min-width: 0;
	flex-wrap: wrap;
}

/* In the topbar the crumbs ARE the bar and need no room beneath them; on a
   page they introduce a heading and do. */
.canvas > .crumbs {
	margin-bottom: var(--s-5);
}

.crumbs a {
	text-decoration: none;
}

.crumbs a:hover {
	color: var(--ink);
}

.crumbs b {
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.014em;
}

.top .right {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: 7px;
}

.iconbtn {
	width: 30px;
	height: 30px;
	border-radius: var(--r-sm);
	border: 1px solid transparent;
	background: none;
	color: var(--body);
	cursor: pointer;
	display: grid;
	place-items: center;
	font-size: var(--t-base);
	position: relative;
	transition: all var(--t-fast) var(--e-io);
}

.iconbtn:hover {
	background: var(--sunk);
	color: var(--ink);
	border-color: var(--line);
}

.iconbtn .dotb {
	position: absolute;
	top: 5px;
	right: 5px;
	width: var(--s-3);
	height: var(--s-3);
	border-radius: 50%;
	background: var(--attn);
	border: 1.5px solid var(--ground);
}

.searchbtn {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	font-size: var(--t-md);
	color: var(--faint);
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	padding: var(--s-3) var(--s-5);
	cursor: pointer;
	min-width: 190px;
	transition: all var(--t-fast) var(--e-io);
}

.searchbtn:hover {
	border-color: color-mix(in srgb, var(--ink) 25%, var(--line));
}

.searchbtn .kbd {
	margin-left: auto;
}

.searchbtn .sglass {
	flex: none;
}

.searchbtn .sword {
	white-space: nowrap;
}

/* A phone gets the glass alone, the size of the bar's other icons, so the
   bar stays one line. */
@media (max-width: 600px) {
	.top {
		flex-wrap: nowrap;
		padding-inline: var(--s-8);
	}

	.top .crumbs {
		flex: 1;
		overflow: hidden;
	}

	.top .crumbs b {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.searchbtn {
		min-width: 0;
		width: 30px;
		height: 30px;
		padding: 0;
		justify-content: center;
		border-color: transparent;
		background: none;
	}

	.searchbtn .sword,
	.searchbtn .kbd {
		display: none;
	}
}

.kbd {
	font-family: var(--mono);
	font-size: var(--t-xs);
	border: 1px solid var(--line);
	border-radius: var(--r-xs);
	padding: 2px 5px;
	color: var(--faint);
	background: var(--ground-2);
}

.canvas {
	padding: var(--s-11) var(--gutter) 90px;
	max-width: none;
	width: 100%;
	/* Centred like .page. It was not, and on a wide screen the configure
	   page sat in the left half under a centred header. */
	margin: 0;
}
/* A phone: the four steps stay four across in smaller type rather than
   wrapping the last one onto a line of its own; and a row of exactly three
   facts stays a row rather than two and an orphan. */
@media (max-width: 560px) {
	.canvas .rail div {
		min-width: 0;
		font-size: 12px;
		padding-top: 7px;
	}
	.canvas .rail div .n {
	font-size: 12px;
	}
	.g4:has(> :nth-child(3):last-child) {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

/* The narrowest phones: Cart, Sign in and Create an account plus the theme
   toggle came to 348px in a 320px viewport, so every signed-out page
   scrolled sideways and the last button was cut off. The row may wrap and
   the buttons tighten, rather than any of them leaving. */
@media (max-width: 400px) {
	.marketing {
		padding-left: var(--s-5);
		padding-right: var(--s-5);
	}
	.marketing .right {
		flex-wrap: wrap;
		gap: var(--s-3);
	}
	.marketing .right .btn {
		padding-left: var(--s-4);
		padding-right: var(--s-4);
	}
}

/* The page's vertical rhythm, as a rule instead of a habit.
 *
 * It used to be written to COLLAPSE into whatever margin a component already
 * carried, on the theory that a block bringing its own air should keep it.
 * Measured across all 60 preview pages, that theory produced four different
 * gaps under a page title -- 6px, 14px, 20px and 52px -- because every
 * component's own margin is a different number and whichever rule came last
 * in this file won. Letting each component decide is what "the margins are
 * wrong" looks like from the outside.
 *
 * So the page level is authoritative now: sections on a page are 20px apart,
 * whatever they are made of. `main.canvas` carries an element as well as a
 * class so it outranks the single-class component margins it has to beat,
 * rather than depending on where in this file a rule happens to sit.
 *
 * .flow is the same rhythm for a wrapper that is NOT the canvas. The server
 * page is the one that needs it: its whole body lives inside #machine-root
 * so htmx can swap the hydrated render in one piece, and that wrapper used
 * to hide every section on the page from this rule -- the portal's most
 * important page was the one with no rhythm at all. */
main.canvas > * + *,
.build-page > * + *,
.flow > * + * {
	margin-top: var(--s-9);
}

/* The one exception, and it is about meaning rather than taste: a lede is the
   sentence belonging to the title above it, not the next section down. It
   keeps the head's own tight 6px and does not take the section gap. */
main.canvas > .ph + .lede,
.flow > .ph + .lede {
	margin-top: 0;
}

/* Nothing animates on load (portal review, October 2026). A page that
   fades in is a page that is briefly not there, and the data it carries is
   the only reason anyone opened it. Motion confirms a press, and that is
   all it does now. */
.view {
	animation: none;
}

@keyframes vin {
	from {
		opacity: 0;
		transform: translateY(var(--s-5));
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* --- Page head ------------------------------------------------------------ */

.ph {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	flex-wrap: wrap;
	margin-bottom: var(--s-3);
}

.ph .t {
	font-size: var(--t-2xl);
	letter-spacing: -0.045em;
	color: var(--ink);
}

.ph .sub {
	font-size: var(--t-sm);
	color: var(--faint);
}

.ph .right {
	margin-left: auto;
	display: flex;
	gap: var(--s-4);
	flex-wrap: wrap;
}

/* Prose is capped at 64 characters. A line of explanation that runs the full
   width of a 1180px canvas is a line nobody finishes. */
.lede {
	font-size: var(--t-base);
	max-width: var(--prose);
	margin: 0 0 var(--s-9);
}

/* --- Buttons -------------------------------------------------------------- */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 7px;
	font-size: var(--t-md);
	padding: 9px var(--s-8);
	border-radius: var(--r-sm);
	border: 1px solid var(--ink);
	background: var(--ink);
	color: var(--ground);
	letter-spacing: -0.008em;
	cursor: pointer;
	white-space: nowrap;
	text-decoration: none;
	transition: transform var(--t-micro) var(--e-out), box-shadow var(--t-fast) var(--e-out),
		opacity var(--t-fast) var(--e-io);
	line-height: 20px;
}

.btn:hover {
	transform: translateY(-1px);
	box-shadow: var(--sh-2);
}

.btn:active {
	transform: translateY(0) scale(0.985);
}

.btn.ghost {
	background: var(--surface);
	color: var(--ink);
	border-color: var(--line);
}

.btn.ghost:hover {
	border-color: var(--ink);
}

/* The accent ramp is reserved for the one action a page exists to offer.
   Two of these on a screen and neither is the primary. */
.btn.accent {
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	border-color: transparent;
	color: #fff;
}

/* Under the pointer the accent button turns to ink rather than a brighter
   blue: a brighter blue on a page of blue chosen things blended in, and a
   button that changes colour outright is one you cannot miss. */
.btn.accent:hover {
	background: var(--ink);
	color: var(--ground);
	box-shadow: var(--sh-2);
}

.btn.danger {
	background: var(--bad);
	border-color: var(--bad);
	color: #fff;
}

/* The destructive verb as an outline. Solid red beside the accent primary is
   two saturated actions on one row -- and the whole point of a confirm is
   that exactly one thing looks like the thing to press, and it is the safe
   one. The red survives in the ink and the border, so what the button DOES
   is never in doubt; only its volume changes. */
.btn.ghost.danger {
	background: var(--surface);
	border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
	color: var(--bad);
}

.btn.ghost.danger:hover {
	border-color: var(--bad);
}

.btn.sm {
	font-size: var(--s-6);
	padding: var(--s-3) 11px;
}

.btn.wide {
	width: 100%;
}

.btn[disabled],
.btn[aria-disabled="true"] {
	opacity: 0.4;
	cursor: not-allowed;
	transform: none !important;
	box-shadow: none !important;
}

.btnrow {
	display: flex;
	gap: 9px;
	flex-wrap: wrap;
	align-items: center;
}

.chipbtn {
	font-size: var(--s-6);
	padding: var(--s-3) var(--s-6);
	border: 1px solid var(--line);
	border-radius: var(--radius-pill);
	color: var(--body);
	background: var(--surface);
	cursor: pointer;
	letter-spacing: -0.005em;
	text-decoration: none;
	transition: all var(--t-fast) var(--e-out);
}

.chipbtn:hover {
	border-color: var(--ink);
	color: var(--ink);
	transform: translateY(-1px);
}

.chipbtn.on {
	background: var(--ink);
	color: var(--ground);
	border-color: var(--ink);
}

/* The row of state chips above a list. Its own class rather than a bare
   .btnrow because it wants the space of a toolbar under the heading, not the
   tight gap of buttons inside a row. */
.chips {
	padding: var(--s-7) 0 var(--s-6);
}

.chipbtn .ct {
	font-size: var(--t-xs);
	opacity: 0.6;
	margin-left: var(--s-3);
}

/* --- Pills ----------------------------------------------------------------
   State, and only state. The dot carries the colour so the pill reads at a
   glance from across a table; the word is there for anyone who cannot use
   the colour, which is the whole reason the word is never dropped. */

.pill {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	font-size: 12.5px;
	letter-spacing: 0;
	text-transform: none;
	padding: var(--s-2) var(--s-5);
	border: 1px solid var(--line);
	border-radius: var(--radius-pill);
	color: var(--ink);
	background: var(--surface);
	white-space: nowrap;
	font-weight: var(--w-semi);
}

.pill::before {
	content: "";
	width: var(--s-3);
	height: var(--s-3);
	border-radius: 50%;
	background: currentColor;
	flex: none;
}

/* Two names, one treatment: `work` is the drawing's class, `working` is what
   this product's state mapping emits. The first port defined only the
   drawing's, so every live "Building"/"Working" pill silently fell through
   to the legacy colours -- and the pulse, the one animation that says
   "something is happening right now", never ran at all. */
.pill.work,
.pill.working {
	color: var(--accent-ink);
	border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
	background: var(--accent-soft);
	box-shadow: 0 0 0 3px var(--accent-glow);
}

.pill.work::before,
.pill.working::before {
	background: var(--accent);
	animation: pulse 1.6s var(--e-io) infinite;
}

/* Running is the drawing's PLAIN pill. The legacy system painted it blue-green,
   and the drawing refuses: "working correctly" is the condition of almost
   every row, and painting it makes the two rows that need reading harder to
   find. Stated in full to beat the legacy colour rules above. */
.pill.ok {
	color: var(--ink);
	border-color: var(--line);
	background: var(--surface);
	box-shadow: none;
}

.pill.ok::before {
	background: currentColor;
	animation: none;
}

/* Stopped: quieter than running, louder than unknown. Somebody switched it
   off on purpose; the pill has nothing to claim and nothing to warn about. */
.pill.off {
	color: var(--body);
	border-color: var(--line);
	background: var(--surface);
	box-shadow: none;
}

.pill.off::before {
	background: var(--faint);
}

@keyframes pulse {
	0%,
	100% {
		box-shadow: 0 0 0 0 var(--accent-glow);
		opacity: 1;
	}

	50% {
		box-shadow: 0 0 0 5px transparent;
		opacity: 0.45;
	}
}

.pill.attn {
	color: var(--attn);
	border-color: color-mix(in srgb, var(--attn) 38%, var(--line));
	background: var(--attn-soft);
}

.pill.bad {
	color: var(--bad);
	border-color: color-mix(in srgb, var(--bad) 38%, var(--line));
	background: var(--bad-soft);
}

/* Unknown is drawn as absence -- a dashed ring with nothing in it -- because
   the honest answer to "what is this machine doing" is sometimes that we do
   not know, and a solid dot in any colour would be a claim. */
.pill.unk {
	color: var(--faint);
	border-style: dashed;
}

.pill.unk::before {
	background: none;
	border: 1px dashed currentColor;
}

.pill.sm {
	font-size: 12px;
	padding: var(--s-1) var(--s-4);
}

/* --- Cards and rows ------------------------------------------------------- */

.card {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-1);
}

.card.attn {
	border-color: color-mix(in srgb, var(--attn) 30%, var(--line));
}

/* A card with a head is the drawing's structured card: the head and body
   carry their own padding, so the card itself must not add a second inset --
   the legacy .card padding above otherwise bleeds through and doubles it,
   leaving the head's hairline floating short of the card's edges. */
.card:has(> .card-h) {
	padding: 0;
}

/* A card that contains a form gets a column measure.
   The drawing's settings rows carry chips, toggles and segmented controls --
   short things that sit happily at the far edge of a 1180px row. Ours carry
   text inputs, and a label on the far left with its input on the far right of
   a wide monitor is two things you look between rather than one row you read.
   Matched on content rather than on a class, so a page cannot forget it.

   A card holding messages is exempt. A ticket's reply box is a textarea too,
   but the card around it is a conversation and wants the room -- narrowing it
   to a form's measure squeezes the thread it belongs to.

   So is the console: xterm types through a hidden textarea of its own, and
   the rule held the whole console to 820px, half a wide screen beside an
   empty gutter (2026-11-04). */
.card:has(.field):not(:has(.msg)):not(.console-card),
.card:has(input[type="text"]):not(:has(.msg)):not(.console-card),
.card:has(input[type="email"]):not(:has(.msg)):not(.console-card),
.card:has(textarea):not(:has(.msg)):not(.console-card) {
	width: 100%;
	max-width: 820px;
}

.card-h {
	padding: var(--t-base) var(--s-8);
	border-bottom: 1px solid var(--hair);
	display: flex;
	align-items: center;
	gap: var(--s-5);
	flex-wrap: wrap;
}

.card-h.attn {
	background: var(--attn-soft);
}

.card-h .ct {
	font-size: var(--s-7);
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.016em;
}

.card-h .cn {
	font-size: var(--t-sm);
	color: var(--faint);
}

.card-h .right {
	margin-left: auto;
	display: flex;
	gap: 7px;
}

.card-b {
	padding: var(--s-8);
}

/* A card's subtitle. It sits under the head with no rule of its own, because
   a sentence introducing a list is not another item in the list -- boxed, it
   reads as the first row and gets counted as one. */
.card-lede {
	padding: var(--s-6) var(--s-8) 0;
	margin: 0;
	font-size: var(--t-base);
	color: var(--body);
	max-width: 68ch;
	line-height: 1.5;
}

.card-lede + .rows.bare {
	margin-top: var(--s-6);
}

.rows {
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	overflow: hidden;
}

/* Rows inside a card have already been framed by the card. */
.rows.bare {
	border: 0;
	border-radius: 0;
}

.row {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: var(--s-6) var(--s-8);
	padding: var(--t-base) var(--s-8);
	border-bottom: 1px solid var(--hair);
	align-items: center;
	transition: background var(--t-fast) var(--e-io);
}

.row:last-child {
	border-bottom: 0;
}

.row:hover {
	background: var(--sunk);
}

.row .rt {
	font-size: var(--t-md);
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.014em;
}

.row .rd {
	font-size: var(--t-sm);
	color: var(--body);
	margin-top: 3px;
	line-height: 1.45;
}

.row.clickable {
	cursor: pointer;
}

/* The payment-method chooser on a phone: the button's column took what it
   needed and the sentence beside it was left one word wide ("Approve / it /
   once / at"), readiness pass of 2026-11-04. Stacked, the sentence gets the
   width and the button sits under what it does. */
@media (max-width: 600px) {
	.pick-rows .row {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* --- Grid ----------------------------------------------------------------- */

.grid {
	display: grid;
	gap: var(--s-6);
}

/* A wrapper that hands its children straight to the surrounding grid. It
   exists for exactly one tension: the live poll needs ONE element to swap
   (hx-swap-oob), and the grid needs the tiles as direct children. contents
   dissolves the box and keeps the element. */
.gcell {
	display: contents;
}

.g2 {
	grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}

.g3 {
	grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
}

.g4 {
	grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}

/* --- Stat tiles -----------------------------------------------------------
   The unknown variant is the important one. A tile with no number behind it
   says so in words at reading size, rather than showing a plausible figure --
   every panel we surveyed would have invented something here. */

.stat {
	display: block;
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r);
	padding: var(--s-7) 15px 15px;
	text-decoration: none;
	transition: transform var(--t-fast) var(--e-out), border-color var(--t-fast) var(--e-io);
}

.stat:hover {
	transform: translateY(-2px);
	border-color: color-mix(in srgb, var(--ink) 20%, var(--line));
}

/* The one tile that is a bill. It is the only tile allowed to carry the
   attention ground, because it is the only one that is asking for something
   rather than reporting something. */
.stat.warm {
	border-color: color-mix(in srgb, var(--attn) 34%, var(--line));
	background: var(--attn-soft);
}

.stat .k {
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
	display: block;
	font-weight: var(--w-semi);
}

.stat .v {
	font-size: 25px;
	color: var(--ink);
	letter-spacing: -0.042em;
	display: block;
	margin-top: var(--s-3);
	font-variant-numeric: tabular-nums;
}

.stat .v small {
	font-size: var(--s-6);
	color: var(--faint);
	letter-spacing: 0;
}

.stat .v.unk {
	color: var(--faint);
	font-size: 14.5px;
	letter-spacing: -0.01em;
}

.stat .foot {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-top: 5px;
	display: block;
	line-height: 1.35;
}

/* A tile whose value is a word rather than a number. Twenty-five pixels is
   sized for "5" and for "£80.00"; it is not sized for "Offline throughout". */
.stat .v.small {
	font-size: 17px;
	letter-spacing: -0.02em;
}

/* A tile whose value is a state, not a figure. The pill brings its own size,
   so this only has to put it where the number would have been. */
.stat .p {
	display: block;
	margin-top: var(--s-3);
}

/* The same quiet foot, used outside a tile -- the sentence that explains an
   empty space where a control would otherwise be. */
.foot {
	font-size: var(--t-sm);
	color: var(--faint);
	line-height: 1.35;
	margin: 0;
}

/* --- Skeletons ------------------------------------------------------------
   The shell pass draws the spots where observed figures land: a shimmer at
   the figure's own size, so the hydrated truth arrives in place instead of
   shoving the layout. A skeleton is never a claim -- the old shell wrote
   "Not running" and "No reading" into these spots, wrongly, for the second
   the hydrate took. */
.skel {
	position: relative;
	display: inline-block;
	vertical-align: middle;
	border-radius: 6px;
	background: color-mix(in srgb, var(--ink) 8%, transparent);
	overflow: hidden;
}

.skel::after {
	content: "";
	position: absolute;
	inset: 0;
	transform: translateX(-100%);
	background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ink) 7%, transparent), transparent);
	animation: skel-sheen 1.4s ease-in-out infinite;
}

@keyframes skel-sheen {
	to {
		transform: translateX(100%);
	}
}

/* Somebody who asked the OS for less motion keeps the resting tint; the
   spots still read as "on its way" without anything moving. */
@media (prefers-reduced-motion: reduce) {
	.skel::after {
		animation: none;
	}
}

/* Each sized like what will land there, which is the whole point: the
   skeleton's job is holding the layout still, not decorating the wait. */
.sk-v {
	width: 84px;
	height: 30px;
}

.sk-line {
	width: 56px;
	height: 10px;
}

.sk-pill {
	width: 82px;
	height: 24px;
	border-radius: var(--radius-pill);
}

.sk-btn {
	width: 68px;
	height: 34px;
	border-radius: var(--radius-control);
}

.sk-plot {
	display: block;
	width: 100%;
	height: 112px;
	border-radius: 8px;
}

/* --- Inline notices ------------------------------------------------------- */

/* nowrap is stated because the previous system's .inline wrapped, and an
   inline notice whose sentence wraps below its own icon is two paragraphs
   with a stray glyph on top. The text takes the remaining width instead. */
.inline {
	border-radius: var(--r);
	padding: var(--s-6) 15px;
	font-size: var(--t-sm);
	color: var(--ink);
	margin: var(--s-7) 0;
	display: flex;
	flex-wrap: nowrap;
	gap: 11px;
	align-items: flex-start;
	line-height: 1.5;
}

.inline > div {
	flex: 1;
	min-width: 0;
}

.inline .ii {
	flex: none;
	font-size: var(--t-base);
	line-height: 1.3;
}

.inline.bad {
	background: var(--bad-soft);
	border: 1px solid color-mix(in srgb, var(--bad) 28%, var(--line));
}

.inline.bad b {
	color: var(--bad);
	font-weight: var(--w-medium);
}

.inline.attn {
	background: var(--attn-soft);
	border: 1px solid color-mix(in srgb, var(--attn) 28%, var(--line));
}

.inline.attn b {
	color: var(--attn);
	font-weight: var(--w-medium);
}

.inline.acc {
	background: var(--accent-soft);
	border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
}

.inline.acc b {
	color: var(--accent-ink);
	font-weight: var(--w-medium);
}

.inline.quiet {
	background: var(--sunk);
	border: 1px solid var(--line);
	color: var(--body);
}

/* --- Fields ---------------------------------------------------------------
   Inputs are mono by default. What gets typed into this panel is hostnames,
   keys and addresses, where a proportional face makes l and 1 the same
   glyph; the .sans variant is for the few fields that take prose. */

.field {
	margin: var(--s-7) 0;
}

.field label {
	font-size: var(--t-sm);
	color: var(--ink);
	display: block;
	margin-bottom: var(--s-3);
	font-weight: var(--w-medium);
}

.field .hint {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-top: 5px;
	display: block;
}

.input,
select.input,
textarea.input {
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	background: var(--surface);
	padding: 11px var(--s-6);
	font-size: var(--t-md);
	color: var(--ink);
	width: 100%;
	transition: border-color var(--t-fast) var(--e-io), box-shadow var(--t-fast) var(--e-io);
}

.input:focus {
	outline: none;
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.input.match {
	border-color: var(--bad);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad) 20%, transparent);
}

.input.sans {
	font-family: var(--sans);
	font-size: var(--t-md);
}

.toggle {
	width: 36px;
	height: var(--s-9);
	border-radius: var(--radius-pill);
	background: var(--line);
	border: 0;
	cursor: pointer;
	position: relative;
	flex: none;
	transition: background var(--t) var(--e-io);
}

.toggle::after {
	content: "";
	position: absolute;
	top: 2px;
	left: 2px;
	width: var(--s-8);
	height: var(--s-8);
	border-radius: 50%;
	background: var(--surface);
	box-shadow: var(--sh-1);
	transition: transform var(--t) var(--e-spring);
}

.toggle.on {
	background: linear-gradient(135deg, var(--accent), var(--accent-2));
}

.toggle.on::after {
	transform: translateX(var(--s-8));
}

.seg {
	display: inline-flex;
	background: var(--sunk);
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	padding: 2px;
	gap: 2px;
}

.seg button,
.seg a {
	border: 0;
	background: none;
	font-size: var(--s-6);
	padding: 5px 11px;
	border-radius: var(--r-xs);
	color: var(--body);
	cursor: pointer;
	text-decoration: none;
	transition: all var(--t-fast) var(--e-io);
}

.seg button.on,
.seg a.on {
	background: var(--surface);
	color: var(--ink);
	box-shadow: var(--sh-1);
	font-weight: var(--w-medium);
}

/* --- Tabs ----------------------------------------------------------------- */

.tabs {
	position: relative;
	display: flex;
	gap: 2px;
	border-bottom: 1px solid var(--line);
	margin: var(--s-9) 0 0;
	overflow-x: auto;
	scrollbar-width: none;
}

.tabs::-webkit-scrollbar {
	display: none;
}

/* A bar with tabs past its edge fades out at that edge (the readiness pass
   of 2026-11-04): at 390px the server pages' tabs scrolled sideways with no
   scrollbar and nothing else to say more were there. panel.js sets the two
   attributes from the bar's own scroll position. */
.tabs[data-more-end] {
	mask-image: linear-gradient(90deg, #000 calc(100% - 40px), transparent);
}

.tabs[data-more-start] {
	mask-image: linear-gradient(90deg, transparent, #000 40px);
}

.tabs[data-more-start][data-more-end] {
	mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent);
}

.tabs button,
.tabs a {
	font-size: var(--t-md);
	padding: 9px var(--t-base);
	color: var(--body);
	background: none;
	border: 0;
	cursor: pointer;
	white-space: nowrap;
	letter-spacing: -0.008em;
	text-decoration: none;
	transition: color var(--t-fast) var(--e-io);
}

/* On a phone the tabs' own air is what pushed a fourth one onto a second
   line, with the underline left under the first; tighter, four fit. */
@media (max-width: 480px) {
	.tabs button,
	.tabs a {
		padding-inline: var(--s-6);
	}
}

/* A strip that still runs past the edge stopped mid-word with nothing to say
   there was more ("Payment me", readiness pass of 2026-11-04). It fades on
   the side it continues past, driven by its own scroll: a strip that fits
   has no scroll timeline, so it never fades, and a browser without scroll
   timelines scrolls it as before. */
@media (max-width: 600px) {
	.tabs {
		animation: tabs-more linear both;
		animation-timeline: scroll(self inline);
	}
}

@keyframes tabs-more {
	from {
		mask-image: linear-gradient(90deg, #000 calc(100% - 40px), transparent);
	}
	10%,
	90% {
		mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 40px), transparent);
	}
	to {
		mask-image: linear-gradient(90deg, transparent, #000 40px);
	}
}

.tabs button:hover,
.tabs a:hover {
	color: var(--ink);
}

.tabs button.on,
.tabs a.on {
	color: var(--ink);
	font-weight: var(--w-medium);
}

.tabs .ind {
	position: absolute;
	bottom: -1px;
	height: 2px;
	border-radius: 2px;
	background: linear-gradient(90deg, var(--accent), var(--accent-2));
	transition: transform var(--t) var(--e-out), width var(--t) var(--e-out);
}

.tabs .dot {
	display: inline-block;
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: var(--attn);
	margin-left: var(--s-3);
	vertical-align: 1px;
}

.pane {
	display: none;
	padding-top: var(--s-8);
}

.pane.on {
	display: block;
}

/* A pane switched by Alpine rather than by the .pane/.pane.on pair -- x-show
   toggles the element itself, so all this has to do is give the content the
   same breathing room a .pane gets under its tab bar. */
.pane-body {
	padding-top: var(--s-8);
}

/* Panels stacked inside a pane breathe. The page grid spaces its own
   children, but a pane is ONE page child, so the cards inside it were
   sitting flush on each other -- the server page used to carry inline
   margin-tops for exactly this. Anything-then-card rather than
   card-then-card, so a card after a form or a notice gets the same air.
   The bare aside is the side-column the server page stacks cards in; the
   classed asides are cards themselves and space through their parents. */
.pane-body > * + .card,
aside:not([class]) > * + .card {
	margin-top: var(--s-9);
}

/* --- Tables --------------------------------------------------------------- */

.tw {
	overflow-x: auto;
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
}

table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--t-base);
	min-width: 560px;
}

th {
	text-align: left;
	font-size: var(--t-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
	font-weight: var(--w-semi);
	padding: 9px var(--s-7);
	border-bottom: 1px solid var(--line);
	background: var(--ground-2);
	position: sticky;
	top: 0;
}

td {
	padding: 11px var(--s-7);
	border-bottom: 1px solid var(--hair);
	color: var(--body);
	vertical-align: middle;
}

tr:last-child td {
	border-bottom: 0;
}

tbody tr {
	transition: background var(--t-fast) var(--e-io);
}

tbody tr:hover {
	background: var(--sunk);
}

/* A table of money documents on a phone. Scrolling it sideways put the
   status and the action -- the two cells anybody opens the page for -- off
   the right edge, behind a gesture nobody guesses. Under 700px each row is a
   card instead: the describing cell on its own line, then the reference,
   figures and status flowing after it, and the action pushed to the end.
   The header goes, because a label above a column that no longer exists
   labels nothing. */
@media (max-width: 700px) {
	table.stack {
		min-width: 0;
	}

	table.stack thead {
		display: none;
	}

	table.stack tbody tr {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--s-2) var(--s-6);
		padding: var(--s-6) var(--s-7);
		border-bottom: 1px solid var(--hair);
	}

	table.stack tbody tr:last-child {
		border-bottom: 0;
	}

	table.stack td {
		padding: 0;
		border-bottom: 0;
	}

	table.stack td.stack-main {
		flex-basis: 100%;
		order: -1;
	}

	table.stack td.col-end {
		margin-left: auto;
	}
}

/* The access matrix (Team page): members down, servers across, and a dot
   where a row may touch a column. Row headers drop the base th uppercase --
   they are email addresses, not labels -- and cells centre, because a dot in
   a left-aligned column reads as a list, not a grid. */
.amx tbody th {
	font-family: var(--sans);
	font-size: var(--t-sm);
	letter-spacing: 0;
	text-transform: none;
	color: var(--ink);
	background: none;
	position: static;
	border-bottom: 1px solid var(--hair);
	white-space: nowrap;
}

.amx tbody tr:last-child th {
	border-bottom: 0;
}

.amx td {
	text-align: center;
}

.amx thead th:not(:first-child) {
	text-align: center;
}

.amx-role {
	margin-left: var(--s-4);
	font-size: var(--t-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--faint);
}

.amx-dot {
	font-size: var(--s-5);
	line-height: 1;
}

.amx-dot.act {
	color: var(--accent-ink);
}

.amx-dot.look {
	color: var(--faint);
}

/* The legend carries the meaning the colour alone must not. */
.amx-key {
	margin: var(--s-4) 0 0;
	font-family: var(--mono);
	font-size: var(--t-xs);
	color: var(--faint);
}

td.ink {
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.012em;
}

td.num {
	font-variant-numeric: tabular-nums;
	font-size: var(--s-6);
}

/* The header IS the control. An arrow beside the word says which way it is
   sorted; a header that only sorts when you find the tiny glyph is a header
   most people never discover sorts at all. */
.sort {
	color: inherit;
	text-decoration: none;
	display: inline-flex;
	align-items: center;
	gap: var(--s-2);
}

.sort:hover {
	color: var(--ink);
}

/* The row of things that act on a list: filter, clear, and how to look at it.
   The view toggle is pushed to the far end so the filter always begins at the
   same x as the list it filters. */
.tools {
	gap: var(--s-4);
	padding: var(--s-7) 0 var(--s-6);
	align-items: center;
}

.tools .seg {
	margin-left: auto;
}

/* The product-line picker groups with the search on the LEFT; only the view
   toggle belongs at the far end. Named `linepick` rather than `lines`, which
   is already the checkout's cart-line grid -- wearing it here would have
   turned this segmented control into a two-column grid, which is the kind of
   collision this stylesheet has been bitten by before. */
.tools .seg.linepick {
	margin-left: 0;
}

.tools .input {
	width: auto;
	min-width: 220px;
}

/* A dropdown in a toolbar sizes to its own words. The base select is
   width:100% because it almost always lives in a field, stacked under a
   label; in a row of controls that would push everything beside it off the
   end. Same treatment the search box already gets above. */
.tools select {
	width: auto;
}

/* The controls at the end of a row or a table cell. Right-aligned and allowed
   to wrap, because a row with three verbs on a narrow screen should stack them
   rather than push the row's text out of the viewport. */
.acts {
	display: flex;
	gap: var(--s-2);
	align-items: center;
	flex-wrap: wrap;
	justify-content: flex-end;
}

.acts form {
	margin: 0;
}

/* An em dash in the faint colour is what a cell says when there is nothing
   to put in it. A blank cell reads as a rendering bug. */
.dash {
	color: var(--faint);
}

/* A figure, anywhere. Tabular so a column of them lines up on the decimal
   without the markup having to be a table. */
.num {
	font-size: var(--s-6);
	font-variant-numeric: tabular-nums;
	color: var(--ink);
}

/* --- Fleet rows -----------------------------------------------------------
   A server is a thing you go INTO, so the row is a target rather than a table
   line: it lifts on hover and the chevron leads. */

/* The list is one grid and every row borrows its columns (subgrid), so a
   column holds the same place on every row. Each row used to size its own,
   which was invisible while every tail was a chevron: a row ending in a
   button (Open the panel) pushed its state and its renewal a hundred pixels
   left of the rows around it. A browser without subgrid keeps the rows'
   own columns, which is how it always looked. */
.fleet {
	display: grid;
	grid-template-columns: 1.5fr auto 0.85fr 0.8fr auto;
	column-gap: var(--s-7);
	row-gap: 7px;
}

/* A fleet row with no page behind it: a service we bill for and cannot see,
   which is every product that is not a VPS until it grows a panel of its own.
   Same row, minus the affordances that would promise a click -- the pointer
   and the hover lift belong to rows that go somewhere. */
.frow.flat {
	cursor: default;
}

.frow.flat:hover {
	border-color: var(--line);
	box-shadow: none;
	transform: none;
}

.frow {
	position: relative;
	display: grid;
	grid-column: 1 / -1;
	grid-template-columns: 1.5fr auto 0.85fr 0.8fr auto;
	grid-template-columns: subgrid;
	gap: var(--s-7);
	align-items: center;
	padding: var(--t-base) 15px;
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	cursor: pointer;
	text-decoration: none;
	opacity: 0;
	transform: translateY(var(--s-5));
	transition: transform var(--t-fast) var(--e-out), box-shadow var(--t-fast) var(--e-out),
		border-color var(--t-fast) var(--e-io), opacity var(--t-fast) var(--e-out);
}

/* Stamped by the server, so the list is visible with no script at all; the
   script only removes it for a beat to let the rows arrive in order. */
.fleet.in .frow {
	opacity: 1;
	transform: none;
}

/* The row IS the link -- it holds no interactive children. The address cell
   it briefly carried (reveal and copy) moved back to the server's own page,
   which re-simplified the markup to one element, one target, one meaning. */

.frow:hover {
	transform: translateY(-2px);
	box-shadow: var(--sh-2);
	border-color: color-mix(in srgb, var(--ink) 22%, var(--line));
}

.frow .host {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-size: var(--s-7);
	letter-spacing: -0.015em;
}

.frow .host small {
	display: block;
	font-size: var(--t-sm);
	color: var(--faint);
	font-weight: var(--w-normal);
	margin-top: 2px;
}

.frow .meta {
	font-size: var(--t-sm);
}

.frow .num {
	font-size: var(--s-6);
}

/* The row's right-hand end: whatever trails the facts, and the chevron. It is
   its own cell so the chevron stays pinned to the edge however much or little
   sits beside it. */
.frow .tail {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	justify-content: flex-end;
}

.frow .go {
	color: var(--faint);
	font-size: var(--t-lg);
	transition: transform var(--t-fast) var(--e-out), color var(--t-fast) var(--e-io);
}

.frow:hover .go {
	transform: translateX(3px);
	color: var(--ink);
}

/* --- Sparkline and plot ---------------------------------------------------
   A gap in the data is drawn as a gap. Joining across it would invent a line
   through hours we never measured, which is the graph equivalent of making
   the number up. */

.plot {
	display: flex;
	align-items: flex-end;
	gap: 2px;
	height: 76px;
	border-bottom: 1px solid var(--hair);
}

/* Each bar sits in a full-height cell: the hover target is the whole column,
   so a 3%-tall reading is as interrogable as a full one, and the tooltip has
   a fixed anchor at the top of the plot instead of riding each bar's height. */
.plot .pc {
	position: relative;
	flex: 1;
	min-width: 2px;
	height: 100%;
	display: flex;
	align-items: flex-end;
}

.plot .pc::after {
	content: attr(data-tip);
	position: absolute;
	bottom: calc(100% + 5px);
	left: 50%;
	transform: translateX(-50%);
	padding: 4px 8px;
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	background: var(--raised);
	box-shadow: var(--sh-2);
	font-size: var(--t-xs);
	color: var(--ink);
	white-space: nowrap;
	pointer-events: none;
	opacity: 0;
	transition: opacity var(--t-fast) var(--e-io);
	z-index: 30;
	/* Not laid out until hovered: an invisible nowrap box centred on a
	   column near the right edge still widened the page, so the machine
	   page scrolled sideways on every phone. */
	display: none;
}

.plot .pc:hover::after {
	display: block;
	opacity: 1;
}

/* The right half's tooltips open leftwards, so a hovered one stays inside
   the card. */
.plot .pc:nth-last-child(-n + 6)::after {
	left: auto;
	right: 0;
	transform: none;
}

/* The first and last columns pin their tooltip to the plot's edge instead of
   centring: centred, half of it leaves the card. */
.plot .pc:first-child::after {
	left: 0;
	transform: none;
}

.plot .pc:last-child::after {
	left: auto;
	right: 0;
	transform: none;
}

.plot .pc:hover i {
	filter: brightness(1.35);
}

.plot i {
	width: 100%;
	border-radius: 2px 2px 0 0;
	background: linear-gradient(180deg, var(--mc, var(--accent)), color-mix(in srgb, var(--mc, var(--accent)) 22%, transparent));
	transform-origin: bottom;
	transform: scaleY(0);
	transition: transform 520ms var(--e-out);
}

.plot.in i {
	transform: scaleY(1);
}

/* Each metric wears its own hue (David, 2026-11-03), from the brand's blue
   and violet outwards; the card's alarm still overrides with the one red. */
.card.metric {
	--mc: var(--accent);
}

.card.metric[data-metric="memory"] {
	--mc: var(--accent-2);
}

.card.metric[data-metric="disk"] {
	--mc: #14b8a6;
}

.metric-sec[data-metric="bandwidth"],
.card.metric[data-metric="network"],
.card.metric[data-metric="bandwidth"] {
	--mc: #38bdf8;
}

.card.metric.alarm {
	--mc: var(--bad);
}

/* A chart's head is always two lines, the title and then its figures
   (David, 2026-11-04): disk and network carry two series' worth of words,
   which wrapped under the title while CPU and memory sat beside it, and the
   grid's cards came out two heights. Figures under the title on every card
   keeps the plots level. */
.card.metric > .card-h,
.chart-grid .card > .card-h {
	flex-direction: column;
	align-items: flex-start;
	gap: 2px;
}

/* Memory and disk are scaled to the machine's size, so the plot's top edge
   is the limit: drawn dashed, it reads as the line usage can't cross. */
.plot.limit {
	border-top: 1px dashed color-mix(in srgb, var(--mc, var(--accent)) 55%, transparent);
}

/* A gap in the readings is hatched and drawn at full height, and it never
   animates: growing it would read as a measurement arriving. */
.plot i.g {
	background: repeating-linear-gradient(135deg, var(--line) 0 1px, transparent 1px 5px);
	border-radius: 0;
	transform: scaleY(1);
}

/* The y gutter beside the plot: the scale's top, middle and zero, right-
   aligned against the bars. A grid so the axis row below starts where the
   bars do, not where the labels do. */
.plotwrap {
	display: grid;
	grid-template-columns: auto 1fr;
	column-gap: var(--s-4);
}

.plotwrap > div:last-child {
	min-width: 0;
}

.yax {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	align-items: flex-end;
	height: 76px;
	font-size: var(--t-xs);
	color: var(--faint);
}

.axis {
	display: flex;
	justify-content: space-between;
	font-size: 12px;
	color: var(--faint);
	padding-top: 7px;
}

/* A chart whose latest reading crossed its one threshold. The card wears the
   failure's border and the sentence below the plot says the same fact in
   words -- colour is never the only carrier. */
.card.alarm {
	border-color: color-mix(in srgb, var(--bad) 34%, var(--line));
}

.card.alarm .cn {
	color: var(--bad);
}

/* --- Terminal and task drawer --------------------------------------------- */

.term {
	background: var(--panel);
	color: var(--panel-ink);
	font-family: var(--mono);
	font-size: var(--t-sm);
	line-height: 1.7;
	padding: 15px 17px;
	overflow: auto;
	max-height: 280px;
	white-space: pre-wrap;
}

.term .t {
	color: #6c6a72;
}

.term .ok {
	color: #4ec27a;
}

.term .er {
	color: #ff7b6e;
}

.term .ac {
	color: #4bb4ff;
}

.term .ln {
	display: block;
	animation: lnin 300ms var(--e-out) both;
}

@keyframes lnin {
	from {
		opacity: 0;
		transform: translateX(-6px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* The live console. Separate from .term because xterm.js measures a fixed box
   to decide how many rows fit -- a class that sizes to its content gives it
   nothing to measure and it renders one row tall. */
/* Full screen is the card, so the drawer bar and its exit control ride
   along; the terminal then takes every row the screen has. */
.card:fullscreen {
	border-radius: 0;
	display: flex;
	flex-direction: column;
}

/* The snapshot form: two fields and the button on one line, wrapping on a
   narrow screen. The optional-marker sits inside the label quietly. */
.snapform {
	display: flex;
	gap: var(--s-5);
	align-items: flex-end;
	flex-wrap: wrap;
}

.snapform .field {
	flex: 1;
	min-width: 180px;
	margin: 0;
}

/* The firewall's add-rule form: five fields and the button on one line,
   wrapping on narrow screens -- the snapform's shape with more columns. */
.fwform {
	display: flex;
	gap: var(--s-5);
	align-items: flex-end;
	flex-wrap: wrap;
}

.fwform .field {
	flex: 1;
	min-width: 110px;
	margin: 0;
}

.hint-inline {
	font-size: var(--t-xs);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--faint);
	margin-left: var(--s-2);
}

.card:fullscreen .console-term {
	height: auto;
	flex: 1;
}

/* The console split leans wider than the default two-column: a terminal
   earns its width, the rail is reference. */
.console-split {
	grid-template-columns: minmax(0, 1fr) 280px;
}

/* The active console mode reads as pressed-in, the way a tab underline says
   "you are here" -- and disabled minis say so without shouting. */
.mini.on {
	background: #26262e;
	border-color: #5a5a66;
	color: #fff;
}

.mini:disabled {
	opacity: 0.4;
	cursor: default;
}

/* The display: noVNC draws into this box, scaled to fit. Taller than the
   terminal because a framebuffer is 4:3-ish, and the same always-dark panel
   ground -- a screen is a screen in either theme. */
.console-vnc {
	height: clamp(480px, calc(100vh - 380px), 1500px);
	background: var(--panel);
	display: flex;
	align-items: center;
	justify-content: center;
	color: #85828b;
	font-family: var(--mono);
	font-size: var(--t-sm);
	overflow: hidden;
}

/* The console grows with the screen (David, 2026-11-04): a fixed 360px
   was a letterbox on a 1440px-tall display. The window's height less the
   head, tabs and key bar above and below it, never under the old size. */
.console-term {
	height: clamp(360px, calc(100vh - 420px), 1400px);
	padding: var(--s-4);
	background: var(--panel);
	/* Scroll stays inside the terminal; nothing chains to the page. */
	overscroll-behavior: contain;
}

/* xterm's own scroll surface: keep it scrollable whatever else cascades, and
   contained for the same reason as its parent. The thin dark scrollbar makes
   the scrollback DISCOVERABLE -- an invisible scrollbar on a dark terminal
   reads as no scrollback at all. */
.console-term .xterm-viewport {
	overflow-y: auto;
	overscroll-behavior: contain;
	scrollbar-width: thin;
	scrollbar-color: #3a3a44 transparent;
}

.cursor {
	display: inline-block;
	width: 7px;
	height: var(--t-base);
	background: var(--panel-ink);
	vertical-align: -2px;
	animation: blink 1.1s steps(2, start) infinite;
}

@keyframes blink {
	50% {
		opacity: 0;
	}
}

.drawer-h {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	background: var(--panel-2);
	color: var(--panel-ink);
	padding: var(--s-5) var(--s-8);
	border-bottom: 1px solid var(--panel-line);
	font-size: var(--s-6);
	flex-wrap: wrap;
}

.drawer-h .id {
	font-family: var(--mono);
	font-size: var(--t-sm);
	color: #85828b;
}

.drawer-h .right {
	margin-left: auto;
	display: flex;
	gap: 7px;
}

.mini {
	font-size: var(--t-xs);
	letter-spacing: 0.1em;
	text-transform: uppercase;
	border: 1px solid #33333c;
	border-radius: var(--r-xs);
	padding: var(--s-2) 9px;
	color: #d6d3d8;
	background: none;
	cursor: pointer;
	transition: all var(--t-fast) var(--e-io);
}

.mini:hover {
	border-color: #5a5a66;
	color: #fff;
}

.prog {
	height: 3px;
	background: var(--panel-line);
	overflow: hidden;
}

.prog i {
	display: block;
	height: 100%;
	width: 0;
	background: linear-gradient(90deg, var(--accent), var(--accent-2));
	transition: width 400ms var(--e-out);
	position: relative;
}

.prog i::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
	animation: shimmer 1.5s linear infinite;
}

@keyframes shimmer {
	from {
		transform: translateX(-100%);
	}

	to {
		transform: translateX(100%);
	}
}

/* --- Build screen ---------------------------------------------------------
   A machine that does not exist yet has no control panel to show, so until
   the build lands the page IS the build: the steps, where it has got to and
   what it is waiting on. A single spinner is why provisioning feels
   untrustworthy everywhere else -- the work already happens in ordered
   stages, and a spinner throws the ordering away. */

/* A machine still being made, or one whose build failed, is the whole page,
   so it sits in one column in the middle of the canvas, its trail and its
   log with it, rather than against the left edge (David, 2026-11-05). */
.build-page {
	max-width: 780px;
	margin-inline: auto;
}

.build {
	max-width: 780px;
}

.bhead {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	flex-wrap: wrap;
	margin-bottom: var(--s-7);
}

.bhead .t {
	font-size: var(--t-2xl);
	letter-spacing: -0.045em;
	color: var(--ink);
}

.bhead .sub {
	font-size: var(--s-6);
	color: var(--faint);
}

.bbar {
	height: var(--s-2);
	background: var(--sunk);
	border-radius: var(--s-2);
	overflow: hidden;
	margin: var(--s-8) 0 var(--s-2);
}

.bbar i {
	display: block;
	height: 100%;
	width: 0;
	background: linear-gradient(90deg, var(--accent), var(--accent-2));
	transition: width 600ms var(--e-out);
	position: relative;
}

.bbar i::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
	animation: shimmer 1.6s linear infinite;
}

.bmeta {
	display: flex;
	justify-content: space-between;
	font-size: var(--t-sm);
	color: var(--faint);
}

.bsteps {
	margin-top: var(--s-8);
	border-top: 1px solid var(--hair);
}

.bstep {
	display: grid;
	grid-template-columns: var(--s-9) 1fr auto;
	gap: 0 var(--t-base);
	padding: var(--s-6) 0;
	border-bottom: 1px solid var(--hair);
	align-items: start;
	transition: opacity var(--t) var(--e-io);
}

.bstep .bi {
	width: var(--s-8);
	height: var(--s-8);
	border-radius: 50%;
	border: 1.5px solid var(--line);
	display: grid;
	place-items: center;
	font-size: 9px;
	color: var(--faint);
	margin-top: 2px;
	transition: all var(--t) var(--e-spring);
}

.bstep .bt {
	font-size: var(--t-md);
	color: var(--ink);
	letter-spacing: -0.014em;
}

.bstep .bd {
	font-size: var(--s-6);
	color: var(--body);
	margin-top: 2px;
}

.bstep .bw {
	font-size: var(--t-sm);
	color: var(--faint);
	align-self: center;
	white-space: nowrap;
}

.bstep.wait {
	opacity: 0.4;
}

.bstep.run .bi {
	border-color: var(--accent);
	color: var(--accent);
	box-shadow: var(--halo);
	animation: pulse 1.6s var(--e-io) infinite;
}

.bstep.run .bt {
	font-weight: var(--w-medium);
}

.bstep.done .bi {
	border-color: var(--ink);
	background: var(--ink);
	color: var(--ground);
}

.bstep.fail .bi {
	border-color: var(--bad);
	background: var(--bad);
	color: #fff;
}

.bstep.fail .bt {
	color: var(--bad);
}

/* The build terminal. Dark in both themes on purpose: it is a terminal, and
   a light one reads as a text box. panel.js types the lines in. */
.bterm {
	--term-bg: #0f1117;
	--term-fg: #d7dbe3;
	--term-dim: #6b7280;
	--term-ok: #4ade80;
	--term-acc: #7cc4ff;
	background: var(--term-bg);
	color: var(--term-fg);
	border-radius: var(--radius-control);
	font-family: var(--mono);
	font-size: 13.5px;
	line-height: 1.65;
	overflow: hidden;
	border: 1px solid rgba(255, 255, 255, 0.06);
	box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 20px 40px -24px rgba(0, 0, 0, 0.5);
	margin-top: var(--s-8);
}

.bterm-bar {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	padding: var(--s-4) var(--s-6);
	background: rgba(255, 255, 255, 0.04);
	border-bottom: 1px solid rgba(255, 255, 255, 0.06);
	color: var(--term-dim);
	font-size: var(--t-sm);
}

.bterm-bar i {
	width: 11px;
	height: 11px;
	border-radius: 50%;
	display: inline-block;
	background: #3a3f47;
}

.bterm-bar i:nth-child(1) {
	background: #ff5f57;
}

.bterm-bar i:nth-child(2) {
	background: #febc2e;
}

.bterm-bar i:nth-child(3) {
	background: #28c840;
}

.bterm-bar .ttl {
	margin-left: var(--s-3);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.bterm-bar .el {
	margin-left: auto;
	font-variant-numeric: tabular-nums;
}

.bterm-body {
	padding: var(--s-6) var(--s-7) var(--s-7);
	min-height: 230px;
}

.bterm-body .ln {
	white-space: pre-wrap;
	word-break: break-word;
}

.bterm-body .ln .ts {
	color: var(--term-dim);
	margin-right: var(--s-4);
}

.bterm-body .ln .ok {
	color: var(--term-ok);
}

.bterm-body .ln .acc {
	color: var(--term-acc);
}

.bterm-body .ln.dim {
	color: var(--term-dim);
}

.bterm-body .cur::after {
	content: "▍";
	color: var(--term-acc);
	animation: blink 1s steps(1) infinite;
	margin-left: 2px;
}

/* The moment a build lands: a fixed sheet the confetti is drawn on, gone
   two seconds later. */
canvas.party {
	position: fixed;
	inset: 0;
	pointer-events: none;
	z-index: 50;
}

@media (prefers-reduced-motion: reduce) {
	.bterm-body .cur::after {
		animation: none;
	}
}


/* --- Backend-required markers ---------------------------------------------
   The design was drawn ahead of the backend, and these are where it says so.
   They are dashed and in the second accent so they can never be mistaken for
   a notice addressed to a customer -- they are addressed to us. Hidden until
   the panel is put in the mode that shows them. */

.needs {
	display: none;
	border: 1px dashed color-mix(in srgb, var(--accent-2) 55%, var(--line));
	border-radius: var(--r);
	background: color-mix(in srgb, var(--accent-2) 7%, var(--surface));
	padding: var(--s-6) 15px;
	margin: var(--t-base) 0;
	font-size: var(--t-sm);
	line-height: 1.5;
}

body.needs-on .needs {
	display: block;
}

.needs .nl {
	font-family: var(--mono);
	font-size: 9.5px;
	letter-spacing: 0.15em;
	text-transform: uppercase;
	color: var(--accent-2);
	display: block;
	margin-bottom: 7px;
	font-weight: var(--w-medium);
}

.needs b {
	color: var(--ink);
	font-weight: var(--w-medium);
}

.needs code {
	background: color-mix(in srgb, var(--accent-2) 12%, transparent);
	font-size: var(--t-sm);
}

.needs .why {
	display: block;
	margin-top: 7px;
	color: var(--body);
}

.needs .why::before {
	content: "Why, ";
	color: var(--faint);
	font-family: var(--mono);
	font-size: var(--t-xs);
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

/* --- Timeline ------------------------------------------------------------- */

.tl {
	position: relative;
	padding-left: 22px;
}

.tl::before {
	content: "";
	position: absolute;
	left: 5px;
	top: var(--s-3);
	bottom: var(--s-3);
	width: 1px;
	background: var(--line);
}

.tli {
	position: relative;
	padding: 0 0 var(--s-9);
}

.tli::before {
	content: "";
	position: absolute;
	left: -21px;
	top: 5px;
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--surface);
	border: 2px solid var(--line);
}

.tli.acc::before {
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.tli.bad::before {
	border-color: var(--bad);
}

.tli .tlt {
	font-size: var(--t-md);
	color: var(--ink);
	letter-spacing: -0.014em;
}

.tli .tlm {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-top: 2px;
}

.tli .tld {
	font-size: var(--t-sm);
	margin-top: var(--s-3);
}

.diff {
	font-family: var(--mono);
	font-size: var(--t-sm);
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	overflow: hidden;
	margin-top: 9px;
}

.diff div {
	padding: 3px 11px;
}

.diff .del {
	background: var(--bad-soft);
	color: var(--bad);
}

.diff .add {
	background: var(--accent-soft);
	color: var(--accent-ink);
}

/* --- Wizard ---------------------------------------------------------------
   The summary is sticky because the question "what am I about to pay" must be
   answerable at every step, not only at the last one. */

.wiz {
	display: grid;
	grid-template-columns: 1fr 292px;
	gap: var(--s-9);
	align-items: start;
}

/* flex-direction is stated because the previous system had a .rail too -- the
   sidebar's -- and it was a column. Inheriting that here stacks four steps
   down the page as four full-width rules. */
.rail {
	display: flex;
	flex-direction: row;
	gap: 0;
	margin-bottom: 22px;
	flex-wrap: wrap;
}

.rail div {
	flex: 1;
	min-width: 110px;
	padding: 9px 0 0;
	border-top: 2px solid var(--line);
	font-size: var(--s-6);
	color: var(--faint);
	transition: all var(--t) var(--e-io);
}

.rail div .n {
	font-size: var(--t-xs);
	letter-spacing: 0.14em;
	display: block;
	margin-bottom: 2px;
}

.rail div.on {
	border-top-color: var(--accent);
	color: var(--ink);
}

.rail div.done {
	border-top-color: var(--ink);
	color: var(--body);
}

/* The question above a set of options. It is a heading rather than a label
   because each one is a decision the page is asking for, and a wizard that
   whispers its questions in label-grey makes people scroll looking for the
   next one. */
.wiz-q {
	display: block;
	font-family: var(--sans);
	font-size: 18px;
	font-weight: var(--w-normal);
	letter-spacing: -0.026em;
	text-transform: none;
	color: var(--ink);
	margin: 26px 0 var(--s-2);
}

/* The old h3 grew a rule out of its right-hand side. A question does not want
   one: the options below it already close the group. */
.wiz-q::after {
	content: none;
}

.wiz-q:first-of-type {
	margin-top: var(--s-9);
}

.opts {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
	gap: var(--s-4);
}

/* An option that is really a radio. The input is off-screen rather than
   hidden, so the group is still one arrow-key control for a keyboard and
   still announces itself as a radio -- display:none would take both away. */
/* The card IS the radio. Zero-sized and transparent rather than
   display:none, because display:none takes the control out of the tab order
   and stops a screen reader calling it a radio -- and then the group is no
   longer one arrow-key control, which is the whole reason it is radios. */
.opt input[type="radio"],
.opt input[type="checkbox"] {
	position: absolute;
	opacity: 0;
	width: 0;
	height: 0;
	margin: 0;
	pointer-events: none;
}

.opt:focus-within {
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.opt .on-t,
.opt .on-d,
.opt .on-p {
	display: block;
}

/* Stated in full because the previous system's .opt was a row in a stacked
   list -- square corners, negative top margin so borders collapse into each
   other. Every one of those has to be undone for it to be a card. */
.opt {
	display: block;
	border: 1px solid var(--line);
	border-radius: var(--r);
	margin: 0;
	background: var(--surface);
	padding: var(--t-base) var(--s-7);
	cursor: pointer;
	transition: all var(--t-fast) var(--e-out);
	position: relative;
	z-index: auto;
}

.opt:first-child,
.opt:last-child {
	border-radius: var(--r);
	margin: 0;
}

/* The previous system drew its own radio dot here. This card says which one
   is chosen with its border and its ring; a dot as well is the same fact
   twice, and the leftover one renders as a stray tick above the label. */
.opt::before {
	content: none;
}

.opt:hover {
	transform: translateY(-2px);
	box-shadow: var(--sh-2);
	border-color: color-mix(in srgb, var(--ink) 25%, var(--line));
}

.opt.on {
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.opt .on-t {
	font-size: var(--t-md);
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.014em;
}

.opt .on-d {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-top: 3px;
}

.opt .on-p {
	font-size: var(--s-6);
	color: var(--ink);
	margin-top: var(--s-4);
}

.opt.dis {
	opacity: 0.45;
	cursor: not-allowed;
}

.opt.dis:hover {
	transform: none;
	box-shadow: none;
	border-color: var(--line);
}

.dirm {
	font-size: var(--s-6);
	vertical-align: 1px;
}

.dirm.up {
	color: var(--accent-ink);
}

.dirm.down {
	color: var(--attn);
}

.summ {
	position: sticky;
	top: 70px;
}

.summ .sr {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	font-size: var(--t-sm);
	padding: 7px 0;
	border-bottom: 1px solid var(--hair);
}

.summ .sr:last-of-type {
	border-bottom: 0;
}

.summ .sr b {
	color: var(--ink);
	font-weight: var(--w-medium);
	text-align: right;
}

.summ .tot {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	padding-top: var(--s-6);
	margin-top: var(--s-3);
	border-top: 1px solid var(--line);
}

.summ .tot .v {
	font-size: 23px;
	color: var(--ink);
	letter-spacing: -0.04em;
	font-variant-numeric: tabular-nums;
}

/* --- Ticket thread -------------------------------------------------------- */

.msg {
	display: grid;
	grid-template-columns: 28px 1fr;
	gap: 11px;
	padding: var(--s-7) 0;
	border-bottom: 1px solid var(--hair);
}

.msg:last-child {
	border-bottom: 0;
}

/* The reply, in the same card as the thread and marked off from it by a rule
   rather than by a gap. It is the next message, not a separate form. */
.replybox {
	border-top: 1px solid var(--hair);
}

/* Send, close, and the line that says closing is reversible. The hint sits on
   the same row as the buttons on purpose: it answers the question somebody
   has while their finger is over the control, and a sentence underneath is a
   sentence read after the decision. */
.replyacts {
	margin-top: var(--s-5);
	align-items: center;
}

.replyacts .hint {
	margin: 0;
}

.msg .who {
	font-size: var(--t-sm);
	color: var(--ink);
	font-weight: var(--w-medium);
}

.msg .when {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-left: 7px;
	font-weight: var(--w-normal);
}

.msg .bod {
	font-size: var(--t-md);
	margin-top: 5px;
	color: var(--body);
}

/* What a message is made of. This div used to hold escaped text and nothing
   else -- no white-space handling at all -- so a reply typed over four lines
   arrived as one paragraph and a pasted log arrived as a wall. richtext emits
   a closed set of tags and these are all of them. */
.bod > :first-child {
	margin-top: 0;
}

.bod > :last-child {
	margin-bottom: 0;
}

.bod p {
	margin: 0 0 var(--s-5);
}

.bod ul,
.bod ol {
	margin: 0 0 var(--s-5);
	padding-left: var(--s-10);
}

.bod li {
	margin: 2px 0;
}

.bod code {
	font-family: var(--mono);
	font-size: var(--t-sm);
	background: var(--sunk);
	border: 1px solid var(--hair);
	border-radius: var(--r-sm);
	padding: 1px 5px;
}

/* A pasted log is the reason this package exists, so it gets its own box and
   its own scrollbar rather than widening the thread. */
.bod pre {
	margin: 0 0 var(--s-5);
	background: var(--sunk);
	border: 1px solid var(--hair);
	border-radius: var(--r-sm);
	padding: var(--s-5) 11px;
	overflow-x: auto;
}

.bod pre code {
	border: 0;
	background: none;
	padding: 0;
	line-height: 1.6;
}

.bod blockquote {
	margin: 0 0 var(--s-5);
	padding: 2px 0 2px var(--s-6);
	border-left: 2px solid var(--line);
	color: var(--faint);
}

/* Hidden until Alpine has initialised the element that carries it. This is
   the whole no-script story for the composer: the editor chrome is cloaked in
   the markup and only ever un-cloaked by Alpine, so a reader with no script
   sees the plain textarea underneath and posts the same field. */
[x-cloak] {
	display: none !important;
}

/* --- The composer ----------------------------------------------------------
   A toolbar and an editable surface, drawn over the textarea that still holds
   the value. editor.js builds it; without script the textarea is simply
   visible and nothing here matches. */

/* The composer's outer wrapper: the chrome and the textarea it stands in
   front of, kept together so the Alpine component can reach both. */
.edw {
	display: block;
	position: relative;
}

.ed {
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	background: var(--surface);
	overflow: hidden;
	transition: border-color var(--t-fast) var(--e-io), box-shadow var(--t-fast) var(--e-io);
}

.ed:focus-within {
	border-color: var(--accent);
	box-shadow: 0 0 0 3px var(--accent-glow);
}

/* The warning state for a submit with nothing written. It replaces the
   browser's own `required` bubble, which cannot point at an element it does
   not consider a form control. */
.ed.ed-empty {
	border-color: var(--bad);
}

.ed-bar {
	display: flex;
	gap: 2px;
	padding: var(--s-2) var(--s-3);
	border-bottom: 1px solid var(--hair);
	background: var(--sunk);
	flex-wrap: wrap;
}

.ed-btn {
	min-width: 28px;
	height: 26px;
	padding: 0 var(--s-3);
	border: 0;
	border-radius: var(--r-sm);
	background: none;
	color: var(--body);
	font: inherit;
	font-size: var(--t-sm);
	line-height: 1;
	cursor: pointer;
}

.ed-btn:hover {
	background: var(--ground-2);
	color: var(--ink);
}

/* Lit when the caret is already inside this mark, so the bar reports the
   state of the text as well as changing it. */
.ed-btn.on {
	background: var(--accent-soft);
	color: var(--accent-ink);
}

.ed-btn svg {
	width: 15px;
	height: 15px;
	display: block;
}

/* The groups: marks, spans, lists, blocks. A hairline rather than a gap,
   because a gap at this size reads as a mistake. */
.ed-sep {
	width: 1px;
	align-self: stretch;
	margin: 3px 3px;
	background: var(--line);
}

/* The link panel, which replaced window.prompt. It sits between the toolbar
   and the writing surface rather than floating over it: there is nothing to
   read underneath it, and a panel in the flow cannot be clipped by the
   composer's own scroll. */
.ed-link {
	display: flex;
	gap: var(--s-3);
	align-items: center;
	flex-wrap: wrap;
	padding: var(--s-5) 11px;
	border-bottom: 1px solid var(--hair);
	background: var(--accent-soft);
}

.ed-link input {
	font: inherit;
	font-size: var(--t-sm);
	height: 28px;
	padding: 0 var(--s-5);
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	background: var(--surface);
	color: var(--ink);
	width: auto;
}

.ed-link .ed-link-url {
	flex: 1;
	min-width: 180px;
	font-family: var(--mono);
}

.ed-link .ed-link-text {
	flex: 1;
	min-width: 120px;
}

.ed-link input:focus {
	outline: 0;
	border-color: var(--accent);
}

/* The two controls at the end of the link row keep their own width: the
   inputs beside them are the parts that should take the slack. */
.ed-link-go,
.ed-link-off {
	flex: none;
}

/* Why the address was refused, in the same words the renderer would use. */
.ed-link-why:not(:empty) {
	flex-basis: 100%;
	font-size: var(--t-sm);
	color: var(--bad);
}

/* The three marks whose BUTTON should look like what it does. The rest are
   glyphs that already do. */
.ed-b {
	font-weight: var(--w-semi);
}

.ed-i {
	font-style: italic;
}

.ed-c {
	font-family: var(--mono);
	font-size: 11px;
}

.ed-body {
	padding: var(--s-6) 11px;
	min-height: 84px;
	max-height: 420px;
	overflow-y: auto;
	font-size: var(--t-md);
	line-height: 1.6;
	color: var(--ink);
	outline: 0;
}

/* The placeholder, which a contenteditable does not get for free. Shown only
   while the surface is genuinely empty: :empty alone is not enough, because
   the browser leaves a stray <br> behind the moment somebody types and
   deletes a character. */
.ed-body:empty::before,
.ed-body:has(> br:only-child)::before {
	content: attr(data-placeholder);
	color: var(--faint);
	pointer-events: none;
}

/* The composer renders what it is composing: the same vocabulary the thread
   shows, so what somebody types looks like what will be posted. */
.ed-body p,
.ed-body div {
	margin: 0 0 var(--s-5);
}

.ed-body ul,
.ed-body ol {
	margin: 0 0 var(--s-5);
	padding-left: var(--s-10);
}

.ed-body code {
	font-family: var(--mono);
	font-size: var(--t-sm);
	background: var(--sunk);
	border: 1px solid var(--hair);
	border-radius: var(--r-sm);
	padding: 1px 5px;
}

.ed-body pre {
	margin: 0 0 var(--s-5);
	background: var(--sunk);
	border: 1px solid var(--hair);
	border-radius: var(--r-sm);
	padding: var(--s-5) 11px;
	overflow-x: auto;
	font-family: var(--mono);
	font-size: var(--t-sm);
}

.ed-body blockquote {
	margin: 0 0 var(--s-5);
	padding: 2px 0 2px var(--s-6);
	border-left: 2px solid var(--line);
	color: var(--faint);
}

.ed-body > :last-child {
	margin-bottom: 0;
}

/* The textarea the editor is standing in front of. It keeps its name and its
   value and stays in the form; it is simply not where anybody is typing.
   Not display:none -- a hidden form control the browser still wants to
   validate is one it cannot focus, and that blocks submit with a message
   nobody can see. */
.ed-source {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* --- Empty states ---------------------------------------------------------
   An empty list gets a sentence and a way out of it. A blank panel is
   indistinguishable from one that failed to load. */

.empty {
	text-align: center;
	padding: 52px var(--s-9);
	border: 1px dashed var(--line);
	border-radius: var(--r);
	background: var(--surface);
}

.empty .eb {
	font-size: 22px;
	color: var(--faint);
	margin-bottom: var(--s-5);
}

.empty .et {
	font-size: var(--t-lg);
	color: var(--ink);
	letter-spacing: -0.02em;
	margin-bottom: var(--s-3);
}

.empty .ebody {
	font-size: var(--t-base);
	max-width: 44ch;
	margin: 0 auto var(--s-8);
}

/* --- Design annotations ---------------------------------------------------
   The reasoning behind a screen, shown only when the panel is put in notes
   mode. Same argument as .needs: addressed to us, never to a customer. */

.ann {
	display: none;
	margin: 11px 0 0;
	padding: 9px var(--s-6);
	border-left: 2px solid var(--accent);
	background: var(--accent-soft);
	border-radius: 0 var(--r-sm) var(--r-sm) 0;
	font-size: var(--s-6);
	color: var(--body);
}

.ann b {
	color: var(--accent-ink);
	font-weight: var(--w-medium);
}

body.notes .ann {
	display: block;
}

/* --- Command palette ------------------------------------------------------ */

.pal-back {
	position: fixed;
	inset: 0;
	background: color-mix(in srgb, var(--ground) 55%, transparent);
	backdrop-filter: blur(var(--s-5));
	display: none;
	z-index: 60;
	padding: 11vh var(--s-9) var(--s-9);
}

.pal-back.on {
	display: block;
	animation: fade var(--t-fast) var(--e-io) both;
}

@keyframes fade {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

.pal {
	max-width: 560px;
	margin: 0 auto;
	background: var(--raised);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-3);
	overflow: hidden;
	animation: palin 260ms var(--e-out) both;
}

@keyframes palin {
	from {
		opacity: 0;
		transform: translateY(-10px) scale(0.97);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

.pal input {
	width: 100%;
	border: 0;
	border-bottom: 1px solid var(--hair);
	background: none;
	padding: var(--s-8) var(--s-8);
	font-family: inherit;
	font-size: var(--t-lg);
	color: var(--ink);
	letter-spacing: -0.015em;
}

.pal input:focus {
	outline: none;
}

.pal-list {
	max-height: 330px;
	overflow-y: auto;
	padding: var(--s-3);
}

.pal-grp {
	font-size: var(--t-xs);
	letter-spacing: 0.15em;
	text-transform: uppercase;
	color: var(--faint);
	padding: var(--s-5) var(--s-6) 5px;
}

.pal-item {
	display: flex;
	align-items: center;
	gap: 11px;
	padding: 9px var(--s-6);
	border-radius: var(--r-sm);
	font-size: var(--t-md);
	color: var(--body);
	cursor: pointer;
	letter-spacing: -0.01em;
	text-decoration: none;
}

.pal-item .ic {
	width: var(--s-8);
	text-align: center;
	color: var(--faint);
	font-size: var(--s-6);
}

.pal-item .nm {
	color: var(--ink);
}

.pal-item .hint {
	margin-left: auto;
	font-size: var(--t-xs);
	color: var(--faint);
}

.pal-item.on {
	background: var(--accent-soft);
}

.pal-item.on .ic,
.pal-item.on .hint {
	color: var(--accent-ink);
}

.pal-empty {
	padding: 26px var(--s-7);
	text-align: center;
	color: var(--faint);
	font-size: var(--t-base);
}

/* The keys, stated once. A palette whose arrows are a secret is a search box
   somebody has to type their way to the bottom of. */
.pal-foot {
	margin: 0;
	padding: var(--s-4) var(--s-6);
	border-top: 1px solid var(--hair);
	font-size: var(--t-xs);
	color: var(--faint);
	text-align: center;
}

/* --- Modal -----------------------------------------------------------------
   A card that floats: the palette's surface, radius and shadow, so a dialog
   reads as part of the panel rather than as a system that interrupted it.
   Stated here to beat the legacy dialog rules above. */
dialog.modal {
	margin: auto;
	width: min(560px, calc(100vw - 2 * var(--s-9)));
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	background: var(--raised);
	color: var(--body);
	box-shadow: var(--sh-3);
	padding: 0;
}

dialog.modal[open] {
	animation: palin 260ms var(--e-out) both;
}

dialog.modal::backdrop {
	background: color-mix(in srgb, var(--ground) 55%, transparent);
	backdrop-filter: blur(10px);
}

dialog.modal .card-h {
	border-bottom: 1px solid var(--hair);
}

/* --- Notifications drawer ------------------------------------------------- */

.nback {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.2);
	display: none;
	z-index: 55;
}

.nback.on {
	display: block;
	animation: fade var(--t-fast) var(--e-io) both;
}

.ndraw {
	position: absolute;
	right: 0;
	top: 0;
	bottom: 0;
	width: min(390px, 92vw);
	background: var(--surface);
	border-left: 1px solid var(--line);
	box-shadow: var(--sh-3);
	animation: slidein 320ms var(--e-out) both;
	display: flex;
	flex-direction: column;
}

@keyframes slidein {
	from {
		transform: translateX(100%);
	}

	to {
		transform: none;
	}
}

.ndraw .nh {
	padding: var(--s-7) var(--s-8);
	border-bottom: 1px solid var(--hair);
	display: flex;
	align-items: center;
	gap: var(--s-5);
}

.ndraw .nh b {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-size: var(--s-7);
}

.ndraw .nh .right {
	margin-left: auto;
}

.ndraw .nb {
	overflow-y: auto;
	flex: 1;
}

.nitem {
	padding: var(--t-base) var(--s-8);
	border-bottom: 1px solid var(--hair);
	display: grid;
	grid-template-columns: auto 1fr;
	gap: 11px;
	cursor: pointer;
	text-decoration: none;
	transition: background var(--t-fast) var(--e-io);
}

.nitem:hover {
	background: var(--sunk);
}

.nitem .nd {
	width: 7px;
	height: 7px;
	border-radius: 50%;
	margin-top: var(--s-3);
	flex: none;
	background: var(--line);
}

.nitem.unread .nd {
	background: var(--accent);
}

/* The dot takes the colour of the thing it is about, so the drawer is
   skimmable by severity without reading a word of it. */
.nitem.attn .nd {
	background: var(--attn);
}

.nitem.bad .nd {
	background: var(--bad);
}

.nitem .nt {
	font-size: var(--t-base);
	color: var(--ink);
	letter-spacing: -0.014em;
}

.nitem .nm {
	font-size: var(--s-6);
	margin-top: 2px;
}

.nitem .nw {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-top: var(--s-2);
}

/* --- Toasts --------------------------------------------------------------- */

.toasts {
	position: fixed;
	right: var(--s-8);
	bottom: var(--s-8);
	z-index: 70;
	display: flex;
	flex-direction: column;
	gap: var(--s-4);
	/* Never wider than the screen minus its own inset: at 340px flat the
	   toast ran 6px past the right edge of a phone. */
	max-width: min(340px, calc(100vw - 2 * var(--s-8)));
}

.toast {
	background: var(--raised);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-3);
	padding: var(--t-base) 15px;
	font-size: var(--t-base);
	animation: toastin 380ms var(--e-spring) both;
}

.toast.out {
	animation: toastout 240ms var(--e-io) both;
}

@keyframes toastin {
	from {
		opacity: 0;
		transform: translateX(30px) scale(0.95);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

@keyframes toastout {
	to {
		opacity: 0;
		transform: translateX(var(--s-9));
	}
}

.toast .tt {
	color: var(--ink);
	font-weight: var(--w-medium);
	display: block;
	margin-bottom: 2px;
	letter-spacing: -0.014em;
}

.toast .bar {
	height: 2px;
	border-radius: 2px;
	background: linear-gradient(90deg, var(--accent), var(--accent-2));
	margin-top: var(--s-5);
	transform-origin: left;
	animation: drain 4.2s linear both;
}

@keyframes drain {
	from {
		transform: scaleX(1);
	}

	to {
		transform: scaleX(0);
	}
}

/* --- Auth ----------------------------------------------------------------- */

.authg {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
	gap: var(--s-7);
}

.authc {
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	padding: 22px var(--s-9);
	box-shadow: var(--sh-1);
}

.authc .al {
	font-size: var(--t-xs);
	letter-spacing: 0.15em;
	text-transform: uppercase;
	color: var(--faint);
	margin-bottom: var(--s-7);
}

.authc .at {
	font-size: var(--t-lg);
	color: var(--ink);
	letter-spacing: -0.03em;
	margin-bottom: 5px;
}

.authc .ad {
	font-size: var(--t-sm);
	margin-bottom: var(--s-7);
}

.codes {
	display: flex;
	gap: var(--s-3);
	margin: var(--s-6) 0;
}

.codes i {
	width: 34px;
	height: 42px;
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	display: grid;
	place-items: center;
	font-family: var(--mono);
	font-size: var(--s-8);
	color: var(--ink);
	font-style: normal;
	background: var(--surface);
}

.codes i.f {
	border-color: var(--accent);
	box-shadow: var(--halo);
}

/* --- Narrow ---------------------------------------------------------------
   Below 900 the rail becomes a drawer and the fleet row drops everything but
   the hostname and its state. The two things somebody scanning a phone needs
   are which machine and whether it is all right. */

@media (max-width: 900px) {
	.shell {
		grid-template-columns: 1fr;
	}

	.side {
		position: fixed;
		left: 0;
		top: 0;
		z-index: 50;
		width: var(--side);
		transform: translateX(-100%);
		/* Hidden, not just moved: off-screen links were still fourteen tab
		   stops a keyboard user walked through, unseen, before reaching the
		   Menu button. visibility waits for the slide out to finish. */
		visibility: hidden;
		transition:
			transform var(--t) var(--e-out),
			visibility 0s linear var(--t);
	}

	.side.open {
		transform: none;
		visibility: visible;
		transition: transform var(--t) var(--e-out);
	}

	.menubtn {
		display: grid !important;
	}

	.wiz {
		grid-template-columns: 1fr;
	}

	.summ {
		position: static;
	}

	.frow {
		grid-template-columns: 1fr auto;
	}

	.frow .meta,
	.frow .num,
	.frow .go {
		display: none;
	}

	/* A row that carries a control in its tail (Open the panel) folds it onto
	   a line of its own under the name and the state, rather than squeezing
	   it beside the pill or wrapping it under the name alone. */
	.frow .tail:has(> form) {
		grid-column: 1 / -1;
		justify-content: flex-start;
	}

	/* A tail holding only the chevron, which is hidden here, would still take
	   a line of its own and leave every row with an empty band at its foot. */
	.frow .tail:not(:has(> form)) {
		display: none;
	}
}

.menubtn {
	display: none;
}

/* ===========================================================================
   BRIDGE -- the last old names, wearing the new design.
   ===========================================================================
   A staging device, and nearly spent. It began by covering every un-ported
   page; the panel/row family it existed for is gone, and what remains is a
   short tail of names the templates still say -- .page-head on the shop pages,
   the table cell utilities, .pay, .said, the signed-out page frame.

   The rule has not changed: porting a page means rewriting its markup to the
   primitives, and these rules then simply stop matching. Nothing new goes in
   here. When no .templ emits any of these names the block goes entirely.
   =========================================================================== */

/* The page head: the same shape as .ph, reached through the old markup. */
.page-head {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	flex-wrap: wrap;
	margin-bottom: var(--s-9);
}

.page-head > div:first-child {
	min-width: 0;
}

.page-head h1 {
	font-family: var(--sans);
	font-size: var(--t-2xl);
	font-weight: var(--w-normal);
	letter-spacing: -0.045em;
	color: var(--ink);
	margin: 0;
}

.page-head p {
	font-size: var(--s-7);
	color: var(--body);
	margin: var(--s-2) 0 0;
	max-width: 64ch;
}

.page-head .actions,
.page-head > div:last-child:not(:first-child) {
	margin-left: auto;
}

.actions {
	display: flex;
	gap: 9px;
	flex-wrap: wrap;
	align-items: center;
}

/* The old saturated button is the new accent ramp. Everything that called
   itself primary meant "this is the one action here", which is exactly what
   the ramp is reserved for. */
.btn.primary {
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	border-color: transparent;
	color: #fff;
}

.btn.primary:hover {
	box-shadow: 0 6px 22px -6px var(--accent-glow), var(--sh-2);
}

/* Pay keeps the ink fill rather than the accent: when money is owed it has to
   out-shout the accent button that is also on the page. */
.btn.pay {
	background: var(--ink);
	border-color: var(--ink);
	color: var(--ground);
}

/* Panels are cards, and their rows are rows. */
.panel {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-1);
	margin-bottom: var(--s-8);
	overflow: hidden;
}

.panel-body > .row {
	border-bottom: 1px solid var(--hair);
}

.panel-body > .row:last-child {
	border-bottom: 0;
}

/* A settings row's three parts, in the vocabulary the new .row already uses. */
.row-label {
	font-size: var(--t-md);
	color: var(--ink);
	font-weight: var(--w-medium);
	letter-spacing: -0.014em;
	display: block;
}

.row-help {
	font-size: var(--t-sm);
	color: var(--body);
	margin: 3px 0 0;
	line-height: 1.45;
	max-width: 64ch;
}

/* A row that is only a sentence -- the page explaining itself rather than
   offering a control. It gets the sunk ground so it reads as commentary
   sitting inside the list, not as another item in it. */
.row.said {
	background: var(--sunk);
	display: block;
}

.row.said .row-help {
	margin: 0;
}

.row-act,
.row-acts {
	display: flex;
	align-items: center;
	gap: var(--s-2);
	justify-content: flex-end;
	flex-wrap: wrap;
}

/* Tables reached through the old wrapper. */
.table-wrap {
	overflow-x: auto;
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
}

.panel .table-wrap {
	border: 0;
	border-radius: 0;
}

/* The two that stay mono are the identifiers: an invoice number, a reference.
   A date and an amount are read as words and numbers, and set as them. */
td.col-id,
.mono-ref {
	font-family: var(--mono);
}

/* The quiet cells: a value that is subordinate, and a value that is absent. */
.cell-sub,
.col-dim,
td.col-num,
td.col-id,
td.col-when,
.mono-ref {
	font-size: var(--t-sm);
	color: var(--faint);
}

.cell-sub {
	display: block;
	margin-top: 2px;
}

.col-num,
.col-when {
	font-variant-numeric: tabular-nums;
}

/* The not-built marker shares the settings column's measure. A full-width red
   block above a 780px card reads as two different pages: the marker belongs to
   the thing under it, so it has to line up with it. */
.pending {
	width: 100%;
	max-width: 780px;
}

.panel .pending,
.panel-body .pending {
	max-width: none;
}

/* Drawn-but-not-built, wherever the old markup says so. */
.soon-chip {
	font-size: 12px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	border: 1px dashed var(--line);
	border-radius: var(--radius-pill);
	padding: 1px var(--s-3);
	color: var(--faint);
	font-weight: var(--w-normal);
}

.locked {
	opacity: 0.55;
}

/* Empty states through the old markup. */
.empty-lead {
	font-size: var(--t-lg);
	color: var(--ink);
	letter-spacing: -0.02em;
	margin: 0 0 var(--s-3);
}

.empty-body {
	font-size: var(--t-base);
	color: var(--body);
	max-width: 44ch;
	margin: 0 auto var(--s-8);
}

/* The signed-out pages: one centred column on the ground colour. */
.page {
	width: 100%;
}

.centre,
.auth {
	max-width: 460px;
	margin: 0 auto;
}

.wordmark {
	font-weight: var(--w-medium);
	letter-spacing: -0.032em;
	color: var(--ink);
	text-decoration: none;
}

.theme-icon {
	width: 15px;
	height: 15px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.4;
	stroke-linecap: round;
}


/* --- The shop, on the panel's own language.

   The front door sells three KINDS of server; each category then sells its
   plans in the shape that suits it -- cards for VPS, a size configurator for
   Minecraft, a stock table for dedicated. Everything reuses the wizard's
   tiles, cards and options so the walk from shop to panel changes nothing
   but the subject. */

/* The door is the heading and one line (portal review, October 2026): it
   used to take 240px before the first product, and the catalogue is what
   somebody came for. The trust marks moved beside the count, where they
   are read on the way to the cards. */
.store-hero {
	padding: 0 0 var(--s-10);
}

.store-hero h1 {
	font-size: var(--t-2xl);
	font-weight: var(--w-semi);
	letter-spacing: -0.045em;
	margin: 0 0 var(--s-3);
}

.store-hero .sub {
	margin: 0;
	color: var(--text-muted);
}

/* The front door: the whole catalogue beside a rail of filters. The rail
   is a form that works on its own; the script judges the same cards by the
   same rule without a round trip, and on a phone turns the rail into a
   sheet. Cards and stock rows are the configure page's own surfaces, so
   the walk from the door to the line changes nothing but the subject. */
.store-lead {
	max-width: var(--prose);
}
.store-hero .sub {
	font-size: var(--t-base);
	line-height: 1.5;
}
.store-claims {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-8);
	font-size: var(--t-sm);
	color: var(--body);
}
.store-claims li::before {
	content: "";
	display: inline-block;
	width: 6px;
	height: 10px;
	margin: 0 var(--s-5) 1px 0;
	border: solid var(--accent);
	border-width: 0 2px 2px 0;
	transform: rotate(45deg);
}
.cat {
	display: grid;
	grid-template-columns: 264px minmax(0, 1fr);
	gap: var(--s-11);
	align-items: start;
}
.cat-main {
	min-width: 0;
	display: grid;
	gap: var(--s-11);
}
.cat-rail {
	position: sticky;
	top: 70px;
	display: grid;
	gap: var(--s-8);
	padding: var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
}
.cat-rail-h {
	display: none;
}
/* A line the other filters have emptied: still pressable (it says what it
   would show), but quiet, so it does not invite a press into nothing. */
/* A line the other filters emptied. Its 0 already says so; the name goes
   quiet but stays readable, since pressing it is still a fair thing to do
   (opacity .45 put it at 2:1). */
.cat-opt.none > span {
	color: var(--faint);
}
.cat-f {
	display: grid;
	gap: var(--s-5);
	padding-bottom: var(--s-8);
	border-bottom: 1px solid var(--hair);
}
.cat-f:last-of-type {
	border-bottom: 0;
	padding-bottom: 0;
}
/* A filter that opens and closes: its name is the row, with a chevron at
   the end that turns when it is open. The rail is then as tall as what
   is in use, so it sticks without a scroll of its own; on a screen too
   short for even that, it scrolls with the page instead of hiding its
   foot. */
details.cat-f {
	display: block;
}
.cat-f > summary {
	list-style: none;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
	min-height: 24px;
	border-radius: 6px;
}
.cat-f > summary::-webkit-details-marker {
	display: none;
}
.cat-f > summary::after {
	content: "";
	flex: none;
	width: 7px;
	height: 7px;
	margin: 0 4px 2px 0;
	border-right: 1.5px solid var(--faint);
	border-bottom: 1.5px solid var(--faint);
	transform: rotate(45deg);
	transition: transform var(--t-fast) var(--e-io);
}
.cat-f[open] > summary::after {
	transform: rotate(-135deg);
	margin-bottom: -3px;
}
.cat-f > summary:hover h4 {
	color: var(--body);
}
.cat-f > summary:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}
.cat-f > summary.cat-fh b {
	margin-right: var(--s-4);
}
.cat-fb {
	margin-top: var(--s-5);
}
.cat-fb.cat-chips {
	display: flex;
}
.cat-fb.cat-opts {
	display: grid;
}
@media (max-height: 820px) {
	.cat-rail {
		position: static;
	}
}
.cat-f h4,
.cat-fh label {
	margin: 0;
	font-size: 12px;
	font-weight: var(--w-medium);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--faint);
}
.cat-fh {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--s-6);
}
.cat-fh b {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
	/* "Any budget" is two words that broke onto two lines in the rail's
	   narrowest layout; the label beside it may wrap, the figure not. */
	white-space: nowrap;
}
.cat-opts {
	display: grid;
	gap: 2px;
}
/* One line of the Browse list: the name with how many it would show, and
   what it starts from underneath. A line's name is a whole phrase and is
   never cut, so the price takes the second line rather than fighting it
   for the first. The chosen one is a filled row, not a ring: the list is
   one control, and the ring belongs to cards. */
.cat-opt {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-areas: "name count" "from from";
	gap: 1px var(--s-5);
	align-items: baseline;
	padding: var(--s-4) var(--s-5);
	border-radius: var(--r-sm);
	font-size: var(--t-base);
	color: var(--body);
	cursor: pointer;
	position: relative;
}
.cat-opt:hover {
	background: var(--sunk);
	color: var(--ink);
}
/* Chosen follows the checked radio, so it is right with no script, and
   moves when the choice does. */
.cat-opt:has(input:checked) {
	background: var(--accent-soft);
	color: var(--ink);
	font-weight: var(--w-medium);
}
.cat-opt:focus-within,
.cat-chip:focus-within,
.cat-check:focus-within {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}
.cat-opt em,
.cat-opt i {
	font-style: normal;
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}
.cat-opt > span {
	grid-area: name;
}
.cat-opt em {
	grid-area: from;
}
.cat-opt i {
	grid-area: count;
	min-width: 1.5em;
	text-align: right;
}
.cat-chips {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3);
}
.cat-chip {
	padding: var(--s-2) var(--s-6);
	border: 1px solid var(--line);
	border-radius: 999px;
	font-size: var(--t-base);
	color: var(--body);
	background: transparent;
	cursor: pointer;
	position: relative;
	font-variant-numeric: tabular-nums;
}
.cat-chip:hover {
	border-color: var(--border-strong);
	color: var(--ink);
}
.cat-chip:has(input:checked) {
	background: var(--accent-fill);
	border-color: var(--accent);
	color: #fff;
}
/* The slider is the platform's, in the accent, with the rungs it stops at
   written under it; the dearest month on the page is the last rung and Any
   is the one past it. */
.cat-range {
	width: 100%;
	margin: var(--s-3) 0 0;
	accent-color: var(--accent);
	cursor: pointer;
}
.cat-ticks {
	display: flex;
	justify-content: space-between;
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}
.cat-check {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	font-size: var(--t-md);
	color: var(--ink);
	cursor: pointer;
}
/* The box is drawn, not the platform's: a sunk square with the page's
   radius, filled in the accent with a white tick when on, so it matches
   the chips beside it in both themes. The input stays a real checkbox for
   the keyboard and the screen reader. The checkout's boxes (keys, terms,
   start now, save the card) are the same control. */
.key-check input[type="checkbox"],
.cat-check input[type="checkbox"] {
	appearance: none;
	flex: none;
	width: 18px;
	height: 18px;
	border: 1px solid var(--border-strong);
	border-radius: 5px;
	background: var(--sunk);
	display: grid;
	place-content: center;
	cursor: pointer;
	transition: background var(--t-fast) var(--e-io), border-color var(--t-fast) var(--e-io);
}
.key-check input[type="checkbox"]::after,
.cat-check input[type="checkbox"]::after {
	content: "";
	width: 5px;
	height: 9px;
	margin-top: -2px;
	border: solid #fff;
	border-width: 0 2px 2px 0;
	transform: rotate(45deg) scale(0);
	transition: transform var(--t-fast) var(--e-io);
}
.key-check input[type="checkbox"]:checked,
.cat-check input[type="checkbox"]:checked {
	background: var(--accent-fill);
	border-color: var(--accent);
}
.key-check input[type="checkbox"]:checked::after,
.cat-check input[type="checkbox"]:checked::after {
	transform: rotate(45deg) scale(1);
}
.cat-check input {
	width: 16px;
	height: 16px;
	margin: 0;
	accent-color: var(--accent);
}
.cat-f .hint {
	margin: 0;
	padding-left: calc(16px + var(--s-5));
}
.cat-foot {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--s-6);
	padding-top: var(--s-6);
	border-top: 1px solid var(--hair);
	font-size: var(--t-base);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}
.cat-foot b {
	color: var(--ink);
	font-weight: var(--w-medium);
}
.cat-foot a {
	color: var(--accent-ink);
}
.cat-apply {
	display: none;
}
/* The line over the catalogue: the count and the chips on the left, the
   small print on the right. A chip is a filter that is on, and pressing it
   is how it goes off. */
.cat-bar {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--s-6) var(--s-9);
	flex-wrap: wrap;
	padding-bottom: var(--s-6);
	border-bottom: 1px solid var(--line);
}
.cat-count {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--s-4);
	font-size: var(--t-base);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}
.cat-count > span {
	margin-right: var(--s-4);
}
.cat-count b {
	color: var(--ink);
	font-weight: var(--w-medium);
}
.cat-chipx {
	display: inline-flex;
	align-items: center;
	gap: var(--s-3);
	/* 28px tall, 32px on a phone: a target, not a label (WCAG 2.5.8). */
	min-height: 28px;
	padding: 2px var(--s-5);
	border: 1px solid var(--line);
	border-radius: 999px;
	font-family: var(--ui);
	font-size: var(--t-sm);
	color: var(--ink);
	background: var(--surface);
	text-decoration: none;
}
.cat-chipx .cx {
	color: var(--faint);
}
.cat-chipx:hover {
	border-color: var(--border-strong);
}
.cat-chipx:hover .cx {
	color: var(--ink);
}
@media (max-width: 600px) {
	.cat-chipx {
		min-height: 32px;
	font-size: var(--t-sm);
	}
}
.cat-right {
	display: flex;
	align-items: center;
	gap: var(--s-7);
	/* Stays at the right even when the chips wrap it onto its own row. */
	margin-left: auto;
}
.cat-units {
	font-size: var(--t-sm);
	color: var(--faint);
}
.cat-filters {
	display: none;
}
.cat-filters i {
	font-style: normal;
	font-size: var(--t-sm);
	min-width: 18px;
	height: 18px;
	padding: 0 5px;
	border-radius: 999px;
	background: var(--accent-fill);
	color: #fff;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}
.cat-none {
	display: grid;
	justify-items: center;
	gap: var(--s-3);
	padding: var(--s-12) var(--s-8);
	border: 1px dashed var(--line);
	border-radius: var(--r);
	text-align: center;
	color: var(--body);
}
.cat-none b {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-size: var(--t-lg);
}
.cat-none .btn {
	margin-top: var(--s-5);
}
/* What a line is made of, in the operator's few words, as chips under the
   sentence. They are facts, not states, so they wear no colour; and they
   are words, not identifiers, so they are set in the sans like the rest. */
.cat-made {
	list-style: none;
	margin: var(--s-4) 0 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3);
}
.cat-made li {
	padding: 2px var(--s-5);
	border: 1px solid var(--line);
	border-radius: 999px;
	font-family: var(--sans);
	font-size: var(--t-sm);
	color: var(--body);
	background: var(--sunk);
	white-space: nowrap;
}
main.canvas > .lede + .cat-made {
	margin-top: var(--s-5);
}
.cat-meta {
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}
.cat-price {
	font-size: 22px;
	font-weight: var(--w-medium);
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}
.cat-more {
	justify-self: start;
	font-size: var(--t-md);
	color: var(--body);
	text-decoration: none;
}
.cat-more::after {
	content: " →";
}
.cat-more:hover {
	color: var(--ink);
}
@media (max-width: 1100px) {
	.cat {
		grid-template-columns: 232px minmax(0, 1fr);
		gap: var(--s-9);
	}
}
/* A phone: the rail is a sheet the Filters button opens, and with no script
   the same button targets it. */
@media (max-width: 900px) {
	.cat {
		grid-template-columns: minmax(0, 1fr);
	}
	.cat-filters {
		display: inline-flex;
	}
	.cat-rail {
		display: none;
	}
	.cat.sheet .cat-rail,
	.cat-rail:target {
		display: grid;
		position: fixed;
		inset: 0;
		z-index: 60;
		max-height: none;
		overflow: auto;
		overscroll-behavior: contain;
		border: 0;
		border-radius: 0;
		padding: var(--s-8) var(--s-8) calc(var(--s-8) + env(safe-area-inset-bottom, 0px));
		align-content: start;
	}
	.cat-rail-h {
		display: flex;
		justify-content: space-between;
		align-items: center;
		font-size: var(--t-lg);
	}
	.cat-apply {
		display: block;
		position: sticky;
		bottom: 0;
		padding-top: var(--s-6);
		background: var(--surface);
	}
	.cat-units {
		display: none;
	font-size: var(--t-sm);
	}
}
@media (max-width: 700px) {}
@media (max-width: 560px) {}

/* The quiet reasons under the lines: true sentences, no icons shouting. */
.store-why {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
	gap: var(--s-6) var(--s-9);
	margin-top: var(--s-10);
	padding-top: var(--s-8);
	border-top: 1px solid var(--line);
	font-size: var(--t-sm);
	color: var(--text-muted);
	line-height: 1.55;
}

.store-why b {
	display: block;
	color: var(--text);
	font-weight: var(--w-semi);
	margin-bottom: 2px;
}

/* The category shelf's card keeps its price and button pinned to the floor,
   so a short description does not float them at a different height to the
   card beside it. */
.plan-foot {
	margin-top: auto;
	display: flex;
	flex-direction: column;
	gap: var(--s-5);
	padding-top: var(--s-6);
	border-top: 1px solid var(--line);
}

.plan-foot .price {
	margin: 0;
}

/* --- Minecraft: the size configurator ------------------------------------ */

.shop-split {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 330px;
	gap: var(--s-7);
	align-items: start;
}

@media (max-width: 940px) {
	.shop-split {
		grid-template-columns: 1fr;
	}
}

.sizerow {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(86px, 1fr));
	gap: var(--s-4);
	margin-bottom: var(--s-7);
}

.sizebtn {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	padding: var(--s-5) var(--s-4);
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r);
	cursor: pointer;
	font: inherit;
	color: var(--text-muted);
	transition: border-color var(--t-fast) var(--e-io), color var(--t-fast) var(--e-io), transform var(--t-fast) var(--e-out);
}

.sizebtn b {
	font-size: var(--t-md);
	color: var(--text);
	font-weight: var(--w-semi);
}

.sizebtn span {
	font-size: var(--t-xs);
	letter-spacing: 0.04em;
}

.sizebtn:hover {
	transform: translateY(-1px);
	border-color: color-mix(in srgb, var(--ink) 20%, var(--line));
}

.sizebtn.on {
	border-color: var(--accent);
	box-shadow: inset 0 0 0 1px var(--accent);
	color: var(--accent);
}

.sizebtn.on b {
	color: var(--accent);
}

.tierpane .pitch {
	margin: 0 0 var(--s-6);
	font-size: var(--t-md);
	color: var(--text-muted);
}

.included {
	margin-top: var(--s-6);
	padding: var(--s-5) var(--s-6);
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r);
	display: flex;
	align-items: baseline;
	gap: var(--s-6);
	flex-wrap: wrap;
}

.inc-t {
	font-size: var(--t-xs);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--text-faint);
}

.included ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-7);
	font-size: var(--t-sm);
	color: var(--text-muted);
}

.included b {
	color: var(--text);
	font-variant-numeric: tabular-nums;
	font-weight: var(--w-semi);
}

.tier-foot {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--s-6);
	margin-top: var(--s-7);
	padding-top: var(--s-6);
	border-top: 1px solid var(--line);
}

.tier-foot .price {
	margin: 0;
}

.tier-foot .price b {
	font-size: 26px;
	letter-spacing: -0.02em;
	font-variant-numeric: tabular-nums;
}

.tier-foot .price span {
	color: var(--text-faint);
	font-size: var(--t-sm);
}

.recc .wiz-q:first-of-type {
	margin-top: 0;
}

.opts.row3 {
	grid-template-columns: repeat(3, 1fr);
}

.opts.row2 {
	grid-template-columns: repeat(2, 1fr);
}

.rec-out {
	margin: var(--s-6) 0 var(--s-4);
	padding: var(--s-4) var(--s-5);
	border: 1px dashed var(--line);
	border-radius: var(--r);
	font-size: var(--t-md);
}

.rec-out span {
	color: var(--text-faint);
	margin-right: 6px;
}

/* --- Dedicated: the stock shelf ------------------------------------------ */

.stock-filters {
	display: flex;
	gap: var(--s-4);
	margin-left: auto;
}

.input.sm {
	padding: 5px 9px;
	font-size: var(--t-sm);
}

.stock-scroll {
	overflow-x: auto;
}

.stock {
	width: 100%;
	min-width: 760px;
	border-collapse: collapse;
}

.stock th {
	text-align: left;
	font-size: var(--t-xs);
	font-weight: var(--w-medium);
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--text-faint);
	padding: var(--s-5) var(--s-8);
	border-bottom: 1px solid var(--line);
	white-space: nowrap;
}

.stock td {
	padding: var(--s-6) var(--s-8);
	border-bottom: 1px solid var(--line);
	vertical-align: middle;
	white-space: nowrap;
	font-size: var(--t-md);
}

.stock tbody tr {
	transition: background var(--t-fast) var(--e-io);
}

.stock tbody tr:hover {
	background: var(--surface);
}

.stock tbody tr:last-child td {
	border-bottom: 0;
}

.stock td.cpu {
	font-weight: var(--w-semi);
	color: var(--text);
}

.stock td.mono-c {
	font-family: var(--mono);
	font-size: var(--t-sm);
	color: var(--text-muted);
}

.stock td.quiet-c {
	color: var(--text-faint);
	font-size: var(--t-sm);
}

.stock th.num,
.stock td.num {
	text-align: right;
}

.stock td.num b {
	font-variant-numeric: tabular-nums;
	letter-spacing: -0.01em;
}

.stock td.act {
	text-align: right;
}

.stock td.act .pill {
	margin-right: var(--s-4);
}

.stock-note {
	margin-top: var(--s-6);
}

/* The VPS shelf, brought up to the same texture as the rest of the shop:
   mono figures, a lift on hover, the price at reading scale. The buttons
   keep the shelf's hush -- that rule stands. */
.plan {
	transition: transform var(--t-fast) var(--e-out), border-color var(--t-fast) var(--e-io), box-shadow var(--t-fast) var(--e-io);
}

.plan:hover {
	transform: translateY(-3px);
	border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

.plan .specs b {
	font-size: var(--t-sm);
	font-variant-numeric: tabular-nums;
}

.plan-foot .price b {
	font-size: 24px;
	letter-spacing: -0.02em;
}

/* --- Networking: the firewall posture page -------------------------------- */

.fw-right {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--s-4);
}

.fw-none {
	margin: 0;
	font-size: var(--t-md);
	color: var(--text-muted);
}

/* --- The account page's security pane -------------------------------------- */

.stackform {
	display: flex;
	flex-direction: column;
	gap: var(--s-5);
	max-width: 420px;
}

.stackform .btn {
	align-self: flex-start;
}

.inline-form {
	display: inline-block;
	margin-top: var(--s-4);
}

.secret-code {
	margin: var(--s-4) 0;
}

/* The enrolment QR code. White in both themes, because the code has to be
   dark on light to scan everywhere; its own four-module margin is inside the
   viewBox, so the tile needs no padding. Big enough for a phone camera at
   arm's length from a laptop, never wider than the card on a phone. */
.qr {
	display: block;
	width: min(208px, 100%);
	height: auto;
	margin: var(--s-4) 0;
	background: #fff;
	border: 1px solid var(--line);
	border-radius: var(--r);
}

.qr path {
	fill: #000;
}

.secret-code code {
	display: inline-block;
	font-family: var(--mono);
	font-size: var(--t-md);
	letter-spacing: 0.08em;
	padding: var(--s-4) var(--s-6);
	background: var(--surface);
	border: 1px dashed var(--line);
	border-radius: var(--r);
	word-break: break-all;
}

/* Recovery codes, shown once: two columns of mono, easy to copy or print. */
.recovery-codes {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
	gap: var(--s-3);
	margin: var(--s-4) 0;
	padding: var(--s-4);
	list-style: none;
	background: var(--surface);
	border: 1px dashed var(--line);
	border-radius: var(--r);
}

.recovery-codes code {
	font-family: var(--mono);
	font-size: var(--t-md);
	letter-spacing: 0.06em;
}

/* The shown-once block: the codes, then the ways to keep them, then the
   acknowledgement that puts them away. */
.codes-once {
	margin-bottom: var(--s-6);
}

.linkform {
	margin-top: var(--s-5);
	max-width: 420px;
}

/* Two federation doors stack; one would sit alone just as happily. */
.oauth-row {
	display: flex;
	flex-direction: column;
	gap: var(--s-4);
}

/* A keyboard shortcut is no use on a touch screen. */
@media (pointer: coarse) {
	.kbd {
		display: none;
	}
}

/* Settings on a phone: the label column and the input stacked, not
   squeezed side by side into two-word lines. */
@media (max-width: 600px) {
	.row:has(> .acts .field) {
		grid-template-columns: minmax(0, 1fr);
	}
	.row > .acts .field input,
	.row > .acts .field select {
		width: 100%;
	}
	select {
		padding-right: 32px;
	}
}

/* --- The shop bar ----------------------------------------------------------
   The signed-out frame's bar, laid out as darkless.cloud's is (David,
   2026-11-06) so the two sites read as one: menus at 15px with a chevron,
   a status pill, the way in as the portal's own button. Below 1000px the
   menus fold into one button and a sheet under the bar. */
.sb {
	position: sticky;
	top: 0;
	z-index: 30;
	border-bottom: 1px solid var(--hair);
	background: var(--ground);
}

@supports (backdrop-filter: blur(1px)) {
	.sb {
		background: color-mix(in srgb, var(--ground) 82%, transparent);
		backdrop-filter: blur(14px) saturate(1.4);
		-webkit-backdrop-filter: blur(14px) saturate(1.4);
	}
}

.sb-in {
	display: flex;
	align-items: center;
	gap: var(--s-8);
	height: 64px;
	padding: 0 var(--gutter);
}

.sb .brand {
	flex: none;
}

.sb-nav {
	display: none;
	align-items: center;
}

.sb-dd {
	position: relative;
}

.sb-l {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	height: 40px;
	padding: 0 var(--s-5);
	border-radius: var(--radius-control);
	color: var(--body);
	font-size: 15px;
	text-decoration: none;
	white-space: nowrap;
}

.sb-l:hover,
.sb-l[aria-expanded="true"] {
	color: var(--ink);
}

.sb-chev {
	width: 16px;
	height: 16px;
	flex: none;
	transition: transform 200ms var(--e-out);
}

[aria-expanded="true"] > .sb-chev {
	transform: rotate(180deg);
}

.sb-r {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--s-4);
}

.sb-status {
	display: none;
	align-items: center;
	height: 32px;
	padding: 0 var(--s-5);
	border: 1px solid var(--line);
	border-radius: var(--radius-control);
	font-family: var(--mono);
	font-size: 11px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--faint);
	text-decoration: none;
}

.sb-status:hover {
	color: var(--ink);
	border-color: var(--ink);
}

.sb-plain {
	display: inline-flex;
	align-items: center;
	height: 40px;
	padding: 0 var(--s-4);
	color: var(--body);
	font-size: 15px;
	font-weight: var(--w-semi);
	text-decoration: none;
	white-space: nowrap;
}

.sb-plain:hover {
	color: var(--ink);
}

.sb-wide {
	display: none;
}

.sb-in-btn {
	gap: 4px;
	white-space: nowrap;
}

.sb-acct {
	display: none;
}

.sb-burger {
	display: inline-grid;
	place-items: center;
	width: 40px;
	height: 40px;
	border: 0;
	border-radius: var(--radius-control);
	background: none;
	color: var(--body);
	cursor: pointer;
}

.sb-burger svg {
	width: 20px;
	height: 20px;
}

.sb-menu {
	position: absolute;
	top: calc(100% + var(--s-3));
	left: 0;
	z-index: 40;
	background: var(--raised);
	border: 1px solid var(--line);
	border-radius: var(--r);
	box-shadow: var(--sh-3);
}

.sb-prod {
	width: 480px;
}

.sb-co {
	width: 200px;
	padding: var(--s-2);
}

.sb-signin {
	left: auto;
	right: 0;
	width: 220px;
	padding: var(--s-2);
}

.sb-cols {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--s-9);
	padding: var(--s-8) var(--s-9) var(--s-4);
}

.sb-h {
	display: block;
	margin-bottom: var(--s-3);
	font-family: var(--mono);
	font-size: 10.5px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--faint);
}

.sb-mi {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	gap: 2px var(--s-5);
	padding: var(--s-4) 0 var(--s-5);
	text-decoration: none;
}

.sb-mi b {
	color: var(--ink);
	font-weight: var(--w-semi);
	font-size: var(--t-sm);
}

.sb-mi:hover b {
	color: var(--accent-ink);
}

.sb-mi em {
	font-family: var(--mono);
	font-style: normal;
	font-size: 12.5px;
	color: var(--body);
	white-space: nowrap;
}

.sb-mi em.out {
	color: var(--faint);
}

.sb-mi span {
	grid-column: 1 / -1;
	color: var(--faint);
	font-size: 12.5px;
	line-height: 1.45;
}

.sb-mf {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-6) var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: 12.5px;
	color: var(--faint);
}

.sb-mf a {
	color: var(--ink);
	font-weight: var(--w-medium);
	text-decoration: none;
	white-space: nowrap;
}

.sb-ml {
	display: flex;
	justify-content: space-between;
	padding: var(--s-4) var(--s-5);
	border-radius: var(--radius-control);
	color: var(--ink);
	font-size: var(--t-sm);
	text-decoration: none;
}

.sb-ml:hover {
	background: var(--sunk);
}

.sb-ml i {
	font-style: normal;
	color: var(--faint);
}

.sb-sheet {
	display: flex;
	flex-direction: column;
	padding: var(--s-3) var(--gutter) var(--s-8);
	border-top: 1px solid var(--hair);
	background: var(--surface);
}

.sb-sheet a {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-5) 0;
	border-bottom: 1px solid var(--hair);
	color: var(--ink);
	text-decoration: none;
}

.sb-sheet a em {
	font-family: var(--mono);
	font-style: normal;
	font-size: 13px;
	color: var(--faint);
}

.sb-sheet-in {
	display: grid;
	gap: var(--s-4);
	margin-top: var(--s-8);
}

.sb-sheet .sb-sheet-in a {
	justify-content: center;
	padding: 0 var(--s-6);
	border-bottom: 0;
}

.sb-sheet .sb-h {
	margin: var(--s-8) 0 0;
}

@media (min-width: 720px) {
	.sb-status,
	.sb-wide {
		display: inline-flex;
	}
}

@media (min-width: 1000px) {
	.sb-nav {
		display: flex;
	}

	.sb-acct {
		display: block;
	}

	.sb-burger,
	.sb-sheet {
		display: none !important;
	}
}

/* --- The site foot and the legal pages ------------------------------------
   The signed-out frame's foot, laid out as darkless.cloud's is (David,
   2026-11-06): who we are and where, four columns of places, and the
   company's identity (SI 2015/17 wants it on every page) under them. Who
   to write to about abuse or a hole is on the Contact page Legal links. */
/* There when somebody looks for Terms or Contact, out of the way
   otherwise (David, 2026-11-06: "I don't really want it to be so
   visible"): small, faint and tight. */
.site-foot {
	width: 100%;
	padding: var(--s-10) var(--gutter) var(--s-8);
	border-top: 1px solid var(--hair);
	font-size: 13px;
	color: var(--faint);
	line-height: 1.55;
}

.site-foot p {
	margin: 0;
}

.sf-grid {
	display: grid;
	gap: var(--s-8);
}

.sf-who .logo {
	height: 18px;
	opacity: 0.7;
}

.sf-who p {
	margin: var(--s-4) 0;
	max-width: 32ch;
}

.sf-cols {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--s-8) var(--s-8);
}

.sf-h {
	display: block;
	margin-bottom: var(--s-3);
	opacity: 0.8;
	font-family: var(--mono);
	font-size: 10px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--faint);
}

.sf-cols ul,
.sf-legal {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	margin: 0;
	padding: 0;
	list-style: none;
}

.sf-cols a {
	color: var(--faint);
	text-decoration: none;
}

.sf-cols a:hover {
	color: var(--ink);
}

.site-foot-company {
	margin-top: var(--s-8) !important;
	padding-top: var(--s-5);
	border-top: 1px solid var(--hair);
	font-size: 12px;
	opacity: 0.85;
}

@media (min-width: 900px) {
	.sf-grid {
		grid-template-columns: minmax(0, 1.2fr) minmax(0, 3fr);
		gap: var(--s-11);
	}

	.sf-cols {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

/* The signed-out frame is at least the window's height, so its footer sits
   at the bottom of a short page rather than halfway up it. */
.frame-out {
	--shop: 1680px;
	--focus: 1180px;
	min-height: 100dvh;
	display: flex;
	flex-direction: column;
	background:
		radial-gradient(700px 360px at 78% -60px, var(--wash-a), transparent 70%),
		radial-gradient(620px 320px at 12% -80px, var(--wash-b), transparent 70%),
		var(--ground);
	background-repeat: no-repeat;
}

/* The column the shop keeps on any screen (David, 2026-11-06: on a
   2560x1440 monitor "things are too wide"). Two widths: the catalogue and
   the configure page have the most to show and take the wide one, with the
   bar and the foot; the checkout and the pages like it have less, and keep
   to a narrower, focused one ("it is okay if this page takes more space ...
   when you get into checkout ... should be more focused"): the bar, the page and the foot
   share it, centred, so a wide screen gets margins rather than a stretched
   page. */
.frame-out > main,
.frame-out .sb-in {
	width: 100%;
	max-width: calc(var(--shop) + 2 * var(--gutter));
	margin-inline: auto;
}

.frame-out .site-foot > * {
	max-width: var(--shop);
	margin-inline: auto;
}

.frame-out > main.page {
	max-width: calc(var(--focus) + 2 * var(--gutter));
}

.frame-out > .site-foot {
	margin-top: auto;
}

/* The legal links and company line at the foot of the account menu, which
   is where the signed-in frame keeps them instead of a footer. */
.me-legal {
	margin-top: var(--s-2);
	padding: var(--s-4) var(--s-5) var(--s-3);
	border-top: 1px solid var(--hair);
	font-size: var(--t-xs);
	color: var(--faint);
	line-height: 1.5;
}

.me-legal-links {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-2) var(--s-5);
}

.me-legal-links a {
	color: var(--body);
	text-decoration: none;
}

.me-legal-links a:hover {
	color: var(--ink);
}

.me-legal p {
	margin: var(--s-3) 0 0;
}

@media (max-width: 600px) {
	.site-foot {
		padding: var(--s-9) var(--s-8) var(--s-10);
	}
}

/* A legal document: prose at a reading measure, headings on the panel's
   scale. The markup inside comes from richtext.Document, so the element
   selectors here are the whole vocabulary it can emit. */
.legal-doc {
	max-width: 72ch;
	font-size: var(--t-base);
	line-height: 1.65;
	color: var(--body);
}

.legal-doc h2,
.legal-doc h3,
.legal-doc h4 {
	color: var(--ink);
	font-weight: 600;
	letter-spacing: -0.015em;
	line-height: 1.3;
	margin: var(--s-11) 0 var(--s-5);
}

.legal-doc h2 {
	font-size: var(--t-lg);
}

.legal-doc h3 {
	font-size: var(--t-lg);
}

.legal-doc h4 {
	font-size: var(--t-md);
}

.legal-doc > :first-child {
	margin-top: 0;
}

.legal-doc p,
.legal-doc ul,
.legal-doc ol,
.legal-doc blockquote,
.legal-doc pre {
	margin: 0 0 var(--s-8);
}

.legal-doc ul,
.legal-doc ol {
	padding-left: var(--s-10);
}

.legal-doc blockquote {
	padding-left: var(--s-8);
	border-left: 2px solid var(--line);
}

.legal-doc a {
	color: var(--accent-ink);
}

.legal-doc a:hover {
	color: var(--accent);
}

.legal-meta {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
}

/* The two promises a buyer makes at the till: the boxes sit on their own,
   above the button they unlock. */
.consent {
	display: grid;
	gap: var(--s-5);
	margin: 0;
	padding: 0;
	border: 0;
}

.consent legend {
	padding: 0;
	margin-bottom: var(--s-3);
	font-size: var(--t-sm);
	color: var(--body);
}

.consent .key-check a {
	color: var(--accent-ink);
}

/* --- Running (October 2026) ------------------------------------------------
   The state of a server, said where it is read. The pill kind "run" is the
   one green on the site; a kind "ok" stays plain, as it always did, so Paid
   and Done do not light up. The dot is the same colour without the word,
   for the start of a row in a list of servers: one glance down the column
   finds the row that is not green. The halo is what makes a 10px dot read
   at arm's length on a 1920px screen. */
.pill.run {
	color: var(--run);
	background: var(--run-soft);
	border-color: color-mix(in srgb, var(--run) 35%, var(--line));
}

.pill.run::before {
	background: var(--run);
	box-shadow: 0 0 0 3px var(--run-glow);
}

.sdot {
	display: inline-block;
	flex: none;
	width: 10px;
	height: 10px;
	border-radius: 50%;
	background: var(--faint);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--faint) 22%, transparent);
	vertical-align: middle;
}

.sdot.run {
	background: var(--run);
	box-shadow: 0 0 0 3px var(--run-glow);
}

.sdot.attn {
	background: var(--attn);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--attn) 22%, transparent);
}

.sdot.bad {
	background: var(--bad);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad) 22%, transparent);
}

.sdot.working {
	background: var(--accent);
	box-shadow: 0 0 0 3px var(--accent-glow);
}

.sdot.off {
	background: var(--faint);
}

/* A game server the panel reports simply there: the accent, not green,
   because the panel's key cannot tell running from offline and green on
   this site means running (2026-11-03). */
.sdot.active {
	background: var(--accent);
}

.sdot.unknown {
	background: var(--line);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--line) 50%, transparent);
}

/* --- Shop and the OS picker (October 2026) ---------------------------------
   The operating system control, the configure page's and the Reinstall
   sheet's alike: one row of families, one row of releases, one sentence.
   The chosen family is a ring on a raised surface rather than another blue
   box, so the page is not a column of blue boxes; the chosen release is
   the one filled chip. A release past support is dashed and refuses the
   press. The control measures its own box, so it wraps to three a row in
   a sheet and scrolls on a phone without being told where it is. */
.cfg-os {
	container-type: inline-size;
	display: grid;
	gap: var(--s-7);
}
.cfg-osfs {
	display: flex;
	gap: var(--s-5);
	/* Room for the focus ring, which the scrolling case would clip. */
	padding: var(--s-1);
	margin: calc(-1 * var(--s-1));
}
/* Two classes deep on purpose: in the Reinstall sheet the control sits in a
   .field, whose own label rule would otherwise stack the tile's mark over
   its name and turn the chips back into blocks. */
.cfg-os .cfg-osf {
	flex: 1 1 0;
	min-width: 0;
	display: flex;
	align-items: center;
	gap: var(--s-6);
	height: 56px;
	margin: 0;
	padding: 0 var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	background: var(--surface);
	color: var(--ink);
	font-size: var(--t-base);
	cursor: pointer;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}
.cfg-osf .oslogo {
	flex: none;
	width: 28px;
	height: 28px;
}
.cfg-osf b {
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	line-height: 1.2;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.cfg-osf:hover {
	border-color: var(--border-strong);
	background: var(--raised);
}
/* The chosen family wears the same blue as the chosen size and term
   above it: one colour for "this one" down the whole page. */
.cfg-osf.on {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}
.cfg-osf.off {
	border-style: dashed;
	color: var(--faint);
	cursor: not-allowed;
}
.cfg-osf.off .oslogo {
	opacity: 0.5;
}
.cfg-osf.off:hover {
	border-color: var(--line);
	background: var(--surface);
}
/* The chips are one row, wrapping, and only the chosen family's row is
   drawn: which one follows the checked radio, in CSS, so the right row is
   up before any script is. */
.cfg-osvs {
	display: none;
	flex-wrap: wrap;
	gap: var(--s-4);
}
.cfg-osvs:has(input:checked) {
	display: flex;
}
/* A chip is a label with no box of its own: its chip and its sentence are
   both items of the row, so the sentence can take a line to itself under
   the chips while still belonging to the radio that shows it. */
.cfg-os .cfg-osv {
	display: contents;
	margin: 0;
	font-size: var(--t-md);
}
.cfg-osv .c {
	position: relative;
	display: inline-flex;
	align-items: center;
	gap: var(--s-4);
	height: 40px;
	padding: 0 var(--s-7);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	color: var(--ink);
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	cursor: pointer;
	white-space: nowrap;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}
.cfg-osv .c input {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	opacity: 0;
	cursor: inherit;
}
.cfg-osv .c:hover {
	border-color: var(--border-strong);
	background: var(--raised);
}
/* Chosen in the page's blue, as the size, the term and the family above
   it are: one colour means "this one" the whole way down the form. */
.cfg-osv .c:has(input:checked) {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
	color: var(--ink);
	font-weight: var(--w-semi);
}
.cfg-osv .c:has(input:focus-visible) {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}
.cfg-osv .c:has(input:disabled) {
	border-style: dashed;
	background: transparent;
	color: var(--faint);
	font-weight: var(--w-normal);
	cursor: not-allowed;
}
.cfg-osv .c small,
.cfg-osv .c i {
	font-size: var(--t-xs);
	font-style: normal;
	font-weight: var(--w-normal);
	color: var(--body);
}
.cfg-osv .c:has(input:disabled) small {
	color: var(--faint);
}
.cfg-osv .c:has(input:checked) small {
	color: var(--body);
}
/* The Current tag: the one word on the filled chip that is not the name. */
.cfg-osv .c i {
	font-weight: var(--w-semi);
	color: var(--ink);
	background: color-mix(in srgb, var(--ink) 10%, transparent);
	border-radius: var(--radius-pill);
	padding: 1px var(--s-4);
}
.cfg-osv .c:has(input:checked) i {
	color: #fff;
	background: var(--accent-fill);
}
/* The sentence: drawn once per release, shown for the checked one, so it
   is right with no script and needs none to change. */
.cfg-osv .say {
	display: none;
	flex-basis: 100%;
	order: 1;
	margin-top: var(--s-2);
	font-size: var(--t-md);
	color: var(--body);
	cursor: default;
}
.cfg-osv:has(input:checked) .say {
	display: block;
}
/* The keep-as-is family a rebuild leads with has one chip that would only
   repeat its tile, so the chip is drawn off-screen (still a radio for the
   keyboard and the reader) and its sentence sits straight under the tiles. */
.cfg-osvs.keep .cfg-osv .c {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}
.cfg-osvs.keep .cfg-osv .say {
	margin-top: 0;
}
/* Inside a sheet or a card the row is too narrow for five or six names,
   so the tiles wrap three to a row; on a phone, two. The
   800 is the server page's card: 786 across with six tiles, where one row
   clipped AlmaLinux and Rocky Linux to their first letters. */
@container (max-width: 800px) {
	.cfg-osfs {
		flex-wrap: wrap;
	}
	.cfg-os .cfg-osf {
		flex: 0 0 calc((100% - 2 * var(--s-5)) / 3);
	}
}
@container (max-width: 480px) {
	/* A phone gets a two-column grid (David, 2026-11-03): the one
	   scrolling row hid most of the families off to the right. Names may
	   take two lines rather than be clipped. */
	.cfg-osfs {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--s-4);
	}
	.cfg-os .cfg-osf {
		flex: none;
		height: auto;
		min-height: 52px;
		padding: var(--s-3) var(--s-5);
		gap: var(--s-4);
	}
	.cfg-os .cfg-osf .oslogo {
		width: 24px;
		height: 24px;
	}
	.cfg-os .cfg-osf b {
		white-space: normal;
		overflow-wrap: anywhere;
	}
}

/* The till's one group before the button (portal review, October 2026):
   the key box, the two promises and the policy sentence under one label,
   every note at 13px, so the panel reads as the total, one group, the
   button, one line. */
.consent {
	gap: var(--s-6);
	padding-top: var(--s-6);
	border-top: 1px solid var(--border);
}
.consent legend {
	float: left;
	width: 100%;
	margin: 0 0 var(--s-2);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}
.consent .key-row {
	gap: var(--s-4);
}
.consent .fineprint {
	font-size: var(--t-sm);
	color: var(--body);
}
/* The line under the buttons, where two paragraphs stood. */
.till-fine {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--text-faint);
	line-height: 1.5;
}

/* The sign-in family (portal review, October 2026): the card holds at 410
   whatever the column under it allows, and the federation buttons carry
   the platform's own mark beside its name. */
.auth {
	max-width: 410px;
}
.auth .field label {
	font-size: var(--t-md);
}
.auth .hint,
.auth .row-help {
	font-size: var(--t-sm);
	color: var(--body);
}
.oauth-mark {
	width: 18px;
	height: 18px;
	flex: none;
}

/* The trust marks on the catalogue's count line, after the chips and
   before the small print. On a phone the line holds the count and the
   Filters button, and the marks wait for the foot, where the same three
   things are said in full. */
.cat-bar .store-claims {
	flex: 1 1 auto;
}
@media (max-width: 900px) {
	.cat-bar .store-claims {
		display: none;
	}
}

/* --- Money, account, tickets (October 2026) --------------------------------
   The four pages that carry the account's paperwork, redrawn on the review's
   two templates. A two-column page is main (2/3) and a rail (1/3) for what
   you act on; a list page is one card holding its filters and its table. The
   rail's cards are quieter than the panel's .card, 12px radius, no shadow, a
   label over a figure, because what they hold is read at a glance and acted
   on with one button. Everything below is scoped to these pages' own class
   names so the other pages of the review can move at their own pace. */

/* The two columns. Main takes what is left and wraps the rail under it when
   there is no longer room for both, which is the review's 1100px line with a
   240px rail and a 40px gutter either side. Billing puts the rail FIRST when
   it stacks: what is owed is the one thing anybody opens that page for, and
   under a table of history it is the last thing they would see. */
.with-rail {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-10);
	align-items: flex-start;
}

.with-rail > .main-col {
	flex: 2 1 640px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: var(--s-10);
}

.with-rail > .rail-col {
	flex: 1 1 340px;
	max-width: 440px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: var(--s-8);
}

@media (max-width: 1100px) {
	.with-rail > .rail-col {
		max-width: none;
	}

	/* Stacked, the rail's cards join the column one by one, so that only
	   the one that is owed goes above the table: the whole rail first put
	   the invoice list 1,700px down a phone, under the next charge, the
	   saved cards, every subscription and the open quote. */
	.with-rail.rail-first > .rail-col {
		display: contents;
	}

	.with-rail.rail-first > .rail-col > * {
		flex: 1 1 100%;
		min-width: 0;
	}

	.with-rail.rail-first > .rail-col > .rc.attn {
		order: -1;
	}
}

/* Above the line the two columns are the layout, not a preference. Letting
   the flex bases decide left the rail wrapping under the table on a 1320px
   screen, where the nav and the gutters leave a thousand pixels: the right
   half of the page stood empty and the owed amount was a scroll away. The
   rail keeps the width the server page's rail has (.srv-cols), and main
   takes the rest, however narrow -- a table scrolls, a card does not. */
@media (min-width: 1101px) {
	.with-rail {
		flex-wrap: nowrap;
	}

	.with-rail > .main-col {
		flex: 1 1 0;
	}

	.with-rail > .rail-col {
		flex: 0 0 clamp(340px, 33.333%, 440px);
	}
}

/* A rail card: a label, the figure or the sentence, and the one button. */
.rc {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-9);
	display: flex;
	flex-direction: column;
	gap: var(--s-3);
	min-width: 0;
}

.rc.attn {
	background: var(--attn-soft);
	border-color: color-mix(in srgb, var(--attn) 45%, var(--line));
}

.rc-lab {
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}

.rc.attn .rc-lab {
	color: var(--attn);
}

.rc-big {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	line-height: 1.2;
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.rc-big small {
	font-size: var(--t-base);
	font-weight: var(--w-medium);
	color: var(--body);
	letter-spacing: 0;
}

.rc-word {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	line-height: 1.3;
}

.rc-note {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--body);
	line-height: 1.5;
}

.rc-note a,
.rc-row a,
.lc-note a,
.rc-foot a {
	color: var(--accent-ink);
}

/* The one button may name a card ("Pay with Visa ending 4242, expires
   08/28"), which is longer than a phone's rail: it wraps rather than
   running out of its card. */
.rc .btn.rc-btn {
	width: 100%;
	margin-top: var(--s-4);
	white-space: normal;
	height: auto;
}

/* Only the card's OWN forms dissolve into its column; the card fields keep
   their fallback form as the box it hides and shows. */
.rc > form {
	margin: 0;
	display: contents;
}

/* The one-click door on an owed card: ink on the amber, so it is the loudest
   thing on the page without being a second accent. */
.rc.attn .btn.rc-btn {
	background: var(--ink);
	border-color: var(--ink);
	color: var(--ground);
}

/* A rail card that is a list: the label as a head, then rows ruled apart. */
.rc.list {
	padding: 0;
	gap: 0;
}

.rc-head {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--s-6);
	padding: var(--s-8) var(--s-9) var(--s-5);
}

.rc-head a {
	font-size: var(--t-sm);
	color: var(--accent-ink);
}

.rc-row {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--s-6);
	padding: 11px var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: var(--t-md);
	color: var(--ink);
	min-width: 0;
}

.rc-row:last-child {
	padding-bottom: var(--s-7);
}

.rc-row > div {
	min-width: 0;
}

.rc-row b {
	font-weight: var(--w-medium);
}

.rc-row .num {
	font-size: var(--t-md);
	font-family: var(--sans);
	font-weight: var(--w-medium);
	white-space: nowrap;
}

.rc-k {
	color: var(--body);
}

.rc-sub {
	display: block;
	font-size: var(--t-sm);
	color: var(--body);
	overflow-wrap: anywhere;
}

.rc-sub.attn {
	color: var(--attn);
}

/* A row's own verbs, under its facts, at table scale. */
.rc-acts {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-4);
	padding: 0 var(--s-9) var(--s-6);
}

.rc-acts form {
	margin: 0;
}

/* The pad under a list card's rows: a sentence about the list, 13px, once. */
.rc-foot {
	margin: 0;
	padding: var(--s-5) var(--s-9) var(--s-7);
	border-top: 1px solid var(--hair);
	font-size: var(--t-sm);
	color: var(--body);
}

/* "or pay another way", between the one-click door and the fields. */
.or-line {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	margin: var(--s-3) 0;
	font-size: var(--t-sm);
	color: var(--faint);
}

.or-line::before,
.or-line::after {
	content: "";
	flex: 1;
	height: 1px;
	background: var(--line);
}

/* The card fields inside a rail card lose the summary's rule: the or-line
   above them already divides. */
.rc .cardpay {
	padding-top: 0;
	border-top: 0;
}

/* A button in a table row or a list row: 32px, one size down. */
.btn.inrow {
	padding: 5px var(--s-6);
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	line-height: 20px;
}

/* --- The list card: filters, then the table, in one frame ----------------- */
.listcard {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	overflow: hidden;
	min-width: 0;
	/* The frame measures itself, so its table can stack by the room it
	   actually has (the container query below). */
	container-type: inline-size;
}

.lc-h {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: center;
	gap: var(--s-6);
	padding: var(--s-8) var(--s-9);
}

.lc-h h2 {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.012em;
}

.lc-h h2 small {
	margin-left: var(--s-4);
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--body);
}

.lc-strip {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-6);
	padding: var(--s-7) var(--s-8);
	border-bottom: 1px solid var(--line);
}

.lc-strip form {
	display: contents;
}

.lc-strip .input {
	width: 260px;
	max-width: 100%;
	font-family: var(--sans);
	font-size: var(--t-md);
	padding: 7px var(--s-6);
	border-radius: var(--r);
	background: var(--sunk);
}

.lc-strip select {
	width: auto;
	font-size: var(--t-md);
}

.lc-strip .grow {
	flex: 1;
}

/* The state tabs, counted. Plain links, so a filtered view is a URL. */
.segtabs {
	display: flex;
	gap: var(--s-2);
	background: var(--sunk);
	border: 1px solid var(--line);
	border-radius: var(--r);
	padding: 3px;
}

.segtabs a {
	display: inline-flex;
	align-items: center;
	gap: var(--s-3);
	padding: 5px var(--s-6);
	border-radius: var(--r-sm);
	font-size: var(--t-md);
	color: var(--body);
	white-space: nowrap;
}

.segtabs a:hover {
	color: var(--ink);
}

.segtabs a.on {
	background: var(--raised);
	color: var(--ink);
	font-weight: var(--w-semi);
	box-shadow: var(--sh-1);
}

.segtabs .n {
	font-size: var(--t-sm);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}

.segtabs .n.due {
	color: var(--attn);
}

/* The table inside the frame: no second border, 48px rows, 14.5px cells,
   identifiers in mono and nothing else. */
.listcard .tw {
	border: 0;
	border-radius: 0;
	background: none;
}

.listcard table {
	font-size: var(--t-md);
}

.listcard th {
	position: static;
	background: none;
	padding: var(--s-6) var(--s-8);
	color: var(--faint);
}

.listcard th:first-child,
.listcard td:first-child {
	padding-left: var(--s-9);
}

.listcard th:last-child,
.listcard td:last-child {
	padding-right: var(--s-9);
}

.listcard td {
	padding: 13px var(--s-8);
	color: var(--ink);
	border-bottom: 1px solid var(--hair);
}

.listcard tr:last-child td {
	border-bottom: 0;
}

.listcard td.c-id {
	font-family: var(--mono);
	font-size: var(--t-sm);
	white-space: nowrap;
}

.listcard td.c-id a {
	color: var(--ink);
}

.listcard td.c-id a:hover {
	color: var(--accent-ink);
}

.listcard td.c-dim {
	color: var(--body);
	white-space: nowrap;
}

.listcard th.c-num,
.listcard td.c-num {
	text-align: right;
	white-space: nowrap;
}

.listcard td.c-num {
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
}

.listcard td.c-act {
	text-align: right;
	white-space: nowrap;
	padding-top: var(--s-4);
	padding-bottom: var(--s-4);
}

.listcard td.c-act form {
	display: inline;
	margin: 0;
}

.listcard td .c-sub {
	display: block;
	margin-top: 2px;
	font-size: var(--t-sm);
	color: var(--body);
}

.listcard td .c-sub.attn {
	color: var(--attn);
}

.listcard td .c-sub a {
	color: var(--body);
}

.listcard td .c-sub a:hover {
	color: var(--accent-ink);
}

.listcard td a.c-link {
	color: var(--ink);
	font-weight: var(--w-medium);
}

.listcard td a.c-link:hover {
	color: var(--accent-ink);
}

/* The row that is somebody's move keeps its amber ground, from the left. */
.listcard tbody tr.row-due td {
	background: color-mix(in srgb, var(--attn-soft) 60%, transparent);
}

.listcard tbody tr.row-due td:first-child {
	box-shadow: inset 2px 0 0 var(--attn);
}

/* An expired card's row, tinted the same way: a renewal charged to it will
   be declined, which is a thing to read before it happens. */
.listcard tbody tr.row-attn td {
	background: color-mix(in srgb, var(--attn-soft) 60%, transparent);
}

/* What the list says when it has nothing in it, or nothing matching. */
.lc-empty {
	margin: 0;
	padding: var(--s-9);
	font-size: var(--t-base);
	color: var(--body);
}

.lc-empty .btn {
	margin-left: var(--s-6);
}

/* The Credit tab (ADR 0081): the balance as the panel's head, the add
   form under it, then the movements. */
.cr-head {
	display: grid;
	gap: var(--s-3);
	padding: var(--s-9) var(--s-9) var(--s-6);
}

.cr-l {
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}

.cr-big {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.cr-add {
	padding: var(--s-6) var(--s-9) 0;
	border-top: 1px solid var(--hair);
	margin-top: var(--s-6);
}

.cr-presets,
.cr-own {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-4);
	align-items: stretch;
}

.cr-presets .btn {
	display: inline-flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 2px;
	font-variant-numeric: tabular-nums;
}

.cr-presets .btn small {
	font-size: var(--t-xs);
	font-weight: 400;
	color: var(--faint);
}

.cr-own input {
	flex: 1 1 12rem;
	min-width: 0;
	font: inherit;
	font-size: var(--t-md);
	padding: 11px var(--s-7);
	border: 1px solid var(--border-strong);
	border-radius: var(--radius-control);
	background: var(--surface);
	color: var(--text);
}

.cr-own input:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* The sign says which way it moved; no colour does, because green is
   kept for what is running (2026-10-21). */
.credit th.num,
.credit td.num {
	text-align: right;
	font-size: inherit;
	font-variant-numeric: tabular-nums;
}

.lc-note {
	margin: 0;
	padding: var(--s-6) var(--s-9) var(--s-7);
	border-top: 1px solid var(--hair);
	font-size: var(--t-sm);
	color: var(--body);
}

/* In a frame narrower than 760px every table becomes cards: the describing
   cell on its own line, the rest flowing after it, the action pushed to the
   end. The frame's own width, not the window's: the same table sits in the
   billing page's main column beside a rail, where a 1320px window leaves it
   620px, and judged by the window it stayed six columns and wrapped "Ryzen 5
   5600X · 64GB, monthly" over five lines. On a phone the two measures agree. */
@container (max-width: 760px) {
	.listcard table.stack {
		min-width: 0;
	}

	.listcard table.stack thead {
		display: none;
	}

	.listcard table.stack tbody tr {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--s-2) var(--s-6);
		padding: var(--s-6) var(--s-9);
		border-bottom: 1px solid var(--hair);
	}

	.listcard table.stack tbody tr:last-child {
		border-bottom: 0;
	}

	.listcard table.stack td,
	.listcard table.stack td:first-child,
	.listcard table.stack td:last-child {
		padding: 0;
		border-bottom: 0;
	}

	.listcard table.stack td.c-main {
		flex-basis: 100%;
		order: -1;
	}

	.listcard table.stack td.c-act {
		margin-left: auto;
	}

	.listcard table.stack td.c-hide {
		display: none;
	}

	/* The amber ground moves from the cells to the row, which is now the
	   card; the cells have no box of their own to carry it. */
	.listcard table.stack tbody tr.row-due,
	.listcard table.stack tbody tr.row-attn {
		background: color-mix(in srgb, var(--attn-soft) 60%, transparent);
	}

	.listcard table.stack tbody tr.row-due {
		box-shadow: inset 2px 0 0 var(--attn);
	}

	.listcard table.stack tbody tr.row-due td,
	.listcard table.stack tbody tr.row-attn td,
	.listcard table.stack tbody tr.row-due td:first-child {
		background: none;
		box-shadow: none;
	}
}

/* --- The invoice as a document, on the system -------------------------------
   The frame keeps the masthead and the brand; the ceremony goes. Labels are
   the system's 12px sans; the reference stays mono because it is the one
   identifier on the page. Screen only: the print block above keeps paper. */
@media screen {
	.doc {
		border: 1px solid var(--line);
		border-radius: var(--r-lg);
		background: var(--surface);
		box-shadow: none;
	}

	.doc-head {
		padding: var(--s-9) var(--s-10);
		background: none;
		border-bottom: 1px solid var(--line);
		align-items: center;
	}

	.doc-kind {
		font-family: var(--sans);
		font-size: var(--t-xs);
		font-weight: var(--w-semi);
		letter-spacing: 0.06em;
		color: var(--accent-ink);
		border-color: color-mix(in srgb, var(--accent) 35%, var(--line));
		border-radius: var(--r-sm);
		padding: 2px var(--s-4);
	}

	.doc-ref b {
		font-size: var(--t-lg);
		font-weight: var(--w-medium);
		color: var(--ink);
	}

	.doc-ref span {
		font-family: var(--sans);
		font-size: var(--t-sm);
		color: var(--body);
	}

	.doc-parties {
		grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
		gap: var(--s-9);
		padding: var(--s-9) var(--s-10);
		border-bottom: 1px solid var(--line);
	}

	.doc-label {
		font-family: var(--sans);
		font-size: var(--t-xs);
		font-weight: var(--w-semi);
		letter-spacing: 0.06em;
		color: var(--faint);
		margin-bottom: var(--s-2);
	}

	.doc-parties p {
		font-size: var(--t-base);
		color: var(--ink);
	}

	.doc-parties p .c-sub {
		display: block;
		font-size: var(--t-sm);
		color: var(--body);
	}

	.doc .table th {
		padding: var(--s-6) var(--s-10);
		background: none;
		border-bottom: 1px solid var(--hair);
		color: var(--faint);
	}

	.doc .table td {
		padding: 15px var(--s-10);
		font-size: var(--t-base);
		color: var(--ink);
		border-bottom: 1px solid var(--hair);
	}

	.doc .table .col-num {
		font-family: var(--sans);
		font-weight: var(--w-medium);
		font-size: var(--t-base);
	}

	.doc .table .col-dim {
		font-family: var(--sans);
		font-size: var(--t-md);
		color: var(--body);
	}

	.doc .table .cell-sub {
		font-family: var(--sans);
		font-size: var(--t-sm);
		color: var(--body);
	}

	.doc .table a.cell-sub {
		color: var(--body);
	}

	.doc .table tfoot .total td {
		padding: var(--s-8) var(--s-10) var(--s-9);
		border-top: 1px solid var(--line);
		border-bottom: 0;
		font-size: var(--t-lg);
		font-weight: var(--w-semi);
		background: none;
	}

	.doc-fine {
		font-family: var(--sans);
		font-size: var(--t-sm);
		color: var(--body);
		padding: var(--s-6) var(--s-10);
		border-top: 1px solid var(--hair);
	}

	.doc-fine .mono-ref {
		color: var(--ink);
		font-size: var(--t-sm);
	}
}

/* The title of an invoice page carries its number: mono, one size under the
   words, because it is an identifier set beside a word. */
.ph .t .ref {
	font-family: var(--mono);
	font-weight: var(--w-medium);
	font-size: var(--t-xl);
	letter-spacing: 0;
	margin-left: var(--s-3);
}

/* Payment attempts, under the document: each one a line. */
.attempts.rc {
	gap: var(--s-4);
}

.attempts .attempt {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	font-size: var(--t-md);
	color: var(--ink);
}

/* --- Your account -------------------------------------------------------
   Rows of settings in a frame: what it is and its note, the value, the
   control. A disclosure row opens its form underneath, native, so changing a
   password works with no script at all. */
.arow,
.arow-d > summary {
	display: grid;
	/* The control sits beside the value it changes, not at the far edge of
	   a 1600px row with a desert between: "Set ... Change" reads as one
	   phrase. */
	grid-template-columns: 240px minmax(0, max-content) auto;
	justify-content: start;
	gap: var(--s-6) var(--s-10);
	align-items: center;
	padding: var(--s-7) var(--s-9);
	border-top: 1px solid var(--hair);
	min-width: 0;
}

.arow.attn,
.arow-d.attn > summary {
	background: color-mix(in srgb, var(--attn-soft) 60%, transparent);
}

.arow .rt,
.arow-d .rt {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--ink);
}

.arow .rd,
.arow-d .rd {
	margin-top: 2px;
	font-size: var(--t-sm);
	color: var(--body);
	line-height: 1.45;
}

.arow .val,
.arow-d .val {
	font-size: var(--t-md);
	color: var(--ink);
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--s-4);
	min-width: 0;
	overflow-wrap: anywhere;
}

/* Not .quiet: that name is the dashboard's bordered "nothing needs you"
   panel, and wearing it here boxed the word "Set". */
.arow .val.muted,
.arow-d .val.muted {
	color: var(--body);
}

/* Inside a summary the title and the note are spans, because a summary
   holds phrasing content; they still stack. */
.arow-d .rt,
.arow-d .rd {
	display: block;
}

.arow .act,
.arow-d .act {
	display: flex;
	gap: var(--s-4);
}

.arow .act form {
	margin: 0;
}

.arow-d > summary {
	list-style: none;
	cursor: pointer;
}

.arow-d > summary::-webkit-details-marker {
	display: none;
}

.arow-d > summary:hover {
	background: var(--sunk);
}

.arow-d[open] > summary .act .btn {
	border-color: var(--ink);
}

/* The opened form, on the row's own ground, at a form's measure. */
.arow-more {
	padding: var(--s-4) var(--s-9) var(--s-9);
	border-top: 1px solid var(--hair);
	background: var(--sunk);
}

.arow-more .stackform {
	max-width: 420px;
}

.arow-more .field {
	margin: var(--s-5) 0 0;
}

.arow-more .btn {
	margin-top: var(--s-5);
}

.arow-more .hint {
	margin-top: var(--s-5);
}

/* The person: initials at the size they are read, beside the name. */
.who-row {
	display: flex;
	align-items: center;
	gap: var(--s-8);
	padding: var(--s-9);
}

.who-row .av {
	width: 56px;
	height: 56px;
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
}

.who-row .who-name {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	line-height: 1.25;
}

.who-row .who-mail {
	font-size: var(--t-md);
	color: var(--body);
	display: flex;
	align-items: center;
	gap: var(--s-4);
	flex-wrap: wrap;
}

@media (max-width: 760px) {
	.arow,
	.arow-d > summary {
		grid-template-columns: minmax(0, 1fr);
		gap: var(--s-4);
	}

	.arow .act,
	.arow-d .act {
		justify-content: flex-start;
	}
}

/* --- Tickets ----------------------------------------------------------------
   The thread at reading size, 20px between turns; the reply in a frame of
   its own under it; the facts and the close in the rail beside the status
   they change. */
.thread {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-5) var(--s-9);
}

.thread .msg {
	padding: var(--s-5) 0;
	border-bottom: 0;
	gap: var(--s-7);
	grid-template-columns: 32px 1fr;
}

.thread .msg + .msg {
	border-top: 1px solid var(--hair);
}

.thread .msg .av {
	width: 32px;
	height: 32px;
	font-size: var(--t-xs);
}

/* Our replies wear the brand mark, not a person's initials: "DA" from
   "Darkless" read as somebody called D. A. (readiness pass of 2026-11-04).
   The icon as a mask, as .logo does, so it is one colour on the ink. */
.thread .msg .av-mark {
	width: 18px;
	height: 18px;
	background: #fff;
	-webkit-mask: url(icon.svg) center / contain no-repeat;
	mask: url(icon.svg) center / contain no-repeat;
}

.thread .msg .av svg {
	width: 16px;
	height: 16px;
}

.thread .msg .who {
	font-size: var(--t-md);
	font-weight: var(--w-semi);
}

.thread .msg .when {
	font-family: var(--sans);
	font-size: var(--t-sm);
	color: var(--body);
	margin-left: var(--s-4);
}

.thread .msg .bod {
	font-size: var(--t-base);
	line-height: 1.55;
	color: var(--ink);
	margin-top: var(--s-2);
}

.composer-card {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-9);
}

.composer-card .btnrow {
	margin-top: var(--s-6);
}

/* Who is waiting, as the first thing in a ticket row: the amber ground says
   it before the word does, and the word says it for everyone else. */
.listcard td .pill {
	vertical-align: middle;
}

/* The right-hand end of a rail row: the figure over the state, both to the
   edge, so a column of amounts lines up down the card. */
.rc-end {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: var(--s-2);
	flex: none;
}

/* A quote's note from whoever wrote it, inside the document at body size. */
.doc-note {
	margin: 0;
	padding: var(--s-9) var(--s-10);
	border-top: 1px solid var(--hair);
	font-size: var(--t-base);
	color: var(--ink);
}

/* A form that fills the verbs row, for the one verb that stands alone. */
.rc-acts form.wide {
	flex: 1;
}

/* --- Server state (October 2026) ------------------------------------------
   The pages about what a server is doing: the dashboard, a VPS, a dedicated
   box. One head for all three (name, state beside it, a line of facts, the
   verbs on the right), one two-column split (main 2/3, a rail of 340 to
   440px), one card, one table. The rail's bounds are the system's own and
   are named once here, so the two column templates cannot drift apart. */
.srv-cols,
.dash-cols {
	--rail-min: 340px;
	--rail-max: 440px;
	display: grid;
	grid-template-columns: minmax(0, 1fr) clamp(var(--rail-min), 33.333%, var(--rail-max));
	gap: var(--s-10);
	align-items: start;
}

.srv-main {
	display: grid;
	gap: var(--s-10);
	min-width: 0;
}

.srv-rail,
.dash-rail {
	display: grid;
	gap: var(--s-8);
	min-width: 0;
	align-content: start;
}

/* The head. The state word sits on the title's baseline in the one colour
   the site gives it; the facts run under both as one wrapping line. */
.shead {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--s-8);
	flex-wrap: wrap;
}

.shead-l {
	display: flex;
	flex-direction: column;
	gap: var(--s-5);
	min-width: 0;
}

.shead-t {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	flex-wrap: wrap;
}

.shead .t {
	margin: 0;
	font-size: var(--t-2xl);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	line-height: 1.15;
	color: var(--ink);
	overflow-wrap: anywhere;
}

.shead-sub {
	margin: 0;
	font-size: var(--t-md);
	color: var(--body);
}

.shead .power {
	gap: var(--s-5);
	flex: none;
	align-items: center;
}

/* The sentence that explains an empty button row wraps to a column of its
   own rather than pushing the title down. */
.shead .power .foot {
	max-width: calc(var(--prose) / 2);
}

/* The one caveat under the hardware buttons, pulled up to the head it
   belongs to rather than sitting a block away from it. */
.shead-note {
	margin: calc(var(--s-6) * -1) 0 0;
	font-size: var(--t-sm);
	color: var(--faint);
}

/* The state word. The dot is the dashboard's, larger and with a wider halo,
   because at a title's size the 10px dot reads as a speck. */
.state {
	display: inline-flex;
	align-items: center;
	gap: var(--s-5);
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	padding-top: var(--s-2);
	white-space: nowrap;
}

.state .sdot {
	width: var(--s-6);
	height: var(--s-6);
	box-shadow: 0 0 0 var(--s-2) color-mix(in srgb, currentColor 20%, transparent);
}

.state.run {
	color: var(--run);
}

.state.run .sdot {
	box-shadow: 0 0 0 var(--s-2) var(--run-glow);
}

.state.attn {
	color: var(--attn);
}

.state.bad {
	color: var(--bad);
}

.state.working {
	color: var(--accent-ink);
}

.state.off {
	color: var(--body);
}

.state.active {
	color: var(--ink);
}

/* The game page's head verb and the one line about the panel to come,
   stacked at the head's right edge. */
.game-power {
	display: grid;
	justify-items: end;
	gap: var(--s-3);
}

@media (max-width: 900px) {
	.game-power {
		justify-items: start;
	}
}

.state.unknown {
	color: var(--faint);
}

.state-slot {
	display: inline-flex;
	align-items: center;
}

/* The facts line: label in body grey, value in ink. Amber is the one colour
   a fact may carry, for the line that needs the customer. */
.facts {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-10);
	font-size: var(--t-md);
	color: var(--body);
	line-height: 1.5;
}

.facts b,
.kv b,
.rrow-t b {
	font-weight: var(--w-medium);
	color: var(--ink);
}

.fact.attn,
.facts .attn,
.facts b.attn {
	color: var(--attn);
}

/* The image it runs, its mark at the line's own height. */
.fact-os {
	display: inline-flex;
	align-items: center;
	gap: var(--s-3);
	color: var(--ink);
}

.fact-os .oslogo {
	width: 16px;
	height: 16px;
}

/* The Networking tab's sub-tabs: a segmented bar above whichever part is
   open, the parts in their own room. */
.net {
	display: grid;
	gap: var(--s-8);
	min-width: 0;
}

/* The firewall's table needs the pane's width, not a form's measure; the
   selector out-ranks the measure cap's own :has() chain. */
.net .card,
.net .card:has(.field):not(:has(.msg)),
.net .card:has(input[type="text"]):not(:has(.msg)) {
	max-width: none;
}

/* Each sub-tab's parts stacked with the system's gap. */
.net > div {
	display: grid;
	gap: var(--s-6);
	min-width: 0;
}

.net-tabs {
	justify-self: start;
	max-width: 100%;
	overflow-x: auto;
}

/* The firewall (David, 2026-11-03: "a proper firewall table"): a head with
   its state and switch, then a card per direction holding its rules in the
   order they are matched, the locked platform row, the default row, and
   the add row under it. */
.fw-head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-4) var(--s-8);
}

.fw-head > div {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-3) var(--s-5);
}

/* The firewall's swappable region stacks its parts like the pane does. */
.fw-region {
	display: grid;
	gap: var(--s-6);
	min-width: 0;
}

.fw-lock {
	vertical-align: -2px;
	color: var(--faint);
}

/* A direction's card: its table runs edge to edge, the add row below. */
.fw-card {
	overflow: hidden;
}




/* The rules grid: a header row, ruled cells and compact rows, the same on
   every width; a phone scrolls it sideways rather than breaking it into
   cards, which is how the panels people already know behave. */
.fwg-wrap {
	overflow-x: auto;
}

/* Smaller type and roomier rows than the pane's prose (David, 2026-11-03:
   "more space, the text is very big"): a rules grid is scanned, not read. */
.fwg {
	width: 100%;
	min-width: 900px;
	border-collapse: collapse;
	font-size: var(--t-xs);
	line-height: 1.4;
}

.fwg th {
	text-align: left;
	padding: var(--s-5) var(--s-7);
	background: var(--sunk);
	border-bottom: 1px solid var(--line);
	border-right: 1px solid var(--line);
	font-size: 10.5px;
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
	white-space: nowrap;
}

.fwg td {
	padding: var(--s-6) var(--s-7);
	border-bottom: 1px solid var(--line);
	border-right: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
	vertical-align: middle;
	white-space: nowrap;
	color: var(--ink);
}

.fwg th:last-child,
.fwg td:last-child {
	border-right: 0;
}

.fwg tbody tr:hover td {
	background: color-mix(in srgb, var(--ink) 3%, transparent);
}

.fwg .mono {
	font-family: var(--mono);
	font-size: 11.5px;
}

.fwg .dim {
	color: var(--body);
	white-space: normal;
}

.fwg tr.off td {
	color: var(--faint);
}

.fwg tr.off .fw-act {
	opacity: 0.55;
}

.fwg-on {
	width: 56px;
	text-align: center;
}

.fwg-n {
	width: 36px;
	text-align: center;
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}

.fwg-acts {
	width: 1%;
	text-align: right;
}

/* The platform's locked row and the default read as part of the grid but
   not as the customer's: shaded, and the default under a dashed rule. */
.fwg tr.fwg-locked td,
.fwg tr.fwg-default td {
	background: var(--sunk);
}

.fwg tr.fwg-default td {
	border-top: 1px dashed var(--line);
}

/* The add row, and a row being edited: inputs sized to their cells. */
.fwg .input {
	height: 28px;
	padding: 0 var(--s-3);
	font-size: var(--t-xs);
	width: 100%;
	min-width: 72px;
}

.fwg select.input {
	min-width: 88px;
}

.fwg tr.fwg-add td {
	background: var(--surface);
	border-top: 2px solid var(--line);
}

.fwg-form {
	display: none;
}

/* Buttons inside the grid at the grid's size, in the interface's face even
   in a fixed-width cell. */
.fwg .btn.sm {
	font-family: var(--sans);
	font-size: 11.5px;
	padding: var(--s-2) var(--s-5);
}

/* Square icon buttons for a row's verbs, named for a screen reader. */
.icon-btn {
	display: inline-grid;
	place-items: center;
	width: 26px;
	height: 26px;
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	background: none;
	color: var(--body);
	cursor: pointer;
}

.icon-btn:hover {
	color: var(--ink);
	border-color: color-mix(in srgb, var(--ink) 25%, var(--line));
}

.icon-btn.bad:hover {
	color: var(--bad);
	border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
}

.icon-btn:disabled {
	opacity: 0.35;
	cursor: default;
}

/* Allow and deny as badges: the one column read first. */
.fw-act {
	display: inline-block;
	padding: 1px 8px;
	border-radius: 999px;
	font-size: 11px;
	font-weight: var(--w-medium);
}

/* Mixed toward the ink, so the green darkens on a light page and lightens
   on a dark one without a token per theme. */
.fw-act.allow {
	color: color-mix(in srgb, #22c55e 70%, var(--ink));
	background: color-mix(in srgb, #22c55e 16%, transparent);
}

.fw-act.deny {
	color: color-mix(in srgb, #ef4444 75%, var(--ink));
	background: color-mix(in srgb, #ef4444 16%, transparent);
}


/* The platform's row and the default row read as part of the table but
   not as the customer's: quieter, and the default set apart by a rule. */



/* A rule's on/off switch: a button, so it works as a form post. */
.fw-switch {
	position: relative;
	width: 34px;
	height: 20px;
	border-radius: 999px;
	border: 1px solid var(--line);
	background: var(--sunk);
	cursor: pointer;
	padding: 0;
}

.fw-switch i {
	position: absolute;
	top: 2px;
	left: 2px;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	background: var(--faint);
	transition: transform var(--t-fast) var(--e-io);
}

.fw-switch.on {
	background: var(--accent-fill);
	border-color: var(--accent-fill);
}

.fw-switch.on i {
	transform: translateX(14px);
	background: #fff;
}

.fw-acts {
	display: inline-flex;
	gap: var(--s-2);
	justify-content: flex-end;
}

.fw-acts form {
	display: contents;
}

/* The add row and an edit: the rule's fields in one wrapping line. */



.fw-src {
	display: flex;
	gap: var(--s-2);
	flex: 2 1 200px;
	min-width: 0;
}




/* The Upgrade tab (2026-11-03 rework): a row per plan to pick from, the
   picked one read as a diff of this plan against it, then the one verb;
   add-ons in the rail beside it. */
.upg-body {
	--diff-add: color-mix(in srgb, #22c55e 70%, var(--ink));
	--diff-del: color-mix(in srgb, #ef4444 65%, var(--ink));
	display: grid;
	gap: var(--s-8);
}

.upg-list {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
	gap: var(--s-4);
}

.upg-opt {
	display: grid;
	grid-template-columns: 1fr;
	align-items: center;
	gap: var(--s-1) var(--s-4);
	padding: var(--s-6) var(--s-7);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	cursor: pointer;
}

.upg-opt:hover {
	border-color: var(--border-strong);
}

.upg-opt.on {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}

/* The whole card is the choice (David, 2026-11-03): the radio is there for
   the keyboard and the screen reader, the tick shows which one is picked. */
.upg-opt {
	position: relative;
}

.upg-opt.on::after {
	content: "✓";
	position: absolute;
	top: var(--s-4);
	right: var(--s-5);
	color: var(--accent);
	font-weight: var(--w-semi);
}

.upg-opt:has(input:focus-visible) {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.upg-opt-name {
	font-weight: var(--w-semi);
	color: var(--ink);
}

.upg-opt-price {
	font-size: var(--t-sm);
	color: var(--body);
	font-variant-numeric: tabular-nums;
}

.upg-opt-price em {
	font-style: normal;
	margin-left: var(--s-3);
	color: var(--diff-add);
}

/* The diff: a grid of label, before, after and what changed, the before
   struck through and the after marked added, as a diff reads. */
.diff {
	display: grid;
	grid-template-columns: minmax(90px, auto) 1fr 1fr auto;
	border: 1px solid var(--line);
	border-radius: var(--r);
	overflow: hidden;
	font-size: var(--t-sm);
	font-variant-numeric: tabular-nums;
}

.diff-h,
.diff-r {
	display: contents;
}

.diff-h > span {
	padding: var(--s-4) var(--s-6);
	background: var(--sunk);
	border-bottom: 1px solid var(--line);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--faint);
}

.diff-r > span {
	padding: var(--s-4) var(--s-6);
	border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}

.diff-r:last-child > span {
	border-bottom: 0;
}

.diff-k {
	color: var(--body);
}

.diff-r:not(.same) .diff-from {
	color: var(--diff-del);
	text-decoration: line-through;
	text-decoration-color: color-mix(in srgb, var(--diff-del) 60%, transparent);
	background: color-mix(in srgb, #ef4444 7%, transparent);
}

.diff-r:not(.same) .diff-to {
	color: var(--diff-add);
	font-weight: var(--w-semi);
	background: color-mix(in srgb, #22c55e 8%, transparent);
}

.diff-r.same .diff-from,
.diff-r.same .diff-to {
	color: var(--faint);
}

.diff-d {
	color: var(--diff-add);
	font-weight: var(--w-medium);
	text-align: right;
	white-space: nowrap;
}

.diff-r.price > span {
	border-top: 1px solid var(--line);
	font-weight: var(--w-medium);
}

.upg-go {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-5);
	margin-top: var(--s-6);
}

.upg-due {
	color: var(--body);
}

.upg-due b {
	color: var(--ink);
	font-size: var(--t-lg);
}

.upg-foot {
	border-top: 1px solid var(--line);
	padding-top: var(--s-6);
}

/* Add-ons in the rail: a row each, name and what it is, price and verb. */
.addon {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-5);
	padding: var(--s-6) var(--s-8);
	border-top: 1px solid var(--line);
}

.addon b {
	display: block;
	color: var(--ink);
	font-weight: var(--w-semi);
}

.addon-r {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	flex: none;
}

.addon-price {
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.addon-price small {
	font-weight: var(--w-normal);
	color: var(--faint);
	margin-left: 1px;
}

.addon-soon {
	font-size: var(--t-xs);
	color: var(--faint);
	border: 1px dashed var(--line);
	border-radius: 999px;
	padding: 2px 8px;
}

.addon.soon b {
	color: var(--body);
}

.upg-cols .srv-main > .card {
	max-width: none;
	min-width: 0;
}

.upg-body,
.upg-diff {
	min-width: 0;
}

/* On a phone the change column goes: the colours already say it, and four
   columns do not fit 390px. */
@media (max-width: 600px) {
	.diff {
		grid-template-columns: minmax(70px, auto) 1fr 1fr;
	}

	.diff-d,
	.diff-h > span:last-child {
		display: none;
	}

	.diff-h > span,
	.diff-r > span {
		padding: var(--s-3) var(--s-4);
	}
}

/* The month's transfer: the figure, then a bar against the allowance in the
   network chart's hue, so the two read as one subject. */
.xfer-n {
	font-size: var(--t-md);
	color: var(--body);
}

.xfer-n b {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

/* The game page's "Usage now": a labelled bar per resource. */
.usage-rows {
	display: grid;
	gap: var(--s-6);
}

.usage-k {
	display: inline-block;
	min-width: 64px;
	color: var(--faint);
	font-size: var(--t-sm);
}

.usage-row .xfer-bar {
	margin-block: var(--s-3) 0;
}

.xfer-bar {
	height: 6px;
	margin-block: var(--s-4);
	border-radius: 999px;
	background: var(--sunk);
	overflow: hidden;
}

.xfer-bar i {
	display: block;
	height: 100%;
	border-radius: inherit;
	background: linear-gradient(90deg, #38bdf8, var(--accent));
}

.fact .id,
.rs .id,
.srv .id {
	font-family: var(--mono);
	font-size: var(--t-sm);
	letter-spacing: -0.01em;
	color: var(--ink);
}

.fact a.id,
.rs a.id {
	color: var(--accent-ink);
	text-decoration: none;
}


/* The card. 12px radius, a 1px line, no shadow: the system's card, drawn
   here because the panel's .card still carries the earlier radius and the
   pages in this section are the first to follow the system. */
.sec {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	overflow: hidden;
	min-width: 0;
}

.sec-h {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
	flex-wrap: wrap;
	padding: var(--s-7) var(--s-9);
	border-bottom: 1px solid var(--line);
}

.sec-t {
	margin: 0;
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.01em;
	color: var(--ink);
}

.sec-n {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--body);
	margin-left: var(--s-2);
}

/* A rail card's head is a label, not a title: the rail informs. */
.sec-l {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-8) var(--s-9) var(--s-5);
}

.lbl {
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}

.sec-h > a,
.sec-l > a,
.rrow > a {
	font-size: var(--t-md);
	color: var(--accent-ink);
	text-decoration: none;
	flex: none;
}


.sec-h .rs {
	flex: 1 1 auto;
	min-width: 0;
	text-align: right;
}

.sec-b {
	display: grid;
	gap: var(--s-6);
	padding: var(--s-6) var(--s-9) var(--s-8);
}

.sec-b p {
	margin: 0;
}

.sec-l + .sec-b {
	padding-top: 0;
}

/* A control gets a 13px note or nothing, and a card gets one, at its foot. */
.sec-note {
	margin: 0;
	padding: var(--s-6) var(--s-9) var(--s-7);
	font-size: var(--t-sm);
	color: var(--faint);
	line-height: 1.45;
}

.sec-b + .sec-note {
	padding-top: 0;
}

/* A rail row: a key line and a second line on the left, one thing on the
   right. Rows rule each other off with the hairline, never the card's line. */
.rrow {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-5) var(--s-9);
	border-top: 1px solid var(--hair);
	min-width: 0;
}

.rrow:last-child {
	padding-bottom: var(--s-7);
}

.rrow.link {
	color: inherit;
	text-decoration: none;
}

.rrow.link:hover {
	background: var(--sunk);
}

.rrow-t {
	display: flex;
	flex-direction: column;
	gap: var(--s-1);
	min-width: 0;
}

.rrow-t code {
	font-family: var(--mono);
	font-size: var(--t-sm);
	color: var(--ink);
}

.rrow .reveal {
	max-width: 100%;
}

.rrow .pill {
	flex: none;
}

.rk {
	font-size: var(--t-base);
	font-weight: var(--w-medium);
	color: var(--ink);
	overflow-wrap: anywhere;
}

.rk.plain {
	font-weight: var(--w-normal);
}

/* The one word the colour carries, beside the row it belongs to, so a
   failed task is read as failed by somebody who cannot see red. */
.rk .bad {
	color: var(--bad);
	font-weight: var(--w-normal);
	margin-left: var(--s-2);
}

.rk .acc {
	color: var(--accent-ink);
	font-weight: var(--w-normal);
	margin-left: var(--s-2);
}

.rs {
	font-size: var(--t-sm);
	color: var(--body);
	line-height: 1.45;
}

.ramt {
	flex: none;
	font-weight: var(--w-medium);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

/* Money that is owed now, in the one colour that means it. */
.rrow.due .rs,
.rrow.due .ramt {
	color: var(--attn);
}

/* A key and its value, the value right-aligned with its qualifier under. */
.krow {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-5) var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: var(--t-md);
}

.krow:last-child {
	padding-bottom: var(--s-7);
}

.krow > .rs {
	flex: none;
	padding-top: var(--s-1);
}

.kv {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	text-align: right;
	min-width: 0;
}

/* The big number: what this server costs, in the system's second size. */
.big {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	letter-spacing: -0.02em;
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.big span {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--body);
	letter-spacing: 0;
}

/* The figures above a bandwidth plot: text first, so a reader who never
   sees the bars has the numbers that matter. */
.bw-figs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-4) var(--s-11);
	padding: var(--s-8) var(--s-9) 0;
	font-size: var(--t-md);
	color: var(--body);
}

.bw-figs b {
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

/* Counters that could not be read: the line and its Try again on one row,
   wrapping on a phone. */
.bw-retry {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-4) var(--s-6);
}

.bw-retry .sec-note {
	margin: 0;
	padding: 0;
}

/* The table. Rows of 48px, cells at control size, the head in the label
   voice; under 900px every row becomes a card of its own lines. */
.srv {
	width: 100%;
	/* The panel's own table keeps a 560px floor and scrolls; this one is
	   the room it has, because under 900px its rows become cards. */
	min-width: 0;
	border-collapse: collapse;
	font-size: var(--t-md);
}

.srv th {
	text-align: left;
	padding: var(--s-5) var(--s-6);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--faint);
	white-space: nowrap;
	background: none;
	border-bottom: 1px solid var(--line);
	position: static;
}

.srv td {
	padding: var(--s-6);
	border-top: 1px solid var(--hair);
	border-bottom: 0;
	vertical-align: middle;
	color: var(--ink);
}

.srv tbody tr:first-child td {
	border-top: 0;
}

.srv th:first-child,
.srv td:first-child {
	padding-left: var(--s-9);
}

.srv th:last-child,
.srv td:last-child {
	padding-right: var(--s-9);
}

.srv .srv-name {
	display: flex;
	gap: var(--s-6);
	align-items: flex-start;
}

.srv .srv-name .sdot {
	margin-top: var(--s-3);
}

.srv .srv-name > div {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.srv .srv-name .rs {
	overflow-wrap: anywhere;
}

.srv .srv-name a {
	color: var(--ink);
	font-weight: var(--w-medium);
	text-decoration: none;
	overflow-wrap: anywhere;
}


.srv .id {
	white-space: nowrap;
}

.srv .dim {
	color: var(--body);
}

.srv .end {
	text-align: right;
	white-space: nowrap;
}

.srv td a {
	color: inherit;
	text-decoration: none;
}


.srv .attn,
.srv .attn a {
	color: var(--attn);
}

/* The row that is somebody's move, and the row that went wrong, each
   tinted in its own colour so faintly that only the row beside it shows
   the difference. */
.srv tr.due td {
	background: color-mix(in srgb, var(--attn) 6%, transparent);
}

.srv tr.failed td {
	background: color-mix(in srgb, var(--bad) 5%, transparent);
}

/* A task's result is a word in the colour of the fact, never a pill: a
   column of pills is a column of dots. */
.srv .res {
	color: var(--body);
	white-space: nowrap;
}

.srv .res.bad {
	color: var(--bad);
	font-weight: var(--w-medium);
}

.srv .res.working {
	color: var(--accent-ink);
}

.srv td .rs.bad {
	display: block;
	color: var(--bad);
}

/* A task that has not finished: a rule down its left, the watch's mark. */
.srv tr.live td:first-child {
	box-shadow: inset 2px 0 0 var(--accent);
}

/* Reverse DNS, edited in the row: the box and its Save on one line. */
/* A VPS address's row: the address, its reverse DNS or the form that
   edits it, and the button, wrapping on a phone. */
.rdns-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-3) var(--s-6);
	width: 100%;
}

/* The verb sits beside the name it edits, not at the row's far end. */
.rdns-row {
	justify-content: flex-start;
}

.rdns {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	max-width: var(--rail-max);
}

.rdns .input {
	flex: 1;
	min-width: 0;
	padding: var(--s-3) var(--s-6);
}

/* --- The dedicated server's tabs (October 2026) ----------------------------
   Overview, Network, Install, Activity under the one head. Most of it is the
   server-state system above; these are the pieces only this page has. */

/* The notice a press came back with, at the head of its own tab. */
.pane-note {
	margin-bottom: var(--s-8);
}

/* The address table's name column takes what the other two leave, and the
   field in it runs that whole width: the rail-wide box it used to be cut
   a long PTR off mid-name. Under 900px the field takes its row, and the
   Save drops under it rather than squeezing it. */
.srv .grow {
	width: 100%;
}

.rdns.full {
	max-width: none;
	flex-wrap: wrap;
}

.rdns.full .input {
	flex: 1 1 24ch;
	font-size: var(--t-id);
	line-height: 1.45;
	resize: none;
	field-sizing: content;
	min-height: 0;
	max-width: 100%;
	/* .srv .id holds identifiers on one line; this one wraps. */
	white-space: pre-wrap;
	word-break: break-all;
}

@media (max-width: 900px) {
	.rdns.full .input {
		flex-basis: 100%;
	}
}

.srv .ptr {
	white-space: normal;
	overflow-wrap: anywhere;
}

.srv.addrs td.id,
.srv.addrs td.dim {
	white-space: nowrap;
}

/* The name and its Edit, side by side: the row's one verb sits where the
   eye already is, and the slack the card has past the columns is the
   table's right edge rather than a gap the verb is stranded across. */
.addrs .ptr-row {
	display: inline-flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-5);
}

/* A rail fact too long for the value side, the processor's name: the key
   over it and the value the card's width, wrapping where it must, with its
   qualifier on the line under. */
.krow.stack {
	flex-direction: column;
	gap: var(--s-2);
}

.krow.stack .kv {
	align-items: flex-start;
	text-align: left;
	gap: var(--s-1);
}

.krow.stack .kv b {
	overflow-wrap: anywhere;
}

.rb-row b.attn {
	color: var(--attn);
}

/* The rescue systems: tiles as the operating systems are, each a real
   radio under its label, chosen in the page's one blue. A system that is
   one of the families wears its mark; any other wears the drive. The
   names are long ("SystemRescue 11.01 amd64") and wrap inside the tile
   rather than being cut. */
.rsc-tiles {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
	gap: var(--s-5);
}

.field .rsc-tile {
	position: relative;
	display: flex;
	align-items: center;
	gap: var(--s-6);
	min-height: 56px;
	margin: 0;
	padding: var(--s-5) var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	background: var(--surface);
	color: var(--ink);
	cursor: pointer;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}

.rsc-tile input {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	opacity: 0;
	cursor: inherit;
}

.rsc-tile:hover {
	border-color: var(--border-strong);
	background: var(--raised);
}

.rsc-tile:has(input:checked) {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}

.rsc-tile:has(input:focus-visible) {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.rsc-tile .oslogo,
.rsc-disk {
	flex: none;
	width: 28px;
	height: 28px;
}

.rsc-disk {
	color: var(--body);
}

.rsc-t {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.rsc-t b {
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	line-height: 1.25;
	overflow-wrap: anywhere;
}

.rsc-t small {
	font-size: var(--t-xs);
	color: var(--body);
}

/* The Install tab's first question, as three cards in a row: what the verb
   does to the disks is the sentence under each name, which is the one thing
   that tells them apart. The same tile treatment as the rescue systems, so
   the chosen one reads the same way down the page. One column on a phone. */
.ipick {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: var(--s-5);
}

@media (max-width: 760px) {
	.ipick {
		grid-template-columns: minmax(0, 1fr);
	}
}

.ipick-card {
	position: relative;
	display: flex;
	align-items: flex-start;
	min-width: 0;
	margin: 0;
	padding: var(--s-7) var(--s-8);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	background: var(--surface);
	color: var(--ink);
	cursor: pointer;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}

.ipick-card input {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	opacity: 0;
	cursor: inherit;
}

.ipick-card:hover {
	border-color: var(--border-strong);
	background: var(--raised);
}

.ipick-card:has(input:checked) {
	border-color: var(--accent);
	background: var(--accent-soft);
	box-shadow: inset 0 0 0 1px var(--accent);
}

.ipick-card:has(input:focus-visible) {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.ipick-t {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	min-width: 0;
}

.ipick-t b {
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	line-height: 1.25;
}

.ipick-t small {
	font-size: var(--t-sm);
	line-height: 1.45;
	color: var(--body);
}

/* The address table's netmask and gateway cells carry their column's name
   as a small key, hidden while the header row says it and shown once the
   table has become the stack of cards below 900px, where a bare number
   beside the type would have no name. */
.addrs .cell-k {
	display: none;
}

@media (max-width: 900px) {
	.addrs .cell-k {
		display: inline;
		margin-right: var(--s-3);
		font-family: var(--sans);
		font-size: var(--t-xs);
		letter-spacing: 0.04em;
		text-transform: uppercase;
		color: var(--body);
	}
}

/* The account's keys as boxes to tick: one row each, the box drawn by the
   platform so it is unmistakably a checkbox, the name and the key's tail. */
.keypick {
	display: grid;
	border: 1px solid var(--line);
	border-radius: var(--r);
	overflow: hidden;
}

.field .keyrow {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	margin: 0;
	padding: var(--s-5) var(--s-7);
	background: var(--surface);
	cursor: pointer;
}

.keyrow + .keyrow {
	border-top: 1px solid var(--hair);
}

.keyrow:hover {
	background: var(--raised);
}

.keyrow input {
	flex: none;
	width: 16px;
	height: 16px;
	margin: 0;
	accent-color: var(--accent);
}

.keyrow-t {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: var(--s-1) var(--s-6);
	min-width: 0;
}

.keyrow-t b {
	font-weight: var(--w-medium);
	color: var(--ink);
}

.keyrow-t .id {
	font-family: var(--mono);
	font-size: var(--t-sm);
	color: var(--body);
	overflow-wrap: anywhere;
}

/* The amber band: the one region of the dashboard allowed the colour. */
.nyou {
	background: var(--attn-soft);
	border: 1px solid color-mix(in srgb, var(--attn) 35%, var(--line));
	border-radius: var(--r-lg);
	overflow: hidden;
}

.nyou-h {
	padding: var(--s-6) var(--s-9);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--attn);
	border-bottom: 1px solid color-mix(in srgb, var(--attn) 35%, var(--line));
}

.nyou-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-8);
	padding: var(--s-7) var(--s-9);
}

.nyou-row + .nyou-row {
	border-top: 1px solid color-mix(in srgb, var(--attn) 20%, var(--line));
}

.nyou-t {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.nyou .btn {
	flex: none;
}

.dash-main {
	min-width: 0;
}

/* Below 1100px the rail stacks under main, except the server page's Connect
   panel, which comes first: on a phone the address and the SSH line are what
   somebody opened the page for (David, 2026-11-03). The rail dissolves into
   the column so its two cards can sit either side of the charts. */
@media (max-width: 1100px) {
	.srv-cols,
	.dash-cols {
		grid-template-columns: minmax(0, 1fr);
	}

	.srv-cols {
		row-gap: var(--s-8);
	}

	.srv-cols > .srv-rail {
		display: contents;
	}

	/* Overview's alone (the readiness pass of 2026-11-04): Access,
	   Console and Networking open on their own work, with the card in its
	   place in the rail below it. */
	.srv-cols.connect-first > .srv-rail > [aria-label="Connect"] {
		order: -1;
	}
}

/* Below 900px a table is a stack of cards: the name across the top, the
   rest of the row on one wrapping line under it, and the columns that were
   only there for a wide screen gone. */
@media (max-width: 900px) {
	.srv thead {
		display: none;
	}

	.srv,
	.srv tbody {
		display: block;
	}

	.srv tr {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--s-3) var(--s-6);
		padding: var(--s-6) var(--s-9);
		border-top: 1px solid var(--hair);
	}

	.srv td {
		display: block;
		padding: 0;
		border: 0;
		min-width: 0;
		max-width: 100%;
	}

	.srv td:last-child {
		flex: 1 1 auto;
	}

	.srv th:first-child,
	.srv td:first-child,
	.srv td:last-child {
		padding: 0;
	}

	.srv td:first-child {
		flex-basis: 100%;
	}

	.srv .wide {
		display: none;
	}

	.srv .end {
		margin-left: auto;
	}

	.srv tr.live td:first-child {
		box-shadow: none;
		padding-left: var(--s-6);
		border-left: 2px solid var(--accent);
	}

	.srv tr.due td,
	.srv tr.failed td {
		background: none;
	}

	.rdns {
		width: 100%;
	}
}

@media (max-width: 760px) {
	.shead {
		flex-direction: column;
	}

	.facts {
		gap: var(--s-2) var(--s-8);
	}
}

/* --- Lists (October 2026) ---------------------------------------------------
   The list pages of the October redesign: Servers, Activity, Networking, SSH
   keys and Team. One table component, and the two page shapes it sits in: a
   list page is the title, one line, and a table that runs to the window's
   edge; a two-column page is that table in the main column with a card of
   what you act on in a rail beside it.

   Three numbers the system states and no token carried: the height of an
   input or a primary button, the height of a button inside a table, and the
   size of an identifier. Named here so nothing below invents them twice. */
:root {
	--ctl: 44px;
	--ctl-sm: 32px;
	--t-id: 13.5px;
}

/* The head: the name, one line under it, the one thing the page offers at
   the right. Bottom-aligned, so the button sits on the title's baseline and
   not above a line of text it has nothing to do with. */
.lh {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: var(--s-8);
	flex-wrap: wrap;
}

.lh h1 {
	margin: 0;
}

/* The line under the title is the page's one sentence: a count, or the one
   thing worth knowing before the rows. Body grey, so the title is read first
   and the line after it, and held to the prose measure, because a sentence
   1800px long is a sentence nobody finishes. */
.lh-sub {
	margin: var(--s-2) 0 0;
	font-size: var(--t-md);
	color: var(--body);
	max-width: var(--prose);
	line-height: 1.5;
}

/* --- The table ---------------------------------------------------------- */

/* The frame. A card by another name: the same line and the same 12px
   corner, no shadow on dark. Its own class rather than .card because .card
   carries a padding and a form measure this must not inherit -- a table's
   edges are the frame's edges. */
.list {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	overflow: hidden;
	min-width: 0;
}

/* The bar above the rows: chips on the left, the filter on the right. */
.list-bar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-6);
	padding: var(--s-7) var(--s-9);
	border-bottom: 1px solid var(--line);
}

/* A section title inside the frame, for the one list that is a sub-table of
   its page (the access matrix): the title at section size, and the legend
   where it is read before the dots, not after them. */
.list-h {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
	padding: var(--s-7) var(--s-9);
	border-bottom: 1px solid var(--line);
}

.list-h h2 {
	margin: 0;
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.015em;
}

.list-note {
	font-size: var(--t-sm);
	color: var(--body);
}

.list-note .act {
	color: var(--accent-ink);
}

/* The one 13px note a list is allowed, under the rows, for the thing
   somebody would otherwise have to find out by pressing. */
.list-foot {
	padding: var(--s-6) var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: var(--t-sm);
	color: var(--faint);
}

/* The rows scroll inside their own box when the window is narrower than the
   columns; the page never scrolls sideways. Positioned so the sr-only
   actions header is contained here rather than parked at the page's edge. */
.list-scroll {
	overflow-x: auto;
	position: relative;
	/* A thin scrollbar that is drawn, not an overlay one that is not: the
	   team and key tables ran wider than their column at 1440 and 2560 px
	   and the Actions column was simply off the edge, with nothing to say
	   so (2026-11-02). */
	scrollbar-width: thin;
}

/* Nothing to list, said inside the frame with the frame's own air. */
.list-empty {
	padding: var(--s-9);
}

/* The bare table rules above this section give every table a 560px floor,
   a sticky shaded header and grey cells. Each is unsaid here on purpose:
   the floor would stop the rows becoming cards on a phone, the header is
   not a strip in this design, and a cell is ink unless the column says
   otherwise. */
.lt {
	width: 100%;
	min-width: 0;
	border-collapse: collapse;
	font-size: var(--t-md);
	font-variant-numeric: tabular-nums;
	color: var(--ink);
}

/* Column heads: 12/600, tracked, uppercase, faint. Read once and never
   again, which is why they are the quietest thing in the frame. */
.lt th {
	text-align: left;
	padding: var(--s-6) var(--s-6);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--faint);
	white-space: nowrap;
	background: none;
	position: static;
	border-bottom: 0;
}

/* A row is 48px: a pill of 24 (12.5 at 1.5 and 3px either side, the one
   thing in a row taller than its line) between 12px above and below, which
   is what the system's "13px vertical padding" measures out to once the
   pill has a 1px border the drawing's does not. A row of text alone is a
   pixel shorter and nobody can see it; a row of buttons is one taller. */
.lt td {
	padding: var(--s-6) var(--s-6);
	border-top: 1px solid var(--hair);
	border-bottom: 0;
	vertical-align: middle;
	line-height: 1.5;
	color: var(--ink);
}

.lt tbody tr,
.lt tbody tr:hover {
	background: none;
	transition: none;
}

.lt .pill {
	padding: var(--s-1) var(--s-5);
	line-height: 1.4;
}

.lt th:first-child,
.lt td:first-child {
	padding-left: var(--s-9);
}

.lt th:last-child,
.lt td:last-child {
	padding-right: var(--s-9);
}

.lt tbody tr:hover td {
	background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* The row somebody has to act on sits on the amber ground the needs-band
   uses, so it is found before a word is read. */
.lt tbody tr.attn td {
	background: color-mix(in srgb, var(--attn) 6%, transparent);
}

/* A product line's heading between groups of rows: the rail's word, at
   label size, on the ground's shade so it reads as a divider and never as
   a row. */
.lt .lt-group th {
	padding: var(--s-4) var(--s-9);
	border-top: 1px solid var(--hair);
	background: var(--ground-2);
	color: var(--body);
}

/* A name in a row: 14.5 at medium, ink, and a link that underlines only
   when it is pointed at -- a column of underlines is a noisy band. */
.lt .name {
	font-weight: var(--w-medium);
	color: var(--ink);
	text-decoration: none;
}

a.name:hover {
	text-underline-offset: 3px;
}

/* A row that carries a second line stands 60px: the two lines sit tighter
   than body text, because together they are one fact and its qualifier. */
.lt .second {
	display: block;
	font-size: var(--t-sm);
	color: var(--body);
	line-height: 1.3;
}

.lt .srv-words,
.lt .stack {
	display: flex;
	flex-direction: column;
	flex: 1 1 auto;
	line-height: 1.3;
	min-width: 0;
}

/* Amber for the words that need somebody: a suspension's reason, a renewal
   that is a charge waiting. Text only; the pill beside it is the state. */
.lt .warn,
.lt .second.warn {
	color: var(--attn);
}

.lt .dim {
	color: var(--body);
}

/* An identifier -- an address, a fingerprint, a short id -- is the one
   thing in a row set in mono, at 13.5, because it is compared character by
   character and never read as a word. */
.lt .ident {
	font-family: var(--mono);
	font-size: var(--t-id);
	letter-spacing: -0.01em;
	color: var(--ink);
	white-space: nowrap;
}

/* The server cell: the state's dot, then the name and whatever qualifies
   it. The dot sits on the first line's centre, whatever the second line
   does. */
.lt .srv {
	display: flex;
	gap: var(--s-6);
	align-items: flex-start;
}

.lt .srv .sdot {
	margin-top: var(--s-3);
}

/* The actions cell: 32px buttons at the row's end, with 8px above and below
   so the row keeps its 48. A button in a table is a ghost: bordered, 13.5,
   medium, because a row of filled buttons is a wall. */
.lt .end {
	text-align: right;
	white-space: nowrap;
	padding-top: var(--s-4);
	padding-bottom: var(--s-4);
}

.lt .end .acts {
	display: inline-flex;
	flex-wrap: nowrap;
	gap: var(--s-2);
	vertical-align: middle;
}

.lt .btn.sm,
.lt .btn.ghost.sm {
	height: var(--ctl-sm);
	padding: 0 var(--s-6);
	font-size: var(--t-id);
	font-weight: var(--w-medium);
	line-height: 1;
	border-radius: var(--r-sm);
}

/* A small link in a cell, where a button would shout: "Make owner…". */
.lt .cell-link {
	font-size: var(--t-md);
	color: var(--accent-ink);
	text-decoration: none;
	padding: 0 var(--s-3);
}

.lt .cell-link:hover {
	text-underline-offset: 3px;
}

/* A control inside a row: a select that saves on change, at the table's
   button height so the row stays 48. The row's own text size, not the
   input's 15, because it sits among cells. */
.lt .sel-row {
	height: var(--ctl-sm);
	padding: 0 var(--s-11) 0 var(--s-5);
	font-size: var(--t-md);
	width: auto;
	min-width: 0;
	border-radius: var(--r-sm);
	vertical-align: middle;
	/* The role's words ("Operator: power, console, password and keys") set
	   the whole table's width; capped so the Actions column stays on the
	   page, the select still names the role. Wider since the Team table
	   stopped sharing its row with the invite form (2026-11-04). */
	max-width: 40ch;
}

.lt .sel-row + .btn {
	margin-left: var(--s-2);
}

/* A list inside a cell: the firewall's rules, one per line. The direction
   in grey, the match in mono, the comment faint; the lines sit tight. */
.lt .rules {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	margin: 0;
	padding: 0;
	list-style: none;
}

.lt .rules li {
	display: flex;
	gap: var(--s-5);
	align-items: baseline;
}

.lt .rules .rdir {
	flex: none;
	width: 5em;
	font-size: var(--t-sm);
	color: var(--body);
}

.lt .rules .rnote {
	white-space: nowrap;
	font-size: var(--t-sm);
	color: var(--faint);
}

/* A cell whose rows stack: top-aligned, or the name floats mid-way down a
   three-rule list. Not ".top", which is the bar across the top of the app. */
.lt td.vt {
	vertical-align: top;
}

/* The status chips in the bar: a segmented control at the table's text
   size, the pressed one lifted, the one that needs somebody amber when it
   is not empty. Scoped to the bar: .seg elsewhere stays the 12px picker it
   is. */
.list-bar .seg {
	padding: var(--s-1);
	gap: var(--s-1);
	border-radius: var(--r);
	background: var(--ground-2);
	max-width: 100%;
	overflow-x: auto;
}

.list-bar .seg a {
	font-size: var(--t-md);
	padding: var(--s-3) var(--s-6);
	border-radius: var(--r-sm);
	color: var(--body);
	white-space: nowrap;
}

.list-bar .seg a.on {
	background: var(--raised);
	color: var(--ink);
	font-weight: var(--w-semi);
}

.list-bar .seg a.attn {
	color: var(--attn);
}

.list-bar .seg .ct {
	margin-left: var(--s-2);
	color: var(--faint);
	font-weight: var(--w-normal);
}

/* The filter form sits at the bar's far end and takes what is left. */
.list-filter {
	display: flex;
	align-items: center;
	gap: var(--s-3);
	margin-left: auto;
	flex: 1 1 260px;
	justify-content: flex-end;
}

.list-filter .input {
	width: 260px;
	max-width: 100%;
	height: calc(var(--ctl-sm) + var(--s-2));
	padding: 0 var(--s-6);
	font-family: var(--sans);
	font-size: var(--t-md);
	background: var(--ground-2);
}

/* --- Two columns -------------------------------------------------------- */

/* Main (2/3) and a rail (1/3, 340 to 440px) for what you act on. Below
   1100 the rail stacks under main, and the card widens to the column. */
.cols {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(340px, 440px);
	gap: var(--s-10);
	align-items: start;
}

.cols > * {
	min-width: 0;
}

/* A card in the rail: 12px corners, 20px padding, 1px line, no shadow. A
   form, mostly, so the inputs run the card's full width and stand 44. */
.railcard {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-9);
	display: flex;
	flex-direction: column;
	gap: var(--s-7);
	min-width: 0;
}

.railcard h2 {
	margin: 0;
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.015em;
}

.railcard label {
	display: flex;
	flex-direction: column;
	gap: var(--s-3);
	font-size: var(--t-sm);
	color: var(--body);
}

.railcard label i {
	font-style: normal;
	color: var(--faint);
}

.railcard input,
.railcard select,
.railcard textarea {
	width: 100%;
	box-sizing: border-box;
	font: inherit;
	font-size: var(--t-base);
	color: var(--ink);
	background: var(--ground-2);
	border: 1px solid var(--line);
	border-radius: var(--r);
	padding: 0 var(--s-6);
	height: var(--ctl);
}

.railcard select {
	padding-right: var(--s-11);
}

/* A checkbox in a rail form sits beside its words, not over them: the
   rail's label stacks its field under its name, which made the invite's
   Billing access box a lone square above its sentence (2026-11-04). */
.railcard label.key-check {
	flex-direction: row;
	align-items: flex-start;
}

.railcard .key-check input[type="checkbox"] {
	width: 18px;
	height: 18px;
	padding: 0;
}

/* A pasted key is an identifier, so it is the one mono input: 13.5 and
   wrapping, with room for the four lines a key folds into. */
.railcard textarea {
	height: auto;
	min-height: calc(var(--ctl) * 2);
	padding: var(--s-5) var(--s-6);
	font-family: var(--mono);
	font-size: var(--t-id);
	line-height: 1.5;
	resize: vertical;
}

.railcard input:focus,
.railcard select:focus,
.railcard textarea:focus {
	outline: none;
	border-color: var(--accent);
	box-shadow: var(--halo);
}

.railcard .btn {
	height: var(--ctl);
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	border-radius: var(--r);
}

/* The control's one 13px note. */
.railnote {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--faint);
	line-height: 1.5;
}

.railnote code,
.railcard label code {
	font-family: var(--mono);
	font-size: var(--t-id);
	color: var(--ink);
}

/* The how-to behind "Don't have a key?", kept at the card's own rhythm. */
.railcard .howto {
	margin: 0;
}

.railcard .howto summary {
	font-size: var(--t-md);
	color: var(--accent-ink);
	cursor: pointer;
}

/* A form standing alone, with no table beside it: the empty keys page. It
   takes the prose measure, which is where a form stops. */
.lone {
	max-width: var(--prose);
}

/* The main column of a two-column page that stacks more than one list:
   24px between blocks, the same as everywhere else on the page. */
.cols-main {
	display: grid;
	gap: var(--s-10);
	min-width: 0;
}

/* A two-column page whose rail has nothing in it, for somebody the rail's
   form would only refuse: the main column takes the width. */
.cols.one {
	grid-template-columns: minmax(0, 1fr);
}

/* The Team page's members table keeps its whole width below 1800px, with
   the invite form under it rather than beside it. */
@media (max-width: 1799px) {
	.cols.team-cols {
		grid-template-columns: minmax(0, 1fr);
	}

	.cols.team-cols > :last-child {
		max-width: 560px;
	}
}

/* The access matrix inside the list frame. Row heads are emails, not column
   labels, so they drop the head's uppercase and take the name's weight; the
   role beside them is sans and quiet, not a mono tag; the cells centre,
   because a dot in a left-aligned column reads as a list, not a grid. */
.lt.amx tbody th {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	letter-spacing: 0;
	text-transform: none;
	color: var(--ink);
	padding: var(--s-6) var(--s-6) var(--s-6) var(--s-9);
	border-top: 1px solid var(--hair);
	white-space: nowrap;
}

.lt.amx td {
	text-align: center;
}

.lt.amx thead th:not(:first-child) {
	text-align: center;
}

.lt.amx .amx-role {
	margin-left: var(--s-4);
	font-family: var(--sans);
	font-size: var(--t-sm);
	font-weight: var(--w-normal);
	letter-spacing: 0;
	text-transform: none;
	color: var(--body);
}

.lt.amx .amx-dot {
	font-size: var(--t-md);
}

/* The caller's own row: the accent, because it is wayfinding ("this one is
   you") and not a state. */
.lt .pill.you {
	color: var(--accent-ink);
	background: var(--accent-soft);
	border-color: color-mix(in srgb, var(--accent) 35%, var(--line));
}

.lt .pill.you::before {
	background: var(--accent);
}

/* A select and its no-script Save button, side by side in a cell. The cell
   takes the actions cell's 8px, so a row with a menu in it stays 48. */
.rolef {
	display: inline-flex;
	align-items: center;
	gap: var(--s-2);
	margin: 0;
}

.lt td:has(> .rolef) {
	padding-top: var(--s-4);
	padding-bottom: var(--s-4);
}

/* --- Narrow ------------------------------------------------------------- */

@media (max-width: 1100px) {
	.cols {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* Below 900 a table becomes cards, with the same facts: each row is a
   block, each cell a line, and the column head is written back beside the
   cells whose meaning it carried. The matrix is exempt, it is a grid and
   not a list, and scrolls instead. */
@media (max-width: 900px) {
	.lt:not(.amx),
	.lt:not(.amx) tbody,
	.lt:not(.amx) tr,
	.lt:not(.amx) td {
		display: block;
	}

	.lt:not(.amx) thead {
		display: none;
	}

	.lt:not(.amx) tbody tr {
		padding: var(--s-6) var(--s-8);
		border-top: 1px solid var(--hair);
	}

	.lt:not(.amx) td,
	.lt:not(.amx) td:first-child,
	.lt:not(.amx) td:last-child {
		padding: var(--s-1) 0;
		border: 0;
	}

	.lt:not(.amx) td[data-h]::before {
		content: attr(data-h);
		display: inline-block;
		min-width: 6.5em;
		margin-right: var(--s-4);
		font-size: var(--t-xs);
		font-weight: var(--w-semi);
		letter-spacing: 0.05em;
		text-transform: uppercase;
		color: var(--faint);
	}

	.lt:not(.amx) .lt-group th {
		display: block;
	}

	.lt:not(.amx) .end {
		text-align: left;
		padding-top: var(--s-4);
	}

	.lt:not(.amx) .end .acts {
		flex-wrap: wrap;
	}

	.lt:not(.amx) tbody tr:hover td {
		background: none;
	}

	.lt:not(.amx) tbody tr.attn td {
		background: none;
	}

	.lt:not(.amx) tbody tr.attn {
		background: color-mix(in srgb, var(--attn) 6%, transparent);
	}

	.lt .rules .rdir {
		width: auto;
	}

	/* A fingerprint is wider than a phone; it breaks rather than scrolls. */
	.lt:not(.amx) .ident {
		white-space: normal;
		overflow-wrap: anywhere;
	}

	.lt:not(.amx) .rules li {
		flex-wrap: wrap;
	}

	.list-bar {
		padding: var(--s-6) var(--s-8);
	}

	.list-bar .seg {
		flex-wrap: wrap;
		overflow: visible;
	}

	/* A row's role menu takes the card's width, as every other control
	   does on a phone. */
	.rolef,
	.lt .sel-row {
		width: 100%;
	}

	.list-filter {
		flex-basis: 100%;
	}

	.list-filter .input {
		width: 100%;
	}

	.list-foot,
	.list-h {
		padding-left: var(--s-8);
		padding-right: var(--s-8);
	}

	.lh .btn {
		width: 100%;
	}
}

/* --- Round two (October 2026) ---------------------------------------------
   The row is the link. A table of servers, invoices or tickets has one
   destination per row, and asking for the one word in it that is a link is
   a game nobody asked to play; panel.js sends a click anywhere on a row
   that carries data-href there, except on a control. The name is still a
   real link for no script, the keyboard and the middle mouse button. */
tr[data-href] {
	cursor: pointer;
}

tr[data-href]:hover > td {
	background: color-mix(in srgb, var(--ink) 4%, transparent);
}

/* The fleet's chips are wrapped so a filter's answer can redraw them, and
   the wrapper takes no part in the bar's layout. */
.fleet-chips {
	display: contents;
}

/* A server to filter the ledger by, at the bar's far end: a select, because
   a chip per server stops fitting on the bar at the sixth server. */
.list-bar .list-pick {
	margin-left: auto;
	display: flex;
	align-items: center;
	gap: var(--s-4);
}

.list-bar .list-pick select {
	font: inherit;
	font-size: var(--t-md);
	color: var(--ink);
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-sm);
	padding: 0 var(--s-6);
	height: 40px;
	min-width: 200px;
}

.list-bar .list-pick button {
	font: inherit;
}

/* The rebuild rail (portal review, October 2026, round three): what the
   rebuild does, as label-and-value lines, and under a hairline the confirm
   the picker on the left used to push around. The input runs the card's
   width and the button under it does too, so the two line up whatever the
   hint beside them wraps to. */
.rb-rows {
	display: grid;
	gap: var(--s-4);
	margin: var(--s-3) 0 var(--s-5);
}
.rb-row {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--s-6);
	font-size: var(--t-md);
	color: var(--body);
}
.rb-row b {
	min-width: 0;
	font-weight: var(--w-medium);
	color: var(--ink);
	text-align: right;
}
.rb-row a {
	color: var(--accent-ink);
}
.rb-confirm {
	display: grid;
	gap: var(--s-5);
	padding-top: var(--s-7);
	border-top: 1px solid var(--hair);
}
.rb-confirm .field {
	margin-bottom: 0;
}
.rb-confirm input {
	width: 100%;
}
/* The rebuild card holds a .field, so the form-measure rule above would
   cap it at 820; but its field is a row of tiles, not a line of text, and
   beside a rail it takes the column it was given. */
.with-rail > .main-col > .card:has(.cfg-os) {
	max-width: none;
}

/* The server page's panes (Access, Snapshots, Networking) are lists with a
   form at the foot, so the form-measure cap above took them for forms and
   held them to 820 on a screen twice that; the list is the card, and it
   takes the width the pane has. */
.pane-body > .card:has(form):not(:has(.msg)) {
	max-width: none;
}

/* --- Catalogue: lines as bands, sizes as rows ----------------------------- */

/* The search box at the head of the rail, Ctrl K from anywhere. */
.cat-rail > * {
	min-width: 0;
}
.cat-find label {
	font-size: 12px;
	font-weight: var(--w-medium);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--faint);
	margin: 0;
}
.cat-q {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	height: 36px;
	padding: 0 var(--s-5);
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
	color: var(--faint);
}
.cat-q:focus-within {
	border-color: var(--accent);
	box-shadow: var(--halo);
}
.cat-q svg {
	width: 14px;
	height: 14px;
	flex: none;
}
.cat-q input {
	/* A search input's own size is twenty characters, which was wider
	   than the rail and made the rail's column grow to fit it. */
	flex: 1 1 0;
	width: 0;
	min-width: 0;
	height: 100%;
	margin: 0;
	padding: 0;
	border: 0;
	background: transparent;
	font: inherit;
	font-size: var(--t-md);
	color: var(--ink);
	outline: 0;
}
.cat-q input::-webkit-search-cancel-button {
	appearance: none;
}
.cat-q kbd {
	flex: none;
	font-family: inherit;
	font-size: 11px;
	color: var(--faint);
	border: 1px solid var(--line);
	border-radius: 5px;
	padding: 1px var(--s-4);
	background: var(--ground);
	white-space: nowrap;
}
/* A count on a chip (the makers): quiet, so the word is read first. */
.cat-chip i {
	font-style: normal;
	font-size: var(--t-sm);
	color: var(--faint);
	margin-left: var(--s-3);
	font-variant-numeric: tabular-nums;
}
.cat-chip:has(input:checked) i {
	color: #fff;
	opacity: 0.8;
}

/* One line as a band: a raised head that says what the line is, and its
   rows under it on the band's own surface. Two lines tell apart by the
   gap between their bands and the words on their heads, which the three
   headings of the old page never managed. */
.cat-band {
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	overflow: hidden;
}
.cat-bh {
	display: flex;
	justify-content: space-between;
	align-items: flex-start;
	gap: var(--s-6) var(--s-9);
	flex-wrap: wrap;
	padding: var(--s-8) var(--s-9) var(--s-7);
	background: var(--raised);
	border-bottom: 1px solid var(--line);
}
.cat-bt h2 {
	display: inline;
	margin: 0 var(--s-5) 0 0;
	font-family: var(--ui);
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	letter-spacing: -0.015em;
	text-transform: none;
	color: var(--ink);
}
.cat-bt p {
	display: inline;
	margin: 0;
	font-size: var(--t-md);
	color: var(--body);
}
.cat-bt .cat-made {
	margin-top: var(--s-4);
}
.cat-bm {
	margin-left: auto;
	text-align: right;
	white-space: nowrap;
	display: grid;
	gap: 2px;
}
.cat-bm b {
	font-size: var(--t-md);
	font-weight: var(--w-medium);
	color: var(--ink);
}
.cat-tw {
	overflow-x: auto;
}
.cat-tbl {
	width: 100%;
	border-collapse: collapse;
}
.cat-tbl th {
	padding: var(--s-4) var(--s-8);
	text-align: left;
	font-family: inherit;
	font-size: var(--t-xs);
	font-weight: var(--w-medium);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--faint);
	white-space: nowrap;
}
.cat-tbl td {
	padding: var(--s-6) var(--s-8);
	border-top: 1px solid var(--hair);
	font-size: var(--t-md);
	color: var(--body);
	white-space: nowrap;
	vertical-align: middle;
}
.cat-tbl tr {
	position: relative;
}
.cat-tbl tbody tr:hover td {
	background: var(--raised);
}
.cat-tbl td.size b {
	font-size: var(--t-lg);
	font-weight: var(--w-medium);
	color: var(--ink);
	letter-spacing: -0.01em;
}
.cat-tbl td.size b.name {
	font-size: var(--t-base);
}
.cat-tbl td.size small {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-left: 3px;
}
.cat-tbl th.num,
.cat-tbl td.num {
	text-align: right;
	font-variant-numeric: tabular-nums;
}
.cat-tbl td.num b {
	font-size: var(--t-base);
	font-weight: var(--w-semi);
	color: var(--ink);
}
.cat-tbl td.num small {
	font-size: var(--t-sm);
	color: var(--faint);
	margin-left: 2px;
}
.cat-yr {
	display: block;
	font-size: var(--t-sm);
	color: var(--body);
	margin-top: 2px;
}
.cat-tbl th.go,
.cat-tbl td.go {
	width: 1%;
	text-align: right;
}
.cat-tbl td.go .btn {
	height: 36px;
	padding: 0 var(--s-7);
}
/* The whole row is the link: the button stays the one focusable thing
   and its hit area is stretched over the row. */
.cat-tbl td.go .btn,
.cat-tbl td.go .btn:hover,
.cat-tbl td.go .btn:active {
	position: static;
	transform: none;
}
.cat-tbl td.go .btn.accent::after {
	content: "";
	position: absolute;
	inset: 0;
	top: 0;
	left: 0;
	width: auto;
	height: auto;
	transform: none;
	border-radius: 0;
}
.cat-tbl tr.sold td,
.cat-tbl tr.sold td.size b,
.cat-tbl tr.sold td.num b {
	color: var(--faint);
}
.cat-tbl tr.sold:hover td {
	background: transparent;
}
.cat-tbl td.sum {
	display: none;
}
/* The link's old rule packed it to the start of its grid cell, and a
   block in a band honours justify-self too now, so it is told to
   stretch: the rule under it spans the band. */
.cat-ask {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-4) var(--s-8);
	padding: var(--s-6) var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: var(--t-md);
	color: var(--body);
}

.cat-band .cat-more {
	display: block;
	justify-self: stretch;
	width: auto;
	max-width: none;
	box-sizing: border-box;
	padding: var(--s-6) var(--s-9);
	border-top: 1px solid var(--hair);
	font-size: var(--t-sm);
	color: var(--link);
}
/* On a phone the columns fold: the size and the month on one line, the
   facts in one quiet line under them beside Choose. */
@media (max-width: 900px) {
	.cat-tbl thead {
		display: none;
	}
	.cat-tbl,
	.cat-tbl tbody {
		display: block;
		width: auto;
		min-width: 0;
	}
	.cat-tbl th.go,
	.cat-tbl td.go {
		width: auto;
	}
	.cat-tbl tr {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto;
		align-items: center;
		column-gap: var(--s-6);
		padding: var(--s-6) var(--s-7);
		border-top: 1px solid var(--hair);
	}
	.cat-tbl td {
		display: block;
		padding: 0;
		border: 0;
		white-space: normal;
	}
	.cat-tbl td.fact,
	.cat-tbl td.avail {
		display: none;
	}
	.cat-tbl td.sum {
		display: block;
		grid-column: 1;
		grid-row: 2;
		font-size: var(--t-sm);
		color: var(--faint);
		margin-top: 2px;
	}
	.cat-tbl td.size {
		grid-column: 1;
		grid-row: 1;
	}
	.cat-tbl td.num {
		grid-column: 2;
		grid-row: 1;
	}
	.cat-tbl td.go {
		grid-column: 2;
		grid-row: 2;
		margin-top: var(--s-3);
	}
	.cat-tbl td.go .btn {
		height: 32px;
		padding: 0 var(--s-6);
		font-size: var(--t-sm);
	}
	.cat-tbl tbody tr:hover td {
		background: transparent;
	}
	.cat-bh {
		padding: var(--s-7);
	}
	.cat-bm {
		margin-left: 0;
		text-align: left;
	}
}

/* --- The till, trimmed (portal review, October 2026) ---------------------
   The checkout's summary is wider than the configure page's, because it
   carries the total, a coupon, the terms and two doors, and at 320 those
   wrapped into a column of fragments. The lines beside it lost their
   ledger look: a label at the left edge and its value at the right read as
   two columns of unrelated words, so a line's facts sit together now, as
   label then value, and wrap as pairs. */
.configure.till {
	grid-template-columns: minmax(0, 1fr) 380px;
}
@media (max-width: 860px) {
	.configure.till {
		grid-template-columns: 1fr;
	}
}
.till .line .specs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-9);
}
.till .line .specs li {
	display: inline-flex;
	gap: var(--s-3);
	justify-content: flex-start;
}

/* The cart's size and the button that empties it, in one row under the
   lines: the count says what the button is about, and the button is a
   button, where a bare muted word was read as a caption. */
.lines-foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
	flex-wrap: wrap;
	padding: 0 var(--s-3);
	font-size: var(--t-sm);
	color: var(--text-muted);
}

/* The one promise, drawn as a row somebody can press anywhere on: a 20px
   box, the sentence beside it, a border that answers the hover and the
   accent when it is ticked. A bare 18px box over an 11px line was the
   smallest thing on the page and the only one that had to be hit. */
.key-check.agree {
	align-items: center;
	gap: var(--s-5);
	padding: var(--s-5) var(--s-6);
	border: 1px solid var(--line);
	border-radius: 8px;
	background: var(--sunk);
	font-size: var(--t-md);
	cursor: pointer;
	transition: border-color var(--t-fast) var(--e-io), background var(--t-fast) var(--e-io);
}
.key-check.agree:hover {
	border-color: var(--border-strong);
}
.key-check.agree:has(input:checked) {
	border-color: var(--accent-line);
	background: var(--accent-dim);
}
.key-check.agree input[type="checkbox"] {
	width: 20px;
	height: 20px;
	margin: 0;
}
.key-check.agree input[type="checkbox"]::after {
	width: 6px;
	height: 10px;
}
.consent:has(.agree) {
	padding-top: 0;
	border-top: 0;
}

/* The far end of the sign-in detour: one column, the width a summary card
   keeps, centred under a head that is centred with it. The card used to sit
   at the left edge of a page that was otherwise empty, at the width of a
   sidebar, and read as something that had fallen off the layout. */
.continue {
	max-width: 560px;
	width: 100%;
	margin: 0 auto;
	display: grid;
	gap: var(--s-8);
}
.continue .page-head {
	justify-content: center;
	text-align: center;
}
.continue .lede {
	margin: 0;
	text-align: center;
	color: var(--text-muted);
}
.continue .continue-card {
	max-width: none;
}
.continue .cfg-name {
	font-size: var(--t-xl);
}
.continue .btnrow {
	margin-top: var(--s-7);
}

/* --- The card sheet (portal review, October 2026) ------------------------
   PayPal's card fields on the till, in a sheet loaded with the page and
   lifted by Pay by card. Hidden under visibility, not display: the frames
   inside only report a size when they have one, and that report is what
   wakes the button (cardfields.js), so a display:none sheet would open on
   skeletons every time, which is the wait this exists to remove. */
.card-sheet {
	position: fixed;
	inset: 0;
	z-index: 60;
	display: grid;
	place-items: center;
	padding: var(--s-7);
	visibility: hidden;
	opacity: 0;
	transition: opacity var(--t-fast) var(--e-io), visibility 0s linear var(--t-fast);
}
.card-sheet.is-open {
	visibility: visible;
	opacity: 1;
	transition: opacity var(--t-fast) var(--e-io);
}
.card-sheet-back {
	position: absolute;
	inset: 0;
	background: color-mix(in srgb, var(--ground) 55%, transparent);
	backdrop-filter: blur(10px);
}
.card-sheet-panel {
	position: relative;
	width: min(440px, 100%);
	max-height: calc(100vh - 2 * var(--s-7));
	overflow-y: auto;
	padding: var(--s-9);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	background: var(--raised);
	color: var(--body);
	box-shadow: var(--sh-3);
	transform: translateY(8px);
	transition: transform var(--t-fast) var(--e-out);
}
.card-sheet.is-open .card-sheet-panel {
	transform: none;
}
.card-sheet-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
}
.card-sheet-head .cf-title {
	font-size: var(--t-lg);
	letter-spacing: -0.02em;
}
.sheet-x {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	border-radius: 8px;
	border: 1px solid transparent;
	background: none;
	color: var(--text-muted);
	cursor: pointer;
}
.sheet-x:hover {
	border-color: var(--line);
	color: var(--ink);
	background: var(--raise, var(--surface));
}
body.sheet-open {
	overflow: hidden;
}

/* Port 25's state and, when blocked, the way to ask: a fact and its button
   on one line under the firewall form. */
.fw-smtp {
	display: flex;
	align-items: center;
	gap: var(--s-4);
	flex-wrap: wrap;
	margin-top: var(--s-5);
}

/* --- The console card (2026-11-03) ---------------------------------------
   A screen takes the pane's width: the form-measure cap held it to 820. */
.pane-body > .console-card {
	max-width: none;
}

   One full-width screen. The mode switch is a pressed-in pair, the actions
   sit at the right, and under the screen a bar carries the keys a phone
   keyboard lacks. Everything wraps rather than overflows at 390px. */
.console-card .seg {
	display: flex;
	gap: 4px;
}

.console-card .drawer-h .right {
	flex-wrap: wrap;
	align-items: center;
}

.console-keys {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
	padding: var(--s-4) var(--s-6);
	background: var(--panel-2);
	border-top: 1px solid var(--panel-line);
}

.console-keys .mini {
	min-width: 36px;
	text-align: center;
}

.console-keys a.mini {
	text-decoration: none;
	display: inline-flex;
	align-items: center;
}

.console-keys-r {
	margin-left: auto;
	display: flex;
	gap: 6px;
	flex-wrap: wrap;
}

/* The phone keyboard's field for the graphical console: focusable, never
   seen. 16px so iOS does not zoom the page when it takes focus. */
.vnc-kbd {
	position: absolute;
	left: -9999px;
	top: 0;
	width: 1px;
	height: 1px;
	opacity: 0;
	font-size: 16px;
}

.console-paste-text {
	width: 100%;
	font-family: var(--mono);
	font-size: 16px;
	resize: vertical;
}

/* Where real full screen is refused, the card fills the window instead. */
.console-card.console-max {
	position: fixed;
	inset: 0;
	z-index: 60;
	border-radius: 0;
	display: flex;
	flex-direction: column;
	padding-top: env(safe-area-inset-top, 0px) !important;
	padding-bottom: env(safe-area-inset-bottom, 0px) !important;
}

.console-card.console-max .console-term,
.console-card.console-max .console-vnc,
.console-card:fullscreen .console-vnc {
	flex: 1;
	height: auto;
}

.console-card:fullscreen {
	display: flex;
	flex-direction: column;
}

html.console-maxed,
html.console-maxed body {
	overflow: hidden;
}

@media (max-width: 600px) {
	.console-term {
		height: 42vh;
		padding: var(--s-3);
	}

	.console-vnc {
		height: 38vh;
	}

	.console-card .drawer-h {
		padding: var(--s-4) var(--s-5);
	}

	.console-card .drawer-h .right {
		margin-left: 0;
		width: 100%;
	}

	.console-keys-r {
		margin-left: 0;
		width: 100%;
	}

	.console-keys .mini {
		padding: var(--s-3) 10px;
	}
}

/* A card's one action, at the right of its head. */
.card-h > .card-h-act {
	margin-left: auto;
}

/* --- States are words, not buttons -----------------------------------------
   A state was a bordered, filled pill, the same shape as the buttons beside
   it, and read as something to press (David, 2026-11-04: "some things
   aren't really buttons"). The dot and the word carry it now, with the
   colour of its kind and nothing around it, so the only rounded, bordered
   things on a page are the ones that do something. Last in the file and
   doubled, so it outranks every kind's own fill. */
.pill.pill,
.lt .pill.pill {
	border-color: transparent;
	background: transparent;
	box-shadow: none;
	padding: 0;
}

/* --- Billing ----------------------------------------------------------------
   Three tabs under one head (2026-11-04): what is owed stands above them in
   the one amber band with the one Pay button, and each tab is a list of
   rows with at most one quiet link at its end. */
.owe-band {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-5) var(--s-8);
	padding: var(--s-7) var(--s-8);
	border: 1px solid color-mix(in srgb, var(--attn) 45%, var(--line));
	border-radius: var(--r-lg);
	background: var(--attn-soft);
}

.owe-t {
	display: flex;
	flex-direction: column;
	gap: var(--s-1);
	min-width: 0;
}

.owe-t b {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.owe-t span {
	color: var(--body);
	max-width: 70ch;
}

.bill-tabs a .n {
	margin-left: var(--s-2);
	font-size: var(--t-xs);
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}

.lrows {
	display: flex;
	flex-direction: column;
}

.lrow {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto minmax(180px, auto);
	align-items: center;
	gap: var(--s-2) var(--s-8);
	padding: var(--s-6) var(--s-8);
	border-bottom: 1px solid var(--hair);
}

.lrow:last-child {
	border-bottom: 0;
}

.lrow-t {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
}

.lrow-t b {
	font-weight: var(--w-medium);
	color: var(--ink);
	overflow-wrap: anywhere;
}

.lrow-t span {
	font-size: var(--t-sm);
	color: var(--faint);
	overflow-wrap: anywhere;
}

.lrow-t span.attn {
	color: var(--attn);
}

.lrow-n {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
	text-align: right;
}

.lrow-e {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: var(--s-7);
}

.lrow-e form {
	display: contents;
}

/* A quiet verb at a row's end: a link in the accent's ink, never a box. */
.lnk {
	font: inherit;
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	color: var(--accent-ink);
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
	text-decoration: none;
	white-space: nowrap;
}

.lnk:hover {
	text-decoration: underline;
}

.lnk:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	border-radius: 2px;
}

.lc-foot {
	padding: var(--s-6) var(--s-8);
	border-top: 1px solid var(--hair);
}

@media (max-width: 640px) {
	.lrow {
		grid-template-columns: minmax(0, 1fr) auto;
		padding-inline: var(--s-6);
	}

	.lrow-e {
		grid-column: 1 / -1;
		justify-content: flex-start;
	}
}

/* --- You, at the rail's foot ------------------------------------------------
   The person block (2026-11-04): initials, name and email under the rail's
   last group, opening upward into the same menu the bar's avatar used to
   open. margin-top:auto pins it to the foot however short the rail is. */
.side-me {
	margin-top: auto;
	padding: var(--s-5) var(--s-5) var(--s-7);
	border-top: 1px solid var(--line);
}

.side-me > summary {
	width: 100%;
	height: auto;
	display: flex;
	align-items: center;
	gap: var(--s-5);
	padding: var(--s-4) var(--s-4);
	border-radius: var(--r);
	place-items: initial;
}

.side-me > summary:hover,
.side-me[open] > summary {
	background: var(--sunk);
}

.side-me > summary:hover .av,
.side-me[open] > summary .av {
	box-shadow: none;
}

.me-id {
	display: flex;
	flex-direction: column;
	min-width: 0;
	line-height: 1.3;
}

.me-id b,
.me-id span {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.me-id b {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-size: var(--t-base);
}

.me-id span {
	color: var(--faint);
	font-size: var(--t-sm);
}

.side-me .me-menu {
	left: var(--s-5);
	right: var(--s-5);
	top: auto;
	bottom: calc(100% - var(--s-3));
	min-width: 0;
	max-width: none;
}

/* Billing's measure: the band, the tabs and the tables share one width, so
   a two-row table is not a stretch of ground with its state at the edge. */
.bill {
	display: flex;
	flex-direction: column;
	gap: var(--s-8);
	max-width: 1120px;
}

/* --- Your spend --------------------------------------------------------------
   The wide screen's column beside Billing's tables (2026-11-04). Hidden
   until the canvas holds the tables at their 1120px and the column at its
   340px with the gutter between: the rail and the canvas's padding take
   about 340px, so 1800px of viewport. Below that the page is the tables. */
.bill-grid {
	display: grid;
	grid-template-columns: minmax(0, 1120px);
	gap: var(--s-10);
	align-items: start;
}

.spend {
	display: none;
}

@media (min-width: 1800px) {
	/* With the column beside them the tables may grow past their measure,
	   because a five-column table is the one thing on the page that reads
	   better wide; together the two fill a 2560px screen's canvas. */
	.bill-grid {
		grid-template-columns: minmax(0, 1fr) 340px;
		max-width: 1960px;
	}

	.bill-grid > .bill {
		max-width: none;
	}

	.spend {
		display: flex;
		flex-direction: column;
		gap: var(--s-6);
		position: sticky;
		top: calc(var(--s-10) + 56px);
	}
}

.sc {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-7);
	display: flex;
	flex-direction: column;
	gap: var(--s-4);
}

.sc-l {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--faint);
}

.sc-l a,
.sc-r {
	font-size: var(--t-sm);
	letter-spacing: 0;
	text-transform: none;
	font-weight: var(--w-medium);
}

.sc-l a {
	color: var(--accent-ink);
	text-decoration: none;
}

.sc-r {
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.sc-big {
	font-size: var(--t-xl);
	font-weight: var(--w-semi);
	color: var(--ink);
	font-variant-numeric: tabular-nums;
}

.sc-big small {
	font-size: var(--t-sm);
	font-weight: 400;
	color: var(--faint);
}

.sc p {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--body);
}

.split {
	display: flex;
	gap: 2px;
	height: 8px;
	border-radius: 4px;
	overflow: hidden;
}

.split i {
	display: block;
	height: 100%;
}

/* A line's colour, in the bar and on its legend's swatch. */
.split .sp-0,
.sc-legend .sp-0::before {
	background: var(--accent);
}

.split .sp-1,
.sc-legend .sp-1::before {
	background: var(--accent-2);
}

.split .sp-2,
.sc-legend .sp-2::before {
	background: color-mix(in srgb, var(--accent) 45%, var(--ground));
}

.split .sp-3,
.sc-legend .sp-3::before {
	background: var(--faint);
}

.sc-legend {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	font-size: var(--t-sm);
}

.sc-legend div {
	display: flex;
	justify-content: space-between;
	gap: var(--s-4);
	color: var(--body);
}

.sc-legend span {
	display: inline-flex;
	align-items: center;
	gap: var(--s-3);
	min-width: 0;
}

.sc-legend span::before {
	content: "";
	width: 8px;
	height: 8px;
	border-radius: 2px;
	flex: none;
}

.sc-legend b,
.sc-up em {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-style: normal;
	font-variant-numeric: tabular-nums;
}

.sc-up {
	display: flex;
	flex-direction: column;
	gap: var(--s-4);
	font-size: var(--t-sm);
}

.sc-up > div {
	display: grid;
	grid-template-columns: 52px minmax(0, 1fr) auto;
	column-gap: var(--s-4);
	align-items: baseline;
}

.sc-up time {
	color: var(--faint);
	font-variant-numeric: tabular-nums;
}

.sc-up b {
	color: var(--ink);
	font-weight: var(--w-medium);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.sc-up small {
	grid-column: 2 / -1;
	color: var(--faint);
	font-size: var(--t-xs);
}

/* What was paid, a row a month, newest first: the money that moved and
   nothing taken off, so credit bought is counted once, as its top-up. */
.sc-paid {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	margin: 0;
	font-size: var(--t-sm);
}

.sc-paid > div {
	display: flex;
	justify-content: space-between;
	gap: var(--s-4);
}

.sc-paid dt {
	color: var(--body);
}

.sc-paid dd {
	margin: 0;
	color: var(--ink);
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
}

.sc-paid .now dt {
	color: var(--ink);
}

.sc-foot {
	font-size: var(--t-xs) !important;
	color: var(--faint) !important;
}

.sc-kv {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	gap: var(--s-2) var(--s-6);
	margin: 0;
	font-size: var(--t-sm);
}

.sc-kv dt {
	color: var(--faint);
}

.sc-kv dd {
	margin: 0;
	color: var(--ink);
	overflow-wrap: anywhere;
}

/* The console's split waits for a wide screen: below 1800px the console
   takes the whole width and the rail is not drawn, because a cramped
   screen of somebody's server is worse than a missing card beside it. */
@media (max-width: 1799px) {
	.srv-cols.wide-only {
		grid-template-columns: minmax(0, 1fr);
	}

	.srv-cols.wide-only > .srv-rail {
		display: none;
	}
}

/* The addresses table in its card, so it is one object with an edge
   rather than rows floating on the ground. */
.addr-card {
	padding: 0;
	overflow: hidden;
}

/* Billing at 2400px and up: the measure lets go and the column widens,
   so a 2560px screen is used edge to edge (David, 2026-11-04). */
@media (min-width: 2400px) {
	.bill-grid {
		grid-template-columns: minmax(0, 1fr) 400px;
		max-width: none;
	}
}

/* The extra-IPv4 ask under the addresses: the sentence and its button on
   one line, wrapping on a phone. */
.addr-more {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-4) var(--s-7);
	margin-top: var(--s-6);
}

/* The paste box opened inside a full-screen console: shown in place, not
   as a page modal, so the browser keeps the console full screen. */
.console-card dialog.in-full {
	position: absolute;
	inset: 0;
	margin: auto;
	width: min(560px, calc(100% - 32px));
	height: fit-content;
	z-index: 5;
	box-shadow: var(--sh-3);
}

.console-card:fullscreen {
	position: relative;
}

/* A rebuild in flight: one panel in the middle of the Rebuild tab, the
   spinner over the words, nothing to press. */
.rb-progress {
	max-width: 560px;
	margin: var(--s-12) auto;
	padding: var(--s-10) var(--s-9);
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--s-5);
	text-align: center;
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
}

.rb-progress h2 {
	margin: 0;
	font-size: var(--t-xl);
	color: var(--ink);
	text-wrap: balance;
}

.rb-progress p {
	margin: 0;
	color: var(--body);
	max-width: 46ch;
}

.rb-spin {
	width: 28px;
	height: 28px;
	border-radius: 50%;
	border: 3px solid color-mix(in srgb, var(--accent) 25%, transparent);
	border-top-color: var(--accent);
	animation: rb-turn 900ms linear infinite;
}

@keyframes rb-turn {
	to {
		transform: rotate(360deg);
	}
}

@media (prefers-reduced-motion: reduce) {
	.rb-spin {
		animation: none;
	}
}

/* --- The move from the old panel (2026-11-04) --------------------------------
   One column in the middle: the tracker, the step's one action, what moves,
   and the help links. */
.move {
	max-width: 760px;
	margin-inline: auto;
	display: flex;
	flex-direction: column;
	gap: var(--s-8);
}

.move-track {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
}

.move-track li {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	padding-right: var(--s-5);
	min-width: 0;
}

/* The line to the next step, drawn from this step's circle. */
.move-track li::before {
	content: "";
	position: absolute;
	top: 10px;
	left: 22px;
	right: 0;
	height: 2px;
	background: var(--line);
}

.move-track li:last-child::before {
	display: none;
}

.move-track li.done::before {
	background: var(--accent);
}

.move-track .n {
	position: relative;
	z-index: 1;
	width: 22px;
	height: 22px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	border: 2px solid var(--line);
	background: var(--ground);
	color: var(--faint);
}

.move-track li.done .n {
	background: var(--accent);
	border-color: var(--accent);
	color: var(--ground);
}

.move-track li.here .n {
	border-color: var(--accent);
	background: var(--accent-soft);
	color: var(--accent-ink);
}

.move-track .t {
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	color: var(--faint);
}

.move-track li.done .t,
.move-track li.here .t {
	color: var(--ink);
}

.move-track .s {
	font-size: var(--t-xs);
	color: var(--faint);
}

.move-now {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: var(--s-8) var(--s-9);
	display: flex;
	flex-direction: column;
	gap: var(--s-5);
}

.move-now.done {
	border-color: color-mix(in srgb, var(--ok, var(--accent)) 40%, var(--line));
}

.move-now.attn {
	border-color: color-mix(in srgb, var(--attn) 45%, var(--line));
	background: var(--attn-soft);
}

.move-now h2 {
	margin: 0;
	font-size: var(--t-lg);
	color: var(--ink);
}

.move-now p {
	margin: 0;
	color: var(--body);
	max-width: 62ch;
}

.move-now .acts {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-5);
}

.move-keep {
	display: flex;
	flex-direction: column;
	gap: 2px;
	padding: var(--s-5) var(--s-6);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--ground);
}

.move-keep b {
	color: var(--ink);
	font-weight: var(--w-medium);
}

.move-keep span {
	font-size: var(--t-sm);
	color: var(--faint);
}

.move-help {
	display: flex;
	flex-direction: column;
	gap: var(--s-4);
}

.move-help h2 {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--ink);
	font-weight: var(--w-semi);
}

.move-help ul {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: var(--s-3);
}

.move-help li {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: var(--s-2) var(--s-8);
	font-size: var(--t-sm);
	color: var(--body);
}

@media (max-width: 600px) {
	.move-track {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		row-gap: var(--s-6);
	}

	.move-track li:nth-child(2)::before {
		display: none;
	}
}

/* A lone button in the step card keeps its own width. */
.move-now > .btn,
.move-now > form {
	align-self: flex-start;
}

/* --- Team (David's sketch, 2026-11-04) -----------------------------------
   Four tabs under one head; a read-only members table whose rows open a
   panel; the panel at the right with radio cards, the Billing switch and a
   live "can / can't" box; a compare table of every role. */
.tm-lt tbody tr.tm-open {
	cursor: pointer;
}

.tm-who {
	display: flex;
	align-items: center;
	gap: var(--s-6);
	min-width: 0;
}

.tm-id {
	display: flex;
	flex-direction: column;
	min-width: 0;
	line-height: 1.3;
}

.tm-id .name .dim {
	margin-left: var(--s-2);
	font-weight: var(--w-normal);
	color: var(--body);
}

.tm-id .second {
	display: block;
	font-size: var(--t-sm);
	color: var(--body);
	overflow-wrap: anywhere;
}

.tm-av {
	width: 32px;
	height: 32px;
	font-size: 12px;
	letter-spacing: 0.02em;
}

.tm-av.lg {
	width: 44px;
	height: 44px;
	font-size: 15px;
}

.tm-bill {
	display: inline-flex;
	align-items: center;
	gap: var(--s-4);
}

/* A grant, not a state: accent rather than the green a running server
   wears. */
.tm-dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	border: 1.5px solid var(--border-strong);
	flex: none;
}

.tm-dot.on {
	border-color: var(--accent);
	background: var(--accent);
}

/* The panel: the tab dimmed behind, the panel at the right edge. */
.tm-sheet {
	position: fixed;
	inset: 0;
	z-index: 60;
	display: flex;
	justify-content: flex-end;
}

/* The canvas spaces every child after the first, and a fixed sheet that
   took the space sat 20px below the top of the window. */
main.canvas > .tm-sheet {
	margin-top: 0;
}

.tm-sheet-back {
	position: absolute;
	inset: 0;
	background: color-mix(in srgb, var(--ground) 55%, transparent);
	backdrop-filter: blur(6px);
}

.tm-panel {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--s-8);
	width: min(520px, 100%);
	height: 100%;
	overflow-y: auto;
	padding: var(--s-9);
	background: var(--raised);
	color: var(--body);
	border-left: 1px solid var(--line);
	box-shadow: var(--sh-3);
	animation: tm-in var(--t) var(--e-out);
}

@keyframes tm-in {
	from {
		transform: translateX(24px);
		opacity: 0;
	}
}

@media (prefers-reduced-motion: reduce) {
	.tm-panel {
		animation: none;
	}
}

.tm-panel-h {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-6);
}

.tm-panel-h h2 {
	margin: 0;
	font-size: var(--t-lg);
	font-weight: var(--w-semi);
	color: var(--ink);
	letter-spacing: -0.015em;
	overflow-wrap: anywhere;
}

.tm-panel-who {
	display: flex;
	align-items: center;
	gap: var(--s-6);
}

.tm-panel-who .name {
	font-weight: var(--w-medium);
	color: var(--ink);
}

.tm-form {
	display: flex;
	flex-direction: column;
	gap: var(--s-8);
}

.tm-email {
	display: flex;
	flex-direction: column;
	gap: var(--s-3);
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	color: var(--ink);
}

.tm-cards {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--s-5);
	margin: 0;
	padding: 0;
	border: 0;
	min-width: 0;
}

.tm-cards legend {
	margin-bottom: var(--s-4);
	padding: 0;
	font-size: var(--t-sm);
	font-weight: var(--w-medium);
	color: var(--ink);
}

.tm-card {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	padding: var(--s-6) var(--s-6) var(--s-6) calc(var(--s-6) + 26px);
	border: 1px solid var(--line);
	border-radius: var(--r);
	background: var(--surface);
	cursor: pointer;
	transition: border-color var(--t-fast) var(--e-io), box-shadow var(--t-fast) var(--e-io);
}

.tm-card:hover {
	border-color: var(--border-strong);
}

.tm-card input {
	position: absolute;
	top: calc(var(--s-6) + 2px);
	left: var(--s-6);
	margin: 0;
	accent-color: var(--accent);
}

.tm-card:has(input:checked) {
	border-color: var(--accent);
	box-shadow: 0 0 0 3px var(--accent-soft);
}

.tm-card:has(input:focus-visible) {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.tm-card.off {
	cursor: not-allowed;
	background: var(--sunk);
}

.tm-card.off .tm-card-t,
.tm-card.off .tm-card-l {
	opacity: 0.6;
}

.tm-card-t {
	font-weight: var(--w-medium);
	color: var(--ink);
}

.tm-card-l {
	font-size: var(--t-sm);
	color: var(--body);
	line-height: 1.4;
}

.tm-card-why {
	font-size: var(--t-xs);
	color: var(--attn);
	line-height: 1.4;
}

.tm-switchrow {
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	padding: var(--s-6);
	border: 1px solid var(--line);
	border-radius: var(--r);
}

.tm-switch {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	cursor: pointer;
}

.tm-switch input {
	appearance: none;
	position: relative;
	flex: none;
	width: 36px;
	height: 20px;
	margin: 0;
	border-radius: var(--radius-pill);
	background: var(--line);
	cursor: pointer;
	transition: background var(--t) var(--e-io);
}

.tm-switch input::after {
	content: "";
	position: absolute;
	top: 2px;
	left: 2px;
	width: 16px;
	height: 16px;
	border-radius: 50%;
	background: var(--surface);
	box-shadow: var(--sh-1);
	transition: transform var(--t) var(--e-spring);
}

.tm-switch input:checked {
	background: linear-gradient(135deg, var(--accent), var(--accent-2));
}

.tm-switch input:checked::after {
	transform: translateX(16px);
}

.tm-switch input:disabled {
	cursor: not-allowed;
	opacity: 0.6;
}

.tm-switch input:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.tm-switch-t {
	font-weight: var(--w-medium);
	color: var(--ink);
}

.tm-switch-l {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--body);
}

.tm-live {
	padding: var(--s-6) var(--s-7);
	border-radius: var(--r);
	background: var(--ground-2);
}

.tm-live-set[hidden] {
	display: none;
}

.tm-live-h {
	margin: 0 0 var(--s-4);
	font-size: var(--t-sm);
	font-weight: var(--w-semi);
	color: var(--ink);
}

.tm-can,
.tm-cant {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: var(--s-2);
	font-size: var(--t-sm);
	line-height: 1.4;
}

.tm-can li,
.tm-cant li {
	position: relative;
	padding-left: 20px;
}

.tm-can li::before,
.tm-cant li::before {
	position: absolute;
	left: 0;
	font-weight: var(--w-semi);
}

.tm-can li {
	color: var(--ink);
}

.tm-can li::before {
	content: "✓";
	color: var(--accent-ink);
}

.tm-cant {
	margin-top: var(--s-4);
}

.tm-cant li {
	color: var(--body);
}

.tm-cant li::before {
	content: "✕";
	color: var(--faint);
}

.tm-live-none {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--attn);
}

.tm-acts {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-4);
}

.tm-hint {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--faint);
}

.tm-hint a {
	color: var(--accent-ink);
}

.tm-fixed {
	display: flex;
	flex-direction: column;
	gap: var(--s-6);
}

.tm-fixed p {
	margin: 0;
	color: var(--body);
}

.tm-grant {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: var(--s-3) var(--s-8);
	margin: 0;
	font-size: var(--t-sm);
}

.tm-grant dt {
	color: var(--faint);
}

.tm-grant dd {
	margin: 0;
	color: var(--ink);
}

/* The doors that are not a change of role, set apart at the foot. */
.tm-danger {
	display: flex;
	flex-direction: column;
	gap: var(--s-6);
	margin-top: auto;
	padding-top: var(--s-8);
	border-top: 1px solid var(--hair);
}

.tm-danger form {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--s-4);
}

.tm-danger p {
	margin: 0;
	font-size: var(--t-sm);
	color: var(--body);
}

/* The compare table: a grid, so it scrolls in its own box on a phone
   rather than turning into cards. */
.lt.tm-cmp {
	min-width: 720px;
}

.lt.tm-cmp tbody th {
	white-space: normal;
	font-weight: var(--w-normal);
	padding-left: var(--s-9);
}

.lt.tm-cmp td,
.lt.tm-cmp thead th:not(:first-child) {
	width: 12%;
}

.lt.tm-cmp .lt-group th {
	padding: var(--s-4) var(--s-9);
	background: var(--ground-2);
	font-size: var(--t-xs);
	font-weight: var(--w-semi);
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--faint);
}

.tm-cell {
	font-size: var(--t-sm);
	white-space: nowrap;
}

.tm-cell.yes {
	color: var(--ink);
	font-weight: var(--w-medium);
}

.tm-cell.billing {
	display: inline-block;
	padding: 1px var(--s-4);
	border-radius: var(--radius-pill);
	background: var(--accent-soft);
	color: var(--accent-ink);
}

.tm-cell.no {
	color: var(--faint);
}

@media (max-width: 600px) {
	.tm-cards {
		grid-template-columns: minmax(0, 1fr);
	}

	.tm-panel {
		width: 100%;
		border-left: 0;
		padding: var(--s-8) var(--gutter, 16px);
	}

	/* A phone scrolls the sheet anyway; the foot follows the form rather
	   than a screen of nothing. */
	.tm-danger {
		margin-top: 0;
	}
}

/* --- The till ---------------------------------------------------------------
   The checkout (David, 2026-11-06, from the redesign preview he chose): the
   order as one card, the pay card the one raised and ringed thing on the
   page, the brand's two fills as tints where a second look pays, and under
   the order what a buyer is unsure of. On a phone the pay card follows the
   order, and the rest follows the card. */
.co-head {
	display: grid;
	gap: var(--s-5);
}

.co-head h1 {
	margin: 0;
	font-size: clamp(28px, 4vw, 38px);
	letter-spacing: -0.025em;
}

.co-steps {
	display: flex;
	align-items: center;
	gap: var(--s-5);
	margin: 0;
	font-family: var(--mono);
	font-size: 11px;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--faint);
}

.co-head .co-steps {
	font-size: 11px;
}

.co-steps b {
	display: inline-flex;
	align-items: center;
	gap: var(--s-4);
	color: var(--ink);
	font-weight: var(--w-medium);
}

.co-steps b::before,
.co-steps span::before {
	content: "";
	display: inline-block;
	width: 8px;
	height: 8px;
	border-radius: 50%;
}

.co-steps b::before {
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	box-shadow: 0 0 0 4px var(--tint);
}

.co-steps span::before {
	margin-right: var(--s-4);
	border: 1px solid var(--line);
	vertical-align: -1px;
}

.co-steps i {
	width: 28px;
	height: 1px;
	background: var(--line);
}

.co-lede {
	margin: 0;
	color: var(--body);
}

.co-grid {
	display: grid;
	gap: var(--s-8);
	align-items: start;
}

.co-items,
.co-card {
	background: var(--surface);
	border: 1px solid var(--line);
	border-radius: 14px;
}

.co-item {
	display: grid;
	grid-template-columns: 48px minmax(0, 1fr) auto;
	gap: 4px var(--s-8);
	padding: 26px 28px;
}

.co-item + .co-item {
	border-top: 1px solid var(--hair);
}

.co-mark {
	grid-row: span 3;
	display: grid;
	place-items: center;
	width: 48px;
	height: 48px;
	border-radius: 11px;
	background: var(--tint);
	border: 1px solid var(--tint-line);
	color: var(--accent-ink);
}

.co-mark svg {
	width: 22px;
	height: 22px;
}

.co-item h2 {
	margin: 0;
	font-size: 17px;
	font-weight: var(--w-semi);
	letter-spacing: -0.01em;
	color: var(--ink);
}

.co-price {
	margin: 0;
	text-align: right;
	color: var(--ink);
	font-size: 16px;
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
}

.co-price .line-price {
	font-size: 17px;
	font-weight: var(--w-medium);
}

.co-price small {
	display: block;
	font-size: 12px;
	font-weight: var(--w-normal);
	color: var(--faint);
}

.co-wide,
.co-facts,
.co-save,
.co-chips,
.co-acts {
	grid-column: 2 / -1;
}

.co-facts {
	margin: 0;
	font-family: var(--mono);
	font-size: 12.5px;
	color: var(--faint);
}

.co-save {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--s-5) var(--s-8);
	margin: var(--s-5) 0 0;
	padding: 12px 14px;
	border-radius: 10px;
	background: var(--tint);
	border: 1px solid var(--tint-line);
	font-size: 14px;
	color: var(--body);
}

.co-save em {
	display: inline-block;
	white-space: nowrap;
	margin-left: var(--s-3);
	padding: 3px 7px;
	border-radius: 5px;
	background: linear-gradient(135deg, var(--accent-fill), var(--accent-fill-2));
	color: #fff;
	font-family: var(--mono);
	font-size: 11px;
	font-style: normal;
	font-weight: var(--w-semi);
	letter-spacing: 0.04em;
}

.co-save .btn {
	border-color: var(--tint-line);
}

.co-chips {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	margin: var(--s-5) 0 0;
	padding: 0;
	list-style: none;
}

.co-chips li {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	max-width: 100%;
	min-height: 28px;
	padding: 0 10px;
	border-radius: 7px;
	background: var(--sunk);
	border: 1px solid var(--hair);
	font-size: 12.5px;
}

.co-chips span {
	color: var(--faint);
}

.co-chips b {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-family: var(--mono);
	font-size: 12px;
	overflow-wrap: anywhere;
}

.co-chips .oslogo {
	width: 15px;
	height: 15px;
}

.co-acts {
	display: flex;
	gap: var(--s-8);
	margin-top: var(--s-5);
	font-size: 13.5px;
}

.co-acts form {
	margin: 0;
}

.co-acts a,
.co-acts button,
.co-foot button,
.co-uncoupon button {
	padding: 0;
	border: 0;
	background: none;
	color: var(--faint);
	font: inherit;
	text-decoration: none;
	cursor: pointer;
}

.co-acts a:hover,
.co-acts button:hover,
.co-foot a:hover,
.co-foot button:hover,
.co-uncoupon button:hover {
	color: var(--ink);
}

.co-foot {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	padding: 14px 24px;
	border-top: 1px solid var(--hair);
	font-size: 13.5px;
}

.co-foot a {
	color: var(--accent-ink);
	text-decoration: none;
}

.co-foot form,
.co-uncoupon {
	margin: 0;
}

/* The pay card: raised, shadowed, and ringed in the brand's two fills. */
.co-sum {
	position: relative;
	padding: 28px;
	border-radius: 16px;
	background: var(--raised);
	box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.55), 0 0 0 1px var(--line);
}

:root:not([data-theme="dark"]) .co-sum {
	box-shadow: 0 24px 50px -28px rgba(30, 20, 80, 0.25), 0 0 0 1px var(--line);
}

@media (prefers-color-scheme: dark) {
	:root:not([data-theme="light"]) .co-sum {
		box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.55), 0 0 0 1px var(--line);
	}
}

.co-sum::before {
	content: "";
	position: absolute;
	inset: 0;
	padding: 1px;
	border-radius: 16px;
	background: linear-gradient(160deg, rgba(56, 176, 255, 0.55), rgba(91, 71, 230, 0.45) 40%, transparent 70%);
	-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
	pointer-events: none;
}

.co-row {
	display: flex;
	justify-content: space-between;
	gap: var(--s-6);
	padding: 5px 0;
	font-size: 14px;
	color: var(--body);
}

.co-row b {
	color: var(--ink);
	font-weight: var(--w-medium);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.co-uncoupon {
	font-size: var(--t-xs);
}

.co-havecoupon {
	margin: var(--s-4) 0 0;
	font-size: 13.5px;
}

.co-havecoupon summary {
	color: var(--accent-ink);
	cursor: pointer;
	list-style: none;
}

.co-havecoupon summary::-webkit-details-marker {
	display: none;
}

.co-havecoupon .coupon-row {
	margin-top: var(--s-5);
}

.co-total {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	margin: var(--s-8) 0 0;
	padding-top: var(--s-8);
	border-top: 1px solid var(--hair);
	color: var(--ink);
	font-weight: var(--w-medium);
}

.co-total b {
	font-size: 34px;
	font-weight: var(--w-semi);
	letter-spacing: -0.03em;
	font-variant-numeric: tabular-nums;
}

.co-total small {
	margin-left: var(--s-3);
	font-size: 12px;
	font-weight: var(--w-normal);
	color: var(--faint);
	letter-spacing: 0;
}

.co-h {
	display: block;
	margin-bottom: var(--s-6);
	font-family: var(--mono);
	font-size: 10.5px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--faint);
}

.co-renew {
	margin: var(--s-7) 0 0;
	padding: 12px 14px;
	border-radius: 10px;
	background: var(--sunk);
}

.co-renew .co-h {
	margin-bottom: var(--s-2);
}

.co-renew .co-row {
	padding: 3px 0;
	font-size: 13px;
}

.co-same {
	margin: var(--s-2) 0 0;
	font-size: 12px;
	color: var(--faint);
}

/* Room around the promise: it is read, then the buttons are pressed. */
.co-sum .consent {
	margin: var(--s-9) 0 var(--s-8);
}

.co-sum .pay-doors {
	display: grid;
	gap: 10px;
}

.co-sum .pay-doors .btn {
	height: 46px;
	border-radius: 9px;
	font-size: 15px;
}

.btn-ic {
	width: 17px;
	height: 17px;
	flex: none;
}

.co-calm {
	margin: var(--s-6) 0 0;
	font-size: 12.5px;
	color: var(--faint);
	text-align: center;
}

.co-calm b {
	color: var(--body);
	font-weight: var(--w-medium);
}

.co-more {
	display: grid;
	gap: var(--s-7);
	min-width: 0;
}

.co-card {
	padding: 26px 28px;
}

.co-next {
	display: grid;
	gap: var(--s-8);
	margin: 0;
	padding: 0;
	list-style: none;
	counter-reset: step;
	font-size: 13.5px;
}

.co-next li {
	display: grid;
	grid-template-columns: 32px minmax(0, 1fr);
	gap: 2px var(--s-5);
	counter-increment: step;
}

.co-next li::before {
	content: counter(step);
	grid-row: span 2;
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	border-radius: 50%;
	background: var(--tint);
	border: 1px solid var(--tint-line);
	font-family: var(--mono);
	font-size: 12px;
	color: var(--accent-ink);
}

.co-next b {
	color: var(--ink);
	font-weight: var(--w-semi);
	font-size: 14px;
}

.co-next span {
	color: var(--faint);
}

.co-keys {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s-5) var(--s-9);
	font-size: 13.5px;
}

.co-ic {
	display: grid;
	place-items: center;
	flex: none;
	width: 36px;
	height: 36px;
	border-radius: 9px;
	background: var(--sunk);
	border: 1px solid var(--hair);
	color: var(--ink);
}

.co-ic svg {
	width: 18px;
	height: 18px;
}

.co-keys p {
	flex: 1 1 260px;
	margin: 0;
	color: var(--body);
}

.co-keys b {
	color: var(--ink);
	font-weight: var(--w-semi);
}

.co-keys a {
	color: var(--accent-ink);
	font-weight: var(--w-medium);
	text-decoration: none;
}

.co-help {
	display: flex;
	flex-wrap: wrap;
	gap: var(--s-3) var(--s-7);
	margin: 0;
	padding: var(--s-2) var(--s-2) 0;
	font-size: 13.5px;
	color: var(--faint);
}

.co-help a {
	color: var(--accent-ink);
	text-decoration: none;
}

/* A phone gives the server's mark's column back to its words. */
@media (max-width: 560px) {
	.co-item {
		grid-template-columns: minmax(0, 1fr) auto;
		padding: 18px;
	}

	.co-mark {
		display: none;
	}

	.co-wide,
	.co-facts,
	.co-save,
	.co-chips,
	.co-acts {
		grid-column: 1 / -1;
	}
}

@media (min-width: 720px) {
	.co-next {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

@media (min-width: 960px) {
	.co-grid {
		grid-template-columns: minmax(0, 1fr) 400px;
		grid-template-areas: "items sum" "more sum";
		gap: var(--s-8) 32px;
	}

	.co-items {
		grid-area: items;
	}

	.co-grid > .co-sum {
		grid-area: sum;
		position: sticky;
		top: calc(64px + var(--s-8));
	}

	.co-more {
		grid-area: more;
	}
}
