/*
 * app.css: portal-wide custom rules that sit alongside Vanilla Framework.
 * Linked from base.templ, signing_shell.templ and assets.templ (admin and operator pages) in
 * internal/interfaces/http/views/layout/, after additional-icons.css to override icon defaults.
 *
 * Kept intentionally small. Add rules here only when Vanilla does not already
 * provide them.
 */

:root {
	/* Spacing scale: Vanilla ships no native spacing tokens (colour-only).
	 * These complement --vf-color-* for a complete design system. Derived from
	 * actual in-repo usage; every value appears >= 2 times in the codebase. */
	--space-xs:  0.25rem;
	--space-sm:  0.5rem;
	--space-md:  0.75rem;
	--space-lg:  1rem;
	--space-xl:  1.5rem;
	--space-2xl: 2rem;
	--space-3xl: 3rem;

	/* Corner radius: Vanilla is squared-off (no native radius token).
	 * House value is 4px; applied to rectangular surfaces below. Pill/circular
	 * elements (chips, badges, switches, radios, avatars) keep their own radius. */
	--radius: 4px;
	/* Brand palette: cluster-sampled from assets/brand/esoppari.png.
	 * --brand-blue is the ACTIONABLE accent (8.3:1 on white, AA for text);
	 * the teal fails text contrast (2.7:1) and is decorative only.
	 * --status-positive is SEMANTIC success (signed states) and must never
	 * be replaced by a brand colour; users read green as signed. */
	--brand-teal: #03B2A2;
	--brand-blue: #05508C;
	--brand-navy: #041A2F;
	--brand-tint: #EEF4FB;
	--status-positive: #0e8420;

	/* The theme is these re-declarations of Vanilla's own colour tokens, not a
	 * per-rule repaint: Vanilla declares them on `:root,.is-light` and on
	 * `.is-dark` at specificity 0,1,0, so the same selector later in source
	 * order wins with no !important, and every stock component follows. A new
	 * rule therefore reaches the theme by naming a token; a literal does not.
	 * --vf-color-accent is the one token left stock: Vanilla reads it only in
	 * `.is-accent` heading variants and `.p-rule--highlight.is-accent::before`
	 * (neither of which this codebase uses) and re-declares it on .is-dark /
	 * .is-paper / .p-strip--* / .p-tooltip__message, so a :root override would
	 * not reach those subtrees anyway (the admin nav is .is-dark). */
	--vf-color-link-default: var(--brand-blue);
	--vf-color-link-visited: var(--brand-blue);
	--vf-color-focus: var(--brand-blue);
	--vf-color-border-default: rgba(4, 26, 47, 0.2);
	--vf-color-border-low-contrast: rgba(4, 26, 47, 0.1);
	--vf-color-border-high-contrast: #5a6b7c;
	--vf-color-border-information: var(--brand-blue);
	--vf-color-background-information-default: rgba(5, 80, 140, 0.1);
	--vf-color-background-information-hover: rgba(5, 80, 140, 0.15);
	--vf-color-background-alt: var(--brand-tint);
	--vf-color-background-inputs: #fff;

	/* PDF viewer theme: the colours of the component/pdf_viewer.templ chrome. */
	--pdf-bg:      #525659;          /* canvas container background (dark grey) */
	--pdf-ctrl-bg: var(--brand-tint); /* control bar, the tint every other alt surface uses */
	--pdf-text:    #fff;             /* loading / error message text (white on dark) */
}

/* Navy chrome: the side nav, the narrow-screen nav bar and anything else
 * wrapped in `.is-dark`. --vf-color-focus stays Vanilla's literal #9cf here
 * (9:1 on navy); nothing in the nav reads the token, and the brand blue would
 * be 2.1:1 against this background. */
.is-dark {
	--vf-color-background-default: var(--brand-navy);
	--vf-color-background-alt: #03142a;
	--vf-color-background-hover: rgba(255, 255, 255, 0.06);
	--vf-color-background-active: rgba(3, 178, 162, 0.12);
	--vf-color-text-default: #fff;
	--vf-color-text-inactive: rgba(255, 255, 255, 0.82);
	--vf-color-text-muted: rgba(255, 255, 255, 0.55);
	--vf-color-border-default: rgba(255, 255, 255, 0.12);
}

/* PDF.js embed container: PDFViewer from pdf_viewer.mjs requires a positioned
 * ancestor with bounded height and its own overflow. The component wrapper sets
 * position:relative + bounded height; this rule makes the inner container fill it. */
.pdfjs-embed-container {
	position: absolute;
	inset: 0;
	overflow: auto;
	outline: none;
	/* Keep one- and two-finger scrolling native; only browser pinch-zoom is
	 * taken over (pdf-viewer.js zooms the document instead). */
	touch-action: pan-x pan-y;
}

/* Focus ring: --brand-blue is 8.29:1 on white. It must clear WCAG 2.1
 * SC 1.4.11 (3:1 for non-text indicators); the brand teal is 2.66:1 and
 * must never be used here. */
.pdfjs-embed-container:focus-visible {
	box-shadow: inset 0 0 0 2px var(--brand-blue, #06c);
}

/* PDFViewer component chrome (component/pdf_viewer.templ). The container height
 * is data-driven (data-viewer-height) and set through CSSOM in pdf-viewer.js init()
 * and is intentionally NOT a style attribute, so the CSP needs no
 * style-src-attr allowance. */
.pdf-viewer {
	border: 1px solid #d9d9d9;
	display: flex;
	flex-direction: column;
}

.pdf-viewer__bar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: var(--space-xs) var(--space-sm);
	background: var(--pdf-ctrl-bg);
	border-bottom: 1px solid #d9d9d9;
	flex-shrink: 0;
	/* Ten controls do not fit a phone or the admin aside on one row. */
	flex-wrap: wrap;
	row-gap: var(--space-xs);
}

.pdf-viewer__group {
	display: flex;
	align-items: center;
	gap: var(--space-xs);
}

/* Breathing room between page navigation and zoom when both share a row. */
.pdf-viewer__group + .pdf-viewer__group {
	margin-left: var(--space-md);
}

/* Tooltip wrappers as flex items: an inline span would add the whitespace
 * around its button as width (~8px per control), enough to wrap the bar in the
 * admin aside. */
.pdf-viewer__bar .p-tooltip {
	display: inline-flex;
}

/* Actions (download, full screen) stay on the right even when the bar wraps.
 * Declared after the sibling rule above: same specificity, last one wins. */
.pdf-viewer__group:last-child {
	margin-left: auto;
}

.pdf-viewer__pageinfo {
	display: flex;
	align-items: center;
	gap: var(--space-xs);
	margin-left: var(--space-xs);
}

.pdf-viewer__page-input {
	width: 3.5rem;
	/* Vanilla's shared text-input rule sets min-width: 8em, which wins over width;
	 * its 1.5rem line-height plus padding would make the input 8px taller than
	 * the is-small buttons beside it. */
	min-width: 0;
	height: 1.5rem;
	margin-bottom: 0;
	padding: 0 0.3rem;
}

/* Zoom readout is a button (click = fit to width). Fixed width + tabular digits
 * keep the bar from shifting as the percentage changes. */
.pdf-viewer__zoom-level {
	min-width: 3.5rem;
	font-variant-numeric: tabular-nums;
}

/* Expanded ("full screen") state: an in-page overlay, not the Fullscreen API,
 * which iPhone Safari does not offer for elements. z-index sits above Vanilla's
 * .l-navigation (103) and .l-status (102) but below .p-modal (150) so the
 * confirm modal still wins. height needs !important because init() writes the
 * inline height through CSSOM. */
.pdf-viewer--expanded {
	position: fixed;
	/* Inset over a dimmed backdrop so the overlay reads as a layer above the
	 * page, not as browser chrome; the spread shadow is the backdrop. */
	inset: var(--space-lg);
	z-index: 140;
	height: auto !important;
	border: 0;
	border-radius: var(--radius);
	overflow: hidden;
	box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.6);
}

@media (max-width: 619px) {
	.pdf-viewer--expanded { inset: var(--space-sm); }
}

/* The admin/operator asides are grid items with z-index 101, a stacking
 * context, inside which the overlay's 140 never meets the side nav's 103. Lift
 * the aside itself while it holds an expanded viewer. */
.l-aside:has(.pdf-viewer--expanded) {
	z-index: 140;
}

/* Reaching the end of the document must not scroll the page underneath. */
.pdf-viewer--expanded .pdfjs-embed-container {
	overscroll-behavior: contain;
}

/* Stage: the positioned, bounded ancestor PDFViewer scrolls inside. */
.pdf-viewer__stage {
	flex: 1;
	position: relative;
	background: var(--pdf-bg);
	min-height: 0;
}

/* Loading / error overlays painted over the stage. The loading overlay is
 * click-through (--loading) so it never traps interaction while the document
 * streams in; the error overlay keeps default pointer events so its message
 * stays selectable. */
.pdf-viewer__overlay {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--pdf-bg);
	color: var(--pdf-text);
}

.pdf-viewer__overlay--loading { pointer-events: none; }

/* No amber reaches 3:1 on the dark stage (Vanilla's caution orange is 2.3:1
 * there), so the error line is weight, not colour; the message carries it. */
.pdf-viewer__error-text { color: var(--pdf-text); font-weight: 500; }

/* Alpine.js x-cloak: hide elements until Alpine has initialised so users
 * never see the FOUC of every x-show branch rendered at once. */
[x-cloak] { display: none !important; }

/* Global confirm modal visibility. Vanilla's `.p-modal` is display:flex; the id
 * selector (higher specificity) keeps it hidden until confirm-modal.js adds the
 * `is-open` class. Class-based rather than an inline `style="display:none"` so
 * the signing portal's CSP needs no style-src-attr allowance. */
#confirm-modal { display: none; }
#confirm-modal.is-open { display: flex; }

/* htmx filter indicator: visible only while the associated form has an
 * in-flight request. Pair with `hx-indicator="#some-id"` on the form and
 * `class="filter-indicator p-icon--status-in-progress u-animation--spin"` on
 * the indicator span so htmx toggles .htmx-request on it during the request.
 */
.filter-indicator {
	display: none;
}

.htmx-request.filter-indicator {
	display: inline-block;
	vertical-align: middle;
}

/* Admin page-header bottom rule: matches the mockup's bordered title band
 * (mockups/pages/dashboard.html .mockup-page-header) so the title + actions
 * strip is visually separated from the page content below.
 *
 * Vanilla ships `.p-panel__header{display:flex}` with no wrap, so a long title
 * (such as the Finnish "Allekirjoituspyynnöt") squeezes the action cluster into a
 * narrow column, and the buttons then wrap *inside* that column, stacking and
 * colliding with the title. Allow the header to wrap so the actions drop onto
 * their own row instead of being compressed. */
.l-main > .p-panel > .p-panel__header {
	border-bottom: 1px solid var(--vf-color-border-default, #e5e5e5);
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-sm);
}

/* The action cluster in a page header or an aside header. Vanilla only sets
 * `margin-left:auto`, so the buttons are plain inline flow with no gap control.
 * Make it a real flex row and zero the buttons' default bottom margin (a header
 * is not a form footer).
 *
 * Deliberately NOT a bare `.p-panel__controls` rule: the nav drawers reuse that
 * class for the "Menu" toggle (admin_nav / operator_nav), which must keep its
 * own layout. */
.l-main > .p-panel > .p-panel__header > .p-panel__controls,
.l-aside .p-panel__header > .p-panel__controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-sm);
}

.l-main > .p-panel > .p-panel__header > .p-panel__controls > *,
.l-aside .p-panel__header > .p-panel__controls > * {
	margin-bottom: 0;
}

@media (max-width: 619px) {
	/* Title and actions share one row whenever both fit: the title takes the
	 * room left and the actions keep their natural width at the right. A title
	 * too long for that still wraps the whole cluster onto a right-aligned row of
	 * its own, so the buttons are never squeezed into a column. The title steps
	 * down to 1.25rem so the widest case in the portals, a draft request's four
	 * actions beside "Allekirjoituspyyntö", still fits one 375px row. The
	 * header's block padding stands in for the heading's top padding and bottom
	 * margin, which would otherwise centre the buttons below the title's text. */
	.l-main > .p-panel > .p-panel__header {
		padding-top: var(--space-md);
		padding-bottom: var(--space-md);
	}
	.l-main > .p-panel > .p-panel__header > .p-heading--3 {
		flex: 1 1 auto;
		min-width: 0;
		margin-bottom: 0;
		padding-top: 0;
		font-size: 1.25rem;
		line-height: 1.75rem;
	}
	.l-main > .p-panel > .p-panel__header > .p-panel__controls {
		margin-left: auto;
		padding-top: 0;
	}
}

/* Clickable dashboard stat cards: the card <div> stays as the outer box,
 * the <a> wraps only the inner content so hover/focus work consistently
 * across browsers. No :has() required. */
.p-data-spotlight__block a {
	display: block;
	text-decoration: none;
	color: inherit;
}

.p-data-spotlight__block a:hover,
.p-data-spotlight__block a:focus-visible {
	background: var(--brand-tint);
	outline: none;
}

/* Circular icon-only action button: every control in a table's action column
 * and every control in a main-panel header. Vanilla has no icon-button
 * variant, so this pairs with p-button--base / --positive / --negative
 * (colour) and a p-tooltip wrapper (hover label) through component.IconAction.
 * ~32px hit area in both places, so a header control and a row control are the
 * same target; on touch a tap fires the action and the aria-label is the
 * accessible name (the tooltip is a hover-only extra). */
.icon-button {
	width: 2rem;
	height: 2rem;
	min-width: 2rem;
	min-height: 2rem;
	padding: 0;
	margin: 0;
	line-height: 1;
	border-radius: 50%;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex-shrink: 0;
}

.icon-button > [class*="p-icon"] {
	margin: 0;
}

/* The neutral (base) icon button has no resting border of its own, so give it
 * a visible circular outline at rest and keep the same colour on hover/focus
 * (Vanilla would otherwise change it on hover). */
.p-button--base.icon-button,
.p-button--base.icon-button:hover,
.p-button--base.icon-button:focus-visible {
	border-color: var(--vf-color-border-high-contrast, #666);
}

/* A blocked icon button (component.IconAction, Disabled: true). It is marked
 * aria-disabled rather than disabled so it stays focusable and the tooltip
 * carrying the block reason is reachable, which also means no Vanilla
 * :disabled rule reaches it: every state has to be written out. The look is
 * the disabled `.signer-grid > .p-button--negative` further down, for the
 * reason that block gives. Keep this after the base-border rule above: it ties
 * that rule's specificity and wins on source order. The focus outline is
 * untouched, the control being focusable on purpose. */
.icon-button[aria-disabled="true"],
.icon-button[aria-disabled="true"]:hover,
.icon-button[aria-disabled="true"]:active,
.icon-button[aria-disabled="true"]:focus-visible {
	cursor: not-allowed;
	opacity: 1;
	background-color: transparent;
	border-color: var(--vf-color-border-default);
}

.icon-button[aria-disabled="true"] > [class*="p-icon"] {
	opacity: 0.55;
}

/* Icon buttons are circular. The portal-wide 4px `--radius` on `.p-button*`
 * (rectangular surfaces) would otherwise square them; re-assert the circle with
 * a two-class selector so it wins regardless of rule order. Per the --radius
 * token contract, pill/circular elements keep 50%. */
.p-button.icon-button,
.p-button--base.icon-button,
.p-button--positive.icon-button,
.p-button--negative.icon-button {
	border-radius: 50%;
}

/* Row-action tooltips are repositioned to position:fixed by tooltip.js (so they
 * escape every overflow ancestor and never clip on short rows). Its arrow would
 * misalign once the bubble is detached/flipped, so hide it for icon-button
 * tooltips. */
.icon-button + .p-tooltip__message::before,
.icon-button + .p-tooltip__message::after,
.pdf-viewer__bar .p-tooltip__message::before,
.pdf-viewer__bar .p-tooltip__message::after {
	display: none;
}

/* No tooltips on a touch-only device. A tap leaves Vanilla's :hover (and the
 * button's focus, which tooltip.js follows) on the trigger until the next tap
 * elsewhere, so the bubble stayed over whatever the tap opened, an aside's
 * heading included. The trigger's aria-label still names it. !important beats
 * the inline display tooltip.js sets. */
@media (hover: none) {
	.p-tooltip > .p-tooltip__message { display: none !important; }
}

/* Defensive no-JS fallback: if tooltip.js hasn't run, open up the MAIN content
 * panel so the CSS-positioned (absolute) bubble isn't clipped at the panel edge.
 * The page still scrolls through `.l-main`; aside panels (`.l-aside .p-panel__content`,
 * which rely on their own overflow for the PDF viewer) are untouched. */
.l-main .p-panel__content {
	overflow: visible;
}

/* Make the content panel fill the viewport. Vanilla ships `.p-panel{min-height:100%}`,
 * but a percentage min-height only resolves when the parent has a definite height,
 * which the grid-item `.l-main` does not reliably provide across breakpoints (it
 * fails on the mobile shell), so on short pages the panel collapsed to content
 * height and the bare area below it showed through. A flex column with the panel as
 * a grow-only child fills the height without depending on percentage resolution:
 * short content stretches the (white) panel to the viewport bottom; tall content
 * keeps its natural height and `.l-main` scrolls.
 *
 * `position: relative` makes `.l-main` the containing block of its absolutely
 * positioned descendants. Without it, an offset-less one such as Vanilla's
 * `.u-off-screen` (pagination's hidden "Next page") resolves against the page,
 * escapes `.l-application`'s `overflow: hidden` at its spot below a long list,
 * and makes the document scroll, sliding the whole shell up over blank page. */
.l-main {
	display: flex;
	flex-direction: column;
	position: relative;
}

.l-main > .p-panel {
	flex: 1 0 auto;
}

/* The same problem one layout over, in both side navs. The account + language +
 * log-out block is a `.p-panel__footer`, which Vanilla gives no rule of its
 * own, inside a `.p-panel` that is not a flex column, so it sat directly under
 * the last nav item rather than at the drawer's bottom: 246px of empty drawer
 * below it for a viewer (8 items), 118px for a tenant_admin (12) at 1440x900.
 * On a viewport short enough for the list to outgrow the drawer it fell off the
 * bottom entirely, taking Log out with it, with no way to scroll it back:
 * `.l-application` clips and `.p-panel__content` is `overflow: hidden`.
 *
 * `height` rather than Vanilla's `min-height: 100%`: a minimum lets the panel
 * grow past the drawer, which is the overflow half of the bug. Pinned to the
 * drawer's height, the list takes the free space and scrolls once it runs out,
 * so the footer stays at the bottom and reachable in both states. */
.l-navigation__drawer .p-panel {
	display: flex;
	flex-direction: column;
	height: 100%;
}

.l-navigation__drawer .p-panel__content {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
}

/* Once the list scrolls, its last visible row is clipped mid-glyph directly
 * above the account block, which reads as two overlapping lines of text. The
 * rule turns that into a boundary. Both navs are `is-dark`, where the border
 * token resolves to a white alpha. */
.l-navigation__drawer .p-panel__footer {
	border-top: 1px solid var(--vf-color-border-default);
}

/* The narrow-screen nav bar's toggle: the hamburger glyph beside the word,
 * so it reads as the menu rather than as a stray label. */
.nav-toggle { display: inline-flex; align-items: center; gap: var(--space-sm); }

/* Below the mobile breakpoint the drawer is a full-height sheet, and the pinned
 * footer above covered the end of the list whenever a phone's viewport, less
 * the browser's own bars, was shorter than the menu: the operator nav's last
 * item sat behind the account block with nothing to say the list scrolled.
 * On a phone the whole sheet scrolls as one instead: header (sticky), every
 * nav item, then the account block, so no item can be hidden behind it at any
 * height. Desktop keeps the pinned footer. The tighter group spacing keeps the
 * full list on the first screen where it fits. */
@media (max-width: 619px) {
	.l-navigation__drawer { height: 100%; overflow-y: auto; }
	.l-navigation__drawer .p-panel { height: auto; min-height: 100%; }
	.l-navigation__drawer .p-panel__content { flex: none; overflow-y: visible; }
	.l-navigation__drawer .p-side-navigation__list { margin-bottom: var(--space-sm); }
}

/* Responsive signer grid: 6 columns on desktop, stacked below Vanilla's
 * 620px mobile breakpoint. The wizard's Step 3 (signers) and the template
 * form use it. When the grid stacks, the per-row visually-hidden labels
 * serve screen readers. */
.signer-grid {
	display: grid;
	/* Country + language are wider than the English labels need because the
	   Finnish/Swedish options ("Mikä tahansa", "Sama kuin asiakirja") fill the
	   box; the larger column-gap keeps the two select boxes visibly separated. */
	grid-template-columns: 2rem 1fr 1fr minmax(7.5rem, 10rem) minmax(7.5rem, 10rem) 2rem;
	gap: var(--space-md);
	align-items: center;
}

.signer-grid--header {
	margin-bottom: var(--space-sm);
}

@media (max-width: 619px) {
	.signer-grid--header { display: none; }
	.signer-grid { grid-template-columns: 1fr; }
	/* The single-column stack stretches every cell; keep the remove control
	   its natural size at the row's end instead of a full-width negative
	   bar (wizard Step 3 and the template form share this grid). No min-width:
	   the control is a circular .icon-button and a floor would oval it. */
	.signer-grid > .p-button--negative { justify-self: end; }
	/* The column header is hidden here, so the per-row screen-reader labels
	   become visible ones: stacked, two bare selects ("Suomi" and "Same as
	   document") gave no hint which was the eID country and which the language.
	   The negative margin keeps each label nearer its own control than the
	   previous one. */
	.signer-grid > label.u-off-screen:not(:focus):not(:active) {
		position: static;
		width: auto;
		height: auto;
		overflow: visible;
		clip-path: none;
		white-space: normal;
		margin-bottom: calc(-1 * var(--space-sm));
	}
}

/* The remove button of the last remaining signer is disabled. Vanilla renders a
   disabled button at one-third opacity, which leaves the negative variant's white
   glyph on faded red and quick-send's base button nearly blank. Keep it legible:
   a neutral outline with a muted dark glyph (the templ swaps the glyph off
   is-light while disabled). The hover rule repeats Vanilla's own disabled:hover
   selector so the red fill never returns on hover. */
.signer-grid > .p-button--negative:disabled,
.signer-grid > .p-button--negative:disabled:hover,
.quick-send__signer-remove:disabled,
.quick-send__signer-remove:disabled:hover {
	opacity: 1;
	background-color: transparent;
	border-color: var(--vf-color-border-default);
}
.signer-grid > .p-button--negative:disabled > .p-icon--delete,
.quick-send__signer-remove:disabled > .p-icon--delete { opacity: 0.55; }

/* Operator tenant combobox results (TenantCombobox). The panel had no height
 * cap, so a one-letter query's 25 matches made a 1300px panel over the whole
 * filter card; it scrolls in its own box instead. Results are full-width rows
 * like the signer typeahead's, wrapped so a long tenant name stays inside the
 * panel (as pills they were each a different width, and a long one ran out
 * of its pill and widened the page). */
.tenant-search__panel { max-height: 20rem; overflow-y: auto; }
/* Vanilla pads the panel through a two-class selector; rows carry their own. */
.tenant-search .tenant-search__panel { padding: 0; }
.tenant-search__panel .p-contextual-menu__link { white-space: normal; overflow-wrap: anywhere; }

/* Search dropdown rows, tenant combobox and signer typeahead alike: the lists
 * are p-list--divided so neighbouring results read as separate choices, and
 * each row gets a touch-height block padding (Vanilla's menu link is 0.25rem). */
.tenant-search__panel .p-contextual-menu__link,
.signer-grid > .p-contextual-menu__dropdown .p-contextual-menu__link,
.quick-send__signer-row > .p-contextual-menu__dropdown .p-contextual-menu__link {
	padding-top: var(--space-sm);
	padding-bottom: var(--space-sm);
}
/* The open panel wears the focused field's ring (the focus outline: 3px of
 * --brand-blue, inset, on --radius corners), so field and list read as one
 * control rather than a list floating over whatever lies beneath it. A border,
 * not an inset shadow, which the rows' hover fill would paint over.
 * overflow: hidden keeps that fill inside the rounded corners. */
.tenant-search__panel,
.signer-grid > .p-contextual-menu__dropdown,
.quick-send__signer-row > .p-contextual-menu__dropdown {
	border: 3px solid var(--brand-blue);
	border-radius: var(--radius);
}
.signer-grid > .p-contextual-menu__dropdown,
.quick-send__signer-row > .p-contextual-menu__dropdown { overflow: hidden; }

/* The tenant combobox field. Vanilla draws its underline on the
 * search-and-filter wrapper, and the inner search input added a second, inset
 * one plus the container's 8px left padding, so the field neither lined up
 * with the selects around it nor with its own results, and Vanilla's grey
 * search fill set it apart from the white fields beside it. One underline,
 * no fill, text on the selects' inset. */
.tenant-search .p-search-and-filter__search-container { padding-left: 0; }
.tenant-search .p-search-and-filter__search-query { border-bottom-color: transparent; margin-bottom: 0; background-color: transparent; }
/* The focus ring goes round the whole field, clear button included, so it is
 * as wide as the results panel under it; on the inner input it stopped short
 * of the clear button. */
.tenant-search .p-search-and-filter__search-container:focus-within {
	outline: 3px solid var(--brand-blue);
	outline-offset: -3px;
	border-radius: var(--radius);
}
.tenant-search .p-search-and-filter__search-query:focus { outline: none; }

/* Signer typeahead: the suggestion panel under a signer row. Vanilla anchors
   the contextual-menu dropdown to a toggle in flow; a signer row has none, so
   the row is the anchor. An absolutely positioned grid child is placed against
   its grid area, so the wizard/template panel spans columns 2 to the end (the
   inputs through the remove button, never the index column); quick-send's row
   is the name+email pair, so it spans that. The operator tenant filter uses
   Vanilla's search-and-filter panel instead, which needs its own input chrome
   and cannot wrap two grid cells. Below 620px the grid stacks to one column,
   where this anchor would be under the whole stack, so anchorSuggestPanel
   (alpine-components.js) sets top to just under the field being typed in. */
.signer-grid,
.quick-send__signer-row { position: relative; }
.signer-grid > .p-contextual-menu__dropdown,
.quick-send__signer-row > .p-contextual-menu__dropdown {
	top: calc(100% + var(--space-xs));
	left: 0;
	right: 0;
	width: auto;
	max-width: none;
}
.signer-grid > .p-contextual-menu__dropdown { grid-column: 2 / -1; }
/* Vanilla's menu link is nowrap + ellipsis for one-line items; the option's
   second line (email, country, language, capacity) must stay readable. */
.signer-grid > .p-contextual-menu__dropdown .p-contextual-menu__link,
.quick-send__signer-row > .p-contextual-menu__dropdown .p-contextual-menu__link { white-space: normal; }
@media (max-width: 619px) {
	.signer-grid > .p-contextual-menu__dropdown { grid-column: 1 / -1; }
}
/* Vanilla's card is overflow:auto, so a panel opened on the last signer row
   would scroll inside the card instead of overlaying the card footer.
   flow-root keeps the block formatting context the overflow gave the card. */
.p-card:has(.signer-grid, .quick-send__signer-row) { overflow: visible; display: flow-root; }

/* Signing capacity: sub-row below each signer in the wizard Step 3 grid.
 * Contains capacity chips, a compact "Add capacity" trigger, and an inline
 * form panel for adding new capacities. */
.signer-capacity {
	margin: var(--space-xs) 0 0 2.5rem;
}

/* Quick-send has no leading index column (unlike the wizard's signer grid),
   so its capacity block sits flush-left rather than indented 2.5rem. */
.signer-capacity--flush {
	margin-left: 0;
}

/* Chips + the "personal (default)" hint + the Add-capacity trigger share one
   wrapping row, so the control is compact with or without capacities. */
.signer-capacity__inline {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-xs);
}

/* Vanilla gives buttons and small-text blocks a bottom margin; in this centered
   row that shifts their visible centre off the chips. Zero every item's margin
   so the chips, the "personal (default)" hint, and the button all line up.
   !important beats Vanilla's two-class `.is-small.p-button` margin rule. */
.signer-capacity__inline > * {
	margin-bottom: 0 !important;
}

.signer-capacity__form {
	border: 1px solid var(--vf-color-border-default);
	border-radius: var(--radius);
	padding: var(--space-sm) var(--space-sm) var(--space-xs);
	margin-top: var(--space-xs);
	background: var(--brand-tint);
}

.signer-capacity__form .p-form__group {
	margin-bottom: var(--space-xs);
}

/* Collapse the stacked label/control gap so the inline capacity form stays
   compact (it is a rarely-used, secondary affordance). */
.signer-capacity__form .p-form__label {
	margin-bottom: 0;
	padding-bottom: 0;
}

.signer-capacity__form input,
.signer-capacity__form select {
	margin-bottom: 0;
	min-height: 0;
	padding-top: 0.2rem;
	padding-bottom: 0.2rem;
	font-size: 0.875rem;
}

.signer-capacity__form-actions {
	display: flex;
	gap: var(--space-sm);
}

/* Desktop (>=620px): the three capacity fields (Type / Org name / Business ID)
   sit inline on one row; below that they stack (default block flow). The
   representative wrapper uses display:contents so its two fields become direct
   flex items alongside Type. */
@media (min-width: 620px) {
	.signer-capacity__form {
		display: flex;
		flex-wrap: wrap;
		align-items: flex-end;
		gap: var(--space-sm);
	}
	.signer-capacity__rep {
		display: contents;
	}
	.signer-capacity__form > .p-form__group,
	.signer-capacity__rep > .p-form__group {
		flex: 1 1 8rem;
		margin-bottom: 0;
	}
	.signer-capacity__form > .p-form-validation,
	.signer-capacity__form-actions {
		flex-basis: 100%;
	}
}

/* Responsive filter row: wraps on narrow widths on desktop. Below
 * Vanilla's 620px mobile breakpoint the fields collapse behind a "Filters"
 * toggle button (component.FilterBar / filterToggle) and, when expanded, lay
 * out as a compact 2-column grid instead of a full-width vertical stack.
 * Used by every admin + operator list-page filter form. */
.filter-row {
	display: flex;
	gap: var(--space-lg);
	flex-wrap: wrap;
	align-items: flex-end;
	margin: var(--space-lg) 0;
	padding: var(--space-lg);
	border: 1px solid var(--vf-color-border-low-contrast, #d9d9d9);
	border-radius: var(--radius);
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* Mobile "Filters" toggle bar: hidden on desktop, shown <620px. Holds a
 * right-aligned, icon-only .icon-button (Vanilla p-tooltip) that reveals/hides
 * the filter fields; desktop renders the fields inline instead. */
.filter-toggle-bar { display: none; }

@media (max-width: 619px) {
	.filter-toggle-bar {
		display: flex;
		justify-content: flex-end;
		margin-top: var(--space-md);
		margin-bottom: var(--space-lg);
	}
	/* Collapsed = hidden ONLY on mobile. On desktop the class is inert, so a
	 * statically-collapsed region still renders inline >=620px. */
	.filter-row.is-collapsed {
		display: none;
	}
	/* Expanded: compact 2-column grid. The raised card chrome (border, padding,
	 * radius, shadow) is inherited from the base .filter-row rule.
	 * minmax(0,1fr) + min-width:0 below neutralise the u-min-width-* utilities so
	 * two cells never overflow a ~375px viewport. */
	.filter-row {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--space-md);
		align-items: end;
	}
	.filter-row .p-form__group {
		margin-bottom: 0;
		min-width: 0;
	}
	/* Span the action cluster full-width so buttons get their own row. It is
	 * either a .filter-actions div (Filter/Reset/spinner) or a trailing
	 * .u-align-self--end group (Apply/spinner); target both explicitly rather
	 * than :last-child, so a field appended after the actions can't be
	 * accidentally full-widthed. */
	.filter-row > .filter-actions,
	.filter-row > .u-align-self--end {
		grid-column: 1 / -1;
	}
	/* The list search spans the first row: in one column its label, the longest
	 * in the bar, wraps to three lines. */
	.filter-row > .filter-search {
		grid-column: 1 / -1;
	}
	/* So does every field but a date: half of a 375px card clipped a text
	 * field's placeholder and wrapped its label, and a select pairing with the
	 * first of a from/to date range stranded the second on a row of its own.
	 * Dates keep the two columns, which is the from/to pairing. */
	.filter-row > .p-form__group:not(:has(input[type="date"])) {
		grid-column: 1 / -1;
	}
}

/* ─── Public signing portal ─────────────────────────────────────────────────
 * Tenant strip, breadcrumb stepper, trust strip, eID provider tiles,
 * receipt-style completion card. Used only by templates under
 * internal/interfaces/http/views/page/signing/. Admin / registration are unaffected. */

/* Ground for every public page (signing, registration, verify, legal): white,
 * against which .signing-topbar and .signing-footer read as navy chrome and
 * .trust-strip as the one tinted band. No layout uses Vanilla's `.is-paper`. */
.signing-body {
	background: var(--vf-color-background-default);
}

/* Tenant brand strip at the top of every signing-portal page. */
.signing-topbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: var(--space-md) 0;
	background: var(--brand-navy);
	color: #fff;
	/* Scoped so the tagline's `u-text--muted` and the secure badge recolour
	 * with the strip instead of each needing its own rule. */
	--vf-color-text-default: #fff;
	--vf-color-text-muted: rgba(255, 255, 255, 0.6);
}

/* Inner flex row of the brand strip: tenant brand on the left, secure-session
 * badge on the right. The .u-fixed-width wrapper is the topbar's single flex
 * child, so the layout row lives one level in. */
.signing-topbar__row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	width: 100%;
}

.signing-topbar__brand {
	display: flex;
	align-items: center;
	gap: var(--space-md);
}

/* The platform mark on the platform topbar, sized like the tenant initials
 * tile it stands in for. The mark is matted, so it sits on the navy as is. */
.signing-topbar__logo {
	width: 2rem;
	height: 2rem;
	flex-shrink: 0;
}

.signing-topbar__initials {
	width: 2rem;
	height: 2rem;
	border-radius: var(--radius);
	background: linear-gradient(160deg, var(--brand-teal), var(--brand-blue));
	color: #fff;
	font-weight: 600;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex-shrink: 0;
}

.signing-topbar__secure {
	color: rgba(255, 255, 255, 0.6);
	font-size: 0.875rem;
}

/* Right-hand group: secure badge + language trigger, kept in one flex row so
 * they collapse gracefully on mobile. */
.signing-topbar__right {
	display: flex;
	align-items: center;
	gap: var(--space-lg);
}

/* Language trigger in the topbar: always visible, all breakpoints. */
.signing-topbar__right .locale-trigger {
	font-size: 0.875rem;
	color: #fff;
	white-space: nowrap;
	margin-bottom: 0;
}

@media (max-width: 619px) {
	.signing-topbar__secure { display: none; }
}

/* 4-step breadcrumb stepper. Vanilla's p-breadcrumbs is the structural base;
 * we add `aria-current="page"` styling and a muted state for upcoming steps.
 * The visited (clickable) link styling is whatever Vanilla applies by default. */
.signing-stepper { margin: 0; }
.signing-stepper .p-breadcrumbs__items {
	display: flex;
	gap: var(--space-md);
	flex-wrap: wrap;
	margin: 0;
	padding: 0;
	list-style: none;
}
.signing-stepper .p-breadcrumbs__item[aria-current="page"] { font-weight: 500; }
.signing-stepper .p-breadcrumbs__item.is-upcoming { color: var(--vf-color-text-muted); }
.signing-stepper__compact { display: none; margin: 0; padding: 0; font-weight: 500; }

/* Phones: the four steps wrap onto two lines and push the first content down.
 * One compact line ("Step 1 of 4: Review") replaces the list. */
@media (max-width: 619px) {
	.signing-stepper.p-strip { padding-top: var(--space-md); padding-bottom: 0; }
	.signing-stepper .p-breadcrumbs__items { display: none; }
	.signing-stepper__compact { display: block; }
}

/* Trust strip: 4 inline trust signals (eIDAS, RFC 3161, PAdES, GDPR) below
 * the landing hero. Wraps to multi-line on narrow widths. */
.trust-strip {
	background: var(--brand-tint);
	padding: var(--space-xl) var(--space-lg);
}

.trust-strip ul {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--space-2xl);
	margin: 0;
	padding: 0;
	list-style: none;
}

.trust-strip li {
	display: inline-flex;
	align-items: center;
	gap: var(--space-sm);
}

.trust-strip strong { font-weight: 500; }

/* Stacked, the centred flex rows would each centre at their own width and the
 * left edges go ragged; a column with the reference tag on a shared right
 * edge keeps the strip scannable. 619px per the mobile convention above. */
@media (max-width: 619px) {
	.trust-strip ul {
		flex-direction: column;
		align-items: stretch;
		gap: var(--space-md);
	}

	.trust-strip li > span:last-child {
		margin-left: auto;
	}
}

/* eID provider selectable tile. Uses :has() so the entire <label> reflects
 * the radio-input state without per-tile JS. */
.eid-card {
	display: block;
	cursor: pointer;
	padding: var(--space-xl);
	border: 2px solid var(--vf-color-border-low-contrast);
	border-radius: var(--radius);
	transition: border-color 0.15s, box-shadow 0.15s;
	background: #fff;
	text-decoration: none;
	color: inherit;
	height: 100%;
	width: 100%;
}

.eid-card:hover {
	border-color: var(--brand-blue);
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.eid-card:has(input:checked) {
	border-color: var(--brand-blue);
	background: var(--brand-tint);
}

.eid-card__head {
	display: flex;
	align-items: center;
	gap: var(--space-md);
	margin-bottom: var(--space-md);
}

.eid-card__logo {
	width: 3rem;
	height: 3rem;
	border-radius: var(--radius);
	flex-shrink: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	font-weight: 700;
	font-size: 0.875rem;
	color: #fff;
}

/* Provider brand colours for the logo badge. Closed provider set, keyed by the
 * provider code so the templ tile passes a class, not an inline background. */
.eid-card__logo--ftn   { background: #003580; }
.eid-card__logo--sbid  { background: #006aa7; }
.eid-card__logo--nbid  { background: #ba0c2f; }
.eid-card__logo--mitid { background: #c8102e; }

.eid-card__meta {
	color: #666;
	font-size: 0.875rem;
}

.eid-card input[type="radio"] { margin: 0 0 0 auto; }

/* A provider an operator has switched off: still shown, so a signer can see
 * why their eID is missing, but inert. The radio is disabled in markup; this
 * only takes away the affordances the hover and selected rules add. */
.eid-card--disabled,
.eid-card--disabled:hover {
	cursor: not-allowed;
	border-color: var(--vf-color-border-low-contrast);
	box-shadow: none;
}

.eid-card--disabled .eid-card__logo {
	opacity: 0.4;
}

/* Hero CTA on the landing page: oversized so it's the obvious next step. */
.signer-cta {
	display: inline-block;
	min-width: 18rem;
	font-size: 1.125rem !important;
	padding: var(--space-lg) var(--space-2xl) !important;
}

/* Width override for the hero CTA in constrained layouts: --fill makes it span
 * its parent (the landing actions, the paired Decline/Sign forms). */
.signer-cta--fill { width: 100%; min-width: 0; }

/* Landing page. DOM order is summary, PDF, notice, actions: below 620px that
 * is the visual order (plain block flow), and the reading order puts the
 * document and the notice before the sentence that says the signer has read
 * them. From 620px up the PDF spans all rows on the left; summary and notice
 * stack on the right. The actions span all rows on the right too, at the
 * bottom (align-self: end), with a sticky bottom pin: the area is the room
 * the sticky travels in, so the button sits at the viewport bottom until the
 * area ends, and over the notice when the notice is long. */
.landing-layout {
	display: grid;
	grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
	grid-template-rows: auto auto 1fr;
	column-gap: var(--space-2xl);
}
.landing-layout__doc { grid-column: 1; grid-row: 1 / 4; }
.landing-layout__info { grid-column: 2; grid-row: 1; }
.landing-layout__notice { grid-column: 2; grid-row: 2; }
.landing-layout__actions { grid-column: 2; grid-row: 1 / 4; align-self: end; }
/* The actions share the side column with the summary and notice, so the
 * column must end at least one actions height below the notice: else an open
 * notice taller than the PDF ends the area at the notice bottom, and the
 * actions cover its last rows at every scroll position. The space is empty
 * when the PDF is the taller column. */
.landing-layout__notice { padding-bottom: 18rem; }

@media (min-width: 620px) and (max-width: 1035px) {
	.landing-layout { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); column-gap: var(--space-xl); }
}

@media (max-width: 619px) {
	.landing-layout { display: block; }
	.landing-layout__notice { padding-bottom: 0; }
	.landing-layout__doc,
	.landing-layout__notice,
	.landing-layout__actions { margin-top: var(--space-xl); }
	/* The viewer sets its height through CSSOM from data-viewer-height (90vh);
	 * on a phone 60vh leaves the notice and the pinned bar in reach. Not in
	 * full screen, which sets its own size. */
	.landing-layout__doc .pdf-viewer:not(.pdf-viewer--expanded) { height: 60vh !important; }
}

/* Receipt-style centred card used by the Identity-Verified and Complete pages. */
.receipt {
	max-width: 40rem;
	margin: 0 auto;
	padding: var(--space-3xl);
	text-align: center;
}

.receipt .p-list--divided { text-align: left; margin: var(--space-2xl) auto; max-width: 32rem; }
.receipt .p-list--divided > div {
	display: grid;
	grid-template-columns: 12rem 1fr;
	gap: var(--space-lg);
	padding: var(--space-sm) 0;
	border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.receipt .p-list--divided dt { color: #666; font-size: 0.875rem; }
.receipt .p-list--divided dd { margin: 0; }

@media (max-width: 619px) {
	.receipt { padding: var(--space-xl); }
	.receipt .p-list--divided > div {
		grid-template-columns: 1fr;
		gap: var(--space-xs);
	}
}

/* Big green medallion that anchors the receipt visually. */
.success-medal {
	width: 5rem;
	height: 5rem;
	border-radius: 50%;
	background: var(--status-positive);
	color: #fff;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	margin-bottom: var(--space-lg);
	box-shadow: 0 4px 16px rgba(14, 132, 32, 0.25);
}

/* Vanilla sizes icons by width/height (1rem), so font-size has no effect. */
.success-medal i { width: 2.5rem; height: 2.5rem; }

/* Medal colour variants for non-success terminal states. Base .success-medal is
 * green; --caution is amber (cancelled / expired / timed-out / failed) and
 * --neutral is grey (GDPR-erased). The signing-portal terminal pages use them. */
.success-medal--caution { background: var(--vf-color-border-caution); box-shadow: 0 4px 16px rgba(204, 121, 0, 0.25); }
.success-medal--neutral { background: #888; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); }

/* "Legally binding under eIDAS" hint under the receipt details. */
.receipt-verify {
	background: var(--brand-tint);
	border: 1px solid var(--brand-blue);
	border-radius: var(--radius);
	padding: var(--space-md) var(--space-lg);
	margin-top: var(--space-xl);
	text-align: left;
	display: flex;
	align-items: flex-start;
	gap: var(--space-md);
}

.receipt-verify i {
	color: var(--brand-blue);
	flex-shrink: 0;
	margin-top: var(--space-xs);
}

/* Identity-verified page action card: document title, countdown, Sign, then
 * the reassurance line and Decline. Sticky from 620px up so Sign stays in view
 * while the details column scrolls; the aside stretches to the row height,
 * which is the room the sticky travels in. */
.sign-actions {
	position: sticky;
	top: var(--space-xl);
	border: 1px solid var(--vf-color-border-default);
	border-radius: var(--radius);
	padding: var(--space-xl);
	max-width: 32rem;
}

.sign-actions__title { margin-bottom: var(--space-md); }
.sign-actions .signer-cta { margin-bottom: 0; }
/* gap, not sibling margins: the Sign form carries u-no-margin, whose
 * !important zeroes any margin set here. */
.sign-actions-bar { display: flex; flex-direction: column; gap: var(--space-md); }
.sign-actions-bar + p { margin-top: var(--space-sm); }

/* Countdown chip. Blue while there is time, amber from 2 minutes, red from 1
 * (is-warning / is-urgent toggled by eid-countdown.js). The digits and the bar
 * length carry the same information, so colour is never the only cue. */
.sign-timer {
	background: var(--brand-tint);
	color: var(--brand-blue);
	border: 1px solid var(--brand-blue);
	border-radius: var(--radius);
	padding: var(--space-sm) var(--space-md);
}

.sign-timer__row {
	display: flex;
	align-items: center;
	gap: var(--space-sm);
	margin: 0;
	padding: 0;
	max-width: none;
	font-weight: 550;
}

.sign-timer__label { flex: 1; }
.sign-timer__value { font-variant-numeric: tabular-nums; font-size: 1.25rem; }

.sign-timer__track {
	height: 4px;
	margin-top: var(--space-xs);
	border-radius: 2px;
	background: rgba(0, 0, 0, 0.1);
	overflow: hidden;
}

/* Width is written through CSSOM as transform: scaleX(ratio), which the CSP's
 * style-src 'self' allows where a style attribute would be dropped. */
.sign-timer__bar {
	height: 100%;
	background: currentColor;
	transform-origin: left center;
	transition: transform 1s linear;
}

.sign-timer.is-warning { background: #fff4e0; color: #7a4a00; border-color: var(--vf-color-border-caution); }
.sign-timer.is-urgent,
.sign-timer.is-expired { background: #fdecee; color: #c7162b; border-color: #c7162b; }

/* Phones: the card's bar (countdown, expired notice, Sign) is fixed to the
 * viewport bottom so Sign never needs a scroll; the reassurance line and
 * Decline stay in flow. The body's bottom padding is the bar's height, so the
 * bar never covers Decline or the footer; it goes on the body rather than the
 * section because the footer comes after the section. Each padding is stated
 * twice, as separate rules: :has() for pages without JS, and the body classes
 * eid-countdown.js sets for browsers without :has(). One selector list would
 * not do, because a browser drops the whole rule over one unknown selector. */
@media (max-width: 619px) {
	body:has(.eid-confirm) { padding-bottom: 8rem; }
	body.has-sign-bar { padding-bottom: 8rem; }
	body:has(.sign-timer.is-expired) { padding-bottom: 17rem; }
	body.has-sign-bar.is-sign-bar-expired { padding-bottom: 17rem; }

	.sign-actions {
		position: static;
		border: 0;
		padding: 0;
		max-width: none;
	}

	.sign-actions__title { display: none; }

	.sign-actions-bar {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 10;
		background: var(--vf-color-background-default);
		border-top: 1px solid var(--vf-color-border-default);
		box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
		padding: var(--space-md) var(--space-lg) calc(var(--space-md) + env(safe-area-inset-bottom));
	}

	.sign-actions-bar .sign-timer { padding: var(--space-xs) var(--space-md); }
	.sign-actions-bar .signer-cta { padding: var(--space-md) var(--space-lg) !important; }
}

/* Primary-action bar of the landing and eID-selection steps. From 620px up
 * it has no styles of its own: on the landing the .review-actions around it
 * is sticky, and on the eID selection the button is in the first viewport. On
 * phones it is fixed to the viewport bottom, with the look of
 * .sign-actions-bar. Each body padding is the bar height in the longest
 * locale, one rule per surface for the reason given above. */
@media (max-width: 619px) {
	body:has(.cta-bar--review) { padding-bottom: 11rem; }
	body:has(.cta-bar--eid-select) { padding-bottom: 10rem; }

	.cta-bar {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 10;
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
		background: var(--vf-color-background-default);
		border-top: 1px solid var(--vf-color-border-default);
		box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
		padding: var(--space-md) var(--space-lg) calc(var(--space-md) + env(safe-area-inset-bottom));
	}

	.cta-bar p { margin: 0; padding: 0; }

	.cta-bar .signer-cta {
		width: 100%;
		min-width: 0;
		margin-bottom: 0;
		padding: var(--space-md) var(--space-lg) !important;
	}
}

/* Landing actions (sentence, Authenticate, Decline) from 620px up: sticky to
 * the viewport bottom inside their two-row grid area (.landing-layout), so the
 * button is in view on every scroll position. The background and the top
 * border cover the notice where the two overlap. Below 620px the
 * .cta-bar inside is fixed instead, and Decline stays in the page flow. */
@media (min-width: 620px) {
	.review-actions {
		position: sticky;
		bottom: 0;
		z-index: 1;
		background: var(--vf-color-background-default);
		border-top: 1px solid var(--vf-color-border-default);
		padding: var(--space-md) 0 var(--space-sm);
	}
}

/* eID selection from 620px up: the hint sits on the button's line, after it,
 * so Back stays level with Continue and the row keeps its height when the
 * hint hides. Below 620px the bar stacks the hint above the button. */
@media (min-width: 620px) {
	.cta-bar--eid-select { display: flex; align-items: center; gap: var(--space-md); }
	.cta-bar--eid-select p { order: 1; margin: 0; padding: 0; }
}

/* Layered privacy notice: Vanilla indents an accordion panel by 3rem, which is
 * a quarter of the review aside on a tablet. Vanilla also gives the accordion
 * tab z-index 2; isolation keeps that inside the card, so the sticky landing
 * actions (z-index 1) paint over the card where the two overlap. */
.notice-disclosure { isolation: isolate; }
.notice-disclosure .p-accordion__panel { padding-left: 0; }

/* Footer strip rendered at the bottom of every signing-portal page. */
.signing-footer {
	background: var(--brand-navy);
	padding: var(--space-lg) 0;
	color: rgba(255, 255, 255, 0.82);
	font-size: 0.875rem;
}

.signing-footer p { margin: 0; text-align: center; max-width: none; }

/* Sticky-footer variant, used by every page under layout.Base. margin-top:auto
 * resolves to 0 once the page overflows, so on a long page the gap above the
 * navy strip would vanish; a transparent top border holds it in both states.
 * background-clip keeps the navy out of that border, so the gap shows the white
 * body rather than 3rem of extra chrome, and the strip keeps its own padding. */
.signing-footer.l-footer--sticky {
	margin-top: auto;
	border-top: var(--space-3xl) solid transparent;
	background-clip: padding-box;
}

/* Creation session page: the page swaps into the .l-site root of its shell,
 * so its wrapper is the flex column that the sticky footer needs. The wrapper
 * also carries the tenant theme classes, which every .tenant-theme rule keys
 * on. */
.creation-page { display: flex; flex-direction: column; flex: 1 0 auto; }
/* An embedded page takes the height of its content, not of the frame: the
 * frame height follows the page, and a 100vh floor would never let it shrink. */
.l-site.is-embedded { min-height: 0; }

/* Language modal: card-grid locale picker shared across admin, signing,
 * registration, and verify surfaces: circular language-code badge
 * (EN/FI/SV), green accent on selected card. */
.locale-modal .p-modal__dialog { max-width: 38rem; }
.locale-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); }
@media (max-width: 619px) { .locale-grid { grid-template-columns: 1fr; } }
.locale-card {
	display: flex; flex-direction: column; align-items: center; cursor: pointer;
	padding: var(--space-xl) var(--space-lg); border: 2px solid var(--vf-color-border-low-contrast);
	border-radius: var(--radius); transition: border-color 0.15s, box-shadow 0.15s;
	background: #fff; min-height: 7rem;
}
.locale-card:hover { border-color: var(--brand-blue); box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.locale-card:focus-visible { outline: 2px solid var(--brand-blue); outline-offset: 2px; }
.locale-card.is-selected { border-color: var(--brand-blue); background: var(--brand-tint); }
.locale-card__code {
	width: 3rem; height: 3rem; border-radius: 50%; background: #e5e5e5;
	display: flex; align-items: center; justify-content: center;
	font-weight: 700; font-size: 0.875rem; color: #333; text-transform: uppercase;
	margin-bottom: var(--space-sm);
}
.locale-card.is-selected .locale-card__code { background: var(--brand-blue); color: #fff; }
.locale-card__native { font-size: 1.25rem; font-weight: 500; margin-bottom: 0.125rem; }
.locale-card__english { font-size: 0.875rem; color: #666; }
/* Language trigger, on the navy topbar and inside page content alike. */
.locale-trigger { display: inline-flex; align-items: center; gap: var(--space-sm); cursor: pointer; color: inherit; font-size: inherit; }
.locale-trigger:hover { text-decoration: underline; }

/* Public /verify/{id} page: centred integrity banner, upload card, and the
 * hash-status line. Constrained narrower than the receipt so the verify result
 * reads as a focused statement. */
.verify-banner { max-width: 36rem; margin: 0 auto var(--space-xl); }
.verify-status { min-height: 1.25rem; margin: var(--space-md) 0 0; }

/* Empty state: list page with zero rows or search prompt.
 * Dashed-border card with circular icon badge, heading, body, and optional CTA.
 * Pattern documented in mockups/components.html#states. */
.empty-state {
	text-align: center;
	padding: var(--space-3xl) var(--space-lg);
	border: 1px dashed var(--vf-color-border-default);
	border-radius: var(--radius);
	background: var(--vf-color-background-default);
}

.empty-state__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 4rem;
	height: 4rem;
	border-radius: 50%;
	background: var(--brand-tint);
	margin-bottom: var(--space-lg);
}

.empty-state__icon i { width: 1.75rem; height: 1.75rem; }

.empty-state h3 { margin: 0 0 var(--space-xs); }

.empty-state p {
	margin: 0 auto;
	max-width: 28rem;
}

.empty-state p:not(:last-child) { margin-bottom: var(--space-xl); }

/* Centres the CTA slot whatever it is handed. .empty-state sets text-align:
   center, which centres an inline button but does nothing for a CTA that is
   itself a flex row; those fell to flex-start and sat hard against the left
   edge. flex-wrap so a multi-button CTA wraps instead of overflowing. */
.empty-state__cta {
	display: flex;
	justify-content: center;
	flex-wrap: wrap;
}

/* Registration plan picker (Step 1): Vanilla's p-pricing-block pattern used
   as a selectable radio group. `.plan-tier-picker` scopes the deviation from
   the stock (marketing) pattern: every tier is a bordered box so the choices
   read as cards, and the selected tier's border turns Vanilla's positive
   green (matching its "Selected" p-button--positive CTA; the theme accent
   var resolves teal, which would clash). aria-checked is bound by the Alpine
   `register` component, so no extra class toggling is needed.
   Annotated in mockups/pages/registration.html. */
.plan-tier-picker .p-pricing-block__tier {
	border: 1px solid #d9d9d9;
	border-radius: var(--radius);
	padding: var(--space-lg);
	margin-bottom: var(--space-lg);
}

.plan-tier-picker .p-pricing-block__tier[aria-checked="true"] {
	border-color: var(--brand-blue);
	/* Second ring instead of a wider border: no layout shift on select. */
	box-shadow: 0 0 0 1px var(--brand-blue);
}

/* The picker renders inside the registration <form>, and Vanilla zeroes
   list-item padding inside forms (`form .p-list--divided .p-list__item`,
   a checkbox-list affordance). That collapses the offerings list and leaves
   the ::before tick (fixed at top .75rem) riding below its text line.
   Restore the stock divided-list rhythm. */
.plan-tier-picker .p-list--divided .p-list__item {
	padding-top: 0.5rem;
	padding-bottom: 1rem;
}

/* Theme rules Vanilla's tokens cannot reach. Everything else in this file
 * inherits the palette through the --vf-color-* re-declarations on :root. */

/* Vanilla paints this rule with --vf-color-text-default, so it would be black. */
.p-rule--highlight::before {
	background-color: var(--brand-blue);
}

/* Active marker and section headings inside the navy nav. */
.is-dark [aria-current=page].p-side-navigation__link::before {
	background-color: var(--brand-teal);
}

.is-dark .p-side-navigation__heading {
	color: var(--vf-color-text-muted);
}

/* Operator console: a teal stripe under the brand block, so an operator can
 * tell the two consoles apart at a glance. Keyed on the panel that holds the
 * operator nav, so the mockup shell renders it with the same rule. */
.p-panel:has(#operator-side-nav) > .p-panel__header {
	border-bottom: 3px solid var(--brand-teal);
}

/* Blue on the black body text is 2.5:1, under the 3:1 a colour-only link cue
 * needs, and Vanilla underlines only on hover. Links in running prose carry
 * the underline at rest; links in tables, navigation and buttons are marked by
 * position and stay as they are. */
p a,
.p-notification__message a,
.legal-document a {
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.075em;
}

/* Current step, marked as the site marks its active nav link, on both
 * breadcrumb steppers: the signing shell's (aria-current="page") and the
 * request wizard's (aria-current="step"). The bold weight and the numeric
 * prefix stay the non-colour cue: teal is 2.7:1 as text. */
.signing-stepper .p-breadcrumbs__item[aria-current="page"] strong,
.p-breadcrumbs__item[aria-current="step"] strong {
	border-bottom: 2px solid var(--brand-teal);
	padding-bottom: 2px;
}

/* Vanilla pins --vf-focus-outline-color to the literal #2e96ff per component
 * group, so --vf-color-focus never reaches these. The selector groups mirror
 * Vanilla's own (bare `button` included, or a modal close or reset button
 * keeps the sky-blue ring beside brand-blue ones), making specificity equal
 * and source order decide; the `.is-dark ...` variants still win inside the
 * nav and keep #9cf there. The filled buttons are left out: Vanilla draws
 * their ring inset on the fill, where brand blue is 1.4:1 on the negative red,
 * so positive keeps its own #003008 and negative gets the dark red below. */
a, .p-button--link, .p-button, .p-button--base, button,
.p-tabs__link, .p-pagination__link, .p-pagination__link--previous, .p-pagination__link--next,
.p-side-navigation__link, .p-side-navigation__toggle, .p-side-navigation__toggle--in-drawer,
.p-contextual-menu__link, .p-accordion__tab, summary, [type=file],
.p-chip, .p-chip--information, .p-chip--positive, .p-chip--caution, .p-chip--negative,
textarea, select, [type=text], [type=email], [type=password], [type=number], [type=search],
[type=tel], [type=url], [type=date], [type=datetime-local] {
	--vf-focus-outline-color: var(--brand-blue);
}

/* 3.2:1 on the negative fill, the same idea as Vanilla's #003008 on green. */
.p-button--negative {
	--vf-focus-outline-color: #2b0509;
}

/* The navy chrome is not `.is-dark`, so a link or button placed in it (the
 * language trigger) would take the brand-blue ring above, 2.1:1 on navy.
 * White is 17.6:1. */
.signing-topbar a, .signing-topbar button, .signing-footer a {
	--vf-focus-outline-color: #fff;
}

/* ─── Tenant theme of the signing ceremony ──────────────────────────────────
 * The tenant's stored theme file (/brand/theme/{tenant}/{asset}.css, written by
 * tenant.ThemeCSS at save time) declares only --tenant-* properties. These
 * rules read them, under the body modifier classes of
 * render.TenantBrand.BodyClasses, so a page without the classes keeps the
 * platform look. Each var() falls back to the platform value: a stylesheet
 * that fails to load gives the platform look, not a transparent bar.
 *
 * The scope of each token and what stays platform colour:
 * architecture/esoppari-spec.md § 15.8. The server derives hover, active,
 * text and muted colours and checks each contrast (tenant.DerivePalette).
 *
 * The decline, secondary and link rules key on the element class only, not
 * on <main>: the confirm modal sits outside <main>, and the admin preview has
 * none. */
.tenant-theme--primary .signer-cta.p-button--positive,
.tenant-theme--primary .creation-send.p-button--positive {
	--vf-color-button-positive-default: var(--tenant-primary, #0e8420);
	--vf-color-button-positive-hover: var(--tenant-primary-hover, #0b6d1a);
	--vf-color-button-positive-active: var(--tenant-primary-active, #0a5f17);
	--vf-color-button-positive-text: var(--tenant-primary-text, #fff);
	/* Vanilla draws this ring inset on the fill, so it takes the text colour
	 * (at least 4.58:1 on the fill), as #003008 does on the platform green. */
	--vf-focus-outline-color: var(--tenant-primary-text, #003008);
}

.tenant-theme--decline .p-button--negative {
	--vf-color-button-negative-default: var(--tenant-decline, #c7162b);
	--vf-color-button-negative-hover: var(--tenant-decline-hover, #b01326);
	--vf-color-button-negative-active: var(--tenant-decline-active, #a21223);
	--vf-color-button-negative-text: var(--tenant-decline-text, #fff);
	/* The fixed #2b0509 ring above is 1.31:1 on a dark tenant red; the text
	 * colour keeps at least 4.58:1 on the fill. */
	--vf-focus-outline-color: var(--tenant-decline-text, #2b0509);
}

.tenant-theme--secondary .p-button--base,
.tenant-theme--secondary .p-button--base:visited {
	color: var(--tenant-secondary, var(--vf-color-text-default));
}

/* On the a element only: .p-checkbox__label::before, .p-link--skip and range
 * inputs also read --vf-color-link-default. */
.tenant-theme--link a:not([class*="p-button"]) {
	--vf-color-link-default: var(--tenant-link, var(--brand-blue));
	--vf-color-link-visited: var(--tenant-link, var(--brand-blue));
}

.tenant-theme--chrome .signing-topbar {
	background: var(--tenant-chrome, var(--brand-navy));
	color: var(--tenant-chrome-text, #fff);
	--vf-color-text-default: var(--tenant-chrome-text, #fff);
	--vf-color-text-muted: var(--tenant-chrome-muted, rgba(255, 255, 255, 0.6));
}

.tenant-theme--chrome .signing-topbar__secure {
	color: var(--tenant-chrome-muted, rgba(255, 255, 255, 0.6));
}

.tenant-theme--chrome .signing-topbar__right .locale-trigger {
	color: var(--tenant-chrome-text, #fff);
}

.tenant-theme--chrome .signing-topbar a,
.tenant-theme--chrome .signing-topbar button {
	--vf-focus-outline-color: var(--tenant-chrome-text, #fff);
}

/* The tenant logo in place of the initials tile. It sits on a white mat: a
 * dark logo on a transparent ground would vanish on dark chrome. The width and
 * height attributes keep the aspect ratio; CSS sets the display height. */
.signing-topbar__tenant-logo {
	height: 2rem;
	width: auto;
	max-width: 10rem;
	padding: 0.125rem 0.375rem;
	box-sizing: content-box;
	background: #fff;
	border-radius: var(--radius);
	object-fit: contain;
	flex-shrink: 0;
}

/* Vanilla paints option rows with --vf-color-background-alt, which is the
 * brand tint; Firefox and Windows browsers honour it, so a dropdown would open
 * blue. Keep the rows on the input surface. */
select option {
	background-color: var(--vf-color-background-inputs);
}

/* ─── Layout utilities ──────────────────────────────────────────────────────
 * Small, composable single-property helpers that replace inline style=
 * attributes (the CSP drops style-src-attr 'unsafe-inline'; see
 * internal/interfaces/middleware/security_headers.go). Vanilla ships no
 * spacing/flex utilities, so these complement its u-* set. Spacing uses the
 * --space-* scale on :root.
 *
 * Margin helpers are !important so they (a) override Vanilla component margins
 * and (b) compose with Vanilla's `u-no-margin` (also !important). Thus
 * `class="u-no-margin u-mb--xl"` gives `margin:0 0 1.5rem`: u-no-margin zeroes
 * all sides, u-mb--xl wins for the bottom. Compose freely:
 * `class="u-flex u-gap--sm u-flex--wrap u-mb--lg"`. */
.u-mt--xs  { margin-top: var(--space-xs) !important; }
.u-mt--sm  { margin-top: var(--space-sm) !important; }
.u-mt--md  { margin-top: var(--space-md) !important; }
.u-mt--lg  { margin-top: var(--space-lg) !important; }
.u-mt--xl  { margin-top: var(--space-xl) !important; }
.u-mt--2xl { margin-top: var(--space-2xl) !important; }
/* Mobile-only top margin: separates a 50/50 split's right column from the left
 * column once they stack below 619px (Vanilla stacks .row columns flush). */
@media (max-width: 619px) { .u-mt--xl-mobile { margin-top: var(--space-xl) !important; } }
.u-mb--xs  { margin-bottom: var(--space-xs) !important; }
.u-mb--sm  { margin-bottom: var(--space-sm) !important; }
.u-mb--md  { margin-bottom: var(--space-md) !important; }
.u-mb--lg  { margin-bottom: var(--space-lg) !important; }
.u-mb--xl  { margin-bottom: var(--space-xl) !important; }
.u-mb--2xl { margin-bottom: var(--space-2xl) !important; }
.u-ml--sm  { margin-left: var(--space-sm) !important; }
.u-ml--lg  { margin-left: var(--space-lg) !important; }
.u-mr--xs  { margin-right: var(--space-xs) !important; }
.u-mr--sm  { margin-right: var(--space-sm) !important; }
/* Vertical padding helpers. Vanilla zeroes `.p-list__item` padding inside a
 * `form`, which leaves a `p-list--divided` separator (a 1px inset box-shadow at
 * the item's top edge) flush against the rows either side of it. Padding, not
 * margin: the separator is painted on the item's own box, so margin would move
 * the line with the row instead of opening space around it. */
.u-pt--sm  { padding-top: var(--space-sm) !important; }
.u-pb--sm  { padding-bottom: var(--space-sm) !important; }

.u-flex          { display: flex; }
.u-inline-flex   { display: inline-flex; }
.u-flex--center  { align-items: center; }
.u-flex--col     { flex-direction: column; }
.u-flex--wrap    { flex-wrap: wrap; }
.u-flex--1       { flex: 1; }
.u-align-self--end { align-self: flex-end; }
.u-justify--between { justify-content: space-between; }
.u-justify--center  { justify-content: center; }
.u-justify--end     { justify-content: flex-end; }
.u-gap--xs       { gap: var(--space-xs); }
.u-gap--sm       { gap: var(--space-sm); }
.u-gap--md       { gap: var(--space-md); }
.u-gap--lg       { gap: var(--space-lg); }
.u-row-gap--lg   { row-gap: var(--space-lg); }

.u-align--baseline { align-items: baseline; }

.u-inline        { display: inline; }
.u-full-width    { width: 100%; }
.u-height-full   { height: 100%; }
.u-overflow-hidden { overflow: hidden; }
.u-clickable     { cursor: pointer; }
.u-bold          { font-weight: bold; }
.u-fs--sm        { font-size: 0.75rem; }
.u-pad--2xl      { padding: var(--space-2xl); }

/* Numeric and date controls are sized by their data, not their container.
 * :where() gives these zero specificity, so a u-maxw--N on the same element
 * still wins. */
:where(input[type="number"], input[inputmode="decimal"], input[inputmode="numeric"]) { max-width: 8rem; }
/* min-width: iOS Safari gives a date input an intrinsic minimum wider than half
 * a phone-width filter card, so the second of a from/to pair overran the card. */
:where(input[type="date"]) { max-width: 10rem; min-width: 0; }

/* Width ladder: cards take 36 to 56, controls 8 to 32. Which mechanism sizes a
 * control (a grid span, a ladder step or the type rules above) is decided in
 * internal/interfaces/http/views/CLAUDE.md, "Before creating any UI". Vanilla
 * floors every text input at min-width: 8em, so 8 is the smallest useful step. */
.u-maxw--8  { max-width: 8rem; }
.u-maxw--14 { max-width: 14rem; }
.u-maxw--18 { max-width: 18rem; }
.u-maxw--24 { max-width: 24rem; }
.u-maxw--32 { max-width: 32rem; }
.u-maxw--36 { max-width: 36rem; }
.u-maxw--40 { max-width: 40rem; }
.u-maxw--44 { max-width: 44rem; }
.u-maxw--56 { max-width: 56rem; }

/* Standalone-button comfortable minimum widths (form selects, CTAs). */
.u-min-width-10 { min-width: 10rem; }
.u-min-width-12 { min-width: 12rem; }
.u-min-width-14 { min-width: 14rem; }
.u-min-width-16 { min-width: 16rem; }
.u-min-width-24 { min-width: 24rem; }

/* Let a p-table's content size its columns. Vanilla's table is
 * `table-layout: fixed`, which gives N columns an equal 1/N however unequal
 * their content is, and its cells are `overflow: hidden; text-overflow:
 * ellipsis`: a status chip is `white-space: nowrap`, so in a locale whose label
 * is longer than the English one the chip's end is silently cut off rather than
 * wrapped ("Odottaa allekirjoituksia" clipped at 1280px, 154px of chip in a
 * 146px cell). There is no column-width mechanism in Vanilla or here to size
 * them by hand, and hard-coded percentages would have to be re-tuned per table
 * and per locale; auto layout asks the content instead and needs neither.
 * Below `p-table--mobile-card`'s own breakpoint the cells stack and this stops
 * applying, which is why it is safe on a narrow screen. */
.u-table--auto { table-layout: auto; }

/* Link that inherits surrounding text styling (clickable cards / table rows). */
.u-plain-link { text-decoration: none; color: inherit; }

/* <button> stripped to look like inline text (nav logout, link-style submit). */
.u-button-reset { border: none; background: none; padding: 0; cursor: pointer; text-align: left; color: inherit; font: inherit; }

/* Unstyled <fieldset> wrapper (no border/margin/padding), used to group radio
 * tiles without the default fieldset chrome. */
.u-fieldset-bare { margin: 0; padding: 0; border: 0; }

/* Two-column definition row used by the signing-request wizard + registration
 * review panes (label | value). One class per dt/dd row. The --plain modifier
 * drops the divider for the final row. */
/* minmax(0, 1fr) and overflow-wrap: a bare 1fr track grows to its longest
 * unbreakable value (a signer's email), which pushed the review card wider than
 * a 375px viewport. Below the mobile breakpoint the 10rem label column would
 * leave the value under half the card, so the pair stacks. */
.review-row {
	display: grid;
	grid-template-columns: 10rem minmax(0, 1fr);
	gap: var(--space-sm);
	padding: var(--space-sm) 0;
	border-bottom: 1px solid var(--vf-color-border-low-contrast, #eee);
}
.review-row dd { overflow-wrap: anywhere; }
.review-row--plain { border-bottom: 0; }
@media (max-width: 619px) {
	.review-row { grid-template-columns: minmax(0, 1fr); gap: var(--space-xs); }
}

/* Detail-page fields: a `row` of `col-*` cells, each a `p-heading--5` label over
 * its value (tenant, user, invoice and request details, billing). Vanilla's row
 * has no row gap, so the label's own bottom margin was the only space: stacked
 * on a phone every value sat closer to the NEXT label than to its own, and a
 * desktop grid that wraps put its second row's labels flush under the first
 * row's values. Tighten label-to-value, space field-to-field. */
.row > [class*="col-"] > .p-heading--5:first-child { margin-bottom: var(--space-xs); }
.row > [class*="col-"]:has(> .p-heading--5:first-child) { margin-bottom: var(--space-lg); }

/* Tab strips wrap below the mobile breakpoint. Vanilla scrolls an overflowing
 * strip sideways, which on a phone (no visible scrollbar) left the last two of
 * the five Settings tabs off-screen with nothing to say they existed, and a
 * selected off-screen tab kept its underline out of view. */
@media (max-width: 619px) {
	.p-tabs__list { flex-wrap: wrap; overflow-x: visible; white-space: normal; }
}

/* Breadcrumbs (and the wizard's step trail, built on them) below the mobile
 * breakpoint: Vanilla puts the "/" before each crumb, so a trail that wraps
 * opened its second line with a bare separator. Hang it after the previous
 * crumb instead, and let a long crumb break anywhere rather than overflow. */
@media (max-width: 619px) {
	.p-breadcrumbs__item:not(:first-of-type) { text-indent: 0; }
	.p-breadcrumbs__item:not(:first-of-type)::before { content: none; }
	.p-breadcrumbs__item:not(:last-of-type)::after { content: "/"; margin: 0 0.5rem; }
	.p-breadcrumbs__item { overflow-wrap: anywhere; }
}

/* Invoice totals: a two-column table of their own under the lines table. As a
 * <tfoot> of that mobile-card table they kept four table columns below Vanilla's
 * card breakpoint (its card mode has no tfoot rule), squeezing every line card
 * into the first column. */
.invoice-totals { width: auto; min-width: 16rem; margin-left: auto; }
.invoice-totals th { text-align: right; }

/* Signing-request wizard step footers (Back / Next / Save draft / Send),
 * default-size peers. A label that wraps inside its button strands the chevron
 * on a line of its own, and a row that cannot wrap pushed Send past a 375px
 * viewport, so each label stays on one line and the row wraps instead. */
.wizard-footer { display: flex; flex-wrap: wrap; gap: var(--space-sm) var(--space-lg); }
/* margin: 0 because the gap spaces them; Vanilla's own button margins on top of
 * it wrapped a Back + Next pair that fits a phone-width card. */
.wizard-footer button { white-space: nowrap; margin: 0; }

/* Dimmed table row for an offboarded membership (operator user detail). */
.is-offboarded { opacity: 0.5; }

/* Filter-row submit/clear button group: the 0.15rem bottom padding baselines
 * the buttons with the adjacent form-field controls. Used by every admin /
 * operator list-page filter form. */
.filter-actions { display: flex; gap: var(--space-sm); align-items: center; padding-bottom: 0.15rem; }

/* Long-tail exact-value helpers: one or two call sites each, kept as utilities
 * so no inline style attribute survives. */
.u-relative       { position: relative; }
.u-z--dropdown    { z-index: 200; }
.u-no-border      { border: none; }
.u-no-shadow      { box-shadow: none; }
.u-bg--alt        { background-color: var(--vf-color-background-alt); }
.u-m--lg          { margin: var(--space-lg) !important; }
.u-mt--n-xs       { margin-top: -0.25rem !important; }
.u-mt--n-sm       { margin-top: -0.5rem !important; }
.u-fs--xs         { font-size: 0.8125rem; }
.u-fs--lg         { font-size: 1.5rem; }
.u-pad--xs-sm     { padding: var(--space-xs) var(--space-sm); }
.u-pad--sm-lg     { padding: var(--space-sm) var(--space-lg); }
.u-pad--2xl-lg    { padding: var(--space-2xl) var(--space-lg); }
.u-pad--3xl-xl    { padding: var(--space-3xl) var(--space-xl); }
/* Wizard "Signing Order Preview" divider. */
.u-divider-top    { margin-top: var(--space-xl); padding-top: var(--space-lg); border-top: 1px solid var(--vf-color-border-default); }
/* Centred hero band on the full-page error layout. */
.error-hero       { min-height: 60vh; display: flex; align-items: center; }
/* Wizard per-signer validation message, indented under the signer field. */
.wizard-signer-error { margin: var(--space-xs) 0 0 2.5rem !important; }
/* Language modal: hidden until toggled by the is-open class (same pattern as
 * confirm-modal). Display is class-gated, not inline style (CSP safe). */
#language-modal { display: none; }
#language-modal.is-open { display: flex; }
/* Quick-send landing: inherited-policy summary rail and its key/value list.
 * The compose/upload column and signer rows reuse Vanilla grid + the layout
 * utilities; only the rail panel + kv list need bespoke rules. */
.quick-send__rail   { background: var(--brand-tint); border: 1px solid var(--vf-color-border-low-contrast); border-radius: var(--radius); padding: var(--space-lg) var(--space-xl); }
.quick-send__kv     { list-style: none; margin: var(--space-sm) 0 0; padding: 0; }
.quick-send__kv li  { display: flex; justify-content: space-between; gap: var(--space-lg); padding: 0.4rem 0; border-bottom: 1px dashed var(--vf-color-border-default); font-size: 0.8125rem; }
.quick-send__kv li:last-child { border-bottom: 0; }
.quick-send__kv li span:first-child { color: var(--vf-color-text-muted, #767676); }
/* Every control in the rail, not just one: the rail states its facts at
 * 0.8125rem, and a default-size control towers over them. Scoped to the rail
 * so all four selects (the three workflow overrides behind "Adjust" and the
 * completion-delivery picker below the rule) stay the same height as each
 * other. The group picker in the compose column is outside this wrapper and
 * keeps the default size. */
.quick-send__rail .p-form__label { font-size: 0.8125rem; margin-bottom: 0.15rem; padding-bottom: 0; }
.quick-send__rail select { font-size: 0.875rem; min-height: 0; padding-top: 0.25rem; padding-bottom: 0.25rem; margin-bottom: 0; }
.quick-send__rail .p-form__group { margin-bottom: var(--space-md); }
.quick-send__rail .p-form-help-text { font-size: 0.75rem; margin-top: 0.25rem; }
/* Chosen-document card: the staged PDF a create surface currently holds, shared
 * by quick send and the wizard's Step 1. A tinted, outlined box rather than a
 * notification, because it carries actions (View / Change) and persists. */
.chosen-document    { border: 2px solid var(--brand-blue); border-radius: var(--radius); background: var(--brand-tint); padding: var(--space-md) var(--space-lg); }
/* The filename's flex:1 (a zero basis) never let the row wrap, so View and
 * Change document were squeezed to min-content (icon over a label broken
 * across two lines) on a phone and, with the Finnish labels, in the 44rem
 * wizard column on desktop too. Give the name a basis so the buttons drop
 * below it when the row runs out. */
.chosen-document { flex-wrap: wrap; }
.chosen-document > .u-flex--1 { flex-basis: 12rem; }
.chosen-document .p-button--base { white-space: nowrap; }
/* Quick-send dropzone, signer grid, and section head: match mockups/pages/quick-send-dashboard.html. */
.quick-send__section-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-lg); }
/* The template + group selects to the right of the section heading. On desktop
   they sit inline; below Vanilla's 620px breakpoint the heading row stacks and
   the selects go full-width; two 12rem-min selects on one row otherwise force
   the card past the viewport (horizontal scroll, clipped dropzone). */
.quick-send__section-controls { display: flex; gap: var(--space-sm); }
@media (max-width: 619px) {
	.quick-send__section-head { flex-direction: column; align-items: stretch; }
	.quick-send__section-controls { flex-direction: column; }
	.quick-send__section-controls .p-form__group { min-width: 0; }
	.quick-send__section-controls select { width: 100%; }
}
.quick-send__dropzone { border: 2px dashed var(--vf-color-border-default); border-radius: var(--radius); background: var(--vf-color-background-default); padding: var(--space-2xl) var(--space-xl); text-align: center; color: #666; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 220px; cursor: pointer; }
.quick-send__dropzone.is-dragover { border-color: var(--brand-blue); background: var(--brand-tint); }
/* A touch-only device cannot drag a file in: it gets "Choose a PDF" instead of
 * "Drop a PDF here or browse". The whole zone opens the picker either way. */
.quick-send__prompt-touch { display: none; }
@media (hover: none) {
	.quick-send__prompt-touch { display: block; }
	.quick-send__prompt-pointer { display: none; }
}
/* The Vanilla validation message tucks up under a normal input; after the tall
   dropzone (no bottom margin) that pulls it onto the dashed border. Push it clear. */
.p-form__group:has(> .quick-send__dropzone) .p-form-validation__message { margin: var(--space-sm) 0 0; }
/* Desktop: name|email grid takes the row, the remove button sits top-right; the
   error and capacity blocks wrap to their own full-width lines below. */
.quick-send__signer { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: start; margin-bottom: var(--space-md); }
.quick-send__signer-row { flex: 1 1 18rem; display: grid; grid-template-columns: 1.2fr 1.6fr; gap: var(--space-sm); }
/* Vanilla inputs carry a ~1.25rem bottom margin; with the row wrapping the
   capacity block onto its own line that margin became dead vertical space
   between the signer and its capacities. Zero it; the flex gap spaces them. */
.quick-send__signer-row input { margin-bottom: 0; }
.quick-send__signer-remove { flex: 0 0 auto; }
.quick-send__signer > .signer-capacity,
.quick-send__signer-error { flex-basis: 100%; }
.quick-send__rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-sm); }
/* Mobile: stack the block and push the remove button below the capacities so it
   no longer sits awkwardly between the email field and the capacity controls. */
@media (max-width: 619px) {
	.quick-send__signer { flex-direction: column; align-items: stretch; }
	/* In a column flex the shorthand basis becomes the main-axis HEIGHT, so reset
	   it so the stacked row/blocks size to their content, not 18rem/100% tall. */
	.quick-send__signer-row { flex: 0 0 auto; grid-template-columns: 1fr; }
	.quick-send__signer > .signer-capacity,
	.quick-send__signer-error { flex-basis: auto; }
	.quick-send__signer > .signer-capacity { order: 4; }
	.quick-send__signer-remove { order: 5; align-self: start; }
}

/* ---------------------------------------------------------------------------
 * Rounded corners: soften Vanilla's squared-off surfaces with the house
 * --radius token. Vanilla sets no radius on these, so plain overrides win by
 * source order (app.css loads last); no !important needed. Pill/circular
 * components (p-chip, p-badge, p-switch, p-radio, p-checkbox, tooltip pointers)
 * are deliberately excluded and keep their native radius.
 * --------------------------------------------------------------------------- */

/* Buttons (all variants used in the codebase). p-button--link is a text link
 * styled as a button; leave it flat (Vanilla resets it to 0). */
.p-button,
.p-button--base,
.p-button--positive,
.p-button--negative,
.p-button--brand {
	border-radius: var(--radius);
}

/* Cards, panels, strips */
.p-card,
.p-panel,
.p-strip.is-bordered {
	border-radius: var(--radius);
}

/* Form controls: text inputs, selects, textareas, validation wrappers.
 * (Exclude checkbox/radio; Vanilla styles those as squares/circles.) */
.p-form-validation__input,
input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="search"], input[type="tel"],
input[type="url"], input[type="date"], input[type="datetime-local"],
select, textarea {
	border-radius: var(--radius);
}

/* Notifications: the codebase uses variant-only classes (no base .p-notification),
 * and Vanilla carries the box styling on the same variant group, so match it. */
.p-notification,
.p-notification--positive,
.p-notification--negative,
.p-notification--caution,
.p-notification--information {
	border-radius: var(--radius);
}

/* Tables: round the outer frame. overflow:hidden clips cell backgrounds and
 * borders to the rounded corners. */
.p-table,
table.p-table--mobile-card {
	border-radius: var(--radius);
	overflow: hidden;
}

/* Mobile card view: below 1036px Vanilla collapses each row into a bordered
 * card (.p-table--mobile-card tbody tr -> display:block; border:1px). Round those
 * cards too. Inert on desktop where tr is a table-row. */
.p-table--mobile-card tbody tr {
	border-radius: var(--radius);
}

/* In card mode Vanilla keeps every cell on one line with an ellipsis, which a
 * card has the room not to need: a request ID, an activity row's resource and
 * an erasure's retention-policy sentence were all cut mid-value on a phone.
 * Cells wrap instead, breaking a long token only where it must. Same breakpoint
 * as Vanilla's card rule (width < 1036px). */
@media (max-width: 1035px) {
	.p-table--mobile-card td,
	.p-table--mobile-card tbody th {
		white-space: normal;
		overflow-wrap: break-word;
	}
}

/* Modals */
.p-modal__dialog {
	border-radius: var(--radius);
}

/* Remaining roundable surfaces: side-navigation items, search box, code */
.p-side-navigation__link,
.p-search-box,
.p-search-box__input,
.p-code-snippet__block,
pre, code {
	border-radius: var(--radius);
}

/* Legal documents (/legal/*): goldmark renders the Markdown sources to
 * classless HTML, so the Vanilla component classes cannot be applied at the
 * element. These rules give that output the same table treatment the portal
 * uses, and are scoped to the wrapper so nothing else inherits them. */
.legal-document table {
	width: 100%;
	margin-bottom: var(--space-xl);
	border-collapse: collapse;
	text-align: left;
	/* Vanilla's base table is `table-layout: fixed`, which divides the DPA
	 * annexes' six columns equally regardless of content. Let the content set
	 * the widths instead. */
	table-layout: auto;
}

/* Vanilla's base cell is `overflow: hidden; text-overflow: ellipsis`, sized for
 * short data values. These cells hold sentences, so unclip them; otherwise a
 * clause is silently truncated mid-word and the reader never learns it. */
.legal-document th,
.legal-document td {
	padding: var(--space-sm) var(--space-md);
	border-bottom: 1px solid var(--vf-color-border-low-contrast);
	overflow: visible;
	text-overflow: clip;
	white-space: normal;
	vertical-align: top;
}

.legal-document thead th {
	border-bottom-width: 2px;
}

.legal-document h2 {
	margin-top: var(--space-2xl);
}

.legal-document h3,
.legal-document h4 {
	margin-top: var(--space-xl);
}

.legal-document blockquote {
	margin: var(--space-xl) 0;
	padding-left: var(--space-lg);
	border-left: 3px solid var(--vf-color-border-default);
}

/* Below the mobile breakpoint the DPA's six-column retention table cannot fit
 * in the viewport, and a table is the one block that will not wrap its way out
 * of it. Scroll it inside its own box so the page itself never scrolls
 * sideways. Vanilla's .p-table--mobile-card would be the nicer answer, but it
 * needs a data-heading on every cell and these tables come from Markdown. */
@media (max-width: 619px) {
	.legal-document table {
		display: block;
		overflow-x: auto;
	}
}

/* The key hint is one token. In a narrow column Vanilla broke it after the
 * dots, and the hint read as two values. Use it in a `u-table--auto` table:
 * in a fixed-layout table the cell clips it with an ellipsis instead. */
.api-key-hint { white-space: nowrap; }

/* The swatch beside a colour field of the Branding tab. brandingColoursForm
 * sets its background through the CSSOM, because the CSP refuses an inline
 * style. A field that holds no #rrggbb shows the empty swatch. */
.branding-swatch {
	display: inline-block;
	flex: none;
	width: 2.25rem;
	height: 2.25rem;
	border: 1px solid var(--vf-color-border-default, #e5e5e5);
	border-radius: 2px;
}
.branding-swatch.is-empty {
	background: repeating-linear-gradient(45deg, #fff 0 6px, #f2f2f2 6px 12px);
}
