/* ==================================================================
 * THE PORTAL, MODERNISED
 *
 * A design layer over app.css. The portal read as a document with a list
 * of links beside it: correct, legible, and nothing like the software
 * people compare it to. Every idea here comes from that comparison and is
 * rebuilt in BrightSide's own palette rather than the indigo those
 * references all happen to share.
 *
 *   a FILLED sidebar             navy, ours, not purple
 *   the current page as a PILL   gold, the brand's own accent
 *   cards on a TINTED ground     so a white card is a card and not the page
 *   KPI TILES with a delta       the number, then whether it is moving
 *   STATUS PILLS                 state readable across a desk
 *   CHIP filters                 a filter you can see and remove
 *   ROWS AS CARDS on a phone     six columns is not a phone screen
 *
 * WHAT IT DELIBERATELY DOES NOT DO. No animation beyond a hover, nothing
 * that moves on load, no gradient behind text, no illustration taking up
 * a third of the sidebar. This is a screen somebody uses for seven hours,
 * not a landing page — and the references are portfolio shots, which is a
 * different job.
 *
 * A SEPARATE FILE, loaded only by the portal and field layouts, so the
 * public site is untouched and every override is in one place.
 * ================================================================== */

:root {
	/* The sidebar is its own small world: dark in both themes. */
	--side: #0e2338;
	--side-ink: #e8eef5;
	--side-soft: #93a9c0;
	--side-line: #1d3a52;

	--r-card: 14px;
	--r-pill: 999px;

	/*
	 * NO COLOURS BUT THE SIDEBAR'S. --lift and --lift-hi lived here as a second
	 * copy of app.css's, a navy shadow with no dark value; app.css now holds
	 * the one copy and re-points it in dark, where a navy shadow on a navy page
	 * is no shadow at all. Everything below paints with app.css's tokens, so it
	 * cannot be right in one theme and wrong in the other:
	 * portal-ui-follows-the-theme.test.ts fails on a literal colour anywhere
	 * outside this block.
	 */

	/*
	 * HOW WIDE THE PORTAL IS. One number, in one place. 22/09/2026.
	 *
	 * "one thing i noticed aswell is you never use the entire screen you
	 * condence everything into the center." Sent in from the plugin, and true
	 * here too: on a 1920px monitor the portal ran to 1600 while the bar above
	 * it -- the mark, who is signed in, what the business is owed -- sat in the
	 * marketing site's 72rem column, 224px inboard on both sides. The chrome and
	 * the screen it framed disagreed about where the page was.
	 *
	 * A CEILING, NOT A WIDTH. Below it the portal is fluid, so every screen
	 * between a laptop and a monitor uses what it is given. The ceiling exists
	 * because a table-and-card control room stops being scannable somewhere past
	 * 1700px -- the eye has to travel too far from a customer's name to their
	 * balance. 105rem is 1680px, the same number the plugin settled on, so the
	 * two systems look the same the day the business moves across.
	 *
	 * USING THE WIDTH IS NOT CRAMMING MORE IN. Everything with a natural size
	 * keeps it: prose caps at `--measure` (app.css, 62ch), a form at the same, a
	 * search row at 34rem, and a card shrinks to what it holds -- see "A CARD IS
	 * AS WIDE AS WHAT IS IN IT" further down.
	 */
	--portal-max: 105rem;

	/*
	 * HOW NARROW A BOX MAY GET WHEN IT SHRINKS TO ITS CONTENT. 68rem, 1088px:
	 * the plugin's own number, so a card is the same size on both systems the
	 * day the business moves across.
	 *
	 * AND IT IS NARROWER THAN A STRETCHED BOX IS TODAY, deliberately. A 1292px
	 * panel holding one 588px notice with 686px of blank white down its right is
	 * not width being used, it is width being wasted inside a border -- and
	 * empty space all on one side INSIDE a box reads as a page that did not
	 * finish drawing, which is worse than the margin it replaced. Nothing whose
	 * content wants the room loses any of it: fit-content is bounded by what is
	 * available, so Customers, the Ledger and the audit log still take every
	 * pixel, and the portal around them is wider than it was.
	 */
	--portal-card-floor: 68rem;
}

/*
 * The ground behind the cards.
 *
 * Never pure white: a white card on a white page is not a card, it is a
 * rectangle of text, which is most of why the old portal read flat.
 */
body:has(.portalnav) { background: var(--wash); }

/* ---- the sidebar ------------------------------------------------ */

/*
 * THE PORTAL USES THE SCREEN.
 *
 * `.wrap` caps at 72rem, which is right for a marketing page — a line of
 * prose past about 75 characters is harder to read, and that is what the
 * measure is protecting. A dashboard is not prose. On a 1400px monitor the
 * office was looking at a column of tables down the middle with a third of
 * the display empty beside it, and every table scrolling sideways inside
 * it.
 *
 * So the portal gets a wider ceiling and keeps a limit rather than going
 * edge to edge: full-bleed on an ultrawide is a different kind of unusable.
 *
 * `screen and`, NOT A BARE MIN-WIDTH. 22/09/2026, carried from the plugin. A
 * media query with no media type is evaluated against the PAGE BOX when the
 * page is printed, and A4 landscape is 297mm -- 1122px -- so this block was
 * live on paper as well as on screen. The print rules hide `.portalnav`, but
 * the GRID stayed: two columns, the first 15.5rem wide and now empty, with
 * every printed commission statement and receipt indented 248px and that much
 * narrower for it. One word keeps every rule that is about a screen on the
 * screen, and leaves the `@media print` block in app.css as the only thing
 * that shapes a page.
 */
@media screen and (min-width: 60rem) {
	.band > .wrap:has(.portalnav) {
		grid-template-columns: 15.5rem minmax(0, 1fr);
		max-width: var(--portal-max);
		/* Between the columns only: see the row gap note in app.css. */
		column-gap: 1.75rem;
	}
}

/*
 * THE BAR ABOVE THE PORTAL USES THE SAME SCREEN.
 *
 * `.wrap` caps at 72rem, which is right for the marketing page the bar was
 * designed on and wrong above a 1680px control room: the mark, the amount the
 * business is owed and who is signed in all sat 224px inboard of the first
 * column of the table underneath them, so the page read as two pages. The same
 * for the card-details rule, which is a band across the top of every staff
 * screen and has to line up with the screen it is addressed to, and for the
 * footer, whose links otherwise start 264px inboard of the table above them.
 *
 * Portal pages only (`body:has(.portalnav)`), and it changes nothing below
 * 72rem, because below that the cap was never what set the width. The body
 * test is in :where() here as everywhere in this file -- see DENSITY below.
 */
:where(body:has(.portalnav)) .appbar__in,
:where(body:has(.portalnav)) .saferule > .wrap,
:where(body:has(.portalnav)) .appfoot > .wrap {
	max-width: var(--portal-max);
}

/*
 * ---- WIDE SCREENS: WHAT MUST AND MUST NOT TAKE THE WIDTH ----
 *
 * 1100px, the plugin's own breakpoint, and for its reason: below about 1140
 * the ceiling was never what was holding the portal in, so starting lower
 * would move tablets and phones for no gain. At 1024 and at 768 not one of
 * these rules is in play, and the phone layout is untouched.
 */
@media screen and (min-width: 68.75rem) {
	/*
	 * A GUTTER THAT GROWS WITH THE SCREEN. Between 1100px and the ceiling the
	 * portal IS the screen, and 16px of padding on a 1400px monitor puts the
	 * first column of a table against the edge of the glass. Above the ceiling
	 * this is inside the box and the auto margins do the rest.
	 */
	.band > .wrap:has(.portalnav) {
		padding-inline: clamp(1rem, 2vw, 2.5rem);
	}

	/*
	 * A CARD IS AS WIDE AS WHAT IS IN IT.
	 *
	 * Widening the portal and leaving every box on it stretched only moves the
	 * empty space from outside the portal to inside the card. The dashboard at
	 * 1920: three 1292px panels each holding one short notice and 686px of
	 * blank white down the right. Empty space split evenly OUTSIDE a box reads
	 * as a margin; the same space inside the box, all of it on one side, reads
	 * as a page that did not finish drawing -- which is worse than what was
	 * reported in the first place.
	 *
	 * So the box shrinks to what it holds, with a floor at --portal-card-floor
	 * so it can never become a narrow strip on a wide screen. `min(100%, …)`
	 * because at 1100px the portal is itself narrower than that floor, and a
	 * card must fit the portal rather than overflow it.
	 *
	 * DIRECT CHILDREN OF THE PORTAL COLUMN ONLY. A `.panel` inside `.cards` is a
	 * grid item, and shrink-wrapping a grid item is how a row of KPI tiles
	 * becomes a row of different-sized tiles. The chain is the same one the
	 * sidebar grid is keyed off, so the two cannot drift apart.
	 *
	 * `.tablewrap` is in the list for the same reason even though it paints
	 * almost nothing: it is the block that holds a table, `.table` is
	 * `width: 100%`, and Customers' four columns were being spread across
	 * 1292px. A table that needs the room still fills the wrapper and still
	 * scrolls inside it when even that is not enough; one that does not stops
	 * where its columns stop.
	 */
	.band > .wrap:has(.portalnav) > .panel,
	.band > .wrap:has(.portalnav) > .briefing,
	.band > .wrap:has(.portalnav) > .tablewrap {
		width: fit-content;
		min-width: min(100%, var(--portal-card-floor));
		max-width: 100%;
	}

	/*
	 * AND THE ROWS INSIDE THEM FIX THEMSELVES, which is the point of doing this
	 * at the box rather than one rule per screen. This morning's rows are a flex
	 * row holding a household and the button that acts on it: in a 1327px box
	 * the sentence ended at x 1504 and "Put them on a round" began at x 1520
	 * with the next household's row directly underneath, and picking the round
	 * on the wrong row is a scheduling mistake on a real customer. Capping the
	 * box brings the control back towards the thing it belongs to.
	 *
	 * WHAT THIS DOES NOT DO is pull a heading's buttons in: `.headrow` is a
	 * heading with its actions on the right edge of the column, lined up with
	 * the right edge of the tiles below it, and that alignment is worth more
	 * than the gap it leaves.
	 */

	/*
	 * THE MAP KEEPS THE WHOLE WIDTH, because it is a picture of where the round
	 * goes rather than a box with writing in it -- and it is safe to let it,
	 * because `.map`'s height is already `clamp(320px, 55vh, 620px)`: it grows
	 * downwards with the window, so a wider map does not become a letterbox the
	 * way a fixed height would. `.mapwrap` is not a `.panel`, so the rule above
	 * never reaches it; this is here to say that is deliberate.
	 */
}

/* ---- DENSITY: an admin tool, not a landing page -------------------
 *
 * THE REAL PROBLEM, and it was mine. The portal inherits the marketing
 * site's type scale, and that scale is built for a page somebody reads
 * once from a sofa:
 *
 *     h1        clamp(2.1rem … 4rem)      64px on a monitor
 *     h2        clamp(1.6rem … 2.5rem)    40px
 *     body      up to 1.125rem            18px
 *     --section clamp(3rem … 6rem)        96px between blocks
 *
 * On a landing page that is right. On a screen showing four figures and a
 * table it is enormous: the heading is bigger than the number it labels,
 * and two thirds of the display is the space between things. Making the
 * cards prettier without fixing this just made a large screen a large
 * pretty screen.
 *
 * So the portal gets its own scale. Everything below is smaller than the
 * public site and none of it is small: 15px body, a 24px h1, and section
 * gaps in the 1–2rem range rather than 3–6.
 *
 * DEFAULTS, NOT ORDERS: `:where()` round the body test. As plain
 * `body:has(.portalnav) h2` the rule scored (0,1,2) -- a :has() counts as a
 * class -- so it beat every heading that had one class of its own. The 86
 * headline money figures (h2.figure) came out at 18px instead of up to 35,
 * the 58 small uppercase group labels (h2.stack-head) at 18px as well, and
 * the sidebar's section names lost their letter-spacing. Inside :where() the
 * test weighs nothing: these still come after app.css's bare h1-h3, so they
 * still beat those, and any component with a class now beats them.
 * the-portal-layer-lets-components-win.test.ts fails on the old form.
 * ------------------------------------------------------------------ */

body:has(.portalnav) {
	font-size: 0.94rem;
	--gap: 1rem;
	--section: 1.25rem;
}

:where(body:has(.portalnav)) section.band { padding-block: 1.25rem; }

:where(body:has(.portalnav)) h1 {
	font-size: clamp(1.35rem, 1.15rem + 0.8vw, 1.6rem);
	margin-bottom: 0.85rem;
	letter-spacing: -0.015em;
}

:where(body:has(.portalnav)) h2 {
	font-size: clamp(1.02rem, 0.95rem + 0.3vw, 1.15rem);
	margin-bottom: 0.5rem;
	letter-spacing: -0.005em;
}

:where(body:has(.portalnav)) h3 {
	font-size: 0.94rem;
	margin-bottom: 0.35rem;
}

/*
 * The cards were sized for three across a marketing page. `.two` and
 * `.three` are named here on purpose: they are the marketing page's own
 * column counts, and the portal's grid replaces them as it always has --
 * lowering the body test must not quietly turn a row of four figures into
 * two wide boxes of empty space.
 */
:where(body:has(.portalnav)) :is(.cards, .cards.two, .cards.three) {
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
	gap: 0.85rem;
}
:where(body:has(.portalnav)) .cards.four {
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 11.5rem), 1fr));
}

:where(body:has(.portalnav)) .panel { padding: 1rem 1.1rem; margin-bottom: 0.85rem; }
:where(body:has(.portalnav)) .panel > :last-child { margin-bottom: 0; }

/* A paragraph of explanation under a figure is a caption, not body copy. */
:where(body:has(.portalnav)) .whisper,
:where(body:has(.portalnav)) .hint { font-size: 0.8rem; }

/*
 * Tables carry a lot of rows; the marketing padding doubles their height.
 *
 * Not a table that has become cards on a phone (.table--stack, app.css):
 * there each cell is a line of a card and keeps the card's padding. This is
 * the stacked rule's weight and later in the page, so it would win the tie --
 * hence the table is left out below 40rem rather than overruled.
 */
:where(body:has(.portalnav)) .table:where(:not(.table--stack)) th,
:where(body:has(.portalnav)) .table:where(:not(.table--stack)) td { padding: 0.55rem 0.7rem; }

@media (min-width: 40.0625rem) {
	:where(body:has(.portalnav)) .table--stack th,
	:where(body:has(.portalnav)) .table--stack td { padding: 0.55rem 0.7rem; }
}

.portalnav__panel {
	background: var(--side);
	border-radius: var(--r-card);
	padding: 0.85rem 0.7rem;
	box-shadow: var(--lift);
}

.portalnav__heading {
	color: var(--side-soft);
	font-size: 0.68rem;
	letter-spacing: 0.12em;
	padding: 0 0.6rem;
	margin-bottom: 0.45rem;
}

.portalnav__group { margin-bottom: 0.9rem; }

.portalnav .portalnav__link {
	display: flex;
	align-items: center;
	gap: 0.65rem;
	color: var(--side-soft);
	border-left: 0;
	border-radius: 10px;
	padding: 0.5rem 0.65rem;
	font-size: 0.86rem;
	font-weight: 500;
	line-height: 1.3;
	transition: background 0.14s ease, color 0.14s ease;
}

/* A lift of the sidebar's own ink over the navy: the same in both themes, like the sidebar. */
.portalnav .portalnav__link:hover,
.portalnav .portalnav__link:focus-visible {
	background: color-mix(in srgb, var(--side-ink) 7%, transparent);
	color: var(--side-ink);
}

/*
 * THE CURRENT PAGE, as a filled pill.
 *
 * A left-edge stripe reads as a bullet point. A filled shape reads as
 * "you are here" from the other side of a desk, which is the one thing
 * navigation has to do. Gold on navy is the brand's own pairing and
 * carries enough contrast to be the only marker needed — so aria-current
 * is not doing the work alone, and neither is colour.
 */
.portalnav .portalnav__link.is-here {
	background: var(--gold);
	color: var(--on-gold);
	font-weight: 700;
	box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--gold) 70%, transparent);
}

.portalnav .portalnav__link.is-here:hover,
.portalnav .portalnav__link.is-here:focus-visible {
	background: var(--gold);
	color: var(--on-gold);
}

/* The icon well, so labels line up whether or not a glyph is present. */
.portalnav .portalnav__link .ico {
	flex: 0 0 1.05rem;
	width: 1.05rem;
	height: 1.05rem;
	opacity: 0.85;
}
.portalnav .portalnav__link.is-here .ico { opacity: 1; }

.portalnav__group--foot {
	border-top: 1px solid var(--side-line);
	margin-top: 1rem;
	padding-top: 0.9rem;
}
/*
 * The name is a link to your own page, and a block like the links under it:
 * inline, the phone's 48px tap height did nothing to it (16px tall beside
 * 48px rows), and its 0.6rem indent made the foot read as two columns.
 */
.portalnav__who { display: block; color: var(--side-soft); padding: 0.35rem 0; }

/*
 * Pointed at, the foot's links say so the way every other sidebar link does:
 * the sidebar's own ink. They showed no change at all.
 */
.portalnav__who:hover,
.portalnav__who:focus-visible,
.portalnav__out:hover,
.portalnav__out:focus-visible {
	color: var(--side-ink);
	text-decoration: underline;
}

/* ---- cards ------------------------------------------------------ */

.panel {
	border-radius: var(--r-card);
	border: 0;
	box-shadow: var(--lift);
	background: var(--paper);
}

/* The one that wants attention keeps its edge, in gold rather than a border. */
.panel--attention {
	border: 0;
	box-shadow: var(--lift), inset 3px 0 0 0 var(--gold);
}

/* ---- KPI tiles --------------------------------------------------- */

.stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
	gap: 0.7rem;
	margin-bottom: 1.1rem;
}

.stat {
	background: var(--paper);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	/*
	 * THE GOLD EDGE, from the live portal. It groups a row of figures as
	 * one set at a glance and costs no space, which a heading above them
	 * does not.
	 */
	border-left: 3px solid var(--gold);
	padding: 0.7rem 0.85rem 0.8rem;
	display: flex;
	flex-direction: column;
	gap: 0.15rem;
	min-width: 0;
}

.stat__top { display: flex; align-items: center; gap: 0.55rem; }

.stat__ico {
	width: 2rem;
	height: 2rem;
	border-radius: 9px;
	display: grid;
	place-items: center;
	background: var(--gold-wash);
	/* Ink on the wash, both of which flip: a fixed navy was 1.03:1 on the dark wash. */
	color: var(--ink);
	flex: 0 0 auto;
}

.stat__label {
	font-size: 0.76rem;
	font-weight: 600;
	color: var(--ink-soft);
}

.stat__big {
	font-size: clamp(1.3rem, 1.1rem + 0.7vw, 1.6rem);
	font-weight: 800;
	letter-spacing: -0.025em;
	line-height: 1.05;
	/* Digits that line up, because these are read as a column. */
	font-variant-numeric: tabular-nums;
}

.stat__note { font-size: 0.8rem; color: var(--ink-soft); }

/*
 * Movement, and which way.
 *
 * COLOUR IS NEVER THE ONLY SIGNAL. The arrow carries it, so this still
 * reads for somebody who cannot tell the green from the red — which is
 * the one thing every dashboard in those references gets wrong.
 */
.delta { font-size: 0.8rem; font-weight: 700; display: inline-flex; gap: 0.2rem; align-items: center; }
.delta--up { color: var(--success); }
.delta--down { color: var(--danger); }
.delta--flat { color: var(--ink-soft); }

/* ---- status badges ----------------------------------------------- */

/*
 * NOT `.pill`. That name is taken: app.css uses it for the filter chips on
 * ten screens, and redefining it here would have quietly shrunk every one
 * of them. This is the little coloured word that says what STATE a row is
 * in — paid, failed, waiting — which is a different job from a control
 * somebody presses.
 */
.state {
	display: inline-flex;
	align-items: center;
	gap: 0.3rem;
	border-radius: var(--r-pill);
	padding: 0.2rem 0.62rem;
	font-size: 0.75rem;
	font-weight: 700;
	white-space: nowrap;
	line-height: 1.5;
}
.state--ok { background: var(--success-wash); color: var(--success); }
/* --warn, not --warn-edge: the edge is a border tone, and as words it was 2:1. */
.state--warn { background: var(--warn-wash); color: var(--warn); }
.state--bad { background: var(--danger-wash); color: var(--danger); }
.state--info { background: var(--gold-wash); color: var(--ink); }
.state--quiet { background: var(--wash); color: var(--ink-soft); }

/*
 * A WHOLE SENTENCE IN A STATE'S COLOURS: "£1.00 more was received for the
 * clean on ... than its price". A chip keeps to one line, and this one pushed
 * the account page to 480px on a phone and the tab bar with it. It wraps like
 * the words it is, each line keeping the chip's fill.
 */
.state.state--says {
	display: inline;
	white-space: normal;
	border-radius: 0.4rem;
	-webkit-box-decoration-break: clone;
	box-decoration-break: clone;
	line-height: 1.8;
}

/* ---- filter chips, modernised ------------------------------------ */

/*
 * These already existed and are used on ten screens. Rather than adding a
 * second thing that looks like a filter, the one that is already there is
 * brought up to the same finish as everything else: no hard border, a soft
 * lift, and a solid fill for the one that is on — which reads at a glance
 * where a pale tint with a gold edge did not.
 *
 * THE FILL IS --chosen, NOT NAVY. A fixed navy "on" pill sat beside "off"
 * pills whose paper turns almost the same navy in dark: 1.05:1 between them,
 * so on the office's own default theme nobody could see which filter was on.
 * --chosen is navy on the light page and gold on the dark one, with
 * --chosen-on for its words.
 *
 * NO HEIGHT OF ITS OWN: app.css gives a pill the project's tap height, and
 * the 38px this used to set was under it on the phone the office carries.
 */
.pill {
	border: 0;
	box-shadow: var(--lift);
	border-radius: var(--r-pill);
	font-size: 0.86rem;
}

.pill.is-on,
.pill[aria-current='page'] {
	background: var(--chosen);
	border-color: transparent;
	color: var(--chosen-on);
	box-shadow: var(--lift-hi);
}

/* ---- rows as cards ----------------------------------------------- */

.rowcard {
	display: flex;
	align-items: center;
	gap: 0.9rem;
	background: var(--paper);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	padding: 0.85rem 1rem;
	margin-bottom: 0.7rem;
	text-decoration: none;
	color: inherit;
}
.rowcard:hover { box-shadow: var(--lift-hi); }
.rowcard__ico {
	width: 2.4rem;
	height: 2.4rem;
	border-radius: 10px;
	display: grid;
	place-items: center;
	background: var(--gold-wash);
	color: var(--ink);
	font-weight: 800;
	flex: 0 0 auto;
}
.rowcard__main { min-width: 0; flex: 1 1 auto; }
.rowcard__title { font-weight: 700; }
.rowcard__sub { font-size: 0.82rem; color: var(--ink-soft); }

/* ---- who is signed in -------------------------------------------- */

.whoami {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	margin-left: auto;
	/* The app bar is a navy surface (app.css's scope), so --ink is its light ink in both themes. */
	color: var(--ink);
	text-decoration: none;
}
.whoami__av {
	width: 2.1rem;
	height: 2.1rem;
	border-radius: 50%;
	background: var(--gold);
	color: var(--on-gold);
	display: grid;
	place-items: center;
	font-weight: 800;
	font-size: 0.85rem;
	flex: 0 0 auto;
}
.whoami__name { font-weight: 700; font-size: 0.86rem; line-height: 1.2; }
.whoami__role { font-size: 0.72rem; opacity: 0.78; line-height: 1.2; }

/* On a narrow phone the initials alone say who is signed in. */
@media (max-width: 30rem) {
	.whoami__name,
	.whoami__role { display: none; }
}

/* ---- tables, softened -------------------------------------------- */

.tablewrap {
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	overflow: auto;
}
.tablewrap .table { margin: 0; }

/* ---- the search bar ---------------------------------------------- */

.searchbar .input {
	border-radius: var(--r-pill);
	padding-inline: 1rem;
}
.searchbar .button { border-radius: var(--r-pill); }

/* ---- form controls ----------------------------------------------- */

/*
 * The boxes were the last thing still reading as a 2005 form: a hard 1px
 * border, square corners, and no relationship to the cards around them.
 * Softened rather than restyled, so every existing form inherits it and
 * nothing needs touching screen by screen.
 *
 * SOFTENED, NOT ERASED. This used to take the border away altogether and
 * leave the box's edge to the card shadow -- a navy shadow, which does not
 * show on a navy page, on a box filled with the same paper as the card it
 * sat in. In dark the lead form was four labels over empty space, and on a
 * customer's page "Jean" and "Murphy" read as plain text nobody could tell
 * was editable. So a box keeps app.css's --edge (3:1 on either ground, where
 * --line was 1.2:1), and inside a card it
 * is filled with the page's --wash, a shade away from the card in both
 * themes: on the page it is paper on wash, in a card wash on paper.
 *
 * The ring is --focus, which is blue on the light page and gold on the dark
 * one. Gold on white was 1.57:1.
 *
 * `:where()` round the body test, so a component that sizes its own boxes
 * (.searchbar's pill, the wizard's thumb-sized fields) still wins.
 */
:where(body:has(.portalnav)) .input {
	border-radius: 10px;
	border-color: var(--edge);
	background: var(--paper);
	box-shadow: var(--lift);
	min-height: 42px;
}

:where(body:has(.portalnav)) :is(.panel, .cfg, .briefing, .stat, .timeline__item, .wizard-day, .wizard-slot, .chartcard) .input {
	background: var(--wash);
	box-shadow: none;
}

:where(body:has(.portalnav)) .input:focus-visible {
	outline: 2px solid var(--focus);
	outline-offset: 1px;
}

/*
 * A default, not an order. As `body:has(.portalnav) .button` this scored
 * (0,2,1) and put a lift under every ghost button and every DISABLED button,
 * whose own rules (app.css) say no shadow -- a disabled button that still
 * looked raised -- and it squared off the search bar's pill.
 */
:where(body:has(.portalnav)) .button {
	border-radius: 10px;
	box-shadow: var(--lift);
}

/* ---- the phone ---------------------------------------------------- */

@media (max-width: 60rem) {
	/*
	 * A table's box scrolls on its own, as a last resort, rather than the page:
	 * the page itself never moves, which is the thing that makes horizontal
	 * scrolling feel broken. On a phone an office table should not need it at
	 * all (below).
	 */
	.tablewrap { -webkit-overflow-scrolling: touch; }

	.stats { grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.7rem; }
	.stat { padding: 0.85rem 0.9rem 0.95rem; }
	.stat__big { font-size: 1.45rem; }
}

/*
 * THE OFFICE ON A PHONE: TABLES THAT FIT AND BUTTONS OF A PHONE'S SIZE.
 *
 * "anywhere where there is tables it needs to fit on a phone screen without a
 * scroll so remove any unnecessary padding along with font size reductions if
 * necessary" and, of /portal/rounds at 375 wide, "here the buttons are massive
 * and the table goes off screen" -- the owner, 25/09/2026.
 *
 * Office pages only -- every one of them has the bar along the bottom -- so a
 * customer's own pages keep their size, and below 40rem, the width app.css
 * turns a .table--stack into cards at. On a laptop nothing here applies.
 */
@media (max-width: 40rem) {
	/*
	 * A TABLE, TIGHTER. app.css holds every table to 26rem at least, which is
	 * wider than a 375 phone's whole column, so every table in the office
	 * scrolled -- most by exactly the 75px that floor is over. Here a table is
	 * as narrow as its words let it be, in 13px type with 11px headings, and
	 * less padding. One that still does not fit, or fits only squeezed, is
	 * turned into cards by tables.js. The floors: 12px for a table's words,
	 * 11px for its small print and headings.
	 */
	:where(body:has(.tabbar)) .table {
		min-width: 0;
		font-size: 0.8125rem;
		line-height: 1.4;
	}

	:where(body:has(.tabbar)) .table:where(:not(.table--stack)) th,
	:where(body:has(.tabbar)) .table:where(:not(.table--stack)) td { padding: 0.45rem 0.5rem; }

	:where(body:has(.tabbar)) .table thead th {
		font-size: 0.6875rem;
		letter-spacing: 0.02em;
	}

	:where(body:has(.tabbar)) .table :is(.whisper, .hint, small) { font-size: 0.75rem; }

	/* A card's names are its small print, under its values' size, not over it. */
	:where(body:has(.tabbar)) .table--stack :is(td, tbody th)::before { font-size: 0.75rem; }

	/*
	 * A BUTTON OF A PHONE'S SIZE. The site's button is a pill for a cold wet
	 * thumb in a van: 54px tall, 1.6rem each side, bold 16px, and the rounds
	 * board's five of them took three lines of the screen. Here: 40px, 14px
	 * type, a little room either side -- still a thumb's target, and a row of
	 * them fits on a line or two.
	 *
	 * `:is()` so the size is the weight of `.button.small`, which app.css holds
	 * at 48px too, and later in the page than it.
	 */
	:where(body:has(.tabbar)) :is(.button, .button.small) {
		min-height: 2.5rem;
		padding: 0.45rem 0.9rem;
		gap: 0.35rem;
		font-size: 0.875rem;
		line-height: 1.25;
	}

	/* The one big button at the foot of a form: a full thumb, and no more. */
	:where(body:has(.tabbar)) :is(.button.big, .button.block) {
		min-height: 2.75rem;
		font-size: 1rem;
	}

	/* And a box in a row of them, the same height as the button beside it. Its words stay 16px: under that iOS zooms the page. */
	:where(body:has(.tabbar)) :is(.searchbar, .listfilter__bar, .inline-form) .input {
		min-height: 2.5rem;
		padding-block: 0.35rem;
	}

	/*
	 * On a map's page, a thumb's 44px, as everything there is held to: the
	 * map's own (.mapscreen, which the page says it is whether or not it has
	 * pins to draw yet) or any page a map is drawn on.
	 */
	:where(body:has(.tabbar):has(.mapscreen, [data-bsqmap])) :is(.button, .button.small) { min-height: 2.75rem; }

	/*
	 * And the rest of what a thumb presses there: the search box beside those
	 * buttons (40px), a word that is a button (Take back, 18px), and a label,
	 * which puts the cursor in its box ("Who", 25px). Its words sit at the foot,
	 * beside the box; not a label that holds its own tick, or one only a screen
	 * reader hears.
	 */
	:where(body:has(.tabbar):has(.mapscreen, [data-bsqmap])) :is(.searchbar, .listfilter__bar, .inline-form) .input { min-height: 2.75rem; }
	:where(body:has(.tabbar):has(.mapscreen, [data-bsqmap])) .linkish { min-height: 2.75rem; }
	:where(body:has(.tabbar):has(.mapscreen, [data-bsqmap])) label[for]:not(.sr-only):not(:has(input, select, textarea)) {
		display: flex;
		flex-wrap: wrap;
		align-items: flex-end;
		column-gap: 0.3em;
		min-height: 2.75rem;
	}
}

/*
 * The standing rule, across the top.
 *
 * Quiet enough to live on every screen without shouting, edged in the
 * danger colour so it reads as a rule rather than a caption. Somebody
 * stops seeing it after a week, which is fine — it is there for the
 * person who has been doing this a fortnight.
 */
.saferule {
	background: var(--danger-wash);
	border-left: 4px solid var(--danger);
	color: var(--ink);
	font-size: 0.82rem;
	font-weight: 600;
	padding: 0.55rem 0;
}

/*
 * A LINE THAT STAYS ONE THING but may wrap inside itself: a swatch and the
 * name after it, in a cell that has to fit a phone. `nowrap` kept them
 * together by pushing the table sideways.
 */
.keepline {
	display: inline-block;
	max-width: 100%;
	overflow-wrap: anywhere;
}

/* Its "i" in the band's own ink: link blue on the red wash was the one colour pair on it nobody chose. */
.saferule .info {
	color: inherit;
}

/* ---- the configuration screens ----------------------------------- */

/*
 * SHORT INPUTS BELONG BESIDE EACH OTHER.
 *
 * One setting per full-width panel meant two fields filled a screen, and
 * the Stripe block — mode, secret key, publishable key, webhook secret —
 * took four scrolls to read as the one thing it is. A textarea still gets
 * the full width, because a paragraph in a narrow column is worse.
 */
.cfggrid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
	gap: 0.85rem;
	margin-bottom: 1.1rem;
}

.cfg {
	background: var(--paper);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	padding: 0.9rem 1rem 1rem;
	min-width: 0;
}

.cfg .field { margin: 0; }
.cfg label { font-weight: 700; font-size: 0.88rem; display: block; margin-bottom: 0.35rem; }
.cfg .input { width: 100%; }
.cfg .hint { font-size: 0.78rem; margin: 0.4rem 0 0; }
/*
 * A caption in the portal's caption size, and at full strength: small AND
 * faded was 2.5:1 in light. --whisper itself is what makes it quiet.
 */
.cfg .whisper { margin: 0.5rem 0 0; }

/* A paragraph wants the room; a postcode box does not. */
.cfg:has(textarea) { grid-column: 1 / -1; }

/*
 * A CONFIGURATION FORM IS NOT A PARAGRAPH.
 *
 * `.stack` caps at `--measure`, which is the reading measure — right for a
 * form somebody fills in as prose, wrong for a grid of short credential
 * boxes. It was holding the whole Integrations screen to 480px inside a
 * 957px column, so the grid could only ever produce one track and every
 * field stacked.
 *
 * Only the config screens: the rest of the portal's forms are genuinely
 * one column of questions and want the measure they have.
 */
.cfgform.stack { max-width: none; }

/* The label spacing inside a card, where app.css assumes a stacked form. */
.cfg label { margin-top: 0; }

/*
 * A setting's name and its "i" (app.css .withinfo): the row keeps the gap
 * under the name, so the label does not add its own; and a tick with nothing
 * under it leaves no gap at the foot of its card.
 */
.cfg .withinfo > label { margin-bottom: 0; }
.cfg .withinfo:last-child { margin-bottom: 0; }
/* A setting's tick is the top line of its card, with no gap above it. */
.cfg .withinfo:has(> label > input[type="checkbox"]) { margin-top: 0; }

/*
 * A tick and its words on one line.
 *
 * app.css gives every `label` display:block and a top margin, which is
 * right for a form where the label sits above its box and wrong for a
 * checkbox, where the label IS the target you click.
 *
 * THE BOX HAS A WIDTH OF ITS OWN. This rule set `flex: 0 0 auto` and nothing
 * else, and app.css then gave every input `width: 100%` -- so the tick's flex
 * basis was the whole card, it could not shrink, and the setting's words were
 * left a column one letter wide down the right edge: the owner's money-rules
 * screen. app.css no longer lets that rule reach a tick at all; the box's
 * own size is here as well so this rule cannot do it again on its own.
 *
 * The words keep the card's label weight (.cfg label, above): the tick and
 * its setting are the heading of the card, not a caption under it.
 */
.cfg label:has(input[type="checkbox"]) {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	/* Level with "Never quote below" in the next card, and as heavy: it was 8px lower and 600. */
	margin: 0;
	font-weight: 700;
	cursor: pointer;
}
.cfg label:has(input[type="checkbox"]) input {
	margin: 0;
	flex: 0 0 auto;
	width: 1.15rem;
	height: 1.15rem;
	/* Ticked is the theme's "chosen" colour: navy on the light page, gold on the dark. */
	accent-color: var(--chosen);
}

/* The group headings were competing with the links under them. */
.portalnav .portalnav__heading { font-size: 0.63rem; opacity: 0.8; }

/* ---- A row of filters over a list -------------------------------- */

/*
 * SIX CONTROLS THAT HAVE TO FIT A PHONE.
 *
 * A grid rather than a flex row, so every box lines up on both edges
 * however long its label is — a ragged bottom edge on a filter bar reads
 * as broken rather than as responsive. `auto-fit` collapses it to one
 * column in a van and to six in the office with no breakpoint to keep in
 * step with the number of filters, which changes.
 */
.filters { margin: 0 0 var(--gap); }

.filters__row {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
	gap: 0.6rem 0.7rem;
	align-items: end;
}

.filters__row .field { margin: 0; }
.filters__row .field label { margin-top: 0; font-size: 0.76rem; }

/* The button sits on the baseline of the boxes, not above them. */
.filters__row .field--go { display: flex; align-items: flex-end; }
.filters__row .field--go .button { width: 100%; }

/*
 * `.nowrap` (a date that does not break at its hyphens) used to be here, so
 * only a page that loaded this file had it and a customer's statement did
 * not. It lives in app.css now, with `time`, for every page.
 */

/* ---- One enquiry from the chat bubble ---------------------------- */

.chatrow__head {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem 1rem;
	align-items: flex-start;
	justify-content: space-between;
}

.chatrow__who { margin: 0; font-size: 1rem; }

/*
 * What they typed, given room and a quiet ground so it reads as their
 * words rather than as another field. It is the only part of the row
 * nobody here wrote.
 */
.chatrow__said {
	margin: 0.6rem 0;
	padding: 0.6rem 0.75rem;
	background: var(--wash);
	border-left: 3px solid var(--gold);
	border-radius: 0 8px 8px 0;
}

.chatrow__close { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.7rem; }
/* "I will take this one" and "Put it back": spaced from the row above as "Mark answered" is. */
.chatrow__claim { margin-top: 0.7rem; }

/* The day being looked at, in the list of days (Team history). */
.table tbody tr.is-open { background: var(--gold-wash); }
.chatrow__close .input { flex: 1 1 auto; margin: 0; }

@media (max-width: 34rem) {
	.chatrow__close { flex-direction: column; align-items: stretch; }
}

/* ==================================================================
 * THE SIDEBAR AS AN ICON RAIL
 *
 * Forty links is why the sections fold. This is the other half: a
 * screen where the sidebar is not the point -- a wide table, the
 * canvassing map, a round being dragged into order -- wants the space
 * back, and wants it back without losing the way out.
 *
 * SO IT COLLAPSES TO ICONS AND KEEPS EVERY LINK. Hovering or tabbing
 * into a group opens its list beside the rail, which is why the icons
 * were worth having in the first place: after a week people navigate
 * by shape, and a rail is that reading taken at its word.
 *
 * IT IS AN ENHANCEMENT, NEVER A REQUIREMENT. The handle is built by
 * nav.js, so with the script blocked there is no rail, no handle, and a
 * perfectly ordinary sidebar -- which is what the markup renders on its
 * own. Nothing here is the only way to reach anything.
 *
 * AND NOT ON A PHONE. The sidebar there is already a drawer that opens
 * over the page; collapsing a drawer to a rail is two ideas fighting.
 * Everything below is inside the desktop query.
 * ================================================================== */

@media (min-width: 60rem) {
	/*
	 * THE CONTENT ACTUALLY GETS THE SPACE. A rail that narrows the
	 * sidebar and leaves the column where it was has done nothing at
	 * all except hide the labels.
	 */
	.band > .wrap:has(.portalnav.is-rail) {
		grid-template-columns: 3.9rem minmax(0, 1fr);
		gap: 1.1rem;
	}

	/*
	 * THE COLLAPSED RAIL DOES NOT CLIP.
	 *
	 * Expanded, this sidebar scrolls: forty links do not fit a short window,
	 * and the bottom of the list — sign out included — has to be reachable.
	 * Collapsed, it is eight icons, and what has to be reachable is the FLYOUT
	 * each one opens.
	 *
	 * Those two wants are incompatible on one element. `overflow-y: auto`
	 * forces `overflow-x` to `auto` as well, so a flyout positioned to the
	 * right of an icon is clipped at the rail's own edge — about eleven pixels
	 * of an eleven-rem panel, which reads as a sliver appearing and vanishing
	 * behind the cards rather than as a menu.
	 *
	 * So the rail gives up the scrolling it does not need. Eight icons are
	 * 560px against a viewport of 950; the list that needed scrolling was the
	 * expanded one, and the expanded one still has it.
	 *
	 * Written here rather than beside the scroll rule so the rail's behaviour
	 * is with the rest of the rail, and specific enough to beat it: the rule
	 * in app.css is three classes and a :has(), so two classes would lose
	 * however late this file loads.
	 */
	.band > .wrap:has(.portalnav) > .portalnav.is-rail {
		max-height: none;
		overflow: visible;
	}

	.portalnav.is-rail .portalnav__panel {
		padding: 0.7rem 0.45rem;
		/* The flyouts have to escape the panel. */
		overflow: visible;
	}

	/*
	 * THE RAIL IS THE GROUP ICONS, not the link icons.
	 *
	 * One shape per section, and the section's links a hover away. Showing
	 * every link's icon instead would be forty identical-looking glyphs in
	 * a column, which is a worse index than no index.
	 *
	 * The heading is the trigger, and it stays a real focusable button --
	 * see nav.js. A rail whose flyouts open only on hover is a keyboard
	 * dead end, and the whole sidebar is behind it.
	 */
	.portalnav.is-rail .portalnav__heading { margin: 0; padding: 0; }

	.portalnav.is-rail .portalnav__heading span,
	.portalnav.is-rail .portalnav__chev { display: none; }

	.portalnav.is-rail .portalnav__fold {
		display: grid;
		place-items: center;
		width: 100%;
		padding: 0.5rem 0;
		border-radius: 10px;
	}

	.portalnav.is-rail .portalnav__heading .ico {
		display: block;
		width: 1.2rem;
		height: 1.2rem;
		opacity: 0.9;
	}

	.portalnav.is-rail .portalnav__group:hover .portalnav__fold,
	.portalnav.is-rail .portalnav__group:focus-within .portalnav__fold {
		background: color-mix(in srgb, var(--side-ink) 8%, transparent);
		color: var(--side-ink);
	}

	/* A hairline instead of a label, so the groups still read as groups. */
	.portalnav.is-rail .portalnav__group + .portalnav__group {
		border-top: 1px solid var(--side-line);
		margin-top: 0.4rem;
		padding-top: 0.4rem;
	}

	.portalnav.is-rail .portalnav__group { position: relative; margin-bottom: 0; }

	/* ---- the flyout ---- */

	/*
	 * Hidden with visibility rather than display so it can be TRANSITIONED.
	 *
	 * The second half of that sentence used to read "and so focus can reach
	 * it", which is simply wrong: `visibility: hidden` takes an element out of
	 * the tab order exactly as `display: none` does. Only `opacity: 0` leaves
	 * it focusable, and this uses both.
	 *
	 * What actually makes the rail keyboard-usable is that each group carries a
	 * focusable TRIGGER OUTSIDE this list -- the `.portalnav__fold` button
	 * nav.js builds -- so :focus-within can fire before anything inside is
	 * reachable. The foot group had no such trigger and was a genuine dead end
	 * until its initials bubble became a button.
	 *
	 * Worth being blunt about, because a comment asserting the accessibility
	 * property it does not have is worse than no comment: it is the thing
	 * somebody reads instead of testing.
	 */
	.portalnav.is-rail .portalnav__list {
		position: absolute;
		left: calc(100% + 0.45rem);
		top: -0.4rem;
		min-width: 11.5rem;
		padding: 0.4rem;
		background: var(--side);
		border-radius: var(--r-card);
		box-shadow: var(--lift), 0 0 0 1px var(--side-line);
		visibility: hidden;
		opacity: 0;
		transform: translateX(-0.25rem);
		transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s;
		z-index: 40;
	}

	.portalnav.is-rail .portalnav__group:hover .portalnav__list,
	.portalnav.is-rail .portalnav__group:focus-within .portalnav__list {
		visibility: visible;
		opacity: 1;
		transform: none;
	}

	/*
	 * A BRIDGE OVER THE GAP. The flyout sits 0.45rem off the rail, and a
	 * pointer crossing that gap was over neither -- so the hover ended on the
	 * way to the menu it had opened. This invisible strip is part of the
	 * flyout, so crossing it keeps the group hovered.
	 */
	.portalnav.is-rail .portalnav__list::before,
	.portalnav.is-rail .portalnav__footmenu::before {
		content: '';
		position: absolute;
		top: 0;
		bottom: 0;
		right: 100%;
		width: 0.6rem;
	}

	/*
	 * The list keeps its ordinary appearance inside the flyout — labels,
	 * icons, the gold pill on the page you are on. It is the same list; it
	 * has only moved.
	 */

	/*
	 * WHICH GROUP HOLDS THE PAGE YOU ARE ON, when its label is hidden.
	 * Without this the rail shows a column of identical icons and the
	 * gold pill is inside a flyout nobody has opened.
	 */
	.portalnav.is-rail .portalnav__group[data-here]::before {
		content: '';
		position: absolute;
		left: -0.45rem;
		top: 0.35rem;
		bottom: 0.35rem;
		width: 3px;
		border-radius: 0 3px 3px 0;
		background: var(--gold);
	}

	/* ---- the foot ---- */

	/* Placed against by the menu below; it is a .portalnav__group, so already relative. */
	.portalnav.is-rail .portalnav__group--foot {
		margin-top: 0.6rem;
		padding-top: 0.6rem;
		display: grid;
		justify-items: center;
	}

	.portalnav.is-rail .portalnav__footicon { display: grid; }

	/*
	 * And it opens the same way as a group, so signing out is one hover
	 * away rather than a reason to expand the whole sidebar again.
	 *
	 * THE MENU MOVES; THE GROUP UNDER THE POINTER NEVER DOES. This used to
	 * reposition the hovered group itself -- position:absolute, anchored at
	 * the bottom, growing upwards -- so the initials jumped about 200px up
	 * the rail, out from under the pointer, the hover ended, the group fell
	 * back under the pointer, and the menu opened and shut about once a
	 * second. With a mouse it could not be used at all. Now the name, the
	 * workspaces, the theme switch and Sign out are a child menu (_shell.njk)
	 * placed beside the rail, like every other flyout, and opened from the
	 * group, which stays where it is.
	 * the-rail-menu-stays-under-the-pointer.test.ts fails on the old rule.
	 */
	.portalnav.is-rail .portalnav__footmenu {
		position: absolute;
		left: calc(100% + 0.45rem);
		bottom: 0;
		min-width: 11.5rem;
		width: max-content;
		max-width: 18rem;
		padding: 0.5rem 0.65rem;
		background: var(--side);
		border-radius: var(--r-card);
		box-shadow: var(--lift), 0 0 0 1px var(--side-line);
		text-align: left;
		visibility: hidden;
		opacity: 0;
		transform: translateX(-0.25rem);
		transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s;
		z-index: 41;
	}

	.portalnav.is-rail .portalnav__group--foot:hover .portalnav__footmenu,
	.portalnav.is-rail .portalnav__group--foot:focus-within .portalnav__footmenu {
		visibility: visible;
		opacity: 1;
		transform: none;
	}
}

/*
 * The group icon is for the RAIL and nowhere else. Expanded, the heading
 * is the word, and an unsized inline SVG beside it renders at its own
 * intrinsic size — which is 110 pixels of nothing across the sidebar.
 */
.portalnav__heading .ico { display: none; }

/* The initials chip. Only ever visible in the rail — see above. */
.portalnav__footicon {
	display: none;
	/* It is a <button> now (see _shell.njk) -- strip the chrome that comes with. */
	border: 0;
	padding: 0;
	font-family: inherit;
	cursor: pointer;
	width: 1.9rem;
	height: 1.9rem;
	border-radius: 50%;
	place-items: center;
	/* Gold words on the gold wash: both flip, and on the navy sidebar both are their dark selves. */
	background: var(--gold-wash);
	color: var(--gold-text);
	font-size: 0.72rem;
	font-weight: 800;
	letter-spacing: 0.02em;
}

/* ---- the handle that does it ---- */

/*
 * On the panel's edge, like every reference that has one: it belongs to
 * the boundary it moves, not to the content on either side of it.
 */
.portalnav__rail {
	position: absolute;
	top: 0.9rem;
	right: -0.8rem;
	width: 1.6rem;
	height: 1.6rem;
	display: none;
	place-items: center;
	padding: 0;
	/*
	 * A LIGHT RING, because half of this circle hangs over the PAGE.
	 *
	 * On a dark page a navy handle with no ring was navy on navy at 1.08:1:
	 * an invisible chevron floating in the margin. The handle is built
	 * inside the sidebar panel, which is a navy surface (app.css), so
	 * --ink-soft here is the light one in both themes: a pale ring that
	 * reads on the dark page, and on the light page the navy circle it
	 * encloses is what stands out.
	 */
	border: 1px solid var(--ink-soft);
	border-radius: 50%;
	background: var(--side);
	color: var(--side-soft);
	cursor: pointer;
	box-shadow: var(--lift);
	z-index: 42;
}

@media (min-width: 60rem) {
	/* Built by script, so it only exists where it can do anything. */
	.portalnav:has(.portalnav__rail) .portalnav__panel { position: relative; }
	.portalnav__rail { display: grid; }
}

.portalnav__rail:hover { color: var(--gold); border-color: var(--gold); }
.portalnav__rail:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/*
 * A chevron drawn in CSS rather than another inline SVG: it is one
 * shape that flips, and a rotation says "this goes back" better than
 * swapping two glyphs.
 */
.portalnav__rail::before {
	content: '';
	width: 0.4rem;
	height: 0.4rem;
	border-left: 2px solid currentcolor;
	border-bottom: 2px solid currentcolor;
	transform: rotate(45deg) translate(0.05rem, -0.05rem);
	transition: transform 0.15s ease;
}

.portalnav.is-rail .portalnav__rail::before {
	transform: rotate(-135deg) translate(0.05rem, -0.05rem);
}

/*
 * Somebody who has asked for less movement gets the flyout instantly
 * rather than not at all: the fade is decoration, the flyout is the
 * navigation.
 */
@media (prefers-reduced-motion: reduce) {
	.portalnav.is-rail .portalnav__list,
	.portalnav.is-rail .portalnav__footmenu { transition: none; }
	.portalnav__rail::before { transition: none; }
}

/*
 * WHAT IS WAITING, beside the link.
 *
 * Gold on navy, which is the same pairing the current-page pill uses --
 * this is the only other thing in the sidebar allowed to shout, and two
 * different accent colours in one column would make neither mean
 * anything.
 *
 * `margin-left: auto` rather than a fixed gap, so it sits hard against
 * the right edge whatever the label is: a column of counts that lines up
 * can be read at a glance, and one that follows the text cannot.
 */
.navcount {
	margin-left: auto;
	flex: 0 0 auto;
	min-width: 1.35rem;
	padding: 0 0.35rem;
	border-radius: 999px;
	background: var(--gold);
	color: var(--on-gold);
	font-size: 0.7rem;
	font-weight: 800;
	line-height: 1.35rem;
	text-align: center;
	font-variant-numeric: tabular-nums;
}

/*
 * A count with nothing in it yet -- Texts in's, at nought, kept as a place a
 * page left open can fill (portal/_navcount.njk) -- takes no room. The
 * browser's own [hidden] would do it too, until a later rule here gave the
 * count a display of its own.
 */
.navcount[hidden] { display: none; }

/* On the current page the pill is already gold, so the count inverts. */
.portalnav .portalnav__link.is-here .navcount {
	background: var(--navy-deep);
	color: var(--gold);
}

/* In the rail the label is hidden, so the count becomes a dot on the icon. */
@media (min-width: 60rem) {
	.portalnav.is-rail .portalnav__list .navcount { margin-left: 0.4rem; }
}

/* ---- filter chips, clickable figures, and a timeline -------------- */

/*
 * THREE PATTERNS THE NEW MONEY AND COMPLAINTS SCREENS NEEDED and the portal
 * did not have. Written here rather than inline, and built from the same
 * tokens as everything above, so they inherit the theme and the dark mode
 * without knowing about either.
 */

/*
 * A row of tick-to-filter chips. Without this the boxes stack one per line
 * with the label floating away from its input, which is what "ways of paying"
 * looked like the first time it was rendered.
 */
.chips {
	border: 0;
	margin: 0 0 1rem;
	padding: 0;
}

.chips__row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	align-items: center;
}

.chip {
	display: inline-flex;
	align-items: center;
	gap: 0.45rem;
	padding: 0.35rem 0.85rem;
	border-radius: var(--r-pill);
	background: var(--paper);
	box-shadow: var(--lift);
	font-size: 0.92rem;
	line-height: 1.3;
	cursor: pointer;
	/* The whole pill is the target, not the 13px box inside it. */
	user-select: none;
	/*
	 * A pill is as wide as its words. Without these two the row stretches the
	 * chips to equal widths and the labels break mid-word — "Direc t debit",
	 * which is how the first render of this looked.
	 */
	flex: 0 0 auto;
	white-space: nowrap;
}

.chip:hover { box-shadow: var(--lift-hi); }

/*
 * Keyboard focus has to be visible on the PILL, not on the hidden-ish box.
 * In the theme's ring colour: gold on a white page was 1.57:1.
 */
.chip:focus-within {
	outline: 2px solid var(--focus);
	outline-offset: 2px;
}

.chip input { margin: 0; }

/*
 * A panel that is a link. The figures at the top of the accounts and
 * complaints screens are links — somebody who reads "£412 owed across 19
 * households" wants the nineteen — and they have to LOOK like links or nobody
 * presses them.
 */
.panel--link {
	display: block;
	color: inherit;
	text-decoration: none;
	transition: box-shadow 120ms ease, transform 120ms ease;
}

.panel--link:hover,
.panel--link:focus-visible {
	box-shadow: var(--lift-hi);
	transform: translateY(-1px);
}

@media (prefers-reduced-motion: reduce) {
	.panel--link { transition: none; }
	.panel--link:hover,
	.panel--link:focus-visible { transform: none; }
}

/* The counterpart of .panel--attention, for a figure that is bad news. */
.panel--warn {
	box-shadow: var(--lift), inset 3px 0 0 0 var(--danger);
}

/*
 * What has been said and done, in order. Used by the ticket screen and by the
 * one column of everything said to a household.
 */
.timeline {
	list-style: none;
	margin: 0 0 1.2rem;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.7rem;
}

.timeline__item {
	background: var(--paper);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	padding: 0.8rem 1rem;
	/* A rule down the left says which side of the conversation this is. */
	border-left: 3px solid var(--line);
}

.timeline__item p { margin: 0 0 0.35rem; }
.timeline__item p:last-child { margin-bottom: 0; }

/* Ours, theirs, and neither — the difference is the meaning of the list. */
.timeline__item--out { border-left-color: var(--gold); }
.timeline__item--in { border-left-color: var(--link); }
.timeline__item--internal { border-left-color: var(--line); }

/*
 * A contact note keeps the lines it was written in -- "rang at 10, no answer"
 * and "rang again at 2, spoke to her" are often one note -- and a long word or
 * a pasted reference wraps rather than pushing the panel sideways.
 */
.contact-note { white-space: pre-line; overflow-wrap: anywhere; }
.contact-note-preview { overflow-wrap: anywhere; }

/*
 * An office task's detail keeps its lines too: "Missing: gutters\nReason: no
 * ladder" is how a checklist exception arrives, and run together it reads as
 * one confused sentence.
 */
.office-task__detail { white-space: pre-line; overflow-wrap: anywhere; }
.office-task__about { overflow-wrap: anywhere; }

/*
 * The column on Accounts. Ten columns share that table, and without a floor
 * this one was squeezed to under six rem -- a ninety-character note read three
 * words to a line. The table scrolls sideways inside its own wrapper rather
 * than the page doing so, as every wide table here does. Not on a phone,
 * where a row is read down the screen rather than across it.
 */
@media (min-width: 34rem) {
	.contact-notes-cell { min-width: 13rem; }
}

/* The add box on a list row: folded until wanted. */
.contact-note-add { margin-top: 0.35rem; }
.contact-note-add > summary { font-size: 0.85rem; }
.contact-note-add textarea { width: 100%; min-width: 12rem; }

/*
 * Settle, on a row of Accounts: a button beside Account until pressed, then
 * its form under it, across the row's last cell (a card's width on a phone).
 */
.settle-debt { display: inline-block; vertical-align: top; margin: 0 0.35rem 0.35rem 0; }
.settle-debt[open] { display: block; margin-right: 0; }
.settle-debt > summary { list-style: none; }
.settle-debt > summary::-webkit-details-marker { display: none; }
.settle-debt > form { margin-top: 0.5rem; min-width: 12rem; max-width: 22rem; }
.settle-debt > form .input { width: 100%; }
/* What Settle did, under the name on its row. */
.settle-debt__said { margin: 0.35rem 0 0; font-size: 0.85rem; }

/* Raising a ticket is folded away until somebody wants it. */
.raise { margin-top: 0.8rem; }

.raise > summary {
	cursor: pointer;
	font-size: 0.92rem;
	padding: 0.3rem 0;
}

.raise[open] > summary { margin-bottom: 0.6rem; }

/* ------------------------------------------------------------------ *
 * The briefing
 *
 * The one part of the dashboard with an opinion. Everything below it is a
 * figure of equal weight; this is the list of what to do first, worst first,
 * each line carrying the worst case by name and the button that acts on it.
 *
 * Built from the existing tokens rather than new colours. The stripe down the
 * left says which KIND of problem a row is -- work, money, people -- and is
 * deliberately not a severity: the order already carries that, and two
 * languages for the same thing on one card is how a reader learns to ignore
 * both.
 * ------------------------------------------------------------------ */

.briefing {
	background: var(--paper);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	padding: 1.1rem 1.2rem;
	margin-bottom: 1.4rem;
}

.briefing__title {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.6rem;
	margin: 0 0 0.9rem;
	font-size: 1.15rem;
}

.briefing__count {
	font-size: 0.85rem;
	font-weight: 600;
	color: var(--whisper);
	letter-spacing: 0.02em;
}

.briefing__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.55rem;
}

.briefing__row {
	display: flex;
	align-items: center;
	gap: 1rem;
	padding: 0.75rem 0.9rem;
	border-radius: var(--r-card);
	background: var(--wash);

	/* The kind of problem, not how bad it is. Overridden per tone below. */
	border-left: 3px solid var(--line);
}

/*
 * Three hues that each have a dark-theme override, so the stripe survives both
 * grounds. --gold-deep and --navy do not: gold is the brand colour and collided
 * with the primary button an inch to its right, and navy on the dark card is
 * navy on navy.
 *
 * The mapping is the conventional one -- money green, people blue, work amber
 * -- precisely so it reads as a category and not as a severity. The ORDER of
 * the rows is what says which is worst, and two languages for that on one card
 * teaches the reader to trust neither.
 */
.briefing__row--work { border-left-color: var(--warn-edge); }
.briefing__row--money { border-left-color: var(--success); }
.briefing__row--people { border-left-color: var(--link); }

/* The words take the room; the button takes what it needs. */
.briefing__what { flex: 1 1 auto; min-width: 0; }

.briefing__head {
	margin: 0;
	font-weight: 700;
	line-height: 1.35;
}

.briefing__detail {
	margin: 0.2rem 0 0;
	font-size: 0.9rem;
	color: var(--ink-soft);
	line-height: 1.4;
}

.briefing__do {
	flex: 0 0 auto;
	white-space: nowrap;
}

.briefing__clear {
	margin: 0;
	color: var(--ink-soft);
	max-width: var(--measure);
}

/*
 * ON A PHONE THE BUTTON GOES UNDER THE WORDS.
 *
 * Side by side at 375px the headline wraps to four lines against a button that
 * will not shrink, and "50 Halls Farm Close owes the most" -- the half that
 * makes the row actionable -- ends up as a column two words wide.
 */
@media (max-width: 34rem) {
	.briefing__row {
		flex-direction: column;
		align-items: stretch;
		gap: 0.6rem;
	}

	.briefing__do { text-align: center; }
}

/* ------------------------------------------------------------------ *
 * The phone's navigation
 *
 * The office had none. Its sidebar and collapsed rail are a desktop answer to
 * a desktop problem, and below 60rem the sidebar is not drawn at all.
 *
 * Five thumb-sized targets across the bottom of EVERY office page (the layout
 * draws it, portal/_tabbar.njk), the fifth opening a pop-out of everything
 * else so nothing is unreachable. From 60rem up it is not rendered at all: the
 * sidebar is there, and two navigations competing for one job is worse than
 * either.
 *
 * 59.99rem, NOT 60rem: the sidebar's query is min-width 60rem, and both
 * include 60rem itself -- a 960px window, half a common laptop screen, drew
 * the sidebar AND the bar, and More opened a second copy of the menu beside
 * the first. The old drawer stopped at 59.99rem for the same reason.
 * ------------------------------------------------------------------ */

.tabbar { display: none; }

@media (max-width: 59.99rem) {
	.tabbar {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 50;

		display: flex;
		align-items: stretch;
		/* At least the room every page leaves at its foot (--tabbar-h, app.css). */
		min-height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));

		background: var(--paper);
		border-top: 1px solid var(--line);

		/*
		 * Above the home indicator on a phone with no bezel. Without this the
		 * bottom row of targets sits under the system gesture area, where a tap
		 * goes to the operating system instead.
		 */
		padding-bottom: env(safe-area-inset-bottom, 0);
	}

	.tabbar__tab {
		flex: 1 1 0;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 0.15rem;

		/* The project's own minimum. */
		min-height: var(--tap-min, 48px);
		padding: 0.45rem 0.2rem;

		/*
		 * "More" is a <button> and the others links: a button brings the
		 * page's margin, fill and edge with it, as the label More used to be
		 * brought a label's top margin and bold. A tab is a tab.
		 */
		margin: 0;
		font-weight: inherit;
		font-family: inherit;
		border: 0;
		border-radius: 0;
		background: transparent;
		box-shadow: none;

		position: relative;
		color: var(--ink-soft);
		text-decoration: none;
		font-size: 0.72rem;
		line-height: 1.1;
		cursor: pointer;
	}

	.tabbar__tab.is-here { color: var(--link); font-weight: 700; }

	/* The icons are sized here rather than in the macro, which the sidebar shares. */
	.tabbar__icon svg { width: 20px; height: 20px; display: block; }

	/*
	 * The box every tab's icon sits in, 20px whatever is in it. "More" is
	 * the ☰ glyph, 13px tall, where the others are 20px drawings, and its
	 * label sat 4px higher than theirs.
	 */
	.tabbar__icon { display: grid; place-items: center; height: 20px; line-height: 1; }

	.tabbar__label { display: block; }

	.tabbar__dot {
		position: absolute;
		top: 0.25rem;
		left: 55%;
		min-width: 1.05rem;
		padding: 0 0.25rem;
		border-radius: 999px;
		background: var(--danger);
		/*
		 * The page's paper, cut out of the red. White was right on the light
		 * page's red and 2.2:1 on the pink the red becomes in dark; the paper
		 * turns dark with it (7.3:1 light, 6.9:1 dark).
		 */
		color: var(--paper);
		font-size: 0.65rem;
		font-weight: 700;
		line-height: 1.6;
		text-align: center;
	}

	/*
	 * TEXTS TO ANSWER, A ROW ON TOP OF THE BAR (05/10/2026, customer text
	 * replies S3). ITS OWN LINK, to Texts in on To answer, saying so -- "3 texts
	 * from customers to answer" -- because the Customers tab opens the customer
	 * list (portal/_tabbar.njk).
	 *
	 * ON TOP OF THE BAR, NEVER OVER A TAB. It first went in as a 28px red link
	 * laid over the top right corner of the Customers tab: under the 44px every
	 * target here keeps (nothing-tappable-is-small.test.ts), and a press a
	 * little right of the Customers icon on a 360px phone opened Texts in (the
	 * review of S3). There is no room beside a tab for a target a thumb can
	 * press -- five tabs are 64px each at 320 -- so the row sits above them:
	 * placed against the bar, which is fixed, and ending where the bar begins
	 * (bottom: 100%), from edge to edge. The tabs are where they always were,
	 * whether it is there or not.
	 *
	 * The project's tap height, as every tab under it. Words that wrap and no
	 * width of its own, so it can never push past the edge of a phone. The
	 * page's paper with the dot's red for the number, so it reads as part of
	 * the bar and as something waiting. Hidden (no row, no target, no room
	 * kept) at nought.
	 */
	.tabbar__texts {
		position: absolute;
		left: 0;
		right: 0;
		bottom: 100%;
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 0.5rem;
		box-sizing: border-box;
		min-height: var(--tap-min);
		padding: 0.35rem 1rem;
		background: var(--paper);
		border-top: 1px solid var(--line);
		color: var(--ink);
		font-size: 0.875rem;
		font-weight: 700;
		line-height: 1.2;
		text-align: center;
		text-decoration: none;
	}

	.tabbar__textsn {
		flex: none;
		min-width: 1.5rem;
		padding: 0.15rem 0.4rem;
		border-radius: 999px;
		background: var(--danger);
		color: var(--paper);
		font-size: 0.8rem;
		line-height: 1.2;
		text-align: center;
	}

	.tabbar__texts[hidden],
	.tabbar__textsn[hidden] { display: none; }

	.tabbar__texts:focus-visible { outline: 3px solid var(--link); outline-offset: -3px; }

	/*
	 * ROOM AT THE BOTTOM OF EVERY PAGE.
	 *
	 * A fixed bar covers the last inch of the document, and the thing it covers
	 * is usually the last row of a table or the submit button of a form -- both
	 * of which are reachable by scrolling on most pages and NOT reachable at
	 * the very bottom of any of them.
	 */
	body:has(.tabbar) { padding-bottom: calc(var(--tabbar-h) + 1rem + env(safe-area-inset-bottom, 0px)); }

	/* And the row of texts to answer on top of it, while it is there. */
	body:has(.tabbar__texts:not([hidden])) { padding-bottom: calc(var(--tabbar-h) + var(--tap-min) + 1rem + env(safe-area-inset-bottom, 0px)); }
}

/*
 * WHAT "MORE" OPENS: a sheet up from the foot of the screen, over the page and
 * the bar, with the page dimmed behind it (app.css, beside the "i"'s dimming).
 * "the more button only adds a side bar rather than brining up a popout
 * model" -- the owner, 25/09/2026.
 *
 * NOTHING HERE SETS `display` ON THE SHEET ITSELF. Whether it is drawn at all
 * is app.css's, in three rules that hide it until it is opened in every
 * browser -- one whose Popover API opens it, and one too old for popovers
 * where nav.js does. A display rule here would put it on every page, open.
 *
 * The groups in it start folded (they are <details>), so it opens as a short
 * list of section names rather than forty links. Every row is a full tap tall.
 */
.navsheet {
	box-sizing: border-box;
	position: fixed;
	inset: auto 0 0 0;
	width: 100%;
	max-width: 32rem;
	height: auto;
	/* Of the screen: a fixed box's containing block is the viewport. A strip of the page stays showing, to tap to shut it. */
	max-height: calc(100% - 2.5rem);
	margin: 0 auto;
	padding: 0 0 env(safe-area-inset-bottom, 0);
	overflow-x: hidden;
	overflow-y: auto;
	overscroll-behavior: contain;
	border: 1px solid var(--line);
	border-bottom: 0;
	border-radius: var(--r-card) var(--r-card) 0 0;
	background: var(--paper);
	color: var(--ink);
	box-shadow: var(--lift-hi);
	font-size: 1rem;
	line-height: 1.35;
	text-align: start;
}

.navsheet__head {
	position: sticky;
	top: 0;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	padding: 0.4rem 0.5rem 0.4rem 1rem;
	background: var(--paper);
	border-bottom: 1px solid var(--line);
}

.navsheet__title {
	margin: 0;
	font-size: 1.05rem;
	font-weight: 700;
	line-height: 1.2;
}

.navsheet__close {
	min-width: var(--tap-min, 48px);
	min-height: var(--tap-min, 48px);
	margin: 0;
	padding: 0 1rem;
	border: 1px solid var(--edge);
	border-radius: var(--r-pill);
	background: transparent;
	color: var(--ink);
	font: inherit;
	font-size: 0.9rem;
	font-weight: 600;
	cursor: pointer;
}

.navsheet__close:hover { background: var(--wash); }

.navsheet__body { padding: 0.25rem 0.5rem; }

.navsheet__group { border-bottom: 1px solid var(--line); }
.navsheet__group:last-child { border-bottom: 0; }

.navsheet__name {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	min-height: var(--tap-min, 48px);
	padding: 0 0.75rem;
	list-style: none;
	font-size: 0.8rem;
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ink-soft);
	cursor: pointer;
}

.navsheet__name::-webkit-details-marker { display: none; }

/* The chevron: down while folded, up once open. */
.navsheet__name::after {
	content: "";
	width: 0.45rem;
	height: 0.45rem;
	margin: 0 0.25rem 0.2rem auto;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: rotate(45deg);
}

.navsheet__group[open] > .navsheet__name::after {
	margin-bottom: -0.2rem;
	transform: rotate(-135deg);
}

.navsheet__name .ico,
.navsheet__link .ico {
	width: 20px;
	height: 20px;
	flex: 0 0 auto;
}

.navsheet__list {
	list-style: none;
	margin: 0;
	padding: 0 0 0.4rem;
}

.navsheet__link {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	min-height: var(--tap-min, 48px);
	padding: 0 0.75rem;
	border-radius: 10px;
	color: var(--ink);
	font-size: 0.95rem;
	font-weight: 600;
	text-decoration: none;
}

.navsheet__link:hover,
.navsheet__link:focus-visible { background: var(--gold-wash); }

.navsheet__link.is-here {
	background: var(--gold-wash);
	box-shadow: inset 3px 0 0 var(--gold);
}

.navsheet__foot {
	padding: 0.4rem 0.5rem 0.75rem;
	border-top: 1px solid var(--line);
}

.navsheet__foot form { margin: 0; }

/* The theme switch and Sign out: rows like the links, not underlined words. */
.navsheet__do {
	display: flex;
	align-items: center;
	width: 100%;
	min-height: var(--tap-min, 48px);
	padding: 0 0.75rem;
	border-radius: 10px;
	color: var(--ink);
	font-size: 0.95rem;
	text-align: start;
	text-decoration: none;
}

.navsheet__do:hover { background: var(--wash); }

/*
 * "Still open from before", on the cleaner's day.
 *
 * A strip rather than cards in the drive: the list below it is the order
 * somebody is actually driving, and a house appearing in the middle of it is
 * how a round gets driven wrong. Closed by default, because on a good week it
 * is empty and on a bad one it must not push today off the screen.
 */
.leftbehind {
	background: var(--warn-wash);
	border-left: 3px solid var(--warn-edge);
	border-radius: var(--r-card, 12px);
	padding: 0.7rem 0.9rem;
	margin-bottom: 0.9rem;
}

.leftbehind > summary {
	cursor: pointer;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5rem;
	min-height: var(--tap-min, 48px);
	align-content: center;
}

.leftbehind ul { margin: 0.6rem 0 0; }
.leftbehind li { padding: 0.35rem 0; }

/*
 * The owed figure in the bar.
 *
 * Pushed to the right of the workspace label and before the person, so the eye
 * order is: whose site, which workspace, what we are owed, who you are. Muted
 * rather than alarming -- it is context on every screen, and a number that
 * shouts on every screen stops being read by the second day.
 */
.appbar__owed {
	display: inline-flex;
	align-items: baseline;
	gap: 0.35rem;
	margin-left: auto;
	padding: 0.25rem 0.65rem;
	border-radius: var(--r-pill, 999px);
	/*
	 * A tint of the bar's own ink. The app bar is a navy surface (app.css's
	 * scope), so --ink in here is the light one in both themes -- which is
	 * also why `inherit` is right for the words: in light they used to
	 * inherit the page's navy ink, navy on navy at 1.06:1.
	 */
	background: color-mix(in srgb, var(--ink) 8%, transparent);
	color: inherit;
	text-decoration: none;
	white-space: nowrap;
}

.appbar__owed:hover,
.appbar__owed:focus-visible { background: color-mix(in srgb, var(--ink) 14%, transparent); }

.appbar__owed-n { font-weight: 700; font-variant-numeric: tabular-nums; }
.appbar__owed-l { font-size: 0.72rem; opacity: 0.75; }

/* On a phone the label goes; the figure and its pill stay. */
@media (max-width: 34rem) {
	.appbar__owed { margin-left: 0.4rem; padding: 0.2rem 0.45rem; }
	.appbar__owed-l { display: none; }
}

/*
 * THE BAR'S THREE TARGETS ARE A THUMB TALL ON A PHONE: the logo home, what the
 * business is owed, and who is signed in. They were 28, 31 and 34px on the
 * phone the office carries (the map pages measured them).
 */
@media (max-width: 60rem) {
	.appbar__brand { height: 44px; }
	.appbar__brand img { height: 28px; }
	.appbar__owed { min-height: 44px; align-items: center; }
	.whoami { min-height: 44px; min-width: 44px; }

	/* A bare fold on an office page -- "Which houses" on the map -- is a thumb tall too. */
	:where(body:has(.portalnav)) details > summary:not([class]) {
		min-height: var(--tap-min);
		padding-block: 0.7rem;
	}
}

/*
 * "Before you set off", on the cleaner's day.
 *
 * A row of small facts, not tiles. The field portal has no dashboard by design
 * and this is not one creeping in: no figures anybody acts on directly, no
 * buttons, and it disappears entirely on a morning where none of it is true.
 */
.beforeoff {
	list-style: none;
	margin: 0 0 0.75rem;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 0.4rem 0.5rem;
}

.beforeoff li {
	flex: 0 0 auto;
	background: var(--wash);
	border: 1px solid var(--line);
	border-radius: var(--r-pill, 999px);
	padding: 0.3rem 0.7rem;
	font-size: 0.85rem;
	white-space: nowrap;
}

.beforeoff strong { font-variant-numeric: tabular-nums; }

/* What a day and each round on it is worth, in the calendar. */
.cal__worth {
	margin-left: auto;
	padding-left: 0.6rem;
	color: var(--ink-soft);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.cal__dayworth {
	font-size: 0.82rem;
	font-weight: 600;
	color: var(--ink-soft);
	font-variant-numeric: tabular-nums;
}

/* The item row already lays out horizontally; make room for the figure. */
.cal__item { display: flex; align-items: baseline; gap: 0.4rem; }

/*
 * The selling menu: a flat list, no headings, no folding.
 *
 * Six items need none of the machinery forty items do, and the folding groups
 * are exactly what made this sidebar wrong for a rep in the first place.
 */
.portalnav__list--flat {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.1rem;
}


/*
 * WHAT THE CLEANER SAID, on the money screens.
 *
 * Quoted rather than styled as a hint, because it is somebody's words and the
 * office is about to charge a customer on the strength of them. The tag beside
 * it is the outcome, amber rather than red: a part clean is not an error, it is
 * a thing that needs a decision.
 */
.said {
	display: inline-block;
	/* --ink-soft: --muted was never declared, so its slate fallback showed on the dark page at 2:1. */
	color: var(--ink-soft);
	font-style: italic;
	line-height: 1.35;
	max-width: 32ch;
}

/*
 * The warning tag's SHAPE only. Its colours are app.css's .tag--warn, which
 * follow the theme; the cream-and-brown literals that were here replaced
 * them on every page that loads this file, so in dark every "switched off",
 * "overdue" and "Paused" was a bright cream pill on the dark page.
 */
.tag--warn {
	display: inline-block;
	padding: 1px 7px;
	border-radius: 999px;
	font-size: 11px;
	font-weight: 600;
	/* No text-transform: capitalize made "Ran out of time" "Ran Out Of Time". A tag's case is .tag's. */
}

/*
 * A tag that is information rather than a flag: "Bank transfer", "Card",
 * "agreed for later". The templates asked for it and no rule existed, so
 * it was the gold .tag -- the same look as the things that want attention.
 */
.tag--quiet {
	background: var(--wash);
	color: var(--ink-soft);
	box-shadow: inset 0 0 0 1px var(--line);
}

/*
 * THE LONGER NOTE UNDER A CHOSEN PAYMENT ROUTE, on the new-sale form.
 *
 * The one that matters is the walk-up note on the cash routes: this form
 * records no clean and no money, and a rep who believes otherwise leaves a
 * household's first clean unrecorded and the cash in somebody's pocket. So it
 * is set apart from the one-line hint above it rather than reading as more of
 * the same sentence. sale-new.js folds away the ones not chosen; with the
 * script blocked they are all shown, which is the right way round.
 */
.outcome__note {
	display: block;
	margin-top: 0.4rem;
	padding-top: 0.4rem;
	border-top: 1px solid var(--line);
	font-size: 0.8rem;
	line-height: 1.45;
	color: var(--ink-soft);
}

/* ============================================================== forecasting
 *
 * THE CONTROL STRIP EVERY FORECASTING SCREEN CARRIES, and the two blocks
 * underneath it.
 *
 * Six class names were used across the eight screens and not one of them had
 * a rule anywhere, because the screens were checked by rendering the route
 * under test and no browser was opened. Rendered that way, the primary
 * control on all eight screens -- the window and scenario chips -- came out
 * as bare inline spans: forty-pixel pills butted together with a single
 * collapsed space, wrapping mid-group, with the "How far ahead" and "Under"
 * labels sitting on their baseline. The section the owner opens first was the
 * one section of the portal that did not look like the portal.
 *
 * THE PILLS THEMSELVES WERE ALWAYS STYLED (.pill, app.css). What was missing
 * is the box around them, which is why the labels are part of this: a chip
 * group with a name in front of it is not the same control as a row of
 * filters, and .pills has nowhere to put the name.
 *
 * AT 390px each label and its own chips stay together and the two groups
 * stack, rather than "26 weeks" landing under "Under".
 */

.fchips {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem 1.25rem;
	margin-bottom: 1rem;
}

.fchips__label {
	font-size: 0.85rem;
	font-weight: 600;
	color: var(--ink-soft);
}

/* Its own flex row, so a group wraps as a group and never splits from its label. */
.fchips__set {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem;
}

@media (max-width: 40rem) {
	.fchips {
		flex-direction: column;
		align-items: flex-start;
		gap: 0.35rem;
	}
}

/*
 * THE INSIGHT LIST ON THE OUTLOOK SCREEN: the two or three things somebody
 * would act on, each a card rather than a bullet. A default <ul> indents
 * forty pixels and draws a disc beside a sentence that is the point of the
 * screen.
 */
.finsights {
	list-style: none;
	margin: 0 0 1.25rem;
	padding: 0;
	display: grid;
	gap: 0.75rem;
}

/*
 * Each insight is a card at the reading measure. At 1920 a row ran the width
 * of the screen, about 176 characters a line -- the owner's rule is the width
 * of the screen for the portal and a readable measure for its words, and
 * these are words.
 */
.finsights__row {
	background: var(--paper);
	border: 1px solid var(--line);
	border-left: 4px solid var(--gold);
	border-radius: var(--r-card);
	padding: 0.85rem 1rem;
	max-width: var(--measure);
}

/* The title is a <strong> on its own line, not a heading: the screen already
   has one h1 and these sit under an h2. */
.finsights__row > strong {
	display: block;
	margin-bottom: 0.25rem;
}

.finsights__row p {
	margin: 0 0 0.35rem;
}

.finsights__row p:last-child {
	margin-bottom: 0;
}

/*
 * THE LINE AT THE FOOT OF EVERY FORECASTING SCREEN: how much history there is
 * behind everything above it. Set apart from the content, because it is a
 * statement ABOUT the screen rather than more of it.
 */
.fcfoot {
	margin-top: 2rem;
	padding-top: 1rem;
	border-top: 1px solid var(--line);
}

/*
 * The print button sits inside a caption paragraph, so as a ghost button it
 * took the caption's grey and then faded it again: 2.9:1. It is a control,
 * so it is in ink.
 */
.fcfoot .button {
	color: var(--ink);
	opacity: 1;
}

.fcfoot p {
	margin: 0 0 0.4rem;
}

.fcfoot p:last-child {
	margin-bottom: 0;
}

/*
 * ASSUMPTIONS: THE ONE PLACE A FORECASTING FIGURE IS TYPED, a figure a row.
 *
 * A row is two lines, not a table: the name and the figure, and under them
 * where it came from and who set it when, with one compact Change and a
 * History link beside it. A table of these would be five columns wide, and
 * "anywhere where there is tables it needs to fit on a phone screen without a
 * scroll" (the owner, 25/09/2026) -- a row wraps instead: at 360px the figure
 * stays beside its name, the facts go under, and the two controls take the
 * line after.
 */
.agroup {
	margin-bottom: 1rem;
}

.alist {
	list-style: none;
	margin: 0;
	padding: 0;
	border-top: 1px solid var(--line);
}

.alist__row {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	justify-content: space-between;
	gap: 0.35rem 1rem;
	padding: 0.6rem 0;
	border-bottom: 1px solid var(--line);
}

/*
 * The words take the room; the controls take what they need. The name keeps
 * at least half the row (or 9rem): a figure that is a sentence -- "nobody has
 * cleaned in the last four weeks", kept on another screen -- wraps under
 * itself rather than squeezing the name to a letter a line.
 */
.alist__what {
	flex: 1 1 14rem;
	min-width: 0;
	display: grid;
	grid-template-columns: minmax(min(9rem, 50%), 1fr) minmax(0, auto);
	gap: 0.1rem 0.75rem;
}

.alist__name {
	font-weight: 600;
	overflow-wrap: anywhere;
}

.alist__figure {
	min-width: 0;
	text-align: right;
	font-variant-numeric: tabular-nums;
	overflow-wrap: anywhere;
}

/* "Use the measure" is a one-button form among the row's controls: on their line, not a step below it. */
.alist__do .aform,
.alist__do .aform__do {
	margin: 0;
}

/* The facts under a figure may be a long note: they wrap, never push. */
.alist__meta {
	grid-column: 1 / -1;
	overflow-wrap: anywhere;
}

.alist__do {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	gap: 0.75rem;
}

.alist__total {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
	margin: 0.5rem 0 0.75rem;
	font-weight: 600;
}

/* The Change dialog's box: a label over each field, the buttons after. */
.aform {
	display: grid;
	gap: 0.35rem;
	margin: 0 0 0.75rem;
}

.aform label {
	font-weight: 600;
	font-size: 0.9rem;
}

.aform__do {
	margin: 0.5rem 0 0;
}

/*
 * THE MONTHLY PLAN: Plan, Monthly and Notes (26/09/2026).
 *
 * ACTUAL, PLAN AND AIM NEVER LOOK THE SAME, AND NEVER BY COLOUR ALONE. A month
 * says its kind in words ("Actual", "Part actual: actual to 25/09, then
 * plan", "Plan"); a plan figure is also italic on a tint; a month gone is
 * plain. The words are the rule; the tint and the italic are the second cue.
 */
.fplan__lead {
	margin: 0 0 0.75rem;
}

.fplan__answer {
	font-size: var(--step-1, 1.15rem);
	margin: 0.75rem 0 1rem;
	max-width: var(--measure);
}

/* A figure the plan is waiting for: the words say it, never a nought. */
.fplan__needs {
	font-style: italic;
	color: var(--ink-soft);
	white-space: normal;
}

/* A month or a figure: plain when it is actual, italic on a tint when it is plan, and part actual in between. */
.fplan__row {
	vertical-align: top;
}

.fplan__row--actual td,
.fplan__row--actual th {
	font-style: normal;
}

.fplan__row--part th {
	font-weight: 700;
}

.fplan__row--plan td,
.fplan__row--plan th,
.fplan__p {
	font-style: italic;
}

.fplan__row--plan td,
.fplan__row--plan th {
	background: var(--wash);
}

/* The words under a figure or a month: its kind, "from today", "of which actual". */
.fplan__sub {
	display: block;
	font-size: 0.78rem;
	font-style: normal;
	color: var(--ink-soft);
	white-space: normal;
}

/* One card a financial year: side by side where there is room, one under another on a phone. */
.fplan__years {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
	gap: 0.75rem;
	margin: 0.5rem 0 1rem;
}

.fplan__year {
	border: 1px solid var(--line);
	border-radius: var(--r-card, var(--radius));
	padding: 0.75rem 0.9rem;
	min-width: 0;
}

.fplan__year h3 {
	margin: 0 0 0.5rem;
	font-size: 1rem;
}

/*
 * The name keeps room for its words and the figure takes the rest: a figure
 * the plan is waiting for is a sentence ("needs churn a year +2"), and with
 * the figure's column sized to its content it squeezed the name to a word a
 * line.
 */
.fplan__year dl {
	display: grid;
	grid-template-columns: minmax(7.5rem, 1fr) minmax(0, 1.2fr);
	gap: 0.35rem 0.75rem;
	margin: 0;
}

.fplan__year dt {
	min-width: 0;
}

.fplan__year dd {
	margin: 0;
	min-width: 0;
	text-align: right;
	font-variant-numeric: tabular-nums;
	overflow-wrap: anywhere;
}


/* A set of figures on Monthly: its own table, the strip choosing which is shown. */
.fplan__set {
	margin: 0.5rem 0 1rem;
}

/* The strip is drawn hidden until its script runs; a hidden set is hidden, whatever a flex rule says. */
.fchips[hidden],
.fplan__set[hidden] {
	display: none;
}

.fplan__set > h2 {
	font-size: 1rem;
	margin: 0 0 0.5rem;
}

/* The month in a row of Monthly: a button that opens every row it has. */
.fplan__month {
	white-space: nowrap;
}

.fplan__note {
	border-top: 1px solid var(--line);
	padding-top: 0.75rem;
	margin-top: 0.75rem;
	max-width: var(--measure);
}

.fplan__note h2 {
	font-size: 1.05rem;
	margin: 0 0 0.35rem;
}

/* An aim: its name and what was typed, then Aim, Actual and Plan, each said in words. */
.faim {
	border: 1px solid var(--line);
	border-radius: var(--r-card, var(--radius));
	padding: 0.75rem 0.9rem;
	margin: 0 0 0.75rem;
}

.faim h2 {
	font-size: 1.05rem;
	margin: 0 0 0.25rem;
}

/* What the plan asks him to check, and the levels he set that have been passed. */
.falerts {
	margin: 0 0 1rem;
}

.falerts ul {
	margin: 0.35rem 0 0;
	padding-left: 1.1rem;
}

/* Work and hands' "Told about": each level passed, its rule in the sentence, a line each. */
.falerts__list {
	margin: 0.35rem 0 0.75rem;
	padding-left: 1.1rem;
}

.falerts__list li + li {
	margin-top: 0.35rem;
}

/*
 * A LEVEL PASSED: its name on a line of its own, and beneath it what it is now,
 * a block (26/09/2026, the plugin's .bsq-alert__said). The sentence ran on after
 * the name in the same bold line, and on a phone it wrapped into and under the
 * name; a block wraps inside itself, under the name, at every width.
 */
.falerts__item > .withinfo {
	margin-bottom: 0;
}

.falerts__said {
	display: block;
	margin: 0.25rem 0 0;
}

/* On paper only: the whole of Monthly, the spreadsheet's shape. */
.print-only {
	display: none;
}

@media print {
	.print-only {
		display: block;
	}

	.fplan__set {
		display: none;
	}
}

/* ==================================================================
 * CLASSES THE TEMPLATES ASKED FOR THAT HAD NO RULE (24/09/2026)
 *
 * Each of these was in a template and in no stylesheet, so it rendered as
 * whatever the element happened to be: a bare list, a bare div, a gold tag
 * where a quiet one was meant. Built from the same tokens as everything
 * above, so they follow the theme without knowing about it.
 * ================================================================== */

/* A second, quieter button beside a primary one: the rounds page's "Find". Drawn as .button.ghost is. */
.button.button--quiet {
	background: transparent;
	color: inherit;
	border-color: currentcolor;
	/* Flat, as .button.ghost is: the portal's lift made it look raised beside real ghost buttons. */
	box-shadow: none;
}

.button.button--quiet:hover,
.button.button--quiet:focus-visible {
	background: color-mix(in srgb, currentColor 10%, transparent);
}

/*
 * A card inside a card, or one that is finished with: a van check question
 * inside its section, an exemption that has been closed. An outline, not a
 * second lifted card on top of the first.
 */
.panel.panel--quiet {
	background: transparent;
	box-shadow: none;
	border: 1px solid var(--line);
}

/*
 * "Are they already on the books?" on the new sale: each match a row, its
 * name and where it matched on the left, the door to it on the right, and
 * the button under the words on a phone. public/js/who.js draws the same
 * list as you type.
 */
.cardlist {
	list-style: none;
	margin: 0.6rem 0 0;
	padding: 0;
	display: grid;
	gap: 0.5rem;
}

.cardlist-item {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 0.5rem 1rem;
	padding: 0.7rem 0.9rem;
	border-radius: var(--r-card);
	background: var(--wash);
	border: 1px solid var(--line);
}

.cardlist-item > div { min-width: 0; flex: 1 1 16rem; }

/*
 * The marketing chart's card. The chart is a row of words and a bar per
 * source, drawn in HTML: its bars take the whole width of the screen, as
 * everything in the portal does, and its words stay the size of the words
 * round them. As an SVG scaled to the width the names grew with the bars, and
 * the card was capped at 52rem to stop them -- which left half a wide screen
 * empty beside a chart.
 */
.chartcard {
	background: var(--paper);
	color: var(--ink);
	border-radius: var(--r-card);
	box-shadow: var(--lift);
	padding: 1rem 1.1rem;
	margin: 0 0 1rem;
}

.srcbars {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 0.5rem;
}

.srcbar {
	display: grid;
	grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr) 3.5rem;
	align-items: center;
	gap: 0.75rem;
}

.srcbar__name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.srcbar__track {
	display: block;
	min-width: 0;
}

.srcbar__fill {
	display: block;
	height: 1.6rem;
	min-width: 2px;
	border-radius: 3px;
	background: currentColor;
	opacity: 0.85;
}

.srcbar__n {
	color: var(--ink-soft);
	font-variant-numeric: tabular-nums;
}

/*
 * The wrapper the list filter puts round what it filters (_list-filter.njk).
 * It has nothing to draw -- the bar and the list inside it are styled -- so
 * it is only told not to add a box of its own.
 */
.listfilter { display: block; min-width: 0; }

/* ==================================================================
 * THE ROUNDS WIZARD (24/09/2026): four steps, one screen each.
 *
 * The owner's words for the house style: one screen at a time, plain words,
 * big targets -- a laptop first, and never broken on a tablet. So the strip
 * across the top says where you are and is the way back; the day picker and
 * the van slots are cards whose whole face is the target; and every row
 * falls to one column below a tablet's width.
 * ================================================================== */

.wizard-steps {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin: 0.5rem 0 1.25rem;
	padding: 0;
	list-style: none;
}

.wizard-steps__step {
	border-radius: var(--r-pill);
	background: var(--paper);
	box-shadow: var(--lift);
	font-size: 0.92rem;
	color: var(--ink-soft);
}

/* The whole pill is the target, link or not. */
.wizard-steps__body {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	min-height: var(--tap-min);
	padding: 0.35rem 0.95rem 0.35rem 0.4rem;
	border-radius: var(--r-pill);
	color: inherit;
	text-decoration: none;
}

a.wizard-steps__body { color: var(--link); }

a.wizard-steps__body:hover { text-decoration: underline; }

.wizard-steps__n {
	display: inline-grid;
	place-items: center;
	flex: 0 0 auto;
	width: 1.9rem;
	height: 1.9rem;
	border-radius: 50%;
	background: var(--wash);
	font-weight: 800;
	color: var(--ink);
}

/*
 * Where you are, and (below) the days picked: the theme's "chosen" colour,
 * navy on the light page and gold on the dark. A gold ring on the white card
 * was 1.6:1 -- the one thing on the step that says "here" was the faintest.
 */
.wizard-steps__step.is-current {
	color: var(--ink);
	font-weight: 700;
	outline: 2px solid var(--chosen);
}

.wizard-steps__step.is-current .wizard-steps__n {
	background: var(--gold);
	color: var(--on-gold);
}

.wizard-steps__step.is-done .wizard-steps__n {
	background: color-mix(in srgb, var(--gold) 35%, transparent);
}

/* On a phone the steps behind and ahead are just their numbers; where you are keeps its words. */
@media (max-width: 640px) {
	.wizard-steps__step:not(.is-current) .wizard-steps__said {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip: rect(0 0 0 0);
		white-space: nowrap;
	}
}

.wizard-gap { margin-top: 1.25rem; }

.wizard-count {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.6rem;
	margin: 0 0 0.5rem;
}

.wizard-count .figure { margin: 0; }

.wizard-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem;
	margin-top: 1rem;
}

/* The buttons that move the wizard on are the biggest thing on the screen. */
.wizard-actions .button {
	min-height: 3.25rem;
	padding: 0.8rem 1.5rem;
	font-size: 1.05rem;
}

/* Still the biggest thing on a phone, at a thumb's 44px rather than a van's pill (the office on a phone, above). */
@media (max-width: 40rem) {
	.wizard-actions .button {
		min-height: 2.75rem;
		padding: 0.5rem 1.1rem;
		font-size: 1rem;
	}
}

.wizard-actions .button[disabled] {
	opacity: 0.45;
	cursor: not-allowed;
}

.wizard-fieldset {
	margin: 0;
	padding: 0;
	border: 0;
}

.wizard-fieldset legend h2 { margin-bottom: 0.75rem; }

.wizard-days {
	display: grid;
	gap: 0.75rem;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
}

.wizard-day {
	padding: 0.9rem 1rem;
	border-radius: var(--r-card);
	background: var(--paper);
	box-shadow: var(--lift);
}

.wizard-day.is-gone { opacity: 0.55; }

.wizard-day__pick {
	display: flex;
	align-items: center;
	gap: 0.7rem;
	margin: 0;
	min-height: var(--tap-min);
	cursor: pointer;
	font-weight: 400;
}

/* A checkbox the size of a thumb, not the browser's thirteen pixels. */
.wizard-day__pick input {
	width: 1.5rem;
	height: 1.5rem;
	margin: 0;
	/* The theme's "chosen": a fixed navy tick was navy on the dark card. */
	accent-color: var(--chosen);
}

.wizard-day:has(input[type='checkbox']:checked) {
	outline: 2px solid var(--chosen);
}

.wizard-day__rounds {
	margin-top: 0.5rem;
	font-size: 0.9rem;
}

.wizard-day .input { max-width: 7rem; }

.wizard-day p { margin: 0.4rem 0 0; }

.wizard-round h2 { margin-bottom: 0.25rem; }

.wizard-slots {
	display: grid;
	gap: 0.75rem;
}

.wizard-slot {
	display: grid;
	grid-template-columns: minmax(10rem, 1.1fr) minmax(12rem, 1fr) minmax(12rem, 1fr);
	gap: 0.5rem 1rem;
	align-items: end;
	padding: 0.9rem 1rem;
	border-radius: var(--r-card);
	background: var(--paper);
	box-shadow: var(--lift);
}

/* A suggestion, not a choice: the gold that reads on the page, dashed. */
.wizard-slot.is-suggested { outline: 2px dashed var(--gold-text); }

.wizard-slot label { margin-top: 0; }

.wizard-slot .input { width: 100%; min-height: var(--tap-min); }

.wizard-slot__name {
	display: grid;
	gap: 0.2rem;
	align-self: center;
}

.wizard-slot__warn {
	color: var(--danger);
	font-weight: 600;
	font-size: 0.9rem;
}

@media (max-width: 780px) {
	.wizard-slot { grid-template-columns: 1fr; }
}

/*
 * THE PAUSE BOX'S TWO CHOICES (Ed, 30/09/2026; views/portal/_pause-choice.njk):
 * "Until a date" with its date box, or "No end date". The date box goes the
 * moment "No end date" is chosen -- CSS, so it works as the page draws; where
 * :has() is not understood it stays, and the server ignores it (pause.ts).
 */
.pausechoice {
	border: 0;
	padding: 0;
	margin: 0;
	min-width: 0;
}

.pausechoice__date { margin: 0 0 0.35rem 2rem; }

.pausechoice:has(input[value="open"]:checked) .pausechoice__date {
	display: none;
}

/*
 * BUSINESS > EXPENSES (02/10/2026; views/portal/expenses.njk).
 *
 * LINES, NOT A TABLE: the office reads this on a phone. Each line is one row
 * -- the day, who and what for, the amount on the right -- that wraps inside
 * its own width at 375px and never pushes the page sideways: a long supplier
 * wraps, an amount is never broken. A line opens in place (a <details>), so
 * the list needs no script.
 */
.expenses__add { display: block; width: 100%; text-align: center; }
.expenses__sum { margin: 0.75rem 0; font-weight: 600; overflow-wrap: anywhere; }
/* Opened from Business > Tax's "no receipt photo" warning: which lines these are, and the way back to all. */
.expenses__only { margin: 0.5rem 0; overflow-wrap: anywhere; }
.expenses { list-style: none; margin: 0; padding: 0; }
.expenses__item { border-top: 1px solid var(--line); }
.expenses__item:first-child { border-top: 0; }
.expense {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.15rem 0.6rem;
	min-height: var(--tap-min);
	padding: 0.6rem 0.15rem;
	color: inherit;
	text-decoration: none;
	cursor: pointer;
}
.expense::-webkit-details-marker { display: none; }
summary.expense { list-style: none; }
div.expense { cursor: default; }
.expense.is-void .expense__amt { text-decoration: line-through; }
.expense__day { flex: 0 0 auto; white-space: nowrap; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.expense__what { flex: 1 1 9rem; min-width: 0; overflow-wrap: anywhere; }
.expense__amt { flex: 0 0 auto; margin-left: auto; white-space: nowrap; font-weight: 700; font-variant-numeric: tabular-nums; }
.expense__clip { vertical-align: -2px; margin-left: 2px; }
/* An opened line says which it is: the words above its facts in bold. */
.expense__box[open] > .expense .expense__what { font-weight: 600; }
/* A garage bill read from a van: its badge sits with the words, never on the amount. */
.expense--van .tag { margin-left: 0.25rem; white-space: nowrap; }
.expense__detail { padding: 0.15rem 0.15rem 1rem; }
.expense__facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.25rem 0.75rem; margin: 0 0 0.75rem; }
.expense__facts dt { color: var(--ink-soft); }
.expense__facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.expense__photo { display: block; max-width: 100%; max-height: 60vh; height: auto; margin: 0.5rem 0; border-radius: var(--r-card); }
.expense__voiding { margin-top: 0.75rem; }
.expense__voiding > summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; }
.expenses__voided { margin-top: 1.25rem; }
.expenses__voided > summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; font-weight: 600; }

/*
 * The add and change form: one column at any width, and the day it was paid
 * shown only once "On another day" is chosen -- CSS, so it works as the page
 * draws; where :has() is not understood the box stays, and the server reads it
 * only for "On another day" (routes/portal-expenses.ts typedFrom).
 */
.expense-form fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.expense-form__paidon { margin: 0.35rem 0 0.35rem 2rem; }
.expense-form__when:not(:has(input[value="later"]:checked)) .expense-form__paidon { display: none; }

/*
 * BUSINESS > TAX (05/10/2026; views/portal/tax.njk).
 *
 * LINES, NOT A TABLE, at 375px with no sideways scroll: a warning is its
 * words and a button that wraps beneath them; "Save everything" is the whole
 * width; each figure is one line -- its name, its sum on the right -- that
 * opens in place (a <details>) to what it is made of, set out as label and
 * value in two columns that wrap inside their own width.
 */
.tax__dates { margin-top: 0.5rem; }
.tax__period { margin: 0.75rem 0; font-weight: 600; }
.tax__warn { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin: 0 0 0.75rem; }
.tax__warn > span { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; }
.tax__pack { display: block; width: 100%; text-align: center; }
.tax__one > summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; font-weight: 600; }
.tax__files { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.5rem; }
.tax__files .button { display: block; width: 100%; text-align: center; }
.tax__figs { list-style: none; margin: 0 0 1rem; padding: 0; }
.tax__figs > li { border-top: 1px solid var(--line); }
.tax__figs > li:first-child { border-top: 0; }
.tax__fig > summary {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.15rem 0.6rem;
	min-height: var(--tap-min);
	padding: 0.6rem 0.15rem;
	cursor: pointer;
	list-style: none;
}
.tax__fig > summary::-webkit-details-marker { display: none; }
.tax__name { flex: 1 1 8rem; min-width: 0; overflow-wrap: anywhere; font-weight: 600; }
.tax__sum { flex: 0 0 auto; margin-left: auto; white-space: nowrap; font-weight: 700; font-variant-numeric: tabular-nums; }
.tax__facts { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.25rem 0.75rem; margin: 0 0 0.75rem; }
.tax__facts dt { color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
.tax__facts dd { margin: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
.tax__vat { margin: 0.75rem 0 1rem; overflow-wrap: anywhere; }
.input--daymonth { max-width: 8em; }

/*
 * DEVELOPER > MESSAGE WORDING (05/10/2026; views/portal/wording.njk and
 * wording-message.njk).
 *
 * ON A PHONE AT 375PX, NOTHING SIDEWAYS (the owner, 25/09/2026). A message's
 * name wraps inside its own width and its marks drop under it; the fill-in
 * buttons wrap onto as many rows as they need; the preview keeps a message's
 * own line breaks and breaks a long link anywhere, because a sign-in link is
 * one unbroken word as wide as three phones. The plugin lost two names off a
 * 375px screen to a stronger button rule; here a name is a plain link.
 */
.wording__group > summary { min-height: var(--tap-min); display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; cursor: pointer; }
.wording__list { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.wording__row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; padding: 0.5rem 0; border-top: 1px solid var(--line); }
.wording__row:first-child { border-top: 0; }
.wording__open { flex: 1 1 12rem; min-width: 0; min-height: var(--tap-min); display: flex; align-items: center; overflow-wrap: anywhere; font-weight: 600; }
.wording__marks { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
.wording__back { display: inline-flex; align-items: center; min-height: var(--tap-min); }
.wording__box { min-width: 0; max-width: 100%; }
.wording__fills { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0 0; }
.wording__fill {
	min-height: var(--tap-min);
	max-width: 100%;
	padding: 0.35rem 0.6rem;
	border: 1.5px solid var(--edge);
	border-radius: calc(var(--radius) * 0.7);
	background: var(--paper);
	color: var(--ink);
	font: inherit;
	font-size: 0.9rem;
	overflow-wrap: anywhere;
	cursor: pointer;
}
.wording__fill.is-needed { font-weight: 800; border-color: var(--ink); }
.wording__more { margin-top: 0.5rem; }
.wording__more > summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; }
.wording__mail { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.75rem; min-width: 0; }
.wording__subject { margin: 0 0 0.5rem; font-weight: 700; overflow-wrap: anywhere; }
.wording__preview { white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; max-width: 100%; }
div.wording__preview[data-wordprev="sms"] { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.75rem; }
.wording__reset { margin-top: 0.75rem; }
/* What the screen refuses as he presses Save, by the button (wording.js): read and focused, with no ring round a box of words. */
.wording__notsaved { min-width: 0; overflow-wrap: anywhere; scroll-margin: 4rem 0; }
.wording__notsaved:focus { outline: none; }
.wording__notsaved:empty { display: none; }

/*
 * NEW ROUND, FOLDED AT THE TOP OF THE ROUNDS PAGE (12/10/2026). Its summary is
 * a button, so it carries no disclosure triangle of its own.
 */
.newround > summary { list-style: none; cursor: pointer; }
.newround > summary::-webkit-details-marker { display: none; }

/*
 * RUN THIS PATCH, folded at the top of a patch's page (12/10/2026). A plain
 * edged box rather than a panel, because what it holds are panels: a card in
 * a card is the shape the office screens keep out. Who has the patch rides on
 * the fold, beside its name.
 */
.runpatch {
	background: var(--paper);
	border: 1px solid var(--line);
	border-radius: var(--r-card, 12px);
	padding: 0.7rem 0.9rem;
}
.runpatch > summary {
	cursor: pointer;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.25rem 0.75rem;
	min-height: var(--tap-min, 48px);
	align-content: center;
	list-style: none;
}
.runpatch > summary::-webkit-details-marker { display: none; }
/* A fold that says it opens: the chevron at the end, turned over when it is open. */
.runpatch > summary::after {
	content: '\25BE';
	margin-left: auto;
	transition: transform 0.15s;
}
.runpatch[open] > summary::after { transform: rotate(180deg); }

/*
 * INSTALL THIS APP, in More (12/10/2026). Each face is hidden until
 * register-sw.js picks the one for this phone, and the row rules above would
 * otherwise beat the browser's own [hidden]. A face with an "i" is a row with
 * the "i" at its end.
 */
.navsheet__do[hidden],
.navsheet__install[hidden] { display: none; }
.navsheet__install {
	display: flex;
	align-items: center;
	gap: 0.25rem;
}
.navsheet__install > .navsheet__do { flex: 1 1 auto; min-width: 0; }

/*
 * REFRESH, last in the bar at the top for staff (12/10/2026): the office
 * installs as an app, and an app on a home screen has no reload button. A
 * thumb's round target in the bar's own ink, as the owed pill is; hidden until
 * refresh.js shows it, and that has to win over the display here.
 */
.appbar__refresh {
	display: inline-grid;
	place-items: center;
	flex: 0 0 auto;
	width: 44px;
	height: 44px;
	margin-left: 0.25rem;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: color-mix(in srgb, var(--ink) 10%, transparent);
	color: var(--ink);
	font: inherit;
	font-size: 1.25rem;
	line-height: 1;
	cursor: pointer;
}
.appbar__refresh:hover,
.appbar__refresh:focus-visible { background: color-mix(in srgb, var(--ink) 20%, transparent); }
.appbar__refresh[hidden] { display: none; }

/*
 * ROOM FOR IT ON A PHONE (the reviewers, 12/10/2026). The bar does not wrap,
 * and with Refresh in it a four-figure owed figure pushed Refresh into the
 * 16px gutter at 360px, and off the edge from five figures -- at 390px too.
 * So on a phone the gaps close a little, the workspace label gives up its
 * room first (it was already cut to an ellipsis), and then the logo, down to
 * 4rem, rather than anything that is pressed or read. Measured in a browser:
 * Refresh ends on the gutter at 360px and 390px up to £1,234,567.89.
 */
@media (max-width: 30rem) {
	.appbar__in { gap: 0.5rem; }
	.appbar__where { flex-shrink: 1000; }
	.appbar__owed { margin-left: 0; }
	.appbar__refresh { margin-left: 0; }
	.appbar__brand { flex: 0 1 auto; min-width: 4rem; }
	/* Narrower than its own width, the logo is drawn smaller rather than squashed. */
	.appbar__brand img { height: 28px; max-width: 100%; object-fit: contain; }
}

/*
 * THE WAY BACK TO THE VAN, where the bar's place name is a link (the
 * sign-off's D2: somebody whose home is the van, on an office page). It is
 * pressed, so it is a thumb's height and it is never the thing that gives way
 * -- the rule above was written for a label, and with Refresh beside it the
 * link was cut to "<- Back to th..." at 360px and 390px (the 12/10 lanes met
 * here; measured in a browser). On a phone it says "Van app", which fits
 * whole beside the logo, the initials and Refresh at 320px.
 */
a.appbar__where {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
}
.appbar__narrow { display: none; }
@media (max-width: 30rem) {
	a.appbar__where { flex-shrink: 0; }
	.appbar__wide { display: none; }
	.appbar__narrow { display: inline; }
}


/*
 * WHAT A PRESS SAID, BESIDE THE ROW it came back to (12/10/2026,
 * public/js/back-here.js): a little room under it, so it does not sit on the
 * row's first words.
 */
.flash--here { margin: 0 0 0.5rem; }
/* Under a table row, a line of its own across the table (on a phone, a card's last line). */
.flash-row > td { padding-top: 0; }

/*
 * WHAT CUSTOMERS TEXT BACK (customer text replies S2): the Texts card on a
 * record, Customers > Texts in, and a household's whole thread. Lines, never a
 * table, so a phone at 375px reads a reply down the screen with nothing to
 * scroll sideways; their words keep the lines they were written in, and a long
 * word or a pasted link wraps rather than pushing the card wider.
 */
.textreplies__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.textreply { min-width: 0; padding: 0.6rem 0 0.6rem 0.75rem; border-left: 3px solid var(--link); }
.textreply.is-focus { background: var(--gold-wash); border-left-color: var(--gold); border-radius: 0 var(--radius) var(--radius) 0; }
.textreply:target { scroll-margin-top: 4rem; }
.textreply p { margin: 0 0 0.35rem; }
/* A div where the chip is, as a chip may carry an "i" (Stop word, Start word) and a paragraph may not. */
.textreply__head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.5rem; margin: 0 0 0.35rem; }
.textreply__who { font-weight: 700; overflow-wrap: anywhere; }
.textreply__words { white-space: pre-line; overflow-wrap: anywhere; }
.textreply__note { white-space: pre-line; overflow-wrap: anywhere; }
.textreply__after { overflow-wrap: anywhere; }
.textreply__bar { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.5rem; margin-top: 0.35rem; }
.textreply__bar > form { margin: 0; }
.textreply__fold { min-width: 0; }
.textreply__fold[open] { flex: 1 1 100%; }
.textreply__fold > summary { list-style: none; }
.textreply__fold textarea { width: 100%; }
.textreplies__fold > summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; }
.textreplies__cands { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.textreplies__cands button { max-width: 100%; white-space: normal; overflow-wrap: anywhere; text-align: left; }
.textthread__gap { background: var(--wash); }

/*
 * A STOP, AND A HOUSEHOLD'S TEXTS OFF (customer text replies S4). The STOP's
 * plain line keeps its words on a phone; "Texts off since ..." sits under the
 * number with its "i" and the manager's switch, wrapping onto a line of their
 * own at 375px rather than pushing the panel wider.
 */
.textreply__stop { overflow-wrap: anywhere; }
.textsoff { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.5rem; margin: 0.35rem 0; min-width: 0; }
.textsoff > span { overflow-wrap: anywhere; }
