/* =============================================================================
   Impact design system

   Every value here comes from the design source rather than being invented
   alongside it, so a colour that looks close but isn't is a bug, not a taste
   difference.

   The only stylesheet of ours. It used to share the job with style.css and
   custom.css, which carried a second palette of their own (--app-*) and
   overrode this one on every page that loaded all three; those are folded in
   here now, onto these tokens, and there is one palette again.

   Loaded after Bootstrap on purpose: the shell and the rebuilt screens are
   written against these tokens, while the pages not yet redesigned still lean
   on Bootstrap underneath — see "Bootstrap bridge" at the bottom.
   ============================================================================= */

/* Light — the default, and the only palette written out in full colour terms.
   Everything below refers to these names, so the dark theme is a redefinition
   rather than a second stylesheet to keep in step. */
:root {
	--ip-bg:            #f6f7f7;
	/* The page itself, behind the shell — see "Background". The same as --ip-bg
	   in this theme; darker than it in the dark one. */
	--ip-canvas:        #f6f7f7;
	--ip-surface:       #ffffff;
	--ip-surface-sunk:  #fafbfb;
	--ip-header-bg:     rgba(246, 247, 247, 0.9);
	--ip-ink:           #16191b;
	--ip-ink-soft:      #2c3134;
	--ip-label:         #4b5563;
	--ip-muted:         #6b7280;
	--ip-faint:         #9aa1a0;
	--ip-nav-fg:        #3d4548;
	--ip-placeholder:   #a3aaa8;

	--ip-border:        #e6e8e7;
	--ip-border-soft:   #eef0ef;
	--ip-border-input:  #dfe3e1;
	--ip-box-border:    #c9cfcd;
	--ip-rule:          #f2f4f3;

	--ip-teal:          #0f766e;
	/* The accent as a bare triple, for Bootstrap's rgba(var(--bs-primary-rgb), …). */
	--ip-teal-rgb:      15, 118, 110;
	--ip-teal-dark:     #0b5951;
	--ip-teal-pale:     #8fbfb9;
	--ip-mint:          #ecfdf5;
	--ip-mint-deep:     #d7f0ec;
	--ip-mint-tint:     #eef2f1;

	--ip-amber:         #b45309;
	--ip-amber-bg:      #fef3c7;
	--ip-amber-fg:      #92400e;
	--ip-blue:          #1d4ed8;
	--ip-blue-bg:       #eff6ff;
	--ip-violet:        #5b21b6;
	--ip-violet-bg:     #f5f3ff;
	--ip-pink:          #9d2463;
	--ip-pink-bg:       #fdf2f8;

	--ip-danger:        #b42318;
	--ip-danger-dark:   #912018;
	--ip-danger-bg:     #fee4e2;
	--ip-danger-border: #f0c2bd;

	--ip-track:         #f0f2f1;
	--ip-hatch-a:       #f2f4f3;
	--ip-hatch-b:       #eaeeec;
	--ip-hatch-fg:      #8b9391;

	/* On the accent, on a dark toast — the colour that sits on a filled
	   surface, which is not the page background in either theme. */
	--ip-on-accent:     #ffffff;

	/* The ground an uploaded logo sits on. Deliberately does not flip with the
	   theme: a logo is artwork someone else drew for a light background, and
	   tinting it — with the accent, or with a dark surface — is editing their
	   mark rather than displaying it. */
	--ip-logo-ground:   #ffffff;

	/* FIRST's own program colours, and the text that sits on each. Like the
	   logo ground, these do not flip with the theme: they are FIRST's marks,
	   not our palette, and a team is recognised by them. The only place they
	   are named — see "Programs" for the one rule that applies them. */
	--ip-frc:           #009cd7;
	--ip-frc-fg:        #ffffff;
	--ip-ftc:           #f57e25;
	--ip-ftc-fg:        #ffffff;
	--ip-fll:           #ffc107;
	--ip-fll-fg:        #222222;
	--ip-fgc:           #00a651;
	--ip-fgc-fg:        #222222;
	--ip-input-bg:      #ffffff;
	--ip-scrim:         rgba(16, 25, 27, 0.42);

	/* Laid over a pill that carries its own status colour, to darken it on
	   hover without knowing which colour that is. It inverts by theme: on a
	   dark surface "darker" reads as damage, so there it lightens instead. */
	--ip-veil-hover:    rgba(0, 0, 0, 0.05);

	--ip-scrollbar:     rgba(0, 0, 0, 0.28);

	/* The loading shimmer: a base and the sheen that travels over it. The sheen
	   darkens on a light page and lightens on a dark one — a white shimmer on a
	   white card is an invisible placeholder. */
	--ip-skeleton-base:  rgba(0, 0, 0, 0.06);
	--ip-skeleton-sheen: rgba(0, 0, 0, 0.10);

	--ip-shadow-1:      rgba(16, 24, 40, 0.04);
	--ip-shadow-2:      rgba(16, 24, 40, 0.06);
	--ip-shadow-3:      rgba(16, 24, 40, 0.10);
	--ip-shadow-4:      rgba(16, 24, 40, 0.28);

	--ip-shadow-sm:     0 1px 2px var(--ip-shadow-1);
	--ip-shadow:        0 1px 3px var(--ip-shadow-2);
	--ip-shadow-pop:    0 8px 24px var(--ip-shadow-3);

	/* Browser-drawn glyphs — the date picker indicator — cannot be recoloured,
	   only inverted. */
	--ip-picker-filter: none;

	/* One family, site-wide. These two used to feed Helvetica Neue and IBM Plex
	   Mono to dozens of selectors (stat values, dates, counts) that outrank the
	   base rule, so the numeric UI rendered in a different typeface from
	   everything around it. */
	--ip-sans: "Manrope", sans-serif;
	--ip-mono: "Manrope", sans-serif;
}

/* Dark.
   The accent lightens rather than keeping its light-theme value: #0f766e on
   #0e1112 is close to unreadable, so the teal moves up the scale and the
   "soft" tints become deep washes instead of near-whites. */
:root[data-theme="dark"] {
	--ip-bg:            #0e1112;
	--ip-canvas:        #080808;
	--ip-surface:       #171b1d;
	--ip-surface-sunk:  #1c2123;
	--ip-header-bg:     rgba(14, 17, 18, 0.88);
	--ip-ink:           #eef1f0;
	--ip-ink-soft:      #dee3e2;
	--ip-label:         #b3bcba;
	--ip-muted:         #95a09e;
	--ip-faint:         #78837f;
	--ip-nav-fg:        #c3cbc9;
	--ip-placeholder:   #5f6a68;

	--ip-border:        #2b3235;
	--ip-border-soft:   #232a2c;
	--ip-border-input:  #363f42;
	--ip-box-border:    #4a5457;
	--ip-rule:          #202628;

	--ip-teal:          #3fbfae;
	--ip-teal-rgb:      63, 191, 174;
	--ip-teal-dark:     #63d3c3;
	--ip-teal-pale:     #2f6f68;
	--ip-mint:          #14312d;
	--ip-mint-deep:     #1b453f;
	--ip-mint-tint:     #1d3733;

	--ip-amber:         #e0a355;
	--ip-amber-bg:      #35270f;
	--ip-amber-fg:      #f0c07a;
	--ip-blue:          #93b4ff;
	--ip-blue-bg:       #17233d;
	--ip-violet:        #c0aefc;
	--ip-violet-bg:     #241c40;
	--ip-pink:          #f0a2c6;
	--ip-pink-bg:       #37162a;

	--ip-danger:        #f5837a;
	--ip-danger-dark:   #ffa7a0;
	--ip-danger-bg:     #3d1714;
	--ip-danger-border: #5e2721;

	--ip-track:         #262d2f;
	--ip-hatch-a:       #1c2123;
	--ip-hatch-b:       #232a2c;
	--ip-hatch-fg:      #78837f;

	/* The accent is light in this theme, so text on it has to be dark. */
	--ip-on-accent:     #06231f;
	--ip-input-bg:      #111516;
	--ip-scrim:         rgba(0, 0, 0, 0.62);
	--ip-veil-hover:    rgba(255, 255, 255, 0.09);
	--ip-scrollbar:     rgba(255, 255, 255, 0.30);

	--ip-skeleton-base:  rgba(255, 255, 255, 0.07);
	--ip-skeleton-sheen: rgba(255, 255, 255, 0.12);

	/* The background texture, which only this theme has — see "Background".
	   Triples where a gradient needs the colour at several strengths. */
	--ip-texture-line:  rgba(255, 255, 255, 0.08);
	--ip-glow:          20, 184, 166;
	--ip-glow-violet:   80, 30, 140;
	--ip-vignette:      8, 8, 8;

	--ip-shadow-1:      rgba(0, 0, 0, 0.30);
	--ip-shadow-2:      rgba(0, 0, 0, 0.40);
	--ip-shadow-3:      rgba(0, 0, 0, 0.50);
	--ip-shadow-4:      rgba(0, 0, 0, 0.60);

	--ip-picker-filter: invert(0.85);

	color-scheme: dark;
}

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

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

html, body {
	height: 100%;
	margin: 0;
	padding: 0;
	color: var(--ip-ink);
	font-family: var(--ip-sans);
	-webkit-font-smoothing: antialiased;
	scroll-behavior: smooth;
	transition: background 180ms ease;
}
/* The shell scrolls #app-main, not the page — see "Shell". The signed-out
   pages hand scrolling back to the viewport (.minimal-page-body). */
body { display: flex; flex-direction: column; overflow: hidden; scrollbar-width: thin; }

/* The page colour is the root's, and <body> is transparent over it. That is
   what lets the dark theme's glows show: they sit at z-index -1, which paints
   them above the root's background but beneath any in-flow block's — so an
   opaque <body> covered them completely. */
html { background: var(--ip-canvas); }
body { background-color: transparent; }

/* || BACKGROUND
   The grid and the glows belong to the dark theme only. They are what the page
   is *made of* there — a flat near-black reads as unfinished — but the same
   treatment on a light ground reads as dirt on the screen, so the light theme
   is deliberately plain and the texture is scoped rather than faded. */
:root[data-theme="dark"] body {
	background-image:
		linear-gradient(var(--ip-texture-line) 1px, transparent 1px),
		linear-gradient(90deg, var(--ip-texture-line) 1px, transparent 1px);
	background-size: 50px 50px;
	background-attachment: fixed;
}
:root[data-theme="dark"] body::before {
	content: '';
	position: fixed;
	bottom: -10%;
	left: 50%;
	transform: translateX(-50%);
	width: 120%;
	height: 70%;
	/* The accent's hue, so the one glow on the page is the same colour as
	   everything else that carries meaning. */
	background: radial-gradient(ellipse at bottom center,
		rgba(var(--ip-glow), 0.16) 0%,
		rgba(var(--ip-glow), 0.03) 40%,
		transparent 70%
	);
	pointer-events: none;
	z-index: -1;
}
:root[data-theme="dark"] body::after {
	content: '';
	position: fixed;
	inset: 0;
	background:
		radial-gradient(ellipse at 15% 20%, rgba(var(--ip-glow), 0.04) 0%, transparent 50%),
		radial-gradient(ellipse at 85% 80%, rgba(var(--ip-glow-violet), 0.03) 0%, transparent 50%),
		radial-gradient(circle at 50% 50%, rgba(var(--ip-vignette), 0) 0%, rgba(var(--ip-vignette), 0.5) 55%, rgba(var(--ip-vignette), 0.92) 100%);
	pointer-events: none;
	z-index: -1;
}

::-webkit-scrollbar { width: 5px; height: 1px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background-color: var(--ip-scrollbar); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background-color: var(--ip-ink); }
::-webkit-scrollbar-corner { background: transparent; }

/* The open list of a <select> is painted by the browser from the options' own
   colours, not the control's, so they carry them explicitly. */
select option,
select optgroup { background-color: var(--ip-surface) !important; color: var(--ip-ink) !important; }

input[type="search"] { user-select: none; }

body a { color: var(--ip-teal); text-decoration: none; }
body a:hover { color: var(--ip-teal-dark); text-decoration: none; }

/* Icon glyphs default to inheriting whatever text-size surrounds them —
   which is how the theme toggle's icon ended up rendering at 13px, sized for
   a "Inspire · 1 yr" label rather than a legible standalone glyph. When an
   icon IS the entire visible content of a control, it needs a size of its
   own rather than borrowing ambient text-size, so it can't silently drift
   with whatever button classes happen to surround it. Deliberately larger
   icons (gallery nav arrows, the sidebar hamburger, tile-editor glyphs) keep
   their own component-scoped rules and are unaffected — none of them carry
   .btn or .gal-close, so this cannot override an intentional size. */
.btn > .bi:only-child,
.gal-close > .bi {
	font-size: 1rem;
	/* Without this, the icon's inherited line-height (1.5 × 16px = 24px) can
	   exceed the button's own line-height and stretch the button taller than
	   its siblings — exactly the kind of mismatch this rule exists to end. */
	line-height: 1;
}

input, select, textarea, button { font: inherit; color: inherit; }

/* Form controls do not inherit the page's colours, so they are set explicitly
   or a dark theme leaves white boxes punched through it. */
input, select, textarea {
	background: var(--ip-input-bg);
	color: var(--ip-ink);
	border-color: var(--ip-border-input);
}
/* The calendar glyph is drawn by the browser and cannot be recoloured. */
input[type="date"]::-webkit-calendar-picker-indicator { filter: var(--ip-picker-filter); cursor: pointer; }
::placeholder { color: var(--ip-placeholder); opacity: 1; }

@keyframes ip-rise-in {
	from { opacity: 0; transform: translateY(6px); }
	to   { opacity: 1; transform: none; }
}

.ip-rise { animation: ip-rise-in 220ms ease both; }

/* A page arriving over its own skeleton (navigation.js marks it) is not
   entering — its structure has been on screen since the click. Rising in from
   transparent there would blank it for a frame and read as a reload. */
#main-content[data-swapped] .ip-rise { animation: none; }

/* The page crossing — navigation.js adds these. Out is quicker than in: the
   old page only has to get out of the way, and it does so while the next one
   is still being fetched. The transition lives on the leaving state alone, so
   coming back in is the animation's job and the two never fight. */
#main-content.is-leaving {
	opacity: 0;
	transition: opacity 140ms ease-out;
}
#main-content.is-entering { animation: app-page-in 200ms ease-out both; }

@keyframes app-page-in {
	from { opacity: 0; }
	to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
	#main-content.is-leaving { transition: none; }
	#main-content.is-entering { animation: none; }
}

.ip-mono { font-family: var(--ip-mono); }

/* -----------------------------------------------------------------------------
   Shell
   -------------------------------------------------------------------------- */

/* #app-main is the column that scrolls. Its scrollbar's room is kept whether or
   not the page is long enough to need it, so moving from a short page to a
   long one does not narrow the content and shift everything sideways. */
#app-main { scrollbar-gutter: stable; }

/* The shell's top bar — Header.php's #app-topbar.
 *
 * Sticky against the scrolling content column rather than fixed to the window,
 * so it takes the column's width without knowing the sidebar's, and needs no
 * spacer under it. Translucent over what scrolls beneath it, which is what
 * --ip-header-bg is for.
 *
 * Drawn once and kept: navigation rewrites the title and the window control in
 * place, and the controls beside them never leave the screen. */
.app-topbar {
	position: sticky;
	top: 0;
	/* Above the page, and below everything that covers the page. The order, on
	   Bootstrap's scale since its dialogs share the screen with ours:
	     1020  this bar (Bootstrap's "sticky"; the team menu is a child of the
	           bar and rides on its stacking context)
	     1040  .ip-flyout scrim and sheet
	     1045  the sidebar as a slide-over (Bootstrap offcanvas)
	     1050  Bootstrap's modal backdrop
	     1055  dialogs: form-modal (Bootstrap) and .ip-modal
	     1060  .fm-suggest-panel, over the dialog it belongs to
	     1065  .gal-overlay
	   Anything that covers the page must sit above 1020, or this bar stays
	   bright and clickable over its backdrop. */
	z-index: 1020;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px 14px;
	min-height: 62px;
	padding: 10px 16px;
	background: var(--ip-header-bg);
	backdrop-filter: blur(8px);
	-webkit-backdrop-filter: blur(8px);
	border-bottom: 1px solid var(--ip-border-soft);
	flex-shrink: 0;
}
.app-topbar__menu {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 36px;
	height: 36px;
	margin-left: -6px;
	border: 0;
	border-radius: 8px;
	background: transparent;
	color: inherit;
	font-size: 1.4rem;
}
.app-topbar__menu:hover { background: var(--ip-veil-hover); }
.app-topbar__heading { flex: 1 1 200px; min-width: 0; }
.app-topbar__title {
	margin: 0;
	font-size: 1.3rem;
	font-weight: 600;
	letter-spacing: -0.015em;
	line-height: 1.3;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* Its line is kept even when a page has no subtitle. Hiding an empty one made
   the bar a few pixels shorter on those pages and re-centred the title, so
   moving between a page with a subtitle and one without nudged everything
   below the bar — and the title itself — up and down. */
.app-topbar__sub {
	margin-top: 1px;
	min-height: 1.4em;
	font-size: 12.5px;
	line-height: 1.4;
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* A title or subtitle that is still on its way, for a page not visited yet in
   this tab — the bar keeps its height, so nothing below it moves when the words
   arrive. */
.app-topbar__title .skeleton { height: 0.9em; width: 11rem; vertical-align: -0.05em; }
.app-topbar__sub .skeleton { height: 0.8em; width: 16rem; max-width: 70%; }
.app-topbar__tools {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
	margin-left: auto;
}
/* The bar's three controls — the award window (.ip-seg), the team switcher
   (.ip-btn) and the theme toggle (.ip-theme-toggle) — are one height, 34px,
   and one family. They were three Bootstrap buttons: the window control
   btn-outline-primary, which the bridge coloured teal, and the other two
   btn-outline-secondary, which it never mapped, so they sat in Bootstrap's
   own grey beside it, at 40, 32 and 36px. Teal is now spent only on the
   window that is selected. */
.app-topbar__tools .ip-seg { height: 34px; align-items: stretch; }
.app-topbar__tools .ip-seg__btn { display: inline-flex; align-items: center; padding: 0 11px; }
.app-topbar__tools .app-topbar__team {
	height: 34px;
	padding: 0 10px 0 8px;
	gap: 8px;
	max-width: 260px;
	/* The window control's and the theme toggle's border, not an input's. */
	border-color: var(--ip-border);
}
.app-topbar__tools .app-topbar__team:hover { background: var(--ip-surface-sunk); }
.app-topbar__team-name {
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	min-width: 0;
}

#main-content { padding-bottom: 40px; }

/* The sidebar. Bootstrap's offcanvas below lg, a fixed column above it; the
   shape, the sizes and the transitions are the ones the app has always had,
   and the colours are the palette's. */
#sidebar {
	display: grid !important;
	grid-template-rows: auto 1fr auto;
	overflow: hidden;
	height: 100dvh;
	user-select: none;
	background-color: var(--ip-surface) !important;
	border-color: var(--ip-border) !important;
	color: var(--ip-nav-fg);
}
#sidebar.offcanvas,
#sidebar.offcanvas-lg {
	height: 100svh !important;
	max-height: 100svh !important;
}
#sidebar > ul {
	min-height: 0;
	overflow-y: auto;
	overflow-x: hidden;
}
#sidebar .nav-link {
	font-size: 1rem;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	color: var(--ip-nav-fg) !important;
}
/* The section headings are <button>s, which the browser paints with its own
   control background. Reset here rather than with Bootstrap's .bg-transparent,
   whose !important also beat the active rule below — an open section then
   showed no highlight at all. */
#sidebar button.nav-link {
	background-color: transparent;
	cursor: pointer;
}
#sidebar .nav-link:hover,
#sidebar button.nav-link:hover {
	background-color: var(--ip-veil-hover);
	color: var(--ip-ink) !important;
}
#sidebar .nav-link.active {
	background-color: var(--ip-teal);
	color: var(--ip-on-accent) !important;
}
/* Badges invert against the active pill. */
#sidebar .nav-link.active .badge {
	background-color: var(--ip-on-accent) !important;
	color: var(--ip-teal) !important;
	font-weight: 600;
}
/* Collapsed, the chevron points down; open, it flips up. */
[data-bs-toggle="collapse"] .sidebar-chevron {
	color: var(--ip-muted);
	transition: transform 0.18s ease, color 0.18s ease;
	font-size: 1.1em;
	transform: rotate(0deg);
}
[data-bs-toggle="collapse"][aria-expanded="true"] .sidebar-chevron {
	transform: rotate(180deg);
	color: var(--ip-ink);
}
#sidebar ul ul .nav-link { font-size: 0.95rem; }

/* The organisation's name, held to two lines. A long one — "Southern Cross
   Robotics & Engineering Collective" — ran to four, and pushed the whole nav
   down by the lines it took. The full name is in its title. */
.sidebar-brand-name {
	font-size: 1.1rem;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	overflow: hidden;
	overflow-wrap: anywhere;
}

/* Scrolls by itself when an open section is taller than the room left — the
   brand above and Log activity below stay put. */
.sidebar-nav-wrapper {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	overflow-x: hidden;
}

/* The account toggle and the settings gear at the foot of the sidebar. One
   rule for both so they cannot drift apart in colour. Their heights are matched
   by the row stretching them (see Header.php); the gear's width is the toggle's
   44px — a 28px avatar inside py-2 — so it reads as a square beside it. */
.sidebar-foot-btn {
	background: var(--ip-veil-hover);
	transition: background-color 0.15s ease, color 0.15s ease;
}
.sidebar-foot-btn:hover {
	background: var(--ip-veil-hover);
	color: var(--ip-ink) !important;
}
.sidebar-foot-btn--square { width: 44px; }
.sidebar-foot-btn.is-active {
	background: var(--ip-mint-tint);
	color: var(--ip-teal) !important;
}

.account-item {
	font-size: 0.9rem;
	transition: background 0.15s ease;
	cursor: pointer;
}
.account-item:hover { background: var(--ip-veil-hover) !important; }

/* Taller tap targets where the sidebar is a slide-over. */
@media (max-width: 991.98px) {
	#sidebar .nav-link { padding-top: 0.6rem; padding-bottom: 0.6rem; }
	.sidebar-chevron { font-size: 0.85rem; }
}

/* The progress line along the top while a page is on its way. */
#nav-progress {
	position: fixed;
	top: 0;
	left: 0;
	height: 3px;
	width: 0%;
	z-index: 9999;
	background: var(--ip-teal);
	transition: width 200ms ease, opacity 300ms ease;
	opacity: 0;
	pointer-events: none;
}
/* A full-page load that follows a fade-out (app.js) starts hidden, so the new
   page fades in rather than flashing. The whole shell on a way in from a
   signed-out page, which had none — see shellParts() in app.js. */
html.full-page-transition-pending #main-content,
html.full-page-transition-pending body > .minimal-page-main { opacity: 0; }
html.full-page-transition-needs-sidebar-fade #sidebar,
html.full-page-transition-needs-sidebar-fade #app-topbar { opacity: 0; }

/* The corner the toasts stack in is always on screen, empty or not, and above
   the page. So it lets clicks through and only a toast itself takes one; before
   this, the empty box swallowed every click in the bottom-right corner — the
   entry flyout's Edit button and the signed-out pages' Help button among them. */
.app-toasts { pointer-events: none; }
.app-toasts .toast { pointer-events: auto; }

/* Theme toggle */
.ip-theme-toggle {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 999px;
	width: 34px;
	height: 34px;
	display: grid;
	place-items: center;
	cursor: pointer;
	font-size: 14px;
	color: var(--ip-label);
	flex: 0 0 34px;
	padding: 0;
}
.ip-theme-toggle:hover { background: var(--ip-surface-sunk); }

/* -----------------------------------------------------------------------------
   Surfaces
   -------------------------------------------------------------------------- */

.ip-card {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	padding: 18px;
	box-shadow: var(--ip-shadow-sm);
}
.ip-card--lg { border-radius: 14px; box-shadow: var(--ip-shadow); }
.ip-card--flush { padding: 0; overflow: hidden; }
.ip-card--sunk { background: var(--ip-surface-sunk); border-color: var(--ip-border-soft); }

.ip-card__head {
	padding: 16px 18px;
	border-bottom: 1px solid var(--ip-border-soft);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
}
.ip-card__title { font-size: 14px; font-weight: 600; }
.ip-card__meta { font-size: 11.5px; color: var(--ip-muted); }

.ip-eyebrow {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-faint);
}

.ip-rule { height: 1px; background: var(--ip-border-soft); margin: 16px 0; }

.ip-stack   { display: flex; flex-direction: column; gap: 16px; }
.ip-stack-sm { display: flex; flex-direction: column; gap: 8px; }
.ip-row     { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.ip-dropzone {
	border: 1.5px dashed var(--ip-border-input);
	border-radius: 11px;
	padding: 26px;
	text-align: center;
	background: var(--ip-surface-sunk);
}
.ip-dropzone__hint {
	font-family: var(--ip-mono);
	font-size: 11.5px;
	color: var(--ip-faint);
	letter-spacing: 0.03em;
	line-height: 1.6;
}

/* -----------------------------------------------------------------------------
   Controls
   -------------------------------------------------------------------------- */

/* Padding and font matched to .ip-input's, line for line, so a full-size button
   is never a different height from the field it sits beside — see the email
   row in settings/profile.php. */
.ip-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 7px;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	padding: 10px 15px;
	font-size: 13.5px;
	font-weight: 500;
	cursor: pointer;
	color: var(--ip-ink);
	text-decoration: none;
}
.ip-btn:hover { background: var(--ip-rule); color: var(--ip-ink); text-decoration: none; }

.ip-btn--primary {
	background: var(--ip-teal);
	color: var(--ip-on-accent);
	border-color: var(--ip-teal);
	font-weight: 600;
	box-shadow: var(--ip-shadow-sm);
}
.ip-btn--primary:hover { background: var(--ip-teal-dark); border-color: var(--ip-teal-dark); color: var(--ip-on-accent); }

.ip-btn--block { width: 100%; }
.ip-btn--sm { padding: 6px 11px; font-size: 12px; }
.ip-btn--quiet { background: transparent; border-color: var(--ip-border); }
.ip-btn--quiet:hover { background: var(--ip-bg); }
/* Outlined rather than filled: a destructive action should be findable, not the
   brightest thing on the panel competing with the action you actually came for. */
.ip-btn--danger { background: transparent; border-color: var(--ip-danger-border); color: var(--ip-danger); }
.ip-btn--danger:hover { background: var(--ip-danger-bg); border-color: var(--ip-danger); color: var(--ip-danger); }

.ip-btn--link {
	background: transparent;
	border: 0;
	color: var(--ip-muted);
	font-size: 12.5px;
	padding: 0;
	text-decoration: underline;
}
.ip-btn--link:hover { background: transparent; color: var(--ip-ink); }

.ip-btn:disabled { opacity: 0.55; cursor: not-allowed; }

/* Segmented control — the award-window and variant switchers */
.ip-seg {
	display: flex;
	position: relative;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 9px;
	padding: 3px;
	gap: 2px;
}
.ip-seg__btn {
	border: 0;
	border-radius: 6px;
	padding: 6px 11px;
	font-size: 12px;
	font-weight: 500;
	cursor: pointer;
	background: transparent;
	color: var(--ip-label);
	white-space: nowrap;
	/* Above the pill, which is a sibling drawn behind it. */
	position: relative;
	z-index: 1;
	transition: color 200ms ease;
}
.ip-seg__btn:hover { opacity: 0.9; }
.ip-seg__btn.is-active { background: var(--ip-teal); color: var(--ip-on-accent); }

/* The pill that travels between options — sized and positioned from JS, since
   the options are as wide as their own labels. See seg.js.

   `has-thumb` is added by that module, and handing the background over is
   conditional on it: a control this never reached keeps the painted-in
   background above and looks exactly as it did before. */
.ip-seg.has-thumb .ip-seg__btn.is-active { background: transparent; }

.ip-seg__thumb {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 0;
	border-radius: 6px;
	background: var(--ip-teal);
	pointer-events: none;
	opacity: 0;
}

/* Only once placed. The first placement is a jump from the corner, and it is
   the one movement that must not be animated. */
.ip-seg.is-ready .ip-seg__thumb {
	transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1),
	            width 300ms cubic-bezier(0.32, 0.72, 0, 1),
	            height 300ms cubic-bezier(0.32, 0.72, 0, 1),
	            opacity 140ms ease;
}

.ip-seg-label {
	font-size: 11px;
	color: var(--ip-faint);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

/* Filter chips */
.ip-chip {
	border: 1px solid var(--ip-border-input);
	background: var(--ip-surface);
	color: var(--ip-label);
	border-radius: 999px;
	padding: 6px 12px;
	font-size: 12px;
	cursor: pointer;
	font-weight: 500;
	white-space: nowrap;
	transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease;
}
.ip-chip.is-active { background: var(--ip-mint); color: var(--ip-teal); border-color: var(--ip-teal); }
.ip-chip--lg { padding: 7px 14px; font-size: 12.5px; }

/* Read-only tag */
.ip-tag {
	display: inline-block;
	font-size: 11px;
	font-weight: 500;
	padding: 3px 8px;
	border-radius: 999px;
	background: var(--ip-rule);
	color: var(--ip-muted);
	white-space: nowrap;
}
.ip-tag--teal   { background: var(--ip-mint);      color: var(--ip-teal); }
.ip-tag--blue   { background: var(--ip-blue-bg);   color: var(--ip-blue); }
.ip-tag--amber  { background: var(--ip-amber-bg);  color: var(--ip-amber-fg); }
.ip-tag--violet { background: var(--ip-violet-bg); color: var(--ip-violet); }
.ip-tag--pink   { background: var(--ip-pink-bg);   color: var(--ip-pink); }

/* Cycling status pill — a record's state, and the control that advances it.
   The third member of the tag/chip pair, and it has to announce itself as one:
   shaped like a tag, it was being read as one, and people did not discover that
   a status could be moved by clicking it. So it carries a ⟳, lifts and darkens
   under the cursor, and its title names the state the click will move it to.
   The colours stay inline because they belong to the state, not to the control. */
.ip-cycle {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	border-radius: 999px;
	padding: 4px 9px 4px 11px;
	font-size: 11px;
	font-weight: 600;
	white-space: nowrap;
	cursor: pointer;
	transition: box-shadow 0.12s ease, transform 0.12s ease;
}
/* An inset shadow rather than a background change: the pill's own background is
   set per state and this must not overwrite it. */
.ip-cycle:hover  { box-shadow: inset 0 0 0 40px var(--ip-veil-hover); transform: translateY(-1px); }
.ip-cycle:active { transform: translateY(0); }
.ip-cycle__glyph { font-size: 12px; line-height: 1; opacity: 0.6; }

/* The line under a section header that spells the cycle out, for the state you
   cannot see from one pill: which order the states go in. */
.ip-cycle-hint {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	margin-top: 9px;
	padding: 5px 10px 5px 8px;
	border: 1px dashed var(--ip-border-input);
	border-radius: 999px;
	font-size: 11.5px;
	color: var(--ip-muted);
}
.ip-cycle-hint__glyph { font-size: 12px; opacity: 0.7; }

@media (prefers-reduced-motion: reduce) {
	.ip-cycle { transition: none; }
	.ip-cycle:hover { transform: none; }

	/* The indicator still moves — it has to, it is the thing that marks the
	   current step — it simply arrives rather than travels. */
	.ip-steps__indicator,
	.ip-step,
	.ip-step__dot,
	.ip-seg__btn,
	.ip-seg.is-ready .ip-seg__thumb,
	.ip-chip { transition: none; }
	.ip-step-panel--enter { animation: none; }
}

/* Fields */
.ip-field { display: flex; flex-direction: column; gap: 6px; }
.ip-field__label { font-size: 12px; font-weight: 600; color: var(--ip-label); }
.ip-field__hint  { font-size: 11.5px; color: var(--ip-faint); }

.ip-input {
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	padding: 10px 12px;
	font-size: 13.5px;
	background: var(--ip-input-bg);
	width: 100%;
	color: var(--ip-ink);
}
.ip-input:focus {
	border-color: var(--ip-teal);
	outline: none;
	box-shadow: 0 0 0 3px var(--ip-mint);
	background: var(--ip-input-bg);
}
textarea.ip-input { resize: vertical; line-height: 1.5; }
/* A <select> keeps its arrow: the browser's own is dropped with the rest of its
   chrome, and this one is drawn in the label colour so it follows the theme. */
select.ip-input {
	appearance: none;
	padding-right: 34px;
	background-image:
		linear-gradient(45deg, transparent 50%, var(--ip-label) 50%),
		linear-gradient(135deg, var(--ip-label) 50%, transparent 50%);
	background-position: calc(100% - 17px) 50%, calc(100% - 12px) 50%;
	background-size: 5px 5px;
	background-repeat: no-repeat;
}
select.ip-input[multiple] { padding-right: 12px; background-image: none; }
.ip-input:disabled, .ip-input[readonly] { color: var(--ip-muted); background-color: var(--ip-surface-sunk); }
.ip-input[type="file"] { padding: 7px 10px; }
.ip-input.is-invalid { border-color: var(--ip-danger); }
.ip-input.is-invalid:focus { box-shadow: 0 0 0 3px var(--ip-danger-bg); }
.invalid-feedback { font-size: 11.5px; color: var(--ip-danger); }
.ip-input--sm { padding: 6px 8px; font-size: 12.5px; border-radius: 7px; }
.ip-input--num { text-align: right; }

/* Search — an .ip-input with its glyph. Drawn by Toolbar::search() rather than
   written out, which is how every box ends up with the same glyph, width and
   placeholder punctuation. */
.ip-search { position: relative; flex: 1 1 220px; min-width: 200px; margin: 0; }
.ip-search > .bi {
	position: absolute;
	left: 12px;
	top: 50%;
	transform: translateY(-50%);
	font-size: 13px;
	color: var(--ip-faint);
	pointer-events: none;
}
.ip-search > .ip-input { padding-left: 34px; }

/* Toolbar — what sits above a list: search, filters, a count, the list's own
   action. Worn with .ip-card for the surface. Each list page used to build this
   row itself, and no two agreed on padding, surface or where the button went.
   Stretched rather than centred, so the search box — sized to match the chips
   and .ip-btn--sm actions beside it — cannot drift a pixel off them. */
.ip-toolbar {
	display: flex;
	flex-wrap: wrap;
	align-items: stretch;
	gap: 10px;
	padding: 12px 14px;
}
.ip-toolbar__filters { display: flex; flex-wrap: wrap; gap: 6px; }
.ip-toolbar__meta { font-size: 12px; color: var(--ip-faint); font-variant-numeric: tabular-nums; }
/* Pushes itself, and anything after it, to the far end of the row. */
.ip-toolbar__end { margin-left: auto; }

/* Footnote — the quiet line under a list saying how it behaves. */
.ip-footnote { font-size: 12px; color: var(--ip-faint); line-height: 1.55; }

/* Combo — a searchable multi-select.
   For a list too long to lay out as chips. Chips are right up to a dozen or so,
   where seeing every option *is* the interface; past that they become a wall to
   read rather than a set to choose from, and the answer is a search box. */
.ip-combo { position: relative; }

.ip-combo__control {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
	border: 1px solid var(--ip-border-input);
	border-radius: 9px;
	background: var(--ip-input-bg);
	padding: 6px 8px;
	cursor: text;
}
.ip-combo__control:focus-within {
	border-color: var(--ip-teal);
	box-shadow: 0 0 0 3px var(--ip-mint);
}

/* The input keeps a minimum so it stays clickable once tokens wrap, and grows to
   take the rest of the row. */
.ip-combo__input {
	flex: 1;
	min-width: 120px;
	border: 0;
	outline: none;
	background: transparent;
	color: var(--ip-ink);
	font-size: 13.5px;
	font-family: inherit;
	padding: 4px 2px;
}

.ip-combo__token {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	background: var(--ip-mint);
	color: var(--ip-teal);
	border: 1px solid var(--ip-teal);
	border-radius: 999px;
	padding: 3px 4px 3px 10px;
	font-size: 12px;
	font-weight: 500;
	white-space: nowrap;
}
.ip-combo__token button {
	border: 0;
	background: transparent;
	color: inherit;
	cursor: pointer;
	font-size: 13px;
	line-height: 1;
	padding: 2px 4px;
	border-radius: 999px;
}
.ip-combo__token button:hover { background: var(--ip-mint-deep); }
.ip-combo__token-sub { font-weight: 400; color: var(--ip-muted); }

.ip-combo__menu {
	position: absolute;
	z-index: 20;
	left: 0;
	right: 0;
	top: calc(100% + 4px);
	max-height: 232px;
	overflow-y: auto;
	overscroll-behavior: contain;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	box-shadow: var(--ip-shadow-pop);
	padding: 4px;
}
.ip-combo__menu[hidden] { display: none; }

.ip-combo__option {
	display: flex;
	align-items: center;
	gap: 10px;
	width: 100%;
	text-align: left;
	border: 0;
	background: transparent;
	color: var(--ip-ink);
	border-radius: 7px;
	padding: 8px 10px;
	font-size: 13px;
	cursor: pointer;
}
/* Highlight follows the keyboard, hover only mirrors it — two independently
   highlighted rows would leave no way to tell which Enter would pick. */
.ip-combo__option.is-active { background: var(--ip-surface-sunk); }
.ip-combo__option.is-on { color: var(--ip-teal); font-weight: 600; }
.ip-combo__option__mark { width: 14px; flex: 0 0 14px; font-size: 11px; }
.ip-combo__option__sub { margin-left: auto; font-size: 11.5px; color: var(--ip-faint); }

.ip-combo__empty { padding: 12px 10px; font-size: 12.5px; color: var(--ip-faint); }

/* The way out of an empty menu: create the thing that was searched for. Sits
   inside the menu because that is where the dead end is — a button elsewhere on
   the form is somewhere else to look at the moment of not finding something. */
.ip-combo__create {
	display: block;
	width: 100%;
	border: 0;
	border-top: 1px solid var(--ip-border-soft);
	background: transparent;
	padding: 10px;
	text-align: left;
	font-size: 12.5px;
	font-weight: 500;
	color: var(--ip-teal);
	cursor: pointer;
}
.ip-combo__create:hover { background: var(--ip-mint); }

.ip-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ip-grid-fit { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }

@media (max-width: 640px) {
	.ip-grid-2 { grid-template-columns: 1fr; }
}

/* Checkbox — square, teal when set */
.ip-check {
	width: 20px;
	height: 20px;
	flex: 0 0 20px;
	border-radius: 5px;
	border: 1.5px solid var(--ip-box-border);
	background: var(--ip-surface);
	color: var(--ip-on-accent);
	cursor: pointer;
	display: grid;
	place-items: center;
	font-size: 11px;
	padding: 0;
}
.ip-check.is-on { border-color: var(--ip-teal); background: var(--ip-teal); }
.ip-check--sm { width: 17px; height: 17px; flex-basis: 17px; border-radius: 4px; font-size: 10px; }

/* -----------------------------------------------------------------------------
   Data display
   -------------------------------------------------------------------------- */

.ip-avatar {
	width: 26px;
	height: 26px;
	flex: 0 0 26px;
	border-radius: 50%;
	background: var(--ip-mint-tint);
	color: var(--ip-teal);
	display: grid;
	place-items: center;
	font-size: 10.5px;
	font-weight: 600;
}
.ip-avatar--lg { width: 28px; height: 28px; flex-basis: 28px; font-size: 11px; }

/* KPI tiles */

/* Queried rather than the viewport: the sidebar is 252px wide above 900px and
   off-canvas below it, so a viewport breakpoint would name a column count the
   content area cannot actually hold. */
.ip-kpi-band { container-type: inline-size; }

.ip-kpis {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: 14px;
}

/* The tiles arrive as one injected block. display:contents lifts them into the
   grid so a card sitting beside them can claim the cells the tiles leave. */
.ip-kpis__tiles { display: contents; }
.ip-kpis__tiles > .loading-pane { grid-column: 1 / -1; }

/* Six tiles: only at four columns is the second row half empty, so that is the
   only width where the companion card sits beside them rather than below. */
.ip-kpis__wide { grid-column: 1 / -1; }

@container (min-width: 460px) { .ip-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (min-width: 660px) { .ip-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container (min-width: 880px) {
	.ip-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); }
	.ip-kpis__wide { grid-column: span 2; }
}

.ip-kpi {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	padding: 16px 16px 14px;
	box-shadow: var(--ip-shadow-sm);
}
.ip-kpi__label { font-size: 11.5px; color: var(--ip-muted); font-weight: 500; }
.ip-kpi__value {
	font-size: 30px;
	font-weight: 600;
	letter-spacing: -0.03em;
	margin-top: 6px;
	line-height: 1;
}
.ip-kpi__delta { font-size: 11.5px; color: var(--ip-teal); margin-top: 7px; font-weight: 500; }

/* Compact stat tiles above a list */
.ip-stats { display: flex; gap: 14px; flex-wrap: wrap; }
.ip-stat {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 11px;
	padding: 12px 16px;
	min-width: 150px;
}
.ip-stat__label { font-size: 11px; color: var(--ip-muted); }
.ip-stat__value { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin-top: 3px; }

/* Column chart */
.ip-bars {
	display: flex;
	align-items: flex-end;
	gap: 8px;
	height: 190px;
	margin-top: 20px;
}
.ip-bars--sm { height: 120px; gap: 7px; margin-top: 14px; }

.ip-bar {
	flex: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 8px;
	height: 100%;
	justify-content: flex-end;
	min-width: 0;
}
.ip-bar__value { font-size: 10.5px; color: var(--ip-muted); font-family: var(--ip-mono); }
.ip-bar__fill {
	width: 100%;
	border-radius: 5px 5px 2px 2px;
	background: var(--ip-teal-pale);
	min-height: 4px;
}
.ip-bar__fill.is-peak { background: var(--ip-teal); }
.ip-bar__label { font-size: 10.5px; color: var(--ip-faint); white-space: nowrap; }

/* Horizontal meter */
.ip-meter {
	height: 7px;
	background: var(--ip-track);
	border-radius: 4px;
	overflow: hidden;
}
.ip-meter__fill { height: 100%; border-radius: 4px; background: var(--ip-teal-pale); }
.ip-meter__fill.is-peak { background: var(--ip-teal); }

.ip-meter-row { margin-bottom: 13px; }
.ip-meter-row__head {
	display: flex;
	justify-content: space-between;
	font-size: 12.5px;
	margin-bottom: 5px;
}
.ip-meter-row__label { font-weight: 500; }
.ip-meter-row__value { color: var(--ip-muted); font-family: var(--ip-mono); }

/* Key/value list */
.ip-kv { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px; }
.ip-kv__key { color: var(--ip-muted); }
.ip-kv__val { font-weight: 600; text-align: right; }
.ip-kv__val--mono { font-family: var(--ip-mono); }

/* Grid tables — a table only in appearance, so a row can be a link and the
   column set can differ per screen without a colspan dance. */
.ip-table { overflow-x: auto; }
.ip-table__head,
.ip-table__row {
	display: grid;
	gap: 12px;
	align-items: center;
	padding: 12px 18px;
}
.ip-table__head {
	border-bottom: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--ip-faint);
	padding-top: 11px;
	padding-bottom: 11px;
}
.ip-table__row {
	border-bottom: 1px solid var(--ip-rule);
	font-size: 12.5px;
	color: inherit;
	text-decoration: none;
}
/* A row that opens something is a <button>, so it is reachable by keyboard. */
button.ip-table__row {
	width: 100%;
	text-align: left;
	background: transparent;
	border: 0;
	border-bottom: 1px solid var(--ip-rule);
	font: inherit;
}
.ip-table__row.is-clickable { cursor: pointer; }
.ip-table__row.is-clickable:hover { background: var(--ip-surface-sunk); text-decoration: none; color: inherit; }
.ip-table__note { padding: 12px 18px; font-size: 12px; color: var(--ip-faint); }

/* A heading that sorts by its column. It looks like the heading it is — the
   arrow on the one in force is what says the rest can be clicked too. */
.ip-table__sort {
	background: transparent;
	border: 0;
	padding: 0;
	font: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	color: inherit;
	text-align: left;
	cursor: pointer;
	white-space: nowrap;
}
.ip-table__sort.ip-cell-right { text-align: right; }
.ip-table__sort:hover,
.ip-table__sort.is-on { color: var(--ip-ink); }

/* A team's avatar from FIRST. Rendered from a data: URI, so it needs an
   explicit box — a broken or absent image must not collapse the row it sits in.
   `pixelated` because these are 40x40 originals: smoothing a tiny logo up to
   28px turns a crisp badge into a smear. */
.ip-team-avatar {
	width: 28px;
	height: 28px;
	flex: 0 0 28px;
	border-radius: 6px;
	object-fit: contain;
	image-rendering: pixelated;
	background: var(--ip-surface-sunk);
}
.ip-team-avatar--lg { width: 40px; height: 40px; flex-basis: 40px; border-radius: 8px; }

/* An entry waiting on a mentor. Bordered rather than sunk: it is an item of
   work in a list of work, not a passive read-out. */
.ip-approval {
	border: 1px solid var(--ip-border-soft);
	border-radius: 9px;
	padding: 11px 12px;
}
.ip-approval__title { font-size: 12.5px; font-weight: 500; line-height: 1.4; }
.ip-approval__meta { font-size: 11.5px; color: var(--ip-faint); margin-top: 3px; }

/* Month band. Sets its own min-width to match the rows it divides, so it spans
   the full scroll width of the table rather than stopping at the viewport. */
.ip-table__group {
	display: flex;
	align-items: baseline;
	gap: 10px;
	padding: 9px 18px;
	background: var(--ip-surface-sunk);
	border-top: 1px solid var(--ip-border-soft);
	border-bottom: 1px solid var(--ip-border-soft);
}
.ip-table__group-label { font-size: 11.5px; font-weight: 600; letter-spacing: 0.03em; }
.ip-table__group-stat { font-size: 11px; color: var(--ip-faint); font-family: var(--ip-mono); }

/* Pagination footer. The buttons and the count sit on one line so the count is
   read as the reason for the button rather than as a separate caption. */
.ip-table__foot {
	display: flex;
	align-items: center;
	gap: 10px;
	flex-wrap: wrap;
	padding: 12px 18px;
}
.ip-table__foot .ip-table__note { padding: 0; }
.ip-table__count { font-family: var(--ip-mono); font-size: 12px; color: var(--ip-faint); }

.ip-cell-strong {
	font-weight: 500;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ip-cell-sub { color: var(--ip-muted); font-size: 11.5px; margin-top: 2px; }
.ip-cell-date { color: var(--ip-faint); font-family: var(--ip-mono); font-size: 11.5px; }
.ip-cell-num { font-family: var(--ip-mono); }
.ip-cell-right { text-align: right; }
.ip-cell-muted { color: var(--ip-muted); }
/* A person: avatar, then the name, which truncates before the avatar squeezes. */
.ip-cell-who { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* A list that has to stay one line so the rows stay aligned — four roles must
   not make a row four lines tall. Give it a title with the whole text. */
.ip-cell-clip {
	font-size: 11.5px;
	color: var(--ip-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Timeline */
.ip-timeline__item { display: flex; gap: 12px; }
.ip-timeline__gutter {
	display: flex;
	flex-direction: column;
	align-items: center;
	flex: 0 0 10px;
}
.ip-timeline__dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	background: var(--ip-teal);
	margin-top: 5px;
}
.ip-timeline__line { width: 1px; flex: 1; background: var(--ip-border); }
.ip-timeline__body { padding-bottom: 16px; flex: 1; }

/* Empty state — every list can be empty on a fresh organisation, and an empty
   grid with no explanation reads as a failed request. */
.ip-empty {
	padding: 34px 20px;
	text-align: center;
	color: var(--ip-faint);
	font-size: 12.5px;
	line-height: 1.6;
}
.ip-empty__title { font-size: 13.5px; font-weight: 600; color: var(--ip-muted); margin-bottom: 4px; }
.ip-empty__action { margin-top: 14px; }
/* In a grid of cards the message would take one cell and sit at the left like a
   stray caption; it spans the grid, and wears a card's surface so it reads as
   the answer rather than as text floating over the page. */
:is(.ip-team-grid, .ip-evidence-grid) > .ip-empty {
	grid-column: 1 / -1;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
}

/* Progress list — a short procedure the app carries out while you watch, one
   step at a time: pending, working (the dot turns), done (a tick) or failed.
   Not the logger's .ip-step tabs, which are places you can click back to; this
   only ever moves forward, and says so by filling in top to bottom. Each step's
   body holds whatever that step needs — a record to copy, a notice, a button. */
.ip-progress { display: flex; flex-direction: column; }
.ip-progress__item { display: flex; gap: 12px; }
.ip-progress__gutter { display: flex; flex-direction: column; align-items: center; flex: 0 0 22px; }
.ip-progress__dot {
	width: 22px;
	height: 22px;
	border-radius: 50%;
	background: var(--ip-track);
	color: var(--ip-faint);
	display: grid;
	place-items: center;
	font-size: 11px;
	font-weight: 600;
	transition: background-color 240ms ease, color 240ms ease;
}
.ip-progress__line { width: 1px; flex: 1; background: var(--ip-border); margin: 4px 0; }
.ip-progress__body { flex: 1; min-width: 0; padding-bottom: 18px; }
.ip-progress__item:last-child .ip-progress__body { padding-bottom: 0; }
.ip-progress__title { font-size: 13.5px; font-weight: 600; line-height: 22px; color: var(--ip-muted); }
.ip-progress__detail { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }

.ip-progress__item.is-current .ip-progress__title { color: var(--ip-ink); }
.ip-progress__item.is-current .ip-progress__dot { background: var(--ip-teal); color: var(--ip-on-accent); }
.ip-progress__item.is-done .ip-progress__title { color: var(--ip-ink-soft); }
.ip-progress__item.is-done .ip-progress__dot { background: var(--ip-mint); color: var(--ip-teal); }
.ip-progress__item.is-failed .ip-progress__dot { background: var(--ip-danger-bg); color: var(--ip-danger); }
/* Working: the dot becomes a turning ring, and its number goes, so it cannot
   be read as "step 2 of 4, idle". */
.ip-progress__item.is-working .ip-progress__dot {
	background: transparent;
	color: transparent;
	border: 2px solid var(--ip-mint-deep);
	border-top-color: var(--ip-teal);
	animation: ip-progress-turn 800ms linear infinite;
}
@keyframes ip-progress-turn { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
	.ip-progress__item.is-working .ip-progress__dot { animation-duration: 2400ms; }
}

/* A DNS record to copy into someone else's control panel, one field a row. */
.ip-record { display: flex; flex-direction: column; gap: 6px; }
.ip-record__row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; }
.ip-record__key { flex: 0 0 56px; color: var(--ip-muted); }
.ip-record__val { flex: 1; min-width: 0; font-family: var(--ip-mono); overflow-wrap: anywhere; }

/* Notice — an inline result that has to stay on screen. A toast is wrong for
   anything the user still has to read and act on, and a dialog is too much for
   "that password was rejected". */
.ip-notice {
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	padding: 11px 13px;
	font-size: 12.5px;
	line-height: 1.55;
	background: var(--ip-surface-sunk);
	color: var(--ip-ink-soft);
}
.ip-notice--ok    { background: var(--ip-mint);      border-color: var(--ip-mint-deep);    color: var(--ip-teal); }
.ip-notice--error { background: var(--ip-danger-bg); border-color: var(--ip-danger-border); color: var(--ip-danger); }
.ip-notice--warn  { background: var(--ip-amber-bg);  border-color: var(--ip-amber-bg);     color: var(--ip-amber-fg); }
.ip-notice ul { margin: 0; padding-left: 18px; }

/* Flyout — the entry detail sheet.
   A sheet rather than a dialog because it is a *reading* surface opened from a
   row: it keeps the list behind it, so closing puts you back where you were
   rather than on a screen you have to find your place in again. Above the top
   bar (see .app-topbar for the order), and below Bootstrap's modal backdrop at
   1050: the dialog it launches (form-modal, 1055) opens over it and both stay
   open. */
.ip-flyout__scrim {
	position: fixed;
	inset: 0;
	background: var(--ip-scrim);
	z-index: 1040;
}

.ip-flyout {
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	max-width: 560px;
	background: var(--ip-bg);
	z-index: 1041;
	box-shadow: -16px 0 48px var(--ip-shadow-3);
	display: flex;
	flex-direction: column;
	animation: ip-slide-in 220ms ease both;
}
@keyframes ip-slide-in {
	from { opacity: 0; transform: translateX(18px); }
	to   { opacity: 1; transform: none; }
}

/* Head and foot are fixed so the primary action stays reachable however long the
   body runs — an entry with twenty attendees must not bury its Edit button. */
.ip-flyout__head {
	padding: 20px 26px 16px;
	border-bottom: 1px solid var(--ip-border);
	background: var(--ip-surface);
	flex-shrink: 0;
}
.ip-flyout__title { font-size: 19px; font-weight: 600; letter-spacing: -0.015em; margin-top: 10px; line-height: 1.3; }
.ip-flyout__sub   { font-size: 12.5px; color: var(--ip-muted); margin-top: 6px; }
.ip-flyout__close {
	background: transparent;
	border: 1px solid var(--ip-border);
	border-radius: 8px;
	width: 30px;
	height: 30px;
	flex: 0 0 30px;
	display: grid;
	place-items: center;
	cursor: pointer;
	color: var(--ip-muted);
	font-size: 14px;
}
.ip-flyout__close:hover { background: var(--ip-surface-sunk); }

.ip-flyout__body {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	/* Contained so reaching the end of the sheet does not start scrolling the
	   list behind it — the body is not scroll-locked, because the edit dialog
	   this launches restores `body { overflow }` when it closes and would
	   silently undo a lock set here. */
	overscroll-behavior: contain;
	padding: 20px 26px 26px;
	display: flex;
	flex-direction: column;
	gap: 16px;
}
/* Cards keep their content height and the body scrolls.
   Without this the sheet silently swallowed rows: `.ip-card--flush` carries
   `overflow: hidden`, which zeroes a flex item's automatic minimum size, so an
   over-tall body was resolved by crushing those cards instead of scrolling —
   a one-person crew list collapsed to just its heading. */
.ip-flyout__body > * { flex-shrink: 0; }
.ip-flyout__foot {
	padding: 14px 26px;
	border-top: 1px solid var(--ip-border);
	background: var(--ip-surface);
	display: flex;
	align-items: center;
	gap: 10px;
	flex-shrink: 0;
}
.ip-flyout__note { flex: 1; font-size: 11.5px; color: var(--ip-faint); line-height: 1.4; }

/* A row inside a flyout card — crew, evidence, travel. */
.ip-flyout__row {
	display: flex;
	align-items: center;
	gap: 11px;
	padding: 11px 18px;
	border-top: 1px solid var(--ip-rule);
}

/* Modal — level with Bootstrap's own dialogs, which it stands beside. At 50 it
   sat under the top bar, and the bar stayed bright and clickable over the
   backdrop. */
.ip-modal {
	position: fixed;
	inset: 0;
	background: var(--ip-scrim);
	z-index: 1055;
	display: grid;
	place-items: center;
	padding: 24px;
	overflow-y: auto;
}
.ip-modal[hidden] { display: none; }

.ip-modal__panel {
	background: var(--ip-surface);
	border-radius: 16px;
	width: 100%;
	max-width: 520px;
	box-shadow: 0 24px 60px var(--ip-shadow-4);
	overflow: hidden;
	animation: ip-rise-in 200ms ease both;
}
.ip-modal__head { padding: 20px 24px 16px; border-bottom: 1px solid var(--ip-border-soft); }
.ip-modal__title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.ip-modal__sub { font-size: 12.5px; color: var(--ip-muted); margin-top: 4px; line-height: 1.5; }
.ip-modal__body { padding: 22px 24px; display: flex; flex-direction: column; gap: 16px; }
.ip-modal__foot {
	padding: 14px 24px;
	border-top: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	display: flex;
	align-items: center;
	gap: 10px;
}
.ip-modal__hint {
	flex: 1;
	display: flex;
	align-items: center;
	gap: 7px;
	font-size: 11.5px;
	color: var(--ip-faint);
}
.ip-modal__hint.is-error { color: var(--ip-danger); }
/* Sized to the line it sits on rather than Bootstrap's default, and coloured
   by the hint so it turns red with the text when something failed. */
.ip-modal__hint .spinner-border {
	flex: 0 0 auto;
	width: 12px;
	height: 12px;
	border-width: 1.5px;
	color: inherit;
}

/* Program picker — the segmented control from the add-team dialog */
.ip-picker-row { display: flex; gap: 8px; }
.ip-picker-btn {
	flex: 1;
	border: 1.5px solid var(--ip-border-input);
	background: var(--ip-surface);
	color: var(--ip-muted);
	border-radius: 10px;
	padding: 10px;
	font-size: 13px;
	font-weight: 600;
	cursor: pointer;
	font-family: var(--ip-mono);
}
.ip-picker-btn:hover { border-color: var(--ip-teal); }
/* The program's own colour when picked — from "Programs", like every tag. */
.ip-picker-btn.is-on { background: var(--prog); color: var(--prog-fg); border-color: var(--prog); }

/* -----------------------------------------------------------------------------
   Screen-specific compositions
   -------------------------------------------------------------------------- */

/* Signed-out pages — sign in, register, join, the two password pages, and the
   error page (AuthCard.php). One narrow card on the app's own background,
   labels above the fields like every other form here, and the wordmark closing
   the page (Wordmark.php). The viewport scrolls these, not an inner column. */
body.minimal-page-body { min-height: 100svh; overflow-y: auto; }

/* The viewport scrolls these pages — body's overflow is handed up to it — and
   scrollbar-gutter does not travel with it, so it is set on the root. Room for
   the scrollbar is kept either way, so a form that grows an error line does not
   push the card sideways. Scoped to these pages: the full shell scrolls
   #app-main instead, and a gutter here would be an empty strip. */
html:has(> body.minimal-page-body) { scrollbar-gutter: stable; }

.minimal-page-main {
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding: 48px 16px;
}

.ip-auth { width: 100%; max-width: 400px; margin: 0 auto; }
.ip-auth__card { padding: 28px 28px 24px; }
.ip-auth__title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.25; margin: 0 0 18px; }
.ip-auth__sub { font-size: 13px; color: var(--ip-muted); line-height: 1.5; margin: -12px 0 22px; }
.ip-auth__sub strong { color: var(--ip-ink); font-weight: 600; }
.ip-auth__label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.ip-auth__label-row a { font-size: 12px; }
.ip-auth__remember { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; cursor: pointer; margin: 0; }
.ip-auth__remember input { accent-color: var(--ip-teal); width: 16px; height: 16px; margin: 2px 0 0; flex: 0 0 16px; }
.ip-auth__hint { display: block; }
.ip-auth__alt { text-align: center; font-size: 13px; color: var(--ip-muted); margin: 18px 0 0; }
.ip-auth .ip-input:disabled { color: var(--ip-muted); background: var(--ip-surface-sunk); }

.ip-wordmark {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 8px;
	margin-top: 40px;
	font-size: 12.5px;
	color: var(--ip-muted);
}
.ip-wordmark img { width: 240px; max-width: 70vw; }

/* Help, in the corner the toasts also use — below them, so a toast covers it
   rather than the other way round. */
.ip-help { position: fixed; right: 16px; bottom: 16px; z-index: 1050; box-shadow: var(--ip-shadow-sm); }

/* A phone: the card starts at the top rather than floating in the middle of a
   screen the keyboard is about to cover half of. */
@media (max-width: 500px) {
	.minimal-page-main { min-height: auto; justify-content: flex-start; padding-top: 24px; }
	.ip-auth__card { padding: 22px 18px 18px; }
}

/* API docs (/docs/api/v3) — Swagger UI inside a card, on the signed-out
   page frame since the docs are read without the app around them.

   Swagger draws in its own light palette, which in the dark theme left dark
   text on a dark card. Its colours are pointed at the tokens here rather than
   patched per element, so it follows the theme switch like everything else.
   The code samples keep Swagger's own dark block: they are dark in both. */
.minimal-page-main:has(.ip-docs) { justify-content: flex-start; }
.ip-docs { width: 100%; max-width: 1100px; margin: 0 auto; }
.ip-docs__head { margin-bottom: 18px; }
.ip-docs__title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
.ip-docs__sub { font-size: 13px; color: var(--ip-muted); line-height: 1.5; margin: 6px 0 0; }
.ip-docs__body { padding: 8px 20px 20px; overflow-x: auto; }

.ip-docs .swagger-ui,
.ip-docs .swagger-ui .info .title,
.ip-docs .swagger-ui .info p,
.ip-docs .swagger-ui .info li,
.ip-docs .swagger-ui .opblock-tag,
.ip-docs .swagger-ui .opblock .opblock-summary-description,
.ip-docs .swagger-ui .opblock .opblock-summary-path,
.ip-docs .swagger-ui .opblock .opblock-section-header h4,
.ip-docs .swagger-ui .opblock-description-wrapper p,
.ip-docs .swagger-ui .parameter__name,
.ip-docs .swagger-ui .parameter__type,
.ip-docs .swagger-ui .response-col_status,
.ip-docs .swagger-ui .response-col_description,
.ip-docs .swagger-ui .responses-inner h4,
.ip-docs .swagger-ui .responses-inner h5,
.ip-docs .swagger-ui table thead tr th,
.ip-docs .swagger-ui table thead tr td,
.ip-docs .swagger-ui .tab li,
.ip-docs .swagger-ui .model-title,
.ip-docs .swagger-ui .model,
.ip-docs .swagger-ui section.models h4,
.ip-docs .swagger-ui .prop-type,
.ip-docs .swagger-ui label,
.ip-docs .swagger-ui .btn { color: var(--ip-ink); font-family: var(--ip-sans); }
.ip-docs .swagger-ui .parameter__in,
.ip-docs .swagger-ui .prop-format,
.ip-docs .swagger-ui .renderedMarkdown p { color: var(--ip-muted); }
.ip-docs .swagger-ui .info { margin: 18px 0; }
.ip-docs .swagger-ui .scheme-container { background: transparent; box-shadow: none; padding: 0 0 12px; }
.ip-docs .swagger-ui .opblock-tag { border-bottom-color: var(--ip-border-soft); }
.ip-docs .swagger-ui .opblock .opblock-section-header { background: var(--ip-surface-sunk); box-shadow: none; }
.ip-docs .swagger-ui section.models { border-color: var(--ip-border); }
.ip-docs .swagger-ui section.models .model-container { background: var(--ip-surface-sunk); }
.ip-docs .swagger-ui .arrow,
.ip-docs .swagger-ui .expand-operation svg,
.ip-docs .swagger-ui .opblock-control-arrow { fill: var(--ip-muted); }
.ip-docs .swagger-ui input[type="text"],
.ip-docs .swagger-ui select,
.ip-docs .swagger-ui textarea {
	background: var(--ip-input-bg);
	color: var(--ip-ink);
	border: 1px solid var(--ip-border-input);
	border-radius: 7px;
}
.ip-docs .swagger-ui .btn { border-color: var(--ip-border-input); border-radius: 8px; box-shadow: none; }
.ip-docs .swagger-ui .btn.authorize { color: var(--ip-teal); border-color: var(--ip-teal); }
.ip-docs .swagger-ui .btn.authorize svg { fill: var(--ip-teal); }
.ip-docs .swagger-ui .btn.execute { background: var(--ip-teal); border-color: var(--ip-teal); color: var(--ip-on-accent); }

/* Time zone converter — the two clocks, with the swap between them. Stacks on a
   phone, where the swap turns to point down the page. */
.ip-tz { display: grid; grid-template-columns: 1fr auto 1fr; gap: 14px; align-items: stretch; }
.ip-tz__swap { align-self: center; }
.ip-tz__clock {
	font-family: var(--ip-mono);
	font-size: 34px;
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1.1;
	color: var(--ip-ink);
}
.ip-tz__meta { font-size: 12px; color: var(--ip-muted); min-height: 1.5em; }
@media (max-width: 720px) {
	.ip-tz { grid-template-columns: 1fr; }
	.ip-tz__swap { justify-self: center; }
	.ip-tz__swap .bi { transform: rotate(90deg); }
}

/* Dashboard */
/* Halves, and the same gap as the KPI band above, so the seam between these two
   cards lands exactly on the band's column 2/3 gutter. A 1.35fr/1fr split put it
   about 7% to the right of that, which reads as a near-miss rather than a
   choice — the eye tracks the vertical edge straight up and finds it moved. */
.ip-dash-split {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 14px;
	align-items: stretch;
}
@media (max-width: 900px) { .ip-dash-split { grid-template-columns: 1fr; } }

/* The two cards hold different amounts of content — a chart with a fixed bar
   height, a list of meters — so stretching them to match left a slab of empty
   card below the bars. Growing the chart's own body to fill the stretched
   card, instead, gives it the same presence as the meters beside it. */
.ip-dash-split > .ip-card { display: flex; flex-direction: column; }
.ip-dash-split #dash-months { flex: 1; display: flex; }
.ip-dash-split #dash-months .ip-bars { flex: 1; height: auto; }

.ip-award-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 16px; }
.ip-award__score { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; line-height: 1; }
.ip-award__check { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; }
.ip-award__mark {
	width: 16px;
	height: 16px;
	flex: 0 0 16px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	font-size: 9px;
	font-weight: 700;
	margin-top: 1px;
	background: var(--ip-mint);
	color: var(--ip-teal);
}
.ip-award__mark.is-gap { background: var(--ip-amber-bg); color: var(--ip-amber-fg); }

/* -----------------------------------------------------------------------------
   Shared lists — the workspace's own drop-down vocabularies

   A rail of lists beside one editor, rather than a card per list: there are six
   of them, and stacking six cards would push seasons and accounts off the page.
   The rail wraps to a full-width strip on a narrow screen, which is what the
   `flex: 999` on the pane buys — the pane takes every spare pixel until the rail
   can no longer sit beside it, and then the two stack.
   -------------------------------------------------------------------------- */

.ip-lists { display: flex; flex-wrap: wrap; align-items: stretch; }

.ip-lists__rail {
	flex: 1 1 190px;
	max-width: 100%;
	border-right: 1px solid var(--ip-border-soft);
	border-bottom: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	padding: 12px;
	display: flex;
	flex-direction: column;
	gap: 3px;
}
.ip-lists__tab {
	display: flex;
	align-items: center;
	gap: 8px;
	text-align: left;
	background: transparent;
	border: 1px solid transparent;
	color: var(--ip-label);
	border-radius: 9px;
	padding: 9px 11px;
	font-size: 12.5px;
	font-family: inherit;
	cursor: pointer;
}
.ip-lists__tab:hover { background: var(--ip-surface); }
.ip-lists__tab.is-active {
	background: var(--ip-surface);
	border-color: var(--ip-border);
	color: var(--ip-ink);
	font-weight: 600;
}
.ip-lists__tab-label { flex: 1; min-width: 0; }
.ip-lists__tab-count { font-family: var(--ip-mono); font-size: 11px; color: var(--ip-faint); }

.ip-lists__pane { flex: 999 1 340px; padding: 18px 20px 20px; min-width: 0; }
.ip-lists__where { font-size: 11px; color: var(--ip-faint); margin-top: 6px; font-family: var(--ip-mono); }

.ip-lists__item {
	display: flex;
	align-items: flex-start;
	gap: 11px;
	flex-wrap: wrap;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	padding: 9px 11px;
	background: var(--ip-surface);
}
/* Retired, not deleted — legible enough to read and put back, quiet enough to
   read as "not on offer". */
.ip-lists__item.is-off { opacity: 0.55; }

.ip-lists__move {
	background: transparent;
	border: 0;
	color: var(--ip-faint);
	font-size: 10px;
	line-height: 1;
	cursor: pointer;
	padding: 2px;
}
.ip-lists__move:hover:not(:disabled) { color: var(--ip-ink); }
.ip-lists__move:disabled { opacity: 0.35; cursor: default; }

.ip-lists__swatch {
	width: 20px;
	height: 20px;
	flex: 0 0 20px;
	border-radius: 6px;
	border-width: 1.5px;
	border-style: solid;
	cursor: pointer;
	padding: 0;
}

/* Edited in place rather than through a dialog: renaming six lists of six is
   the whole task on this screen, and a modal each time is six clicks per edit. */
.ip-lists__edit {
	min-width: 0;
	border: 1px solid transparent;
	border-radius: 7px;
	padding: 5px 7px;
	background: transparent;
	color: var(--ip-ink);
	font-family: inherit;
	font-size: 13px;
	font-weight: 500;
}
.ip-lists__edit:hover { border-color: var(--ip-border-soft); }
.ip-lists__edit:focus {
	border-color: var(--ip-border-input);
	background: var(--ip-surface-sunk);
	outline: none;
}
.ip-lists__edit--hint {
	width: 100%;
	padding-left: 35px;
	font-size: 12px;
	font-weight: 400;
	color: var(--ip-muted);
}
.ip-lists__edit--hint::placeholder { color: var(--ip-placeholder); }

.ip-lists__uses { font-size: 11px; font-family: var(--ip-mono); color: var(--ip-faint); white-space: nowrap; }

/* What a role lets somebody do, on the one list that carries capabilities. */
.ip-lists__cap {
	display: flex;
	align-items: center;
	gap: 4px;
	font-size: 11px;
	color: var(--ip-muted);
	white-space: nowrap;
	cursor: pointer;
}
.ip-lists__cap input { margin: 0; cursor: pointer; }

.ip-lists__pill {
	border: 0;
	border-radius: 999px;
	padding: 4px 10px;
	font-size: 11px;
	font-weight: 600;
	font-family: inherit;
	cursor: pointer;
	white-space: nowrap;
	background: var(--ip-mint);
	color: var(--ip-teal);
}
.ip-lists__pill.is-off { background: var(--ip-rule); color: var(--ip-muted); }
.ip-lists__pill:hover { box-shadow: inset 0 0 0 40px var(--ip-veil-hover); }

.ip-lists__remove {
	background: transparent;
	border: 0;
	color: var(--ip-faint);
	font-size: 12px;
	cursor: pointer;
	padding: 4px;
}
.ip-lists__remove:hover { color: var(--ip-danger); }

.ip-lists__note {
	font-size: 11.5px;
	color: var(--ip-faint);
	margin-top: 12px;
	line-height: 1.5;
	text-wrap: pretty;
}

@media (max-width: 640px) {
	.ip-lists__rail { border-right: 0; flex-direction: row; flex-wrap: wrap; }
	.ip-lists__tab { flex: 1 1 auto; }
}

/* -----------------------------------------------------------------------------
   Season plan

   The goals the Learning screen measures the roster against. Borrows the list
   editor's move buttons, pill and remove — same card, same gestures — and adds
   only what a goal has that a list value does not: the target and its skills.
   -------------------------------------------------------------------------- */

/* The body of the Teams and Season plan cards in settings. A second rule used to
   give it a surface, border and shadow of its own, from when these panels stood
   alone; inside the card that drew a card within a card. */
.ip-plan { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 14px; }
.ip-plan__list { display: flex; flex-direction: column; gap: 7px; }

.ip-plan__item {
	display: flex;
	align-items: flex-start;
	gap: 11px;
	flex-wrap: wrap;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	padding: 10px 11px;
	background: var(--ip-surface);
}
.ip-plan__item.is-off { opacity: 0.55; }

/* The number reads first. It is the whole difference between a goal and a
   wish — "five members", not "members". */
.ip-plan__target {
	flex: 0 0 auto;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	min-width: 46px;
	padding: 4px 6px;
	border-radius: 9px;
	background: var(--ip-surface-sunk);
	border: 1px solid var(--ip-border-soft);
}
.ip-plan__target-num {
	font-size: 17px;
	font-weight: 650;
	line-height: 1.1;
	color: var(--ip-ink);
	font-family: var(--ip-mono);
}
.ip-plan__target-word { font-size: 9.5px; color: var(--ip-faint); text-transform: uppercase; letter-spacing: 0.04em; }

.ip-plan__title { font-size: 13.5px; font-weight: 600; line-height: 1.35; text-wrap: pretty; }
.ip-plan__detail {
	font-size: 12px;
	color: var(--ip-muted);
	margin-top: 3px;
	line-height: 1.5;
	text-wrap: pretty;
}

.ip-plan__skills { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }

/* Coloured inline from the workspace's palette, so this rule carries shape
   only — see DESIGN.md on why a literal colour below :root is a bug. */
.ip-plan__skill {
	font-size: 11px;
	font-weight: 550;
	padding: 2px 8px;
	border-radius: 999px;
	white-space: nowrap;
}
.ip-plan__skill.is-off { text-decoration: line-through; opacity: 0.7; }

.ip-plan__none { font-size: 11.5px; color: var(--ip-faint); font-style: italic; }

/* -----------------------------------------------------------------------------
   Linked evidence — activities attached to an award criterion

   The chip is a count on a sub-point; the row is one activity in the dialog
   behind it.
   -------------------------------------------------------------------------- */

.ip-evidence-chip {
	flex: 0 0 auto;
	min-width: 20px;
	padding: 1px 7px;
	border-radius: 999px;
	border: 1px solid var(--ip-mint-deep);
	background: var(--ip-mint);
	color: var(--ip-teal);
	font-family: var(--ip-mono);
	font-size: 10.5px;
	line-height: 1.6;
	cursor: pointer;
}
.ip-evidence-chip:hover { background: var(--ip-mint-deep); }

.ip-evidence-row {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	padding: 10px 12px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	background: var(--ip-surface);
	text-decoration: none;
	color: inherit;
}
.ip-evidence-row:hover { border-color: var(--ip-mint-deep); background: var(--ip-surface-sunk); }

.ip-evidence-row__date { flex: 0 0 auto; font-size: 11px; color: var(--ip-faint); padding-top: 2px; }
.ip-evidence-row__name { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
.ip-evidence-row__note {
	font-size: 12px;
	color: var(--ip-muted);
	margin-top: 3px;
	line-height: 1.45;
	/* Two lines, then cut: a dialog listing twenty activities is a list, and a
	   paragraph on each one stops it being scannable. */
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}
.ip-evidence-row__subs { display: block; font-size: 11px; color: var(--ip-faint); margin-top: 5px; line-height: 1.45; }

/* -----------------------------------------------------------------------------
   Approval queue
   -------------------------------------------------------------------------- */

/* Sticky at the left edge, not the top: the row below is 900px wide and scrolls
   sideways, and a header that travelled with it would carry the filters off. */
.ip-approvals__head {
	position: sticky;
	left: 0;
	display: flex;
	align-items: center;
	gap: 12px;
	flex-wrap: wrap;
	padding: 16px 18px;
	border-bottom: 1px solid var(--ip-border-soft);
	background: var(--ip-surface);
}

.ip-approvals__row {
	display: grid;
	grid-template-columns: minmax(220px, 1fr) 150px 130px 96px 250px;
	min-width: 900px;
	gap: 12px;
	align-items: center;
	padding: 13px 18px;
	border-bottom: 1px solid var(--ip-rule);
	font-size: 13px;
}
.ip-approvals__row--head {
	padding: 11px 18px;
	border-bottom: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--ip-faint);
}
.ip-approvals__row.is-clickable { cursor: pointer; }
.ip-approvals__row.is-clickable:hover { background: var(--ip-surface-sunk); }

/* The question a mentor sent back, shown on the row rather than behind a click:
   it is the reason the entry is still here. */
.ip-approvals__asked {
	font-size: 11.5px;
	color: var(--ip-amber);
	margin-top: 5px;
	line-height: 1.45;
	text-wrap: pretty;
}

.ip-approvals__filter {
	border: 1px solid var(--ip-border-input);
	background: var(--ip-surface);
	color: var(--ip-label);
	border-radius: 7px;
	padding: 6px 12px;
	font-size: 11.5px;
	font-weight: 500;
	font-family: inherit;
	cursor: pointer;
}
.ip-approvals__filter.is-active {
	background: var(--ip-mint);
	border-color: var(--ip-teal);
	color: var(--ip-teal);
}

.ip-approvals__empty {
	padding: 26px 18px;
	text-align: center;
	font-size: 12.5px;
	color: var(--ip-faint);
	line-height: 1.6;
}

.ip-gaps { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; }
.ip-gap { border-left: 3px solid var(--ip-amber); }
.ip-gap.is-strong { border-left-color: var(--ip-teal); }
.ip-gap__tag {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-amber);
}
.ip-gap.is-strong .ip-gap__tag { color: var(--ip-teal); }
.ip-gap__title { font-size: 13.5px; font-weight: 500; margin-top: 8px; line-height: 1.4; }
.ip-gap__body { font-size: 12px; color: var(--ip-muted); margin-top: 6px; line-height: 1.5; }

.ip-program-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-top: 16px; }
.ip-program {
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	padding: 14px;
	background: var(--ip-surface-sunk);
}
.ip-program__count { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; }
.ip-program__name { font-size: 12.5px; font-weight: 600; }
.ip-program__detail { font-size: 11.5px; color: var(--ip-muted); margin-top: 6px; line-height: 1.5; }

/* Log activity */
.ip-log-layout { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.ip-log-main { flex: 1 1 470px; min-width: 0; }
.ip-log-rail { flex: 1 1 272px; min-width: 0; max-width: 296px; }

.ip-steps {
	display: flex;
	position: relative;
	border-bottom: 1px solid var(--ip-border-soft);
	padding: 0 8px;
}

/* One underline that travels, rather than a border lit on whichever step is
   current. The movement is what says "you advanced" — a border swapping colour
   in place says only that something changed, and leaves the eye to work out
   which direction it went.

   Every step is `flex: 1`, so one step is the track divided by the count and
   translateX(index * 100%) — a percentage of the indicator's own width — lands
   on step `index` exactly, whatever the count. Both are set from JS; the
   fallbacks keep it parked under step one if they never are.

   `bottom: 0` puts it in the last 2px of the step, which is where the current
   step's own bottom border used to sit, so nothing shifts. */
.ip-steps__indicator {
	position: absolute;
	left: 8px;
	bottom: 0;
	height: 2px;
	width: calc((100% - 16px) / var(--ip-step-count, 1));
	background: var(--ip-teal);
	border-radius: 2px;
	transform: translateX(calc(var(--ip-step-index, 0) * 100%));
	transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
}

.ip-step {
	flex: 1;
	background: transparent;
	border: 0;
	/* Transparent, and kept: the indicator above draws the visible line, but the
	   border still has to hold the 2px the layout was built around. */
	border-bottom: 2px solid transparent;
	padding: 14px 6px 12px;
	cursor: pointer;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 6px;
	color: var(--ip-muted);
	transition: color 200ms ease;
}
.ip-step.is-current { color: var(--ip-teal); }
.ip-step__dot {
	width: 22px;
	height: 22px;
	border-radius: 50%;
	background: var(--ip-track);
	color: var(--ip-faint);
	display: grid;
	place-items: center;
	font-size: 11px;
	font-weight: 600;
	transition: background-color 240ms ease, color 240ms ease, transform 240ms ease;
}
.ip-step.is-current .ip-step__dot {
	background: var(--ip-teal);
	color: var(--ip-on-accent);
	transform: scale(1.09);
}
.ip-step.is-done .ip-step__dot { background: var(--ip-mint); color: var(--ip-teal); }
.ip-step__label { font-size: 11.5px; font-weight: 500; }

.ip-step-panel { padding: 26px 26px 22px; min-height: 430px; }

/* Applied only when the step actually changes — `render()` also runs on every
   field edit, and a panel that faded in each time somebody ticked a box would
   be noise rather than feedback. See `paintSteps()` in log.js. */
@keyframes ip-step-enter {
	from { opacity: 0; transform: translateY(5px); }
	to   { opacity: 1; transform: none; }
}
.ip-step-panel--enter { animation: ip-step-enter 240ms ease both; }
.ip-step-panel__title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.ip-step-panel__sub { font-size: 13px; color: var(--ip-muted); margin-top: 4px; line-height: 1.5; }

.ip-step-foot {
	padding: 14px 20px;
	border-top: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	display: flex;
	align-items: center;
	gap: 10px;
}
.ip-step-foot__progress { flex: 1; font-size: 12px; color: var(--ip-faint); }

.ip-type-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; margin-top: 22px; }
.ip-type {
	text-align: left;
	background: var(--ip-surface);
	border: 1.5px solid var(--ip-border);
	border-radius: 11px;
	padding: 15px;
	cursor: pointer;
	display: flex;
	gap: 12px;
	align-items: flex-start;
}
.ip-type:hover { border-color: var(--ip-teal); }
.ip-type.is-on { background: var(--ip-mint); border-color: var(--ip-teal); }
.ip-type__glyph {
	width: 32px;
	height: 32px;
	flex: 0 0 32px;
	border-radius: 9px;
	background: var(--ip-rule);
	color: var(--ip-muted);
	display: grid;
	place-items: center;
	font-size: 15px;
	line-height: 1;
}
.ip-type.is-on .ip-type__glyph { background: var(--ip-mint-deep); color: var(--ip-teal); }
.ip-type__name { font-size: 13.5px; font-weight: 600; }
.ip-type__hint { font-size: 11.5px; color: var(--ip-muted); line-height: 1.45; }

/* The same types as pills, for the one-page variant */
.ip-type-pill {
	display: flex;
	align-items: center;
	gap: 8px;
	background: var(--ip-surface);
	border: 1.5px solid var(--ip-border);
	border-radius: 999px;
	padding: 8px 14px;
	cursor: pointer;
	font-size: 13px;
	font-weight: 500;
}
.ip-type-pill:hover { border-color: var(--ip-teal); }
.ip-type-pill.is-on { background: var(--ip-mint); border-color: var(--ip-teal); }
.ip-type-pill__glyph { font-family: var(--ip-mono); font-size: 12px; color: var(--ip-muted); }
.ip-type-pill.is-on .ip-type-pill__glyph { color: var(--ip-teal); }

.ip-member-row {
	display: flex;
	align-items: center;
	gap: 12px;
	border: 1px solid var(--ip-border);
	background: var(--ip-surface);
	border-radius: 10px;
	padding: 9px 12px;
}
.ip-member-row.is-on { border-color: var(--ip-teal); background: var(--ip-mint); }
.ip-member-row__name { flex: 1; font-size: 13px; font-weight: 500; }
.ip-member-row__role { font-size: 11.5px; color: var(--ip-faint); }

/* Attendance grid — a person per row, a day per column.
   Used where an activity runs over several days and the roster differs between
   them, which the single-tick member list cannot express. The columns are fixed
   width and the whole thing scrolls sideways rather than shrinking: a tick
   target that narrows with every extra day stops being tappable at four. */
/* The scroll box. Deliberately NOT a grid: the rows are the grids, and making
   this one too would auto-place each row into the column tracks and lay the
   whole table out on a single line. */
.ip-attend {
	overflow: auto;
	max-height: 340px;
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	background: var(--ip-surface);
}
/* Sized to its content so every row is the same width and they scroll as one;
   `min-width: 100%` keeps the borders full-bleed when the days fit. */
.ip-attend__table {
	width: max-content;
	min-width: 100%;
}
.ip-attend__row {
	display: grid;
	grid-template-columns: var(--attend-cols);
	align-items: center;
	gap: 12px;
	padding: 7px 12px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-attend__row:first-child { border-top: 0; }
.ip-attend__row.is-on { background: var(--ip-mint); }
/* Sticky vertically only — it scrolls sideways with the days it labels. */
.ip-attend__row--head {
	background: var(--ip-rule);
	position: sticky;
	top: 0;
	z-index: 1;
}
.ip-attend__hrs { text-align: right; }
.ip-attend__label {
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-faint);
}
.ip-attend__name {
	font-size: 13px;
	font-weight: 500;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ip-attend__day { display: flex; flex-direction: column; align-items: center; line-height: 1.25; }
.ip-attend__daynum { font-size: 10px; font-weight: 600; color: var(--ip-muted); }
.ip-attend__date { font-size: 10px; color: var(--ip-faint); font-family: var(--ip-mono); }

/* The cell is the control: a whole tappable square rather than a checkbox with
   a label beside it, because the label is the column heading. */
.ip-attend__cell {
	width: 30px;
	height: 30px;
	margin: 0 auto;
	border-radius: 8px;
	border: 1.5px solid var(--ip-box-border);
	background: var(--ip-surface);
	color: var(--ip-faint);
	font-size: 12px;
	cursor: pointer;
	display: grid;
	place-items: center;
	padding: 0;
}
.ip-attend__cell.is-on {
	border-color: var(--ip-teal);
	background: var(--ip-teal);
	color: var(--ip-on-accent);
}
.ip-attend__cell:disabled { opacity: 0.4; cursor: default; }
.ip-attend__note { font-size: 11.5px; color: var(--ip-faint); margin-top: 8px; }

/* The square at the head of a file row — a preview when the file is a picture,
   its glyph when it is not.

   Fixed 1:1 and `flex: none` so both branches occupy the identical box: the
   names beside them line up down the column, and a list of mixed types has no
   ragged left edge. `overflow: hidden` with `object-fit: cover` crops a
   photograph of any aspect ratio to the square rather than letterboxing it. */
.ip-file-thumb {
	position: relative;
	flex: none;
	width: 32px;
	height: 32px;
	border-radius: 6px;
	overflow: hidden;
	background: var(--ip-mint);
	color: var(--ip-teal);
	display: grid;
	place-items: center;
	font-size: 15px;
}

.ip-file-thumb img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	/* Held back until the bytes are there, so the shimmer underneath is what
	   shows rather than a half-painted thumbnail. `.gal-thumb img` does the
	   same, for the same reason. */
	opacity: 0;
	transition: opacity 0.2s ease;
}

/* The skeleton fills the frame and covers the glyph while the bytes are on
   their way. `skeleton-image` inherits the border radius, so it takes the box's
   own corners rather than looking pasted into it. Its fade-out on the way back
   out is shared with the other two frames — see the `.skeleton` rules. */
.ip-file-thumb .skeleton { position: absolute; inset: 0; display: block; }

.ip-file-thumb.is-ready img { opacity: 1; }

/* No fade on the glyph, and none wanted: at the moment this lands the shimmer
   above it is still fully opaque, so it goes while nothing can see it. What
   shows through the crossfade is the frame's own mint, which is the surface the
   rest of the thumbnail is anyway.

   Removed rather than left underneath, for the reason the shared rules give:
   the picture settling over it may have transparent parts, and a glyph inside
   somebody's logo is worse than no glyph at all. */
.ip-file-thumb.is-ready > i { display: none; }

/* One-page variant sections */
.ip-section { overflow: hidden; }
.ip-section__toggle {
	width: 100%;
	display: flex;
	align-items: center;
	gap: 12px;
	background: transparent;
	border: 0;
	padding: 16px 20px;
	cursor: pointer;
	text-align: left;
}
.ip-section__mark {
	width: 22px;
	height: 22px;
	border-radius: 50%;
	background: var(--ip-track);
	color: var(--ip-faint);
	display: grid;
	place-items: center;
	font-size: 11px;
	font-weight: 600;
}
.ip-section__mark.is-done { background: var(--ip-mint); color: var(--ip-teal); }
.ip-section__title { flex: 1; font-size: 13px; font-weight: 600; }
.ip-section__summary { font-size: 12px; color: var(--ip-muted); font-family: var(--ip-mono); }
.ip-section__caret { font-size: 10px; color: var(--ip-faint); }
.ip-section__body { padding: 0 20px 20px; }

.ip-summary-rail__headline { font-size: 15px; font-weight: 600; margin-top: 10px; line-height: 1.35; }
.ip-summary-rail__tip {
	background: var(--ip-bg);
	border-radius: 9px;
	padding: 12px;
	font-size: 12px;
	color: var(--ip-label);
	line-height: 1.55;
}

/* Teams */
.ip-teams-layout { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.ip-teams-main { flex: 1 1 440px; min-width: 0; }
.ip-teams-rail { flex: 1 1 330px; min-width: 0; max-width: 400px; }
.ip-people-rail { flex: 1 1 300px; min-width: 0; max-width: 330px; }

/* The rail's cap is for sitting beside the list. Once the two have wrapped —
   below both bases and the gap, about 788px of content — the rail is the full
   width under the list, and a capped one stopped short of the list's right
   edge. Queried against the layout, not the viewport: the sidebar comes and
   goes at 900px, so the viewport says nothing about the room here. */
.ip-teams-layout { container-type: inline-size; }
@container (max-width: 787px) {
	.ip-teams-rail,
	.ip-people-rail { max-width: none; }
}

.ip-team-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
.ip-team-card {
	text-align: left;
	background: var(--ip-surface);
	border: 1.5px solid var(--ip-border);
	border-radius: 12px;
	padding: 16px;
	cursor: pointer;
	box-shadow: var(--ip-shadow-sm);
	display: flex;
	flex-direction: column;
	gap: 10px;
}
.ip-team-card:hover { border-color: var(--ip-teal); }
.ip-team-card.is-on { border-color: var(--ip-teal); }
.ip-team-card__name { font-size: 14.5px; font-weight: 600; letter-spacing: -0.01em; }
.ip-team-card__help { font-size: 12px; color: var(--ip-muted); line-height: 1.5; }
.ip-team-card__stats {
	display: flex;
	gap: 14px;
	font-size: 11.5px;
	color: var(--ip-faint);
	font-family: var(--ip-mono);
	margin-top: 2px;
}

/* Programs — the one place a program is matched to its colour. Everything that
   shows a program (the .ip-prog tag, the add-team picker) reads --prog and
   --prog-fg from here rather than naming a colour itself, so changing a
   program's colour is one line in the token block. */
.ip-prog--FRC, .ip-picker-btn.is-FRC { --prog: var(--ip-frc); --prog-fg: var(--ip-frc-fg); }
.ip-prog--FTC, .ip-picker-btn.is-FTC { --prog: var(--ip-ftc); --prog-fg: var(--ip-ftc-fg); }
.ip-prog--FLL, .ip-picker-btn.is-FLL { --prog: var(--ip-fll); --prog-fg: var(--ip-fll-fg); }
.ip-prog--FGC, .ip-picker-btn.is-FGC { --prog: var(--ip-fgc); --prog-fg: var(--ip-fgc-fg); }

.ip-prog {
	display: inline-block;
	font-family: var(--ip-mono);
	font-size: 12px;
	font-weight: 600;
	line-height: 1.3;
	border-radius: 6px;
	padding: 3px 7px;
	white-space: nowrap;
	background: var(--prog, var(--ip-rule));
	color: var(--prog-fg, var(--ip-muted));
}

/* -----------------------------------------------------------------------------
   Team switcher — which of our teams you are looking at

   Header, top right. Sits beside the award-window control on the pages that
   have one, so it sheds the name and then the number as the viewport narrows
   rather than pushing anything off.
   -------------------------------------------------------------------------- */

/* The picture FIRST publishes, on the Settings row. White behind it for the
   same reason the org logo gets one: these are logos drawn for paper. */
.ip-team-avatar {
	flex: 0 0 auto;
	width: 30px;
	height: 30px;
	object-fit: contain;
	border-radius: 7px;
	background: #fff;
	border: 1px solid var(--ip-border-soft);
	padding: 2px;
}

.ip-mini-stats { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin: 18px 0; }
.ip-mini-stat {
	background: var(--ip-surface-sunk);
	border: 1px solid var(--ip-border-soft);
	border-radius: 9px;
	padding: 11px;
}
.ip-mini-stat__value { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
.ip-mini-stat__label { font-size: 10.5px; color: var(--ip-muted); margin-top: 2px; }

/* Evidence */
.ip-evidence-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
/* A button rather than a link: the tile selects into the rail, and the rail
   carries the link that opens the file. Hence the reset — a button brings its
   own font, padding and centred text. */
.ip-evidence {
	background: var(--ip-surface);
	border: 1px solid var(--ip-border);
	border-radius: 12px;
	overflow: hidden;
	box-shadow: var(--ip-shadow-sm);
	display: flex;
	flex-direction: column;
	color: inherit;
	text-decoration: none;
	font: inherit;
	text-align: left;
	padding: 0;
	width: 100%;
	cursor: pointer;
}
.ip-evidence:hover { border-color: var(--ip-teal); color: inherit; text-decoration: none; }
.ip-evidence.is-selected {
	border-color: var(--ip-teal);
	box-shadow: 0 0 0 1px var(--ip-teal), var(--ip-shadow-sm);
}
.ip-evidence__preview {
	height: 128px;
	background: repeating-linear-gradient(135deg, var(--ip-hatch-a) 0 8px, var(--ip-hatch-b) 8px 16px);
	display: grid;
	place-items: center;
	border-bottom: 1px solid var(--ip-border-soft);
	overflow: hidden;
}
.ip-evidence__preview img { width: 100%; height: 100%; object-fit: cover; }
.ip-evidence__kind {
	font-family: var(--ip-mono);
	font-size: 10.5px;
	color: var(--ip-hatch-fg);
	letter-spacing: 0.04em;
}
.ip-evidence__body { padding: 14px; display: flex; flex-direction: column; gap: 7px; flex: 1; }
.ip-evidence__title { font-size: 13.5px; font-weight: 600; line-height: 1.35; }
.ip-evidence__from { font-size: 11.5px; color: var(--ip-muted); line-height: 1.5; flex: 1; }
.ip-evidence__link {
	font-size: 11px;
	color: var(--ip-faint);
	border-top: 1px solid var(--ip-rule);
	padding-top: 8px;
	margin-top: 4px;
}

/* -----------------------------------------------------------------------------
   Linking an activity to award criteria — the log form's last step
   -------------------------------------------------------------------------- */

.ip-link-block {
	border-top: 1px solid var(--ip-border-soft);
	padding-top: 18px;
	margin-top: 2px;
}

.ip-link-row {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 10px;
	padding: 11px 13px;
	background: var(--ip-surface);
}
.ip-link-row.is-on { border-color: var(--ip-teal); background: var(--ip-mint); }

/* Carries the award's own colour, straight off the criteria file — the one
   place a literal colour is right, because it is data rather than styling. */
.ip-link-row__stripe { width: 3px; align-self: stretch; border-radius: 2px; flex: 0 0 3px; }

.ip-link-row__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.ip-link-row__award { font-size: 12px; font-weight: 600; }
.ip-link-row__text { font-size: 12.5px; color: var(--ip-label); line-height: 1.45; text-wrap: pretty; }
.ip-link-row__tag {
	font-family: var(--ip-mono);
	font-size: 9.5px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-faint);
	border: 1px solid var(--ip-border-soft);
	border-radius: 4px;
	padding: 2px 5px;
	white-space: nowrap;
}

.ip-link-browse {
	margin-top: 12px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 11px;
	padding: 13px;
	background: var(--ip-surface-sunk);
}

.ip-link-pick {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	text-align: left;
	width: 100%;
	background: var(--ip-surface);
	border: 1px solid var(--ip-border-soft);
	border-radius: 9px;
	padding: 9px 11px;
	font: inherit;
	color: inherit;
	cursor: pointer;
}
.ip-link-pick:hover { border-color: var(--ip-teal); }
.ip-link-pick.is-on { border-color: var(--ip-teal); background: var(--ip-mint); }

.ip-link-pick__mark {
	width: 16px;
	height: 16px;
	flex: 0 0 16px;
	margin-top: 1px;
	border-radius: 5px;
	border: 1.5px solid var(--ip-border-input);
	display: grid;
	place-items: center;
	font-size: 10px;
	font-weight: 700;
}
.ip-link-pick.is-on .ip-link-pick__mark {
	background: var(--ip-teal);
	border-color: var(--ip-teal);
	color: var(--ip-on-accent);
}
.ip-link-pick__text { flex: 1; min-width: 0; font-size: 12.5px; color: var(--ip-label); line-height: 1.45; text-wrap: pretty; }

/* Report builder */
.ip-report-layout { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.ip-report-side { flex: 1 1 276px; min-width: 0; max-width: 300px; }
.ip-report-main { flex: 1 1 470px; min-width: 0; }

.ip-output {
	display: flex;
	align-items: flex-start;
	gap: 9px;
	text-align: left;
	background: var(--ip-surface);
	border: 1.5px solid var(--ip-border);
	border-radius: 10px;
	padding: 11px 12px;
	cursor: pointer;
	width: 100%;
}
.ip-output:hover { border-color: var(--ip-teal); }
.ip-output.is-on { background: var(--ip-mint); border-color: var(--ip-teal); }
.ip-output__glyph { font-family: var(--ip-mono); font-size: 11px; color: var(--ip-teal); margin-top: 1px; }
.ip-output__name { font-size: 12.5px; font-weight: 600; }
.ip-output__hint { font-size: 11px; color: var(--ip-muted); line-height: 1.45; }

.ip-section-toggle {
	display: flex;
	align-items: center;
	gap: 9px;
	background: transparent;
	border: 0;
	padding: 8px 6px;
	border-radius: 7px;
	cursor: pointer;
	text-align: left;
	width: 100%;
}
.ip-section-toggle:hover { background: var(--ip-bg); }
.ip-section-toggle__label { font-size: 12.5px; flex: 1; }
.ip-section-toggle.is-off .ip-section-toggle__label { color: var(--ip-faint); }
.ip-section-toggle__words { font-size: 10.5px; color: var(--ip-faint); font-family: var(--ip-mono); }

.ip-doc { padding: 34px 40px 48px; max-width: 780px; }
.ip-doc__kicker {
	font-family: var(--ip-mono);
	font-size: 10.5px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--ip-teal);
}
.ip-doc__title {
	font-size: 30px;
	font-weight: 600;
	letter-spacing: -0.025em;
	margin-top: 10px;
	line-height: 1.15;
	text-wrap: pretty;
}
.ip-doc__meta { font-size: 13.5px; color: var(--ip-muted); margin-top: 10px; }
.ip-doc__stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
	gap: 12px;
	margin: 26px 0 30px;
	padding: 18px 0;
	border-top: 1px solid var(--ip-border);
	border-bottom: 1px solid var(--ip-border);
}
.ip-doc__stat-value { font-size: 25px; font-weight: 600; letter-spacing: -0.025em; }
.ip-doc__stat-label { font-size: 11px; color: var(--ip-muted); margin-top: 3px; line-height: 1.4; }
.ip-doc__body { display: flex; flex-direction: column; gap: 26px; }
.ip-doc__heading { font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; }
.ip-doc__text {
	font-size: 13.5px;
	line-height: 1.68;
	color: var(--ip-ink-soft);
	margin-top: 9px;
	text-wrap: pretty;
}
.ip-doc__cite { margin-top: 14px; border-left: 2px solid var(--ip-teal); padding: 4px 0 4px 14px; }
.ip-doc__cite-text { font-size: 13px; font-style: italic; color: var(--ip-ink-soft); line-height: 1.6; }
.ip-doc__cite-from { font-size: 11.5px; color: var(--ip-faint); margin-top: 6px; }

@media (max-width: 640px) { .ip-doc { padding: 22px 18px 32px; } }

/* Plans & billing — /organisation/subscription

   One page for what the workspace is on, what each plan includes, and how it
   is paid for. Laid out against its own width rather than the viewport's: the
   sidebar takes 252px above 900px and nothing below, so a viewport breakpoint
   lands in the wrong place on one side of that line or the other. */
.ip-billing { container: billing / inline-size; }
/* The page shows and hides whole blocks — the plans while the payment form is open, the
   saving tag, the invoices — and several of them set their own display, which
   would otherwise beat the attribute. */
.ip-billing [hidden] { display: none; }

.ip-billing-overview {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 16px;
	flex-wrap: wrap;
	padding: 20px 22px;
}
.ip-billing-overview__plan {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px;
	margin-top: 6px;
	font-size: 26px;
	font-weight: 600;
	letter-spacing: -0.02em;
	line-height: 1.15;
}
.ip-billing-overview__line {
	margin-top: 6px;
	max-width: 68ch;
	font-size: 13px;
	line-height: 1.55;
	color: var(--ip-muted);
	text-wrap: pretty;
}
.ip-billing-overview__actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* Three facts under the plan, divided by rules rather than boxed: they are one
   read-out, not three cards. */
.ip-billing-facts {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	border-top: 1px solid var(--ip-border-soft);
}
.ip-billing-fact {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 3px;
	min-width: 0;
	padding: 14px 22px 16px;
}
.ip-billing-fact + .ip-billing-fact { border-left: 1px solid var(--ip-border-soft); }
.ip-billing-fact__label { font-size: 11px; color: var(--ip-muted); }
.ip-billing-fact__value { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.ip-billing-fact__note { font-size: 12px; color: var(--ip-muted); }
.ip-billing-fact .ip-btn--link { margin-top: 4px; font-size: 12px; }

.ip-billing-overview__foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	flex-wrap: wrap;
	padding: 12px 22px;
	border-top: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	font-size: 12px;
	line-height: 1.5;
	color: var(--ip-muted);
}
.ip-billing-overview__foot .ip-row { gap: 8px; }

.ip-billing-bar {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 12px;
	flex-wrap: wrap;
}
.ip-billing-bar__title { font-size: 15px; font-weight: 600; }
.ip-billing-bar__meta { margin-top: 2px; font-size: 12px; color: var(--ip-muted); }

/* The tiers, side by side. Each card spans two rows of this grid through
   subgrid, so the buying halves and the feature lists line up across cards
   even when one blurb runs a line longer than the other. */
.ip-tiers {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 16px;
}
.ip-tier {
	display: grid;
	grid-row: span 2;
	grid-template-rows: subgrid;
	row-gap: 18px;
	padding: 24px;
	background: var(--ip-surface);
	border: 1.5px solid var(--ip-border);
	border-radius: 14px;
	box-shadow: var(--ip-shadow-sm);
}
.ip-tier.is-featured { border-color: var(--ip-teal); }
.ip-tier__buy { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.ip-tier__head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.ip-tier__name { font-size: 14px; font-weight: 600; }
.ip-tier__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.ip-tier__amount { font-size: 34px; font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; }
.ip-tier__per { font-size: 13px; color: var(--ip-muted); }
.ip-tier__sub { margin-top: 4px; font-size: 12px; color: var(--ip-muted); }
.ip-tier__blurb { font-size: 12.5px; line-height: 1.5; color: var(--ip-muted); text-wrap: pretty; }
/* Pinned to the foot of the buying half, so the two buttons share a line. */
.ip-tier__buy .ip-btn { margin-top: auto; }
.ip-tier__features {
	display: flex;
	flex-direction: column;
	gap: 9px;
	min-width: 0;
	padding-top: 16px;
	border-top: 1px solid var(--ip-border-soft);
}
.ip-tier__feature { display: flex; gap: 9px; font-size: 12.5px; line-height: 1.45; }
.ip-tier__tick { color: var(--ip-teal); font-weight: 700; }

/* The payment form: our fields on the left, what is being bought on the right. */
.ip-billing-pay {
	display: grid;
	grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
	gap: 24px;
	padding: 22px;
}
.ip-billing-pay__fields { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.ip-billing-pay__summary {
	display: flex;
	flex-direction: column;
	gap: 10px;
	align-self: start;
	padding: 16px 18px;
	border: 1px solid var(--ip-border-soft);
	border-radius: 12px;
	background: var(--ip-surface-sunk);
	font-size: 12.5px;
	color: var(--ip-muted);
}
.ip-billing-pay__line { display: flex; justify-content: space-between; gap: 12px; }
.ip-billing-pay__line strong { color: var(--ip-ink); font-weight: 600; }
.ip-billing-pay__total {
	padding-top: 10px;
	border-top: 1px solid var(--ip-border-soft);
	font-size: 14px;
	color: var(--ip-ink);
}
.ip-billing-pay__foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	flex-wrap: wrap;
	padding: 12px 22px;
	border-top: 1px solid var(--ip-border-soft);
	background: var(--ip-surface-sunk);
	font-size: 12px;
	color: var(--ip-muted);
}

.ip-field__error { font-size: 11.5px; color: var(--ip-danger); }

/* A Stripe Element standing in for .ip-input. The frame inside draws the text
   (its colours are handed over from these tokens in subscription.js); the box,
   and its focus and error states, are ours — Stripe sets these classes on the
   div it mounts into. The height matches a text input's line so the name field
   and the card fields sit as one set. */
.ip-input--stripe { min-height: 40px; }
.ip-input--stripe.StripeElement--focus {
	border-color: var(--ip-teal);
	box-shadow: 0 0 0 3px var(--ip-mint);
}
.ip-input--stripe.StripeElement--invalid,
.ip-input.is-invalid { border-color: var(--ip-danger); }

.ip-billing-notes {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
	gap: 20px;
}
.ip-billing-note__title { font-size: 13px; font-weight: 600; }
.ip-billing-note__body { margin-top: 6px; font-size: 12px; line-height: 1.55; color: var(--ip-muted); }
.ip-billing-foot { font-size: 12px; line-height: 1.55; color: var(--ip-faint); }

/* Wide enough for each card to put its features beside the price rather than
   under it, which is what stops two cards across 1600px reading as banners. */
@container billing (min-width: 1180px) {
	.ip-tier {
		grid-row: auto;
		grid-template-rows: none;
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		column-gap: 28px;
	}
	.ip-tier__features {
		padding: 2px 0 0 24px;
		border-top: 0;
		border-left: 1px solid var(--ip-border-soft);
	}
}

@container billing (max-width: 620px) {
	.ip-tiers { grid-template-columns: minmax(0, 1fr); }
	.ip-billing-pay { grid-template-columns: minmax(0, 1fr); padding: 18px; }
	.ip-billing-pay__summary { order: -1; }
	.ip-billing-facts { grid-template-columns: minmax(0, 1fr); }
	.ip-billing-fact + .ip-billing-fact { border-left: 0; border-top: 1px solid var(--ip-border-soft); }
}

/* -----------------------------------------------------------------------------
   Settings

   The frame every /settings page sits in (SettingsLayout.php): the settings
   menu down the left and the page beside it. A menu of its own rather than
   entries in the sidebar, because the sidebar is for the work and setup
   visited once a term only crowded it.
   -------------------------------------------------------------------------- */

.ip-settings {
	display: grid;
	grid-template-columns: 210px minmax(0, 1fr);
	gap: 28px;
	align-items: start;
}

/* Sticky, so on a long page like Plans & billing the way to the next setting
   does not scroll away with the content. */
.ip-settings__nav {
	position: sticky;
	top: 12px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}
.ip-settings__group { display: flex; flex-direction: column; gap: 2px; }
.ip-settings__group-label {
	padding: 0 10px 6px;
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--ip-faint);
}
.ip-settings__link {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 7px 10px;
	border-radius: 8px;
	font-size: 13px;
	color: var(--ip-nav-fg);
	text-decoration: none;
}
.ip-settings__link:hover { background: var(--ip-veil-hover); color: var(--ip-ink); }
.ip-settings__link .bi { font-size: 14px; color: var(--ip-muted); }
.ip-settings__link.is-active { background: var(--ip-mint); color: var(--ip-teal); font-weight: 600; }
.ip-settings__link.is-active .bi { color: var(--ip-teal); }

.ip-settings__body { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.ip-settings__head { display: flex; flex-direction: column; gap: 4px; }
.ip-settings__title { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.ip-settings__summary { font-size: 12.5px; line-height: 1.5; color: var(--ip-muted); text-wrap: pretty; }

/* Below the width where the sidebar folds away, the menu becomes one row of
   tabs above the page: a 210px rail beside a phone-width page leaves the page
   nothing. The group labels go — the tabs read in order without them. */
@media (max-width: 900px) {
	.ip-settings { grid-template-columns: minmax(0, 1fr); gap: 18px; }
	.ip-settings__nav {
		position: static;
		flex-direction: row;
		gap: 2px;
		overflow-x: auto;
		padding-bottom: 6px;
		border-bottom: 1px solid var(--ip-border-soft);
		scrollbar-width: thin;
	}
	.ip-settings__group { flex-direction: row; gap: 2px; }
	.ip-settings__group-label { display: none; }
	.ip-settings__link { white-space: nowrap; }
}

/* -----------------------------------------------------------------------------
   FullCalendar

   The widget exposes its own custom properties, so theming it is a matter of
   pointing those at ours rather than overriding its rules — which means it
   follows the light/dark switch for free.
   -------------------------------------------------------------------------- */

.fc {
	--fc-page-bg-color: transparent;
	--fc-border-color: var(--ip-border-soft);
	--fc-neutral-bg-color: var(--ip-surface-sunk);
	--fc-today-bg-color: var(--ip-mint);
	--fc-button-bg-color: var(--ip-surface);
	--fc-button-border-color: var(--ip-border-input);
	--fc-button-text-color: var(--ip-ink);
	--fc-button-hover-bg-color: var(--ip-rule);
	--fc-button-hover-border-color: var(--ip-border-input);
	--fc-button-active-bg-color: var(--ip-teal);
	--fc-button-active-border-color: var(--ip-teal);
	--fc-list-event-hover-bg-color: var(--ip-surface-sunk);

	font-family: var(--ip-sans);
	color: var(--ip-ink);
}

.fc .fc-toolbar-title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.fc .fc-button {
	border-radius: 8px;
	font-size: 12.5px;
	font-weight: 500;
	padding: 6px 11px;
	box-shadow: none;
	text-transform: capitalize;
}
.fc .fc-button:focus { box-shadow: 0 0 0 3px var(--ip-mint); }
.fc .fc-button-primary:not(:disabled).fc-button-active { color: var(--ip-on-accent); }

.fc .fc-col-header-cell-cushion {
	font-size: 10.5px;
	font-weight: 600;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--ip-faint);
	padding: 8px 4px;
	text-decoration: none;
}
.fc .fc-daygrid-day-number { font-size: 11.5px; color: var(--ip-muted); text-decoration: none; }
.fc .fc-daygrid-day.fc-day-today .fc-daygrid-day-number { color: var(--ip-teal); font-weight: 600; }

.fc .fc-event {
	border-radius: 6px;
	border-width: 0 0 0 2px;
	font-size: 11.5px;
	font-weight: 500;
	padding: 2px 6px;
	cursor: pointer;
}
.fc .fc-daygrid-more-link { font-size: 11px; color: var(--ip-teal); }
.fc .fc-list-empty { background: transparent; color: var(--ip-faint); }
.fc-theme-standard .fc-list-day-cushion { background: var(--ip-surface-sunk); }

/* -----------------------------------------------------------------------------
   Suggestion panels — form-modal's lookup and suggest fields

   Without `position: fixed` here, every lookup dropdown in the app — teams,
   members, places, roles — ignored the coordinates JS had set on it and laid
   out in normal flow at the bottom of <body>, usually below the fold. The
   search was running and the rows existed; nobody could see them.
   -------------------------------------------------------------------------- */

/* The rest of form-modal's own pieces. The dialog around them is still a
   Bootstrap .modal for its behaviour — focus trap, static backdrop, stacking
   over the entry flyout — and drawn through the bridge's .modal-* rules to
   match .ip-modal; everything inside it is design-system markup. */
.fm-field > .ip-field__label,
.fm-field .col-12 > .ip-field__label,
[class*="col-"] > .ip-field__label { display: block; margin-bottom: 6px; }
.fm-field > .ip-field__hint { margin-top: 6px; }
.fm-sublabel { font-weight: 500; color: var(--ip-muted); margin: 8px 0 4px; display: block; }
.fm-static { font-size: 13.5px; color: var(--ip-muted); padding: 10px 0; }
.fm-subform { padding: 14px; }
.fm-subform__title { font-size: 13px; font-weight: 600; }
.fm-checklist { border: 1px solid var(--ip-border-input); border-radius: 9px; padding: 8px; }
.input-group > .ip-input { flex: 1 1 auto; width: 1%; min-width: 0; }

.fm-suggest { position: relative; }

/* Appended to <body> so a modal's overflow cannot clip it, positioned in JS
   against the input's viewport rect. The z-index clears Bootstrap's modal,
   which sits at 1055. */
.fm-suggest-panel {
	position: fixed;
	z-index: 1060;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	border: 1px solid var(--ip-border);
	border-radius: 10px;
	/* Opaque, not translucent: this sits over form fields, and the text behind
	   it would otherwise show through the rows. */
	background: var(--ip-surface);
	box-shadow: var(--ip-shadow-pop);
}

/* JS sets the height in whole rows, so nothing is ever cut mid-row. */
.fm-suggest-list { overflow-y: auto; overscroll-behavior: contain; }

.fm-suggest-option {
	display: flex;
	align-items: baseline;
	gap: 8px;
	width: 100%;
	text-align: left;
	background: transparent;
	color: var(--ip-ink);
	border: 0 solid var(--ip-rule);
	border-bottom-width: 1px;
	padding: 9px 12px;
	font: inherit;
	cursor: pointer;
}
.fm-suggest-list .fm-suggest-option:last-child { border-bottom-width: 0; }

/* The name stays readable in full; the sub-label is what gives way. Both are
   held to one line so every row is the same height — which is what lets the
   panel end on a row boundary. */
.fm-suggest-main { flex: 0 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fm-suggest-sub {
	flex: 1 1 auto;
	min-width: 0;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	font-size: 12px;
	color: var(--ip-muted);
}

/* Keyboard and pointer highlight are the same thing here — `.active` is set by
   the arrow keys, and hover has to match it or the two disagree on screen. */
.fm-suggest-option:hover,
.fm-suggest-option.active { background: var(--ip-surface-sunk); color: var(--ip-ink); }
.fm-suggest-option.active { box-shadow: inset 2px 0 0 var(--ip-teal); }

.fm-suggest-option[disabled],
.fm-suggest-option.is-disabled { opacity: 0.55; cursor: default; }

/* Required credit for the data provider — see places.js. */
.fm-suggest-credit {
	flex: 0 0 auto;
	padding: 6px 12px;
	border-top: 1px solid var(--ip-border-soft);
	font-size: 11px;
	color: var(--ip-faint);
	text-align: right;
}
.fm-suggest-credit a { color: inherit; text-decoration: none; }
.fm-suggest-credit a:hover { text-decoration: underline; }

/* -----------------------------------------------------------------------------
   Placeholders, file tiles, the crop window and the gallery

   Styles the JavaScript depends on — without them, loading placeholders render
   as empty spans, file tiles as unstyled boxes, and the gallery overlay as a
   stack of unpositioned divs.

   The rgba(0,0,0,…) veils are left as they are on purpose — they sit over
   photographs, where a token that follows the theme would stop doing its job
   in light mode.
   -------------------------------------------------------------------------- */

/* || IMAGE CROP (form-modal "crop" field) */
/* The window is square, so the 1:1 ratio is a property of the layout rather than
   something the user can drag out of shape. */
.fm-crop-viewport {
	position:relative;
	width:100%;
	max-width:18rem;
	aspect-ratio:1 / 1;
	margin-inline:auto;
	overflow:hidden;
	border-radius:12px;
	border:1px solid var(--ip-border);
	background-color:var(--ip-surface-sunk);
	cursor:grab;
	touch-action:none;      /* panning is ours, not the browser's */
	user-select:none;
}
.fm-crop-viewport:active {cursor:grabbing;}
.fm-crop-viewport:focus-visible {outline:2px solid var(--ip-teal); outline-offset:2px;}
.fm-crop-viewport img {
	position:absolute;
	max-width:none;         /* undo the global img fluid rule, or panning fights it */
	user-select:none;
	-webkit-user-drag:none;
}

/* || LOADING PLACEHOLDERS */
/* A shimmer stand-in for a value that has not arrived yet. Give it a width so
   the layout does not jump when the real text lands on top of it. */
.skeleton {
	display:inline-block;
	min-width:3rem;
	width:8rem;
	height:1em;
	vertical-align:middle;
	border-radius:6px;
	background-color:var(--ip-skeleton-base);
	background-image:linear-gradient(90deg,
		transparent 0%,
		var(--ip-skeleton-sheen) 50%,
		transparent 100%);
	background-size:200% 100%;
	background-repeat:no-repeat;
	animation:skeleton-shimmer 1.5s ease-in-out infinite;
}
/* Handing a frame over to the picture that was loading into it.
 *
 * The shimmer fades out while the image fades in, so the two cross rather than
 * the shimmer being switched off and the image starting from nothing. All three
 * of these used `display:none` and got a visible flick of empty frame between
 * the two, because `display` is not a transitionable property and the swap
 * therefore happened in one frame however long the image's own fade was.
 *
 * `visibility` is what finally takes the placeholder out of play, delayed by
 * exactly the fade so it lands on the last frame of it. That delay is the whole
 * trick and it is why this is not simply `opacity: 0`: these three all warn, in
 * their own comments, that the shimmer must end up *gone* rather than covered,
 * since a PNG with transparency would otherwise let it carry on showing through
 * the picture. A placeholder left at zero opacity is still painted, still
 * composited, and still running its shimmer keyframes on every loaded thumbnail
 * on the screen. */
.gal-thumb .skeleton,
.fm-tile .skeleton,
.ip-file-thumb .skeleton {
	transition:opacity 0.2s ease, visibility 0s linear 0.2s;
}
.gal-thumb-ready .skeleton,
.fm-tile-ready .skeleton,
.ip-file-thumb.is-ready .skeleton,
.ip-file-thumb.is-failed .skeleton {
	opacity:0;
	visibility:hidden;
}

.skeleton-block {display:block;}
.skeleton-circle {border-radius:50%; width:1em;}
/* Inside a table cell the width is a percentage of the column, so the floor has
   to be low enough not to blow out narrow columns. */
.skeleton-cell {min-width:1.25rem;}
/* Fills an image frame — logo box, avatar, thumbnail — so the placeholder is the
   shape of the thing that is arriving. */
.skeleton-image {width:100%; height:100%; min-width:0; border-radius:inherit;}
/* Placeholder rows should not react to the row hover/pointer styling that real
   rows carry — there is nothing to click yet. */
.skeleton-row td {cursor:default;}

/* Shaped placeholders — loading.js draws them, page-skeletons.js lays whole
 * pages out of them. Each borrows the metrics of the component it stands in
 * for (a row is an .ip-table__row's padding, a tile is an .ip-kpi), so the
 * real thing lands on top of it without the layout moving. Colour comes only
 * from .skeleton, which already answers to both themes. */
.sk-soft   { opacity: 0.6; }
.sk-gap    { margin-top: 8px; }
.sk-gap-lg { margin-top: 22px; }
.sk-grow   { flex: 1 1 auto; }
.sk-stack  { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.sk-line   { display: flex; align-items: center; gap: 10px; min-width: 0; }
.sk-between { justify-content: space-between; }
.sk-lines  { gap: 9px; }
.sk-list   { gap: 14px; }
.sk-kv     { gap: 11px; }
.sk-detail { gap: 20px; }
.sk-doc    { gap: 10px; }

.sk-table__head,
.sk-table__row {
	display: grid;
	gap: 12px;
	align-items: center;
	padding: 12px 18px;
}
.sk-table__head {
	padding-top: 13px;
	padding-bottom: 13px;
	background: var(--ip-surface-sunk);
	border-bottom: 1px solid var(--ip-border-soft);
}
.sk-table__row { min-height: 56px; border-bottom: 1px solid var(--ip-rule); }
.sk-table__row:last-child { border-bottom: 0; }

.sk-card { display: flex; flex-direction: column; padding: 16px; }
.skeleton.sk-tile__frame { width: 100%; height: auto; aspect-ratio: 4 / 3; border-radius: 0; }
.sk-tile__caption { padding: 12px 14px 14px; }

/* No 3rem floor: a column is narrower than that on a card of this width, and
   the bars would overflow into each other. */
.skeleton.sk-bar { width: 100%; min-width: 0; min-height: 4px; border-radius: 5px 5px 2px 2px; }
.skeleton.sk-chip   { display: inline-block; height: 26px; border-radius: 999px; }
.skeleton.sk-pill   { border-radius: 999px; }
.skeleton.sk-seg    { display: inline-block; height: 30px; border-radius: 9px; }
.skeleton.sk-input  { display: block; flex: 1 1 auto; height: 34px; border-radius: 8px; }
.skeleton.sk-button { display: inline-block; height: 32px; border-radius: 8px; }

.sk-heat {
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	gap: 6px;
	margin-top: 16px;
}
.sk-heat .skeleton { display: block; width: auto; height: 44px; border-radius: 6px; }

.sk-cal__grid {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	margin-top: 16px;
	border-top: 1px solid var(--ip-border-soft);
	border-left: 1px solid var(--ip-border-soft);
}
.sk-cal__cell {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 6px;
	min-height: 92px;
	padding: 6px;
	border-right: 1px solid var(--ip-border-soft);
	border-bottom: 1px solid var(--ip-border-soft);
}

.sk-podium {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
	gap: 14px;
}
.sk-steps {
	display: flex;
	gap: 22px;
	padding: 16px 24px;
	overflow: hidden;
	border-bottom: 1px solid var(--ip-border-soft);
}
.sk-fields {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
	gap: 16px;
}
.sk-settings-link { padding: 7px 10px; }

@keyframes skeleton-shimmer {
	0%   {background-position:-100% 0;}
	100% {background-position: 200% 0;}
}

/* A travelling gradient is exactly the kind of thing motion sensitivity reacts
   to — keep the placeholder, drop the movement. Bootstrap already slows its own
   spinners under this query. */
@media (prefers-reduced-motion: reduce) {
	.skeleton { animation: none; background-image: none; }
}

.fm-tiles {display:grid; gap:8px;}
.fm-tile {
	position:relative;
	aspect-ratio:1 / 1;
	overflow:hidden;
	border-radius:10px;
	border:1px solid var(--ip-border);
	background-color:var(--ip-surface-sunk);
}
.fm-tile img {
	position:absolute;
	inset:0;
	width:100%;
	height:100%;
	object-fit:cover;      /* square tiles, whatever shape the photo is */
	display:block;
	/* Held back until the bytes are there, so the skeleton underneath is what shows
	   rather than a half-painted picture. */
	opacity:0;
	transition:opacity 0.2s ease;
	user-select:none;
	-webkit-user-drag:none;
}
.fm-tile-ready img {opacity:1;}
/* The skeleton's own fade-out, and why it ends up gone rather than merely
   transparent, is with the `.skeleton` rules above — one place for all three
   frames that use it. */
/* The whole tile is the button. It sits over the picture rather than around it so
   the focus ring traces the tile's own corners. */
.fm-tile-open {
	position:absolute;
	inset:0;
	display:flex;
	flex-direction:column;
	align-items:center;
	justify-content:center;
	gap:0.15rem;
	padding:0;
	border:0;
	border-radius:inherit;
	background-color:transparent;
	color:#fff;
	cursor:pointer;
}
.fm-tile-open:disabled {cursor:default;}
.fm-tile-open:focus-visible {outline:2px solid var(--ip-teal); outline-offset:-2px;}
.fm-tile-open:hover:not(:disabled) {background-color:rgba(0,0,0,0.25);}
/* A document in a picture field: nothing to show but what kind of thing it is. */
.fm-tile-doc {
	display:flex;
	align-items:center;
	justify-content:center;
	height:100%;
	font-size:1.75rem;
	color:var(--ip-muted);
}
/* Corner controls sit above the tile's own button, or the click that removes a
   picture would open it instead. */
.fm-tile-toggle {
	position:absolute;
	top:4px;
	right:4px;
	z-index:2;
	display:flex;
	align-items:center;
	justify-content:center;
	width:22px;
	height:22px;
	padding:0;
	border:0;
	border-radius:50%;
	background-color:rgba(0,0,0,0.6);
	color:#fff;
	font-size:0.8rem;
	line-height:1;
	opacity:0;
	transition:opacity 0.12s ease, background-color 0.12s ease;
}
/* Hidden until wanted on a pointer, but a touch screen has no hover to reveal it
   with and a keyboard has to be able to reach it. */
.fm-tile:hover .fm-tile-toggle,
.fm-tile-toggle:focus-visible,
.fm-tile-removed .fm-tile-toggle {opacity:1;}
@media (hover: none) {
	.fm-tile-toggle {opacity:1;}
}
.fm-tile-toggle:hover {background-color:var(--ip-danger);}
.fm-tile-removed .fm-tile-toggle:hover {background-color:var(--ip-blue);}
.fm-tile-toggle:focus-visible {outline:2px solid #fff; outline-offset:1px;}
/* Struck off, but still there to be put back — the list presentation says the same
   thing with a line through the name. Specific enough to win against the rule that
   reveals a loaded picture, which would otherwise put it back to full strength. */
.fm-tile-ready.fm-tile-removed img {opacity:0.25; filter:grayscale(1);}
.fm-tile-removed {border-style:dashed;}
.fm-tile-flag {
	position:absolute;
	bottom:4px;
	left:4px;
	padding:0.05rem 0.3rem;
	border-radius:5px;
	background-color:var(--ip-teal);
	color:#fff;
	font-size:0.65rem;
	line-height:1.4;
}
/* The overflow tile. The picture behind it is dimmed hard enough that the count
   reads cleanly over anything, however bright the photo. */
.fm-tile-ready.fm-tile-more img {opacity:0.45; filter:brightness(0.5);}
.fm-tile-more .fm-tile-open {background-color:rgba(0,0,0,0.35);}
.fm-tile-more .fm-tile-open:hover {background-color:rgba(0,0,0,0.15);}
.fm-tile-more-count {font-size:0.8rem; font-weight:600;}
.fm-tile-more-icon {font-size:1.1rem;}

/* Upload progress, drawn over each picture as it goes up. Over, not instead of: a
   bar that cannot say which photo it belongs to is not worth the room it takes, and
   the whole point of showing this per file is being able to see which is which. */
.fm-tile-progress {
	position:absolute;
	inset:0;
	z-index:3;
	display:flex;
	align-items:center;
	justify-content:center;
	background-color:rgba(0,0,0,0.55);
	/* Invisible to the pointer, so the tile's own button underneath still works —
	   looking at a photo while it uploads costs nothing. */
	pointer-events:none;
	transition:background-color 0.2s ease;
}
/* Stored: out of the way, since the picture is the news now and not the ring. */
.fm-tile-progress[data-fm-state="done"] {background-color:rgba(0,0,0,0.15);}
/* A file already on its way cannot be called back, so the button that would try
   to goes away for as long as it is going. */
.fm-tile-uploading .fm-tile-toggle {display:none;}

/* Registered so it can be transitioned: an unregistered custom property is just a
   token to the engine, and stepping between two conic gradients is not something it
   would know how to do. Progress events arrive in whatever bursts the network gives
   them, and this is what turns those into something that moves evenly. */
@property --fm-progress {
	syntax:'<number>';
	inherits:false;
	initial-value:0;
}
/* The ring: a full disc of two colours with the middle covered over. */
.fm-tile-ring {
	--fm-progress:0;
	display:flex;
	align-items:center;
	justify-content:center;
	width:46px;
	height:46px;
	border-radius:50%;
	background:conic-gradient(
		var(--ip-teal) calc(var(--fm-progress) * 1%),
		rgba(255,255,255,0.25) 0
	);
	transition:--fm-progress 0.2s linear, background-color 0.2s ease;
}
.fm-tile-mark {
	display:flex;
	align-items:center;
	justify-content:center;
	width:36px;
	height:36px;
	border-radius:50%;
	background-color:rgba(0,0,0,0.82);
	color:#fff;
	font-size:0.7rem;
	font-weight:600;
	line-height:1;
	/* Digits of one width, or the number jitters as it counts up. */
	font-variant-numeric:tabular-nums;
}
.fm-tile-mark .bi {font-size:1rem;}
.fm-tile-mark .spinner-border {width:1rem; height:1rem; border-width:2px;}
/* Finished, one way or the other: the ring is whole, and its colour carries which. */
.fm-tile-progress[data-fm-state="done"] .fm-tile-ring {background:var(--ip-teal);}
.fm-tile-progress[data-fm-state="done"] .fm-tile-mark {color:var(--ip-teal);}
.fm-tile-progress[data-fm-state="failed"] .fm-tile-ring {background:var(--ip-danger);}
.fm-tile-progress[data-fm-state="failed"] .fm-tile-mark {color:var(--ip-danger);}
.fm-tile-progress[data-fm-state="queued"] .fm-tile-mark {color:rgba(255,255,255,0.55);}

/* The overflow tile gets a bar along the bottom instead of a ring: the count it
   already carries sits exactly where the ring would go, and the count is the more
   useful of the two. It covers every file behind the tile at once. */
.fm-tile-bar {
	position:absolute;
	right:8px;
	bottom:8px;
	left:8px;
	z-index:3;
	height:4px;
	overflow:hidden;
	border-radius:2px;
	background-color:rgba(255,255,255,0.25);
	pointer-events:none;
}

/* The same bar for the list presentation, where documents are named rather than
   shown and there is no tile to draw over. */
.fm-file-bar {
	display:inline-block;
	width:5rem;
	height:4px;
	overflow:hidden;
	border-radius:2px;
	background-color:rgba(255,255,255,0.2);
	vertical-align:middle;
}
.fm-tile-bar > span,
.fm-file-bar > span {
	display:block;
	width:0;
	height:100%;
	background-color:var(--ip-teal);
	transition:width 0.2s linear;
}
.fm-tile-bar[data-fm-state="done"] > span,
.fm-file-bar[data-fm-state="done"] > span {background-color:var(--ip-teal);}
.fm-tile-bar[data-fm-state="failed"] > span,
.fm-file-bar[data-fm-state="failed"] > span {background-color:var(--ip-danger);}

/* || IMAGE GALLERY (gallery.js) */
/* Fixed to the viewport, and mounted inside the open dialog so Bootstrap counts it
   as part of the modal for focus purposes — see openGallery. .modal-dialog carries
   a transform and would capture `fixed`; the .modal element does not. */
.gal-overlay {
	position:fixed;
	inset:0;
	z-index:1065;
	display:flex;
	flex-direction:column;
	background-color:rgba(0,0,0,0.92);
	opacity:0;
	transition:opacity 0.26s ease;
	overscroll-behavior:contain;
	touch-action:pan-y;
}
.gal-overlay.gal-open {opacity:1;}
.gal-overlay:focus {outline:none;}
.gal-bar {
	flex:0 0 auto;
	display:flex;
	align-items:center;
	gap:0.75rem;
	padding:0.75rem 1rem;
	color:rgba(255,255,255,0.85);
	font-size:0.875rem;
}
.gal-count {flex:0 0 auto; font-variant-numeric:tabular-nums;}
/* The name gives way rather than the counter — one is a fixed few characters, the
   other is a filename of any length at all. */
.gal-name {
	flex:1 1 auto;
	min-width:0;
	white-space:nowrap;
	overflow:hidden;
	text-overflow:ellipsis;
	color:rgba(255,255,255,0.55);
}
.gal-icon-btn {
	display:flex;
	align-items:center;
	justify-content:center;
	padding:0;
	border:0;
	border-radius:50%;
	background-color:rgba(255,255,255,0.08);
	color:#fff;
	transition:background-color 0.12s ease;
}
.gal-icon-btn:hover {background-color:rgba(255,255,255,0.2);}
.gal-icon-btn:focus-visible {outline:2px solid #fff; outline-offset:2px;}
.gal-close, .gal-remove {flex:0 0 auto; width:34px; height:34px;}
.gal-remove:hover {background-color:var(--ip-danger);}
.gal-stage {
	flex:1 1 auto;
	display:flex;
	align-items:center;
	gap:0.5rem;
	padding:0 0.5rem;
	min-height:0;               /* or the frame refuses to shrink and the strip is pushed off */
}
.gal-nav {width:44px; height:44px; flex:0 0 auto; font-size:1.1rem;}
.gal-frame {
	position:relative;
	flex:1 1 auto;
	height:100%;
	min-width:0;
	display:flex;
	align-items:center;
	justify-content:center;
}
/* `inset` with auto margins rather than leaning on the flex container to place it —
   the static position of an absolute child of a flexbox is not somewhere to be
   clever. */
.gal-layer {
	position:absolute;
	inset:0;
	margin:auto;
	max-width:100%;
	max-height:100%;
	object-fit:contain;
	border-radius:6px;
	opacity:0;
	transition:opacity 0.26s ease, transform 0.26s ease;
	user-select:none;
	-webkit-user-drag:none;
}
/* Enters offset towards the side it came from, then settles into place. Compounded
   with .gal-layer so the settled state cannot lose to the base rule on source order. */
.gal-layer.gal-from-right {transform:translateX(24px);}
.gal-layer.gal-from-left {transform:translateX(-24px);}
.gal-layer.gal-settled {opacity:1; transform:translateX(0);}
.gal-spinner {
	position:absolute;
	width:2rem;
	height:2rem;
	opacity:0;
	pointer-events:none;
	transition:opacity 0.15s ease;
}
.gal-spinner-on {opacity:0.7;}
/* Struck off, the same treatment its tile gets — and compounded with .gal-settled,
   which would otherwise put it back to full strength on source order alone. */
.gal-frame-removed .gal-layer.gal-settled {opacity:0.3; filter:grayscale(1);}
/* Says what the dimming means. "Removed" while the picture is plainly still on
   screen needs the explaining. */
.gal-flag {
	position:absolute;
	bottom:0.5rem;
	padding:0.15rem 0.6rem;
	border-radius:999px;
	background-color:var(--ip-danger);
	color:#fff;
	font-size:0.75rem;
	line-height:1.5;
	opacity:0;
	transition:opacity 0.15s ease;
	pointer-events:none;
}
.gal-frame-removed .gal-flag {opacity:1;}
.gal-strip {
	flex:0 0 auto;
	display:flex;
	gap:6px;
	padding:0.75rem 1rem;
	overflow-x:auto;
	overscroll-behavior-x:contain;
	scrollbar-width:thin;
}
.gal-thumb {
	position:relative;           /* the picture sits over the skeleton, not after it */
	flex:0 0 auto;
	width:56px;
	height:56px;
	padding:0;
	overflow:hidden;
	border:2px solid transparent;
	border-radius:8px;
	background-color:rgba(255,255,255,0.06);
	opacity:0.5;
	transition:opacity 0.12s ease, border-color 0.12s ease;
}
.gal-thumb img {
	position:absolute;
	inset:0;
	width:100%;
	height:100%;
	object-fit:cover;
	display:block;
	/* Held back until the bytes are there, so the shimmer underneath is what shows
	   rather than a half-painted thumbnail. */
	opacity:0;
	transition:opacity 0.2s ease;
}
/* Block, or the inline-block placeholder sits on a baseline and leaves the
   thumbnail's own background showing as a strip beneath it. */
.gal-thumb .skeleton {display:block;}
.gal-thumb-ready img {opacity:1;}
.gal-thumb:hover {opacity:0.85;}
.gal-thumb-on {opacity:1; border-color:var(--ip-teal);}
/* Marked for removal, so the strip shows which without having to walk them. */
.gal-thumb-off img {filter:grayscale(1); opacity:0.4;}
.gal-thumb-off {border-color:var(--ip-danger);}
.gal-thumb:focus-visible {outline:2px solid #fff; outline-offset:2px;}
/* Narrow screens: the side arrows cost width the picture needs more, and a swipe
   does the same job. */
@media (max-width: 575.98px) {
	.gal-nav {display:none;}
	.gal-thumb {width:44px; height:44px;}
}
/* … */

/* A clickable image frame — the logo box on the organisation page. */
.image-drop {cursor:pointer; transition:border-color 0.15s ease, background-color 0.15s ease;}
.image-drop:hover, .image-drop:focus-visible {border-color:var(--ip-teal) !important;}
.image-drop:focus-visible {outline:2px solid var(--ip-teal); outline-offset:2px;}

/* Centred spinner for a panel or table body that is still filling. */
.loading-pane {
	display:flex;
	align-items:center;
	justify-content:center;
	gap:0.6rem;
	padding:1.75rem 1rem;
	color:var(--ip-muted);
}
/* … */

/* -----------------------------------------------------------------------------
   Bootstrap bridge

   The app ran on `data-bs-theme="dark"` before this redesign, and a lot of
   markup was written against it — `btn-outline-light` is white-on-transparent,
   which was right on a dark panel and is invisible on a white one.

   So rather than rewrite every call site at once, the Bootstrap classes that
   assumed a dark background were mapped onto this palette here. This is a
   bridge, not a design, and it shrinks as screens are rebuilt: the sign-in
   pages, the dialog engine's contents and the activity list have been, and
   the rules for cards, tables, form controls, text-bg-* badges and the light,
   secondary, success and danger buttons went with them. What is left is what
   something still uses — the dialogs' Bootstrap .modal shell, the sidebar's
   Log activity button, badges, toasts. Before
   adding a rule here, build the thing from ip-* instead.
   -------------------------------------------------------------------------- */

/* Bootstrap's own names, pointed at ours. Declared once rather than per theme:
   each --ip-* on the right resolves against whichever theme is in force on
   <html>, so one mapping covers both. Bootstrap's [data-bs-theme="dark"]
   carries the same specificity as :root, and this file loads after it, so
   these win in either theme. */
:root {
	--bs-primary:              var(--ip-teal);
	--bs-primary-rgb:          var(--ip-teal-rgb);
	--bs-link-color:           var(--ip-teal);
	--bs-link-color-rgb:       var(--ip-teal-rgb);
	--bs-link-hover-color:     var(--ip-teal-dark);
	--bs-link-hover-color-rgb: var(--ip-teal-rgb);

	--bs-body-bg:         var(--ip-canvas);
	--bs-body-color:      var(--ip-ink);
	--bs-emphasis-color:  var(--ip-ink);
	--bs-secondary-color: var(--ip-muted);
	--bs-secondary-bg:    var(--ip-surface-sunk);
	--bs-tertiary-bg:     var(--ip-surface-sunk);
	--bs-border-color:    var(--ip-border);

	--bs-border-radius:     8px;
	--bs-border-radius-sm:  8px;
	--bs-border-radius-lg:  8px;
	--bs-border-radius-xl:  8px;
	--bs-border-radius-xxl: 8px;
	--bs-dropdown-item-border-radius: 5px;
}

/* Component variables have to be set *on the component*. Bootstrap declares
   --bs-card-bg inside the .card rule itself, not on :root, so a root-level
   override never reaches it. The same is true of every block below. */
.modal {
	--bs-modal-bg:                  var(--ip-surface);
	--bs-modal-border-color:        var(--ip-border);
	--bs-modal-header-border-color: var(--ip-border-soft);
	--bs-modal-footer-border-color: var(--ip-border-soft);
}
.dropdown-menu {
	--bs-dropdown-bg:                var(--ip-surface);
	--bs-dropdown-border-color:      var(--ip-border);
	--bs-dropdown-link-color:        var(--ip-ink);
	--bs-dropdown-link-hover-bg:     var(--ip-veil-hover);
	--bs-dropdown-link-hover-color:  var(--ip-ink);
	--bs-dropdown-link-active-bg:    var(--ip-teal);
	--bs-dropdown-link-active-color: var(--ip-on-accent);
	--bs-dropdown-header-color:      var(--ip-muted);
}
.offcanvas, .offcanvas-lg, .offcanvas-md, .offcanvas-xl {
	--bs-offcanvas-bg:           var(--ip-surface);
	--bs-offcanvas-border-color: var(--ip-border);
	--bs-offcanvas-color:        var(--ip-nav-fg);
}
.toast {
	--bs-toast-bg:           var(--ip-surface);
	--bs-toast-border-color: var(--ip-border);
	--bs-toast-header-bg:    var(--ip-surface-sunk);
	--bs-toast-color:        var(--ip-ink);
}

/* Dialogs */
.modal-content {
	border: 1px solid var(--ip-border);
	border-radius: 16px;
	box-shadow: 0 24px 60px var(--ip-shadow-4);
	background: var(--ip-surface);
	color: var(--ip-ink);
}
.modal-header, .modal-footer { border-color: var(--ip-border-soft); }
.modal-footer { background: var(--ip-surface-sunk); }
.modal-title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.modal-backdrop.show { opacity: 0.42; background: var(--ip-scrim); }
/* A dialog body scrolls inside the viewport rather than pushing its own
   footer — and the Save button — off the bottom of the screen. */
.modal-body { overflow-y: auto; max-height: 70vh; }
.modal.fade .modal-dialog { transform: scale(0.95); transition: transform 0.25s ease, opacity 0.15s ease; }
.modal.fade.show .modal-dialog { transform: scale(1); }
.modal.modal-static .modal-dialog { transform: scale(1.02) !important; }

/* A white close glyph on a white header is the same bug as the button below. */
.btn-close-white {
	filter: none;
	--bs-btn-close-color: var(--ip-ink);
}

/* Buttons */
.btn {
	border-radius: 9px;
	font-size: 13px;
	font-weight: 500;
	padding: 9px 15px;
}
.btn-sm { padding: 6px 11px; font-size: 12px; border-radius: 8px; }

/* Bootstrap writes #0d6efd literally into each button's own variable block
   rather than reading --bs-primary, and its :active state reads those
   variables at a higher specificity than the rules below — so the variables
   are pointed at the palette as well as the properties. */
.btn-primary {
	--bs-btn-active-color:         var(--ip-on-accent);
	--bs-btn-active-bg:            var(--ip-teal-dark);
	--bs-btn-active-border-color:  var(--ip-teal-dark);
	--bs-btn-disabled-color:       var(--ip-on-accent);
	--bs-btn-disabled-bg:          var(--ip-teal);
	--bs-btn-disabled-border-color:var(--ip-teal);
	--bs-btn-focus-shadow-rgb:     var(--ip-teal-rgb);
}

.btn-primary {
	background: var(--ip-teal);
	border-color: var(--ip-teal);
	color: var(--ip-on-accent);
	font-weight: 600;
}
.btn-primary:hover, .btn-primary:focus {
	background: var(--ip-teal-dark);
	border-color: var(--ip-teal-dark);
	color: var(--ip-on-accent);
}

.btn-link { color: var(--ip-teal); }

.nav-pills {
	--bs-nav-pills-link-active-color: var(--ip-on-accent);
	--bs-nav-pills-link-active-bg:    var(--ip-teal);
}
/* The dialog engine's section tabs and the sidebar's nav both build their
   buttons once and toggle `.active` on them, so the change has something to
   transition from. Bootstrap ships none of its own. */
.nav-pills .nav-link { transition: background-color 200ms ease, color 200ms ease; }
@media (prefers-reduced-motion: reduce) {
	.nav-pills .nav-link { transition: none; }
}

/* Form controls */
.input-group-text {
	background: var(--ip-bg);
	border-color: var(--ip-border-input);
	color: var(--ip-muted);
	border-radius: 9px;
}
.form-check-input:checked { background-color: var(--ip-teal); border-color: var(--ip-teal); }
.form-check-input:focus { border-color: var(--ip-teal); box-shadow: 0 0 0 3px var(--ip-mint); }

/* Badges — map Bootstrap's palette onto the tag tones.
   The base rule matters as much as the variants: a `.badge` with no
   `text-bg-*` inherits Bootstrap's white text, which is how the lookup field's
   chips ended up as an invisible label beside a floating remove button. */
.badge {
	font-weight: 500;
	border-radius: 999px;
	padding: 3px 9px;
	font-size: 11px;
	background: var(--ip-rule);
	color: var(--ip-muted);
}
.badge .btn-close { --bs-btn-close-color: currentColor; opacity: 0.6; }
.badge .btn-close:hover { opacity: 1; }

.text-muted { color: var(--ip-muted) !important; }

/* -----------------------------------------------------------------------------
   Print — the report builder's whole point is a document somebody prints
   -------------------------------------------------------------------------- */

@media print {
	/* Paper has one theme. Someone reading the app in dark mode still expects a
	   printed report on white — and a page of near-black ink is a cartridge
	   nobody agreed to spend, so the light palette is forced back on. */
	:root, :root[data-theme="dark"] {
		--ip-bg:           #ffffff;
		--ip-canvas:       #ffffff;
		--ip-surface:      #ffffff;
		--ip-surface-sunk: #ffffff;
		--ip-input-bg:     #ffffff;
		--ip-ink:          #16191b;
		--ip-ink-soft:     #2c3134;
		--ip-label:        #4b5563;
		--ip-muted:        #6b7280;
		--ip-faint:        #9aa1a0;
		--ip-border:       #e6e8e7;
		--ip-border-soft:  #eef0ef;
		--ip-rule:         #f2f4f3;
		--ip-teal:         #0f766e;
		--ip-teal-pale:    #8fbfb9;
		--ip-track:        #f0f2f1;
		--ip-on-accent:    #ffffff;
		color-scheme: light;
	}

	#sidebar,
	#nav-progress,
	#toast-container,
	.app-topbar,
	.ip-report-side,
	.ip-card__head { display: none !important; }

	/* The grid and the glows are dark-theme texture; on paper they are toner. */
	body { background-image: none !important; }
	body::before, body::after { display: none !important; }

	#main-content,
	#app-main { overflow: visible !important; height: auto !important; }
	body { overflow: visible; }

	.ip-report-main { flex: 1 1 100%; max-width: none; }
	.ip-card, .ip-card--lg { border: 0; box-shadow: none; }
	.ip-doc { max-width: none; padding: 0; }
	body { background: #ffffff; }
}
